@mu-crew/mu 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +45 -31
  2. package/dist/cli.js +186 -20
  3. package/dist/cli.js.map +1 -1
  4. package/dist/extension/mu-pi.js +103 -74
  5. package/dist/extension/mu-pi.js.map +1 -1
  6. package/dist/index.d.ts +33 -4
  7. package/dist/index.js +70 -4
  8. package/dist/index.js.map +1 -1
  9. package/docs/VISION.md +3 -1
  10. package/docs/VOCABULARY.md +4 -1
  11. package/docs/architecture/dag.md +3 -3
  12. package/docs/guide/delegate.md +30 -3
  13. package/docs/reference/env.md +1 -0
  14. package/docs/reference/naming.md +18 -0
  15. package/docs/specs/2026-09-29-task-substates.md +1 -1
  16. package/package.json +1 -1
  17. package/prompts/mu-debug.md +8 -0
  18. package/prompts/mu-sweep.md +8 -0
  19. package/prompts/mu-until.md +8 -0
  20. package/prompts/ultrathink.md +2 -1
  21. package/skills/mu/SKILL.md +70 -31
  22. package/skills/mu/recipes/adversarial-review.md +27 -19
  23. package/skills/mu/recipes/{triage.md → backlog-triage.md} +9 -4
  24. package/skills/mu/recipes/codemode-driver.md +68 -20
  25. package/skills/mu/recipes/deep-research.md +23 -8
  26. package/skills/mu/recipes/fan-out.md +15 -11
  27. package/skills/mu/recipes/findings.md +103 -0
  28. package/skills/mu/recipes/hypothesis-panel.md +18 -13
  29. package/skills/mu/recipes/long-run.md +4 -4
  30. package/skills/mu/recipes/loop-until-done.md +7 -3
  31. package/skills/mu/recipes/orchestrator-loop.md +38 -27
  32. package/skills/mu/recipes/plan.md +8 -3
  33. package/skills/mu/recipes/recovery.md +2 -0
  34. package/skills/mu/recipes/refute.md +38 -36
  35. package/skills/mu/recipes/remote-workers.md +7 -2
  36. package/skills/mu/recipes/review-panel.md +36 -31
  37. package/skills/mu/recipes/rules-audit.md +30 -17
  38. package/skills/mu/recipes/tasks-or-calls.md +67 -0
  39. package/skills/mu/recipes/tournament.md +13 -10
  40. package/skills/mu/recipes/ultrathink.md +20 -11
  41. package/skills/mu/recipes/watcher.md +4 -1
  42. package/skills/mu/recipes/waves.md +6 -5
  43. package/skills/mu/recipes/worker.md +3 -1
package/README.md CHANGED
@@ -20,9 +20,9 @@ in-progress, and blocked tasks, the log tail, workspaces, and doctor.*
20
20
  socket inside each agent, so send, state, wait, and abort are exact,
21
21
  locally and over ssh. The pane stays pi's normal TUI.
22
22
 
23
- mu persists state and coordinates handoffs. It does not choose models,
24
- run your tests, or pass messages between agents. Every read path has
25
- `--json`, and every change goes through a typed CLI verb.
23
+ mu persists state and coordinates handoffs. Every read path has
24
+ `--json`, and every change goes through a typed CLI verb. See
25
+ [What mu is not](#what-mu-is-not).
26
26
 
27
27
  ## Install
28
28
 
@@ -71,24 +71,6 @@ For one helper, ask pi to delegate (`mu_delegate` ships with `mu link
71
71
  pi`), or spawn into the reserved `scratch` workstream:
72
72
  `mu agent spawn scout-1 -w scratch`.
73
73
 
74
- ## mu delegates vs hidden subagents
75
-
76
- Claude Code, Codex, and pi-subagents delegate to a hidden child process
77
- and return only its result. A mu delegate is an ordinary agent in a
78
- pane.
79
-
80
- | | Hidden subagent | mu delegate |
81
- | ----------------------- | ------------------------ | ----------- |
82
- | Visibility | none while it runs | a pane; attach and watch |
83
- | Steer mid-run | no | `mu agent send`; stop with `mu agent abort` |
84
- | Keep talking after it answers | no | yes, with `keep: true` |
85
- | Transcript | collapses into a result | a normal pi session log |
86
- | If the work grows | re-brief a new agent | same task DAG and workspaces |
87
- | Cost | light | one pane and one pi process |
88
-
89
- For many tiny calls, a hidden subagent is lighter. mu bets that seeing
90
- and steering agent work is worth a pane.
91
-
92
74
  ## Ultrathink, the mu way
93
75
 
94
76
  Long agent runs fail in two ways: the agent stops at 35 of 50 items and
@@ -98,19 +80,28 @@ agents, and have other agents try to refute each result before it
98
80
  counts. Claude Code's ultracode does this with a hidden script. mu does
99
81
  it in the open:
100
82
 
101
- - **The plan is the DAG.** Every unit, every review, and every round
83
+ - **The plan is the DAG.** Every unit, review gate, finding and round
102
84
  is a task before an agent spawns. `mu state` shows 35 of 50 done, so
103
85
  nobody can declare victory early.
104
- - **Reviews are tasks.** A `review_x` task is blocked by `x` and run by
105
- a fresh agent, ideally on another model, against criteria written
106
- up front. A rejection adds `fix_x` and a second review to the DAG
107
- instead of rewriting history.
86
+ - **Findings are tasks.** A reviewer records each problem as a task in
87
+ triage ([findings](skills/mu/recipes/findings.md)), and it blocks the
88
+ review until someone accepts it as work or closes it rejected or
89
+ duplicate. The graph holds the whole review.
90
+ - **Checks are calls.** Refuters, claim checkers and judges are
91
+ [delegate calls](skills/mu/recipes/tasks-or-calls.md): fresh agents,
92
+ ideally on another model, whose verdicts land on the task they
93
+ judged. A 50-unit run adds about 50 units, 50 review gates and the
94
+ findings that survive, not a task per check.
108
95
  - **Every agent is a pane.** Attach to any worker, reviewer, or judge
109
96
  mid-run and steer it.
110
97
  - **Nothing lives only in a context window.** Verdicts, evidence, and
111
98
  stop rules are task notes. The run survives compaction, crashes, and
112
99
  a new orchestrator.
113
100
 
101
+ Not every review needs a record. Reviewing someone's PR or a doc runs
102
+ as delegates only: findings come back in their answers, and nothing
103
+ lands in the ops log.
104
+
114
105
  Ask your orchestrator to *ultrathink* a job, and it follows
115
106
  [skills/mu/recipes/ultrathink.md](skills/mu/recipes/ultrathink.md).
116
107
  That recipe composes smaller ones you can also use on their own:
@@ -118,23 +109,46 @@ That recipe composes smaller ones you can also use on their own:
118
109
  | Recipe | Shape |
119
110
  | ------ | ----- |
120
111
  | [fan-out](skills/mu/recipes/fan-out.md) | one task per unit, merged as each closes |
121
- | [adversarial-review](skills/mu/recipes/adversarial-review.md) | a fresh agent tries to reject each unit before it ships |
122
- | [refute](skills/mu/recipes/refute.md) | finders, then one refuter per finding, then a report |
112
+ | [adversarial-review](skills/mu/recipes/adversarial-review.md) | a review gate per unit: a fresh agent tries to reject it before it ships |
113
+ | [refute](skills/mu/recipes/refute.md) | finders record findings, one refuter call per finding, report from the graph |
123
114
  | [hypothesis-panel](skills/mu/recipes/hypothesis-panel.md) | independent theories from separate evidence, each attacked |
124
115
  | [tournament](skills/mu/recipes/tournament.md) | competing attempts, judged in pairs |
125
116
  | [loop-until-done](skills/mu/recipes/loop-until-done.md) | rounds until a stop rule written up front holds |
126
- | [triage](skills/mu/recipes/triage.md) | quarantined readers classify, a trusted actor acts |
117
+ | [backlog-triage](skills/mu/recipes/backlog-triage.md) | quarantined readers classify, a trusted actor acts |
127
118
  | [deep-research](skills/mu/recipes/deep-research.md) | searchers by angle, one checker per claim, a cited report |
128
- | [review-panel](skills/mu/recipes/review-panel.md) | one reviewer per angle on a diff, refuted, then fixed or reported |
119
+ | [review-panel](skills/mu/recipes/review-panel.md) | one reviewer per angle on a diff; delegates by default, tasks when fixes follow |
129
120
  | [rules-audit](skills/mu/recipes/rules-audit.md) | one checker per AGENTS.md rule plus a skeptic; or mine repeated corrections into new rules |
130
121
 
131
122
  In pi, `mu link pi` adds them as commands: `/ultrathink <job>`,
132
123
  `/mu-research <question>`, `/mu-review [target]`, `/mu-refute <scope>`,
133
- `/mu-tournament <task>`, and `/mu-rules-audit [check <target> | mine]`.
124
+ `/mu-tournament <task>`, `/mu-rules-audit [check <target> | mine]`,
125
+ `/mu-debug <symptom>`, `/mu-sweep <change>`, and `/mu-until <stop rule>`.
134
126
 
135
127
  All recipes, including the orchestrator loop and remote workers, are
136
128
  in [skills/mu/recipes/](skills/mu/recipes/).
137
129
 
130
+ ## What mu is not
131
+
132
+ - **Not a hidden subagent.** Every agent, including a one-off
133
+ `mu_delegate` helper, is an ordinary agent in a pane: attach, steer,
134
+ abort, or keep talking to it. That costs a pane and a pi process per
135
+ agent; for many tiny calls a hidden subagent is lighter. See
136
+ [the comparison](docs/guide/delegate.md#delegates-vs-hidden-subagents).
137
+ - **Not a workflow engine.** No DSL, no hidden plan, no script runner.
138
+ The plan is the task DAG; recipes are instructions an agent follows,
139
+ and every step is a CLI verb.
140
+ - **Not a model router.** mu does not pick models, thinking levels, or
141
+ tools. Pass them in the spawn command (`--cli pi_big`).
142
+ - **Not a chat bus.** Agents coordinate through task notes, claims, and
143
+ the ops log, not messages to each other.
144
+ - **Not a daemon or a service.** No background process, config file, web
145
+ UI, or RPC. State is one SQLite file; sync rides on ordinary commands.
146
+ - **Not a test runner or a judge.** `--evidence` records what an agent
147
+ observed; checking it is the orchestrator's job, and the recipes say how.
148
+
149
+ The full list, with reasons: [VISION § What it is not](docs/VISION.md#what-it-is-not)
150
+ and [ROADMAP § Anti-feature pledges](docs/ROADMAP.md#anti-feature-pledges).
151
+
138
152
  ## Documentation
139
153
 
140
154
  - [Getting started](docs/guide/getting-started.md): the first
package/dist/cli.js CHANGED
@@ -393,7 +393,7 @@ var init_status = __esm({
393
393
  TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
394
394
  TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
395
395
  TASK_SUBSTATES = {
396
- OPEN: ["todo", "parked"],
396
+ OPEN: ["todo", "parked", "triage"],
397
397
  IN_PROGRESS: ["active"],
398
398
  CLOSED: ["done", "rejected", "wontfix", "duplicate", "superseded"]
399
399
  };
@@ -607,7 +607,7 @@ CREATE VIEW ready AS
607
607
  SELECT t.*
608
608
  FROM tasks t
609
609
  WHERE t.status = 'OPEN'
610
- AND t.substate <> 'parked'
610
+ AND t.substate NOT IN ('parked', 'triage')
611
611
  AND NOT EXISTS (
612
612
  SELECT 1
613
613
  FROM task_edges e
@@ -5850,7 +5850,7 @@ var init_id = __esm({
5850
5850
  });
5851
5851
 
5852
5852
  // src/tasks/errors.ts
5853
- var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskParkedError, InvalidSubstateError;
5853
+ var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskInTriageError, TaskParkedError, InvalidSubstateError;
5854
5854
  var init_errors2 = __esm({
5855
5855
  "src/tasks/errors.ts"() {
5856
5856
  "use strict";
@@ -6198,6 +6198,30 @@ var init_errors2 = __esm({
6198
6198
  ];
6199
6199
  }
6200
6200
  };
6201
+ TaskInTriageError = class extends Error {
6202
+ constructor(taskId, workstream) {
6203
+ super(`task ${taskId} is in triage (OPEN/triage); accept it or claim with --force`);
6204
+ this.taskId = taskId;
6205
+ this.workstream = workstream;
6206
+ }
6207
+ taskId;
6208
+ workstream;
6209
+ name = "TaskInTriageError";
6210
+ errorNextSteps() {
6211
+ const ws = this.workstream;
6212
+ return [
6213
+ {
6214
+ intent: "Accept it as work, then claim",
6215
+ command: `mu task accept ${this.taskId} -w ${ws}`
6216
+ },
6217
+ {
6218
+ intent: "Decline it",
6219
+ command: `mu task close ${this.taskId} --as rejected --why "<reason>" -w ${ws}`
6220
+ },
6221
+ { intent: "Claim it anyway", command: `mu task claim ${this.taskId} --force -w ${ws}` }
6222
+ ];
6223
+ }
6224
+ };
6201
6225
  TaskParkedError = class extends Error {
6202
6226
  constructor(taskId, workstream) {
6203
6227
  super(`task ${taskId} is parked (OPEN/parked); unpark it or claim with --force`);
@@ -6760,8 +6784,17 @@ function addTaskImpl(db, opts) {
6760
6784
  const now = (/* @__PURE__ */ new Date()).toISOString();
6761
6785
  const insertResult = db.prepare(
6762
6786
  `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days, created_at, updated_at)
6763
- VALUES (?, ?, ?, 'OPEN', 'todo', ?, ?, ?, ?)`
6764
- ).run(wsId, opts.localId, opts.title, opts.impact, opts.effortDays, now, now);
6787
+ VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?, ?)`
6788
+ ).run(
6789
+ wsId,
6790
+ opts.localId,
6791
+ opts.title,
6792
+ opts.triage === true ? "triage" : "todo",
6793
+ opts.impact,
6794
+ opts.effortDays,
6795
+ now,
6796
+ now
6797
+ );
6765
6798
  const newTaskId = Number(insertResult.lastInsertRowid);
6766
6799
  if (opts.blockedBy && opts.blockedBy.length > 0) {
6767
6800
  const blockerLookupSameWs = db.prepare(
@@ -7049,6 +7082,28 @@ function parkTask(db, localId, opts) {
7049
7082
  })()
7050
7083
  );
7051
7084
  }
7085
+ function acceptTask(db, localId, opts) {
7086
+ return withOpContext(
7087
+ db,
7088
+ { intent: "task.accept", group: "new" },
7089
+ () => db.transaction(() => {
7090
+ const before = getTask(db, localId, opts.workstream);
7091
+ if (!before) throw new TaskNotFoundError(localId);
7092
+ if (before.status !== "OPEN" || before.substate !== "triage") {
7093
+ return {
7094
+ previousStatus: before.status,
7095
+ status: before.status,
7096
+ previousSubstate: before.substate,
7097
+ substate: before.substate,
7098
+ changed: false
7099
+ };
7100
+ }
7101
+ const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
7102
+ recordEvidenceNote(db, localId, before.workstreamName, "ACCEPT", opts);
7103
+ return r;
7104
+ })()
7105
+ );
7106
+ }
7052
7107
  function unparkTask(db, localId, opts) {
7053
7108
  return withOpContext(
7054
7109
  db,
@@ -7191,6 +7246,9 @@ function assertNotParked(task, opts) {
7191
7246
  if (task.status === "OPEN" && task.substate === "parked" && opts.force !== true) {
7192
7247
  throw new TaskParkedError(task.name, task.workstreamName);
7193
7248
  }
7249
+ if (task.status === "OPEN" && task.substate === "triage" && opts.force !== true) {
7250
+ throw new TaskInTriageError(task.name, task.workstreamName);
7251
+ }
7194
7252
  }
7195
7253
  async function resolveActorIdentity() {
7196
7254
  const worker = await resolveWorkerIdentity();
@@ -10083,6 +10141,10 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
10083
10141
  blocked: listBlocked(db, workstream),
10084
10142
  recentClosed: listRecentClosed(db, workstream),
10085
10143
  parkedCount: listTasks(db, workstream, { status: "OPEN", substate: "parked" }).length,
10144
+ triage: listTasks(db, workstream, { status: "OPEN", substate: "triage" }),
10145
+ taskCount: db.prepare(
10146
+ "SELECT count(*) AS n FROM tasks t JOIN workstreams w ON w.id = t.workstream_id WHERE w.name = ?"
10147
+ ).get(workstream).n,
10086
10148
  allTasks: opts.withAllTasks === true ? listTasks(db, workstream) : [],
10087
10149
  workspaces: listWorkspaces2(db, workstream),
10088
10150
  workspaceOrphans: listWorkspaceOrphans(db, workstream),
@@ -10156,6 +10218,8 @@ function minimalSnapshot(workstream) {
10156
10218
  blocked: [],
10157
10219
  recentClosed: [],
10158
10220
  parkedCount: 0,
10221
+ triage: [],
10222
+ taskCount: 0,
10159
10223
  allTasks: [],
10160
10224
  workspaces: [],
10161
10225
  workspaceOrphans: [],
@@ -11324,7 +11388,7 @@ function colorPair(pair) {
11324
11388
  const text2 = formatPair(pair);
11325
11389
  switch (pair.status) {
11326
11390
  case "OPEN":
11327
- return pair.substate === "parked" ? pc.dim(text2) : pc.cyan(text2);
11391
+ return pair.substate === "parked" || pair.substate === "triage" ? pc.dim(text2) : pc.cyan(text2);
11328
11392
  case "IN_PROGRESS":
11329
11393
  return pc.yellow(text2);
11330
11394
  case "CLOSED":
@@ -11344,7 +11408,7 @@ function colorStatus(status) {
11344
11408
  function inkColorForPair(pair) {
11345
11409
  switch (pair.status) {
11346
11410
  case "OPEN":
11347
- return pair.substate === "parked" ? "gray" : "cyan";
11411
+ return pair.substate === "parked" || pair.substate === "triage" ? "gray" : "cyan";
11348
11412
  case "IN_PROGRESS":
11349
11413
  return "yellow";
11350
11414
  case "CLOSED":
@@ -16537,6 +16601,7 @@ function yankCommandForTask2(t, ws) {
16537
16601
  switch (t.status) {
16538
16602
  case "OPEN":
16539
16603
  if (t.substate === "parked") return `mu task unpark ${t.name} -w ${ws}`;
16604
+ if (t.substate === "triage") return `mu task accept ${t.name} -w ${ws}`;
16540
16605
  return t.ownerName === null ? `mu task claim ${t.name} -w ${ws}` : `mu task release ${t.name} -w ${ws}`;
16541
16606
  case "IN_PROGRESS":
16542
16607
  return `mu task close ${t.name} -w ${ws} --evidence "..."`;
@@ -17596,6 +17661,7 @@ function snapshotKey(s) {
17596
17661
  blocked: s.blocked.map(taskKey2),
17597
17662
  recentClosed: s.recentClosed.map(taskKey2),
17598
17663
  parkedCount: s.parkedCount,
17664
+ triage: s.triage.map(taskKey2),
17599
17665
  allTasks: s.allTasks.map(taskKey2),
17600
17666
  commitsBackend: s.commitsBackend ?? null,
17601
17667
  recentCommits: s.recentCommits.map((c) => [c.sha, c.subject, c.author, c.relTime]),
@@ -18769,7 +18835,8 @@ async function cmdTaskAdd(db, localId, opts) {
18769
18835
  title: opts.title,
18770
18836
  impact: opts.impact,
18771
18837
  effortDays: opts.effortDays,
18772
- ...hasBlockers ? { blockedBy } : {}
18838
+ ...hasBlockers ? { blockedBy } : {},
18839
+ ...opts.triage === true ? { triage: true } : {}
18773
18840
  });
18774
18841
  const createdNote = opts.note !== void 0 ? addNote(db, createdTask.name, unescapeNoteText(opts.note), {
18775
18842
  author: initialNoteAuthor,
@@ -18793,7 +18860,7 @@ async function cmdTaskAdd(db, localId, opts) {
18793
18860
  intent: "Add a blocker",
18794
18861
  command: `mu task block ${task.name} --by <other-id> -w ${workstream}`
18795
18862
  },
18796
- {
18863
+ opts.triage === true ? { intent: "Accept it as work", command: `mu task accept ${task.name} -w ${workstream}` } : {
18797
18864
  intent: "Claim and start",
18798
18865
  command: `mu task claim ${task.name} -w ${workstream} --self (or --for <worker>)`
18799
18866
  }
@@ -18916,6 +18983,12 @@ async function cmdTaskShow(db, rawId, opts = {}) {
18916
18983
  if (task.substate === "parked") {
18917
18984
  console.log("");
18918
18985
  printNextSteps([{ intent: "Return it to next", command: `mu task unpark ${ref}` }]);
18986
+ } else if (task.substate === "triage") {
18987
+ console.log("");
18988
+ printNextSteps([
18989
+ { intent: "Accept it as work", command: `mu task accept ${ref}` },
18990
+ { intent: "Decline it", command: `mu task close ${ref} --as rejected --why "<reason>"` }
18991
+ ]);
18919
18992
  } else if (task.status === "CLOSED" && task.substate !== "done") {
18920
18993
  console.log("");
18921
18994
  printNextSteps([{ intent: "Reopen it", command: `mu task open ${ref}` }]);
@@ -19040,6 +19113,17 @@ async function cmdTaskList(db, opts) {
19040
19113
  const basis = relTimeBasisForSort(sortKey);
19041
19114
  if (basis !== null) tableOpts.relTimeBasis = basis;
19042
19115
  console.log(formatTaskListTable(tasks, tableOpts));
19116
+ const first = tasks.find((t) => t.status === "OPEN" && t.substate === "triage");
19117
+ if (first !== void 0) {
19118
+ printNextSteps([
19119
+ { intent: "Accept as work", command: `mu task accept ${first.name} -w ${workstream}` },
19120
+ {
19121
+ intent: "Decline",
19122
+ command: `mu task close ${first.name} --as rejected|duplicate --why "<reason>" -w ${workstream}`
19123
+ },
19124
+ { intent: "Read the evidence", command: `mu task notes ${first.name} -w ${workstream}` }
19125
+ ]);
19126
+ }
19043
19127
  }
19044
19128
  async function cmdTaskNext(db, opts) {
19045
19129
  const workstream = await resolveWorkstream(opts.workstream);
@@ -19060,16 +19144,26 @@ async function cmdTaskNext(db, opts) {
19060
19144
  }
19061
19145
  if (tasks.length === 0) {
19062
19146
  const parked = listTasks(db, workstream, { substate: "parked" }).length;
19063
- if (parked === 0) {
19147
+ const triage = listTasks(db, workstream, { substate: "triage" }).length;
19148
+ if (parked === 0 && triage === 0) {
19064
19149
  console.log(pc.dim("(no ready tasks)"));
19065
19150
  return;
19066
19151
  }
19067
- console.log(pc.dim(`(no ready tasks; ${parked} parked)`));
19152
+ const held = [parked > 0 ? `${parked} parked` : "", triage > 0 ? `${triage} in triage` : ""].filter(Boolean).join(", ");
19153
+ console.log(pc.dim(`(no ready tasks; ${held})`));
19068
19154
  printNextSteps([
19069
- {
19070
- intent: "List parked tasks",
19071
- command: `mu task list --substate parked -w ${workstream}`
19072
- }
19155
+ ...triage > 0 ? [
19156
+ {
19157
+ intent: "Triage findings",
19158
+ command: `mu task list --substate triage -w ${workstream}`
19159
+ }
19160
+ ] : [],
19161
+ ...parked > 0 ? [
19162
+ {
19163
+ intent: "List parked tasks",
19164
+ command: `mu task list --substate parked -w ${workstream}`
19165
+ }
19166
+ ] : []
19073
19167
  ]);
19074
19168
  return;
19075
19169
  }
@@ -19638,7 +19732,14 @@ async function cmdTaskClose(db, rawId, opts = {}) {
19638
19732
  const taskRow = getTask(db, localId, ws);
19639
19733
  const r = closeTask(db, localId, sdkOpts);
19640
19734
  if ("skipped" in r) {
19735
+ const inTriage = r.blockingIds.filter((id) => getTask(db, id, ws)?.substate === "triage");
19641
19736
  const blockingNextSteps = [
19737
+ ...inTriage.length > 0 ? [
19738
+ {
19739
+ intent: `Triage ${inTriage.length} blocking finding(s) (accept, or close --as rejected|duplicate)`,
19740
+ command: `mu task list --substate triage -w ${ws}`
19741
+ }
19742
+ ] : [],
19642
19743
  {
19643
19744
  intent: "Watch the remaining blockers (returns when one closes)",
19644
19745
  command: `mu task wait ${r.blockingIds.join(" ")} -w ${ws} --first --any --on-stall exit`
@@ -19771,6 +19872,32 @@ async function cmdTaskPark(db, rawId, opts) {
19771
19872
  }
19772
19873
  printNextSteps(nextSteps3);
19773
19874
  }
19875
+ async function cmdTaskAccept(db, rawId, opts = {}) {
19876
+ const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
19877
+ assertTaskInWorkstream(db, localId, opts.workstream);
19878
+ const ws = await resolveWorkstream(opts.workstream);
19879
+ const sdkOpts = { workstream: ws };
19880
+ if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
19881
+ const r = acceptTask(db, localId, sdkOpts);
19882
+ const nextSteps3 = [
19883
+ {
19884
+ intent: "Claim it",
19885
+ command: `mu task claim ${localId} -w ${ws} (--self / --for <worker>)`
19886
+ },
19887
+ { intent: "Triage the next one", command: `mu task list --substate triage -w ${ws}` }
19888
+ ];
19889
+ if (opts.json) {
19890
+ emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
19891
+ return;
19892
+ }
19893
+ if (!r.changed) {
19894
+ const pair = formatPair({ status: r.status, substate: r.substate });
19895
+ console.log(pc.dim(`${localId} is ${pair}, not in triage (no-op)`));
19896
+ } else {
19897
+ console.log(`Accepted ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
19898
+ }
19899
+ printNextSteps(nextSteps3);
19900
+ }
19774
19901
  async function cmdTaskUnpark(db, rawId, opts = {}) {
19775
19902
  const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
19776
19903
  assertTaskInWorkstream(db, localId, opts.workstream);
@@ -19849,7 +19976,10 @@ function wireTaskCommands(program) {
19849
19976
  ).requiredOption("-t, --title <title>", "task title").requiredOption("-i, --impact <n>", "impact 1..100", parseImpact).requiredOption("-e, --effort-days <days>", "effort in days (>0)", parsePositiveNumber).option(
19850
19977
  "-b, --blocked-by <ids...>",
19851
19978
  "task ids that block this one (repeat or comma-separate; or both)"
19852
- ).option("--note <text>", "append an initial note after creating the task").option("--note-author <name>", "author label for --note (default: current actor)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(id) {
19979
+ ).option("--note <text>", "append an initial note after creating the task").option("--note-author <name>", "author label for --note (default: current actor)").option(
19980
+ "--triage",
19981
+ "start as OPEN/triage: a proposal (e.g. a review finding) kept out of next/claim until `mu task accept`"
19982
+ ).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(id) {
19853
19983
  const opts = this.opts();
19854
19984
  return handle((db) => cmdTaskAdd(db, id, opts), this)();
19855
19985
  });
@@ -19954,6 +20084,12 @@ function wireTaskCommands(program) {
19954
20084
  const opts = this.opts();
19955
20085
  return handle((db) => cmdTaskPark(db, id, opts), this)();
19956
20086
  });
20087
+ task.command("accept <id>").description(
20088
+ "Accept an OPEN/triage task (a proposed finding) as work: OPEN/todo, back in `next`. No-op on any other state. Decline with `close --as rejected|duplicate --why`."
20089
+ ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
20090
+ const opts = this.opts();
20091
+ return handle((db) => cmdTaskAccept(db, id, opts), this)();
20092
+ });
19957
20093
  task.command("unpark <id>").description("Return a parked task to OPEN (back in `next`). No-op on any other state.").option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
19958
20094
  const opts = this.opts();
19959
20095
  return handle((db) => cmdTaskUnpark(db, id, opts), this)();
@@ -19978,7 +20114,7 @@ function wireTaskCommands(program) {
19978
20114
  ).option(
19979
20115
  "--actor <name>",
19980
20116
  "override the actor name used for the log (only valid with --self; defaults to pane title or $USER)"
19981
- ).option("--force", "claim even when the task is parked (OPEN/parked)").option(
20117
+ ).option("--force", "claim even when the task is parked or in triage (OPEN/parked, OPEN/triage)").option(
19982
20118
  "--strict-staleness",
19983
20119
  "refuse --for dispatch when the target agent's workspace is stale (default: warn and proceed)"
19984
20120
  ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(taskId) {
@@ -21395,7 +21531,7 @@ function classifyError(err) {
21395
21531
  }
21396
21532
  if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError || // Substate refusals (ts_4): a missing --why, a park from the wrong
21397
21533
  // phase, a claim on a parked task, a pair outside TASK_SUBSTATES.
21398
- err instanceof SubstateReasonRequiredError || err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof InvalidSubstateError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
21534
+ err instanceof SubstateReasonRequiredError || err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof TaskInTriageError || err instanceof InvalidSubstateError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
21399
21535
  // the source DB: both are name/state collisions, not usage errors.
21400
21536
  err instanceof RebuildTargetExistsError || err instanceof RebuildTargetIsSourceError || // An abbreviated group id matching several groups: the operator must
21401
21537
  // disambiguate. Guessing would be catastrophic for `mu undo`.
@@ -23020,6 +23156,7 @@ function fullJsonShape(d) {
23020
23156
  orphans: d.view.orphans,
23021
23157
  tracks: d.tracks,
23022
23158
  ready: withRoiAll(d.ready),
23159
+ triage: withRoiAll(d.triage),
23023
23160
  inProgress: withRoiAll(d.inProgress),
23024
23161
  blocked: withRoiAll(d.blocked),
23025
23162
  recentClosed: withRoiAll(d.recentClosed),
@@ -23092,6 +23229,7 @@ async function cmdState(db, opts) {
23092
23229
  }
23093
23230
  renderFullMode(perWs);
23094
23231
  }
23232
+ var BIG_WORKSTREAM_TASKS = 300;
23095
23233
  function renderFullMode(perWs) {
23096
23234
  perWs.forEach((d, i) => {
23097
23235
  if (i > 0) console.log("");
@@ -23124,6 +23262,15 @@ function renderFullCard(d) {
23124
23262
  );
23125
23263
  }
23126
23264
  }
23265
+ const total = d.taskCount;
23266
+ const open = ready.length + inProgress.length + blocked.length + d.triage.length;
23267
+ if (total >= BIG_WORKSTREAM_TASKS && open * 10 < total) {
23268
+ console.log(
23269
+ pc.dim(
23270
+ `hint: ${total} tasks, ${open} open; start the next effort in a new <project>-<purpose> workstream`
23271
+ )
23272
+ );
23273
+ }
23127
23274
  console.log("");
23128
23275
  console.log(pc.bold(`Tracks (${tracks.length})`));
23129
23276
  console.log(formatTracks(tracks));
@@ -23131,6 +23278,13 @@ function renderFullCard(d) {
23131
23278
  console.log(pc.bold(`Ready (${ready.length})`));
23132
23279
  console.log(ready.length === 0 ? pc.dim(" (none)") : formatTaskListTable(ready));
23133
23280
  console.log("");
23281
+ if (d.triage.length > 0) {
23282
+ console.log(
23283
+ `${pc.bold(`Triage (${d.triage.length})`)} ${pc.dim(`accept: mu task accept <id> -w ${workstreamName} \xB7 decline: mu task close <id> --as rejected|duplicate --why ...`)}`
23284
+ );
23285
+ console.log(formatTaskListTable(d.triage));
23286
+ console.log("");
23287
+ }
23134
23288
  console.log(pc.bold(`In progress (${inProgress.length})`));
23135
23289
  console.log(inProgress.length === 0 ? pc.dim(" (none)") : formatTaskListTable(inProgress));
23136
23290
  console.log("");
@@ -23736,6 +23890,7 @@ function wireWorkspaceCommands(program) {
23736
23890
  }
23737
23891
 
23738
23892
  // src/cli/workstream.ts
23893
+ import { basename as basename2 } from "path";
23739
23894
  init_mux2();
23740
23895
  init_output();
23741
23896
  init_workstream();
@@ -23764,6 +23919,7 @@ async function cmdInit(db, name, opts = {}) {
23764
23919
  await mux.enableMuPaneBordersForSession(sessionName).catch(() => {
23765
23920
  });
23766
23921
  const created = !sessionAlready || dbCreated;
23922
+ const nameHint = name.includes("-") ? void 0 : `name workstreams <project>-<purpose> (e.g. ${projectGuess() ?? "repo"}-${name}); one per effort`;
23767
23923
  const nextSteps3 = [
23768
23924
  { intent: "Attach the session", command: mux.attachHint({ session: sessionName }) },
23769
23925
  {
@@ -23781,6 +23937,7 @@ async function cmdInit(db, name, opts = {}) {
23781
23937
  muxSessionAlreadyExisted: sessionAlready,
23782
23938
  dbRowAlreadyExisted: !dbCreated,
23783
23939
  muWindowRepaired,
23940
+ ...nameHint && created ? { hint: nameHint } : {},
23784
23941
  nextSteps: nextSteps3
23785
23942
  });
23786
23943
  return;
@@ -23796,8 +23953,13 @@ async function cmdInit(db, name, opts = {}) {
23796
23953
  return;
23797
23954
  }
23798
23955
  console.log(`Created workstream ${pc.bold(name)} (mux session ${pc.bold(sessionName)})`);
23956
+ if (nameHint && created) console.log(pc.dim(`hint: ${nameHint}`));
23799
23957
  printNextSteps(nextSteps3);
23800
23958
  }
23959
+ function projectGuess() {
23960
+ const base = basename2(process.cwd()).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "");
23961
+ return base === "" ? void 0 : base;
23962
+ }
23801
23963
  async function cmdWorkstreamList(db, opts = {}) {
23802
23964
  if (opts.tornDown === true) {
23803
23965
  const gone = listTornDownWorkstreams(db);
@@ -24053,8 +24215,12 @@ function withPositionalWorkstream(opts, name) {
24053
24215
  return { ...opts, workstream: name };
24054
24216
  }
24055
24217
  function wireWorkstreamCommands(program) {
24056
- const workstream = program.command("workstream").description("Workstream-level commands");
24057
- workstream.command("init <name>").description("Create the workstream's mux session and register it in the DB").option(...JSON_OPT).action(function(name) {
24218
+ const workstream = program.command("workstream").description(
24219
+ "Workstream-level commands. Name them <project>-<purpose> (hail-auth), one per effort; tear down when it ships. Reviews nobody will track belong in scratch delegates, not a workstream."
24220
+ );
24221
+ workstream.command("init <name>").description(
24222
+ "Create the workstream's mux session and register it in the DB. Name it <project>-<purpose>."
24223
+ ).option(...JSON_OPT).action(function(name) {
24058
24224
  const opts = this.opts();
24059
24225
  return handle((db) => cmdInit(db, name, opts), this)();
24060
24226
  });