Compare commits

...

2 Commits

Author SHA1 Message Date
Jiayuan
5552f96e4c fix(migrations): drop CHECK constraint before updating rows
The UP migration tried to UPDATE status values while the old
constraint still only allowed 'in_review', causing a CHECK
violation. Reorder to: drop constraint → update rows → add new
constraint. Same fix applied to the DOWN migration.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-02 08:04:47 +02:00
Jiayuan
0a16fa784c feat: rename issue status in_review to needs_review
"Needs review" more clearly conveys the expected action — that
someone should come and review — whereas "in review" implies a
review is already in progress.

Changes across the full stack:
- Database: new migration 068 to update CHECK constraint and
  existing rows; init migration updated for fresh installs.
- Go backend: valid-status list, search ranking, autopilot
  listeners, notification labels, runtime CLAUDE.md template.
- TypeScript: IssueStatus type, status config, icon component,
  onboarding, landing page, test mocks.
- Docs: issues.mdx (en/zh), CLI reference (zh), CLI_AND_DAEMON.md,
  docs-outline.md.

Closes MUL-1676

Co-authored-by: multica-agent <github@multica.ai>
2026-05-02 08:00:45 +02:00
23 changed files with 62 additions and 42 deletions

View File

@@ -341,7 +341,7 @@ multica issue assign <id> --unassign
multica issue status <id> in_progress
```
Valid statuses: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`, `cancelled`.
Valid statuses: `backlog`, `todo`, `in_progress`, `needs_review`, `done`, `blocked`, `cancelled`.
### Comments

View File

@@ -260,7 +260,7 @@ multica issue assign <id> --unassign
multica issue status <id> in_progress
```
Valid statuses: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`, `cancelled`.
Valid statuses: `backlog`, `todo`, `in_progress`, `needs_review`, `done`, `blocked`, `cancelled`.
### Comments

View File

@@ -28,7 +28,7 @@ Multica has seven statuses. **Any status can move directly to any other** — Mu
| `backlog` | Not scheduled yet |
| `todo` | Scheduled, ready to start |
| `in_progress` | Being worked on |
| `in_review` | Awaiting review |
| `needs_review` | Awaiting review |
| `done` | Completed |
| `blocked` | Stuck on an external factor |
| `cancelled` | Cancelled |

View File

@@ -28,7 +28,7 @@ Multica 提供七种状态。**任何状态可以直接改到任何状态**—
| `backlog` | 还没排期 |
| `todo` | 已排期、准备开工 |
| `in_progress` | 正在做 |
| `in_review` | 等待 review |
| `needs_review` | 等待 review |
| `done` | 已完成 |
| `blocked` | 被外部因素卡住 |
| `cancelled` | 已取消 |

View File

@@ -137,7 +137,7 @@ const allAssignees: Assignee[] = [
{ type: "agent", id: "tina", name: "Tina-dev" },
];
const statusCycle: IssueStatus[] = ["backlog", "todo", "in_progress", "in_review", "done"];
const statusCycle: IssueStatus[] = ["backlog", "todo", "in_progress", "needs_review", "done"];
const priorityCycle: IssuePriority[] = ["none", "low", "medium", "high", "urgent"];
function TeammatesVisual() {

View File

@@ -36,7 +36,7 @@
- ⬜ Not started
- 🔍 Source research正在读源码验证
- ✍️ Drafting正在写初稿
- 👀 In review待 review
- 👀 Needs review待 review
- ✅ Shipped
### 1.4 Flag只在需要决策时填
@@ -198,7 +198,7 @@ multica issue assign <issue-id> --agent <agent-slug>
## 板块 1Welcome & Quickstart
### 1.1 Welcome — 👀 In review [v1]
### 1.1 Welcome — 👀 Needs review [v1]
- **Source files**: `README.md`, `docs/docs-rewrite-plan.md`(定位段), `apps/docs/content/docs/index.mdx`(现状)
- **目标读者**: P0 新用户 / evaluator第一次听说 Multica
@@ -336,7 +336,7 @@ multica issue assign <issue-id> --agent <agent-slug>
- **写什么**1500-2000 字):
- **Issues 部分**:
- Polymorphic assigneemember/agent/null——第一次正式提"可以分配给 agent"
- Status 枚举backlog/todo/in_progress/in_review/done/blocked/cancelled**无强制 FSM**
- Status 枚举backlog/todo/in_progress/needs_review/done/blocked/cancelled**无强制 FSM**
- Priority / Label / Subscription / Reaction / Dependency / Bulk 操作
- Issue number per-workspace 自增
- Comment reply 树、@mention

View File

@@ -4,7 +4,7 @@ export const STATUS_ORDER: IssueStatus[] = [
"backlog",
"todo",
"in_progress",
"in_review",
"needs_review",
"done",
"blocked",
"cancelled",
@@ -14,7 +14,7 @@ export const ALL_STATUSES: IssueStatus[] = [
"backlog",
"todo",
"in_progress",
"in_review",
"needs_review",
"done",
"blocked",
"cancelled",
@@ -25,7 +25,7 @@ export const BOARD_STATUSES: IssueStatus[] = [
"backlog",
"todo",
"in_progress",
"in_review",
"needs_review",
"done",
"blocked",
];
@@ -43,7 +43,7 @@ export const STATUS_CONFIG: Record<
backlog: { label: "Backlog", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent", dividerColor: "bg-muted-foreground/40", columnBg: "bg-muted/40" },
todo: { label: "Todo", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent", dividerColor: "bg-muted-foreground/40", columnBg: "bg-muted/40" },
in_progress: { label: "In Progress", iconColor: "text-warning", hoverBg: "hover:bg-warning/10", dividerColor: "bg-warning", columnBg: "bg-warning/5" },
in_review: { label: "In Review", iconColor: "text-success", hoverBg: "hover:bg-success/10", dividerColor: "bg-success", columnBg: "bg-success/5" },
needs_review: { label: "Needs Review", iconColor: "text-success", hoverBg: "hover:bg-success/10", dividerColor: "bg-success", columnBg: "bg-success/5" },
done: { label: "Done", iconColor: "text-info", hoverBg: "hover:bg-info/10", dividerColor: "bg-info", columnBg: "bg-info/5" },
blocked: { label: "Blocked", iconColor: "text-destructive", hoverBg: "hover:bg-destructive/10", dividerColor: "bg-destructive", columnBg: "bg-destructive/5" },
cancelled: { label: "Cancelled", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent", dividerColor: "bg-muted-foreground/40", columnBg: "bg-muted/40" },

View File

@@ -4,7 +4,7 @@ export type IssueStatus =
| "backlog"
| "todo"
| "in_progress"
| "in_review"
| "needs_review"
| "done"
| "blocked"
| "cancelled";

View File

@@ -203,14 +203,14 @@ vi.mock("@multica/core/api", () => ({
// Mock issue config
vi.mock("@multica/core/issues/config", () => ({
ALL_STATUSES: ["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"],
BOARD_STATUSES: ["backlog", "todo", "in_progress", "in_review", "done", "blocked"],
STATUS_ORDER: ["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"],
ALL_STATUSES: ["backlog", "todo", "in_progress", "needs_review", "done", "blocked", "cancelled"],
BOARD_STATUSES: ["backlog", "todo", "in_progress", "needs_review", "done", "blocked"],
STATUS_ORDER: ["backlog", "todo", "in_progress", "needs_review", "done", "blocked", "cancelled"],
STATUS_CONFIG: {
backlog: { label: "Backlog", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },
todo: { label: "Todo", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },
in_progress: { label: "In Progress", iconColor: "text-warning", hoverBg: "hover:bg-warning/10" },
in_review: { label: "In Review", iconColor: "text-success", hoverBg: "hover:bg-success/10" },
needs_review: { label: "Needs Review", iconColor: "text-success", hoverBg: "hover:bg-success/10" },
done: { label: "Done", iconColor: "text-info", hoverBg: "hover:bg-info/10" },
blocked: { label: "Blocked", iconColor: "text-destructive", hoverBg: "hover:bg-destructive/10" },
cancelled: { label: "Cancelled", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },

View File

@@ -74,14 +74,14 @@ vi.mock("@multica/core/api", () => ({
// Mock issue config
vi.mock("@multica/core/issues/config", () => ({
ALL_STATUSES: ["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"],
BOARD_STATUSES: ["backlog", "todo", "in_progress", "in_review", "done", "blocked"],
STATUS_ORDER: ["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"],
ALL_STATUSES: ["backlog", "todo", "in_progress", "needs_review", "done", "blocked", "cancelled"],
BOARD_STATUSES: ["backlog", "todo", "in_progress", "needs_review", "done", "blocked"],
STATUS_ORDER: ["backlog", "todo", "in_progress", "needs_review", "done", "blocked", "cancelled"],
STATUS_CONFIG: {
backlog: { label: "Backlog", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },
todo: { label: "Todo", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },
in_progress: { label: "In Progress", iconColor: "text-warning", hoverBg: "hover:bg-warning/10" },
in_review: { label: "In Review", iconColor: "text-success", hoverBg: "hover:bg-success/10" },
needs_review: { label: "Needs Review", iconColor: "text-success", hoverBg: "hover:bg-success/10" },
done: { label: "Done", iconColor: "text-info", hoverBg: "hover:bg-info/10" },
blocked: { label: "Blocked", iconColor: "text-destructive", hoverBg: "hover:bg-destructive/10" },
cancelled: { label: "Cancelled", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },

View File

@@ -92,7 +92,7 @@ function InProgressIcon() {
return <ProgressCircle progress={0.5} />;
}
function InReviewIcon() {
function NeedsReviewIcon() {
return <ProgressCircle progress={0.75} />;
}
@@ -147,7 +147,7 @@ const STATUS_RENDERERS: Record<IssueStatus, () => React.ReactNode> = {
backlog: BacklogIcon,
todo: TodoIcon,
in_progress: InProgressIcon,
in_review: InReviewIcon,
needs_review: NeedsReviewIcon,
done: DoneIcon,
blocked: BlockedIcon,
cancelled: CancelledIcon,

View File

@@ -240,7 +240,7 @@ function WelcomeIllustration() {
actor={{ kind: "agent", name: "Review Agent", provider: "openclaw" }}
issueId="MCA-42"
content="Reviewed Monday's draft — left 4 notes on tone. Standing by for the new one."
status="in_review"
status="needs_review"
/>
<MockActivityCard
className="translate-x-6 rotate-[1deg]"
@@ -286,7 +286,7 @@ function MockActivityCard({
actor: ActivityActor;
issueId: string;
content: React.ReactNode;
status?: Extract<IssueStatus, "in_progress" | "done" | "in_review">;
status?: Extract<IssueStatus, "in_progress" | "done" | "needs_review">;
timestamp?: string;
className?: string;
}) {

View File

@@ -179,7 +179,7 @@ var issueSearchCmd = &cobra.Command{
}
var validIssueStatuses = []string{
"backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled",
"backlog", "todo", "in_progress", "needs_review", "done", "blocked", "cancelled",
}
func init() {

View File

@@ -507,7 +507,7 @@ func TestValidIssueStatuses(t *testing.T) {
"backlog": true,
"todo": true,
"in_progress": true,
"in_review": true,
"needs_review": true,
"done": true,
"blocked": true,
"cancelled": true,

View File

@@ -31,7 +31,7 @@ func registerAutopilotListeners(bus *events.Bus, svc *service.AutopilotService)
return
}
// Only handle statuses that finalize an autopilot run.
if issue.Status != "done" && issue.Status != "in_review" && issue.Status != "cancelled" && issue.Status != "blocked" {
if issue.Status != "done" && issue.Status != "needs_review" && issue.Status != "cancelled" && issue.Status != "blocked" {
return
}
// Load the full issue from DB to check origin_type.

View File

@@ -25,7 +25,7 @@ var statusLabels = map[string]string{
"backlog": "Backlog",
"todo": "Todo",
"in_progress": "In Progress",
"in_review": "In Review",
"needs_review": "Needs Review",
"done": "Done",
"blocked": "Blocked",
"cancelled": "Cancelled",

View File

@@ -433,10 +433,10 @@ func TestSweepResetsInProgressIssueToTodo(t *testing.T) {
}
}
// TestSweepDoesNotResetIssueAlreadyInReview verifies that the sweeper only resets
// TestSweepDoesNotResetIssueAlreadyNeedsReview verifies that the sweeper only resets
// issues that are truly stuck in in_progress — it must not clobber issues whose
// agents already moved them forward (e.g. to in_review) before the task timed out.
func TestSweepDoesNotResetIssueAlreadyInReview(t *testing.T) {
// agents already moved them forward (e.g. to needs_review) before the task timed out.
func TestSweepDoesNotResetIssueAlreadyNeedsReview(t *testing.T) {
if testPool == nil {
t.Skip("no database connection")
}
@@ -455,11 +455,11 @@ func TestSweepDoesNotResetIssueAlreadyInReview(t *testing.T) {
t.Fatalf("failed to find test agent: %v", err)
}
// Issue already advanced to in_review by the agent before the task timed out.
// Issue already advanced to needs_review by the agent before the task timed out.
var issueID string
err = testPool.QueryRow(ctx, `
INSERT INTO issue (workspace_id, title, status, priority, creator_type, creator_id, assignee_type, assignee_id)
SELECT $1, 'Already in_review issue', 'in_review', 'none', 'member', m.user_id, 'agent', $2
SELECT $1, 'Already needs_review issue', 'needs_review', 'none', 'member', m.user_id, 'agent', $2
FROM member m WHERE m.workspace_id = $1 LIMIT 1
RETURNING id
`, testWorkspaceID, agentID).Scan(&issueID)
@@ -494,14 +494,14 @@ func TestSweepDoesNotResetIssueAlreadyInReview(t *testing.T) {
broadcastFailedTasks(ctx, queries, nil, bus, failedTasks)
// Issue should remain in_review — the sweeper must not clobber agent progress.
// Issue should remain needs_review — the sweeper must not clobber agent progress.
var issueStatus string
err = testPool.QueryRow(ctx, `SELECT status FROM issue WHERE id = $1`, issueID).Scan(&issueStatus)
if err != nil {
t.Fatalf("failed to query issue status: %v", err)
}
if issueStatus != "in_review" {
t.Fatalf("expected issue status 'in_review' to be preserved, got '%s'", issueStatus)
if issueStatus != "needs_review" {
t.Fatalf("expected issue status 'needs_review' to be preserved, got '%s'", issueStatus)
}
}

View File

@@ -124,7 +124,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
b.WriteString("### Write\n")
b.WriteString("- `multica issue create --title \"...\" [--description \"...\"] [--priority X] [--status X] [--assignee X] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>] [--attachment <path>]` — Create a new issue. `--attachment` may be repeated to upload multiple files; labels and subscribers are not accepted here, attach them after create with the commands below.\n")
b.WriteString("- `multica issue update <id> [--title X] [--description X] [--priority X] [--status X] [--assignee X] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>]` — Update one or more issue fields in a single call. Use `--parent \"\"` to clear the parent.\n")
b.WriteString("- `multica issue status <id> <status>` — Shortcut for `issue update --status` when you only need to flip status (todo, in_progress, in_review, done, blocked, backlog, cancelled)\n")
b.WriteString("- `multica issue status <id> <status>` — Shortcut for `issue update --status` when you only need to flip status (todo, in_progress, needs_review, done, blocked, backlog, cancelled)\n")
b.WriteString("- `multica issue assign <id> --to <name>` — Assign an issue to a member or agent by name (use `--unassign` to remove assignee)\n")
b.WriteString("- `multica issue label add <issue-id> <label-id>` — Attach a label to an issue (look up the label id via `multica label list`)\n")
b.WriteString("- `multica issue label remove <issue-id> <label-id>` — Detach a label from an issue\n")
@@ -261,7 +261,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
fmt.Fprintf(&b, "3. Run `multica issue status %s in_progress`\n", ctx.IssueID)
b.WriteString("4. Follow your Skills and Agent Identity to complete the task (write code, investigate, etc.)\n")
fmt.Fprintf(&b, "5. **Post your final results as a comment — this step is mandatory**: `multica issue comment add %s --content \"...\"`. Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID)
fmt.Fprintf(&b, "6. When done, run `multica issue status %s in_review`\n", ctx.IssueID)
fmt.Fprintf(&b, "6. When done, run `multica issue status %s needs_review`\n", ctx.IssueID)
fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID)
}

View File

@@ -382,7 +382,7 @@ func buildSearchQuery(phrase string, terms []string, queryNum int, hasNum bool,
// Status priority: active issues first
statusRank := `CASE i.status
WHEN 'in_progress' THEN 0
WHEN 'in_review' THEN 1
WHEN 'needs_review' THEN 1
WHEN 'todo' THEN 2
WHEN 'blocked' THEN 3
WHEN 'backlog' THEN 4

View File

@@ -244,7 +244,7 @@ func (s *AutopilotService) SyncRunFromIssue(ctx context.Context, issue db.Issue)
wsID := util.UUIDToString(issue.WorkspaceID)
switch issue.Status {
case "done", "in_review":
case "done", "needs_review":
if _, err := s.Queries.UpdateAutopilotRunCompleted(ctx, db.UpdateAutopilotRunCompletedParams{
ID: run.ID,
}); err != nil {

View File

@@ -55,7 +55,7 @@ CREATE TABLE issue (
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'backlog'
CHECK (status IN ('backlog', 'todo', 'in_progress', 'in_review', 'done', 'blocked', 'cancelled')),
CHECK (status IN ('backlog', 'todo', 'in_progress', 'needs_review', 'done', 'blocked', 'cancelled')),
priority TEXT NOT NULL DEFAULT 'none'
CHECK (priority IN ('urgent', 'high', 'medium', 'low', 'none')),
assignee_type TEXT CHECK (assignee_type IN ('member', 'agent')),

View File

@@ -0,0 +1,10 @@
-- Revert: rename issue status 'needs_review' → 'in_review'.
-- 1. Drop the constraint so the UPDATE is allowed.
ALTER TABLE issue DROP CONSTRAINT IF EXISTS issue_status_check;
-- 2. Revert existing rows.
UPDATE issue SET status = 'in_review' WHERE status = 'needs_review';
-- 3. Re-add the original constraint.
ALTER TABLE issue ADD CONSTRAINT issue_status_check
CHECK (status IN ('backlog', 'todo', 'in_progress', 'in_review', 'done', 'blocked', 'cancelled'));

View File

@@ -0,0 +1,10 @@
-- Rename issue status 'in_review' → 'needs_review'.
-- 1. Drop the existing constraint so the UPDATE is allowed.
ALTER TABLE issue DROP CONSTRAINT IF EXISTS issue_status_check;
-- 2. Update existing rows.
UPDATE issue SET status = 'needs_review' WHERE status = 'in_review';
-- 3. Add the new constraint.
ALTER TABLE issue ADD CONSTRAINT issue_status_check
CHECK (status IN ('backlog', 'todo', 'in_progress', 'needs_review', 'done', 'blocked', 'cancelled'));