chore(teams): CLI move flag, docs, dead code, terminology

- multica issue update --team <UUID-or-key> 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 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-07-06 17:18:48 +08:00
parent fc8755502f
commit 7eec19d3df
9 changed files with 47 additions and 45 deletions

View File

@@ -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 | **自动化** |

View File

@@ -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": "负责人",

View File

@@ -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 (
<DropdownMenu>

View File

@@ -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 == "" {

View File

@@ -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 <UUID>` filters a listing to one team.
Team binds at creation time only, and it can change later:
- `multica issue update <id> --team <UUID-or-key>` 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

View File

@@ -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 <id> --team <UUID-or-key>` (`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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,