@mmerterden/multi-agent-pipeline 17.5.0 → 17.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +24 -0
  3. package/README.tr.md +24 -0
  4. package/docs/features.md +44 -3
  5. package/docs/token-budget-history.md +1 -1
  6. package/install/templates/claude-hooks.json +13 -1
  7. package/package.json +1 -1
  8. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  9. package/pipeline/commands/multi-agent/feedback/SKILL.md +7 -1
  10. package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
  11. package/pipeline/commands/multi-agent/issue/SKILL.md +13 -1
  12. package/pipeline/commands/multi-agent/jira/SKILL.md +13 -1
  13. package/pipeline/commands/multi-agent/resume/SKILL.md +16 -1
  14. package/pipeline/commands/multi-agent/setup/SKILL.md +14 -16
  15. package/pipeline/commands/multi-agent/update/SKILL.md +13 -56
  16. package/pipeline/multi-agent-refs/features/code-graph.md +20 -0
  17. package/pipeline/multi-agent-refs/features/doctor.md +23 -0
  18. package/pipeline/multi-agent-refs/features/maturity-followup.md +166 -0
  19. package/pipeline/multi-agent-refs/features/package-manager.md +80 -0
  20. package/pipeline/multi-agent-refs/features/usage-reporting.md +79 -0
  21. package/pipeline/multi-agent-refs/features/verify-by-test.md +1 -1
  22. package/pipeline/multi-agent-refs/phases/phase-0-init.md +5 -2
  23. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +8 -2
  24. package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -1
  25. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  26. package/pipeline/preferences-template.json +1 -1
  27. package/pipeline/schemas/agent-state.schema.json +122 -11
  28. package/pipeline/schemas/prefs.schema.json +35 -0
  29. package/pipeline/schemas/token-budget.json +2 -2
  30. package/pipeline/scripts/doctor.mjs +65 -0
  31. package/pipeline/scripts/feedback-send.mjs +1 -1
  32. package/pipeline/scripts/graph-report.mjs +155 -1
  33. package/pipeline/scripts/maturity-followup.mjs +294 -0
  34. package/pipeline/scripts/package-manager.mjs +310 -0
  35. package/pipeline/scripts/usage-register.mjs +271 -0
  36. package/pipeline/scripts/usage-report.mjs +2 -2
  37. package/pipeline/skills/.skill-manifest.json +5 -5
  38. package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +14 -0
  39. package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +14 -0
  40. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +13 -0
  41. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +6 -0
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * usage-register.mjs - ask the reporting endpoint for this machine's token.
4
+ *
5
+ * WHY THIS EXISTS
6
+ *
7
+ * Operational reporting needs two things to work: `usageLog.enabled` true AND a
8
+ * token that resolves. Since v15.8.0 both are arranged automatically - but only
9
+ * inside `/multi-agent:update`, as forty lines of shell embedded in a skill. A
10
+ * new user installs the package, runs `/multi-agent:setup` (which explicitly
11
+ * says registration happens in update), works for weeks, and never registers.
12
+ * Their runs emit nothing, and the panel cannot tell that apart from nobody
13
+ * using the pipeline at all - which is exactly what it looked like.
14
+ *
15
+ * So the registration becomes one deterministic call that three places make:
16
+ * setup (onboarding), update (existing path), and the first run of a machine
17
+ * that reached neither.
18
+ *
19
+ * WHAT IT NEVER DOES
20
+ *
21
+ * - It never ships a secret: the token is REQUESTED, and it is write-only.
22
+ * - It never writes the token to a file. The credential store holds it; prefs
23
+ * hold only the name of the entry.
24
+ * - It never registers when `usageLog.optOut` is true. That is permanent and
25
+ * checked before anything else.
26
+ * - It never fails a caller. Offline, endpoint down, ingest disabled by the
27
+ * admin, no credential store: one status line, exit 0.
28
+ * - It never reports run CONTENT. That is usage-report.mjs's contract, and it
29
+ * is coarse metadata only.
30
+ *
31
+ * Usage:
32
+ * node usage-register.mjs [--prefs <path>] [--json] [--quiet] [--dry-run] [--feedback]
33
+ *
34
+ * Exit: always 0. `--json` says what happened; the shell does not need to care.
35
+ *
36
+ * @module pipeline/scripts/usage-register
37
+ */
38
+
39
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
40
+ import { execFileSync, spawnSync } from "node:child_process";
41
+ import { join } from "node:path";
42
+ import { homedir, hostname, userInfo } from "node:os";
43
+ import { pathToFileURL } from "node:url";
44
+
45
+ const DEFAULT_ENDPOINT = "https://mmerterden.com/api/usage/ingest";
46
+ const TIMEOUT_MS = 10000;
47
+
48
+ function readJson(file) {
49
+ try {
50
+ return JSON.parse(readFileSync(file, "utf8"));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /** The name this machine reports under: the GitHub login, never the git identity. */
57
+ export function reportingUser(prefs, { env = process.env } = {}) {
58
+ const declared = prefs?.global?.identities?.[0]?.username;
59
+ if (typeof declared === "string" && declared.trim()) return declared.trim();
60
+ try {
61
+ const out = execFileSync("gh", ["api", "user", "--jq", ".login"], {
62
+ encoding: "utf8",
63
+ timeout: 5000,
64
+ stdio: ["ignore", "pipe", "ignore"],
65
+ }).trim();
66
+ if (out) return out;
67
+ } catch {
68
+ /* gh is optional */
69
+ }
70
+ try {
71
+ return env.USER || userInfo().username || "unknown";
72
+ } catch {
73
+ return "unknown";
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Should this machine register at all?
79
+ *
80
+ * The three "no" answers are deliberately distinguishable: the caller prints
81
+ * them, and "you opted out" and "we could not reach the endpoint" are different
82
+ * facts about the same silence.
83
+ */
84
+ export function decideRegistration(
85
+ prefs,
86
+ { env = process.env, hasStoredToken = false, forFeedback = false } = {},
87
+ ) {
88
+ const g = prefs?.global || {};
89
+ const u = g.usageLog || {};
90
+ // `optOut` is a choice about PASSIVE collection. /multi-agent:feedback is the
91
+ // opposite: somebody typed a message meant to be read, and it rides the same
92
+ // token. So a feedback run may mint one - and it must NOT switch telemetry on
93
+ // when it does, which is what `enableTelemetry` below is for.
94
+ if (u.optOut === true && !forFeedback) return { act: false, reason: "opted out" };
95
+ if (env.MULTI_AGENT_USAGE_TOKEN) return { act: false, reason: "token in environment" };
96
+ if (typeof u.token === "string" && u.token.trim())
97
+ return { act: false, reason: "token in prefs" };
98
+ if (hasStoredToken) return { act: false, reason: "token already onboarded" };
99
+ return { act: true, reason: "no token resolves" };
100
+ }
101
+
102
+ /** The /register URL derived from whatever endpoint the prefs name. */
103
+ export function registerUrl(endpoint) {
104
+ const ep = (endpoint || DEFAULT_ENDPOINT).trim().replace(/\/+$/, "");
105
+ return ep.endsWith("/ingest") ? `${ep.slice(0, -"/ingest".length)}/register` : `${ep}/register`;
106
+ }
107
+
108
+ function storedToken(prefs, credStore) {
109
+ const name = prefs?.global?.keychainMapping?.usage_ingest;
110
+ if (!name || !existsSync(credStore)) return null;
111
+ const r = spawnSync("bash", [credStore, "get", name], { encoding: "utf8", timeout: 8000 });
112
+ const out = (r.stdout || "").trim();
113
+ return out || null;
114
+ }
115
+
116
+ async function requestToken(url, body) {
117
+ const ctrl = new AbortController();
118
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
119
+ try {
120
+ const res = await fetch(url, {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify(body),
124
+ signal: ctrl.signal,
125
+ });
126
+ if (!res.ok) return { token: null, why: `endpoint answered ${res.status}` };
127
+ const json = await res.json().catch(() => null);
128
+ const token = typeof json?.token === "string" ? json.token.trim() : "";
129
+ return token ? { token, why: null } : { token: null, why: "endpoint returned no token" };
130
+ } catch (e) {
131
+ return {
132
+ token: null,
133
+ why: e?.name === "AbortError" ? "endpoint timed out" : "endpoint unreachable",
134
+ };
135
+ } finally {
136
+ clearTimeout(timer);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Write the switch, and the credential-store ENTRY NAME when there is one.
142
+ *
143
+ * `entryName` is optional on purpose: a token can resolve from the environment
144
+ * or from `usageLog.token`, and those machines still need the switch flipped.
145
+ * Claiming "enabled" while writing nothing is the failure this whole feature is
146
+ * about, one level smaller.
147
+ *
148
+ * The token itself is never written here, whatever the caller passes.
149
+ */
150
+ export function persistPrefs(prefsPath, entryName = null, { enableTelemetry = true } = {}) {
151
+ const j = readJson(prefsPath);
152
+ if (!j) return false;
153
+ j.global = j.global || {};
154
+ if (entryName) {
155
+ j.global.keychainMapping = j.global.keychainMapping || {};
156
+ j.global.keychainMapping.usage_ingest = entryName;
157
+ }
158
+ j.global.usageLog = j.global.usageLog || {};
159
+ if (enableTelemetry) j.global.usageLog.enabled = true;
160
+ writeFileSync(prefsPath, `${JSON.stringify(j, null, 2)}\n`);
161
+ return true;
162
+ }
163
+
164
+ async function main(argv) {
165
+ const opts = { quiet: false, json: false, dryRun: false, prefs: null, feedback: false };
166
+ for (let i = 0; i < argv.length; i += 1) {
167
+ const a = argv[i];
168
+ if (a === "--quiet") opts.quiet = true;
169
+ else if (a === "--json") opts.json = true;
170
+ else if (a === "--dry-run") opts.dryRun = true;
171
+ else if (a === "--feedback") opts.feedback = true;
172
+ else if (a === "--prefs") opts.prefs = argv[++i];
173
+ }
174
+ const HOME = homedir();
175
+ const prefsPath = opts.prefs || join(HOME, ".claude", "multi-agent-preferences.json");
176
+ const credStore = join(HOME, ".claude", "lib", "credential-store.sh");
177
+
178
+ const say = (line) => {
179
+ if (!opts.quiet) process.stdout.write(`${line}\n`);
180
+ };
181
+ const done = (status, detail) => {
182
+ if (opts.json) process.stdout.write(`${JSON.stringify({ status, detail })}\n`);
183
+ return 0;
184
+ };
185
+
186
+ const prefs = readJson(prefsPath);
187
+ if (!prefs) {
188
+ // Setup writes this file; nothing here creates it. An installer that seeds
189
+ // state is the one thing the install contract forbids.
190
+ say(" -> operational reporting: no preferences file yet (run /multi-agent:setup)");
191
+ return done("skipped", "no prefs");
192
+ }
193
+
194
+ const has = Boolean(storedToken(prefs, credStore));
195
+ const decision = decideRegistration(prefs, { hasStoredToken: has, forFeedback: opts.feedback });
196
+ if (!decision.act) {
197
+ if (decision.reason !== "opted out") {
198
+ // Already has a token: make sure the switch is actually on. A token with
199
+ // enabled:false is the other half of the same silence.
200
+ if (prefs.global?.usageLog?.enabled !== true && !opts.dryRun) {
201
+ const name = prefs.global?.keychainMapping?.usage_ingest || null;
202
+ if (persistPrefs(prefsPath, name)) {
203
+ say(" -> operational reporting: enabled (a token was already onboarded)");
204
+ return done("enabled", decision.reason);
205
+ }
206
+ say(" -> operational reporting: could not write preferences");
207
+ return done("unavailable", "prefs write failed");
208
+ }
209
+ }
210
+ say(` -> operational reporting: unchanged (${decision.reason})`);
211
+ return done("skipped", decision.reason);
212
+ }
213
+
214
+ const url = registerUrl(prefs.global?.usageLog?.endpoint);
215
+ const user = reportingUser(prefs);
216
+ let host = "unknown";
217
+ try {
218
+ host = hostname().split(".")[0] || "unknown";
219
+ } catch {
220
+ /* a nameless host is still a host */
221
+ }
222
+
223
+ if (opts.dryRun) {
224
+ say(` -> operational reporting: would register ${user} with ${url}`);
225
+ return done("dry-run", url);
226
+ }
227
+
228
+ const { token, why } = await requestToken(url, { u: user, c: host });
229
+ if (!token) {
230
+ say(` -> operational reporting left off (${why})`);
231
+ return done("unavailable", why);
232
+ }
233
+
234
+ const optedOut = prefs.global?.usageLog?.optOut === true;
235
+ const entry = `${process.env.USER || user}_Usage_Ingest_Token`;
236
+ const set = spawnSync("bash", [credStore, "set", entry], {
237
+ input: token,
238
+ encoding: "utf8",
239
+ timeout: 10000,
240
+ });
241
+ if (set.status !== 0) {
242
+ say(" -> operational reporting left off (the credential store refused the token)");
243
+ return done("unavailable", "credential store write failed");
244
+ }
245
+ persistPrefs(prefsPath, entry, { enableTelemetry: !optedOut });
246
+ if (optedOut) {
247
+ say(" -> feedback token registered; telemetry stays off (usageLog.optOut is set)");
248
+ return done("registered-feedback-only", entry);
249
+ }
250
+ say(
251
+ " -> operational reporting: registered this machine (write-only token in the credential store)",
252
+ );
253
+ say(" opt out any time: set global.usageLog.optOut=true in multi-agent-preferences.json");
254
+ return done("registered", entry);
255
+ }
256
+
257
+ function isEntrypoint() {
258
+ const arg = process.argv[1];
259
+ if (!arg) return false;
260
+ try {
261
+ return import.meta.url === pathToFileURL(arg).href;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+
267
+ if (isEntrypoint()) {
268
+ main(process.argv.slice(2)).then((code) => {
269
+ process.exitCode = code;
270
+ });
271
+ }
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Enable + configure via prefs.global.usageLog:
11
11
  * { "enabled": true,
12
- * "endpoint": "https://mmerterden.vercel.app/api/usage/ingest",
12
+ * "endpoint": "https://mmerterden.com/api/usage/ingest",
13
13
  * "token": "<shared ingest token>" } // token may instead come from
14
14
  * // env MULTI_AGENT_USAGE_TOKEN
15
15
  * Nothing is sent when enabled is not true or no token resolves.
@@ -30,7 +30,7 @@ import { createHash } from "crypto";
30
30
  import { costUsd } from "./_cost.mjs";
31
31
 
32
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
- const ENDPOINT_DEFAULT = "https://mmerterden.vercel.app/api/usage/ingest";
33
+ const ENDPOINT_DEFAULT = "https://mmerterden.com/api/usage/ingest";
34
34
  const TIMEOUT_MS = 2500;
35
35
 
36
36
  // The emitter ships verbatim into ~/.claude, ~/.copilot and ~/.codex, so it must
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-15T12:49:28Z",
3
+ "generatedAt": "2026-09-15T20:06:53Z",
4
4
  "skillCount": 212,
5
5
  "entries": [
6
6
  {
@@ -93,11 +93,11 @@
93
93
  },
94
94
  {
95
95
  "path": "shared/core/multi-agent-issue/SKILL.md",
96
- "sha256": "aec8737af368b01c7e3133521c711861456c18b07e3829425e5c0485a0181051"
96
+ "sha256": "8f8918c8a0f5d6431892e65737c685bb47a62379d023ac7c760a8a605d3eb0a4"
97
97
  },
98
98
  {
99
99
  "path": "shared/core/multi-agent-jira/SKILL.md",
100
- "sha256": "0bd2046752f106f2a5f2974eab61a9618263153230bfbd9b0e59024fc5bff0a0"
100
+ "sha256": "1ceb984239217060ab9fa3e7964270f9c6245bef0a3cac429bc329548d91e617"
101
101
  },
102
102
  {
103
103
  "path": "shared/core/multi-agent-kill/SKILL.md",
@@ -181,7 +181,7 @@
181
181
  },
182
182
  {
183
183
  "path": "shared/core/multi-agent-setup/SKILL.md",
184
- "sha256": "40dc72793cd6bd1054d6203275fbc9f4ecdd6d8dc5de8f2437e7f670f4c5c646"
184
+ "sha256": "d475f14de5db9ffe0c8ae5b3b83374d5f82a41c29da5271c7399d151e6bd4c1d"
185
185
  },
186
186
  {
187
187
  "path": "shared/core/multi-agent-stack/SKILL.md",
@@ -233,7 +233,7 @@
233
233
  },
234
234
  {
235
235
  "path": "shared/core/multi-agent-update/SKILL.md",
236
- "sha256": "e93e913473cb5d3a5810b30f26011a0d18754bed983890f4261b87972526d94a"
236
+ "sha256": "8a0b089ea5c0d81813dc936d8b9d012f956e6381f71e025ca8878e538a8e9303"
237
237
  },
238
238
  {
239
239
  "path": "shared/core/multi-agent/SKILL.md",
@@ -34,3 +34,17 @@ Interactively list unassigned GitHub issues, let the user pick one, and send it
34
34
  - If the user passed the `autopilot` flag, start in autopilot mode
35
35
 
36
36
  6. **gh auth restore** - return to `{owner}` once done
37
+
38
+ ## Maturity: the picked item may not be ready
39
+
40
+ The pipeline scores the item before anything is built. A blocker does not end the
41
+ run any more: an interactive run asks at that step (open the item and fix it,
42
+ continue without it - recording which gap was waved through - or abort), and an
43
+ autopilot run with `prefs.global.maturityFollowup.autopilotCommentsOnIssue` on
44
+ posts ONE comment on the item asking for what is missing, then stops and waits for
45
+ `resume`. Never a status change, never an assignee, never a close; `Ref:`, never a
46
+ closing keyword. Read the item's existing comments first and take the prior ask
47
+ from the newest one of ours - a scan is a new run with a fresh state file, so
48
+ state alone would make every scan a first ask. Full contract:
49
+ `$HOME/.claude/multi-agent-refs/features/maturity-followup.md`.
50
+
@@ -36,3 +36,17 @@ Interactively list the user's open Jira issues, let the user pick one, and send
36
36
  5. **Start the pipeline** - run the `/multi-agent` command:
37
37
  - Input: `"PROJ-{id}" "branch-name"`
38
38
  - If the `autopilot` flag is present, in autopilot mode
39
+
40
+ ## Maturity: the picked item may not be ready
41
+
42
+ The pipeline scores the item before anything is built. A blocker does not end the
43
+ run any more: an interactive run asks at that step (open the item and fix it,
44
+ continue without it - recording which gap was waved through - or abort), and an
45
+ autopilot run with `prefs.global.maturityFollowup.autopilotCommentsOnIssue` on
46
+ posts ONE comment on the item asking for what is missing, then stops and waits for
47
+ `resume`. Never a status change, never an assignee, never a close; `Ref:`, never a
48
+ closing keyword. Read the item's existing comments first and take the prior ask
49
+ from the newest one of ours - a scan is a new run with a fresh state file, so
50
+ state alone would make every scan a first ask. Full contract:
51
+ `$HOME/.claude/multi-agent-refs/features/maturity-followup.md`.
52
+
@@ -141,6 +141,19 @@ Save the resolved mapping to preferences:
141
141
 
142
142
  `null` = not mapped (missing or skipped). Pipeline phases read this mapping to retrieve tokens dynamically - never hardcoded key names.
143
143
 
144
+ ### Step 2.7 - Operational reporting
145
+
146
+ ```bash
147
+ node "$HOME/.claude/scripts/usage-register.mjs"
148
+ ```
149
+
150
+ Requests a per-machine **write-only** token when none resolves, stores it in the
151
+ credential store only, and switches reporting on. Registration used to happen
152
+ only in update, so a machine that ran setup and nothing else never reported at
153
+ all. `usageLog.optOut: true` blocks it permanently; an unreachable endpoint
154
+ leaves reporting off with one line. Contract:
155
+ `$HOME/.claude/multi-agent-refs/features/usage-reporting.md`.
156
+
144
157
  ### Step 3 - Jira Project Key (auto-learned)
145
158
 
146
159
  Jira project keys are NOT asked during setup. They are auto-learned from pipeline usage:
@@ -37,6 +37,12 @@ Update the pipeline with a single command. Preferences are preserved; only skill
37
37
  4b. Refresh the stack-plugin marketplace so the latest plugin versions are picked up:
38
38
  `claude marketplace update multi-agent-plugins` (or `claude marketplace add {owner}/multi-agent-plugins` if not yet added)
39
39
  5. `node migrate-prefs.mjs` (if there is a schema upgrade)
40
+ 5b. `node "$HOME/.claude/scripts/usage-register.mjs"` - operational reporting.
41
+ It requests a per-machine **write-only** token when none resolves, stores it in
42
+ the credential store only, and switches reporting on. A no-op when a token is
43
+ already onboarded, and permanently blocked by `usageLog.optOut: true`. Offline
44
+ or endpoint down leaves reporting off with one line, never an error. Contract:
45
+ `$HOME/.claude/multi-agent-refs/features/usage-reporting.md`.
40
46
  6. Show the new version + changelog
41
47
  7. Smoke test (schema + cross-cli)
42
48