@mmerterden/multi-agent-pipeline 17.5.1 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +276 -0
  2. package/README.md +59 -1
  3. package/README.tr.md +57 -0
  4. package/docs/adr/0011-dormant-ci.md +25 -1
  5. package/docs/features.md +24 -0
  6. package/docs/server-readiness.md +188 -0
  7. package/docs/token-budget-history.md +1 -1
  8. package/index.js +16 -1
  9. package/install/_common.mjs +42 -17
  10. package/install/_dev-only-files.mjs +8 -0
  11. package/install/_unattended-profile.mjs +113 -0
  12. package/install/index.mjs +48 -0
  13. package/install/templates/claude-hooks.json +13 -1
  14. package/manifest.json +1049 -0
  15. package/package.json +5 -2
  16. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  17. package/pipeline/commands/multi-agent/feedback/SKILL.md +7 -1
  18. package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
  19. package/pipeline/commands/multi-agent/issue/SKILL.md +13 -1
  20. package/pipeline/commands/multi-agent/jira/SKILL.md +13 -1
  21. package/pipeline/commands/multi-agent/resume/SKILL.md +16 -1
  22. package/pipeline/commands/multi-agent/setup/SKILL.md +14 -16
  23. package/pipeline/commands/multi-agent/status/SKILL.md +52 -21
  24. package/pipeline/commands/multi-agent/update/SKILL.md +13 -56
  25. package/pipeline/lib/_jira-auth.sh +8 -0
  26. package/pipeline/lib/analysis-jira-write.sh +32 -0
  27. package/pipeline/lib/ask-choice.sh +13 -2
  28. package/pipeline/lib/autopilot-state.sh +8 -0
  29. package/pipeline/lib/fatal.mjs +129 -0
  30. package/pipeline/lib/figma-mcp-refresh.sh +18 -0
  31. package/pipeline/lib/figma-screenshot.sh +18 -0
  32. package/pipeline/lib/invoked-directly.mjs +43 -0
  33. package/pipeline/lib/jira-publish.sh +42 -0
  34. package/pipeline/lib/md2confluence-v3.py +47 -0
  35. package/pipeline/lib/outbound-gate.mjs +175 -0
  36. package/pipeline/lib/plan-todos.sh +27 -6
  37. package/pipeline/lib/post-pr-review.sh +77 -8
  38. package/pipeline/lib/repo-hygiene.sh +8 -3
  39. package/pipeline/lib/require-jq.sh +40 -0
  40. package/pipeline/lib/run-paths.sh +335 -0
  41. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +70 -0
  42. package/pipeline/multi-agent-refs/features/code-graph.md +20 -0
  43. package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
  44. package/pipeline/multi-agent-refs/features/doctor.md +68 -0
  45. package/pipeline/multi-agent-refs/features/maturity-followup.md +166 -0
  46. package/pipeline/multi-agent-refs/features/package-manager.md +80 -0
  47. package/pipeline/multi-agent-refs/features/usage-reporting.md +79 -0
  48. package/pipeline/multi-agent-refs/features/verify-by-test.md +1 -1
  49. package/pipeline/multi-agent-refs/features/verify.md +83 -0
  50. package/pipeline/multi-agent-refs/phases/operations.md +13 -2
  51. package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -3
  52. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +8 -2
  53. package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -1
  54. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  55. package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
  56. package/pipeline/preferences-template.json +1 -1
  57. package/pipeline/schemas/agent-state.schema.json +122 -11
  58. package/pipeline/schemas/prefs.schema.json +35 -0
  59. package/pipeline/schemas/token-budget.json +2 -2
  60. package/pipeline/scripts/_run-paths.mjs +372 -0
  61. package/pipeline/scripts/aggregate-metrics.mjs +64 -64
  62. package/pipeline/scripts/autopilot-arming.mjs +2 -1
  63. package/pipeline/scripts/autopilot-intake.mjs +2 -1
  64. package/pipeline/scripts/autopilot-runner.mjs +206 -2
  65. package/pipeline/scripts/build-references.mjs +2 -1
  66. package/pipeline/scripts/build-stack-plugins.mjs +10 -2
  67. package/pipeline/scripts/capture-evidence.sh +7 -2
  68. package/pipeline/scripts/classify-plan-safety.mjs +2 -1
  69. package/pipeline/scripts/cost-analyze.mjs +600 -0
  70. package/pipeline/scripts/cost-budget-check.mjs +4 -12
  71. package/pipeline/scripts/council-view.mjs +2 -1
  72. package/pipeline/scripts/crush-json.mjs +2 -1
  73. package/pipeline/scripts/diff-explain.mjs +6 -9
  74. package/pipeline/scripts/diff-risk-score.mjs +2 -1
  75. package/pipeline/scripts/doctor.mjs +203 -4
  76. package/pipeline/scripts/evidence-gate.mjs +9 -3
  77. package/pipeline/scripts/feedback-send.mjs +13 -3
  78. package/pipeline/scripts/gc-abandoned.sh +29 -13
  79. package/pipeline/scripts/gc-worktrees.sh +11 -4
  80. package/pipeline/scripts/github-ssh-setup.sh +64 -7
  81. package/pipeline/scripts/graph-mermaid.mjs +4 -2
  82. package/pipeline/scripts/graph-report.mjs +155 -1
  83. package/pipeline/scripts/keychain-save.sh +101 -30
  84. package/pipeline/scripts/learn-from-transcripts.mjs +2 -1
  85. package/pipeline/scripts/learning-curve.mjs +34 -29
  86. package/pipeline/scripts/make-manifest.mjs +199 -0
  87. package/pipeline/scripts/maturity-followup.mjs +294 -0
  88. package/pipeline/scripts/migrate-prefs.mjs +2 -1
  89. package/pipeline/scripts/migrate-state.mjs +94 -4
  90. package/pipeline/scripts/package-manager.mjs +310 -0
  91. package/pipeline/scripts/phase-banner.sh +6 -2
  92. package/pipeline/scripts/phase-tracker.sh +41 -3
  93. package/pipeline/scripts/plan-coverage-gate.mjs +6 -2
  94. package/pipeline/scripts/pre-commit-check.sh +7 -0
  95. package/pipeline/scripts/pre-push-check.sh +7 -0
  96. package/pipeline/scripts/purge.sh +23 -6
  97. package/pipeline/scripts/render-agent-log-cost.sh +9 -2
  98. package/pipeline/scripts/render-cost-summary.sh +9 -2
  99. package/pipeline/scripts/render-work-summary.sh +11 -4
  100. package/pipeline/scripts/review-file-filter.mjs +4 -2
  101. package/pipeline/scripts/review-scope.mjs +2 -1
  102. package/pipeline/scripts/routine-registry.mjs +2 -1
  103. package/pipeline/scripts/run-aggregator.mjs +13 -14
  104. package/pipeline/scripts/run-metrics.mjs +3 -1
  105. package/pipeline/scripts/runs-index.mjs +343 -0
  106. package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
  107. package/pipeline/scripts/search-logs.sh +18 -0
  108. package/pipeline/scripts/test-gap-scan.mjs +2 -1
  109. package/pipeline/scripts/test-integrity-gate.mjs +2 -1
  110. package/pipeline/scripts/update-issue-progress.sh +56 -7
  111. package/pipeline/scripts/usage-register.mjs +271 -0
  112. package/pipeline/scripts/usage-report.mjs +14 -3
  113. package/pipeline/scripts/validate-analysis-doc.mjs +2 -1
  114. package/pipeline/scripts/validate-code-graph.mjs +6 -3
  115. package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
  116. package/pipeline/scripts/validate-diff-risk.mjs +6 -3
  117. package/pipeline/scripts/validate-test-gap.mjs +6 -3
  118. package/pipeline/scripts/validate-triage.mjs +3 -1
  119. package/pipeline/scripts/verify-citations.mjs +4 -2
  120. package/pipeline/scripts/verify.mjs +327 -0
  121. package/pipeline/scripts/worktree-finalize.sh +13 -4
  122. package/pipeline/scripts/write-state.mjs +154 -15
  123. package/pipeline/skills/.skill-manifest.json +6 -6
  124. package/pipeline/skills/.skills-index.json +56 -1
  125. package/pipeline/skills/shared/README.md +8 -3
  126. package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +14 -0
  127. package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +14 -0
  128. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +13 -0
  129. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +33 -9
  130. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +6 -0
  131. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
  132. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
  133. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
  134. package/pipeline/skills/skills-index.md +6 -1
@@ -29,6 +29,54 @@
29
29
 
30
30
  set -euo pipefail
31
31
 
32
+ # jq is not optional on this path. Without the guard below a missing binary
33
+ # renders as EMPTY DATA and the work continues on it; see lib/require-jq.sh.
34
+ for _rq in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/require-jq.sh" \
35
+ "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/require-jq.sh" \
36
+ "$HOME/.claude/lib/require-jq.sh" \
37
+ "$HOME/.copilot/lib/require-jq.sh" \
38
+ "$HOME/.codex/lib/require-jq.sh"; do
39
+ [ -f "$_rq" ] || continue
40
+ # shellcheck source=/dev/null
41
+ . "$_rq" && break
42
+ done
43
+ unset _rq
44
+ if ! command -v ma_require_jq >/dev/null 2>&1; then
45
+ # The helper itself is missing, which is an install problem, not a jq one.
46
+ ma_require_jq() { command -v jq >/dev/null 2>&1 || { echo "jq not found - cannot ${1:-continue}." >&2; return 1; }; }
47
+ fi
48
+ ma_require_jq "update the issue" || exit 3
49
+
50
+ # Outbound leak gate. Every byte below is composed at runtime out of command
51
+ # output, error text and file excerpts, any of which can carry a token that was
52
+ # on this machine a second earlier - and once it is in a comment it is in
53
+ # someone else's database. The repo's own leak scanner looks at FILES IN THE
54
+ # REPO and never sees this text. See lib/outbound-gate.mjs.
55
+ ma_outbound_gate() {
56
+ local body_file="$1" og=""
57
+ for c in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/outbound-gate.mjs" \
58
+ "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/outbound-gate.mjs" \
59
+ "$HOME/.claude/lib/outbound-gate.mjs" \
60
+ "$HOME/.copilot/lib/outbound-gate.mjs" \
61
+ "$HOME/.codex/lib/outbound-gate.mjs"; do
62
+ [ -f "$c" ] && { og="$c"; break; }
63
+ done
64
+ # Missing gate is NOT an open door: refusing to publish beats publishing
65
+ # unchecked, and the only way this file is absent is a broken install.
66
+ if [ -z "$og" ]; then
67
+ echo "outbound-gate.mjs not found - refusing to publish unchecked text." >&2
68
+ return 7
69
+ fi
70
+ node "$og" --file "$body_file"
71
+ }
72
+
73
+ # Run-state path resolution: pipeline/lib/run-paths.sh owns the two layouts
74
+ # (nested <root>/<project>/<id>/ and flat <root>/<id>/) and every id spelling.
75
+ _MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
76
+ # shellcheck source=/dev/null
77
+ . "$_MA_RP_HERE/../lib/run-paths.sh" 2>/dev/null || . "$HOME/.claude/lib/run-paths.sh"
78
+
79
+
32
80
  # Rewrite the `### Progress` block of the body file ($2) with $1 and print the
33
81
  # result to stdout. The replaced region is bounded at the FIRST of:
34
82
  # - a line starting with `<!--` (the legend comment, printed and kept), or
@@ -90,13 +138,10 @@ fi
90
138
  # per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/)
91
139
  # - not the ~/.claude/projects/*/state/ path this used to glob, which no
92
140
  # writer in the pipeline ever populates.
93
- AGENT_STATE=""
94
- for candidate in "$HOME"/.claude/logs/multi-agent/*/"$TASK_ID"/agent-state.json; do
95
- if [ -f "$candidate" ]; then
96
- AGENT_STATE="$candidate"
97
- break
98
- fi
99
- done
141
+ # This globbed ONLY the nested layout. phase-tracker.sh and most Phase 0 paths
142
+ # write flat, so on a real machine the majority of runs resolved to nothing and
143
+ # this script reported "no agent-state.json" for a run whose state existed.
144
+ AGENT_STATE="$(ma_resolve_run_file "$TASK_ID" agent-state.json 2>/dev/null || true)"
100
145
 
101
146
  if [ -z "$AGENT_STATE" ]; then
102
147
  echo "update-issue-progress: no agent-state.json found for task=$TASK_ID" >&2
@@ -154,6 +199,10 @@ if cmp -s "$TMP_BODY" "$TMP_NEW"; then
154
199
  fi
155
200
 
156
201
  # Apply.
202
+ if ! ma_outbound_gate "$TMP_NEW"; then
203
+ echo "update-issue-progress: outbound gate refused the new body; #$ISSUE_NUM left unchanged" >&2
204
+ exit 7
205
+ fi
157
206
  if ! gh issue edit "$ISSUE_NUM" --repo "$ORG_REPO" --body-file "$TMP_NEW" >/dev/null; then
158
207
  echo "update-issue-progress: gh issue edit failed for #$ISSUE_NUM" >&2
159
208
  exit 4
@@ -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
@@ -609,7 +609,18 @@ async function main() {
609
609
  // Windows-safe entry-point check: compare file URLs, never string-split a path.
610
610
  const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
611
611
  if (invokedDirectly) {
612
- main().catch(() => process.exit(0));
612
+ // Exit 0 stays: usage telemetry must never be the reason a run fails, and a
613
+ // non-zero exit here would propagate into whatever called it.
614
+ //
615
+ // But the silence goes. `catch(() => process.exit(0))` reported success for
616
+ // every failure, so a telemetry path that had been broken for weeks looked
617
+ // exactly like one that worked - there was no surface on which anyone could
618
+ // notice. One line on stderr costs nothing and is the difference between a
619
+ // degraded feature and an invisible one.
620
+ main().catch((err) => {
621
+ process.stderr.write(`usage-report: not sent - ${err?.message ?? err}\n`);
622
+ process.exit(0);
623
+ });
613
624
  }
614
625
 
615
626
  export {
@@ -41,6 +41,7 @@
41
41
  // Exit: 0 valid, 1 invalid (or WARN under --strict), 64 usage error.
42
42
 
43
43
  import { readFileSync } from "node:fs";
44
+ import { runMain } from "../lib/fatal.mjs";
44
45
 
45
46
  // A repo-less run (Locked 35) still splits by channel, derived from the evidence:
46
47
  // "mobile" and "web" are its platform values. "none" is the narrower case where the
@@ -1019,4 +1020,4 @@ function main() {
1019
1020
  console.log(`validate-analysis-doc: OK (mode=${mode}, ${warns.length} warning(s))`);
1020
1021
  }
1021
1022
 
1022
- main();
1023
+ runMain("validate-analysis-doc", main);
@@ -162,13 +162,16 @@ export function validate(obj) {
162
162
  obj = JSON.parse(raw);
163
163
  } catch (e) {
164
164
  console.log(JSON.stringify({ ok: false, errors: [`JSON parse error: ${e.message}`] }));
165
- process.exit(1);
165
+ process.exitCode = 1;
166
+ return;
166
167
  }
167
168
  const errors = validate(obj);
168
169
  if (errors.length === 0) {
169
170
  console.log(JSON.stringify({ ok: true, errors: [] }));
170
- process.exit(0);
171
+ return;
171
172
  }
173
+ // `errors` is one entry per violation and has no bound, so process.exit(1)
174
+ // here truncated the report exactly when there was most of it to read.
172
175
  console.log(JSON.stringify({ ok: false, errors }));
173
- process.exit(1);
176
+ process.exitCode = 1;
174
177
  })();
@@ -40,6 +40,7 @@
40
40
  // Exit: 0 valid, 1 invalid (or WARN under --strict), 64 usage error.
41
41
 
42
42
  import { readFileSync } from "node:fs";
43
+ import { runMain } from "../lib/fatal.mjs";
43
44
 
44
45
  const REQUIRED_FM = ["run_name", "generated_at", "language", "complaint_count", "graylog_degraded"];
45
46
 
@@ -263,4 +264,4 @@ function main() {
263
264
  process.exit(0);
264
265
  }
265
266
 
266
- main();
267
+ runMain("validate-complaint-doc", main);
@@ -120,13 +120,16 @@ function validate(obj) {
120
120
  obj = JSON.parse(raw);
121
121
  } catch (e) {
122
122
  console.log(JSON.stringify({ ok: false, errors: [`JSON parse error: ${e.message}`] }));
123
- process.exit(1);
123
+ process.exitCode = 1;
124
+ return;
124
125
  }
125
126
  const errors = validate(obj);
126
127
  if (errors.length === 0) {
127
128
  console.log(JSON.stringify({ ok: true, errors: [] }));
128
- process.exit(0);
129
+ return;
129
130
  }
131
+ // `errors` is one entry per violation and has no bound, so process.exit(1)
132
+ // here truncated the report exactly when there was most of it to read.
130
133
  console.log(JSON.stringify({ ok: false, errors }));
131
- process.exit(1);
134
+ process.exitCode = 1;
132
135
  })();
@@ -88,13 +88,16 @@ function validate(obj) {
88
88
  obj = JSON.parse(raw);
89
89
  } catch (e) {
90
90
  console.log(JSON.stringify({ ok: false, errors: [`JSON parse error: ${e.message}`] }));
91
- process.exit(1);
91
+ process.exitCode = 1;
92
+ return;
92
93
  }
93
94
  const errors = validate(obj);
94
95
  if (errors.length === 0) {
95
96
  console.log(JSON.stringify({ ok: true, errors: [] }));
96
- process.exit(0);
97
+ return;
97
98
  }
99
+ // `errors` is one entry per violation and has no bound, so process.exit(1)
100
+ // here truncated the report exactly when there was most of it to read.
98
101
  console.log(JSON.stringify({ ok: false, errors }));
99
- process.exit(1);
102
+ process.exitCode = 1;
100
103
  })();
@@ -292,4 +292,6 @@ function validate(raw) {
292
292
  const raw = await readInput();
293
293
  const result = validate(raw);
294
294
  console.log(JSON.stringify(result, null, 2));
295
- process.exit(result.ok ? result.code : 1);
295
+ // Assigned rather than called: process.exit() would cut this payload short when
296
+ // stdout is a pipe, which is how every caller reads it.
297
+ process.exitCode = result.ok ? result.code : 1;
@@ -45,6 +45,8 @@
45
45
  import { readFileSync, realpathSync, statSync } from "node:fs";
46
46
  import { resolve as resolvePath, sep } from "node:path";
47
47
  import { spawnSync } from "node:child_process";
48
+ import { runMain } from "../lib/fatal.mjs";
49
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
48
50
 
49
51
  /**
50
52
  * Reject a path before it reaches git.
@@ -423,6 +425,6 @@ function main() {
423
425
  process.exit(report.unresolved.length === 0 ? 0 : 1);
424
426
  }
425
427
 
426
- if (import.meta.url === `file://${process.argv[1]}`) {
427
- main();
428
+ if (invokedDirectly(import.meta.url)) {
429
+ runMain("verify-citations", main);
428
430
  }