@mmerterden/multi-agent-pipeline 20.4.0 → 20.5.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.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,22 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [20.5.0] - 2026-09-25
20
+
21
+ ### Added
22
+
23
+ - `pipeline/lib/redact.mjs` replaces every credential the outbound gate recognises with `[redacted:<rule>]`. `audit-log.sh` and `log-metric.sh` pass each line through it, so a token copied into an error message never reaches `audit.jsonl` or `metrics.jsonl`; the rest of the message is kept for debugging. `smoke-log-redaction.sh` runs every provider in `secret-patterns.json` through both writers.
24
+ - Autopilot `githubAssignee` (default `@me`): GitHub intake lists only issues assigned to the operator, as the Jira search already does. `"*"` lists every labelled issue.
25
+ - Every persona declares `disallowedTools: Write, Edit, NotebookEdit`; `lint-personas` requires it. MCP tools stay inherited.
26
+
27
+ ### Fixed
28
+
29
+ - Autopilot refuses to push a commit whose message would close an issue on merge, and the closing-keyword check recognises the full issue URL form.
30
+ - Autopilot takes nothing while the installed version is below the published `required` floor (`blocked-version-floor`, not an attempt), instead of spending a session per queued item on a run whose Phase 0 halts.
31
+ - `update-check.sh` reads the installed version marker before any repo clone, so a stale clone no longer reports a forced update after every update. It also treats a future cache stamp as stale, writes the cache atomically, remembers an unreachable registry for an hour, honours `updateCheck.ttlHours`, and orders a prerelease below its release.
32
+ - `/multi-agent:update` skips the download and reinstall when already current.
33
+ - `audit-log.sh` escapes newlines and tabs, and the audit file and its archives are created 0600.
34
+
19
35
  ## [20.4.0] - 2026-09-25
20
36
 
21
37
  ### Changed
package/docs/facts.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "Generated by pipeline/scripts/gen-facts.mjs. Do not hand-edit: the site reads this, and a number edited here instead of at its source is the drift this file removes.",
3
3
  "generatedAt": "2026-09-25",
4
- "version": "20.4.0",
4
+ "version": "20.5.0",
5
5
  "phaseSchema": 2,
6
6
  "phases": [
7
7
  {
package/manifest.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.4.0",
4
- "commit": "47524b22c14c8b28503ea0707ac4b7d7dfa3718f",
5
- "generatedAt": "2026-09-25T15:35:01.768Z",
3
+ "version": "20.5.0",
4
+ "commit": "2779213750032f49aefb95d894ef3df49e29e325",
5
+ "generatedAt": "2026-09-25T17:15:16.484Z",
6
6
  "algorithm": "sha256",
7
- "fileCount": 1217,
7
+ "fileCount": 1218,
8
8
  "files": {
9
- "CHANGELOG.md": "3d7abc887673cc1897d4eea75c3db3d4cd2a396a6cb8148c3e1e607e29c2984b",
9
+ "CHANGELOG.md": "40dbfcb1d32886e1930ea869d1ce2a50c24bde5388085e9689e6e1c34c910a95",
10
10
  "LICENSE": "8d8c51aaf1e1538b180c69965fa038db71be86798cc3e26a143bebfefe16a43e",
11
11
  "README.md": "0613eebb0b38062bd94c44043595d1950b5c9b470874dd8e402c052fd20960cf",
12
12
  "README.tr.md": "0c6b5e6943d6c3d3bf3a7ed99c705fb9a78d15d7722e6252f58404d3f6d3524d",
@@ -33,7 +33,7 @@
33
33
  "docs/best-practices.md": "f8b75b0d38b5896a6a852bb30a1edddeb24dab8b5b7b1d9313290e1241c72024",
34
34
  "docs/ecosystem.md": "e6c00f30e95b5e178f77bdfbefd686c8a4164f39c03865a89ab23465d1a37acf",
35
35
  "docs/engineering.md": "550528cd47c7d2c921729c68d0782a00ac8e77a4bdfec479f6749f9c3235d188",
36
- "docs/facts.json": "5b4dd591c0972a9021bb96761715555b63a2de819d4207439f32c27a3193f01c",
36
+ "docs/facts.json": "2ed95fd7398d21ff4d92fce2e0f6442db876bd68c96002d964491a98902282e2",
37
37
  "docs/features.md": "6c9f0efb00c683e6f611c440c87847b187f8b0eee59e9e812f3a241695bc2ae1",
38
38
  "docs/performance.md": "bbf2f3922c25dcba0c1a9e69ef420e3871a729af5b35009a49b48a5d3c5cd151",
39
39
  "docs/recovery-guide.md": "59a8f1b66b34cb9ff5507b3d54dac3b9915e184aa044d9c758d18cac818d510e",
@@ -61,17 +61,17 @@
61
61
  "install/templates/multi-agent-autopilot-awake.plist.template": "ddd25c76c56fcbb38a89de8e35637372d70ace993d6bc487a88ca9cfb702fe1f",
62
62
  "install/templates/multi-agent-autopilot.plist.template": "122c8a61cd927edd99375d6e3da40f1d42bcceed5b6641785c8b28bb930bc77e",
63
63
  "install/unattended-profile-legacy.json": "f52e86b998956f09ad306b1735a6c10b58580d7082d8a55665a4c6bb15c3aa1f",
64
- "package.json": "ef6a78d030798ebfd4823be3c4b884ce9a7b24a04d1b1ea9e2671d6c8ef8ec3c",
65
- "pipeline/agents/android-architect.md": "baa5787b3a4c800dd065e27e05f44462927c9938c8bb676c80f18cb10e2b12c4",
66
- "pipeline/agents/backend-architect.md": "a83bd7f8b0a5289a1ef9a9b59d60fc759b8db260606ff2df427cf5ade5db8530",
67
- "pipeline/agents/bulk-reader.md": "0b9d415fab2c449d43329d759049af9401255d06b32d57a38c95e684837b19bf",
68
- "pipeline/agents/code-reviewer.md": "544351ee4b2620bf7abc30e0e839a37f2a6c2b21051696642dbc958960d123ff",
69
- "pipeline/agents/dev-critic.md": "93598bf9c2b27430a38966491fcce37ffdec56b58c3811221e274316cc7c94e1",
70
- "pipeline/agents/explorer.md": "9e72db35159b455a21a956375ffa457871131c7234f6ff5ae1e46c97a8112563",
71
- "pipeline/agents/ios-architect.md": "31d9415e4a6e5daade3246c401f4e5b44627f850313f624b72747c8479c9ff2f",
72
- "pipeline/agents/plan-critic.md": "6cebab0d01449ee5ead70bdaffc5e409b3df8890863f1027c3f9a569f0ac04ee",
73
- "pipeline/agents/security-auditor.md": "8d399b8eef2d0f4b3bb6164d25bbbcb4285895dbb0cb9e3f82b2c1f762fda856",
74
- "pipeline/agents/task-clarifier.md": "9dba052a9ed44cafcc8302a4ae8628d9bb3b97f815b13af9985221821b8a5b12",
64
+ "package.json": "e8b5ebb47535e622d1e13c31676a183dca1af3af7b089ba3a0e8a90101429e23",
65
+ "pipeline/agents/android-architect.md": "11fb74543cb0ebcf9525ca6654fe95d53da7229e0bf4a1149b31fd9005abc453",
66
+ "pipeline/agents/backend-architect.md": "b02a606c301e4714131aed70ae6d16eeb77fbc819a001e2752cff2488d4246de",
67
+ "pipeline/agents/bulk-reader.md": "6597052cd7d205bac2b1c7633f5232fe1a40730664a913b8eb77efd5a786dc25",
68
+ "pipeline/agents/code-reviewer.md": "995c72b5d79bc64e45230b54e8ac5b90fbab73f187a509b331f83e5ec3493800",
69
+ "pipeline/agents/dev-critic.md": "33843216b79977ce2926fc9c54f8c9eef30b91632a11ef624647229b61a9b36a",
70
+ "pipeline/agents/explorer.md": "12564aec29d4581684bf84313b86a4add5a6989d3b8db5c35769674cab5649c6",
71
+ "pipeline/agents/ios-architect.md": "e1fe0c034a4dfa76794ed04ba9f4bf85d5cdd3a5d6b948cc0635e8dda8baeef9",
72
+ "pipeline/agents/plan-critic.md": "02d2a0e384cfbabfaf4726043a566486b6f034073efd1aa33c6d65febe8141d5",
73
+ "pipeline/agents/security-auditor.md": "aa6e695b3ea4e9fc11da421537aefe0fc99bfd0a9bd0b93bddfc9a5a9a009365",
74
+ "pipeline/agents/task-clarifier.md": "90c37ede5ccd0ffd09423e75ff83ba9b04f48bf0b6b0dabfda020c757b9bacc2",
75
75
  "pipeline/claude-md-template.md": "bcc75808a54f32f11e688875cda23d3be3c1524511258e3d4ea5e7c0d2a92a92",
76
76
  "pipeline/commands/archive-guard.md": "16c5d3feaa91950402d62e66adc7fcf519a25d028a9303cb1e9c08af2765c04b",
77
77
  "pipeline/commands/deploy.md": "d20f95f3b1d2357c8c92f3047e8fe11eadf539ff13cab9b54f072f0ad697242a",
@@ -81,7 +81,7 @@
81
81
  "pipeline/commands/multi-agent/analysis-resolve/SKILL.md": "3c712e86d4b2e80d1ba51811ea32c8d5fa11291ab62c6dae944f9c19ddfd9868",
82
82
  "pipeline/commands/multi-agent/analysis/SKILL.md": "a20662c5c3495bb3fe676e43a8d71ce3148886f2ace582496b094dec6d7aea78",
83
83
  "pipeline/commands/multi-agent/autopilot-off/SKILL.md": "59cd59a3f69e51dd8fe39f414e448f79237fa6764986bd0ddbdbf9ff1523fb69",
84
- "pipeline/commands/multi-agent/autopilot-on/SKILL.md": "4503e6709f184c42623ba72d238b985f910b8ae7554a8aca2703982a87588b63",
84
+ "pipeline/commands/multi-agent/autopilot-on/SKILL.md": "d8f7847004f2bdcf7923527bddfaa17fb9f09a83fd5e3a9d997e0d1871907938",
85
85
  "pipeline/commands/multi-agent/autopilot-status/SKILL.md": "73cea063993bb5c6781112f7bba6f6dd2a5014bcdd4400f9f3c48b582f50252d",
86
86
  "pipeline/commands/multi-agent/autopilot/SKILL.md": "4f90d957f9cbbeb74015e09efb82bad4d87210a0a123e23fad274d12b470427d",
87
87
  "pipeline/commands/multi-agent/build-optimize/SKILL.md": "e74d6f5be703acef1704092d5fe53485d95fe6c220bb768accaf68ebd212b7c1",
@@ -137,7 +137,7 @@
137
137
  "pipeline/commands/multi-agent/test/SKILL.md": "5a67be2b1ec0321e79f8b7f35be53676e349e55a4f8ca09a07f0de89dadac8c4",
138
138
  "pipeline/commands/multi-agent/testflight-validation/SKILL.md": "38fa8d3d3c8340d025e100c53a70183cbad22665b7ddccbb6102c05986cce489",
139
139
  "pipeline/commands/multi-agent/uninstall/SKILL.md": "b2124820a6e10cdd72be71df2183aad4f8bd8988ec90248c88599c1514f7d9ab",
140
- "pipeline/commands/multi-agent/update/SKILL.md": "63b896036401a37da80de26a53eb855b0dba368215ee96ab0f83ef22478f7d21",
140
+ "pipeline/commands/multi-agent/update/SKILL.md": "f596052221345312ce03937b88604ab82b303e3255a64b5dfe7c49dda87a5148",
141
141
  "pipeline/commands/sim-test.md": "17fcefc2030f0ab24b3ee59807dfc11264b34d7455138081996859b1fee8664e",
142
142
  "pipeline/contract/CHANGELOG.md": "33d5dd6b35d8498928f431729dbccc9aae2f42e3beb04da1bd52f682846b2c10",
143
143
  "pipeline/contract/README.md": "4a60faddcd31973302bab93bb3fab74da96024c79aaba3676c6ceeabc2088132",
@@ -198,6 +198,7 @@
198
198
  "pipeline/lib/plan-todos.sh": "4992bed180774a27f84892add8b09e0f7b53e9b8fefb75f60325a04d1af18c56",
199
199
  "pipeline/lib/post-pr-review.sh": "c7a8f8c648ac7aebf904e70ae482c026de5b3cc897b376cd8aa52356fda8b805",
200
200
  "pipeline/lib/pr-request-location.mjs": "622ca4705ec8ee00e0a4609657d8ee684fcdc077f3099ab312cf41b5b0c41efd",
201
+ "pipeline/lib/redact.mjs": "63dd8a1d5a7f0d6e2dca764b186212e393e59f480c5f0985afe4685c2361f9fc",
201
202
  "pipeline/lib/regular-file.mjs": "084dd2c98a664a1e4a937757cc5e5e5d7e162b6aa422f8c7cfdb27f7834c6cbc",
202
203
  "pipeline/lib/repo-cache.sh": "82bbdd3d61746042ad5341554a1077098d4b56f15caf6c5cc092377a273c8f45",
203
204
  "pipeline/lib/repo-hygiene.sh": "67f4227d2bc84198d51205f1f16d055e76a07fe11a4e5c2b8103559c44cefa04",
@@ -279,7 +280,7 @@
279
280
  "pipeline/multi-agent-refs/features/stack-adapters.md": "71e23385da872ceed35263f4838f154800aa2da55bb6688ccca74726b70a3ef5",
280
281
  "pipeline/multi-agent-refs/features/stack-skill-routing.md": "8d45fb7331509411617d2bf5c42e752eaaa7ada24dcf38140f2710b28ab4fc0f",
281
282
  "pipeline/multi-agent-refs/features/unattended-gates.md": "46b516cac81a4eacf5d58bc3b1613e2266c26f1559be8dac060d1dc6b7d7ca01",
282
- "pipeline/multi-agent-refs/features/unattended-security.md": "384561f8f9900e031b1d888c901e30e3a33e30c6a2bc5d32852374b1372effd0",
283
+ "pipeline/multi-agent-refs/features/unattended-security.md": "8b7c5718e95c81debd915650ff2458b4764fa78eeceed779476955b4f04165a8",
283
284
  "pipeline/multi-agent-refs/features/url-enrichment.md": "72e01a799d9fa54b56c762ba404a9f9236adbbf98320e395a96f6b0f5917e76b",
284
285
  "pipeline/multi-agent-refs/features/usage-reporting.md": "6c232060da49033ef2c9ccead69084dfb7cfe45333f670b407b8afada8b99007",
285
286
  "pipeline/multi-agent-refs/features/verify-by-test.md": "b0735adaa7ead797167b8d9ea48a10851a138083c6e638738cc96c66b02a1e37",
@@ -341,7 +342,7 @@
341
342
  "pipeline/schemas/analysis-spec.schema.json": "42e4d527c69f952fb6a08bca5bcbbd14dfb75291cf028ce3c653043db70bd622",
342
343
  "pipeline/schemas/answer-request.schema.json": "29d4211a2a05a230e0707b171b73341d2e56f6b83edb178b731905211aab4e23",
343
344
  "pipeline/schemas/answer-result.schema.json": "7e998e8dcd48d6152c867fd178c6c1107e97c21d19429e67554e7d7a3aa42a28",
344
- "pipeline/schemas/autopilot-config.schema.json": "bc30ae33f5cc6884d883a485637c5fb22e64396148e4f5f8ecf3b33b3c67a7bf",
345
+ "pipeline/schemas/autopilot-config.schema.json": "c183332327520026c02f87f65377a1838e5de9cb9210337e6c4a6aaa7a0706c5",
345
346
  "pipeline/schemas/bulk-read-output.schema.json": "468d447ee69d5576d16835cdd167a9191bf33d2b6e3af89373907582ef93f49e",
346
347
  "pipeline/schemas/clarify-output.schema.json": "13c3d933d24d9541a97fe7630f97587157c92e420794d6dea359726377c986a6",
347
348
  "pipeline/schemas/code-graph.schema.json": "ec022b4ac1ff1daada79cadc4469b7365f9faebd81d839c40bd931b41222fb4e",
@@ -411,7 +412,7 @@
411
412
  "pipeline/scripts/README.md": "937adf6f34ce18fc07d91d6556abcdd39debf2df7819c3f16e009430dc0549a7",
412
413
  "pipeline/scripts/_autopilot-config.mjs": "0f42d2791f1d71c4aad5a0735ca56fef1ed27d275b72f99d8ca8f95fcb8aa83a",
413
414
  "pipeline/scripts/_autopilot-ops.mjs": "5e84d628482434c8f09dc69601c73ab70e928af15c2f068d998d2f61bd692d1a",
414
- "pipeline/scripts/_autopilot-outcomes.mjs": "939cf6cbce7217b8c0b86ca982927167e6a212ccd129b459062886d5feaf7f95",
415
+ "pipeline/scripts/_autopilot-outcomes.mjs": "5b2423814d979ed5c657473290083b296f7245d40507c866e7931b2771001236",
415
416
  "pipeline/scripts/_code-graph.mjs": "b1b269ef90c8c0c356c9691e0e7b2f0b56a5130a69f9ca2535c9dfcb8c653807",
416
417
  "pipeline/scripts/_command-contract.mjs": "e97a5c624e0768769da54d4175e9f7655f1de1dcd0832468bc66bf140b0f5a6f",
417
418
  "pipeline/scripts/_cost.mjs": "bdf03835af4719b72e4574f2d5ca827e94f9d80e49527a8e9e200a0aa4ecd7a9",
@@ -432,14 +433,14 @@
432
433
  "pipeline/scripts/analysis-story-tree.mjs": "8fd371d3e552a3f60533f18c7699074ae6089016c6663512410c4fa02494548f",
433
434
  "pipeline/scripts/anonymize-findings.mjs": "5d7e042793790c1b924d4d952b4f79b7e5f1dd0a3fe936f1ee16a4fa2ba820dd",
434
435
  "pipeline/scripts/answer-question.mjs": "b948df9d063f623a7ccd9cc65903c4388018b6b968228f77eb2e229223a61b1c",
435
- "pipeline/scripts/audit-log-rotate.sh": "5c3b737c4fe07256c885e6d5dca4f9a14574ebf43dbf34dd96e7818b89806b33",
436
- "pipeline/scripts/audit-log.sh": "17de3aac87488cbc6b49c441c2491c936df1a1dc8cd1bc27feda8274e600a721",
436
+ "pipeline/scripts/audit-log-rotate.sh": "5a4ef7d9ac75abbc5a5103fc2202b89707a2f2c00432ece133a132b483cd7450",
437
+ "pipeline/scripts/audit-log.sh": "5e0e45da2202a1a36f09408bf4c727f978c9850bf709c125cd2c0ccf414d7ff4",
437
438
  "pipeline/scripts/autopilot-arming.mjs": "38224fc58dc208e5e2d5793302cb67c6d0ab75be2dc4259cbe55ac87102a59b7",
438
439
  "pipeline/scripts/autopilot-awake.mjs": "23ae96e519fbaef86980e732196a581a861f4ffe9a319ce0bd3ffd7c9fe48b10",
439
- "pipeline/scripts/autopilot-intake.mjs": "40c1eb1719b4d5b92c8bca2fe9e27660467136641d2cdba3cac3c076cb0c7612",
440
+ "pipeline/scripts/autopilot-intake.mjs": "a65ac29850de94002bebd5db3f09b406c65f1875b32058cccea6b440f95c7526",
440
441
  "pipeline/scripts/autopilot-menubar.swift": "4fa39e603630565ac2cfb5a8b0bcf891dbefb02ce272252b9480605241670ac7",
441
- "pipeline/scripts/autopilot-publish.mjs": "9d2df8deb0c66f13f8322ded4a288e650366651f873e15bc3f764ffe19ff18bd",
442
- "pipeline/scripts/autopilot-runner.mjs": "7cb08c10a7032dccc9c049b7e448e6d6fdbe1d341a411e85e5c33b7fe9ecab2d",
442
+ "pipeline/scripts/autopilot-publish.mjs": "6a9727685d08c57983b9b6dfb398b7f7a7ef015ed14b8f61ac40207bb40f93d4",
443
+ "pipeline/scripts/autopilot-runner.mjs": "bf4398aff2bc98ff2d1a1e9c3606cf96e8e45b92dbe6980c5b5f0c3c5d7fdd3f",
443
444
  "pipeline/scripts/autopilot-status.sh": "8b9807155cfe5eff09ee3e01a8c3e5d1bbf7554db22de89de438531e4554b32f",
444
445
  "pipeline/scripts/base-branch-candidates.mjs": "c73a02033ed9f10c15b3aaf010432f0d4e429a75eba14731e9a64492e3fe3859",
445
446
  "pipeline/scripts/build-lock.sh": "9cf9a3f5100c337af86264ddc60f2ec763b6402c11c639946020037d78d5d365",
@@ -503,7 +504,7 @@
503
504
  "pipeline/scripts/learning-curve.mjs": "d574eec427c7f37a7a77a9ef9e2499f50dadc2215a20ae24e31654afd3497f36",
504
505
  "pipeline/scripts/learnings-ledger.mjs": "411bf80329a1c7700e5a523d5f0a7d1d3a716514f9656703bce67e26e71e0bb0",
505
506
  "pipeline/scripts/localize-commands.mjs": "d095d354be4bf5ea7c0a68969c1531421908b195755d591c5012334d821f201e",
506
- "pipeline/scripts/log-metric.sh": "41d5d6efeb336747216d6767f9b4025169c58a4e82a9fc03d5364a17b7309f73",
507
+ "pipeline/scripts/log-metric.sh": "f77b1c832fc7cb14dfe51feff023d76f76fb8891f0a61d514b52952e47681b8f",
507
508
  "pipeline/scripts/make-manifest.mjs": "db692c1bfcc0a7721ffb0b4d9ec57b74fadcaddf2821a32e222a4d4a513c425a",
508
509
  "pipeline/scripts/match-skills.mjs": "dd6c023a623ce6e9999f3eceaacd91f8d0c8ac621e3f025c8949e2203dc05bdf",
509
510
  "pipeline/scripts/maturity-followup.mjs": "531eb9578a71ea5bb2510bb24a48f264c36aaf7adceca7b4ec3dccd1c63ae412",
@@ -524,7 +525,7 @@
524
525
  "pipeline/scripts/plan-coverage-gate.mjs": "1a83bc52584438a9bd5cba96d2ddefed92a6a317d30ee695e67ef6c00b93e5a8",
525
526
  "pipeline/scripts/plan-critique-gate.mjs": "b139394e5f16d11e4997d0bb0b08529f439dbd7fad39781ef4c2475247d8fa97",
526
527
  "pipeline/scripts/plugin-refresh.sh": "687a33199ad963616ab537e32210ec8c967e76ecc42f494ee8a1dc9fe947fc30",
527
- "pipeline/scripts/pr-request.mjs": "59b86c7fd768067af7016836d6a29debec36c4d8f10171ae4591fd6773977989",
528
+ "pipeline/scripts/pr-request.mjs": "e6f4ded2366c6599bb345b3011ecc771e13b8a1561afa4c9a50bca4edff17456",
528
529
  "pipeline/scripts/pre-commit-check.sh": "81937ed7c0974fdfe440e19ce31405a38bbdf2b45a9bf226f490268fcf915942",
529
530
  "pipeline/scripts/pre-push-check.sh": "ff98e73d02aa75cfa9411c29c838241e8529e832e1421ca9c25c39aa07bcd968",
530
531
  "pipeline/scripts/probe-evidence-capability.sh": "ac74252eed6b773fdbc7ec475a63606925f1ba7b5e7c6f911329f46fd920eae7",
@@ -573,7 +574,7 @@
573
574
  "pipeline/scripts/ui-tree-dumper.swift": "a09e42ef069eafa08c05afd8333e05b754ef6a7d8b9687712039b0f2c9801273",
574
575
  "pipeline/scripts/unattended_policy.py": "a03dda2176d8a4ebce05b34f353fdfa01ac5e67ddcc7a163e0966bbe9f83d7e7",
575
576
  "pipeline/scripts/uninstall.mjs": "ea698d098ebc5590fbe9656af72d26773c9addfa8a8b9f747ed16514ee96f686",
576
- "pipeline/scripts/update-check.sh": "327a02306884093d2b93f395d2c297d5dc6b0c59757a740004bfafb13616fef0",
577
+ "pipeline/scripts/update-check.sh": "9c00647e7ca4249414b2e5af08f61705ab5bd2106b6f66ae8ebce8aa143dc957",
577
578
  "pipeline/scripts/update-issue-progress.sh": "ccfef87743e91272e009e396ac07461d2784b2fa84a06c802a582526eeeb9ed9",
578
579
  "pipeline/scripts/usage-identity.mjs": "3e2479bafc290f1acb253f5273339c87550f97d332818a26e5ac266d7ef289dc",
579
580
  "pipeline/scripts/usage-register.mjs": "2844d2187226006d1a8b4e9b044d7591f4a5d3313cb24f9e388b36639022ce4c",
@@ -596,7 +597,7 @@
596
597
  "pipeline/scripts/worktree-prepare.sh": "9391c828375b127f77c86e3c830dbbaf355a61464c6437319969f2f1eecc72b1",
597
598
  "pipeline/scripts/worktrees.mjs": "90dfd09b70fffe9b56c331a4ae2692cc248f4898f4ee6aee4239ef0b20888a90",
598
599
  "pipeline/scripts/write-state.mjs": "e1198bf970eace07d3eff22b394808a526e473df65d3d58aa2d5a12cfdac8b49",
599
- "pipeline/skills/.skill-manifest.json": "cffda3b70d7f2807055ef6e963637b2d4e9650465a2b606d0d8dab429da35d03",
600
+ "pipeline/skills/.skill-manifest.json": "04ce3e26081d98d092e0795f82aace9f6717bcff749a60b9573833343e8608be",
600
601
  "pipeline/skills/.skills-index.json": "042b1090f3e2ca310c6ce70d1bb304e8c7f96bd73133e504b95d83801608e470",
601
602
  "pipeline/skills/shared/README.md": "98196ea7c9d84637e8917ca4053c34b45de46586f71fb0c0034dce0808af857d",
602
603
  "pipeline/skills/shared/core/apple-archive-compliance/SKILL.md": "7239fef717ea45b79276bfcc2c3903acd57950b6e00dd6a2a66399c4b09cbd51",
@@ -607,7 +608,7 @@
607
608
  "pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md": "9e405b0f7403c72b00a7ddf1e4b8518cfe37feb31d91d8e5ba70f0341efa5d0c",
608
609
  "pipeline/skills/shared/core/multi-agent-analysis/SKILL.md": "fff7f2c2d4aa769a5c08beb776a848f91caf485c1d84dd5a4a3571ff02c35e0e",
609
610
  "pipeline/skills/shared/core/multi-agent-autopilot-off/SKILL.md": "aabfc6c4c0e0b1be7e054f252ea021a47436823ac6a0d5815d0fcab9e7d1f5ee",
610
- "pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md": "ca671ffe46809e3b0c825a6b5c19e8ea00e529874531eb480a0987428366d2f4",
611
+ "pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md": "d4f23f6f7bb493a4efa2cfa252fab50b22b6506aeaf31985db4498b5f010c8c8",
611
612
  "pipeline/skills/shared/core/multi-agent-autopilot-status/SKILL.md": "0833c8c63b82c8c360bd2c5a9df77a958aa8829326fdb3ad0c7d8d8197a42c29",
612
613
  "pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md": "426158ddbb5e8d6e594cd2e02aceb197e1769d692602dfbd2a368a6707081bd3",
613
614
  "pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md": "8534891d1198597a0893d131e6b944d15443f62b24cb5584c486a235845fd281",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.4.0",
3
+ "version": "20.5.0",
4
4
  "description": "6-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -4,6 +4,7 @@ description: Android architect - evaluates design decisions, patterns, and mod
4
4
  model: fable
5
5
  preferredModel: fable
6
6
  modelRationale: "Architecture decisions across Gradle modules, Compose stability, Hilt graphs, and migration paths run on fable, the top tier (opus is the first fallback). No downgrade recommended."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  You are a senior Android architect with deep expertise in large-scale modular Android applications.
@@ -4,6 +4,7 @@ description: Backend architect - evaluates API design, data modeling, and syst
4
4
  model: fable
5
5
  preferredModel: fable
6
6
  modelRationale: "API design + migration safety + scaling analysis are top-tier concerns; fable is the top tier (opus is the first fallback) and handles concurrent-write / rollback edge cases in database migration reviews."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  You are a senior backend architect with expertise in API design, distributed systems, and data modeling.
@@ -4,6 +4,7 @@ description: "Reads ONE large file and returns a structured, line-numbered summa
4
4
  model: haiku
5
5
  preferredModel: haiku
6
6
  modelRationale: "Reading a file and reporting what is in it is extraction, not judgement - the task has a single source, a fixed output shape, and no reasoning chain. Haiku is the right rung and the whole point: the saving is the difference between this rung and the caller's. A worker that reasons is the wrong tool here, and the contract below forbids it explicitly, because a cheap rung's opinion about code is worth less than nothing."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  # Bulk Reader
@@ -3,7 +3,8 @@ name: code-reviewer
3
3
  description: "Code reviewer for multi-agent Phase 3 - security, architecture, quality, performance. Default model is fable (opus is the first fallback); Phase 3 orchestrator overrides to sonnet for Reviewer 3."
4
4
  model: fable
5
5
  preferredModel: fable
6
- modelRationale: "Reviewer 1 tier - deep security + architecture review runs on opus (top available intelligence tier). Phase 3 orchestrator overrides to sonnet for Reviewer 3 (quality/correctness focus) via CLAUDE_CODE_SUBAGENT_MODEL before dispatch. Copilot CLI adds Reviewer 2 on gpt-5.4 for cross-model diversity."
6
+ modelRationale: "Reviewer 1 tier - deep security + architecture review runs on fable, the top tier (opus is the first fallback). Phase 3 orchestrator overrides to sonnet for Reviewer 3 (quality/correctness focus) via CLAUDE_CODE_SUBAGENT_MODEL before dispatch. Copilot CLI adds Reviewer 2 on gpt-5.4 for cross-model diversity."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  # Code Reviewer Agent
@@ -4,6 +4,7 @@ description: "Phase 2.5 evaluator - runs after Dev's last edit, before Phase 3
4
4
  model: sonnet
5
5
  preferredModel: sonnet
6
6
  modelRationale: "Critic tier - deterministic checklist + build/test verification needs reliable JSON output and tool-use, not deep reasoning. Sonnet hits the cost/quality sweet spot. Opus override only when the diff is large (>500 LOC) or touches security-critical paths; orchestrator decides via PHASE_MODEL_OVERRIDE."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  # Dev Critic Agent - Phase 2.5
@@ -4,6 +4,7 @@ description: "Codebase explorer agent for multi-agent Phase 1 - scans project
4
4
  model: sonnet
5
5
  preferredModel: sonnet
6
6
  modelRationale: "Scan-heavy, pattern-recognition work - sonnet matches opus quality on file inventory and dependency graphs at a fraction of the cost. If the task requires architectural synthesis (rare at Phase 1), the orchestrator can override."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  # Codebase Explorer Agent
@@ -4,6 +4,7 @@ description: iOS architect - evaluates design decisions, patterns, and module
4
4
  model: fable
5
5
  preferredModel: fable
6
6
  modelRationale: "Architecture decisions require deep reasoning across modules, protocols, and migration paths - fable is the top tier (opus is the first fallback). No downgrade recommended; if cost is a concern, skip the architect consultation entirely."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  You are a senior iOS architect with deep expertise in large-scale modular iOS applications.
@@ -4,6 +4,7 @@ description: "Phase 1 adversarial plan critic - runs once after the plan valid
4
4
  model: fable
5
5
  preferredModel: fable
6
6
  modelRationale: "Finding the failure a planner did not see is judgement over the whole plan, analysis and codebase, so it takes the top rung. Resolved through model-dispatch.sh with a fable default: opus while the fable rung is off, the same resolution review uses. On opus the critic is the planner's own model, which weakens the independence the role exists for; plan-critique-gate.mjs, not the critic, decides the gate either way."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  # Plan Critic Agent - Phase 1, one round
@@ -4,6 +4,7 @@ description: Security specialist - static application security review across m
4
4
  model: opus
5
5
  preferredModel: opus
6
6
  modelRationale: "Security reasoning + compliance catalog cross-reference (OWASP Web/API Top 10, OWASP Mobile Top 10, Apple ITMS, Google Play policy) plus honest confidence calibration on subtle vulnerabilities (auth-flow gaps, injection sinks, SSRF, cert-pinning bypass, sensitive-data leaks). False negatives are expensive and a wrong severity is worse than silence; opus (top available tier) keeps the miss rate and the miscalibration rate low."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  You are a static application security auditor. You read code, configuration and dependency manifests and report vulnerabilities. You do NOT run the application, fire payloads, or reach a live target: this is a defensive, read-only review. Everything you claim is grounded in something you can point to in the source.
@@ -4,6 +4,7 @@ description: "Phase 0 Step 9 task ambiguity scorer - reads task description, e
4
4
  model: haiku
5
5
  preferredModel: haiku
6
6
  modelRationale: "Ambiguity scoring is a low-stakes classification + targeted question generation. Haiku hits this well - short context (issue body + Jira description), structured JSON output, no reasoning chain needed. Sonnet override is available via PHASE_MODEL_OVERRIDE for repos where ambiguity nuance matters (legal, regulated domains)."
7
+ disallowedTools: Write, Edit, NotebookEdit
7
8
  ---
8
9
 
9
10
  # Task Clarifier Agent - Phase 0 Step 9
@@ -76,7 +76,7 @@ The same name works on both sides and both mechanisms already exist:
76
76
 
77
77
  | Source | Query | Extra condition |
78
78
  |---|---|---|
79
- | GitHub | `gh issue list --repo <r> --label <label> --state open` | none - the label is the whole gate |
79
+ | GitHub | `gh issue list --repo <r> --label <label> --assignee @me --state open` | **assigned to you** (`githubAssignee`, `"*"` for any), so a label an issue form applies is not enough |
80
80
  | Jira | `assignee = currentUser() AND labels = "<label>" AND resolution = EMPTY AND status not in (Done, Closed, Cancelled)` | **assigned to you**, so a label somebody else adds is not enough |
81
81
 
82
82
  Two Jira caveats worth saying out loud when Jira is selected: labels are one
@@ -52,7 +52,9 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
52
52
  line above is where the floor is reported, so the user sees why a run halted;
53
53
  `$REQUIRED` does not survive into the later shell blocks.
54
54
 
55
- 2. **Stop early when already current** (still refresh the marketplace, step 4b):
55
+ 2. **Stop early when already current.** When the line below prints, skip
56
+ step 3 (no download, no reinstall) and continue at step 4b, so the stack
57
+ plugins are still refreshed:
56
58
  ```bash
57
59
  [ "$CUR" = "$LATEST" ] && echo "✓ Already up to date: v$LATEST"
58
60
  ```
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * redact.mjs - replace every credential in a text with a visible marker.
4
+ *
5
+ * Uses the certain rules of outbound-gate.mjs, so the log writers, the commit
6
+ * hook and the outbound gate recognise one provider set. A match becomes
7
+ * `[redacted:<rule>]`; the text around it is kept, because a log line is read
8
+ * to debug a failure and the message is what makes it useful.
9
+ *
10
+ * As a CLI it reads stdin and writes the redacted text to stdout. It never
11
+ * fails its caller: on an internal error it writes the input unchanged.
12
+ *
13
+ * @module pipeline/lib/redact
14
+ */
15
+
16
+ import { RULES } from "./outbound-gate.mjs";
17
+ import { invokedDirectly } from "./invoked-directly.mjs";
18
+
19
+ const CERTAIN = RULES.filter((r) => r.certain);
20
+
21
+ /** @param {string} text */
22
+ export function redact(text) {
23
+ if (typeof text !== "string" || !text) return text;
24
+ let out = text;
25
+ for (const rule of CERTAIN) {
26
+ const re = new RegExp(
27
+ rule.re.source,
28
+ rule.re.flags.includes("g") ? rule.re.flags : `${rule.re.flags}g`,
29
+ );
30
+ out = out.replace(re, () =>
31
+ rule.name === "url-with-credentials" ? `[redacted:${rule.name}]@` : `[redacted:${rule.name}]`,
32
+ );
33
+ }
34
+ return out;
35
+ }
36
+
37
+ if (invokedDirectly(import.meta.url)) {
38
+ let input = "";
39
+ process.stdin.setEncoding("utf8");
40
+ process.stdin.on("data", (d) => (input += d));
41
+ process.stdin.on("end", () => {
42
+ let out;
43
+ try {
44
+ out = redact(input);
45
+ } catch {
46
+ out = input;
47
+ }
48
+ process.stdout.write(out);
49
+ });
50
+ }
@@ -589,6 +589,11 @@ hashes `~/.claude/{scripts,lib}`, the settings files, the profile and
589
589
  `~/Library/LaunchAgents` before and after publish verification (`hostFingerprint`
590
590
  in `autopilot-publish.mjs`) and refuses to push if any changed.
591
591
 
592
+ It also refuses to launch (`blocked-version-floor`) while the installed version
593
+ is below the published `required` floor (`require-supported-version.sh` exits
594
+ 3): the child's Phase 0 would halt on the same check, and the update it needs is
595
+ refused unattended. Like every `blocked-*` outcome it is not an attempt.
596
+
592
597
  The profile's `env` reaches the session and the commands it runs. The Claude
593
598
  Code docs pass environment to a stdio MCP server through the server entry, so
594
599
  pin the two toolkit variables there as well: `claude mcp add --env
@@ -50,6 +50,11 @@
50
50
  "default": "agent-queue",
51
51
  "description": "Only issues carrying this label are picked up. `gh issue list --label` does the filtering server-side."
52
52
  },
53
+ "githubAssignee": {
54
+ "type": "string",
55
+ "default": "@me",
56
+ "description": "Only issues assigned to this login are picked up (`gh issue list --assignee`), matching the Jira search's assignee = currentUser(). An issue form can apply the queue label for anyone who opens an issue, and an unattended run acts on the issue's text. \"*\" picks up every labelled issue."
57
+ },
53
58
  "jiraJql": {
54
59
  "type": "string",
55
60
  "description": "Full JQL, so an instance that cannot use labels is a config change rather than a redesign. The label form is sugar for: assignee = currentUser() AND labels = \"<label>\" AND resolution = EMPTY AND status not in (Done, Closed, Cancelled) ORDER BY priority DESC, created ASC. Jira labels are a single global namespace anyone can write to, so on a shared instance prefer a qualified name, and note that the Labels field must be on the edit screen for the issue types in play. An issue that more than one repo's search returns is queued once, for the first repo in config order, so repos sharing a Jira instance should each carry a jiraJql that matches only their own issues."
@@ -41,6 +41,7 @@ export const OUTCOME = Object.freeze({
41
41
  BLOCKED_NO_PATH: "blocked-no-path",
42
42
  BLOCKED_GUARD_MISSING: "blocked-guard-missing",
43
43
  BLOCKED_SANDBOX_UNAVAILABLE: "blocked-sandbox-unavailable",
44
+ BLOCKED_VERSION_FLOOR: "blocked-version-floor",
44
45
  });
45
46
 
46
47
  /**
@@ -107,6 +108,7 @@ export const BLOCKED = new Set([
107
108
  OUTCOME.BLOCKED_NO_PATH,
108
109
  OUTCOME.BLOCKED_GUARD_MISSING,
109
110
  OUTCOME.BLOCKED_SANDBOX_UNAVAILABLE,
111
+ OUTCOME.BLOCKED_VERSION_FLOOR,
110
112
  ]);
111
113
 
112
114
  /** Prefix families. `stopped-<status>` is a run that ended mid-way. */
@@ -89,6 +89,9 @@ if [ -s "$AUDIT_FILE" ]; then
89
89
  # of the content it archived. gzip -f removes the plain file on success, so
90
90
  # only touch whichever of the two actually exists.
91
91
  [ -f "$ROTATED.gz" ] && touch "$ROTATED.gz"
92
+ # An archive holds the same events as the live file, so it gets the same mode.
93
+ [ -f "$ROTATED.gz" ] && chmod 600 "$ROTATED.gz" 2>/dev/null
94
+ [ -f "$ROTATED" ] && chmod 600 "$ROTATED" 2>/dev/null
92
95
  [ -f "$ROTATED" ] && touch "$ROTATED"
93
96
  fi
94
97
 
@@ -39,9 +39,10 @@ esac
39
39
  AUDIT_FILE="${AUDIT_FILE:-${LOGS_ROOT:-$HOME/.claude/logs/multi-agent}/audit.jsonl}"
40
40
  mkdir -p "$(dirname "$AUDIT_FILE")" 2>/dev/null || exit 0
41
41
 
42
- # Escape a string for embedding inside a JSON double-quoted value.
42
+ # Escape a string for embedding inside a JSON double-quoted value. Newlines,
43
+ # carriage returns and tabs are escaped too: a raw one splits the JSONL line.
43
44
  json_escape() {
44
- printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
45
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g; s/\r/\\r/g' | awk 'NR > 1 { printf "\\n" } { printf "%s", $0 }'
45
46
  }
46
47
 
47
48
  # SHA-256 hash the repo URL (never log plaintext per section18.2.2)
@@ -85,6 +86,13 @@ LINE="{\"ts\":\"${TS}\",\"event\":\"$(json_escape "$EVENT")\",\"service\":\"$(js
85
86
  [ -n "$EXTRAS" ] && LINE="${LINE},${EXTRAS}"
86
87
  LINE="${LINE}}"
87
88
 
89
+ # Caller-supplied values can carry a credential copied out of an error.
90
+ # redact.mjs replaces it with a marker; without node the line is written as is.
91
+ REDACT="$(dirname "${BASH_SOURCE[0]:-$0}")/../lib/redact.mjs"
92
+ [ -f "$REDACT" ] && LINE=$(printf '%s' "$LINE" | node "$REDACT" 2>/dev/null || printf '%s' "$LINE")
93
+
94
+ [ -f "$AUDIT_FILE" ] || (umask 077 && : > "$AUDIT_FILE") 2>/dev/null || true
95
+ chmod 600 "$AUDIT_FILE" 2>/dev/null || true
88
96
  echo "$LINE" >> "$AUDIT_FILE" 2>/dev/null || true
89
97
 
90
98
  # Opportunistic rotation. audit-log-rotate.sh documents a launchd/cron install,
@@ -245,22 +245,24 @@ function sh(cmd, args, opts = {}) {
245
245
  }
246
246
  }
247
247
 
248
- function fetchGitHub(repo) {
248
+ /**
249
+ * The `gh issue list` arguments for one repo. Only issues assigned to the
250
+ * operator are listed by default, as the Jira search does with
251
+ * `assignee = currentUser()`: an issue form can apply the queue label for
252
+ * anyone who opens an issue, and an unattended run acts on the issue's text.
253
+ * `githubAssignee: "*"` lists every labelled issue.
254
+ */
255
+ export function githubListArgs(repo) {
249
256
  const label = repo.githubLabel || "agent-queue";
250
- const out = sh("gh", [
251
- "issue",
252
- "list",
253
- "--repo",
254
- repo.nameWithOwner,
255
- "--label",
256
- label,
257
- "--state",
258
- "open",
259
- "--json",
260
- "number,title,labels,url,createdAt",
261
- "--limit",
262
- "100",
263
- ]);
257
+ const assignee = repo.githubAssignee || "@me";
258
+ const args = ["issue", "list", "--repo", repo.nameWithOwner, "--label", label];
259
+ if (assignee !== "*") args.push("--assignee", assignee);
260
+ args.push("--state", "open", "--json", "number,title,labels,url,createdAt", "--limit", "100");
261
+ return args;
262
+ }
263
+
264
+ function fetchGitHub(repo) {
265
+ const out = sh("gh", githubListArgs(repo));
264
266
  let rows;
265
267
  try {
266
268
  rows = JSON.parse(out || "[]");
@@ -112,7 +112,7 @@ import { OUTCOME } from "./_autopilot-outcomes.mjs";
112
112
  import { commandFor, getAdapter, loadAdapters, resolveStack } from "./_stack-adapter.mjs";
113
113
  import { checkCommit, park, updateState } from "./gate-ledger.mjs";
114
114
  import { LOCKFILES } from "./package-manager.mjs";
115
- import { PROTECTED_BRANCHES, validatePrRequest } from "./pr-request.mjs";
115
+ import { PROTECTED_BRANCHES, hasAutoClose, validatePrRequest } from "./pr-request.mjs";
116
116
  import { summarize } from "./test-summary.mjs";
117
117
 
118
118
  const HERE = import.meta.dirname;
@@ -576,6 +576,20 @@ function checkRemoteBranch(stage, url, branch, headSha, prior) {
576
576
  }
577
577
 
578
578
  /** Gate ledger: the commit hook's requirement, re-checked for every commit in the range. */
579
+ // A closing keyword in a commit message closes the issue when the PR merges,
580
+ // whatever the PR text says, and issues are closed by people, not by a run.
581
+ function checkCommitMessages(stage, commits) {
582
+ for (const sha of commits) {
583
+ const message = stage.out(["log", "-1", "--format=%B", sha]) || "";
584
+ if (hasAutoClose(message)) {
585
+ throw new Refusal(
586
+ "auto-close",
587
+ `commit ${sha.slice(0, 12)} carries an auto-close keyword; use Ref: #N`,
588
+ );
589
+ }
590
+ }
591
+ }
592
+
579
593
  function checkLedger(state, commits, wt, env) {
580
594
  for (const c of commits) {
581
595
  const parent = gitOut(wt, ["rev-parse", "--verify", `${c}^1`], env);
@@ -1319,6 +1333,7 @@ export async function publish({ sessionId, statePath, item, config, env = proces
1319
1333
  const mergeBase = stage.out(["merge-base", baseSha, headSha]);
1320
1334
  if (!mergeBase) throw new Refusal("branch", `${req.branch} shares no history with ${req.base}`);
1321
1335
 
1336
+ checkCommitMessages(stage, commits);
1322
1337
  checkLedger(state, commits, wt, localEnv);
1323
1338
  checkTestLog(state, wt, statePath);
1324
1339
  checkSecrets(wt, `${mergeBase}..${headSha}`, env);
@@ -2086,6 +2086,26 @@ async function main() {
2086
2086
  return 0;
2087
2087
  }
2088
2088
 
2089
+ // ---- 3d. VERSION --------------------------------------------------------
2090
+ // Phase 0 of the child halts below the published floor, and the update it
2091
+ // would need is refused unattended. Checked here so a floor costs no session.
2092
+ const floorGate = join(SCRIPTS, "require-supported-version.sh");
2093
+ if (existsSync(floorGate)) {
2094
+ const floor = spawnSync("bash", [floorGate], { encoding: "utf-8", timeout: 30_000 });
2095
+ if (floor.status === 3) {
2096
+ const [, local, , required] = (floor.stdout || "").trim().split("|");
2097
+ const reason = `installed v${local || "?"} is below the required v${required || "?"}; run the update`;
2098
+ log(`not launching: ${reason}`);
2099
+ if (!DRY) {
2100
+ record({ source: next.source, id: next.id, outcome: OUTCOME.BLOCKED_VERSION_FLOOR });
2101
+ writeState("queue.json", { ...queue, blockedReason: reason });
2102
+ refreshStatus();
2103
+ }
2104
+ tick({ action: "version-floor" });
2105
+ return 0;
2106
+ }
2107
+ }
2108
+
2089
2109
  // ---- 4. TAKE -----------------------------------------------------------
2090
2110
  const sessionId = randomUUID();
2091
2111
  const taskId = `ap-${Date.now()}`;
@@ -81,6 +81,11 @@ PHASE_SCHEMA="$(node -e 'const c=require("'"$(cd "$(dirname "$0")" && pwd)"'/../
81
81
  TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
82
82
  LINE="{\"ts\":\"${TS}\",\"task_id\":\"${TASK_ID}\",\"phase\":\"${PHASE}\",\"event\":\"${EVENT}\",\"details\":${DETAILS},\"phaseSchema\":${PHASE_SCHEMA}}"
83
83
 
84
+ # Caller-supplied values can carry a credential copied out of an error.
85
+ # redact.mjs replaces it with a marker; without node the line is written as is.
86
+ REDACT="$(cd "$(dirname "$0")" && pwd)/../lib/redact.mjs"
87
+ [ -f "$REDACT" ] && LINE=$(printf '%s' "$LINE" | node "$REDACT" 2>/dev/null || printf '%s' "$LINE")
88
+
84
89
  # Atomic append (one line, well under PIPE_BUF).
85
90
  echo "$LINE" >> "$METRICS_FILE" 2>/dev/null || {
86
91
  echo "warn: log-metric: cannot write to $METRICS_FILE - telemetry skipped" >&2
@@ -46,7 +46,14 @@ export const FIELDS = ["branch", "base", "title", "body", "draft", "repo"];
46
46
  export const PROTECTED_BRANCHES = ["main", "master", "develop"];
47
47
  const REF = /^[A-Za-z0-9._/-]+$/;
48
48
  const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
49
- const AUTO_CLOSE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+(?:[\w.-]+\/[\w.-]+)?#\d+/i;
49
+ // GitHub closes an issue from `#N`, `owner/repo#N` and the issue's full URL.
50
+ const AUTO_CLOSE =
51
+ /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+(?:(?:[\w.-]+\/[\w.-]+)?#\d+|https?:\/\/[^\s/]+\/[\w.-]+\/[\w.-]+\/issues\/\d+)/i;
52
+
53
+ /** True when the text would close an issue once the PR or commit merges. */
54
+ export function hasAutoClose(text) {
55
+ return typeof text === "string" && AUTO_CLOSE.test(text);
56
+ }
50
57
 
51
58
  /**
52
59
  * The request object `write` produces. draft is always true.
@@ -92,8 +99,7 @@ export function validatePrRequest(req) {
92
99
  if (typeof req.branch === "string" && req.branch === req.base)
93
100
  errors.push("branch and base are the same");
94
101
  for (const k of ["title", "body"]) {
95
- if (typeof req[k] === "string" && AUTO_CLOSE.test(req[k]))
96
- errors.push(`${k} carries an auto-close keyword; use Ref: #N`);
102
+ if (hasAutoClose(req[k])) errors.push(`${k} carries an auto-close keyword; use Ref: #N`);
97
103
  }
98
104
  const secrets = [];
99
105
  for (const k of ["title", "body"]) {
@@ -26,7 +26,8 @@
26
26
  # Usage:
27
27
  # bash pipeline/scripts/update-check.sh # auto-detect local version
28
28
  # bash pipeline/scripts/update-check.sh --local 10.8.0 # explicit local version
29
- # bash pipeline/scripts/update-check.sh --ttl-hours 24 # cache window (default 24)
29
+ # bash pipeline/scripts/update-check.sh --ttl-hours 24 # cache window (default: prefs
30
+ # # updateCheck.ttlHours, else 24)
30
31
  # bash pipeline/scripts/update-check.sh --force # ignore cache
31
32
  # bash pipeline/scripts/update-check.sh --print-required # emit the floor only
32
33
  #
@@ -41,7 +42,8 @@ set -euo pipefail
41
42
  PKG="@mmerterden/multi-agent-pipeline"
42
43
  REGISTRY_URL="https://registry.npmjs.org/${PKG/\//%2F}"
43
44
  CACHE_FILE="${UPDATE_CHECK_CACHE:-$HOME/.claude/logs/multi-agent/.update-check}"
44
- TTL_HOURS=24
45
+ TTL_HOURS=""
46
+ FAIL_TTL_SECONDS=3600
45
47
  LOCAL_VERSION=""
46
48
  FORCE=0
47
49
  PRINT_REQUIRED=0
@@ -49,37 +51,65 @@ PRINT_REQUIRED=0
49
51
  while [ $# -gt 0 ]; do
50
52
  case "$1" in
51
53
  --local) LOCAL_VERSION="${2:-}"; shift; [ $# -gt 0 ] && shift ;;
52
- --ttl-hours) TTL_HOURS="${2:-24}"; shift; [ $# -gt 0 ] && shift ;;
54
+ --ttl-hours) TTL_HOURS="${2:-}"; shift; [ $# -gt 0 ] && shift ;;
53
55
  --force) FORCE=1; shift ;;
54
56
  --print-required) PRINT_REQUIRED=1; shift ;;
55
57
  *) shift ;;
56
58
  esac
57
59
  done
58
60
 
59
- # Local version: explicit arg, else read from the pipeline repo clone.
61
+ # Local version: explicit arg, else the marker the installer stamps. The
62
+ # marker is what is installed and what /multi-agent:update rewrites, so it is
63
+ # read before any repo clone; a clone is only a fallback for a machine that
64
+ # runs the pipeline from source without installing it.
60
65
  if [ -z "$LOCAL_VERSION" ]; then
61
- for candidate in "$HOME/multi-agent-pipeline" "$HOME/dev/multi-agent-pipeline" "$HOME/projects/multi-agent-pipeline"; do
62
- if [ -f "$candidate/package.json" ]; then
63
- LOCAL_VERSION=$(node -p "require('$candidate/package.json').version" 2>/dev/null || true)
66
+ for marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version" \
67
+ "$HOME/.codex/.pipeline-version"; do
68
+ if [ -f "$marker" ]; then
69
+ LOCAL_VERSION=$(head -1 "$marker" 2>/dev/null | tr -d '[:space:]')
64
70
  [ -n "$LOCAL_VERSION" ] && break
65
71
  fi
66
72
  done
67
73
  fi
68
- # npx-only installs have no repo clone; the installer stamps the version here.
69
74
  if [ -z "$LOCAL_VERSION" ]; then
70
- for marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version" \
71
- "$HOME/.codex/.pipeline-version"; do
72
- if [ -f "$marker" ]; then
73
- LOCAL_VERSION=$(head -1 "$marker" 2>/dev/null | tr -d '[:space:]')
75
+ for candidate in "$HOME/multi-agent-pipeline" "$HOME/dev/multi-agent-pipeline" "$HOME/projects/multi-agent-pipeline"; do
76
+ if [ -f "$candidate/package.json" ]; then
77
+ LOCAL_VERSION=$(node -p "require('$candidate/package.json').version" 2>/dev/null || true)
74
78
  [ -n "$LOCAL_VERSION" ] && break
75
79
  fi
76
80
  done
77
81
  fi
78
82
  [ -z "$LOCAL_VERSION" ] && exit 0 # cannot determine local version -> silent no-op
79
83
 
80
- # Highest of two dotted versions; ties resolve to the second argument.
81
- highest_of() {
82
- printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1
84
+ # Cache lifetime: --ttl-hours, else prefs global.updateCheck.ttlHours, else 24.
85
+ # Anything that is not a whole number falls back to 24.
86
+ if [ -z "$TTL_HOURS" ]; then
87
+ PREFS_FILE="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
88
+ if [ -f "$PREFS_FILE" ] && command -v jq >/dev/null 2>&1; then
89
+ TTL_HOURS=$(jq -r '.global.updateCheck.ttlHours // empty' "$PREFS_FILE" 2>/dev/null || true)
90
+ fi
91
+ fi
92
+ case "$TTL_HOURS" in (""|*[!0-9]*) TTL_HOURS=24 ;; esac
93
+
94
+ # 0 when $1 sorts strictly below $2. Semver order: numeric major.minor.patch,
95
+ # and a prerelease (1.2.3-beta) below its release (1.2.3). A leading v is ignored.
96
+ version_lt() {
97
+ local a="${1#v}" b="${2#v}" ac ap bc bp i x y
98
+ ac="${a%%-*}"; bc="${b%%-*}"
99
+ ap=""; bp=""
100
+ [ "$ac" != "$a" ] && ap="${a#*-}"
101
+ [ "$bc" != "$b" ] && bp="${b#*-}"
102
+ for i in 1 2 3; do
103
+ x=$(printf '%s' "$ac" | cut -d. -f"$i"); y=$(printf '%s' "$bc" | cut -d. -f"$i")
104
+ case "$x" in (""|*[!0-9]*) x=0 ;; esac
105
+ case "$y" in (""|*[!0-9]*) y=0 ;; esac
106
+ [ "$x" -lt "$y" ] && return 0
107
+ [ "$x" -gt "$y" ] && return 1
108
+ done
109
+ [ -n "$ap" ] && [ -z "$bp" ] && return 0
110
+ [ -z "$ap" ] && [ -n "$bp" ] && return 1
111
+ [ -n "$ap" ] && [ "$(printf '%s\n%s\n' "$ap" "$bp" | sort | head -1)" = "$ap" ] && [ "$ap" != "$bp" ] && return 0
112
+ return 1
83
113
  }
84
114
 
85
115
  now=$(date +%s)
@@ -92,6 +122,8 @@ if [ "$FORCE" -eq 0 ] && [ -f "$CACHE_FILE" ]; then
92
122
  cached_latest=$(cut -d'|' -f2 "$CACHE_FILE" 2>/dev/null || echo "")
93
123
  cached_required=$(cut -d'|' -f3 "$CACHE_FILE" 2>/dev/null || echo "")
94
124
  case "$cached_epoch" in (*[!0-9]*|"") cached_epoch=0 ;; esac
125
+ # A stamp in the future is a clock or copy mistake, not a fresh answer.
126
+ [ "$cached_epoch" -gt "$now" ] && cached_epoch=0
95
127
  if [ $((now - cached_epoch)) -lt $((TTL_HOURS * 3600)) ] && [ -n "$cached_latest" ]; then
96
128
  latest="$cached_latest"
97
129
  required="$cached_required"
@@ -100,6 +132,16 @@ fi
100
132
 
101
133
  # Stale or missing cache -> one bounded registry call (silent on any failure).
102
134
  # The abbreviated packument carries dist-tags at a quarter of the full document.
135
+ # A failed read is remembered for FAIL_TTL_SECONDS in "<cache>.fail", so an
136
+ # offline machine does not wait out the timeout on every call.
137
+ if [ -z "$latest" ] && [ "$FORCE" -eq 0 ] && [ -f "$CACHE_FILE.fail" ]; then
138
+ failed_at=$(head -1 "$CACHE_FILE.fail" 2>/dev/null | tr -d '[:space:]')
139
+ case "$failed_at" in (*[!0-9]*|"") failed_at=0 ;; esac
140
+ if [ "$failed_at" -le "$now" ] && [ $((now - failed_at)) -lt "$FAIL_TTL_SECONDS" ]; then
141
+ exit 0
142
+ fi
143
+ fi
144
+
103
145
  if [ -z "$latest" ]; then
104
146
  tags=$(curl -sm 3 -H 'Accept: application/vnd.npm.install-v1+json' "$REGISTRY_URL" 2>/dev/null \
105
147
  | { if command -v jq >/dev/null 2>&1; then
@@ -110,14 +152,25 @@ if [ -z "$latest" ]; then
110
152
  latest="${tags%%|*}"
111
153
  required="${tags#*|}"
112
154
  [ "$required" = "$tags" ] && required=""
113
- [ -z "$latest" ] && exit 0
114
155
  mkdir -p "$(dirname "$CACHE_FILE")" 2>/dev/null || exit 0
115
- printf '%s|%s|%s\n' "$now" "$latest" "$required" > "$CACHE_FILE" 2>/dev/null || true
156
+ if [ -z "$latest" ]; then
157
+ printf '%s\n' "$now" > "$CACHE_FILE.fail" 2>/dev/null || true
158
+ exit 0
159
+ fi
160
+ rm -f "$CACHE_FILE.fail" 2>/dev/null || true
161
+ # Written through a temp file and a rename, so a concurrent reader never
162
+ # sees a half-written line.
163
+ tmp_cache=$(mktemp "$CACHE_FILE.XXXXXX" 2>/dev/null) || exit 0
164
+ if printf '%s|%s|%s\n' "$now" "$latest" "$required" > "$tmp_cache" 2>/dev/null; then
165
+ mv -f "$tmp_cache" "$CACHE_FILE" 2>/dev/null || rm -f "$tmp_cache"
166
+ else
167
+ rm -f "$tmp_cache"
168
+ fi
116
169
  fi
117
170
 
118
171
  # A floor above latest is a publisher mistake (the `required` tag was moved to a
119
172
  # version that `latest` no longer covers). Clamp rather than brick every user.
120
- if [ -n "$required" ] && [ "$(highest_of "$required" "$latest")" != "$latest" ]; then
173
+ if [ -n "$required" ] && version_lt "$latest" "$required"; then
121
174
  required="$latest"
122
175
  fi
123
176
 
@@ -127,8 +180,7 @@ if [ "$PRINT_REQUIRED" -eq 1 ]; then
127
180
  fi
128
181
 
129
182
  # Forced update: local sorts strictly BELOW the required floor.
130
- if [ -n "$required" ] && [ "$LOCAL_VERSION" != "$required" ] \
131
- && [ "$(highest_of "$LOCAL_VERSION" "$required")" = "$required" ]; then
183
+ if [ -n "$required" ] && version_lt "$LOCAL_VERSION" "$required"; then
132
184
  printf '%s|%s|force\n' "$LOCAL_VERSION" "$latest"
133
185
  exit 0
134
186
  fi
@@ -137,7 +189,7 @@ fi
137
189
 
138
190
  # Update available only when latest sorts strictly ABOVE local (a dev machine
139
191
  # running ahead of the registry must not see an "update" prompt).
140
- if [ "$(highest_of "$LOCAL_VERSION" "$latest")" = "$latest" ]; then
192
+ if version_lt "$LOCAL_VERSION" "$latest"; then
141
193
  printf '%s|%s\n' "$LOCAL_VERSION" "$latest"
142
194
  fi
143
195
  exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-25T15:14:42Z",
3
+ "generatedAt": "2026-09-25T16:25:54Z",
4
4
  "skillCount": 218,
5
5
  "entries": [
6
6
  {
@@ -29,7 +29,7 @@
29
29
  },
30
30
  {
31
31
  "path": "shared/core/multi-agent-autopilot-on/SKILL.md",
32
- "sha256": "ca671ffe46809e3b0c825a6b5c19e8ea00e529874531eb480a0987428366d2f4"
32
+ "sha256": "d4f23f6f7bb493a4efa2cfa252fab50b22b6506aeaf31985db4498b5f010c8c8"
33
33
  },
34
34
  {
35
35
  "path": "shared/core/multi-agent-autopilot-status/SKILL.md",
@@ -74,7 +74,7 @@ The same name works on both sides and both mechanisms already exist:
74
74
 
75
75
  | Source | Query | Extra condition |
76
76
  |---|---|---|
77
- | GitHub | `gh issue list --repo <r> --label <label> --state open` | none - the label is the whole gate |
77
+ | GitHub | `gh issue list --repo <r> --label <label> --assignee @me --state open` | **assigned to you** (`githubAssignee`, `"*"` for any), so a label an issue form applies is not enough |
78
78
  | Jira | `assignee = currentUser() AND labels = "<label>" AND resolution = EMPTY AND status not in (Done, Closed, Cancelled)` | **assigned to you**, so a label somebody else adds is not enough |
79
79
 
80
80
  Two Jira caveats worth saying out loud when Jira is selected: labels are one