@mmerterden/multi-agent-pipeline 20.28.0 → 20.30.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 (186) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +19 -6
  3. package/README.tr.md +5 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +7 -7
  7. package/install/catalog-history.json +1 -1
  8. package/install/claude.mjs +122 -1
  9. package/install/codex.mjs +2 -0
  10. package/install/copilot.mjs +13 -0
  11. package/install/templates/claude-hooks.json +30 -1
  12. package/manifest.json +189 -77
  13. package/package.json +2 -1
  14. package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -1
  15. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
  16. package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
  17. package/pipeline/commands/multi-agent/office-on/SKILL.md +50 -0
  18. package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
  19. package/pipeline/commands/multi-agent/resume/SKILL.md +2 -1
  20. package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
  21. package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
  22. package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
  23. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  24. package/pipeline/contract/CHANGELOG.md +62 -0
  25. package/pipeline/contract/README.md +52 -6
  26. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  27. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  28. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  29. package/pipeline/contract/manifest.json +367 -22
  30. package/pipeline/contract/types/index.d.ts +531 -4
  31. package/pipeline/lib/hook-registration.mjs +252 -0
  32. package/pipeline/lib/prefs-value.mjs +44 -0
  33. package/pipeline/lib/run-completion.mjs +148 -0
  34. package/pipeline/multi-agent-refs/analysis/render.md +4 -4
  35. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -4
  36. package/pipeline/multi-agent-refs/features/doctor.md +11 -0
  37. package/pipeline/multi-agent-refs/features/office-threat-model.md +91 -0
  38. package/pipeline/multi-agent-refs/help/en.md +3 -0
  39. package/pipeline/multi-agent-refs/help/tr.md +3 -0
  40. package/pipeline/multi-agent-refs/phases/operations.md +17 -0
  41. package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
  42. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
  43. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +1 -1
  44. package/pipeline/multi-agent-refs/phases/phase-3-review.md +2 -2
  45. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -6
  46. package/pipeline/multi-agent-refs/phases/phase-5-report.md +5 -4
  47. package/pipeline/office/NOTICE.md +42 -0
  48. package/pipeline/office/assets/BIN.png +0 -0
  49. package/pipeline/office/assets/BOOKSHELF.png +0 -0
  50. package/pipeline/office/assets/CACTUS.png +0 -0
  51. package/pipeline/office/assets/CLOCK.png +0 -0
  52. package/pipeline/office/assets/COFFEE.png +0 -0
  53. package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
  54. package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
  55. package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
  56. package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
  57. package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
  58. package/pipeline/office/assets/DESK_FRONT.png +0 -0
  59. package/pipeline/office/assets/DESK_SIDE.png +0 -0
  60. package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
  61. package/pipeline/office/assets/HANGING_PLANT.png +0 -0
  62. package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
  63. package/pipeline/office/assets/LARGE_PLANT.png +0 -0
  64. package/pipeline/office/assets/PC_BACK.png +0 -0
  65. package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
  66. package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
  67. package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
  68. package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
  69. package/pipeline/office/assets/PC_SIDE.png +0 -0
  70. package/pipeline/office/assets/PLANT.png +0 -0
  71. package/pipeline/office/assets/PLANT_2.png +0 -0
  72. package/pipeline/office/assets/POT.png +0 -0
  73. package/pipeline/office/assets/SMALL_PAINTING.png +0 -0
  74. package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
  75. package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
  76. package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
  77. package/pipeline/office/assets/SOFA_BACK.png +0 -0
  78. package/pipeline/office/assets/SOFA_FRONT.png +0 -0
  79. package/pipeline/office/assets/SOFA_SIDE.png +0 -0
  80. package/pipeline/office/assets/TABLE_FRONT.png +0 -0
  81. package/pipeline/office/assets/WHITEBOARD.png +0 -0
  82. package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
  83. package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
  84. package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
  85. package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
  86. package/pipeline/office/assets/char_0.png +0 -0
  87. package/pipeline/office/assets/char_1.png +0 -0
  88. package/pipeline/office/assets/char_2.png +0 -0
  89. package/pipeline/office/assets/char_3.png +0 -0
  90. package/pipeline/office/assets/char_4.png +0 -0
  91. package/pipeline/office/assets/char_5.png +0 -0
  92. package/pipeline/office/assets/floor_0.png +0 -0
  93. package/pipeline/office/assets/floor_1.png +0 -0
  94. package/pipeline/office/assets/floor_2.png +0 -0
  95. package/pipeline/office/assets/floor_3.png +0 -0
  96. package/pipeline/office/assets/floor_4.png +0 -0
  97. package/pipeline/office/assets/floor_5.png +0 -0
  98. package/pipeline/office/assets/floor_6.png +0 -0
  99. package/pipeline/office/assets/floor_7.png +0 -0
  100. package/pipeline/office/assets/floor_8.png +0 -0
  101. package/pipeline/office/assets/wall_0.png +0 -0
  102. package/pipeline/office/index.html +5319 -0
  103. package/pipeline/preferences-template.json +7 -0
  104. package/pipeline/rules/figma-pipeline.md +2 -0
  105. package/pipeline/schemas/activity-chat.schema.json +78 -0
  106. package/pipeline/schemas/activity-roles.json +48 -0
  107. package/pipeline/schemas/activity-roles.schema.json +49 -0
  108. package/pipeline/schemas/activity.schema.json +429 -0
  109. package/pipeline/schemas/agent-state.schema.json +20 -5
  110. package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
  111. package/pipeline/schemas/autopilot-on.schema.json +111 -0
  112. package/pipeline/schemas/contract-error.schema.json +33 -3
  113. package/pipeline/schemas/issue-maturity.schema.json +68 -0
  114. package/pipeline/schemas/ledger.schema.json +202 -0
  115. package/pipeline/schemas/permissions.schema.json +47 -0
  116. package/pipeline/schemas/phases.json +3 -2
  117. package/pipeline/schemas/phases.schema.json +5 -1
  118. package/pipeline/schemas/prefs.schema.json +56 -0
  119. package/pipeline/schemas/prs.schema.json +73 -0
  120. package/pipeline/schemas/review-verdict-request.schema.json +20 -0
  121. package/pipeline/schemas/review-verdict.schema.json +62 -0
  122. package/pipeline/schemas/routine-delete.schema.json +59 -0
  123. package/pipeline/schemas/routines.schema.json +38 -0
  124. package/pipeline/schemas/run-evidence.schema.json +29 -0
  125. package/pipeline/schemas/run-results.schema.json +218 -0
  126. package/pipeline/schemas/runs-index.schema.json +1 -1
  127. package/pipeline/schemas/service-read.schema.json +58 -0
  128. package/pipeline/schemas/stack-change.schema.json +82 -0
  129. package/pipeline/schemas/stack-request.schema.json +33 -0
  130. package/pipeline/schemas/stack.schema.json +59 -0
  131. package/pipeline/schemas/steer-request.schema.json +25 -0
  132. package/pipeline/schemas/steer.schema.json +59 -0
  133. package/pipeline/schemas/tool-call-request.schema.json +19 -0
  134. package/pipeline/schemas/tool-call.schema.json +47 -0
  135. package/pipeline/schemas/tools.schema.json +60 -0
  136. package/pipeline/scripts/_active-run.mjs +158 -0
  137. package/pipeline/scripts/_autopilot-outcomes.mjs +1 -0
  138. package/pipeline/scripts/_cost.mjs +66 -1
  139. package/pipeline/scripts/_guard_rules.py +605 -0
  140. package/pipeline/scripts/_office-smoke-lib.sh +115 -0
  141. package/pipeline/scripts/_spawn-async.mjs +73 -0
  142. package/pipeline/scripts/_transcript-tail.mjs +274 -0
  143. package/pipeline/scripts/activity-snapshot.mjs +1027 -0
  144. package/pipeline/scripts/agent-guard.py +118 -7
  145. package/pipeline/scripts/agent-guard.sh +50 -3
  146. package/pipeline/scripts/argv-secret-scan.mjs +114 -0
  147. package/pipeline/scripts/autopilot-control.mjs +214 -9
  148. package/pipeline/scripts/autopilot-runner.mjs +8 -2
  149. package/pipeline/scripts/confirm-command.mjs +70 -0
  150. package/pipeline/scripts/contract-server.mjs +1178 -48
  151. package/pipeline/scripts/cost-analyze.mjs +2 -53
  152. package/pipeline/scripts/cost-table.json +1 -0
  153. package/pipeline/scripts/doctor.mjs +54 -0
  154. package/pipeline/scripts/evidence-gate.mjs +8 -4
  155. package/pipeline/scripts/gate-ledger.mjs +163 -7
  156. package/pipeline/scripts/office-build.mjs +89 -0
  157. package/pipeline/scripts/office-control.mjs +383 -0
  158. package/pipeline/scripts/office-ledger.mjs +409 -0
  159. package/pipeline/scripts/plan-coverage-gate.mjs +9 -8
  160. package/pipeline/scripts/pre-commit-check.sh +12 -0
  161. package/pipeline/scripts/routine-registry.mjs +42 -6
  162. package/pipeline/scripts/run-results.mjs +373 -0
  163. package/pipeline/scripts/run-scope-guard.mjs +188 -0
  164. package/pipeline/scripts/run-scope-guard.sh +23 -0
  165. package/pipeline/scripts/run-stop-guard.mjs +246 -0
  166. package/pipeline/scripts/run-stop-guard.sh +25 -0
  167. package/pipeline/scripts/service-read.mjs +244 -0
  168. package/pipeline/scripts/stack-toggle.mjs +285 -0
  169. package/pipeline/scripts/steer-run.mjs +97 -0
  170. package/pipeline/scripts/toolkit-tools.mjs +599 -0
  171. package/pipeline/scripts/uninstall.mjs +4 -0
  172. package/pipeline/scripts/validate-state.mjs +26 -0
  173. package/pipeline/scripts/work-items.mjs +304 -0
  174. package/pipeline/scripts/write-state.mjs +114 -0
  175. package/pipeline/skills/.skill-manifest.json +19 -7
  176. package/pipeline/skills/.skills-index.json +34 -1
  177. package/pipeline/skills/shared/README.md +6 -3
  178. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
  179. package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
  180. package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +22 -0
  181. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
  182. package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
  183. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
  184. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
  185. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  186. package/pipeline/skills/skills-index.md +4 -1
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file work-items.mjs - the work list and the PR list a client shows next
4
+ * to the run list, built on what the pipeline already uses:
5
+ *
6
+ * maturity <key> lib/issue-fetcher.sh <key>, the fetch and maturity score
7
+ * /multi-agent:jira, /multi-agent:issue and Phase 0 run;
8
+ * title, type, status, score, blockers, warnings, summary
9
+ * and a description excerpt, scrubbed
10
+ * prs open pull requests on GitHub where you are asked to
11
+ * review or are the author (`gh search prs`, the queries
12
+ * /multi-agent:review lists). Bitbucket is reported as a
13
+ * source that is not implemented, never as an empty list
14
+ * verdict <task> the review verdict of a finished /multi-agent:review run
15
+ * whose every finding was triaged (accepted, rejected or
16
+ * deferred); a running review or one with a pending
17
+ * finding is refused. The preview carries findingsDigest,
18
+ * a hash of the findings the decision was made from, so a
19
+ * confirmation can be refused when they changed since:
20
+ * `preview` computes it the way lib/post-pr-review.sh does
21
+ * (approve when no accepted blocking or important finding,
22
+ * else needs-work) without posting; `post` runs
23
+ * post-pr-review.sh itself, which posts the inline comments
24
+ * and the verdict with the user's own gh or Bitbucket
25
+ * credential
26
+ *
27
+ * The scripts run as child processes without blocking the caller's event
28
+ * loop.
29
+ *
30
+ * The list of assigned issues is issues.mjs (`GET /v1/issues`); nothing here
31
+ * fetches it a second time. Credentials are only ever read by the scripts
32
+ * above through credential-store.sh; no value reaches this process's output.
33
+ *
34
+ * Usage:
35
+ * work-items.mjs maturity <key>
36
+ * work-items.mjs prs
37
+ * work-items.mjs verdict <task-id> [--post]
38
+ *
39
+ * MA_GH_BIN stands in for gh, MA_ISSUE_FETCHER and MA_POST_PR_REVIEW for the
40
+ * two scripts (tests).
41
+ *
42
+ * Exit: 0 printed, 1 refused, 2 usage, 3 no such run or issue.
43
+ *
44
+ * @module pipeline/scripts/work-items
45
+ */
46
+
47
+ import { createHash } from "node:crypto";
48
+ import { existsSync } from "node:fs";
49
+ import { dirname, join } from "node:path";
50
+ import { fileURLToPath } from "node:url";
51
+ import { scrub } from "./_redact.mjs";
52
+ import { selectRuns } from "./runs-index.mjs";
53
+ import { createRedactor } from "./transcript-index.mjs";
54
+ import { spawnAsync } from "./_spawn-async.mjs";
55
+ import { readRegularJson } from "../lib/regular-file.mjs";
56
+ import { runMain } from "../lib/fatal.mjs";
57
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
58
+
59
+ export const CONTRACT_VERSION = "1.0.0";
60
+ const HERE = dirname(fileURLToPath(import.meta.url));
61
+ /** A Jira key, `owner/repo#N` or `#N`: the structured inputs launch accepts too. */
62
+ export const ISSUE_KEY =
63
+ /^(?:[A-Z][A-Z0-9_]{0,15}-[0-9]{1,9}|[A-Za-z0-9._-]{1,100}\/[A-Za-z0-9._-]{1,100}#[0-9]{1,9}|#[0-9]{1,9})$/;
64
+ const TIMEOUT_MS = 60_000;
65
+
66
+ export class WorkError extends Error {
67
+ constructor(code, message) {
68
+ super(message);
69
+ this.code = code;
70
+ }
71
+ }
72
+
73
+ const libScript = (env, name, override) =>
74
+ env[override] ||
75
+ [join(HERE, "..", "lib", name), join(HERE, "..", "..", "lib", name)].find((p) => existsSync(p));
76
+
77
+ /**
78
+ * The maturity check for one issue, from issue-fetcher.sh.
79
+ *
80
+ * @returns {{contractVersion, key, title, type, status, kind, maturity, description}}
81
+ */
82
+ export async function issueMaturity(key, env = process.env) {
83
+ if (typeof key !== "string" || !ISSUE_KEY.test(key)) {
84
+ throw new WorkError("bad-key", "the key is not a Jira key, owner/repo#N or #N");
85
+ }
86
+ const script = libScript(env, "issue-fetcher.sh", "MA_ISSUE_FETCHER");
87
+ if (!script) throw new WorkError("not-installed", "issue-fetcher.sh is not installed");
88
+ const r = await spawnAsync("bash", [script, key], { env, timeoutMs: TIMEOUT_MS });
89
+ let d;
90
+ try {
91
+ d = JSON.parse(r.stdout);
92
+ } catch {
93
+ throw new WorkError("fetch-failed", "the issue could not be fetched");
94
+ }
95
+ if (!d || typeof d !== "object")
96
+ throw new WorkError("fetch-failed", "the issue could not be fetched");
97
+ const R = createRedactor();
98
+ const m = d.maturity && typeof d.maturity === "object" ? d.maturity : {};
99
+ const codes = (v) => (Array.isArray(v) ? v.map((x) => String(x?.code ?? x)).slice(0, 20) : []);
100
+ return {
101
+ contractVersion: CONTRACT_VERSION,
102
+ key,
103
+ kind: ["jira", "github", "freetext"].includes(d.kind) ? d.kind : "freetext",
104
+ title: R.excerpt(d.title ?? "", 200),
105
+ type: d.type ? String(d.type) : null,
106
+ status: d.status ? String(d.status) : null,
107
+ maturity: {
108
+ score: Number.isFinite(m.score) ? m.score : null,
109
+ blockers: codes(m.blockers),
110
+ warnings: codes(m.warnings),
111
+ summary: m.summary ? R.excerpt(m.summary, 400) : null,
112
+ },
113
+ description: d.description ? R.excerpt(d.description, 2000) : null,
114
+ };
115
+ }
116
+
117
+ async function ghSearch(env, role) {
118
+ const gh = env.MA_GH_BIN || "gh";
119
+ const flag = role === "reviewer" ? "--review-requested=@me" : "--author=@me";
120
+ const r = await spawnAsync(
121
+ gh,
122
+ [
123
+ "search",
124
+ "prs",
125
+ "--state",
126
+ "open",
127
+ flag,
128
+ "--json",
129
+ "number,title,url,repository,author,updatedAt",
130
+ "--limit",
131
+ "50",
132
+ ],
133
+ { env, timeoutMs: TIMEOUT_MS },
134
+ );
135
+ if (r.error || r.status !== 0) return { ok: false, items: [] };
136
+ try {
137
+ const rows = JSON.parse(r.stdout);
138
+ return {
139
+ ok: true,
140
+ items: (Array.isArray(rows) ? rows : []).map((p) => ({
141
+ source: "github",
142
+ repo: String(p?.repository?.nameWithOwner ?? p?.repository?.name ?? ""),
143
+ number: Number(p?.number) || 0,
144
+ title: scrub(String(p?.title ?? "")),
145
+ url: typeof p?.url === "string" ? p.url : null,
146
+ author: p?.author?.login ? String(p.author.login) : null,
147
+ updatedAt: typeof p?.updatedAt === "string" ? p.updatedAt : null,
148
+ role,
149
+ })),
150
+ };
151
+ } catch {
152
+ return { ok: false, items: [] };
153
+ }
154
+ }
155
+
156
+ /** Open pull requests on every host the pipeline can list, one list. */
157
+ export async function pullRequests({ env = process.env, redact = false } = {}) {
158
+ const [reviewer, author] = await Promise.all([
159
+ ghSearch(env, "reviewer"),
160
+ ghSearch(env, "author"),
161
+ ]);
162
+ const seen = new Set();
163
+ const items = [];
164
+ for (const p of [...reviewer.items, ...author.items]) {
165
+ const k = `${p.repo}#${p.number}`;
166
+ if (seen.has(k)) continue;
167
+ seen.add(k);
168
+ items.push(redact ? { ...p, url: null } : p);
169
+ }
170
+ items.sort((a, b) => ((a.updatedAt ?? "") < (b.updatedAt ?? "") ? 1 : -1));
171
+ return {
172
+ contractVersion: CONTRACT_VERSION,
173
+ items,
174
+ sources: [
175
+ reviewer.ok || author.ok
176
+ ? { source: "github", ok: true, count: items.length }
177
+ : { source: "github", ok: false, count: 0, reason: "gh could not list pull requests" },
178
+ { source: "bitbucket", ok: false, count: 0, reason: "not-implemented" },
179
+ ],
180
+ };
181
+ }
182
+
183
+ function stateOf(taskId) {
184
+ const { found, records } = selectRuns({ taskId });
185
+ if (!found || !records.length) throw new WorkError("no-run", "no such run");
186
+ const dir = records[0].dir;
187
+ for (const p of [join(dir, "agent-state.json"), join(dir, "artifacts", "agent-state.json")]) {
188
+ if (existsSync(p)) return { taskId: records[0].taskId, state: readRegularJson(p, null) ?? {} };
189
+ }
190
+ throw new WorkError("no-review", "the run has no review to publish");
191
+ }
192
+
193
+ const FINISHED = new Set(["complete", "completed", "done"]);
194
+ const TRIAGED = new Set(["accepted", "rejected", "deferred"]);
195
+
196
+ /** A hash of the findings a decision is made from, in their recorded order. */
197
+ export function findingsDigest(findings) {
198
+ const rows = findings.map((f) => [
199
+ f?.status ?? null,
200
+ f?.severity ?? null,
201
+ f?.file ?? null,
202
+ Number.isInteger(f?.line) ? f.line : null,
203
+ f?.issue ?? f?.title ?? null,
204
+ ]);
205
+ return createHash("sha256").update(JSON.stringify(rows)).digest("hex");
206
+ }
207
+
208
+ /**
209
+ * The verdict post-pr-review.sh would post for a review run, without posting.
210
+ * Refused when the run has no review input, its findings were never written,
211
+ * the run has not finished, or any finding is still untriaged.
212
+ */
213
+ export function reviewVerdictPreview(taskId) {
214
+ const { taskId: id, state } = stateOf(taskId);
215
+ const review = state.review;
216
+ if (!review || typeof review !== "object" || !review.input) {
217
+ throw new WorkError("no-review", "the run has no review to publish");
218
+ }
219
+ if (!Array.isArray(review.findings)) {
220
+ throw new WorkError("no-findings", "the review has not recorded its findings yet");
221
+ }
222
+ if (!FINISHED.has(state.status)) {
223
+ throw new WorkError("review-incomplete", "the review run has not finished");
224
+ }
225
+ const untriaged = review.findings.filter((f) => !TRIAGED.has(f?.status)).length;
226
+ if (untriaged) {
227
+ throw new WorkError(
228
+ "review-incomplete",
229
+ `${untriaged} finding(s) are not triaged yet (accepted, rejected or deferred)`,
230
+ );
231
+ }
232
+ const accepted = review.findings.filter((f) => f?.status === "accepted");
233
+ const blocking = accepted.filter((f) => f?.severity === "blocking").length;
234
+ const important = accepted.filter((f) => f?.severity === "important").length;
235
+ return {
236
+ contractVersion: CONTRACT_VERSION,
237
+ taskId: id,
238
+ provider: ["github", "bitbucket-server"].includes(review.input.provider)
239
+ ? review.input.provider
240
+ : null,
241
+ prNumber: Number.isInteger(review.input.prNumber) ? review.input.prNumber : null,
242
+ decision: blocking === 0 && important === 0 ? "approve" : "needs-work",
243
+ acceptedBlocking: blocking,
244
+ acceptedImportant: important,
245
+ findings: review.findings.length,
246
+ findingsDigest: findingsDigest(review.findings),
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Post the review and its verdict through post-pr-review.sh, as
252
+ * /multi-agent:review does. With `expected` ({decision, findingsDigest} from
253
+ * the preview a person confirmed), refused with `changed` when either differs
254
+ * now.
255
+ */
256
+ export async function postReviewVerdict(taskId, env = process.env, expected = null) {
257
+ const preview = reviewVerdictPreview(taskId);
258
+ if (
259
+ expected &&
260
+ (expected.decision !== preview.decision || expected.findingsDigest !== preview.findingsDigest)
261
+ ) {
262
+ throw new WorkError("changed", "the findings or the decision changed since the preview");
263
+ }
264
+ const script = libScript(env, "post-pr-review.sh", "MA_POST_PR_REVIEW");
265
+ if (!script) throw new WorkError("not-installed", "post-pr-review.sh is not installed");
266
+ const r = await spawnAsync("bash", [script, preview.taskId], {
267
+ env,
268
+ timeoutMs: 5 * TIMEOUT_MS,
269
+ maxBytes: 1024 * 1024,
270
+ });
271
+ if (r.status !== 0) {
272
+ throw new WorkError("post-failed", `post-pr-review.sh exited ${r.status ?? "on a signal"}`);
273
+ }
274
+ return { ...preview, posted: true };
275
+ }
276
+
277
+ async function main(argv) {
278
+ const [cmd, arg, flag] = argv;
279
+ try {
280
+ let out;
281
+ if (cmd === "maturity" && arg) out = await issueMaturity(arg);
282
+ else if (cmd === "prs" && !arg) out = await pullRequests();
283
+ else if (cmd === "verdict" && arg && (flag === undefined || flag === "--post")) {
284
+ out = flag ? await postReviewVerdict(arg) : reviewVerdictPreview(arg);
285
+ } else {
286
+ process.stderr.write(
287
+ "usage: work-items.mjs maturity <key> | prs | verdict <task-id> [--post]\n",
288
+ );
289
+ return 2;
290
+ }
291
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
292
+ return 0;
293
+ } catch (err) {
294
+ if (!(err instanceof WorkError)) throw err;
295
+ process.stderr.write(`work-items: ${err.message}\n`);
296
+ return err.code === "no-run" ? 3 : 1;
297
+ }
298
+ }
299
+
300
+ if (invokedDirectly(import.meta.url)) {
301
+ runMain("work-items", async () => {
302
+ process.exitCode = await main(process.argv.slice(2));
303
+ });
304
+ }
@@ -32,6 +32,9 @@
32
32
  * 4 - the lock was taken from us mid-write; nothing was written, retry
33
33
  * 3 - I/O error (disk full, permission denied, parent dir missing)
34
34
  * 5 - --if-rev did not match; the record moved, nothing was written
35
+ * 6 - the write moves the run into `status: complete` without the evidence
36
+ * lib/run-completion.mjs requires (last phase of the mode, Phase 4
37
+ * evidence, a plan-coverage verdict); nothing was written
35
38
  *
36
39
  * Tunables (env):
37
40
  * WRITE_STATE_LOCK_TIMEOUT_MS acquire window before exit 2 (default 15000)
@@ -67,6 +70,7 @@ import { dirname, basename } from "path";
67
70
  import { Buffer } from "node:buffer";
68
71
  import { runMain } from "../lib/fatal.mjs";
69
72
  import { invokedDirectly } from "../lib/invoked-directly.mjs";
73
+ import { completionGaps } from "../lib/run-completion.mjs";
70
74
 
71
75
  /**
72
76
  * @typedef {{ replace: boolean, path: string, ifRev: number|null }} ParsedArgs
@@ -320,6 +324,104 @@ function undeclaredKeys(payload, current) {
320
324
  );
321
325
  }
322
326
 
327
+ const COMPLETE = new Set(["complete", "completed"]);
328
+ const STOP_GUARD_HALT = /^\d+:stop-guard:/;
329
+
330
+ /**
331
+ * Lift the halt run-stop-guard.mjs records after its repeat ceiling once the
332
+ * run advances a phase. That halt says only "the session kept stopping"; a
333
+ * later phase transition is the run going on, and leaving the halt in place
334
+ * would show a moving run as paused and make every later Stop let it through.
335
+ * A halt any other step recorded, or a write that sets status or haltReason
336
+ * itself, is left alone.
337
+ *
338
+ * @param {object} next
339
+ * @param {object} current
340
+ * @param {unknown} payload
341
+ */
342
+ export function clearStopGuardHalt(next, current, payload) {
343
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
344
+ if (Object.hasOwn(payload, "status") || Object.hasOwn(payload, "haltReason")) return;
345
+ if (typeof current?.haltReason !== "string" || !STOP_GUARD_HALT.test(current.haltReason)) return;
346
+ const was = Number(current.currentPhase);
347
+ const now = Number(payload.currentPhase);
348
+ if (!Number.isInteger(now) || !(now > (Number.isInteger(was) ? was : -1))) return;
349
+ next.haltReason = null;
350
+ next.status = "in_progress";
351
+ }
352
+
353
+ /**
354
+ * Clear a wait the run has moved past. A write that advances `currentPhase`
355
+ * without setting `waitingFor` itself ends any wait recorded in the earlier
356
+ * phase, so a clear the step forgot cannot keep the run marked as waiting
357
+ * (and the stop guard off) for the rest of the run. `question` is left to
358
+ * answer-question.mjs, which clears it together with its pendingQuestion.
359
+ *
360
+ * @param {object} next
361
+ * @param {object} current
362
+ * @param {unknown} payload
363
+ */
364
+ export function clearPassedWait(next, current, payload) {
365
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
366
+ if (Object.hasOwn(payload, "waitingFor")) return;
367
+ if (typeof current?.waitingFor !== "string" || current.waitingFor === "question") return;
368
+ const was = Number(current.currentPhase);
369
+ const now = Number(payload.currentPhase);
370
+ if (!Number.isInteger(now) || !(now > (Number.isInteger(was) ? was : -1))) return;
371
+ next.waitingFor = null;
372
+ }
373
+
374
+ /**
375
+ * The refusal message for a write that moves the run into `complete` without
376
+ * the evidence lib/run-completion.mjs asks for, or null.
377
+ *
378
+ * Only the transition is judged: a merge that sets `status: complete`, or a
379
+ * replace whose document is complete over one that was not. A later write to
380
+ * an already complete run that does not touch `status` goes through, so the
381
+ * report steps after the transition keep working.
382
+ *
383
+ * @param {object} next
384
+ * @param {unknown} payload
385
+ * @param {boolean} replace
386
+ * @param {string|null} diskStatus
387
+ * @returns {string|null}
388
+ */
389
+ export function completionRefusal(next, payload, replace, diskStatus) {
390
+ if (!COMPLETE.has(next?.status)) return null;
391
+ const entering = replace
392
+ ? !COMPLETE.has(diskStatus)
393
+ : Boolean(payload && typeof payload === "object" && COMPLETE.has(payload.status));
394
+ if (!entering) return null;
395
+ const gaps = completionGaps(next);
396
+ if (!gaps.length) return null;
397
+ return `write-state: refusing status "${next.status}": ${gaps.join("; ")}. Nothing was written. Finish the missing step, or record a halt (status "paused" with a haltReason).`;
398
+ }
399
+
400
+ /**
401
+ * Bind the run to the host session that drives it.
402
+ *
403
+ * `hostSessionId` is the Claude Code session (CLAUDE_CODE_SESSION_ID in the
404
+ * Bash environment) that made the run's last phase transition. The Stop hook
405
+ * (run-stop-guard.mjs) and the run-scoped PreToolUse guards read it to find
406
+ * the run a hook call belongs to. Stamped only when the write creates the
407
+ * document or sets `currentPhase`: those are the driver's writes, while a
408
+ * second session editing a running task (`/multi-agent:steer`) sets neither
409
+ * and so does not take the run over.
410
+ *
411
+ * @param {object} next
412
+ * @param {unknown} payload
413
+ * @param {boolean} existed
414
+ * @param {Record<string,string|undefined>} [env]
415
+ */
416
+ export function stampHostSession(next, payload, existed, env = process.env) {
417
+ const sid = env.CLAUDE_CODE_SESSION_ID;
418
+ if (typeof sid !== "string" || !sid.trim()) return;
419
+ const transition =
420
+ payload && typeof payload === "object" && Object.hasOwn(payload, "currentPhase");
421
+ if (existed && !transition) return;
422
+ next.hostSessionId = sid.trim();
423
+ }
424
+
323
425
  /**
324
426
  * @param {object} base
325
427
  * @param {object} patch
@@ -397,6 +499,8 @@ async function main() {
397
499
  }
398
500
  }
399
501
  next = deepMerge(current, payload);
502
+ clearStopGuardHalt(next, current, payload);
503
+ clearPassedWait(next, current, payload);
400
504
  }
401
505
 
402
506
  // A state document is an object. Anything else cannot carry `rev`, and
@@ -415,9 +519,11 @@ async function main() {
415
519
  // would tell the next reader that nothing changed.
416
520
  let priorRev = Number.isInteger(next.rev) ? next.rev : 0;
417
521
  let diskRev = null;
522
+ let diskStatus = null;
418
523
  if (existsSync(path)) {
419
524
  try {
420
525
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
526
+ diskStatus = parsed?.status ?? null;
421
527
  if (Number.isInteger(parsed?.rev)) {
422
528
  diskRev = parsed.rev;
423
529
  priorRev = Math.max(priorRev, parsed.rev);
@@ -448,7 +554,15 @@ async function main() {
448
554
  }
449
555
  }
450
556
 
557
+ const refusal = completionRefusal(next, payload, replace, diskStatus);
558
+ if (refusal) {
559
+ console.error(refusal);
560
+ releaseLock(path);
561
+ process.exit(6);
562
+ }
563
+
451
564
  next.rev = priorRev + 1;
565
+ stampHostSession(next, payload, fileExistedBefore);
452
566
 
453
567
  const unknown = undeclaredKeys(payload, replace ? {} : current);
454
568
  if (unknown.length) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-24T11:39:37Z",
4
- "skillCount": 234,
3
+ "generatedAt": "2026-10-24T21:22:00Z",
4
+ "skillCount": 237,
5
5
  "entries": [
6
6
  {
7
7
  "path": "shared/core/apple-archive-compliance/SKILL.md",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  {
31
31
  "path": "shared/core/multi-agent-autopilot-on/SKILL.md",
32
- "sha256": "d4f23f6f7bb493a4efa2cfa252fab50b22b6506aeaf31985db4498b5f010c8c8"
32
+ "sha256": "7e4fbc84ed2bac8ba5fae1b9384ffbd6a40421a3df7bc1a63ef8f0f7fe6a3e55"
33
33
  },
34
34
  {
35
35
  "path": "shared/core/multi-agent-autopilot-status/SKILL.md",
@@ -127,6 +127,18 @@
127
127
  "path": "shared/core/multi-agent-model/SKILL.md",
128
128
  "sha256": "57ba710657d74176b0f19d4a97d3183df1f7b0c056eb1eef2792308914be836c"
129
129
  },
130
+ {
131
+ "path": "shared/core/multi-agent-office-off/SKILL.md",
132
+ "sha256": "316c7f1cd910e76096301330cac7e02f1a2b28e66cda88e9f8bb966a14553e9e"
133
+ },
134
+ {
135
+ "path": "shared/core/multi-agent-office-on/SKILL.md",
136
+ "sha256": "f1841f3092275e54b669a2701796c187ecaa16d71b0cd4311915218319d6950c"
137
+ },
138
+ {
139
+ "path": "shared/core/multi-agent-office-status/SKILL.md",
140
+ "sha256": "fba0513f8941c026172ea0ce76cbc76d88a5e813fd713d68a1377eb5a4204c86"
141
+ },
130
142
  {
131
143
  "path": "shared/core/multi-agent-postmortem/SKILL.md",
132
144
  "sha256": "41edd907cbaae1523eab1cd716390c9e9cd889574141cb88c33966e221b0987d"
@@ -213,7 +225,7 @@
213
225
  },
214
226
  {
215
227
  "path": "shared/core/multi-agent-serve/SKILL.md",
216
- "sha256": "90e4dd7225e10e670118ed05d0c8a9e346112c4bbbdb24a7fe7c5c334a83e848"
228
+ "sha256": "82ba766a7b23285f1b2b3eba2bcbd671ade82b28cc094093832075970504f732"
217
229
  },
218
230
  {
219
231
  "path": "shared/core/multi-agent-setup/SKILL.md",
@@ -221,7 +233,7 @@
221
233
  },
222
234
  {
223
235
  "path": "shared/core/multi-agent-stack/SKILL.md",
224
- "sha256": "ac964cd00dd6ed06512530489d867efbc51d7d7614265f9aa022679325d5defe"
236
+ "sha256": "0e32b95c62324bb592d10b4c2c515509998e7260f6c4aae94271683ede9ba763"
225
237
  },
226
238
  {
227
239
  "path": "shared/core/multi-agent-status/SKILL.md",
@@ -229,7 +241,7 @@
229
241
  },
230
242
  {
231
243
  "path": "shared/core/multi-agent-steer/SKILL.md",
232
- "sha256": "20f332bd2fe2745da5341f8d8187211fc47c06b4b5f5031e217affee0282ddb5"
244
+ "sha256": "9ac0c48c3cbafd98022a04b24ef4f16723dda6a852202002231f43bbb7dd453b"
233
245
  },
234
246
  {
235
247
  "path": "shared/core/multi-agent-store-ready/SKILL.md",
@@ -237,7 +249,7 @@
237
249
  },
238
250
  {
239
251
  "path": "shared/core/multi-agent-sync/SKILL.md",
240
- "sha256": "e96afa9b89a2ab6e2fe13a01158f98272c6828e5cef03c8909240766aaefd44b"
252
+ "sha256": "17f734fa2b84d9a4c757f9745be8ff40d3097f5de89caf55124b48439a737093"
241
253
  },
242
254
  {
243
255
  "path": "shared/core/multi-agent-test-accessibility/SKILL.md",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "skillCount": 234,
3
+ "skillCount": 237,
4
4
  "entries": [
5
5
  {
6
6
  "name": "accessibility-compliance-accessibility-audit",
@@ -1355,6 +1355,39 @@
1355
1355
  "triggerPaths": [],
1356
1356
  "relativePath": "shared/core/multi-agent-model/SKILL.md"
1357
1357
  },
1358
+ {
1359
+ "name": "multi-agent-office-off",
1360
+ "description": "Close the office panel and stop its local server. Use when done with the panel.",
1361
+ "platform": null,
1362
+ "group": "core",
1363
+ "plugin": null,
1364
+ "invokeAs": "multi-agent-office-off",
1365
+ "triggerKeywords": [],
1366
+ "triggerPaths": [],
1367
+ "relativePath": "shared/core/multi-agent-office-off/SKILL.md"
1368
+ },
1369
+ {
1370
+ "name": "multi-agent-office-on",
1371
+ "description": "Open the office panel: a local browser page with agents at work, their chats, cost ledger and new runs. Use when you want to watch or drive runs from a browser.",
1372
+ "platform": null,
1373
+ "group": "core",
1374
+ "plugin": null,
1375
+ "invokeAs": "multi-agent-office-on",
1376
+ "triggerKeywords": [],
1377
+ "triggerPaths": [],
1378
+ "relativePath": "shared/core/multi-agent-office-on/SKILL.md"
1379
+ },
1380
+ {
1381
+ "name": "multi-agent-office-status",
1382
+ "description": "Whether the office panel is running, its URL (never the token) and its last actions. Use when unsure if the panel is open.",
1383
+ "platform": null,
1384
+ "group": "core",
1385
+ "plugin": null,
1386
+ "invokeAs": "multi-agent-office-status",
1387
+ "triggerKeywords": [],
1388
+ "triggerPaths": [],
1389
+ "relativePath": "shared/core/multi-agent-office-status/SKILL.md"
1390
+ },
1358
1391
  {
1359
1392
  "name": "multi-agent-postmortem",
1360
1393
  "description": "Post-mortem of a run from its session transcripts: loops, stalls, root-cause hypotheses on file:line, fixes. Read-only, redacted. Use when a run went wrong or ran far too long.",
@@ -2,11 +2,11 @@
2
2
 
3
3
  Single source of truth for skills delivered to both Claude Code (`~/.claude/skills/`) and Copilot CLI (`~/.copilot/skills/`) by the installer.
4
4
 
5
- **Total:** 234 skills (69 core + 165 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
5
+ **Total:** 237 skills (72 core + 165 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
6
6
 
7
7
  ## Directory layout
8
8
 
9
- - **`core/`** - 69 `multi-agent*` orchestration skills that are pipeline-critical. Edits here are core-code changes.
9
+ - **`core/`** - 72 `multi-agent*` orchestration skills that are pipeline-critical. Edits here are core-code changes.
10
10
  - **`external/`** - 165 iOS / Android / generic skills that ship into the stack plugins. Each names its origin in `metadata.source`: `multi-agent-pipeline` when written here, or the upstream and license a `NOTICE-*.md` in this directory covers.
11
11
  - Install destinations (ADR-0009): Claude Code gets NO local copy of `external/` - it loads those skills from the `multi-agent-plugins` marketplace, namespaced (`ai-<stack>-toolkit:<name>`); only the two compliance catalogs from `core/` land in `~/.claude/skills/`. Copilot CLI and Codex CLI receive a flat copy filtered to the enabled stacks. `external/` remains the single authoring source that `build-stack-plugins.mjs` publishes from.
12
12
 
@@ -14,7 +14,7 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
14
14
 
15
15
  ## Categories
16
16
 
17
- - [Pipeline Orchestration](#pipeline-orchestration) - 69
17
+ - [Pipeline Orchestration](#pipeline-orchestration) - 72
18
18
  - [iOS / Apple Ecosystem](#ios-apple-ecosystem) - 94
19
19
  - [Android / Kotlin](#android-kotlin) - 16
20
20
  - [Web](#web) - 10
@@ -57,6 +57,9 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
57
57
  | [`multi-agent-log`](./core/multi-agent-log/) | `core` | Show the agent-log.md for the given task. With no ID, shows the most recent task. Use when asked what a task did, or to read its log. |
58
58
  | [`multi-agent-manual-test`](./core/multi-agent-manual-test/) | `core` | Switch to the active task's branch and prepare it for manual testing in Xcode. Phase 3 user-test step, standalone (the UI Bug Hunter lives a |
59
59
  | [`multi-agent-model`](./core/multi-agent-model/) | `core` | Turn the top model rung on or off and keep the cost ledger's pricing in step with it. Use when asked to enable or disable Fable, or which mo |
60
+ | [`multi-agent-office-off`](./core/multi-agent-office-off/) | `core` | Close the office panel and stop its local server. Use when done with the panel. |
61
+ | [`multi-agent-office-on`](./core/multi-agent-office-on/) | `core` | Open the office panel: a local browser page with agents at work, their chats, cost ledger and new runs. Use when you want to watch or drive |
62
+ | [`multi-agent-office-status`](./core/multi-agent-office-status/) | `core` | Whether the office panel is running, its URL (never the token) and its last actions. Use when unsure if the panel is open. |
60
63
  | [`multi-agent-postmortem`](./core/multi-agent-postmortem/) | `core` | Post-mortem of a run from its session transcripts: loops, stalls, root-cause hypotheses on file:line, fixes. Read-only, redacted. Use when a |
61
64
  | [`multi-agent-prune-logs`](./core/multi-agent-prune-logs/) | `core` | Delete per-task project logs under ~/.claude/logs/multi-agent (filter by age/project/task). Audit trail + metrics are preserved. Dry-run fir |
62
65
  | [`multi-agent-prune-prompts`](./core/multi-agent-prune-prompts/) | `core` | Zero-base prompt review: measure the always-on instruction footprint, classify every rule block, propose keep/trial-removal/delete; applies |
@@ -162,25 +162,23 @@ Render `templates/multi-agent-autopilot.plist.template` and show the resolved
162
162
  "Configure only" is a real answer and the safe default to offer first: the queue
163
163
  can be watched with `autopilot-status` for a while before anything runs.
164
164
 
165
- On **Start**:
165
+ On **Start**, load the schedule and the awake agent with the same script the
166
+ office panel's `POST /v1/autopilot/on` calls, naming every repo selected in step 3:
166
167
 
167
168
  ```bash
168
- launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.multi-agent.autopilot.plist" 2>/dev/null \
169
- || launchctl load "$HOME/Library/LaunchAgents/com.multi-agent.autopilot.plist"
169
+ node "$HOME/.claude/scripts/autopilot-control.mjs" on --repo <owner/name> [--repo <owner/name>...]
170
170
  ```
171
171
 
172
- Then the awake agent, rendered from
173
- `templates/multi-agent-autopilot-awake.plist.template` with the `awake.display`
174
- answer saved above:
175
-
176
- ```bash
177
- . "$HOME/.claude/lib/autopilot-state.sh"
178
- node "$(ma_ap_asset scripts/autopilot-awake.mjs)" install
179
- ```
180
-
181
- A non-zero exit means launchd refused the job: say so, with its path, and say
182
- the Mac may sleep between ticks until it is fixed. The schedule itself stays on.
183
- "Configure only" installs neither.
172
+ It keeps exactly those repos in `config.json`, probes their credentials again
173
+ (`autopilot-arming.mjs probeCredentials`), renders
174
+ `templates/multi-agent-autopilot.plist.template` with its seven placeholders
175
+ (NODE, TICK, LABEL, ROOT, INTERVAL, HOME, CLAUDE), loads it with `launchctl
176
+ bootstrap` (falling back to `load`) and installs the awake agent with the
177
+ `awake.display` answer saved above. `scheduled: false` with probe problems means
178
+ nothing was loaded; say so with the problems. Exit 1 with `launchd refused`
179
+ means launchd refused the job: say so, with its path, and say the Mac may sleep
180
+ between ticks until it is fixed. "Configure only" is `--configure-only`, which
181
+ saves the selection and installs neither.
184
182
 
185
183
  ### 9. Report what is now true
186
184
 
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: multi-agent-office-off
3
+ description: "Close the office panel and stop its local server. Use when done with the panel."
4
+ user-invocable: true
5
+ metadata:
6
+ language: en
7
+ ---
8
+
9
+ # multi-agent-office-off
10
+
11
+ 1. `node "$HOME/.copilot/scripts/office-control.mjs" off --json`
12
+ 2. Report in `outputLanguage`: whether a server was `stopped`, and the
13
+ `remote` line as given. A panel that was already closed is not an error.
14
+
15
+ Only the recorded server is signalled, and only when its token file, command
16
+ and start time still match.