@mmerterden/multi-agent-pipeline 20.11.1 → 20.12.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 +15 -0
- package/docs/facts.json +2 -2
- package/install/claude.mjs +9 -0
- package/manifest.json +22 -21
- package/package.json +5 -5
- package/pipeline/agents/code-reviewer.md +1 -1
- package/pipeline/commands/multi-agent/model/SKILL.md +7 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +5 -6
- package/pipeline/lib/agent-model-sync.mjs +95 -0
- package/pipeline/lib/model-rung.sh +7 -0
- package/pipeline/multi-agent-refs/features/doctor.md +10 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +10 -0
- package/pipeline/multi-agent-refs/features/phone-api.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/multi-agent-refs/features/usage-reporting.md +13 -0
- package/pipeline/scripts/doctor.mjs +48 -0
- package/pipeline/scripts/migrate-prefs.mjs +14 -0
- package/pipeline/scripts/unattended_policy.py +12 -5
- package/pipeline/scripts/usage-report.mjs +84 -9
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,21 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [20.12.0] - 2026-10-01
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `usage-report.mjs` records the outcome of every send attempt in `~/.claude/logs/multi-agent/usage-last-send.json`: `accepted`, `not-accepted`, `rejected` with the status code, `redirected` with the host it pointed to, `failed`, or `skipped` with its reason. It carries the run id, the event and the endpoint host, never the token. Any outcome other than `accepted` also prints one line on stderr. Previously the response was discarded, so a failed send left no trace on the panel or the machine.
|
|
24
|
+
- `/multi-agent:doctor` gains the `usage-report` check: it reads that record and turns anything but `accepted` into a WARN with the step that fixes it.
|
|
25
|
+
- `lib/agent-model-sync.mjs`: the installed agents whose `preferredModel` is `fable` (code-reviewer, plan-critic, the iOS, Android and backend architects) have their `model:` line written from `modelFallback.fableEnabled`, `fable` when on and `opus` when off. `/multi-agent:model on|off` re-applies it on every flip and the installer on every install, so the one switch covers agents spawned without an explicit model as well as routed dispatches. Sources keep `model: fable`; `/multi-agent:sync` normalizes the repo copy back with `--fable-on`.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `migrate-prefs.mjs` rewrites a `usageLog.endpoint` on `mmerterden.vercel.app` to `mmerterden.com`. The platform host answers every request with a 308, which the usage clients never follow.
|
|
30
|
+
- The unattended guard resolves a directory's worktree root once per decision instead of up to three times, which cuts a program-provenance check from seven git calls to five.
|
|
31
|
+
- `test/unattended-guard.test.mjs` is now `test/unattended-guard.serial.mjs` and runs after the parallel `test/*.test.mjs` pool. Inside the pool its guard decisions could outlast the guard's 5-second internal deadline, and the guard fails closed when that happens.
|
|
32
|
+
- Lockfile: `brace-expansion` 5.0.12 and `fast-uri` 3.1.8 (transitive), which clears the high and moderate advisories the scorecard gates on.
|
|
33
|
+
|
|
19
34
|
## [20.11.1] - 2026-09-29
|
|
20
35
|
|
|
21
36
|
### 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
|
-
"generatedAt": "2026-
|
|
4
|
-
"version": "20.
|
|
3
|
+
"generatedAt": "2026-10-01",
|
|
4
|
+
"version": "20.12.0",
|
|
5
5
|
"phaseSchema": 2,
|
|
6
6
|
"phases": [
|
|
7
7
|
{
|
package/install/claude.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import { EXTERNAL_SKILLS_MANIFEST } from "./_platform-filter.mjs";
|
|
46
46
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
47
47
|
import { registerMcpServer } from "./_mcp-register.mjs";
|
|
48
|
+
import { applyFableRung, fableEnabled } from "../pipeline/lib/agent-model-sync.mjs";
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* A matcher an older install wrote. Hook matchers only ever see the tool name,
|
|
@@ -371,6 +372,14 @@ function installAgents(pipelineSrc, dest, useSymlinks) {
|
|
|
371
372
|
}
|
|
372
373
|
copyDir(agentsSrc, target, { useSymlinks });
|
|
373
374
|
console.log(` -> ${countFiles(agentsSrc)} files copied to ${dest}`);
|
|
375
|
+
if (useSymlinks || isDryRun()) return;
|
|
376
|
+
const on = fableEnabled(join(dirname(dest), "multi-agent-preferences.json"));
|
|
377
|
+
const changed = applyFableRung(target, on);
|
|
378
|
+
if (changed.length) {
|
|
379
|
+
console.log(
|
|
380
|
+
` -> ${changed.length} fable-tier agents on ${on ? "fable" : "opus"} (fableEnabled)`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
374
383
|
}
|
|
375
384
|
|
|
376
385
|
function installRules(pipelineSrc, dest, useSymlinks) {
|
package/manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "20.
|
|
4
|
-
"commit": "
|
|
5
|
-
"generatedAt": "2026-
|
|
3
|
+
"version": "20.12.0",
|
|
4
|
+
"commit": "67faa4d431ba4b6d942630d6acbcfedbec220c9a",
|
|
5
|
+
"generatedAt": "2026-10-01T18:29:41.758Z",
|
|
6
6
|
"algorithm": "sha256",
|
|
7
|
-
"fileCount":
|
|
7
|
+
"fileCount": 1231,
|
|
8
8
|
"files": {
|
|
9
|
-
"CHANGELOG.md": "
|
|
9
|
+
"CHANGELOG.md": "787a8bd3432c3fdbbf572f8d8ef13c548291f0a46691abba28811943d516eaef",
|
|
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": "
|
|
36
|
+
"docs/facts.json": "8f5a257e848433f3c3deca22a452e5407d7853913b5c7f60a304ff5a5aab4fee",
|
|
37
37
|
"docs/features.md": "6c9f0efb00c683e6f611c440c87847b187f8b0eee59e9e812f3a241695bc2ae1",
|
|
38
38
|
"docs/performance.md": "bbf2f3922c25dcba0c1a9e69ef420e3871a729af5b35009a49b48a5d3c5cd151",
|
|
39
39
|
"docs/recovery-guide.md": "59a8f1b66b34cb9ff5507b3d54dac3b9915e184aa044d9c758d18cac818d510e",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"install/_telemetry.mjs": "5433d753f909beed2c8789272bba05e245482c472f83d3444f58000c4c9a634b",
|
|
53
53
|
"install/_unattended-profile.mjs": "e1ed82bde35d19105ee12c2a551eb778912b0e5c93ab227fd15c993098d98de0",
|
|
54
54
|
"install/catalog-history.json": "15422563f83ca98278f94ae3e34730112e65888262e1da0442768352f46a75d4",
|
|
55
|
-
"install/claude.mjs": "
|
|
55
|
+
"install/claude.mjs": "6af22bb9b2594aee502c36550020d2c9c4c3222061ed8a88a0885687cd91ebec",
|
|
56
56
|
"install/codex.mjs": "2c2238cd9dd2f95b4a50a505387e8e49ddc1513ae13998f05f1ed3e3a71b18e7",
|
|
57
57
|
"install/copilot.mjs": "300a74f17084d1a38f16428ca7477394c29e33cd755156e19b92e457999be234",
|
|
58
58
|
"install/index.mjs": "07ec26ecdd81cc983f146faf4b7095c98edce39c89a6fd83c24ce43229d2d9e7",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"install/templates/multi-agent-autopilot-awake.plist.template": "ddd25c76c56fcbb38a89de8e35637372d70ace993d6bc487a88ca9cfb702fe1f",
|
|
63
63
|
"install/templates/multi-agent-autopilot.plist.template": "122c8a61cd927edd99375d6e3da40f1d42bcceed5b6641785c8b28bb930bc77e",
|
|
64
64
|
"install/unattended-profile-legacy.json": "f52e86b998956f09ad306b1735a6c10b58580d7082d8a55665a4c6bb15c3aa1f",
|
|
65
|
-
"package.json": "
|
|
65
|
+
"package.json": "64ce800c06e8882db62e426c03d3f729ec4344f44d9c6f50602d539c319ec054",
|
|
66
66
|
"pipeline/agents/android-architect.md": "11fb74543cb0ebcf9525ca6654fe95d53da7229e0bf4a1149b31fd9005abc453",
|
|
67
67
|
"pipeline/agents/backend-architect.md": "b02a606c301e4714131aed70ae6d16eeb77fbc819a001e2752cff2488d4246de",
|
|
68
68
|
"pipeline/agents/bulk-reader.md": "6597052cd7d205bac2b1c7633f5232fe1a40730664a913b8eb77efd5a786dc25",
|
|
69
|
-
"pipeline/agents/code-reviewer.md": "
|
|
69
|
+
"pipeline/agents/code-reviewer.md": "213798685e9bff10a08f0774f57bcfbab42ee15993095ec3a2c84e13a3453725",
|
|
70
70
|
"pipeline/agents/dev-critic.md": "33843216b79977ce2926fc9c54f8c9eef30b91632a11ef624647229b61a9b36a",
|
|
71
71
|
"pipeline/agents/explorer.md": "12564aec29d4581684bf84313b86a4add5a6989d3b8db5c35769674cab5649c6",
|
|
72
72
|
"pipeline/agents/ios-architect.md": "e1fe0c034a4dfa76794ed04ba9f4bf85d5cdd3a5d6b948cc0635e8dda8baeef9",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"pipeline/commands/multi-agent/language/SKILL.md": "4b58b25bda9449a52df005f8d3417ace2f8b3a18d84524dc021a9561f1d16b9d",
|
|
105
105
|
"pipeline/commands/multi-agent/log/SKILL.md": "033bafcf4c9b8d117a892f50a3039fd11d753b4db070ed14c50e8388613482fa",
|
|
106
106
|
"pipeline/commands/multi-agent/manual-test/SKILL.md": "7b81b1ff572fb4865495283c39f93d5c803fff2e8ce9008bf1a2ab51046c3d38",
|
|
107
|
-
"pipeline/commands/multi-agent/model/SKILL.md": "
|
|
107
|
+
"pipeline/commands/multi-agent/model/SKILL.md": "56b636f8ab42c24defd3d1260f1a5e4dd332f4556d9b6d1a700322cbfa6f4f3d",
|
|
108
108
|
"pipeline/commands/multi-agent/prune-logs/SKILL.md": "a7b22228ec338623c386cfd3744092488eed6871c65b531bdb6a0eb8dc8c2e9b",
|
|
109
109
|
"pipeline/commands/multi-agent/prune-prompts/SKILL.md": "1aefd2b737b34aaff18f3fd12090322c04dbc41c981200622d7989c9ad45e9c2",
|
|
110
110
|
"pipeline/commands/multi-agent/purge/SKILL.md": "c881805f80d7db909bcf09f1a5c980ebaf0c31d9a8ee9cae4f729387ff4ca19d",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"pipeline/commands/multi-agent/status/SKILL.md": "a2ed316434aca49f692aafe11f95873822b9b955558a53f4f6a9667ee32a3659",
|
|
131
131
|
"pipeline/commands/multi-agent/steer/SKILL.md": "07e738c228ba328f717f86e97e7af207df090b16433e28e613d430c3ff0aee3e",
|
|
132
132
|
"pipeline/commands/multi-agent/store-ready/SKILL.md": "2fd7a908ee84669361bded693e7af53a17eb811e27cb4aa94237c0fce80591fa",
|
|
133
|
-
"pipeline/commands/multi-agent/sync/SKILL.md": "
|
|
133
|
+
"pipeline/commands/multi-agent/sync/SKILL.md": "8d2a1d86a97cb27c4af5feba0eaa64bc937f979c3cba1e38dc1821557c243300",
|
|
134
134
|
"pipeline/commands/multi-agent/test-accessibility/SKILL.md": "d62992541303a0776ca088d5f3d5475108fd3fba5e25af04efb3ea4bbbd1fe21",
|
|
135
135
|
"pipeline/commands/multi-agent/test-dark-mode/SKILL.md": "6bfe158c94f76fc6f4d06454978d0c5cad6ec00645ab48c1d1cee15ad93ae095",
|
|
136
136
|
"pipeline/commands/multi-agent/test-dynamic-type/SKILL.md": "c093fe1a78a4266ad4cf1e48b3cc233d9abd7e1c524d0b8e5db0c81d8b4d58fd",
|
|
@@ -162,6 +162,7 @@
|
|
|
162
162
|
"pipeline/contract/types/index.d.ts": "5b738dd173c4946f8dae9cb926ad63b8adac68b77000085895608bfd87ac1aa9",
|
|
163
163
|
"pipeline/lib/_jira-auth.sh": "fc468736057f8841d824eb1a4db527442562f8979fea333ebaef5527f4369e9f",
|
|
164
164
|
"pipeline/lib/account-resolver.sh": "f7bef9beba3f839268509361651be0b1e1f319f2f23821f90ba466fdf185a8f6",
|
|
165
|
+
"pipeline/lib/agent-model-sync.mjs": "2e0264ca8d8fef761563db219d588fed9067847043f960f68f055c186c20b668",
|
|
165
166
|
"pipeline/lib/analysis-jira-write.sh": "477e7b4e2aaaa8289ab0cdb315f04c3a9b22beb8f075bbb73b6fa174d6679197",
|
|
166
167
|
"pipeline/lib/ask-choice.sh": "b77282645cf64d016cdebf43d1ee299874a1411f5c51b6ae3b6c580d1bbfe78a",
|
|
167
168
|
"pipeline/lib/autopilot-activation.sh": "29102f7936ea8b8238fac4e9c557bdbb39e2ea5afdc3b6cdd20622593d36da9b",
|
|
@@ -193,7 +194,7 @@
|
|
|
193
194
|
"pipeline/lib/json-file-lock.mjs": "6ff8d8d7b890fc549e07287eca1e8261b69f930ec523d08fb73786677aa0a45a",
|
|
194
195
|
"pipeline/lib/md2confluence-v3.py": "3677ca47931e4b3d0fd2ce3e11299fc5f6d24799eb8332e37dd60b6c3c4653a2",
|
|
195
196
|
"pipeline/lib/model-dispatch.sh": "7edd5c92d41ee3bf74972e0827ccbb671853d5410e7fb0af0b7cb020f95e05c5",
|
|
196
|
-
"pipeline/lib/model-rung.sh": "
|
|
197
|
+
"pipeline/lib/model-rung.sh": "8cecb4d390f306270713bd162e311dee879995e393970405c9a8f9af11da1727",
|
|
197
198
|
"pipeline/lib/multi-repo-pipeline.sh": "5cca6bcc4fb857448ddadb31add343952be89423a328267e56496c7b7cdc7710",
|
|
198
199
|
"pipeline/lib/normalize-text.mjs": "c2eeed65ab85cdc27a12ecaadc45dfa00b49a7611250c9ad881b9e93882a51c1",
|
|
199
200
|
"pipeline/lib/outbound-gate.mjs": "1ebc840e975f19b0da4699b3a37361ecc8a2be1a7a558d2b2fb39b8b64244011",
|
|
@@ -263,13 +264,13 @@
|
|
|
263
264
|
"pipeline/multi-agent-refs/features/cost-analysis.md": "42f1b90b41ef454f29ccfd3f93c959920f0e74cbca65e140695e9a30d6d90b87",
|
|
264
265
|
"pipeline/multi-agent-refs/features/design-conformance.md": "a88ca595827cafa0c87c2facfcf9190dac69485e0596e5c198c6fc1613880452",
|
|
265
266
|
"pipeline/multi-agent-refs/features/dev-critic.md": "08e01edef04b5ac216f8f43332126966d33da7b95625e47a8a6cd8ac105b263f",
|
|
266
|
-
"pipeline/multi-agent-refs/features/doctor.md": "
|
|
267
|
+
"pipeline/multi-agent-refs/features/doctor.md": "708aba37c507bf7559af816e6328a11ba0acdc6d61d023bc10afc727d068876a",
|
|
267
268
|
"pipeline/multi-agent-refs/features/external-context-injection.md": "fe0373f27c3e9aae660a55a3ba9a8163df329b4d95c727d6023b6b3afaf71d66",
|
|
268
269
|
"pipeline/multi-agent-refs/features/jira-context.md": "7d14802ed9ad1747e1a06dec26f22ff9e8959e03c47d7a018af45b8274aea47c",
|
|
269
270
|
"pipeline/multi-agent-refs/features/maturity-followup.md": "e663e5bcf0b3b4d4d2a67f4516dbb9fb12520dfbdedb47a9c5f57c13a9a0633e",
|
|
270
|
-
"pipeline/multi-agent-refs/features/model-fallback.md": "
|
|
271
|
+
"pipeline/multi-agent-refs/features/model-fallback.md": "e6b9635124f3557013dfbfa71d414a9d9abe9ddc89f2228a5496086c8e3b0c72",
|
|
271
272
|
"pipeline/multi-agent-refs/features/package-manager.md": "b9e52f798f5dcba43abff1fe5cad8d5d5374058c9a6847c993975c665acab05a",
|
|
272
|
-
"pipeline/multi-agent-refs/features/phone-api.md": "
|
|
273
|
+
"pipeline/multi-agent-refs/features/phone-api.md": "817eeaffd3772b0a80811b0cd17dee3498f08f6eccf8fd7e56deb0bc24c1c434",
|
|
273
274
|
"pipeline/multi-agent-refs/features/plan-critic.md": "d218c8599d893211480a412d46efdbba4aa48e3b18e8bb0ebfd66782f3b8ebc5",
|
|
274
275
|
"pipeline/multi-agent-refs/features/plan-todos.md": "8780d80b4541daee2b05080759a0694ef7e6270abe9a4621deb92ec7bac94e8e",
|
|
275
276
|
"pipeline/multi-agent-refs/features/prior-fix-detection.md": "9bd172fa116b0c334576ee3df2dc478810a7e282422410e045be74ac2299253d",
|
|
@@ -288,9 +289,9 @@
|
|
|
288
289
|
"pipeline/multi-agent-refs/features/stack-adapters.md": "71e23385da872ceed35263f4838f154800aa2da55bb6688ccca74726b70a3ef5",
|
|
289
290
|
"pipeline/multi-agent-refs/features/stack-skill-routing.md": "1989f7f64be6c5ee0a42d8e9962aead7cd7472d795915ab953d89d9382f317e6",
|
|
290
291
|
"pipeline/multi-agent-refs/features/unattended-gates.md": "46b516cac81a4eacf5d58bc3b1613e2266c26f1559be8dac060d1dc6b7d7ca01",
|
|
291
|
-
"pipeline/multi-agent-refs/features/unattended-security.md": "
|
|
292
|
+
"pipeline/multi-agent-refs/features/unattended-security.md": "18da3d51c175a42ab6312051a50b9d0339524dad2cc34e0350de62fbf0cece91",
|
|
292
293
|
"pipeline/multi-agent-refs/features/url-enrichment.md": "72e01a799d9fa54b56c762ba404a9f9236adbbf98320e395a96f6b0f5917e76b",
|
|
293
|
-
"pipeline/multi-agent-refs/features/usage-reporting.md": "
|
|
294
|
+
"pipeline/multi-agent-refs/features/usage-reporting.md": "095d86b839c1289c1023919da3972e561c1bbd4a32a1fc9f8e694abe887005ac",
|
|
294
295
|
"pipeline/multi-agent-refs/features/verify-by-test.md": "b0735adaa7ead797167b8d9ea48a10851a138083c6e638738cc96c66b02a1e37",
|
|
295
296
|
"pipeline/multi-agent-refs/features/verify.md": "1a6db5e7239eac01e51100353013abd16bf785a7fce88b40139fca27f4166be4",
|
|
296
297
|
"pipeline/multi-agent-refs/features/visual-evidence.md": "2b6aa2e4ff05c7a1fbe9e0fedfa4b28b9ad01c93a64f1ca6f6cd3e02293866aa",
|
|
@@ -481,7 +482,7 @@
|
|
|
481
482
|
"pipeline/scripts/crush-json.mjs": "303382670f8f170c8c6710202853a2de2915625813cd157185f9be4e2b27258e",
|
|
482
483
|
"pipeline/scripts/diff-explain.mjs": "194d0635639607cb1af3de064453162e01c0d59dc0c2e187d21d45bae3bc559c",
|
|
483
484
|
"pipeline/scripts/diff-risk-score.mjs": "b3e8da42fea1258162e40f4eb7809074831339b7563874527f7e825f7aa795ca",
|
|
484
|
-
"pipeline/scripts/doctor.mjs": "
|
|
485
|
+
"pipeline/scripts/doctor.mjs": "0ac54a2630fc00b39bdf430f9466f91e6b52296a23dca8694ac1a1bd894bcfea",
|
|
485
486
|
"pipeline/scripts/evidence-gate.mjs": "347a8d3fa1d17d996de345585c216bd7071f4c1d687999daf6e48881720ea5df",
|
|
486
487
|
"pipeline/scripts/feedback-send.mjs": "d3ed66e9dacdfb4b5eacf1b244bf66752304fb4fcb144531441b458f3ab806be",
|
|
487
488
|
"pipeline/scripts/finding-fingerprint.mjs": "fa12ab10701db5d2bfff014e41d1cd4c0b2b772aee5fc69205de14b2761b0c83",
|
|
@@ -518,7 +519,7 @@
|
|
|
518
519
|
"pipeline/scripts/maturity-followup.mjs": "531eb9578a71ea5bb2510bb24a48f264c36aaf7adceca7b4ec3dccd1c63ae412",
|
|
519
520
|
"pipeline/scripts/memory-load.sh": "c6a1976372ed033c9f3b55e757e22d05b4ef0d9774bfe185453d8847525b4a89",
|
|
520
521
|
"pipeline/scripts/memory-save.sh": "ab6b4c3d78707e0ac66fe56726308782568f247463d4c7edf2316b2dd399ee6b",
|
|
521
|
-
"pipeline/scripts/migrate-prefs.mjs": "
|
|
522
|
+
"pipeline/scripts/migrate-prefs.mjs": "9d38ef7dac14c48bf1c6c8053680999728d3179c598e6c9e53f5a66290fc323e",
|
|
522
523
|
"pipeline/scripts/migrate-state.mjs": "b6fb42f56ea56ab10c00e947f013e3aecaeef1aa0d819493eedbef7cdf7d4370",
|
|
523
524
|
"pipeline/scripts/note-session.sh": "1466d93f401c66074705635117a052a3f811ca22f928defa1ec2e4c804c19fc1",
|
|
524
525
|
"pipeline/scripts/observations.mjs": "edef08a8ab8f967030da0575d83f501eb6256460c0b72c2ba377c629b3ee0674",
|
|
@@ -583,13 +584,13 @@
|
|
|
583
584
|
"pipeline/scripts/token-budget-report.mjs": "508bbc659df478c535c0ecdd3f8611372af8fef0b0932c7b19a247af2b2b9265",
|
|
584
585
|
"pipeline/scripts/triage-memory.mjs": "2d8541145f1749593ae5728fb4c9eb3001484edb5e4d37be473aeec34d1dbe7c",
|
|
585
586
|
"pipeline/scripts/ui-tree-dumper.swift": "a09e42ef069eafa08c05afd8333e05b754ef6a7d8b9687712039b0f2c9801273",
|
|
586
|
-
"pipeline/scripts/unattended_policy.py": "
|
|
587
|
+
"pipeline/scripts/unattended_policy.py": "f1df6470c798bd7d4bc9f8338eaabf98bdcfab00198d98b7e2e654aab84b709c",
|
|
587
588
|
"pipeline/scripts/uninstall.mjs": "880923522ef59fdd98cb7ecb1bdc85c0f15d1e2eed2d79d6d353e88712ca95fa",
|
|
588
589
|
"pipeline/scripts/update-check.sh": "9c00647e7ca4249414b2e5af08f61705ab5bd2106b6f66ae8ebce8aa143dc957",
|
|
589
590
|
"pipeline/scripts/update-issue-progress.sh": "ccfef87743e91272e009e396ac07461d2784b2fa84a06c802a582526eeeb9ed9",
|
|
590
591
|
"pipeline/scripts/usage-identity.mjs": "47c1f907fb1ab47672687ea0cbf2d0737985ae91ec2b48b4fe260e14c3d35d70",
|
|
591
592
|
"pipeline/scripts/usage-register.mjs": "cc19d7ce5efe57255ce1521d33ad4b9a75b6833385ed6edcc5e339faf3c6e06c",
|
|
592
|
-
"pipeline/scripts/usage-report.mjs": "
|
|
593
|
+
"pipeline/scripts/usage-report.mjs": "af1672206b1fa7d6612fa70aa46fd63372552619fdba866d89819a12ba943549",
|
|
593
594
|
"pipeline/scripts/validate-analysis-doc.mjs": "ae56b7578ef51e008a99f84eba9def3e0f1dd2bd87a3880f2083aa98b365f7a1",
|
|
594
595
|
"pipeline/scripts/validate-analysis.mjs": "d2dc9ccf45f6ca2702908761939edd830d18b3b273016a4b449f26fe285d5ea1",
|
|
595
596
|
"pipeline/scripts/validate-code-graph.mjs": "0ceac2f5b59f641334d54dc30bee5265361fb5fe1fc97e2f9791b546efe79941",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.12.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",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node index.js",
|
|
23
|
-
"test": "npm run format:check && npm run lint && node --test test/*.test.mjs && node pipeline/scripts/run-smokes.mjs && node pipeline/scripts/lint-skills.mjs && node pipeline/scripts/gen-skills-index.mjs --check && node pipeline/scripts/gen-catalog-history.mjs --check && node pipeline/scripts/lint-personas.mjs && node pipeline/scripts/lint-mcp-refs.mjs && node pipeline/scripts/eval-triage.mjs && node pipeline/scripts/eval-golden-tasks.mjs && node pipeline/scripts/eval-intent.mjs && node pipeline/scripts/eval-recall.mjs && node pipeline/scripts/eval-guard-robustness.mjs && node pipeline/scripts/validate-schemas.mjs && node pipeline/scripts/validate-prefs.mjs && node pipeline/scripts/scorecard.mjs",
|
|
24
|
-
"test:unit": "node --test test/*.test.mjs",
|
|
23
|
+
"test": "npm run format:check && npm run lint && node --test test/*.test.mjs && node --test test/*.serial.mjs && node pipeline/scripts/run-smokes.mjs && node pipeline/scripts/lint-skills.mjs && node pipeline/scripts/gen-skills-index.mjs --check && node pipeline/scripts/gen-catalog-history.mjs --check && node pipeline/scripts/lint-personas.mjs && node pipeline/scripts/lint-mcp-refs.mjs && node pipeline/scripts/eval-triage.mjs && node pipeline/scripts/eval-golden-tasks.mjs && node pipeline/scripts/eval-intent.mjs && node pipeline/scripts/eval-recall.mjs && node pipeline/scripts/eval-guard-robustness.mjs && node pipeline/scripts/validate-schemas.mjs && node pipeline/scripts/validate-prefs.mjs && node pipeline/scripts/scorecard.mjs",
|
|
24
|
+
"test:unit": "node --test test/*.test.mjs && node --test test/*.serial.mjs",
|
|
25
25
|
"test:smoke": "node pipeline/scripts/run-smokes.mjs",
|
|
26
26
|
"lint:skills": "node pipeline/scripts/lint-skills.mjs",
|
|
27
|
-
"test:quick": "node --test test/*.test.mjs && node pipeline/scripts/lint-skills.mjs && node pipeline/scripts/lint-personas.mjs && node pipeline/scripts/lint-mcp-refs.mjs",
|
|
28
|
-
"test:coverage": "c8 --clean --reporter=none node --test test/*.test.mjs && c8 --clean=false --check-coverage --reporter=text --reporter=lcov node pipeline/scripts/run-smokes.mjs",
|
|
27
|
+
"test:quick": "node --test test/*.test.mjs && node --test test/*.serial.mjs && node pipeline/scripts/lint-skills.mjs && node pipeline/scripts/lint-personas.mjs && node pipeline/scripts/lint-mcp-refs.mjs",
|
|
28
|
+
"test:coverage": "c8 --clean --reporter=none node --test test/*.test.mjs && c8 --clean=false --reporter=none node --test test/*.serial.mjs && c8 --clean=false --check-coverage --reporter=text --reporter=lcov node pipeline/scripts/run-smokes.mjs",
|
|
29
29
|
"lint": "eslint .",
|
|
30
30
|
"lint:fix": "eslint . --fix",
|
|
31
31
|
"format": "prettier --write \"**/*.{js,mjs,json,yml}\" --ignore-path .gitignore --ignore-path .prettierignore",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-reviewer
|
|
3
|
-
description: "Code reviewer for multi-agent Phase 3 - security, architecture, quality, performance.
|
|
3
|
+
description: "Code reviewer for multi-agent Phase 3 - security, architecture, quality, performance. Model follows the fable switch (fable when on, opus when off); Phase 3 orchestrator overrides to sonnet for Reviewer 3."
|
|
4
4
|
model: fable
|
|
5
5
|
preferredModel: fable
|
|
6
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."
|
|
@@ -38,6 +38,13 @@ This command sets both, together:
|
|
|
38
38
|
Flipping one without the other is the defect this closes, so the pair moves as
|
|
39
39
|
one write or not at all.
|
|
40
40
|
|
|
41
|
+
The same step re-applies the installed agents (`lib/agent-model-sync.mjs`): every
|
|
42
|
+
agent under `~/.claude/agents` whose `preferredModel` is `fable` gets
|
|
43
|
+
`model: fable` while the switch is on and `model: opus` while it is off. An agent
|
|
44
|
+
spawned without an explicit model runs on that line, so without this the switch
|
|
45
|
+
would cover routed dispatches only. The installer applies the same rule on every
|
|
46
|
+
install and update, so a sync never puts the agents back on a rung that is off.
|
|
47
|
+
|
|
41
48
|
## What it reports with no argument
|
|
42
49
|
|
|
43
50
|
The current rung, the pricing model, and **what the switch means on this host** -
|
|
@@ -134,12 +134,10 @@ Step 0 gate rules and why: `features/doctor.md`.
|
|
|
134
134
|
~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
135
135
|
&& { echo "ABORT: corporate reference leaked into pipeline/commands"; exit 1; } || true
|
|
136
136
|
```
|
|
137
|
-
- **
|
|
138
|
-
`
|
|
139
|
-
`
|
|
140
|
-
|
|
141
|
-
after copying commands into `pipeline/commands/`, normalize the copy and
|
|
142
|
-
verify no sidecar or Turkish description leaked:
|
|
137
|
+
- **Installed-tree artifacts.** Picker descriptions are Turkish with a
|
|
138
|
+
`description-en:` sidecar when `outputLanguage=tr`, and fable-tier agents
|
|
139
|
+
carry `model: opus` while `fableEnabled` is off. The repo always gets the
|
|
140
|
+
English form and `model: fable`, so normalize after copying:
|
|
143
141
|
```bash
|
|
144
142
|
# normalize: restore English descriptions, strip description-en sidecars
|
|
145
143
|
node ~/multi-agent-pipeline/pipeline/scripts/localize-commands.mjs restore \
|
|
@@ -149,6 +147,7 @@ Step 0 gate rules and why: `features/doctor.md`.
|
|
|
149
147
|
# backstop: repo description lines must be English (Turkish-diacritics proxy)
|
|
150
148
|
grep -rn '^description:.*[çğıöşüÇĞİÖŞÜ]' ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
151
149
|
&& { echo "ABORT: localized description leaked into pipeline/commands"; exit 1; } || true
|
|
150
|
+
node ~/multi-agent-pipeline/pipeline/lib/agent-model-sync.mjs --dir ~/multi-agent-pipeline/pipeline/agents --fable-on
|
|
152
151
|
```
|
|
153
152
|
|
|
154
153
|
6. **Cross-platform smoke gate** (final step of the REPO sync, before push):
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-model-sync.mjs - installed agents follow the fable switch.
|
|
3
|
+
*
|
|
4
|
+
* prefs.global.modelFallback.fableEnabled is the one switch for the fable rung.
|
|
5
|
+
* The router honours it for every dispatch it resolves, but an agent spawned
|
|
6
|
+
* without an explicit model runs on its frontmatter `model:`. So every installed
|
|
7
|
+
* agent whose `preferredModel` is fable has that line written from the switch:
|
|
8
|
+
* fable while it is on, opus while it is off. Sources keep `model: fable`; only
|
|
9
|
+
* installed copies change. Called by model-rung.sh on every flip and by the
|
|
10
|
+
* installer after it lays the agents down.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node agent-model-sync.mjs [--dir <agents dir>] [--prefs <prefs file>] [--fable-on]
|
|
14
|
+
*
|
|
15
|
+
* --fable-on ignores the switch and writes fable: the sync step uses it to put
|
|
16
|
+
* the repo copy back on its source form.
|
|
17
|
+
*
|
|
18
|
+
* @module pipeline/lib/agent-model-sync
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
|
|
26
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} dir - an installed agents directory
|
|
30
|
+
* @param {boolean} fableOn
|
|
31
|
+
* @returns {string[]} the file names whose model line changed
|
|
32
|
+
*/
|
|
33
|
+
export function applyFableRung(dir, fableOn) {
|
|
34
|
+
if (!existsSync(dir)) return [];
|
|
35
|
+
// A linked agents directory is the source tree itself (a maintainer's
|
|
36
|
+
// --symlink install); writing there would edit the shipped personas.
|
|
37
|
+
if (lstatSync(dir).isSymbolicLink()) return [];
|
|
38
|
+
const want = fableOn ? "fable" : "opus";
|
|
39
|
+
const changed = [];
|
|
40
|
+
for (const name of readdirSync(dir).sort()) {
|
|
41
|
+
if (!name.endsWith(".md")) continue;
|
|
42
|
+
const path = join(dir, name);
|
|
43
|
+
let text;
|
|
44
|
+
try {
|
|
45
|
+
text = readFileSync(path, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const front = FRONTMATTER.exec(text);
|
|
50
|
+
if (!front || !/^preferredModel:\s*fable\s*$/m.test(front[1])) continue;
|
|
51
|
+
const nextFront = front[1].replace(/^model:.*$/m, `model: ${want}`);
|
|
52
|
+
if (nextFront === front[1]) continue;
|
|
53
|
+
const next = text.replace(front[1], nextFront);
|
|
54
|
+
if (lstatSync(path).isSymbolicLink()) rmSync(path);
|
|
55
|
+
writeFileSync(path, next);
|
|
56
|
+
changed.push(name);
|
|
57
|
+
}
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} prefsPath
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
export function fableEnabled(prefsPath) {
|
|
66
|
+
try {
|
|
67
|
+
const prefs = JSON.parse(readFileSync(prefsPath, "utf8"));
|
|
68
|
+
return prefs?.global?.modelFallback?.fableEnabled === true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function arg(argv, flag) {
|
|
75
|
+
const i = argv.indexOf(flag);
|
|
76
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
80
|
+
if (invokedDirectly) {
|
|
81
|
+
const argv = process.argv.slice(2);
|
|
82
|
+
const claude = join(homedir(), ".claude");
|
|
83
|
+
const dir = arg(argv, "--dir") ?? join(claude, "agents");
|
|
84
|
+
const prefs =
|
|
85
|
+
arg(argv, "--prefs") ??
|
|
86
|
+
(process.env.MULTI_AGENT_PREFS || undefined) ??
|
|
87
|
+
join(claude, "multi-agent-preferences.json");
|
|
88
|
+
const on = argv.includes("--fable-on") || fableEnabled(prefs);
|
|
89
|
+
const changed = applyFableRung(dir, on);
|
|
90
|
+
if (changed.length) {
|
|
91
|
+
process.stdout.write(
|
|
92
|
+
`agents on ${on ? "fable" : "opus"}: ${changed.map((n) => n.replace(/\.md$/, "")).join(", ")}\n`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -128,6 +128,13 @@ fi
|
|
|
128
128
|
mv "$TMP" "$PREFS"
|
|
129
129
|
trap - EXIT
|
|
130
130
|
|
|
131
|
+
# Agents spawned without an explicit model run on their frontmatter, so the
|
|
132
|
+
# installed fable-tier agents follow the switch in the same step.
|
|
133
|
+
if command -v node >/dev/null 2>&1 && [ -f "$_MA_MR_HERE/agent-model-sync.mjs" ]; then
|
|
134
|
+
node "$_MA_MR_HERE/agent-model-sync.mjs" --prefs "$PREFS" ||
|
|
135
|
+
echo "model-rung: installed agents could not be updated; run node $_MA_MR_HERE/agent-model-sync.mjs" >&2
|
|
136
|
+
fi
|
|
137
|
+
|
|
131
138
|
echo "fable rung: $CUR -> $WANT"
|
|
132
139
|
echo "cost pricing: $PRICING -> $WANT_PRICING"
|
|
133
140
|
echo ""
|
|
@@ -293,6 +293,16 @@ readiness, not correctness: a machine that fails all four still runs the
|
|
|
293
293
|
pipeline perfectly well with someone watching it. What they catch is the
|
|
294
294
|
failure mode of an unwatched run - stopping without saying so.
|
|
295
295
|
|
|
296
|
+
|
|
297
|
+
### usage-report
|
|
298
|
+
|
|
299
|
+
The outcome `usage-report.mjs` recorded for its last send attempt, read from
|
|
300
|
+
`logs/multi-agent/usage-last-send.json`. SKIP when `usageLog.enabled` is off or
|
|
301
|
+
nothing has been sent yet; OK when the last send was accepted; WARN for every
|
|
302
|
+
other outcome (`redirected`, `rejected`, `not-accepted`, `failed`, `skipped`),
|
|
303
|
+
with the status code or reason and the step that fixes it. The emitter runs
|
|
304
|
+
detached from phase-tracker with its output discarded, so this record is the
|
|
305
|
+
only surface on which a reporting path that stopped working shows up.
|
|
296
306
|
### unattended-contract
|
|
297
307
|
|
|
298
308
|
Is `multi-agent-refs/unattended-contract.md` installed. It is the only place
|
|
@@ -132,6 +132,16 @@ evidence, and there is now one fewer of them. Triage also runs on `opus`, which
|
|
|
132
132
|
makes it the same model as Reviewer 1; the Step 3 anonymisation requirement
|
|
133
133
|
already covers that case and is not optional here.
|
|
134
134
|
|
|
135
|
+
**Installed agents follow the switch.** Five personas declare
|
|
136
|
+
`preferredModel: fable` (code-reviewer, plan-critic, and the iOS, Android and
|
|
137
|
+
backend architects). A dispatch the router resolves passes its model explicitly,
|
|
138
|
+
but an agent spawned without one runs on its frontmatter `model:`. So the
|
|
139
|
+
installed copies are written from `fableEnabled`: `fable` while it is on, `opus`
|
|
140
|
+
while it is off. `model-rung.sh` re-applies them on every flip and the installer
|
|
141
|
+
on every install; sources keep `model: fable`, so `lint-personas.mjs` still holds
|
|
142
|
+
`model === preferredModel` there. A `--symlink` install links the source tree and
|
|
143
|
+
is left alone.
|
|
144
|
+
|
|
135
145
|
**Commands without Phase 0 resolve it themselves.** `/multi-agent:review`,
|
|
136
146
|
`/multi-agent:review-analysis` and the untracked-branch tail of
|
|
137
147
|
`/multi-agent:resume` dispatch reviewers and triage without a Phase 0 Step 0, so
|
|
@@ -303,4 +303,4 @@ Scripts: `contract-server.mjs` (phone branch, `isPhonePath`), `_phone-auth.mjs`
|
|
|
303
303
|
`unattended-policy.json` (`publisherScripts`). Tests:
|
|
304
304
|
`test/phone-api.test.mjs`, `test/contract-server.test.mjs`,
|
|
305
305
|
`test/launch-request.test.mjs`, `test/redact.test.mjs`,
|
|
306
|
-
`test/contract-kit.test.mjs`, `test/unattended-guard.
|
|
306
|
+
`test/contract-kit.test.mjs`, `test/unattended-guard.serial.mjs`.
|
|
@@ -35,7 +35,7 @@ the variable the same way.
|
|
|
35
35
|
|
|
36
36
|
With the variable unset every hook behaves as it did without this feature:
|
|
37
37
|
`smoke-unattended-redteam.sh` runs the same inputs both ways, and
|
|
38
|
-
`test/unattended-guard.
|
|
38
|
+
`test/unattended-guard.serial.mjs` holds the attended verdicts.
|
|
39
39
|
|
|
40
40
|
## The guard
|
|
41
41
|
|
|
@@ -722,7 +722,7 @@ Scripts: `agent-guard.sh`, `agent-guard.py`, `unattended_policy.py`,
|
|
|
722
722
|
`lib/outbound-gate.mjs`, `lib/unattended-settings-location.mjs`,
|
|
723
723
|
`install/_unattended-profile.mjs`. Data:
|
|
724
724
|
`schemas/unattended-policy.json`, `schemas/unattended-profile.json`,
|
|
725
|
-
`schemas/pr-request.schema.json`. Tests: `test/unattended-guard.
|
|
725
|
+
`schemas/pr-request.schema.json`. Tests: `test/unattended-guard.serial.mjs`,
|
|
726
726
|
`test/pr-request.test.mjs`, `test/autopilot-publish.test.mjs`,
|
|
727
727
|
the runner's tests, `test/outbound-gate.test.mjs`,
|
|
728
728
|
`test/unattended-profile.test.mjs`, `test/agent-guard.test.mjs`; smokes
|
|
@@ -147,6 +147,19 @@ reason, `unavailable` with the cause, `declined`, `registered`, `enabled`, `dry-
|
|
|
147
147
|
"you turned it off" and "we could not reach the endpoint" are different facts
|
|
148
148
|
about the same empty panel.
|
|
149
149
|
|
|
150
|
+
Every send attempt past the on-switch overwrites
|
|
151
|
+
`~/.claude/logs/multi-agent/usage-last-send.json` with its outcome: `accepted`,
|
|
152
|
+
`not-accepted` (the server answered but did not keep the record), `rejected`
|
|
153
|
+
(with the status code), `redirected` (with the host it pointed to; a redirect is
|
|
154
|
+
never followed, because the token would ride along), `failed` (network or
|
|
155
|
+
timeout), or `skipped` (`no-token`, `no-login`, `endpoint-not-allowed`). It
|
|
156
|
+
carries the run id, the event and the endpoint host, never the token. The
|
|
157
|
+
phase-tracker ping discards the emitter's output, so `/multi-agent:doctor`
|
|
158
|
+
reads this file (check `usage-report`) and turns anything but `accepted` into a
|
|
159
|
+
WARN with one step. `migrate-prefs.mjs` rewrites an endpoint on the platform
|
|
160
|
+
host to the site's domain, so a stored redirecting endpoint does not outlive an
|
|
161
|
+
update.
|
|
162
|
+
|
|
150
163
|
`prefs.global.usageLog.endpoint` overrides where both calls go - the register URL
|
|
151
164
|
is derived from it, so a self-hosted ingest gets its own registration rather than
|
|
152
165
|
this one's. Absent means the shipped default, and every shipped default names the
|
|
@@ -99,6 +99,7 @@ const CHECK_IDS = [
|
|
|
99
99
|
"mcp-surface",
|
|
100
100
|
"disk-space",
|
|
101
101
|
"worktree-residue",
|
|
102
|
+
"usage-report",
|
|
102
103
|
];
|
|
103
104
|
|
|
104
105
|
// Only under `--profile server`. Kept in their own list so `--list-checks`
|
|
@@ -883,6 +884,52 @@ function checkMcpSurface() {
|
|
|
883
884
|
);
|
|
884
885
|
}
|
|
885
886
|
|
|
887
|
+
// usage-report.mjs runs detached with its output discarded, so the record it
|
|
888
|
+
// writes after every send attempt is the one place a broken reporting path
|
|
889
|
+
// becomes visible.
|
|
890
|
+
const USAGE_STEPS = {
|
|
891
|
+
"no-login": () => `run node ${join(HERE, "usage-identity.mjs")} --refresh`,
|
|
892
|
+
"no-token": () => `run node ${join(HERE, "usage-register.mjs")}`,
|
|
893
|
+
"endpoint-not-allowed": () => "set usageLog.endpoint to an https address",
|
|
894
|
+
rejected: () => `run node ${join(HERE, "usage-register.mjs")}`,
|
|
895
|
+
"not-accepted": () => `run node ${join(HERE, "usage-identity.mjs")} --refresh`,
|
|
896
|
+
redirected: (r) =>
|
|
897
|
+
r.location
|
|
898
|
+
? `set usageLog.endpoint to the address on ${r.location} it redirects to`
|
|
899
|
+
: `run node ${join(HERE, "migrate-prefs.mjs")}`,
|
|
900
|
+
failed: () => "re-run a phase once the reporting endpoint is reachable",
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
function checkUsageReport() {
|
|
904
|
+
const prefs = readJson(join(CLAUDE, "multi-agent-preferences.json"));
|
|
905
|
+
const usage = prefs?.global?.usageLog;
|
|
906
|
+
if (usage?.enabled !== true || usage?.optOut === true) {
|
|
907
|
+
skip("usage-report", "usage reporting is off");
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const last = readJson(join(CLAUDE, "logs", "multi-agent", "usage-last-send.json"));
|
|
911
|
+
if (!last || typeof last.outcome !== "string") {
|
|
912
|
+
skip("usage-report", "no report has been sent from this machine yet");
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (last.outcome === "accepted") {
|
|
916
|
+
ok("usage-report");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const key = last.outcome === "skipped" ? last.reason : last.outcome;
|
|
920
|
+
const stepFor = USAGE_STEPS[key] ?? USAGE_STEPS.failed;
|
|
921
|
+
const detail = [last.status, last.reason, last.location && `to ${last.location}`]
|
|
922
|
+
.filter(Boolean)
|
|
923
|
+
.join(", ");
|
|
924
|
+
const when = last.t ? ` at ${last.t}` : "";
|
|
925
|
+
report(
|
|
926
|
+
"usage-report",
|
|
927
|
+
"WARN",
|
|
928
|
+
`the last usage report${last.id ? ` for ${last.id}` : ""} was ${last.outcome}${detail ? ` (${detail})` : ""}${when}, so runs are missing from the usage panel`,
|
|
929
|
+
stepFor(last),
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
886
933
|
function checkDiskSpace() {
|
|
887
934
|
let freeGb = null;
|
|
888
935
|
try {
|
|
@@ -1175,6 +1222,7 @@ function main() {
|
|
|
1175
1222
|
checkMcpSurface();
|
|
1176
1223
|
checkDiskSpace();
|
|
1177
1224
|
checkWorktreeResidue();
|
|
1225
|
+
checkUsageReport();
|
|
1178
1226
|
if (PROFILE === "server") checkServerProfile(resolved);
|
|
1179
1227
|
|
|
1180
1228
|
const blocked = results.filter((r) => r.severity === "BLOCK");
|
|
@@ -15,6 +15,7 @@ import path from "node:path";
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { runMain } from "../lib/fatal.mjs";
|
|
18
|
+
import { canonicalEndpoint } from "../lib/usage-endpoint.mjs";
|
|
18
19
|
import { lockTimeoutFrom, withFileLockSync, writeJsonAtomicSync } from "../lib/json-file-lock.mjs";
|
|
19
20
|
|
|
20
21
|
const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences.json");
|
|
@@ -445,9 +446,22 @@ function migrate(prefs) {
|
|
|
445
446
|
// reportContent / wikiScope structure. Idempotent - safe to re-run.
|
|
446
447
|
migrateReportChannels(out, changes);
|
|
447
448
|
|
|
449
|
+
// 13. usageLog.endpoint on the platform host: it answers every request with a
|
|
450
|
+
// redirect, and the usage clients never follow one with a credential attached.
|
|
451
|
+
migrateUsageEndpoint(out, changes);
|
|
452
|
+
|
|
448
453
|
return { out, changes, alreadyMigrated: changes.length === 0 };
|
|
449
454
|
}
|
|
450
455
|
|
|
456
|
+
function migrateUsageEndpoint(out, changes) {
|
|
457
|
+
const usage = out.global.usageLog;
|
|
458
|
+
if (!usage || typeof usage !== "object" || typeof usage.endpoint !== "string") return;
|
|
459
|
+
const next = canonicalEndpoint(usage.endpoint);
|
|
460
|
+
if (next === usage.endpoint) return;
|
|
461
|
+
changes.push(`usageLog.endpoint: ${usage.endpoint} → ${next}`);
|
|
462
|
+
usage.endpoint = next;
|
|
463
|
+
}
|
|
464
|
+
|
|
451
465
|
// v5.7+ sub-migration: legacy jiraCommentDefault / confluenceDefault / wikiDefault booleans
|
|
452
466
|
// → new reportChannels {pr,jira,confluence,wiki} object + wikiScope array. Also initializes
|
|
453
467
|
// reportContent + autopilotReportTimeoutSeconds with schema defaults. Old fields are removed
|
|
@@ -570,15 +570,22 @@ def _run_base_sha(env=None):
|
|
|
570
570
|
return sha if re.match(r"^[0-9a-f]{7,40}$", sha) else None
|
|
571
571
|
|
|
572
572
|
|
|
573
|
+
_TOP_CACHE = {}
|
|
574
|
+
|
|
575
|
+
|
|
573
576
|
def _worktree_top(d):
|
|
577
|
+
if d in _TOP_CACHE:
|
|
578
|
+
return _TOP_CACHE[d]
|
|
574
579
|
try:
|
|
575
580
|
import subprocess
|
|
576
581
|
|
|
577
582
|
r = subprocess.run(["git", "-C", d, "rev-parse", "--show-toplevel"],
|
|
578
583
|
capture_output=True, text=True, timeout=3)
|
|
579
|
-
|
|
584
|
+
top = os.path.realpath(r.stdout.strip()) if r.returncode == 0 and r.stdout.strip() else None
|
|
580
585
|
except Exception:
|
|
581
|
-
|
|
586
|
+
top = None
|
|
587
|
+
_TOP_CACHE[d] = top
|
|
588
|
+
return top
|
|
582
589
|
|
|
583
590
|
|
|
584
591
|
def _tracked_clean(path, env=None, cwd=None):
|
|
@@ -609,11 +616,11 @@ def _tracked_clean(path, env=None, cwd=None):
|
|
|
609
616
|
|
|
610
617
|
# The file's path relative to the worktree root, so the base lookup
|
|
611
618
|
# names it correctly and a file outside this worktree is rejected.
|
|
612
|
-
top =
|
|
619
|
+
top = _worktree_top(d)
|
|
613
620
|
ls = g("ls-files", "--error-unmatch", "--", name)
|
|
614
621
|
st = g("status", "--porcelain", "--untracked-files=all", "--", name)
|
|
615
|
-
in_worktree = top
|
|
616
|
-
rel = os.path.relpath(p,
|
|
622
|
+
in_worktree = top is not None
|
|
623
|
+
rel = os.path.relpath(p, top) if in_worktree else None
|
|
617
624
|
at_base = (
|
|
618
625
|
g("cat-file", "-e", f"{base}:{rel}").returncode == 0
|
|
619
626
|
and g("diff", "--quiet", base, "--", name).returncode == 0
|
|
@@ -27,7 +27,15 @@
|
|
|
27
27
|
* @module pipeline/scripts/usage-report
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
readFileSync,
|
|
32
|
+
existsSync,
|
|
33
|
+
readdirSync,
|
|
34
|
+
appendFileSync,
|
|
35
|
+
mkdirSync,
|
|
36
|
+
renameSync,
|
|
37
|
+
writeFileSync,
|
|
38
|
+
} from "fs";
|
|
31
39
|
import { canonicalEndpoint } from "../lib/usage-endpoint.mjs";
|
|
32
40
|
import { homedir, platform } from "os";
|
|
33
41
|
import { dirname, join } from "path";
|
|
@@ -709,27 +717,78 @@ function buildEvent(state, { event = null, login = null } = {}) {
|
|
|
709
717
|
};
|
|
710
718
|
}
|
|
711
719
|
|
|
720
|
+
function hostOf(url) {
|
|
721
|
+
try {
|
|
722
|
+
return new URL(url).hostname;
|
|
723
|
+
} catch {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* The outcome of one POST. A redirect is never followed, because the ingest
|
|
730
|
+
* token would ride along to wherever it points; its target host is reported
|
|
731
|
+
* instead, so a moved endpoint is visible rather than silent.
|
|
732
|
+
*/
|
|
712
733
|
async function post(endpoint, token, event) {
|
|
713
734
|
const ctrl = new AbortController();
|
|
714
735
|
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
715
736
|
try {
|
|
716
|
-
await fetch(endpoint, {
|
|
737
|
+
const res = await fetch(endpoint, {
|
|
717
738
|
method: "POST",
|
|
718
739
|
headers: {
|
|
719
740
|
"Content-Type": "application/json",
|
|
720
741
|
"X-Usage-Token": token,
|
|
721
742
|
},
|
|
722
743
|
body: JSON.stringify(event),
|
|
723
|
-
redirect: "
|
|
744
|
+
redirect: "manual",
|
|
724
745
|
signal: ctrl.signal,
|
|
725
746
|
});
|
|
726
|
-
|
|
727
|
-
|
|
747
|
+
if (res.status >= 300 && res.status < 400) {
|
|
748
|
+
return {
|
|
749
|
+
outcome: "redirected",
|
|
750
|
+
status: res.status,
|
|
751
|
+
location: hostOf(res.headers.get("location") || ""),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (!res.ok) return { outcome: "rejected", status: res.status };
|
|
755
|
+
let body = null;
|
|
756
|
+
try {
|
|
757
|
+
body = await res.json();
|
|
758
|
+
} catch {
|
|
759
|
+
/* a 2xx without a JSON body is still a delivery */
|
|
760
|
+
}
|
|
761
|
+
return { outcome: body?.accepted === false ? "not-accepted" : "accepted", status: res.status };
|
|
762
|
+
} catch (err) {
|
|
763
|
+
const reason = ctrl.signal.aborted ? "timeout" : (err?.cause?.code ?? err?.name ?? "error");
|
|
764
|
+
return { outcome: "failed", reason: codeOf(String(reason), 40) };
|
|
728
765
|
} finally {
|
|
729
766
|
clearTimeout(timer);
|
|
730
767
|
}
|
|
731
768
|
}
|
|
732
769
|
|
|
770
|
+
export const LAST_SEND_FILE = "usage-last-send.json";
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Overwrites the record of the most recent send attempt. The emitter runs
|
|
774
|
+
* detached with its output discarded, so this file is the only place a broken
|
|
775
|
+
* reporting path shows up; doctor reads it. It carries no token and no path.
|
|
776
|
+
*/
|
|
777
|
+
function recordSend(entry) {
|
|
778
|
+
const dir = join(homedir(), ".claude", "logs", "multi-agent");
|
|
779
|
+
const target = join(dir, LAST_SEND_FILE);
|
|
780
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
781
|
+
try {
|
|
782
|
+
mkdirSync(dir, { recursive: true });
|
|
783
|
+
writeFileSync(tmp, JSON.stringify({ t: new Date().toISOString(), ...entry }) + "\n", {
|
|
784
|
+
mode: 0o600,
|
|
785
|
+
});
|
|
786
|
+
renameSync(tmp, target);
|
|
787
|
+
} catch {
|
|
788
|
+
/* logs dir unwritable - never break the run */
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
733
792
|
// The write-only ingest token rides in a header; sending it over http would
|
|
734
793
|
// leak it in cleartext, contradicting the setup contract ("transmitted only
|
|
735
794
|
// over TLS"). Accept https anywhere, and http ONLY for loopback (local dev).
|
|
@@ -759,10 +818,16 @@ async function main() {
|
|
|
759
818
|
if (!args.dryRun) {
|
|
760
819
|
if (prefs.optOut === true) return;
|
|
761
820
|
if (prefs.enabled !== true) return;
|
|
762
|
-
if (!endpointAllowed(endpoint))
|
|
821
|
+
if (!endpointAllowed(endpoint)) {
|
|
822
|
+
recordSend({ id: args.taskId, outcome: "skipped", reason: "endpoint-not-allowed" });
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
763
825
|
}
|
|
764
826
|
const token = args.dryRun ? "" : resolveToken();
|
|
765
|
-
if (!args.dryRun && !token)
|
|
827
|
+
if (!args.dryRun && !token) {
|
|
828
|
+
recordSend({ id: args.taskId, outcome: "skipped", reason: "no-token" });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
766
831
|
|
|
767
832
|
const statePath = resolveStatePath(args);
|
|
768
833
|
let state = statePath ? readJson(statePath) : null;
|
|
@@ -782,7 +847,10 @@ async function main() {
|
|
|
782
847
|
const who = resolveReportingLogin();
|
|
783
848
|
if (!who.login) {
|
|
784
849
|
process.stderr.write(`usage-report: not sent - ${who.reason}\n`);
|
|
785
|
-
if (!args.dryRun)
|
|
850
|
+
if (!args.dryRun) {
|
|
851
|
+
recordSend({ id: runId(state), ev: kind, outcome: "skipped", reason: "no-login" });
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
786
854
|
}
|
|
787
855
|
|
|
788
856
|
const event = buildEvent(state, { event: kind, login: who.login });
|
|
@@ -793,7 +861,14 @@ async function main() {
|
|
|
793
861
|
}
|
|
794
862
|
|
|
795
863
|
if (event.st !== "in_progress") appendErrorLedger(event);
|
|
796
|
-
await post(endpoint, token, event);
|
|
864
|
+
const result = await post(endpoint, token, event);
|
|
865
|
+
recordSend({ id: event.id, ev: event.ev, host: hostOf(endpoint), ...result });
|
|
866
|
+
if (result.outcome !== "accepted") {
|
|
867
|
+
const detail = result.status ?? result.reason;
|
|
868
|
+
const where = result.location ? ` to ${result.location}` : "";
|
|
869
|
+
const verb = result.outcome === "not-accepted" ? "not accepted" : result.outcome;
|
|
870
|
+
process.stderr.write(`usage-report: ${verb}${where} (${detail}) by ${hostOf(endpoint)}\n`);
|
|
871
|
+
}
|
|
797
872
|
}
|
|
798
873
|
|
|
799
874
|
// Entry-point check: compare file URLs, never string-split a path.
|