From 7eec19d3dfaa36fd998fa2ab2c1043b493ce809a Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:18:48 +0800 Subject: [PATCH] chore(teams): CLI move flag, docs, dead code, terminology - multica issue update --team exposes move-to-team on the CLI - working-on-issues SKILL.md + source-map document move semantics, alias fallback, and creation-time-only team binding - drop TeamPicker's unused allowedTeamIds and the uncalled ProjectHasTeam query (strong-association leftovers), regenerate sqlc - zh issues.json team property uses Team; conventions glossary gains the Team entry (kept in English, rationale included) Co-Authored-By: Claude Fable 5 --- .../docs/developers/conventions.zh.mdx | 5 +++++ packages/views/locales/zh-Hans/issues.json | 4 ++-- .../views/teams/components/team-picker.tsx | 17 +++++--------- server/cmd/multica/cmd_issue.go | 12 ++++++++++ .../multica-working-on-issues/SKILL.md | 11 ++++++++++ .../working-on-issues-source-map.md | 10 +++++++++ server/pkg/db/generated/issue.sql.go | 3 ++- server/pkg/db/generated/project.sql.go | 22 ------------------- server/pkg/db/queries/project.sql | 8 ------- 9 files changed, 47 insertions(+), 45 deletions(-) diff --git a/apps/docs/content/docs/developers/conventions.zh.mdx b/apps/docs/content/docs/developers/conventions.zh.mdx index b3a036e5b7..a70ad04b25 100644 --- a/apps/docs/content/docs/developers/conventions.zh.mdx +++ b/apps/docs/content/docs/developers/conventions.zh.mdx @@ -111,6 +111,10 @@ Multica 的产品名词分两类: - `task` ↔ `执行任务`(上下文清楚后可简写为「任务」) - `issue` 没有公认中文译法 —— 保留英文;标题可大写为 `Issue` - `skill` 没有公认中文译法 —— 保留英文;标题可大写为 `Skills` +- `team` 保留英文 `Team` —— 它是 issue 编号命名空间的所有者,key(如 `MUL`)直接构成 + issue 标识符(`MUL-42`),英文形态与 key 的绑定更自然;译成「团队」会与泛指人群的 + 日常用语混淆("和团队一起协作"里的团队不是这个实体)。UI/文档一律写 `Team`, + 泛指人群时才用「团队」 **为什么 `issue` / `skill` / `task` 不强制译,而 `project` / `autopilot` 必译**: @@ -124,6 +128,7 @@ Multica 的产品名词分两类: | 英 | 中 | | --- | --- | | Workspace | **工作区** | +| Team | **Team**(保留英文,见上) | | Agent | **智能体** | | Project | **项目** | | Autopilot | **自动化** | diff --git a/packages/views/locales/zh-Hans/issues.json b/packages/views/locales/zh-Hans/issues.json index d543f6edc7..d34446bb75 100644 --- a/packages/views/locales/zh-Hans/issues.json +++ b/packages/views/locales/zh-Hans/issues.json @@ -198,7 +198,7 @@ "pull_request_card_files_count_other": "{{count}} 个文件", "pull_request_card_show_more": "展开剩余 {{count}} 个", "pull_request_card_show_less": "收起", - "prop_team": "团队", + "prop_team": "Team", "prop_status": "状态", "prop_priority": "优先级", "prop_stage": "阶段", @@ -397,7 +397,7 @@ "updated_ago": "更新于 {{time}}" }, "actions": { - "team": "团队", + "team": "Team", "status": "状态", "priority": "优先级", "assignee": "负责人", diff --git a/packages/views/teams/components/team-picker.tsx b/packages/views/teams/components/team-picker.tsx index 9e86d259d0..96ba424d7d 100644 --- a/packages/views/teams/components/team-picker.tsx +++ b/packages/views/teams/components/team-picker.tsx @@ -36,29 +36,22 @@ export function TeamPicker({ onChange, triggerRender, align = "start", - allowedTeamIds, disabled = false, }: { teamId: string | null; onChange: (teamId: string) => void; triggerRender?: ReactElement; align?: "start" | "center" | "end"; - // When set, restricts the offered teams to this id set (e.g. the selected - // project's teams). Undefined means no constraint. - allowedTeamIds?: string[]; // Locked display (e.g. sub-issues inherit the parent's team server-side). disabled?: boolean; }) { const { t } = useT("teams"); const wsId = useWorkspaceId(); - const { data: allTeams = [] } = useQuery(activeTeamListOptions(wsId)); - const teams = allowedTeamIds - ? allTeams.filter((team) => allowedTeamIds.includes(team.id)) - : allTeams; - // Resolve the display label against the unfiltered list so a selection - // outside `allowedTeamIds` (e.g. before the caller converges it) still - // renders instead of flashing the placeholder. - const current = allTeams.find((team) => team.id === teamId); + // Always the full active list: team is a creation-time default, never + // constrained by the selected project (the old allowedTeamIds filter was + // a strong-association-model leftover). + const { data: teams = [] } = useQuery(activeTeamListOptions(wsId)); + const current = teams.find((team) => team.id === teamId); return ( diff --git a/server/cmd/multica/cmd_issue.go b/server/cmd/multica/cmd_issue.go index 0a2986437e..f8ed38503e 100644 --- a/server/cmd/multica/cmd_issue.go +++ b/server/cmd/multica/cmd_issue.go @@ -373,6 +373,7 @@ func init() { issueUpdateCmd.Flags().String("start-date", "", "New start date (calendar day, YYYY-MM-DD; pass empty string to clear)") issueUpdateCmd.Flags().String("due-date", "", "New due date (calendar day, YYYY-MM-DD)") issueUpdateCmd.Flags().String("parent", "", "Parent issue ID (use --parent \"\" to clear)") + issueUpdateCmd.Flags().String("team", "", "Move the issue to this Team (UUID or key); it is renumbered and the old identifier keeps resolving") issueUpdateCmd.Flags().Int("stage", 0, "Stage ordinal (>=1) for this sub-issue; see `issue create --stage`") issueUpdateCmd.Flags().String("output", "json", "Output format: table or json") @@ -1084,6 +1085,17 @@ func runIssueUpdate(cmd *cobra.Command, args []string) error { if priorityChanged { body["priority"] = priorityFlag } + if cmd.Flags().Changed("team") { + v, _ := cmd.Flags().GetString("team") + if v == "" { + return fmt.Errorf("--team requires a Team UUID or key") + } + teamID, err := resolveTeamRef(ctx, client, v) + if err != nil { + return err + } + body["team_id"] = teamID + } if cmd.Flags().Changed("project") { v, _ := cmd.Flags().GetString("project") if v == "" { diff --git a/server/internal/service/builtin_skills/multica-working-on-issues/SKILL.md b/server/internal/service/builtin_skills/multica-working-on-issues/SKILL.md index 7a9e46e2b9..8d8c7d56db 100644 --- a/server/internal/service/builtin_skills/multica-working-on-issues/SKILL.md +++ b/server/internal/service/builtin_skills/multica-working-on-issues/SKILL.md @@ -174,6 +174,17 @@ one explicitly (the team owns the `TEAM_KEY-NUMBER` identifier namespace), or om it to fall back to the workspace's default team, whose key is the legacy workspace prefix. `multica issue list --team ` filters a listing to one team. +Team binds at creation time only, and it can change later: + +- `multica issue update --team ` moves the issue to another + team. It is renumbered under the target team's key; the old identifier is + recorded as an alias and keeps resolving (CLI lookups, API, GitHub branch/PR + linking), so existing references never break. +- Parent and child issues may live in different teams. A child only inherits + its parent's team as a creation-time default when you don't pass `--team`. +- A project's team set is likewise a creation-time default, not a constraint — + an issue in a project may belong to any team. + Parallel children — all start now: ```bash diff --git a/server/internal/service/builtin_skills/multica-working-on-issues/references/working-on-issues-source-map.md b/server/internal/service/builtin_skills/multica-working-on-issues/references/working-on-issues-source-map.md index d1cc274a63..49d92b14f5 100644 --- a/server/internal/service/builtin_skills/multica-working-on-issues/references/working-on-issues-source-map.md +++ b/server/internal/service/builtin_skills/multica-working-on-issues/references/working-on-issues-source-map.md @@ -76,6 +76,16 @@ key — letter-first, 1-7 chars total (`[a-z][a-z0-9]{0,6}`, narrowed from the o workspace has a default team whose key is the legacy workspace prefix, so a bare `MUL-2759` routes through that default team. +**Move-to-team (issue update `--team`).** `UpdateIssue`'s `team_id` branch +(`handler/issue.go`, move block) renumbers the issue under the target team's +counter and records the old `key-number` in `issue_identifier_alias` +(`UpsertIssueIdentifierAlias`). Identifier resolution falls back to that alias +in `resolveIssueByIdentifier` (`handler.go`) and in GitHub branch/PR linking +(`github.go`), so pre-move references keep resolving. The CLI exposes this as +`multica issue update --team ` (`cmd_issue.go`, +`resolveTeamRef` accepts a key or UUID). Team is a creation-time default +elsewhere: parent/child and project↔team carry no cross-team validation. + **Reference-only flag (MUL-3739).** The link row carries a `reference_only` boolean (`migrations/127_issue_pull_request_reference_only.up.sql`). The handler computes a `qualifyingIdents` set = identifiers in **title or branch** (any diff --git a/server/pkg/db/generated/issue.sql.go b/server/pkg/db/generated/issue.sql.go index 16a806fe89..b5211de3b8 100644 --- a/server/pkg/db/generated/issue.sql.go +++ b/server/pkg/db/generated/issue.sql.go @@ -478,7 +478,7 @@ func (q *Queries) FindActiveDuplicateIssue(ctx context.Context, arg FindActiveDu } const findRecentAutopilotDuplicateIssue = `-- name: FindRecentAutopilotDuplicateIssue :one -SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority, i.assignee_type, i.assignee_id, i.creator_type, i.creator_id, i.parent_issue_id, i.acceptance_criteria, i.context_refs, i.position, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.origin_type, i.origin_id, i.first_executed_at, i.start_date, i.metadata, i.stage FROM issue i +SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority, i.assignee_type, i.assignee_id, i.creator_type, i.creator_id, i.parent_issue_id, i.acceptance_criteria, i.context_refs, i.position, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.origin_type, i.origin_id, i.first_executed_at, i.start_date, i.metadata, i.stage, i.team_id FROM issue i WHERE i.workspace_id = $1 AND i.status NOT IN ('done', 'cancelled') AND i.origin_type = 'autopilot' @@ -540,6 +540,7 @@ func (q *Queries) FindRecentAutopilotDuplicateIssue(ctx context.Context, arg Fin &i.StartDate, &i.Metadata, &i.Stage, + &i.TeamID, ) return i, err } diff --git a/server/pkg/db/generated/project.sql.go b/server/pkg/db/generated/project.sql.go index cea4090da9..86fad965dd 100644 --- a/server/pkg/db/generated/project.sql.go +++ b/server/pkg/db/generated/project.sql.go @@ -364,28 +364,6 @@ func (q *Queries) ListProjects(ctx context.Context, arg ListProjectsParams) ([]P return items, nil } -const projectHasTeam = `-- name: ProjectHasTeam :one -SELECT EXISTS ( - SELECT 1 FROM project_team - WHERE workspace_id = $1 - AND project_id = $2 - AND team_id = $3 -)::boolean -` - -type ProjectHasTeamParams struct { - WorkspaceID pgtype.UUID `json:"workspace_id"` - ProjectID pgtype.UUID `json:"project_id"` - TeamID pgtype.UUID `json:"team_id"` -} - -func (q *Queries) ProjectHasTeam(ctx context.Context, arg ProjectHasTeamParams) (bool, error) { - row := q.db.QueryRow(ctx, projectHasTeam, arg.WorkspaceID, arg.ProjectID, arg.TeamID) - var column_1 bool - err := row.Scan(&column_1) - return column_1, err -} - const replaceProjectTeams = `-- name: ReplaceProjectTeams :exec WITH deleted AS ( DELETE FROM project_team diff --git a/server/pkg/db/queries/project.sql b/server/pkg/db/queries/project.sql index 6703f6c83b..975800ebdf 100644 --- a/server/pkg/db/queries/project.sql +++ b/server/pkg/db/queries/project.sql @@ -63,14 +63,6 @@ WHERE pt.workspace_id = $1 AND pt.project_id = $2 ORDER BY wt.is_default DESC, wt.name ASC, wt.created_at ASC; --- name: ProjectHasTeam :one -SELECT EXISTS ( - SELECT 1 FROM project_team - WHERE workspace_id = $1 - AND project_id = $2 - AND team_id = $3 -)::boolean; - -- name: ListProjectTeamsByProjects :many SELECT pt.project_id, wt.id, wt.workspace_id, wt.name, wt.key, wt.description, wt.icon, wt.issue_counter, wt.is_default, wt.archived_at, wt.archived_by,