@mmerterden/multi-agent-pipeline 17.6.0 → 18.0.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 (103) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +43 -1
  3. package/README.tr.md +41 -0
  4. package/docs/adr/0011-dormant-ci.md +25 -1
  5. package/docs/server-readiness.md +188 -0
  6. package/index.js +16 -1
  7. package/install/_common.mjs +42 -17
  8. package/install/_dev-only-files.mjs +8 -0
  9. package/install/_unattended-profile.mjs +113 -0
  10. package/install/index.mjs +48 -0
  11. package/manifest.json +1049 -0
  12. package/package.json +5 -2
  13. package/pipeline/commands/multi-agent/status/SKILL.md +52 -21
  14. package/pipeline/lib/_jira-auth.sh +8 -0
  15. package/pipeline/lib/analysis-jira-write.sh +32 -0
  16. package/pipeline/lib/ask-choice.sh +13 -2
  17. package/pipeline/lib/autopilot-state.sh +8 -0
  18. package/pipeline/lib/fatal.mjs +129 -0
  19. package/pipeline/lib/figma-mcp-refresh.sh +18 -0
  20. package/pipeline/lib/figma-screenshot.sh +18 -0
  21. package/pipeline/lib/invoked-directly.mjs +43 -0
  22. package/pipeline/lib/jira-publish.sh +42 -0
  23. package/pipeline/lib/md2confluence-v3.py +47 -0
  24. package/pipeline/lib/outbound-gate.mjs +175 -0
  25. package/pipeline/lib/plan-todos.sh +27 -6
  26. package/pipeline/lib/post-pr-review.sh +77 -8
  27. package/pipeline/lib/repo-hygiene.sh +8 -3
  28. package/pipeline/lib/require-jq.sh +40 -0
  29. package/pipeline/lib/run-paths.sh +335 -0
  30. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +70 -0
  31. package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
  32. package/pipeline/multi-agent-refs/features/doctor.md +45 -0
  33. package/pipeline/multi-agent-refs/features/verify.md +83 -0
  34. package/pipeline/multi-agent-refs/phases/operations.md +13 -2
  35. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  36. package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
  37. package/pipeline/scripts/_run-paths.mjs +372 -0
  38. package/pipeline/scripts/aggregate-metrics.mjs +64 -64
  39. package/pipeline/scripts/autopilot-arming.mjs +2 -1
  40. package/pipeline/scripts/autopilot-intake.mjs +2 -1
  41. package/pipeline/scripts/autopilot-runner.mjs +206 -2
  42. package/pipeline/scripts/build-references.mjs +2 -1
  43. package/pipeline/scripts/build-stack-plugins.mjs +10 -2
  44. package/pipeline/scripts/capture-evidence.sh +7 -2
  45. package/pipeline/scripts/classify-plan-safety.mjs +2 -1
  46. package/pipeline/scripts/cost-analyze.mjs +600 -0
  47. package/pipeline/scripts/cost-budget-check.mjs +4 -12
  48. package/pipeline/scripts/council-view.mjs +2 -1
  49. package/pipeline/scripts/crush-json.mjs +2 -1
  50. package/pipeline/scripts/diff-explain.mjs +6 -9
  51. package/pipeline/scripts/diff-risk-score.mjs +2 -1
  52. package/pipeline/scripts/doctor.mjs +138 -4
  53. package/pipeline/scripts/evidence-gate.mjs +9 -3
  54. package/pipeline/scripts/feedback-send.mjs +12 -2
  55. package/pipeline/scripts/gc-abandoned.sh +29 -13
  56. package/pipeline/scripts/gc-worktrees.sh +11 -4
  57. package/pipeline/scripts/github-ssh-setup.sh +64 -7
  58. package/pipeline/scripts/graph-mermaid.mjs +4 -2
  59. package/pipeline/scripts/keychain-save.sh +101 -30
  60. package/pipeline/scripts/learn-from-transcripts.mjs +2 -1
  61. package/pipeline/scripts/learning-curve.mjs +34 -29
  62. package/pipeline/scripts/make-manifest.mjs +199 -0
  63. package/pipeline/scripts/migrate-prefs.mjs +2 -1
  64. package/pipeline/scripts/migrate-state.mjs +94 -4
  65. package/pipeline/scripts/phase-banner.sh +6 -2
  66. package/pipeline/scripts/phase-tracker.sh +41 -3
  67. package/pipeline/scripts/plan-coverage-gate.mjs +6 -2
  68. package/pipeline/scripts/pre-commit-check.sh +7 -0
  69. package/pipeline/scripts/pre-push-check.sh +7 -0
  70. package/pipeline/scripts/purge.sh +23 -6
  71. package/pipeline/scripts/render-agent-log-cost.sh +9 -2
  72. package/pipeline/scripts/render-cost-summary.sh +9 -2
  73. package/pipeline/scripts/render-work-summary.sh +11 -4
  74. package/pipeline/scripts/review-file-filter.mjs +4 -2
  75. package/pipeline/scripts/review-scope.mjs +2 -1
  76. package/pipeline/scripts/routine-registry.mjs +2 -1
  77. package/pipeline/scripts/run-aggregator.mjs +13 -14
  78. package/pipeline/scripts/run-metrics.mjs +3 -1
  79. package/pipeline/scripts/runs-index.mjs +343 -0
  80. package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
  81. package/pipeline/scripts/search-logs.sh +18 -0
  82. package/pipeline/scripts/test-gap-scan.mjs +2 -1
  83. package/pipeline/scripts/test-integrity-gate.mjs +2 -1
  84. package/pipeline/scripts/update-issue-progress.sh +56 -7
  85. package/pipeline/scripts/usage-report.mjs +12 -1
  86. package/pipeline/scripts/validate-analysis-doc.mjs +2 -1
  87. package/pipeline/scripts/validate-code-graph.mjs +6 -3
  88. package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
  89. package/pipeline/scripts/validate-diff-risk.mjs +6 -3
  90. package/pipeline/scripts/validate-test-gap.mjs +6 -3
  91. package/pipeline/scripts/validate-triage.mjs +3 -1
  92. package/pipeline/scripts/verify-citations.mjs +4 -2
  93. package/pipeline/scripts/verify.mjs +327 -0
  94. package/pipeline/scripts/worktree-finalize.sh +13 -4
  95. package/pipeline/scripts/write-state.mjs +154 -15
  96. package/pipeline/skills/.skill-manifest.json +2 -2
  97. package/pipeline/skills/.skills-index.json +56 -1
  98. package/pipeline/skills/shared/README.md +8 -3
  99. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +33 -9
  100. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
  101. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
  102. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
  103. package/pipeline/skills/skills-index.md +6 -1
@@ -16,12 +16,22 @@
16
16
  *
17
17
  * # Replace: full overwrite from stdin JSON
18
18
  * cat new-state.json | node write-state.mjs --replace /path/to/agent-state.json
19
+ * cat patch.json | node write-state.mjs --if-rev=7 /path/to/agent-state.json
20
+ *
21
+ * `--if-rev=<n>` is compare-and-swap: write only if the record on disk is
22
+ * still at revision n. The lock closes the window between two WRITERS; it does
23
+ * nothing about the one between a read and a write, which is where an editor
24
+ * lives - `/multi-agent:steer` reads the state, a human thinks, and the run may
25
+ * move three phases before the edit lands. Without this the edit merges over
26
+ * the top and reinstates decisions the run already replaced.
19
27
  *
20
28
  * Exit codes:
21
29
  * 0 - write succeeded
22
30
  * 1 - invalid JSON on stdin
23
31
  * 2 - lock timeout (another writer held the lock past the acquire window)
32
+ * 4 - the lock was taken from us mid-write; nothing was written, retry
24
33
  * 3 - I/O error (disk full, permission denied, parent dir missing)
34
+ * 5 - --if-rev did not match; the record moved, nothing was written
25
35
  *
26
36
  * Tunables (env):
27
37
  * WRITE_STATE_LOCK_TIMEOUT_MS acquire window before exit 2 (default 15000)
@@ -52,9 +62,11 @@ import {
52
62
  } from "fs";
53
63
  import { dirname, basename } from "path";
54
64
  import { Buffer } from "node:buffer";
65
+ import { runMain } from "../lib/fatal.mjs";
66
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
55
67
 
56
68
  /**
57
- * @typedef {{ replace: boolean, path: string }} ParsedArgs
69
+ * @typedef {{ replace: boolean, path: string, ifRev: number|null }} ParsedArgs
58
70
  */
59
71
 
60
72
  /** @returns {ParsedArgs} */
@@ -62,18 +74,26 @@ function parseArgs() {
62
74
  const args = process.argv.slice(2);
63
75
  let replace = false;
64
76
  let path = null;
77
+ let ifRev = null;
65
78
  for (const a of args) {
66
79
  if (a === "--replace") replace = true;
67
- else if (a.startsWith("--")) {
80
+ else if (a.startsWith("--if-rev=")) {
81
+ const v = Number(a.slice("--if-rev=".length));
82
+ if (!Number.isInteger(v) || v < 0) {
83
+ console.error(`write-state: --if-rev takes a non-negative integer, got ${a.slice(9)}`);
84
+ process.exit(3);
85
+ }
86
+ ifRev = v;
87
+ } else if (a.startsWith("--")) {
68
88
  console.error(`write-state: unknown flag ${a}`);
69
89
  process.exit(3);
70
90
  } else path = a;
71
91
  }
72
92
  if (!path) {
73
- console.error("usage: write-state.mjs [--replace] <path>");
93
+ console.error("usage: write-state.mjs [--replace] [--if-rev=<n>] <path>");
74
94
  process.exit(3);
75
95
  }
76
- return { replace, path };
96
+ return { replace, path, ifRev };
77
97
  }
78
98
 
79
99
  /**
@@ -103,8 +123,13 @@ async function acquireLock(
103
123
  try {
104
124
  writeFileSync(stagePath, String(process.pid));
105
125
  linkSync(stagePath, lockPath); // fails EEXIST if another writer holds it
126
+ // The inode of the lock WE created. Everything after this point checks
127
+ // against it: a lock at the same path with a different inode is somebody
128
+ // else's, and both releasing it and writing under it are the same bug the
129
+ // reclaim path below already refuses to commit.
130
+ const ino = statSync(lockPath).ino;
106
131
  unlinkSync(stagePath);
107
- return;
132
+ return ino;
108
133
  } catch (err) {
109
134
  try {
110
135
  unlinkSync(stagePath);
@@ -208,16 +233,46 @@ function lockIdentityIfStale(lockPath, staleMs) {
208
233
  // Path whose lock this process currently holds (null when none). Lets the
209
234
  // top-level error handler release our lock on unexpected failures.
210
235
  let heldLockFor = null;
236
+ // The inode of that lock. The path alone does not identify it: a lock deleted
237
+ // and re-created at the same path is a different file and a different owner.
238
+ let heldLockIno = null;
239
+
240
+ /**
241
+ * True when the lock at `path` is still the one this process created.
242
+ *
243
+ * @param {string} path
244
+ * @param {number | null} ino
245
+ * @returns {boolean}
246
+ */
247
+ function stillOurs(path, ino) {
248
+ if (ino === null) return true; // nothing was ever locked; nothing to lose
249
+ try {
250
+ return statSync(`${path}.lock`).ino === ino;
251
+ } catch {
252
+ return false; // our lock is gone, which means somebody removed it
253
+ }
254
+ }
211
255
 
212
256
  /** @param {string} path */
213
- function releaseLock(path) {
257
+ function releaseLock(path, ino = heldLockIno) {
214
258
  const lockPath = `${path}.lock`;
215
259
  try {
260
+ // By identity, never by path - the same rule acquireLock states and this
261
+ // function used to break. If our lock was reclaimed as stale while we were
262
+ // starved (the abandon ceiling, or the unreadable-PID window), the lock now
263
+ // at this path belongs to a LIVE writer, and unlinking it puts two writers
264
+ // inside at once and loses one update. That is the third point in this
265
+ // cycle where "delete by path" produces the same data loss; the other two
266
+ // are documented in acquireLock.
267
+ if (ino !== null && statSync(lockPath).ino !== ino) return;
216
268
  unlinkSync(lockPath);
217
269
  } catch {
218
270
  /* already gone - fine */
219
271
  }
220
- if (heldLockFor === path) heldLockFor = null;
272
+ if (heldLockFor === path) {
273
+ heldLockFor = null;
274
+ heldLockIno = null;
275
+ }
221
276
  }
222
277
 
223
278
  /**
@@ -225,6 +280,22 @@ function releaseLock(path) {
225
280
  * @param {object} patch
226
281
  * @returns {object}
227
282
  */
283
+ /**
284
+ * The version a NEWLY created state document is stamped with, read from the
285
+ * schema so the writer and the schema cannot disagree. The literal is a
286
+ * fallback for an unreadable schema, which validate-schemas.mjs would already
287
+ * have failed on.
288
+ */
289
+ const STATE_SCHEMA_VERSION = (() => {
290
+ try {
291
+ const schemaPath = new URL("../schemas/agent-state.schema.json", import.meta.url);
292
+ const sv = JSON.parse(readFileSync(schemaPath, "utf-8"))?.properties?.schemaVersion;
293
+ return sv?.const ?? sv?.enum?.at(-1) ?? "2.1.0";
294
+ } catch {
295
+ return "2.1.0";
296
+ }
297
+ })();
298
+
228
299
  function deepMerge(base, patch) {
229
300
  if (Array.isArray(patch)) return patch; // arrays replace
230
301
  if (typeof patch !== "object" || patch === null) return patch;
@@ -236,7 +307,7 @@ function deepMerge(base, patch) {
236
307
  }
237
308
 
238
309
  async function main() {
239
- const { replace, path } = parseArgs();
310
+ const { replace, path, ifRev } = parseArgs();
240
311
 
241
312
  // Read stdin
242
313
  const chunks = [];
@@ -257,7 +328,7 @@ async function main() {
257
328
  }
258
329
 
259
330
  try {
260
- await acquireLock(path);
331
+ heldLockIno = await acquireLock(path);
261
332
  heldLockFor = path;
262
333
  } catch (err) {
263
334
  if (err.code === "LOCK_TIMEOUT") {
@@ -272,6 +343,8 @@ async function main() {
272
343
  // here must release OUR lock explicitly before exiting - otherwise a corrupt
273
344
  // state file (or any I/O error) leaks agent-state.json.lock and blocks the
274
345
  // next writer until the stale-lock window elapses.
346
+ const fileExistedBefore = existsSync(path);
347
+
275
348
  try {
276
349
  let next;
277
350
  if (replace) {
@@ -304,16 +377,67 @@ async function main() {
304
377
  // carries a `rev` from before the record moved, and following it down
305
378
  // would tell the next reader that nothing changed.
306
379
  let priorRev = Number.isInteger(next.rev) ? next.rev : 0;
380
+ let diskRev = null;
307
381
  if (existsSync(path)) {
308
382
  try {
309
383
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
310
- if (Number.isInteger(parsed?.rev)) priorRev = Math.max(priorRev, parsed.rev);
384
+ if (Number.isInteger(parsed?.rev)) {
385
+ diskRev = parsed.rev;
386
+ priorRev = Math.max(priorRev, parsed.rev);
387
+ }
311
388
  } catch {
312
389
  /* unreadable on disk - the merge path above already threw for that */
313
390
  }
314
391
  }
392
+
393
+ // Compare-and-swap. `rev` already told a reader whether the record moved;
394
+ // --if-rev is what lets it ACT on that instead of merging over the top.
395
+ //
396
+ // The lock closes the window between two writers. It does not close the
397
+ // one between a READ and a write, and `/multi-agent:steer` puts a second
398
+ // writer in exactly that window on purpose: it reads the state, a human
399
+ // thinks, and by the time the edit lands the run may have moved three
400
+ // phases. A merge then reinstates decisions the run already replaced.
401
+ //
402
+ // Refusing costs a re-read. Merging costs a phase that silently reverts.
403
+ if (ifRev !== null) {
404
+ const seen = diskRev ?? 0;
405
+ if (seen !== ifRev) {
406
+ console.error(
407
+ `write-state: rev is ${seen}, expected ${ifRev} - the record moved since you read it; re-read and retry`,
408
+ );
409
+ releaseLock(path);
410
+ process.exit(5);
411
+ }
412
+ }
413
+
315
414
  next.rev = priorRev + 1;
316
415
 
416
+ // schemaVersion is stamped only when this call CREATES the document. The
417
+ // migration runner keys on it, and a document that never carries one can
418
+ // never be migrated - measured on a real install, 43 of 43 state files had
419
+ // no schemaVersion, so pipeline/schemas/migrations/ was unreachable code.
420
+ // A merge into an existing unstamped file is deliberately left alone:
421
+ // stamping there would claim a conformance nothing verified.
422
+ if (!fileExistedBefore && typeof next.schemaVersion !== "string") {
423
+ next.schemaVersion = STATE_SCHEMA_VERSION;
424
+ }
425
+
426
+ // Last check before the write lands: is the lock still ours? Everything
427
+ // above read the file under a lock we believed we held, and if that belief
428
+ // stopped being true the rename would overwrite a concurrent writer's
429
+ // update with a merge that never saw it. Reporting success while losing an
430
+ // update is the one outcome this script exists to prevent, so it refuses
431
+ // instead - a caller that sees exit 4 can read and retry, which a caller
432
+ // that saw exit 0 never will.
433
+ if (!stillOurs(path, heldLockIno)) {
434
+ console.error(
435
+ `write-state: the lock was taken from us mid-write (${path}.lock). ` +
436
+ `Nothing was written. Re-read the state and retry.`,
437
+ );
438
+ process.exit(4);
439
+ }
440
+
317
441
  const tmp = `${dirname(path)}/.${basename(path)}.${process.pid}.tmp`;
318
442
  writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n");
319
443
  renameSync(tmp, path); // atomic on POSIX
@@ -327,8 +451,23 @@ async function main() {
327
451
  process.exit(0);
328
452
  }
329
453
 
330
- main().catch((err) => {
331
- console.error(`write-state: unhandled - ${err.message}`);
332
- if (heldLockFor) releaseLock(heldLockFor);
333
- process.exit(3);
334
- });
454
+ // Only run when invoked as a program. The lock helpers are the part with the
455
+ // subtle behaviour and they are unreachable from a test otherwise: the window
456
+ // they guard is microseconds wide, so a test that drives this file as a CLI
457
+ // measures the scheduler rather than the code.
458
+ const isDirectRun = invokedDirectly(import.meta.url);
459
+ if (isDirectRun) {
460
+ // Exit 3 is this file's documented "died without writing" code, and the
461
+ // cleanup is the reason the handler exists at all: a writer that dies holding
462
+ // the advisory lock makes the NEXT writer wait out the acquire window and
463
+ // then decide, on nothing but age, whether to take a lock that may still be
464
+ // live. Releasing on the abnormal path keeps that decision from arising.
465
+ runMain("write-state", main, {
466
+ code: 3,
467
+ cleanup: () => {
468
+ if (heldLockFor) releaseLock(heldLockFor);
469
+ },
470
+ });
471
+ }
472
+
473
+ export { acquireLock, releaseLock, stillOurs, lockIdentityIfStale };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-15T20:06:53Z",
3
+ "generatedAt": "2026-09-17T11:16:58Z",
4
4
  "skillCount": 212,
5
5
  "entries": [
6
6
  {
@@ -189,7 +189,7 @@
189
189
  },
190
190
  {
191
191
  "path": "shared/core/multi-agent-status/SKILL.md",
192
- "sha256": "436d1716ce5fc4977dadfce8a85b09877f93c626953739a7ad70eefbbcd25869"
192
+ "sha256": "3305fcce80bdad9ea0032e382276c5692a197749d9c012910daaea7a332bce0e"
193
193
  },
194
194
  {
195
195
  "path": "shared/core/multi-agent-steer/SKILL.md",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "skillCount": 207,
3
+ "skillCount": 212,
4
4
  "entries": [
5
5
  {
6
6
  "name": "accessibility-compliance-accessibility-audit",
@@ -959,6 +959,17 @@
959
959
  "triggerPaths": [],
960
960
  "relativePath": "shared/core/multi-agent-analysis/SKILL.md"
961
961
  },
962
+ {
963
+ "name": "multi-agent-analysis-jira",
964
+ "description": "Turn a rendered analysis document into a Jira story tree: derive stories from the document's own rule ids, check coverage both ways, preview every byte, then create only what does not already exist. Use when an analysis is final and the work needs tickets.",
965
+ "platform": null,
966
+ "group": "core",
967
+ "plugin": null,
968
+ "invokeAs": "multi-agent-analysis-jira",
969
+ "triggerKeywords": [],
970
+ "triggerPaths": [],
971
+ "relativePath": "shared/core/multi-agent-analysis-jira/SKILL.md"
972
+ },
962
973
  {
963
974
  "name": "multi-agent-analysis-resolve",
964
975
  "description": "Resolve the Section 20 Risks and Open Questions of an analysis v3 document one row at a time: up to 3 source-labeled answer candidates per row (evidence / repo / AI reasoned), chosen answer merged into the target section, doc updated in place with a changelog bump. Same locked decisions as /multi-ag",
@@ -981,6 +992,39 @@
981
992
  "triggerPaths": [],
982
993
  "relativePath": "shared/core/multi-agent-autopilot/SKILL.md"
983
994
  },
995
+ {
996
+ "name": "multi-agent-autopilot-off",
997
+ "description": "Turn off continuous mode on this machine: the schedule is removed, work already running finishes, the repo selection is kept. Use when the machine should stop picking work up.",
998
+ "platform": null,
999
+ "group": "core",
1000
+ "plugin": null,
1001
+ "invokeAs": "multi-agent-autopilot-off",
1002
+ "triggerKeywords": [],
1003
+ "triggerPaths": [],
1004
+ "relativePath": "shared/core/multi-agent-autopilot-off/SKILL.md"
1005
+ },
1006
+ {
1007
+ "name": "multi-agent-autopilot-on",
1008
+ "description": "Turn on continuous mode on THIS machine: pick the repos it watches; labelled GitHub issues and assigned+labelled Jira items then run in a worktree and stop at an open PR. Use when the machine should pick work up unattended.",
1009
+ "platform": null,
1010
+ "group": "core",
1011
+ "plugin": null,
1012
+ "invokeAs": "multi-agent-autopilot-on",
1013
+ "triggerKeywords": [],
1014
+ "triggerPaths": [],
1015
+ "relativePath": "shared/core/multi-agent-autopilot-on/SKILL.md"
1016
+ },
1017
+ {
1018
+ "name": "multi-agent-autopilot-status",
1019
+ "description": "Show what continuous mode is doing here: what runs at which phase, what is queued, what waits for an answer, what PRs it opened today. Use when asked whether autopilot is on.",
1020
+ "platform": null,
1021
+ "group": "core",
1022
+ "plugin": null,
1023
+ "invokeAs": "multi-agent-autopilot-status",
1024
+ "triggerKeywords": [],
1025
+ "triggerPaths": [],
1026
+ "relativePath": "shared/core/multi-agent-autopilot-status/SKILL.md"
1027
+ },
984
1028
  {
985
1029
  "name": "multi-agent-build-optimize",
986
1030
  "description": "Wrapper that dispatches to xcode-build-orchestrator on iOS repos. Benchmarks the current Xcode build, runs compilation / project / SPM analyzers, produces a recommend-first optimization plan in .build-benchmark/, and re-benchmarks after the developer approves changes. Fails fast on non-iOS stacks. U",
@@ -1047,6 +1091,17 @@
1047
1091
  "triggerPaths": [],
1048
1092
  "relativePath": "shared/core/multi-agent-diff-explain/SKILL.md"
1049
1093
  },
1094
+ {
1095
+ "name": "multi-agent-doctor",
1096
+ "description": "Health check for the installed pipeline: layout, preferences, credentials, hooks and host capabilities, each with one actionable step. Exit code is the verdict. Use when a run failed for an environmental reason, before a sync, or after an update.",
1097
+ "platform": null,
1098
+ "group": "core",
1099
+ "plugin": null,
1100
+ "invokeAs": "multi-agent-doctor",
1101
+ "triggerKeywords": [],
1102
+ "triggerPaths": [],
1103
+ "relativePath": "shared/core/multi-agent-doctor/SKILL.md"
1104
+ },
1050
1105
  {
1051
1106
  "name": "multi-agent-feedback",
1052
1107
  "description": "Send one message to the maintainer: a bug, an idea or a question. Only the text you type is sent - no logs, no repo names, no paths. Shows the payload and asks before sending. Use when a run went wrong and the maintainer should know.",
@@ -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:** 207 skills (54 core + 153 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
5
+ **Total:** 212 skills (59 core + 153 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
6
6
 
7
7
  ## Directory layout
8
8
 
9
- - **`core/`** - 54 `multi-agent*` orchestration skills that are pipeline-critical. Edits here are core-code changes.
9
+ - **`core/`** - 59 `multi-agent*` orchestration skills that are pipeline-critical. Edits here are core-code changes.
10
10
  - **`external/`** - 153 iOS / Android / generic skills imported from the upstream skill library. Mirrors of third-party guidance.
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) - 54
17
+ - [Pipeline Orchestration](#pipeline-orchestration) - 59
18
18
  - [iOS / Apple Ecosystem](#ios-apple-ecosystem) - 90
19
19
  - [Android / Kotlin](#android-kotlin) - 13
20
20
  - [Web](#web) - 10
@@ -29,14 +29,19 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
29
29
  | [`google-play-compliance`](./core/google-play-compliance/) | `core` | Google Play Store publication compliance - bundletool + aapt2 + apksigner orchestration + 21-rule policy catalog with Play Console error c |
30
30
  | [`multi-agent`](./core/multi-agent/) | `core` | Task orchestrator: runs the full pipeline from a Jira ID or GitHub Issue URL - analysis → plan → TDD development → parallel review (Fable |
31
31
  | [`multi-agent-analysis`](./core/multi-agent-analysis/) | `core` | Standalone feature-spec analysis. Two profiles picked at intake: global (23-section development handoff, 8 of them in Lite mode) or corporat |
32
+ | [`multi-agent-analysis-jira`](./core/multi-agent-analysis-jira/) | `core` | Turn a rendered analysis document into a Jira story tree: derive stories from the document's own rule ids, check coverage both ways, preview |
32
33
  | [`multi-agent-analysis-resolve`](./core/multi-agent-analysis-resolve/) | `core` | Resolve the Section 20 Risks and Open Questions of an analysis v3 document one row at a time: up to 3 source-labeled answer candidates per r |
33
34
  | [`multi-agent-autopilot`](./core/multi-agent-autopilot/) | `core` | Launch any task in autopilot mode: skips every confirmation, runs end-to-end autonomously. Use when a task should run end to end with no con |
35
+ | [`multi-agent-autopilot-off`](./core/multi-agent-autopilot-off/) | `core` | Turn off continuous mode on this machine: the schedule is removed, work already running finishes, the repo selection is kept. Use when the m |
36
+ | [`multi-agent-autopilot-on`](./core/multi-agent-autopilot-on/) | `core` | Turn on continuous mode on THIS machine: pick the repos it watches; labelled GitHub issues and assigned+labelled Jira items then run in a wo |
37
+ | [`multi-agent-autopilot-status`](./core/multi-agent-autopilot-status/) | `core` | Show what continuous mode is doing here: what runs at which phase, what is queued, what waits for an answer, what PRs it opened today. Use w |
34
38
  | [`multi-agent-build-optimize`](./core/multi-agent-build-optimize/) | `core` | Wrapper that dispatches to xcode-build-orchestrator on iOS repos. Benchmarks the current Xcode build, runs compilation / project / SPM analy |
35
39
  | [`multi-agent-channels`](./core/multi-agent-channels/) | `core` | Multi-channel reporter - Jira/Confluence/Wiki/PR description. Multi-select channels + content, humanizer pass, reviewer-preserving Bitbuck |
36
40
  | [`multi-agent-complaint-analysis`](./core/multi-agent-complaint-analysis/) | `core` | Customer-complaint triage. Ingests complaints (paste, csv/xlsx/txt/json file, Jira issue, Confluence URL), fetches Graylog evidence per trx/ |
37
41
  | [`multi-agent-create-jira`](./core/multi-agent-create-jira/) | `core` | Create a standards-compliant Jira issue (Task / Bug / Story): asks the type, mines project conventions, drafts from a standard template with |
38
42
  | [`multi-agent-design-check`](./core/multi-agent-design-check/) | `core` | Mock-mode vs Figma design audit (iOS / Android, local-only). Pick repo + module, gate on mock support, enumerate every state driver into a c |
39
43
  | [`multi-agent-diff-explain`](./core/multi-agent-diff-explain/) | `core` | Map Phase 4 triage findings to branch diff lines. Read-only post-hoc command, used after review to answer 'which finding lines up with which |
44
+ | [`multi-agent-doctor`](./core/multi-agent-doctor/) | `core` | Health check for the installed pipeline: layout, preferences, credentials, hooks and host capabilities, each with one actionable step. Exit |
40
45
  | [`multi-agent-feedback`](./core/multi-agent-feedback/) | `core` | Send one message to the maintainer: a bug, an idea or a question. Only the text you type is sent - no logs, no repo names, no paths. Shows t |
41
46
  | [`multi-agent-forget`](./core/multi-agent-forget/) | `core` | Remove a saved /multi-agent routine (created by /multi-agent:save): deletes its local-only command and its registry entry. Asks which one an |
42
47
  | [`multi-agent-garbage-collect`](./core/multi-agent-garbage-collect/) | `core` | Sweep leftover /tmp scratch (picker state, review diffs, channel payloads, analysis drafts) from past runs. Dry-run first; confirms before d |
@@ -11,20 +11,44 @@ Show every active and completed task as a table.
11
11
 
12
12
  ## Steps
13
13
 
14
- 1. **Detect project** - Find the repo root from cwd (if none, look at all known repos):
15
- - `~/my-ui-components/.worktrees/`
16
- - `~/my-figma-app/.worktrees/`
17
- - `~/my-ios-app/.worktrees/`
14
+ 1. **Ask the producer, do not go looking.** One command answers the whole
15
+ question:
18
16
 
19
- 2. **Scan worktrees** - Read the `agent-state.json` file in each worktree directory:
20
17
  ```bash
21
- find {repo}/.worktrees/ -name "agent-state.json" -maxdepth 2
18
+ node "$HOME/.claude/scripts/runs-index.mjs" # grouped table
19
+ node "$HOME/.claude/scripts/runs-index.mjs" --json # the same records
22
20
  ```
23
21
 
24
- 3. **Parse state** - For each task:
25
- - `taskId`, `branch`, `currentPhase`, `status`, `startedAt`, `autopilot`
22
+ `runs-index.mjs` resolves through `lib/run-paths.sh` / `scripts/_run-paths.mjs`,
23
+ so it sees both directory layouts (`<project>/<id>/` and the flat `<id>/`),
24
+ the salvaged `artifacts/` copy Phase 6 leaves behind, and every spelling of a
25
+ task id - and it counts a run that exists in both layouts once. Do NOT
26
+ re-scan the tree by hand: the earlier instruction here listed three
27
+ hard-coded `.worktrees/` paths and a single `find` depth, and on a real
28
+ install that combination missed a quarter of the runs and double-counted
29
+ others. It also scanned worktrees for `agent-state.json`, which Phase 0 has
30
+ never written there ("never inside the worktree", `phases/phase-0-init.md`).
26
31
 
27
- 4. **Show as a table**:
32
+ 2. **Fields per run** (already in the output): `taskId`, `project`, `branch`,
33
+ `currentPhase`, `status`, `startedAt`, `worktreePath`, `prUrl`, `autopilot`,
34
+ `phases[]`, `tokens`, `estUsd`, `group`, plus `duplicateOf` when the run also
35
+ exists in the other layout and `salvaged` when its state is the Phase 6 copy.
36
+
37
+ 3. **Groups are computed, not judged.** Report the `group` the producer
38
+ returns rather than re-deriving it.
39
+
40
+ | Group | Test | Action offered |
41
+ |---|---|---|
42
+ | `waiting` - Waiting on you | `status == "awaiting_input"`, or a `pr.url`, or phase >= 6 | `resume #N` - the work landed, it needs your answer |
43
+ | `stopped` - Stopped mid-development | anything else past phase 0 | `resume #N` or `kill #N` |
44
+ | `question` - Left at a question | phase 0 | `garbage-collect --abandoned` - nothing was built |
45
+ | `unknown` - Status not recorded | no `status` field | say so; offer nothing |
46
+
47
+ A run with no `status` is **not** placed in an actionable group. Unknown is
48
+ not a finding, and calling it dead is the same false claim in the other
49
+ direction.
50
+
51
+ 4. **Show as a table**, grouped per step 3:
28
52
  ```
29
53
  🤖 Multi-Agent Tasks
30
54
 
@@ -19,7 +19,10 @@ else
19
19
  BUILD_NUMBER=${BUILD_NUMBER:-1}
20
20
  fi
21
21
 
22
- ARCH_LIST=( ${ARCHES:-} )
22
+ # read -ra, not ( $ARCHES ): a bare expansion into an array splits on IFS and
23
+ # then GLOBS each word, so an ARCHES carrying a * builds for whatever files are
24
+ # in the working directory.
25
+ read -ra ARCH_LIST <<< "${ARCHES:-}"
23
26
  if [[ ${#ARCH_LIST[@]} -eq 0 ]]; then
24
27
  HOST_ARCH=$(uname -m)
25
28
  ARCH_LIST=("$HOST_ARCH")
@@ -15,7 +15,10 @@ fi
15
15
  echo "Creating self-signed certificate '$CERT_NAME'..."
16
16
 
17
17
  TEMP_CONFIG=$(mktemp)
18
- trap "rm -f $TEMP_CONFIG" EXIT
18
+ # Single-quoted on purpose: the path is read when the trap FIRES, not when it is
19
+ # installed. Double quotes bake in whatever the variable held at install time,
20
+ # which is a template users copy into scripts where the order differs.
21
+ trap 'rm -f "$TEMP_CONFIG"' EXIT
19
22
 
20
23
  cat > "$TEMP_CONFIG" <<EOFCONF
21
24
  [ req ]
@@ -17,7 +17,8 @@ echo "$APP_STORE_CONNECT_API_KEY_P8" | sed 's/\\n/\n/g' > /tmp/app-store-connect
17
17
  trap 'rm -f /tmp/app-store-connect-key.p8 /tmp/${APP_NAME}Notarize.zip' EXIT
18
18
 
19
19
  ARCHES_VALUE=${ARCHES:-"arm64 x86_64"}
20
- ARCH_LIST=( ${ARCHES_VALUE} )
20
+ # See package_app.sh: a bare expansion into an array globs as well as splits.
21
+ read -ra ARCH_LIST <<< "${ARCHES_VALUE}"
21
22
  for ARCH in "${ARCH_LIST[@]}"; do
22
23
  swift build -c release --arch "$ARCH"
23
24
  done
@@ -3,7 +3,7 @@
3
3
  > Auto-generated by `pipeline/scripts/build-skills-index.mjs` - do not hand-edit.
4
4
  > Regenerate with `node pipeline/scripts/build-skills-index.mjs`.
5
5
 
6
- **207 skills** across 2 groups.
6
+ **212 skills** across 2 groups.
7
7
 
8
8
  | Group | Name | Platform | Description |
9
9
  |-------|------|----------|-------------|
@@ -94,14 +94,19 @@
94
94
  | external | `monorepo-architect` | - | Expert in monorepo architecture, build systems, and dependency management at scale. Masters Nx, Turborepo, Bazel, and Lerna for efficient mu |
95
95
  | core | `multi-agent` | - | Task orchestrator: runs the full pipeline from a Jira ID or GitHub Issue URL - analysis → plan → TDD development → parallel review (Fable |
96
96
  | core | `multi-agent-analysis` | - | Standalone feature-spec analysis. Two profiles picked at intake: global (23-section development handoff, 8 of them in Lite mode) or corporat |
97
+ | core | `multi-agent-analysis-jira` | - | Turn a rendered analysis document into a Jira story tree: derive stories from the document's own rule ids, check coverage both ways, preview |
97
98
  | core | `multi-agent-analysis-resolve` | - | Resolve the Section 20 Risks and Open Questions of an analysis v3 document one row at a time: up to 3 source-labeled answer candidates per r |
98
99
  | core | `multi-agent-autopilot` | - | Launch any task in autopilot mode: skips every confirmation, runs end-to-end autonomously. Use when a task should run end to end with no con |
100
+ | core | `multi-agent-autopilot-off` | - | Turn off continuous mode on this machine: the schedule is removed, work already running finishes, the repo selection is kept. Use when the m |
101
+ | core | `multi-agent-autopilot-on` | - | Turn on continuous mode on THIS machine: pick the repos it watches; labelled GitHub issues and assigned+labelled Jira items then run in a wo |
102
+ | core | `multi-agent-autopilot-status` | - | Show what continuous mode is doing here: what runs at which phase, what is queued, what waits for an answer, what PRs it opened today. Use w |
99
103
  | core | `multi-agent-build-optimize` | - | Wrapper that dispatches to xcode-build-orchestrator on iOS repos. Benchmarks the current Xcode build, runs compilation / project / SPM analy |
100
104
  | core | `multi-agent-channels` | - | Multi-channel reporter - Jira/Confluence/Wiki/PR description. Multi-select channels + content, humanizer pass, reviewer-preserving Bitbuck |
101
105
  | core | `multi-agent-complaint-analysis` | - | Customer-complaint triage. Ingests complaints (paste, csv/xlsx/txt/json file, Jira issue, Confluence URL), fetches Graylog evidence per trx/ |
102
106
  | core | `multi-agent-create-jira` | - | Create a standards-compliant Jira issue (Task / Bug / Story): asks the type, mines project conventions, drafts from a standard template with |
103
107
  | core | `multi-agent-design-check` | - | Mock-mode vs Figma design audit (iOS / Android, local-only). Pick repo + module, gate on mock support, enumerate every state driver into a c |
104
108
  | core | `multi-agent-diff-explain` | - | Map Phase 4 triage findings to branch diff lines. Read-only post-hoc command, used after review to answer 'which finding lines up with which |
109
+ | core | `multi-agent-doctor` | - | Health check for the installed pipeline: layout, preferences, credentials, hooks and host capabilities, each with one actionable step. Exit |
105
110
  | core | `multi-agent-feedback` | - | Send one message to the maintainer: a bug, an idea or a question. Only the text you type is sent - no logs, no repo names, no paths. Shows t |
106
111
  | core | `multi-agent-forget` | - | Remove a saved /multi-agent routine (created by /multi-agent:save): deletes its local-only command and its registry entry. Asks which one an |
107
112
  | core | `multi-agent-garbage-collect` | - | Sweep leftover /tmp scratch (picker state, review diffs, channel payloads, analysis drafts) from past runs. Dry-run first; confirms before d |