mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-07 11:14:28 +02:00
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:
@@ -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 | **自动化** |
|
||||
|
||||
@@ -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": "负责人",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user