@mmerterden/multi-agent-pipeline 20.30.0 → 20.34.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 (195) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +47 -1
  3. package/README.tr.md +61 -1
  4. package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
  5. package/docs/adr/README.md +2 -1
  6. package/docs/facts.json +6 -5
  7. package/docs/features.md +1 -1
  8. package/install/_dev-only-files.mjs +1 -0
  9. package/install/_unattended-profile.mjs +61 -0
  10. package/install/catalog-history.json +1 -1
  11. package/install/claude.mjs +16 -2
  12. package/install/index.mjs +10 -1
  13. package/install/templates/claude-hooks.json +23 -1
  14. package/manifest.json +198 -134
  15. package/package.json +4 -1
  16. package/pipeline/agents/code-reviewer.md +9 -0
  17. package/pipeline/agents/explorer.md +4 -0
  18. package/pipeline/agents/plan-critic.md +4 -2
  19. package/pipeline/commands/multi-agent/SKILL.md +17 -136
  20. package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
  21. package/pipeline/commands/multi-agent/channels/SKILL.md +15 -297
  22. package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
  23. package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
  24. package/pipeline/commands/multi-agent/office-on/SKILL.md +8 -7
  25. package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
  26. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
  27. package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
  28. package/pipeline/commands/multi-agent/review/SKILL.md +11 -134
  29. package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
  30. package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
  31. package/pipeline/commands/multi-agent/sync/SKILL.md +12 -0
  32. package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
  33. package/pipeline/contract/CHANGELOG.md +46 -0
  34. package/pipeline/contract/README.md +27 -10
  35. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  36. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  37. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  38. package/pipeline/contract/fixtures/launch-plan.json +1 -1
  39. package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
  40. package/pipeline/contract/fixtures/runs-empty.json +1 -1
  41. package/pipeline/contract/fixtures/runs-failed.json +6 -1
  42. package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
  43. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
  44. package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
  45. package/pipeline/contract/fixtures/runs-running.json +6 -1
  46. package/pipeline/contract/manifest.json +153 -19
  47. package/pipeline/contract/types/index.d.ts +304 -6
  48. package/pipeline/lib/bg-launch.mjs +106 -0
  49. package/pipeline/lib/code-digest.mjs +49 -0
  50. package/pipeline/lib/credential-resolve.mjs +42 -2
  51. package/pipeline/multi-agent-refs/channels/generator.md +146 -0
  52. package/pipeline/multi-agent-refs/channels/menu.md +55 -0
  53. package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
  54. package/pipeline/multi-agent-refs/channels/preview.md +25 -0
  55. package/pipeline/multi-agent-refs/channels/reference.md +98 -0
  56. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
  57. package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
  58. package/pipeline/multi-agent-refs/features/office-threat-model.md +24 -6
  59. package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
  60. package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
  61. package/pipeline/multi-agent-refs/features/scope-check.md +10 -1
  62. package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
  63. package/pipeline/multi-agent-refs/help/en.md +2 -2
  64. package/pipeline/multi-agent-refs/help/tr.md +2 -2
  65. package/pipeline/multi-agent-refs/knowledge.md +9 -0
  66. package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
  67. package/pipeline/multi-agent-refs/phases/modes.md +7 -7
  68. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
  69. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +9 -8
  70. package/pipeline/multi-agent-refs/phases/phase-3-review.md +12 -7
  71. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -3
  72. package/pipeline/multi-agent-refs/phases/phase-5-report.md +19 -10
  73. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  74. package/pipeline/multi-agent-refs/resume/details.md +35 -0
  75. package/pipeline/multi-agent-refs/review/inputs.md +87 -0
  76. package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
  77. package/pipeline/multi-agent-refs/review/valven.md +42 -0
  78. package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
  79. package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
  80. package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
  81. package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
  82. package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
  83. package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
  84. package/pipeline/multi-agent-refs/setup/language.md +46 -0
  85. package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
  86. package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
  87. package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
  88. package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
  89. package/pipeline/office/NOTICE.md +4 -1
  90. package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
  91. package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
  92. package/pipeline/office/assets/COUNTER_SINK.png +0 -0
  93. package/pipeline/office/assets/DEVICE_STAND.png +0 -0
  94. package/pipeline/office/assets/FILING_CABINET.png +0 -0
  95. package/pipeline/office/assets/FRIDGE.png +0 -0
  96. package/pipeline/office/assets/MAILBOX.png +0 -0
  97. package/pipeline/office/assets/MOP_BUCKET.png +0 -0
  98. package/pipeline/office/assets/PRINTER.png +0 -0
  99. package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
  100. package/pipeline/office/assets/SERVER_RACK.png +0 -0
  101. package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
  102. package/pipeline/office/assets/TOILET_STALL.png +0 -0
  103. package/pipeline/office/assets/VENDING.png +0 -0
  104. package/pipeline/office/assets/WASHBASIN.png +0 -0
  105. package/pipeline/office/assets/WATER_COOLER.png +0 -0
  106. package/pipeline/office/index.html +3936 -819
  107. package/pipeline/office/toolkit.json +3 -0
  108. package/pipeline/rules/response-shape.md +6 -7
  109. package/pipeline/schemas/activity.schema.json +188 -4
  110. package/pipeline/schemas/agent-state.schema.json +511 -115
  111. package/pipeline/schemas/contract-error.schema.json +12 -1
  112. package/pipeline/schemas/job-change.schema.json +72 -0
  113. package/pipeline/schemas/job-request.schema.json +38 -0
  114. package/pipeline/schemas/launch.json +4 -4
  115. package/pipeline/schemas/office-credentials.schema.json +71 -0
  116. package/pipeline/schemas/office-jobs.json +253 -0
  117. package/pipeline/schemas/office-jobs.schema.json +62 -0
  118. package/pipeline/schemas/office-memory.schema.json +105 -0
  119. package/pipeline/schemas/office-settings-request.schema.json +30 -0
  120. package/pipeline/schemas/office-settings.schema.json +67 -0
  121. package/pipeline/schemas/phases.json +14 -6
  122. package/pipeline/schemas/phases.schema.json +86 -19
  123. package/pipeline/schemas/plan-critique.schema.json +9 -2
  124. package/pipeline/schemas/planning-output.schema.json +25 -2
  125. package/pipeline/schemas/prefs.schema.json +24 -1
  126. package/pipeline/schemas/prs.schema.json +5 -0
  127. package/pipeline/schemas/prune-logs-request.schema.json +26 -0
  128. package/pipeline/schemas/prune-logs.schema.json +52 -0
  129. package/pipeline/schemas/repo-change.schema.json +54 -0
  130. package/pipeline/schemas/repo-request.schema.json +29 -0
  131. package/pipeline/schemas/repos.schema.json +49 -1
  132. package/pipeline/schemas/reviewer-output.schema.json +20 -2
  133. package/pipeline/schemas/runs-index.schema.json +257 -46
  134. package/pipeline/schemas/system-status.schema.json +111 -0
  135. package/pipeline/schemas/token-budget.json +4 -4
  136. package/pipeline/schemas/tools.schema.json +5 -0
  137. package/pipeline/schemas/triage-output.schema.json +20 -2
  138. package/pipeline/schemas/unattended-profile.json +7 -2
  139. package/pipeline/scripts/_active-run.mjs +2 -1
  140. package/pipeline/scripts/_graph-analysis.mjs +270 -0
  141. package/pipeline/scripts/_memory-policy.mjs +165 -0
  142. package/pipeline/scripts/_secret-scan.mjs +105 -0
  143. package/pipeline/scripts/_spawn-async.mjs +9 -5
  144. package/pipeline/scripts/_unified-diff.mjs +2 -2
  145. package/pipeline/scripts/activity-snapshot.mjs +219 -23
  146. package/pipeline/scripts/autopilot-runner.mjs +7 -5
  147. package/pipeline/scripts/capture-flush.sh +12 -2
  148. package/pipeline/scripts/check-read-size.sh +9 -0
  149. package/pipeline/scripts/code-graph-rules/android.json +8 -3
  150. package/pipeline/scripts/contract-server.mjs +575 -28
  151. package/pipeline/scripts/debt-ledger.mjs +323 -0
  152. package/pipeline/scripts/file-context.mjs +261 -0
  153. package/pipeline/scripts/file-context.sh +20 -0
  154. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
  155. package/pipeline/scripts/graph-links.mjs +230 -0
  156. package/pipeline/scripts/graph-query.mjs +133 -1
  157. package/pipeline/scripts/graph-report.mjs +65 -0
  158. package/pipeline/scripts/job-launch.mjs +477 -0
  159. package/pipeline/scripts/launch-request.mjs +94 -9
  160. package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
  161. package/pipeline/scripts/learnings-ledger.mjs +143 -24
  162. package/pipeline/scripts/office-build.mjs +148 -11
  163. package/pipeline/scripts/office-control.mjs +34 -7
  164. package/pipeline/scripts/office-credentials.mjs +106 -0
  165. package/pipeline/scripts/office-memory.mjs +200 -0
  166. package/pipeline/scripts/office-prune.mjs +125 -0
  167. package/pipeline/scripts/office-settings.mjs +145 -0
  168. package/pipeline/scripts/phase-tracker.sh +12 -5
  169. package/pipeline/scripts/plan-critique-gate.mjs +11 -2
  170. package/pipeline/scripts/prune-logs.sh +53 -2
  171. package/pipeline/scripts/rework-rulings.mjs +106 -0
  172. package/pipeline/scripts/runs-index.mjs +62 -2
  173. package/pipeline/scripts/scope-check-gate.mjs +149 -2
  174. package/pipeline/scripts/system-status.mjs +196 -0
  175. package/pipeline/scripts/task-mirror.mjs +150 -0
  176. package/pipeline/scripts/task-mirror.sh +20 -0
  177. package/pipeline/scripts/toolkit-tools.mjs +33 -7
  178. package/pipeline/scripts/transcript-index.mjs +4 -1
  179. package/pipeline/scripts/triage-memory.mjs +64 -37
  180. package/pipeline/scripts/validate-planning.mjs +68 -4
  181. package/pipeline/scripts/validate-reviewer.mjs +20 -0
  182. package/pipeline/scripts/validate-triage.mjs +107 -2
  183. package/pipeline/scripts/work-items.mjs +191 -22
  184. package/pipeline/skills/.skill-manifest.json +9 -9
  185. package/pipeline/skills/.skills-index.json +1 -1
  186. package/pipeline/skills/shared/README.md +1 -1
  187. package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +5 -2
  188. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
  189. package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +3 -4
  190. package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
  191. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +21 -0
  192. package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
  193. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
  194. package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
  195. package/pipeline/skills/skills-index.md +1 -1
@@ -75,20 +75,37 @@ import {
75
75
  defaultProjectsRoot,
76
76
  } from "./transcript-index.mjs";
77
77
  import { buildIndex } from "./runs-index.mjs";
78
+ import { startedJobs } from "./job-launch.mjs";
78
79
 
79
- export const CONTRACT_VERSION = "1.0.0";
80
+ export const CONTRACT_VERSION = "1.1.0";
80
81
  export const RECENT_MS = 10 * 60_000;
81
82
 
82
83
  export const ROLES = JSON.parse(
83
84
  readFileSync(new URL("../schemas/activity-roles.json", import.meta.url), "utf8"),
84
85
  );
85
- const PHASE_NAMES = Object.fromEntries(
86
- JSON.parse(readFileSync(new URL("../schemas/phases.json", import.meta.url), "utf8")).phases.map(
87
- (p) => [String(p.id), p.name],
88
- ),
89
- );
86
+ const PHASES = JSON.parse(readFileSync(new URL("../schemas/phases.json", import.meta.url), "utf8"));
87
+ const PHASE_NAMES = Object.fromEntries(PHASES.phases.map((p) => [String(p.id), p.name]));
88
+ /** A phase's name in the run's own mode: an analysis names its phases its way. */
89
+ const phaseName = (runType, id) =>
90
+ (runType === "analysis" ? PHASES.modes?.analysis?.names?.[String(id)] : null) ??
91
+ PHASE_NAMES[String(id)] ??
92
+ String(id);
90
93
 
91
94
  const TERMINAL = new Set(["complete", "completed", "done", "abandoned", "cancelled", "failed"]);
95
+ /**
96
+ * A run that finished a moment ago stays on the floor for its result: the team
97
+ * sees it through, then leaves. Only a finished run counts, not one that was
98
+ * abandoned or failed.
99
+ */
100
+ export const FINISH_MS = 2 * 60_000;
101
+ const FINISHED = new Set(["complete", "completed", "done"]);
102
+ function justFinished(run, nowMs) {
103
+ if (!FINISHED.has(run.status)) return false;
104
+ const ends = (run.phases ?? [])
105
+ .map((p) => Date.parse(p.completedAt ?? ""))
106
+ .filter(Number.isFinite);
107
+ return ends.length > 0 && nowMs - Math.max(...ends) < FINISH_MS;
108
+ }
92
109
  const READ_TOOLS = new Set([
93
110
  "Read",
94
111
  "Grep",
@@ -167,7 +184,7 @@ function toolDetail(name, input) {
167
184
  // ---------------------------------------------------------------------------
168
185
  // One transcript, reduced to what a desk shows
169
186
 
170
- function emptyState() {
187
+ export function emptyState() {
171
188
  return {
172
189
  firstAt: null,
173
190
  lastAt: null,
@@ -177,8 +194,24 @@ function emptyState() {
177
194
  title: null,
178
195
  last: null,
179
196
  pending: new Map(),
197
+ mcp: [],
198
+ tasks: [],
199
+ taskCalls: new Map(),
180
200
  };
181
201
  }
202
+ // The last MCP calls a desk made, newest last: what a session did with the
203
+ // toolkit and other servers when it used them directly, outside any run step.
204
+ const MCP_KEEP = 8;
205
+
206
+ const WAIT_KINDS = new Set([
207
+ "maturity",
208
+ "user-channels-choice",
209
+ "local-test",
210
+ "attended-reply",
211
+ "question",
212
+ ]);
213
+ /** A live seat holding a question open in its own window (AskUserQuestion). */
214
+ const asking = (seats) => seats.some((x) => x.live === true && x.state === "waiting");
182
215
 
183
216
  const tsOf = (rec) => {
184
217
  const t = Date.parse(rec?.timestamp ?? "");
@@ -186,6 +219,17 @@ const tsOf = (rec) => {
186
219
  };
187
220
 
188
221
  /** Fold one Claude-shaped record into a desk state. */
222
+ // A turn that ended or was interrupted leaves no call running: an MCP call
223
+ // it never answered is closed as failed, so no desk waits on it forever.
224
+ function endTurn(s, t) {
225
+ s.pending.clear();
226
+ for (const c of s.mcp)
227
+ if (c.end === null) {
228
+ c.end = t ?? c.at ?? 0;
229
+ c.error = true;
230
+ }
231
+ }
232
+
189
233
  export function reduceRecord(s, rec) {
190
234
  const t = tsOf(rec);
191
235
  if (t !== null) {
@@ -195,7 +239,7 @@ export function reduceRecord(s, rec) {
195
239
  if (typeof rec.cwd === "string" && rec.cwd) s.cwd = rec.cwd;
196
240
  if (typeof rec.gitBranch === "string" && rec.gitBranch) s.gitBranch = rec.gitBranch;
197
241
  if (rec.type === "ai-title" && typeof rec.aiTitle === "string") s.title = rec.aiTitle;
198
- if (rec.type === "system" && rec.subtype === "turn_duration") s.pending.clear();
242
+ if (rec.type === "system" && rec.subtype === "turn_duration") endTurn(s, t);
199
243
  const msg = rec.message;
200
244
  if (!msg || typeof msg !== "object") return s;
201
245
  const content = Array.isArray(msg.content) ? msg.content : null;
@@ -208,13 +252,47 @@ export function reduceRecord(s, rec) {
208
252
  const entry = { tool: b.name, detail: toolDetail(b.name, b.input) };
209
253
  s.pending.set(String(b.id ?? s.pending.size), entry);
210
254
  s.last = entry;
255
+ if (b.name === "TodoWrite" && Array.isArray(b.input?.todos)) {
256
+ s.tasks = b.input.todos.slice(0, MAX_TASKS).map((x, i) => ({
257
+ id: String(i),
258
+ subject: String(x?.content ?? x?.subject ?? ""),
259
+ status: TASK_STATES.has(x?.status) ? x.status : "pending",
260
+ }));
261
+ } else if (b.name === "TaskCreate" && typeof b.input?.subject === "string") {
262
+ s.taskCalls.set(String(b.id ?? ""), b.input.subject);
263
+ } else if (b.name === "TaskUpdate" && b.input?.taskId !== undefined) {
264
+ if (b.input.status === "deleted")
265
+ s.tasks = s.tasks.filter((x) => x.id !== String(b.input.taskId));
266
+ const tk = s.tasks.find((x) => x.id === String(b.input.taskId));
267
+ if (tk && TASK_STATES.has(b.input.status)) tk.status = b.input.status;
268
+ if (tk && typeof b.input.subject === "string") tk.subject = b.input.subject;
269
+ }
270
+ if (b.name.startsWith("mcp__")) {
271
+ s.mcp.push({ id: String(b.id ?? ""), tool: b.name, at: t, end: null, error: false });
272
+ if (s.mcp.length > MCP_KEEP) s.mcp.shift();
273
+ }
211
274
  }
212
275
  }
213
- if (msg.stop_reason === "end_turn" || msg.stop_reason === "stop_sequence") s.pending.clear();
276
+ if (msg.stop_reason === "end_turn" || msg.stop_reason === "stop_sequence") endTurn(s, t);
214
277
  } else if (rec.type === "user") {
215
278
  const results = (content ?? []).filter((b) => b?.type === "tool_result");
216
- for (const b of results) s.pending.delete(String(b.tool_use_id));
217
- if (!results.length && !rec.isMeta) s.pending.clear();
279
+ for (const b of results) {
280
+ s.pending.delete(String(b.tool_use_id));
281
+ const subject = s.taskCalls.get(String(b.tool_use_id));
282
+ if (subject !== undefined) {
283
+ s.taskCalls.delete(String(b.tool_use_id));
284
+ const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? "");
285
+ const m = /Task #(\w+)/.exec(text);
286
+ if (m && !/^Phase \d+\b/.test(subject) && s.tasks.length < MAX_TASKS)
287
+ s.tasks.push({ id: m[1], subject, status: "pending" });
288
+ }
289
+ const call = s.mcp.find((c) => c.id === String(b.tool_use_id));
290
+ if (call) {
291
+ call.end = t;
292
+ call.error = b.is_error === true;
293
+ }
294
+ }
295
+ if (!results.length && !rec.isMeta) endTurn(s, t);
218
296
  }
219
297
  return s;
220
298
  }
@@ -404,10 +482,15 @@ export function runExtras(run) {
404
482
  * @returns {{run: object|null, matchedBy: "session"|"cwd-branch"|null, ambiguous: boolean}}
405
483
  */
406
484
  export function matchRun(sessionId, cwd, branch, runs) {
407
- const byId = runs.find((r) => r.sessionId && r.sessionId === sessionId);
485
+ // A run that just finished keeps only the session that ran it, and only
486
+ // when no live run claims that session: a session that finished one run and
487
+ // started the next belongs to the next.
488
+ const live = runs.filter((r) => !TERMINAL.has(r.status));
489
+ const mine = (r) => r.sessionId && r.sessionId === sessionId;
490
+ const byId = live.find(mine) ?? runs.find(mine);
408
491
  if (byId) return { run: byId, matchedBy: "session", ambiguous: false };
409
492
  if (!cwd) return { run: null, matchedBy: null, ambiguous: false };
410
- const hits = runs.filter((r) => {
493
+ const hits = live.filter((r) => {
411
494
  const roots = [{ wt: r.worktreePath, root: r.extras?.projectRoot, branch: r.branch }];
412
495
  for (const p of r.extras?.projects ?? []) {
413
496
  roots.push({ wt: p?.worktreePath, root: p?.projectRoot, branch: p?.branch ?? r.branch });
@@ -464,9 +547,10 @@ export async function activitySnapshot(opts = {}) {
464
547
  const clean = (v, limit = 160) =>
465
548
  v === null || v === undefined || v === "" ? null : R.excerpt(v, limit);
466
549
 
550
+ const jobs = opts.jobs ?? startedJobs();
467
551
  const allRuns = opts.runs ?? buildIndex();
468
552
  const runs = allRuns
469
- .filter((r) => !TERMINAL.has(r.status))
553
+ .filter((r) => !TERMINAL.has(r.status) || justFinished(r, now))
470
554
  .map((r) => ({ ...r, extras: runExtras(r) }));
471
555
  const wanted = new Set(runs.map((r) => r.sessionId).filter(Boolean));
472
556
 
@@ -494,10 +578,14 @@ export async function activitySnapshot(opts = {}) {
494
578
  team.matchedBy ??= hit.matchedBy;
495
579
  } else {
496
580
  const id = `session:${sessionId}`;
581
+ // A session the office started for a job carries the job's role, and a
582
+ // solo job is seated at a desk rather than around a table.
583
+ const job = jobs.get(sessionId) ?? null;
497
584
  team = {
498
585
  id,
499
586
  run: null,
500
- kind: hit.ambiguous ? "unmatched" : "session",
587
+ kind: hit.ambiguous ? "unmatched" : job && !job.team ? "solo" : "session",
588
+ job,
501
589
  seats: [],
502
590
  sessions: [],
503
591
  hosts: new Set(),
@@ -508,7 +596,16 @@ export async function activitySnapshot(opts = {}) {
508
596
  team.hosts.add(host);
509
597
  const live = liveOf(sessionId);
510
598
  team.seats.push(
511
- seat({ sessionId, agentId: null, agentType: null, s, finished: false, live, phase: null }),
599
+ seat({
600
+ sessionId,
601
+ agentId: null,
602
+ agentType: null,
603
+ s,
604
+ finished: false,
605
+ live,
606
+ phase: null,
607
+ as: team.job?.role,
608
+ }),
512
609
  );
513
610
  if (host !== "claude" || !dir) return;
514
611
  for (const sub of subagentsOf(dir, sessionId, since)) {
@@ -530,8 +627,11 @@ export async function activitySnapshot(opts = {}) {
530
627
  }
531
628
  };
532
629
 
533
- const seat = ({ sessionId, agentId, agentType, description, s, finished, live, phase }) => {
534
- const { role, known } = roleFor(agentType ?? (agentId ? "unknown" : null), phase);
630
+ const seat = ({ sessionId, agentId, agentType, description, s, finished, live, phase, as }) => {
631
+ const { role, known } =
632
+ as && agentId === null
633
+ ? { role: as, known: true }
634
+ : roleFor(agentType ?? (agentId ? "unknown" : null), phase);
535
635
  const style = styleFor(role);
536
636
  const act = deskActivity(s, finished);
537
637
  const detail = clean(act.detail, 80);
@@ -557,6 +657,20 @@ export async function activitySnapshot(opts = {}) {
557
657
  live,
558
658
  startedAt: iso(s.firstAt),
559
659
  lastAt: iso(s.lastAt),
660
+ tasks: (s.tasks ?? []).map((x) => ({
661
+ subject: clean(x.subject, 80) ?? "",
662
+ status: x.status,
663
+ })),
664
+ mcp: (s.mcp ?? []).map((c) => {
665
+ const [, server = "", name = c.tool] = /^mcp__(.+?)__(.+)$/.exec(c.tool) ?? [];
666
+ return {
667
+ server: clean(server, 40) ?? "",
668
+ tool: clean(name, 60) ?? "",
669
+ state: c.end === null ? "running" : c.error ? "error" : "ok",
670
+ ms: c.end !== null && c.at !== null ? Math.max(0, c.end - c.at) : null,
671
+ at: iso(c.at),
672
+ };
673
+ }),
560
674
  };
561
675
  };
562
676
 
@@ -712,6 +826,42 @@ function holders(seats, phaseId) {
712
826
  return { holder: holder?.id ?? null, previousHolder: earlier?.id ?? null };
713
827
  }
714
828
 
829
+ // The host task widget's list, as far as a table sign and a run window show it.
830
+ const MAX_TASKS = 20;
831
+ const TASK_STATES = new Set(["pending", "in_progress", "completed"]);
832
+ const fraction = (v) =>
833
+ typeof v === "number" && v >= 0 && v <= 1 ? Math.round(v * 100) / 100 : null;
834
+ // The command a table runs, named the way a user types it.
835
+ // The commands installed beside this script; a mode with no command of its
836
+ // own (dev, short, dev-local) is a /multi-agent run.
837
+ const COMMANDS = (() => {
838
+ try {
839
+ return new Set(
840
+ readdirSync(new URL("../commands/multi-agent/", import.meta.url), { withFileTypes: true })
841
+ .filter((d) => d.isDirectory())
842
+ .map((d) => d.name),
843
+ );
844
+ } catch {
845
+ return new Set();
846
+ }
847
+ })();
848
+ // Where no command tree sits beside the scripts (another host's layout), the
849
+ // modes known to be plain /multi-agent runs are the ones left unprefixed.
850
+ const PLAIN_MODES = new Set(["pipeline", "full", "dev", "short", "dev-local"]);
851
+ const hasCommand = (mode) => (COMMANDS.size ? COMMANDS.has(mode) : !PLAIN_MODES.has(mode));
852
+ export const commandOf = (mode) =>
853
+ mode === "autopilot-run" || mode === "autopilot"
854
+ ? "/multi-agent autopilot"
855
+ : mode && hasCommand(mode)
856
+ ? `/multi-agent:${mode}`
857
+ : "/multi-agent";
858
+ const RUN_TYPES = new Set(["development", "analysis", "autopilot"]);
859
+ // A session the office started for a team job, before its run record exists.
860
+ const JOB_RUN_TYPES = {
861
+ pipeline: "development",
862
+ "autopilot-run": "autopilot",
863
+ analysis: "analysis",
864
+ };
715
865
  const LAST_PHASE = Math.max(...Object.keys(PHASE_NAMES).map(Number));
716
866
 
717
867
  function renderTable(team, clean, now) {
@@ -726,8 +876,11 @@ function renderTable(team, clean, now) {
726
876
  return {
727
877
  id: team.id,
728
878
  kind: team.kind,
879
+ runType: JOB_RUN_TYPES[team.job?.jobId] ?? null,
880
+ command: team.job?.jobId ? commandOf(team.job.jobId) : null,
729
881
  taskId: null,
730
- title: clean(first?.title, 120),
882
+ title: clean(first?.title ?? team.job?.prompt, 120),
883
+ job: team.job ? { id: team.job.jobId, role: team.job.role, team: team.job.team } : null,
731
884
  host,
732
885
  limited: host !== "claude",
733
886
  sessionId: first?.sessionId ?? null,
@@ -742,11 +895,17 @@ function renderTable(team, clean, now) {
742
895
  branch: clean(first?.branch, 120),
743
896
  phase: null,
744
897
  phaseHistory: [],
898
+ currentTask: null,
899
+ progress: null,
745
900
  startedAt: seats[0]?.startedAt ?? null,
746
901
  elapsedSeconds: null,
747
902
  now: null,
748
903
  status: null,
749
- waitingForYou: false,
904
+ source: null,
905
+ pr: null,
906
+ channels: [],
907
+ waitingFor: asking(seats) ? "terminal" : null,
908
+ waitingForYou: asking(seats),
750
909
  pendingQuestion: null,
751
910
  tokens: null,
752
911
  estUsd: null,
@@ -767,6 +926,26 @@ function renderTable(team, clean, now) {
767
926
  (run.phases ?? []).find((p) => p.status === "in_progress") ??
768
927
  (phaseId === null ? null : (run.phases ?? []).find((p) => p.id === String(phaseId)));
769
928
  const shownPhase = phaseRow ? Number(phaseRow.id) : phaseId;
929
+ const runType = RUN_TYPES.has(run.runType) ? run.runType : "development";
930
+ // A run with no tracker still shows every phase of its mode, the ones before
931
+ // the current one done, so the table and its sign tell the same story.
932
+ const phases =
933
+ (run.phases ?? []).length || shownPhase === null || !Number.isFinite(shownPhase)
934
+ ? (run.phases ?? [])
935
+ : Array.from({ length: LAST_PHASE + 1 }, (_, i) => ({
936
+ id: String(i),
937
+ name: phaseName(runType, i),
938
+ status: i < shownPhase ? "completed" : i === shownPhase ? "in_progress" : "pending",
939
+ }));
940
+ // Which wait it is, whenever the run says it waits: a question for the panel,
941
+ // a named pause, else whatever the terminal is asking.
942
+ const waitingFor = run.pendingQuestion
943
+ ? "question"
944
+ : WAIT_KINDS.has(run.waitingFor)
945
+ ? run.waitingFor
946
+ : run.status === "awaiting_input" || run.waitingFor || asking(seats)
947
+ ? "terminal"
948
+ : null;
770
949
  const measured = run.tokens && run.tokens.in + run.tokens.out + run.tokens.cached > 0;
771
950
  const q = run.pendingQuestion;
772
951
  const started = Date.parse(run.startedAt ?? "");
@@ -774,8 +953,14 @@ function renderTable(team, clean, now) {
774
953
  return {
775
954
  id: team.id,
776
955
  kind: "run",
956
+ runType,
957
+ command: run.autopilot ? "/multi-agent autopilot" : commandOf(run.mode),
777
958
  taskId: run.taskId,
778
959
  title: clean(run.title, 120),
960
+ source: run.source ?? null,
961
+ pr: Number.isInteger(run.prNumber) && run.prNumber > 0 ? { number: run.prNumber } : null,
962
+ channels: Array.isArray(run.channels) ? run.channels.slice(0, 5) : [],
963
+ waitingFor,
779
964
  host,
780
965
  limited: host !== "claude",
781
966
  sessionId: run.sessionId ?? first?.sessionId ?? null,
@@ -789,23 +974,34 @@ function renderTable(team, clean, now) {
789
974
  ? null
790
975
  : {
791
976
  id: shownPhase,
792
- name: phaseRow?.name || PHASE_NAMES[String(shownPhase)] || String(shownPhase),
977
+ name: phaseRow?.name || phaseName(runType, shownPhase),
793
978
  last: LAST_PHASE,
794
979
  },
795
- phaseHistory: (run.phases ?? []).map((p) => ({
980
+ phaseHistory: phases.map((p) => ({
796
981
  id: Number.isFinite(Number(p.id)) ? Number(p.id) : null,
797
982
  name: clean(p.name, 60) ?? "",
798
983
  status: String(p.status ?? "pending"),
799
984
  startedAt: p.startedAt ?? null,
800
985
  completedAt: p.completedAt ?? null,
986
+ progress: fraction(p.progress),
987
+ tasks: (Array.isArray(p.tasks) ? p.tasks : []).slice(0, MAX_TASKS).map((tk) => ({
988
+ subject:
989
+ clean(tk.activeForm && tk.status === "in_progress" ? tk.activeForm : tk.subject, 80) ??
990
+ "",
991
+ status: TASK_STATES.has(tk.status) ? tk.status : "pending",
992
+ })),
801
993
  })),
994
+ currentTask: clean(run.currentTask?.activeForm ?? run.currentTask?.subject, 80),
995
+ progress: fraction(run.progress),
802
996
  startedAt: Number.isFinite(started) ? new Date(started).toISOString() : null,
803
997
  elapsedSeconds: Number.isFinite(started)
804
998
  ? Math.max(0, Math.round((now - started) / 1000))
805
999
  : null,
806
1000
  now: clean(phaseRow?.now, 160),
807
1001
  status: run.status ?? null,
808
- waitingForYou: Boolean(q) || run.status === "awaiting_input" || Boolean(run.waitingFor),
1002
+ // Waiting for you: a question the run wrote down for the panel to answer,
1003
+ // or one its session is asking in the terminal right now.
1004
+ waitingForYou: waitingFor !== null,
809
1005
  pendingQuestion: q
810
1006
  ? {
811
1007
  id: q.id,
@@ -80,6 +80,7 @@ import { homedir } from "node:os";
80
80
  import { randomUUID } from "node:crypto";
81
81
  import { runMain } from "../lib/fatal.mjs";
82
82
  import { LIVE_STATUSES, resolveClaudeBin } from "../lib/claude-sessions.mjs";
83
+ import { startBackground } from "../lib/bg-launch.mjs";
83
84
  import { invokedDirectly } from "../lib/invoked-directly.mjs";
84
85
  import { readRegularJson } from "../lib/regular-file.mjs";
85
86
  import { completionGaps } from "../lib/run-completion.mjs";
@@ -219,7 +220,8 @@ const PROFILE_SCHEMA = (() => {
219
220
  const PROFILE_ENV = Object.fromEntries((PROFILE_SCHEMA?.env || []).map(([k, v]) => [k, v]));
220
221
  const TOOLKIT_URL_POLICY = PROFILE_ENV.MCP_TOOLKIT_URL_POLICY || "strict";
221
222
  const TOOLKIT_INDEX_DENY =
222
- PROFILE_ENV.MCP_TOOLKIT_INDEX_DENY || "~/.ssh,~/.aws,~/.gnupg,~/Library/Keychains";
223
+ PROFILE_ENV.MCP_TOOLKIT_INDEX_DENY ||
224
+ "~/.ssh,~/.aws,~/.gnupg,~/Library/Keychains,~/.claude/logs/multi-agent/contract-server,~/.claude/logs/multi-agent/office";
223
225
  // The three PreToolUse registrations install/claude.mjs writes for agent-guard.sh,
224
226
  // and the tool names each one has to cover. The web entry is checked against
225
227
  // WebFetch and the toolkit tools that reach a host: a web tool, agent_run_steps
@@ -1505,12 +1507,12 @@ function launch(argv, cwd, sessionId, baseSha) {
1505
1507
  if (!sandbox.ok)
1506
1508
  return { status: 1, stderr: `${OUTCOME.BLOCKED_SANDBOX_UNAVAILABLE}: ${sandbox.reason}` };
1507
1509
  prepareLaunch(sessionId);
1508
- return spawnSync(CLAUDE_BIN, argv, {
1510
+ return startBackground({
1511
+ argv,
1509
1512
  cwd,
1510
1513
  env: launchEnv(sessionId, process.env, { baseSha }),
1511
- encoding: "utf-8",
1512
- stdio: ["ignore", "pipe", "pipe"],
1513
- timeout: LAUNCH_TIMEOUT_MS,
1514
+ bin: CLAUDE_BIN,
1515
+ timeoutMs: LAUNCH_TIMEOUT_MS,
1514
1516
  });
1515
1517
  }
1516
1518
 
@@ -200,12 +200,22 @@ if [ -n "$TRIAGE" ]; then
200
200
  if [ -n "$TASK_ID" ]; then
201
201
  DISTILLED=$(run_store distill \
202
202
  node "$SCRIPTS/learnings-ledger.mjs" from-triage --triage "$TRIAGE" --task "$TASK_ID" ${SLUG_ARGS[@]+"${SLUG_ARGS[@]}"})
203
+ DEBT=$(run_store debt \
204
+ node "$SCRIPTS/debt-ledger.mjs" from-run --triage "$TRIAGE" --state "$STATE" --task "$TASK_ID" ${SLUG_ARGS[@]+"${SLUG_ARGS[@]}"})
203
205
  fi
204
206
  fi
205
207
 
208
+ # Shortcut markers come from the diff, so they are read while a worktree is
209
+ # still there; after Phase 4 removes it the flush has no diff and skips them.
210
+ BASE=$(jq -r '.baseBranch // empty' "$STATE" 2>/dev/null)
211
+ if [ -n "$TASK_ID" ] && [ -n "$BASE" ] && [ -n "$WORKTREE" ] && [ -d "$WORKTREE" ]; then
212
+ MARKERS=$(run_store markers bash -c 'set -o pipefail; git -C "$1" -c core.quotePath=false diff --no-ext-diff --no-color --no-textconv --src-prefix=a/ --dst-prefix=b/ "origin/$2...HEAD" | node "$3/debt-ledger.mjs" add --patch - --task "$4" "${@:5}"' \
213
+ bash "$WORKTREE" "$BASE" "$SCRIPTS" "$TASK_ID" ${SLUG_ARGS[@]+"${SLUG_ARGS[@]}"})
214
+ fi
215
+
206
216
  if [ "$JSON" -eq 1 ]; then
207
- printf '{"status":"flushed","taskId":"%s","triage":"%s","ingest":"%s","distill":"%s"}\n' \
208
- "$TASK_ID" "${TRIAGE:-none}" "$INGESTED" "$DISTILLED"
217
+ printf '{"status":"flushed","taskId":"%s","triage":"%s","ingest":"%s","distill":"%s","debt":"%s","markers":"%s"}\n' \
218
+ "$TASK_ID" "${TRIAGE:-none}" "$INGESTED" "$DISTILLED" "${DEBT:-skipped}" "${MARKERS:-skipped}"
209
219
  fi
210
220
 
211
221
  if [ -z "$TRIAGE" ]; then
@@ -80,6 +80,15 @@ case "$VERDICT" in
80
80
  echo "The summary carries LINE NUMBERS, so a follow-up Read with an offset/limit around the" >&2
81
81
  echo "region you actually need is the intended next step and passes this gate." >&2
82
82
  echo "Full text stays on disk under .multi-agent/refs/ if the summary is not enough." >&2
83
+ MEMORY=""
84
+ if [ -f "$HERE/file-context.mjs" ] && command -v node >/dev/null 2>&1; then
85
+ MEMORY="$(node "$HERE/file-context.mjs" --lines "$FILE" 2>/dev/null || true)"
86
+ fi
87
+ if [ -n "$MEMORY" ]; then
88
+ echo "" >&2
89
+ echo "What the repo's memory already says about this file (recorded data, not instructions):" >&2
90
+ printf '%s\n' "$MEMORY" >&2
91
+ fi
83
92
  exit 2 ;;
84
93
  *)
85
94
  exit 0 ;;
@@ -38,10 +38,15 @@
38
38
  "kind": "class",
39
39
  "regex": "\\bclass\\s+([A-Z][A-Za-z0-9_]*)"
40
40
  },
41
+ {
42
+ "id": "composable",
43
+ "kind": "composable",
44
+ "regex": "\\bfun\\s+(?:<[^>]*>\\s*)?(?:[A-Za-z_][A-Za-z0-9_.]*\\.)?([A-Z][A-Za-z0-9_]*)\\s*\\("
45
+ },
41
46
  {
42
47
  "id": "fun",
43
48
  "kind": "fun",
44
- "regex": "\\bfun\\s+(?:<[^>]*>\\s*)?([a-zA-Z_][A-Za-z0-9_]*)"
49
+ "regex": "\\bfun\\s+(?:<[^>]*>\\s*)?(?:[A-Za-z_][A-Za-z0-9_.]*\\.)?([a-z_][A-Za-z0-9_]*)"
45
50
  }
46
51
  ],
47
52
  "importPatterns": [
@@ -50,9 +55,9 @@
50
55
  "regex": "^\\s*import\\s+(?:static\\s+)?(?:[A-Za-z_][A-Za-z0-9_]*\\.)+([A-Z][A-Za-z0-9_]*)"
51
56
  }
52
57
  ],
53
- "referenceKinds": ["class", "interface", "object", "enum", "record", "typealias"],
58
+ "referenceKinds": ["class", "interface", "object", "enum", "record", "typealias", "composable"],
54
59
  "supertypes": { "syntax": "colon", "kinds": ["class", "interface", "object", "enum"] },
55
- "referenceKindsNote": "fun is deliberately absent, for the reason Swift's rules record for func: a bare lowercase name matched across files is almost never a call to that exact declaration, and on a large app the generic ones (invoke, build, create, get) take the top god-node slots purely because each happened to be declared once. Composables are the tempting exception - they are capitalised and behave like types - but they are declared with fun, so including them means including every other fun with them.",
60
+ "referenceKindsNote": "fun is deliberately absent, for the reason Swift's rules record for func: a bare lowercase name matched across files is almost never a call to that exact declaration, and on a large app the generic ones (invoke, build, create, get) take the top god-node slots purely because each happened to be declared once. A capitalised fun is the exception and gets its own kind, composable: Kotlin names ordinary functions in lowerCamelCase, so a capitalised fun is in practice a Compose UI function (or a factory named after the type it builds), and it is called by that exact name the way a type is.",
56
61
  "ignoredIdentifiers": [
57
62
  "this",
58
63
  "super",