@mmerterden/multi-agent-pipeline 17.6.0 → 19.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 (272) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/README.md +76 -18
  3. package/README.tr.md +55 -16
  4. package/docs/adr/0002-instruction-driven-flag.md +1 -0
  5. package/docs/adr/0005-lazy-phase-docs.md +11 -1
  6. package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +1 -0
  7. package/docs/adr/0010-own-code-graph.md +1 -0
  8. package/docs/adr/0011-dormant-ci.md +25 -1
  9. package/docs/adr/0014-six-phase-consolidation.md +134 -0
  10. package/docs/adr/README.md +2 -1
  11. package/docs/architecture.md +37 -38
  12. package/docs/best-practices.md +1 -1
  13. package/docs/ecosystem.md +37 -26
  14. package/docs/engineering.md +1 -1
  15. package/docs/facts.json +45 -0
  16. package/docs/features.md +54 -53
  17. package/docs/performance.md +5 -5
  18. package/docs/recovery-guide.md +9 -9
  19. package/docs/server-readiness.md +188 -0
  20. package/docs/token-budget-history.md +3 -1
  21. package/index.js +18 -3
  22. package/install/_codex-agents.mjs +1 -1
  23. package/install/_common.mjs +42 -17
  24. package/install/_dev-only-files.mjs +8 -0
  25. package/install/_unattended-profile.mjs +113 -0
  26. package/install/index.mjs +48 -0
  27. package/install/templates/claude-hooks.json +1 -1
  28. package/install/templates/codex-instructions.md +1 -1
  29. package/install/templates/copilot-instructions.md +28 -28
  30. package/manifest.json +1065 -0
  31. package/package.json +6 -3
  32. package/pipeline/agents/dev-critic.md +3 -3
  33. package/pipeline/commands/figma-to-swiftui.md +1 -1
  34. package/pipeline/commands/multi-agent/SKILL.md +8 -8
  35. package/pipeline/commands/multi-agent/analysis/SKILL.md +9 -9
  36. package/pipeline/commands/multi-agent/autopilot/SKILL.md +7 -7
  37. package/pipeline/commands/multi-agent/channels/SKILL.md +15 -15
  38. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +6 -6
  39. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  40. package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
  41. package/pipeline/commands/multi-agent/help/SKILL.md +62 -62
  42. package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
  43. package/pipeline/commands/multi-agent/local/SKILL.md +11 -11
  44. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +13 -13
  45. package/pipeline/commands/multi-agent/log/SKILL.md +2 -2
  46. package/pipeline/commands/multi-agent/manual-test/SKILL.md +9 -9
  47. package/pipeline/commands/multi-agent/model/SKILL.md +69 -0
  48. package/pipeline/commands/multi-agent/refactor/SKILL.md +3 -3
  49. package/pipeline/commands/multi-agent/resume/SKILL.md +4 -4
  50. package/pipeline/commands/multi-agent/resume-local/SKILL.md +19 -17
  51. package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
  52. package/pipeline/commands/multi-agent/route-off/SKILL.md +36 -0
  53. package/pipeline/commands/multi-agent/route-on/SKILL.md +74 -0
  54. package/pipeline/commands/multi-agent/route-status/SKILL.md +56 -0
  55. package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
  56. package/pipeline/commands/multi-agent/status/SKILL.md +54 -23
  57. package/pipeline/commands/multi-agent/steer/SKILL.md +2 -2
  58. package/pipeline/commands/multi-agent/sync/SKILL.md +12 -13
  59. package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
  60. package/pipeline/lib/_jira-auth.sh +8 -0
  61. package/pipeline/lib/analysis-jira-write.sh +32 -0
  62. package/pipeline/lib/ask-choice.sh +13 -2
  63. package/pipeline/lib/autopilot-state.sh +8 -0
  64. package/pipeline/lib/credential-inventory.sh +1 -1
  65. package/pipeline/lib/fatal.mjs +129 -0
  66. package/pipeline/lib/fetch-fortify.sh +1 -1
  67. package/pipeline/lib/figma-mcp-refresh.sh +18 -0
  68. package/pipeline/lib/figma-screenshot.sh +18 -0
  69. package/pipeline/lib/invoked-directly.mjs +43 -0
  70. package/pipeline/lib/jira-publish.sh +42 -0
  71. package/pipeline/lib/md2confluence-v3.py +47 -0
  72. package/pipeline/lib/model-rung.sh +142 -0
  73. package/pipeline/lib/outbound-gate.mjs +175 -0
  74. package/pipeline/lib/phase-schema.mjs +88 -0
  75. package/pipeline/lib/plan-todos.sh +32 -11
  76. package/pipeline/lib/post-pr-review.sh +77 -8
  77. package/pipeline/lib/repo-hygiene.sh +8 -3
  78. package/pipeline/lib/require-jq.sh +40 -0
  79. package/pipeline/lib/route-state.sh +161 -0
  80. package/pipeline/lib/run-paths.sh +335 -0
  81. package/pipeline/multi-agent-refs/_account-picker.md +1 -1
  82. package/pipeline/multi-agent-refs/_dev-context.md +1 -1
  83. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  84. package/pipeline/multi-agent-refs/analysis/evidence.md +0 -9
  85. package/pipeline/multi-agent-refs/analysis/intake.md +1 -1
  86. package/pipeline/multi-agent-refs/analysis/locked.md +21 -22
  87. package/pipeline/multi-agent-refs/analysis/render.md +1 -1
  88. package/pipeline/multi-agent-refs/analysis/synthesis.md +12 -6
  89. package/pipeline/multi-agent-refs/android-guide.md +1 -1
  90. package/pipeline/multi-agent-refs/audit-guide.md +13 -13
  91. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  92. package/pipeline/multi-agent-refs/channels/jira.md +3 -3
  93. package/pipeline/multi-agent-refs/channels/pr.md +4 -4
  94. package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
  95. package/pipeline/multi-agent-refs/component-dispatch.md +3 -3
  96. package/pipeline/multi-agent-refs/cross-cli-contract.md +31 -6
  97. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +74 -4
  98. package/pipeline/multi-agent-refs/features/code-graph.md +5 -5
  99. package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
  100. package/pipeline/multi-agent-refs/features/design-conformance.md +1 -1
  101. package/pipeline/multi-agent-refs/features/dev-critic.md +3 -3
  102. package/pipeline/multi-agent-refs/features/doctor.md +47 -2
  103. package/pipeline/multi-agent-refs/features/external-context-injection.md +3 -3
  104. package/pipeline/multi-agent-refs/features/maturity-followup.md +3 -3
  105. package/pipeline/multi-agent-refs/features/model-fallback.md +5 -5
  106. package/pipeline/multi-agent-refs/features/plan-todos.md +1 -1
  107. package/pipeline/multi-agent-refs/features/repo-map.md +1 -1
  108. package/pipeline/multi-agent-refs/features/review-delta.md +3 -3
  109. package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
  110. package/pipeline/multi-agent-refs/features/scope-check.md +4 -4
  111. package/pipeline/multi-agent-refs/features/skill-conformance.md +2 -2
  112. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +1 -1
  113. package/pipeline/multi-agent-refs/features/verify-by-test.md +4 -4
  114. package/pipeline/multi-agent-refs/features/verify.md +83 -0
  115. package/pipeline/multi-agent-refs/features/visual-evidence.md +19 -19
  116. package/pipeline/multi-agent-refs/features/worktree-finalize.md +6 -6
  117. package/pipeline/multi-agent-refs/issue-jira-triad.md +10 -10
  118. package/pipeline/multi-agent-refs/knowledge.md +11 -11
  119. package/pipeline/multi-agent-refs/multi-repo-integration-build.md +13 -13
  120. package/pipeline/multi-agent-refs/payload-contracts.md +8 -8
  121. package/pipeline/multi-agent-refs/phases/log-format.md +10 -10
  122. package/pipeline/multi-agent-refs/phases/modes.md +30 -30
  123. package/pipeline/multi-agent-refs/phases/operations.md +21 -10
  124. package/pipeline/multi-agent-refs/phases/phase-0-init.md +25 -25
  125. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +599 -0
  126. package/pipeline/multi-agent-refs/phases/{phase-3-dev.md → phase-2-dev.md} +129 -49
  127. package/pipeline/multi-agent-refs/phases/{phase-4-review.md → phase-3-review.md} +225 -107
  128. package/pipeline/multi-agent-refs/phases/{phase-6-commit.md → phase-4-commit.md} +23 -23
  129. package/pipeline/multi-agent-refs/phases/{phase-7-report.md → phase-5-report.md} +29 -29
  130. package/pipeline/multi-agent-refs/phases.md +44 -48
  131. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  132. package/pipeline/multi-agent-refs/progress-contract.md +6 -6
  133. package/pipeline/multi-agent-refs/readiness-review.md +1 -1
  134. package/pipeline/multi-agent-refs/rules.md +7 -7
  135. package/pipeline/multi-agent-refs/swiftui-guide.md +2 -2
  136. package/pipeline/multi-agent-refs/tracker-contract.md +31 -32
  137. package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
  138. package/pipeline/multi-agent-refs/wiki-capture.md +14 -14
  139. package/pipeline/preferences-template.json +9 -1
  140. package/pipeline/rules/outside-the-pipeline.md +1 -1
  141. package/pipeline/schemas/agent-state.schema.json +50 -50
  142. package/pipeline/schemas/analysis-output.schema.json +2 -2
  143. package/pipeline/schemas/autopilot-config.schema.json +1 -1
  144. package/pipeline/schemas/code-graph.schema.json +1 -1
  145. package/pipeline/schemas/criteria-manifest.schema.json +1 -1
  146. package/pipeline/schemas/dev-critic-output.schema.json +1 -1
  147. package/pipeline/schemas/diff-risk.schema.json +1 -1
  148. package/pipeline/schemas/migrations/prefs-2.4.0-to-2.5.0.mjs +2 -2
  149. package/pipeline/schemas/migrations/prefs-2.6.0-to-2.7.0.mjs +31 -0
  150. package/pipeline/schemas/migrations/state-2.1.0-to-2.2.0.mjs +129 -0
  151. package/pipeline/schemas/phases.json +105 -0
  152. package/pipeline/schemas/plan-todos.schema.json +5 -5
  153. package/pipeline/schemas/planning-output.schema.json +1 -1
  154. package/pipeline/schemas/prefs.schema.json +100 -56
  155. package/pipeline/schemas/reviewer-output.schema.json +3 -3
  156. package/pipeline/schemas/route-config.schema.json +74 -0
  157. package/pipeline/schemas/scope-check.schema.json +1 -1
  158. package/pipeline/schemas/test-gap.schema.json +1 -1
  159. package/pipeline/schemas/token-budget.json +12 -18
  160. package/pipeline/schemas/triage-output.schema.json +6 -6
  161. package/pipeline/scripts/README.md +3 -3
  162. package/pipeline/scripts/_code-graph.mjs +2 -2
  163. package/pipeline/scripts/_run-paths.mjs +372 -0
  164. package/pipeline/scripts/_smoke-root.sh +1 -1
  165. package/pipeline/scripts/aggregate-metrics.mjs +65 -65
  166. package/pipeline/scripts/autopilot-arming.mjs +2 -1
  167. package/pipeline/scripts/autopilot-intake.mjs +2 -1
  168. package/pipeline/scripts/autopilot-runner.mjs +206 -2
  169. package/pipeline/scripts/build-references.mjs +2 -1
  170. package/pipeline/scripts/build-stack-plugins.mjs +10 -2
  171. package/pipeline/scripts/capture-evidence.sh +7 -2
  172. package/pipeline/scripts/capture-flush.sh +8 -8
  173. package/pipeline/scripts/capture-resume.sh +3 -3
  174. package/pipeline/scripts/classify-plan-safety.mjs +3 -2
  175. package/pipeline/scripts/cost-analyze.mjs +600 -0
  176. package/pipeline/scripts/cost-budget-check.mjs +4 -12
  177. package/pipeline/scripts/council-view.mjs +2 -1
  178. package/pipeline/scripts/crush-json.mjs +2 -1
  179. package/pipeline/scripts/diff-explain.mjs +7 -10
  180. package/pipeline/scripts/diff-risk-score.mjs +2 -1
  181. package/pipeline/scripts/doctor.mjs +140 -6
  182. package/pipeline/scripts/evidence-gate.mjs +9 -3
  183. package/pipeline/scripts/feedback-send.mjs +12 -2
  184. package/pipeline/scripts/gc-abandoned.sh +32 -16
  185. package/pipeline/scripts/gc-tmp.sh +1 -1
  186. package/pipeline/scripts/gc-worktrees.sh +12 -5
  187. package/pipeline/scripts/gen-facts.mjs +175 -0
  188. package/pipeline/scripts/gen-mode-dispatch.mjs +32 -37
  189. package/pipeline/scripts/gen-ref-toc.mjs +1 -1
  190. package/pipeline/scripts/github-ssh-setup.sh +64 -7
  191. package/pipeline/scripts/graph-mermaid.mjs +4 -2
  192. package/pipeline/scripts/graph-report.mjs +1 -1
  193. package/pipeline/scripts/jira-attach.sh +1 -1
  194. package/pipeline/scripts/keychain-save.sh +101 -30
  195. package/pipeline/scripts/learn-from-transcripts.mjs +3 -2
  196. package/pipeline/scripts/learning-curve.mjs +36 -31
  197. package/pipeline/scripts/log-metric.sh +17 -4
  198. package/pipeline/scripts/make-manifest.mjs +199 -0
  199. package/pipeline/scripts/memory-save.sh +1 -1
  200. package/pipeline/scripts/migrate-prefs.mjs +24 -6
  201. package/pipeline/scripts/migrate-state.mjs +94 -4
  202. package/pipeline/scripts/phase-banner.sh +26 -22
  203. package/pipeline/scripts/phase-tracker.sh +48 -10
  204. package/pipeline/scripts/plan-coverage-gate.mjs +8 -4
  205. package/pipeline/scripts/pre-commit-check.sh +7 -0
  206. package/pipeline/scripts/pre-push-check.sh +7 -0
  207. package/pipeline/scripts/purge.sh +23 -6
  208. package/pipeline/scripts/render-agent-log-cost.sh +10 -3
  209. package/pipeline/scripts/render-cost-summary.sh +9 -2
  210. package/pipeline/scripts/render-work-summary.sh +14 -7
  211. package/pipeline/scripts/review-file-filter.mjs +5 -3
  212. package/pipeline/scripts/review-scope.mjs +2 -1
  213. package/pipeline/scripts/routine-registry.mjs +2 -1
  214. package/pipeline/scripts/run-aggregator.mjs +26 -20
  215. package/pipeline/scripts/run-metrics.mjs +4 -2
  216. package/pipeline/scripts/runs-index.mjs +353 -0
  217. package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
  218. package/pipeline/scripts/search-logs.sh +18 -0
  219. package/pipeline/scripts/smoke-cross-cli-behavior.sh +6 -6
  220. package/pipeline/scripts/smoke-schema-validation.sh +26 -7
  221. package/pipeline/scripts/test-gap-scan.mjs +2 -1
  222. package/pipeline/scripts/test-integrity-gate.mjs +2 -1
  223. package/pipeline/scripts/token-budget-report.mjs +13 -2
  224. package/pipeline/scripts/triage-memory.mjs +2 -2
  225. package/pipeline/scripts/update-issue-progress.sh +56 -7
  226. package/pipeline/scripts/usage-report.mjs +12 -1
  227. package/pipeline/scripts/validate-analysis-doc.mjs +75 -18
  228. package/pipeline/scripts/validate-code-graph.mjs +6 -3
  229. package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
  230. package/pipeline/scripts/validate-diff-risk.mjs +6 -3
  231. package/pipeline/scripts/validate-planning.mjs +1 -1
  232. package/pipeline/scripts/validate-reviewer.mjs +1 -1
  233. package/pipeline/scripts/validate-state.mjs +45 -5
  234. package/pipeline/scripts/validate-test-gap.mjs +6 -3
  235. package/pipeline/scripts/validate-triage.mjs +6 -4
  236. package/pipeline/scripts/verify-citations.mjs +4 -2
  237. package/pipeline/scripts/verify.mjs +327 -0
  238. package/pipeline/scripts/worktree-finalize.sh +18 -9
  239. package/pipeline/scripts/write-state.mjs +154 -15
  240. package/pipeline/skills/.skill-manifest.json +37 -21
  241. package/pipeline/skills/.skills-index.json +104 -5
  242. package/pipeline/skills/shared/README.md +15 -6
  243. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +2 -2
  244. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +2 -2
  245. package/pipeline/skills/shared/core/multi-agent/SKILL.md +69 -71
  246. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +3 -3
  247. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +14 -14
  248. package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +5 -5
  249. package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +1 -1
  250. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +25 -23
  251. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
  252. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +2 -2
  253. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +8 -8
  254. package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +6 -6
  255. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +71 -0
  256. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +3 -3
  257. package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +1 -1
  258. package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +7 -7
  259. package/pipeline/skills/shared/core/multi-agent-route-off/SKILL.md +39 -0
  260. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +76 -0
  261. package/pipeline/skills/shared/core/multi-agent-route-status/SKILL.md +59 -0
  262. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
  263. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +35 -11
  264. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +2 -2
  265. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
  266. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
  267. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
  268. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
  269. package/pipeline/skills/skills-index.md +13 -4
  270. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +0 -263
  271. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +0 -344
  272. package/pipeline/multi-agent-refs/phases/phase-5-test.md +0 -182
@@ -21,6 +21,7 @@
21
21
  import fs from "node:fs";
22
22
  import os from "node:os";
23
23
  import path from "node:path";
24
+ import { rowPhase } from "../lib/phase-schema.mjs";
24
25
 
25
26
  const args = parseArgs(process.argv.slice(2));
26
27
  const METRICS =
@@ -64,7 +65,12 @@ for (const e of events) {
64
65
  models: new Set(),
65
66
  });
66
67
  const t = byTask.get(e.task_id);
67
- t.phases.add(e.phase);
68
+ // Normalised to the current vocabulary before it goes in the set. This log
69
+ // is append-only across the v19.0.0 renumbering, so the raw field carries
70
+ // two different meanings and a set of raw values cannot be compared to
71
+ // anything.
72
+ const cp = rowPhase(e);
73
+ if (cp !== null) t.phases.add(cp);
68
74
  const d = e.details || {};
69
75
  if (typeof d.tokens_in === "number") t.tokens_in += d.tokens_in;
70
76
  if (typeof d.tokens_out === "number") t.tokens_out += d.tokens_out;
@@ -113,7 +119,12 @@ for (const [taskId, t] of byTask) {
113
119
  summary.totalTokensOut += t.tokens_out;
114
120
  summary.totalUsd += usd;
115
121
  const total = t.tokens_in + t.tokens_out;
116
- const isMulti = t.phases.has("4") && t.premium_calls > 5; // heuristic
122
+ // Heuristic: a task that reached Review with many premium calls is a
123
+ // multi-repo run. Keyed on Review (3), not on the literal "4" this line used
124
+ // to carry - under the six-phase contract 4 is Commit, and every run reaches
125
+ // Commit, so the old literal would have classified nearly everything as
126
+ // multi-repo and applied the wrong ceiling.
127
+ const isMulti = t.phases.has(3) && t.premium_calls > 5;
117
128
  const tokenLimit = isMulti ? limits.perTaskTokensMultiRepo : limits.perTaskTokens;
118
129
  const usdLimit = isMulti ? limits.perTaskUsdMultiRepo : limits.perTaskUsd;
119
130
  if (total > tokenLimit)
@@ -3,8 +3,8 @@
3
3
  /**
4
4
  * @file triage-memory.mjs - v8.3.0
5
5
  *
6
- * Lightweight, zero-dep persistence layer for past Phase 4 triage findings.
7
- * Used by Phase 7 to ingest results, by Phase 1/4 to look up prior art when
6
+ * Lightweight, zero-dep persistence layer for past Phase 3 triage findings.
7
+ * Used by Phase 5 to ingest results, by Phase 1/4 to look up prior art when
8
8
  * a new task overlaps with a previous one, and by /multi-agent:search.
9
9
  *
10
10
  * Storage: append-only JSONL, one file per repo:
@@ -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
@@ -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 {
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // Checks (ERROR = blocking exit 1; WARN = advisory, still exit 0 unless --strict):
13
13
  // - Front-matter block with required keys (feature, platform, language, mode,
14
- // template_version); mode in {full, lite}; platform in the known set.
14
+ // template_version); mode must be full (lite removed in v19.0.0); platform in the known set.
15
15
  // - Never-omitted sections present by title keyword (Locked 2): Summary,
16
16
  // Goals, User Stories, API Contracts, Architecture, Files to Add, Risks,
17
17
  // References.
@@ -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
@@ -244,16 +245,31 @@ function main() {
244
245
  for (const k of REQUIRED_FM) {
245
246
  if (!parsed.fm[k]) errors.push(`front-matter missing required key: ${k}`);
246
247
  }
248
+ // `mode` is accepted only as "full" now. Lite was removed in v19.0.0: it
249
+ // picked sections from a fixed list chosen by a three-signal score, which
250
+ // fought Locked 2 in both directions - a small feature with rich business
251
+ // rules lost Section 15 because it was not on the list, and a feature with
252
+ // no API contract kept Section 9 because it was. An older document carrying
253
+ // `mode: lite` is still readable; it is reported as a legacy value rather
254
+ // than rejected, because the document was valid when it was written.
247
255
  const mode = parsed.fm.mode;
248
- if (mode && mode !== "full" && mode !== "lite") {
249
- errors.push(`front-matter mode must be full|lite, got: ${mode}`);
256
+ if (mode === "lite") {
257
+ warns.push(
258
+ "front-matter says mode: lite - Lite was removed in v19.0.0 and this is a pre-v19 document. Sections are chosen by evidence now (Locked 2).",
259
+ );
260
+ } else if (mode && mode !== "full") {
261
+ errors.push(`front-matter mode must be full, got: ${mode}`);
250
262
  }
251
263
  const plat = parsed.fm.platform;
252
264
  if (plat && !KNOWN_PLATFORMS.has(plat)) {
253
265
  errors.push(`front-matter platform not in known set: ${plat}`);
254
266
  }
255
267
  }
256
- const mode = parsed?.fm?.mode || "full";
268
+ // A pre-v19 `lite` document is validated as Full: the checks it skipped were
269
+ // skipped because of a mode that no longer exists, and re-skipping them here
270
+ // would carry that exemption forward forever.
271
+ const mode = "full";
272
+ void parsed?.fm?.mode;
257
273
  const profile = (parsed?.fm?.profile || "global").toLowerCase();
258
274
  if (profile !== "global" && profile !== "corporate") {
259
275
  errors.push(`front-matter profile must be global|corporate, got: ${profile}`);
@@ -384,7 +400,7 @@ function main() {
384
400
  } else if (rows.some((l) => /\bAS-\d{2,3}\b/.test(l))) {
385
401
  // Only when there is something to own. A Section 20 with no stamped row
386
402
  // has no owner to check, and warning there would fire on every document
387
- // that never needed the column - noise that Phase 4 runs under --strict.
403
+ // that never needed the column - noise that Phase 3 runs under --strict.
388
404
  warns.push(
389
405
  "Risks and Open Questions carries AS-NN rows but no owner column; ownership cannot be checked",
390
406
  );
@@ -504,7 +520,7 @@ function main() {
504
520
  }
505
521
  mark("traceability-matrix");
506
522
  } else {
507
- mark("traceability-matrix", "lite mode");
523
+ mark("traceability-matrix");
508
524
  }
509
525
  } else {
510
526
  mark("payload-in-table-cell", "global profile");
@@ -579,7 +595,7 @@ function main() {
579
595
 
580
596
  mark("optin-coverage-sections");
581
597
 
582
- // 2c. The Test Plan is Phase 3's RED input (phase-3-dev.md step 5b): the tests
598
+ // 2c. The Test Plan is Phase 3's RED input (phase-2-dev.md step 5b): the tests
583
599
  // written first come from it. Full mode without it hands dev an empty matrix.
584
600
  // It is Section 15 in the global profile and Section 19 in the corporate one;
585
601
  // the check matches on the title, so only the stack-optional case needs an
@@ -593,7 +609,7 @@ function main() {
593
609
  // The omission table allows exactly one case: a backend-only run with no
594
610
  // contract testing planned. The validator knows the platform, so that case
595
611
  // warns instead of blocking; every other platform hands dev an empty matrix.
596
- const msg = "no Section 15 Test Plan; Full mode dev reads it as the RED input (Locked 31)";
612
+ const msg = "no Section 15 Test Plan; dev reads it as the RED input (Locked 30)";
597
613
  if (parsed?.fm?.platform === "backend") {
598
614
  warns.push(`${msg} - allowed for backend only when no contract testing is planned`);
599
615
  } else {
@@ -614,8 +630,7 @@ function main() {
614
630
  }
615
631
  }
616
632
 
617
- if (mode !== "full") mark("test-plan", "lite mode");
618
- else if (noPlatform) mark("test-plan", "no target platform");
633
+ if (noPlatform) mark("test-plan", "no target platform");
619
634
  else mark("test-plan");
620
635
 
621
636
  // 2d. Section 3 carries the diagrams. Omission is legitimate under the
@@ -725,7 +740,7 @@ function main() {
725
740
 
726
741
  mark("concept-table-evidence", conceptBody ? undefined : "no concept table");
727
742
 
728
- // 2g. Locked 31 main clause: a rule with no test scenario fails the gate.
743
+ // 2g. Locked 30 main clause: a rule with no test scenario fails the gate.
729
744
  // The pre-existing occurrence-count heuristic below stays as the looser net.
730
745
  if (mode === "full") {
731
746
  const idRx = /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
@@ -746,7 +761,7 @@ function main() {
746
761
  }
747
762
  }
748
763
 
749
- mark("rule-to-test", mode === "full" ? undefined : "lite mode");
764
+ mark("rule-to-test");
750
765
 
751
766
  // 2h. Redesign mode (Locked 37). Eight checks, live only when the front-matter
752
767
  // opts in. The contract they enforce is analysis/redesign.md, which loads only
@@ -793,7 +808,7 @@ function main() {
793
808
  );
794
809
  }
795
810
  // 3. A rule that lives only in a shared module legitimately reads this way,
796
- // so it warns rather than blocks - and Phase 4 runs --strict, so it blocks
811
+ // so it warns rather than blocks - and Phase 3 runs --strict, so it blocks
797
812
  // there, which is where a human is already looking.
798
813
  if (/cross-cutting/i.test(evidence) && certainty.includes("confirmed")) {
799
814
  warns.push(
@@ -870,7 +885,7 @@ function main() {
870
885
  // keyword as a raw substring of the heading line - deliberately, since Locked
871
886
  // 2 re-flows section numbers and the corporate profile numbers them
872
887
  // differently - so an unrelated `### 4.9 Notes on the old Current Behaviour
873
- // audit process` matched, and Phase 4 runs --strict, which turns this warning
888
+ // audit process` matched, and Phase 3 runs --strict, which turns this warning
874
889
  // into a blocked document. The artefact itself is the honest signal: a
875
890
  // rendered current-behaviour table always carries `CB-<slug>-NN` ids, and a
876
891
  // paragraph that merely uses the words never does.
@@ -920,7 +935,49 @@ function main() {
920
935
 
921
936
  mark("placeholder-bodies", profile === "global" ? undefined : "corporate profile");
922
937
 
923
- // 5. Traceability (Full mode only, Locked 31)
938
+ // 4b. Locked 2's numbering half, which was ungated until v19.0.0.
939
+ //
940
+ // It is NOT "numbering re-flows 1..N". That is what the rule used to say and
941
+ // it could not hold: Locked 30 threads ids across the document by section
942
+ // NUMBER ("Section 15.1 scenario", "Section 4.4", "Section 3/5 layout cells"),
943
+ // so a re-flowed document sends every one of those references to the wrong
944
+ // section. No emitted document ever re-flowed. The rule was the wrong half.
945
+ //
946
+ // What holds instead: a rendered section keeps its canonical template number,
947
+ // so gaps are correct and `1, 2, 4, 9, 13, 14, 21` is a valid document. Two
948
+ // things are still defects and both are checked here - a number outside the
949
+ // template's range, and numbers that repeat or go backwards. Either one means
950
+ // a cross-reference lands somewhere its author did not intend.
951
+ {
952
+ const MAX_SECTION = 23;
953
+ const nums = [];
954
+ for (const l of text.split("\n")) {
955
+ const m = /^##\s+(\d+)\.\s/.exec(l);
956
+ if (m) nums.push(Number(m[1]));
957
+ }
958
+ if (nums.length === 0) {
959
+ mark("section-numbering", "no numbered top-level sections");
960
+ } else {
961
+ const outOfRange = nums.filter((n) => n < 1 || n > MAX_SECTION);
962
+ const notAscending = [];
963
+ for (let i = 1; i < nums.length; i++) {
964
+ if (nums[i] <= nums[i - 1]) notAscending.push(`${nums[i - 1]} then ${nums[i]}`);
965
+ }
966
+ if (outOfRange.length > 0) {
967
+ errors.push(
968
+ `section number outside the 1..${MAX_SECTION} template range (Locked 2): ${outOfRange.join(", ")}`,
969
+ );
970
+ }
971
+ if (notAscending.length > 0) {
972
+ errors.push(
973
+ `section numbers repeat or go backwards (Locked 2): ${notAscending.slice(0, 4).join("; ")}`,
974
+ );
975
+ }
976
+ mark("section-numbering");
977
+ }
978
+ }
979
+
980
+ // 5. Traceability (Locked 30)
924
981
  if (mode === "full") {
925
982
  const idRe = /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
926
983
  const all = text.match(idRe) || [];
@@ -930,7 +987,7 @@ function main() {
930
987
  counts.set(key, (counts.get(key) || 0) + 1);
931
988
  }
932
989
  if (counts.size === 0 && profile === "global") {
933
- warns.push("Full mode but no BR-<slug>-NN business-rule ids found (Section 4.4 / Locked 31)");
990
+ warns.push("no BR-<slug>-NN business-rule ids found (Section 4.4 / Locked 30)");
934
991
  }
935
992
  for (const [id, n] of counts) {
936
993
  if (n < 2) {
@@ -941,7 +998,7 @@ function main() {
941
998
  }
942
999
  }
943
1000
 
944
- mark("br-reference-count", mode === "full" ? undefined : "lite mode");
1001
+ mark("br-reference-count");
945
1002
 
946
1003
  // 6. Manual test scenarios (Section 15.7): every MT row carries a BR id.
947
1004
  // A manual scenario nobody can trace back to a rule is a scenario nobody can
@@ -1019,4 +1076,4 @@ function main() {
1019
1076
  console.log(`validate-analysis-doc: OK (mode=${mode}, ${warns.length} warning(s))`);
1020
1077
  }
1021
1078
 
1022
- main();
1079
+ 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
  })();
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // validate-planning.mjs - runtime validator for Phase 2 planning output.
2
+ // validate-planning.mjs - runtime validator for Phase 1 planning output.
3
3
  //
4
4
  // Phase 2 returns a task decomposition conforming to
5
5
  // pipeline/schemas/planning-output.schema.json. Phase 3 iterates over `tasks`
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // validate-reviewer.mjs - runtime validator for Phase 4 reviewer subagent output.
2
+ // validate-reviewer.mjs - runtime validator for Phase 3 reviewer subagent output.
3
3
  //
4
4
  // Each of the 3 parallel reviewers (Opus, GPT-5.4, Sonnet) must return an
5
5
  // object matching pipeline/schemas/reviewer-output.schema.json. This validator
@@ -14,7 +14,7 @@
14
14
  // actually make a resume unsafe:
15
15
  // - the file does not parse (truncated / corrupt)
16
16
  // - root is not an object
17
- // - currentPhase is missing or out of the 0..7 range (resume keys on it)
17
+ // - currentPhase is missing or out of the 0..5 range (resume keys on it)
18
18
  // - phases is present but not an object, or an entry is not an object
19
19
  // Phase status strings are NOT enum-checked: real writers use "completed"
20
20
  // alongside the schema's "done", and an odd status does not make a resume
@@ -66,9 +66,33 @@ async function main() {
66
66
  process.exit(1);
67
67
  }
68
68
 
69
+ // Which numbering this file was written under. v19.0.0 renumbered eight
70
+ // phases into six, and a run that finished at `currentPhase: 7` in October
71
+ // was correct when it was written - calling it invalid now would mark most
72
+ // of the log directory broken and teach the reader to ignore this validator.
73
+ // Same decision the metrics file got: label the generation, do not rewrite
74
+ // history. A file stamped 2.2.0 or later is generation 2 and bounded 0..5;
75
+ // anything older, including the unstamped files that predate stamping
76
+ // entirely, is generation 1 and bounded 0..7.
77
+ const GEN2_FROM = "2.2.0";
78
+ const cmpVersion = (a, b) => {
79
+ const pa = String(a).split(".").map(Number);
80
+ const pb = String(b).split(".").map(Number);
81
+ for (let i = 0; i < 3; i++) {
82
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
83
+ }
84
+ return 0;
85
+ };
86
+ const gen1 = !s.schemaVersion || cmpVersion(s.schemaVersion, GEN2_FROM) < 0;
87
+ const maxPhase = gen1 ? 7 : 5;
88
+ const phaseKey = gen1 ? /^[0-7]$/ : /^[0-5]$/;
89
+
69
90
  // currentPhase is the field resume keys on - it must be present and in range.
70
- if (!Number.isInteger(s.currentPhase) || s.currentPhase < 0 || s.currentPhase > 7) {
71
- errors.push(`currentPhase must be integer 0..7, got ${JSON.stringify(s.currentPhase)}`);
91
+ if (!Number.isInteger(s.currentPhase) || s.currentPhase < 0 || s.currentPhase > maxPhase) {
92
+ errors.push(
93
+ `currentPhase must be integer 0..${maxPhase}, got ${JSON.stringify(s.currentPhase)}` +
94
+ (gen1 ? " (generation 1 file: unstamped or schemaVersion < 2.2.0)" : ""),
95
+ );
72
96
  }
73
97
 
74
98
  // phases is optional (some early/aborted states omit it), but if present it
@@ -78,12 +102,28 @@ async function main() {
78
102
  errors.push("phases, when present, must be an object keyed by phase number");
79
103
  } else {
80
104
  for (const [k, v] of Object.entries(s.phases)) {
81
- if (!/^[0-7]$/.test(k)) {
82
- errors.push(`phases key invalid: "${k}" (expected "0".."7")`);
105
+ if (!phaseKey.test(k)) {
106
+ errors.push(`phases key invalid: "${k}" (expected "0".."${maxPhase}")`);
83
107
  continue;
84
108
  }
85
109
  if (typeof v !== "object" || v === null) {
86
110
  errors.push(`phases["${k}"] must be an object`);
111
+ continue;
112
+ }
113
+ // The rework cap. Every phase doc says "hard-kill at retryCount === 3"
114
+ // and the schema writes `maximum: 3`, but nothing read either: a fourth
115
+ // round produced a state that violates the schema and passed this
116
+ // validator, which is the one place that could have said so.
117
+ //
118
+ // What this does NOT do is stop the loop - that is the orchestrator's
119
+ // job and it stays prose. What it does is make an over-cap state fail
120
+ // instead of being carried forward as if it were fine.
121
+ if (v.retryCount !== undefined) {
122
+ if (!Number.isInteger(v.retryCount) || v.retryCount < 0 || v.retryCount > 3) {
123
+ errors.push(
124
+ `phases["${k}"].retryCount must be integer 0..3 (the rework cap), got ${JSON.stringify(v.retryCount)}`,
125
+ );
126
+ }
87
127
  }
88
128
  }
89
129
  }
@@ -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
  })();
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // validate-triage.mjs - behavioral validator for Phase 4 triage output.
2
+ // validate-triage.mjs - behavioral validator for Phase 3 triage output.
3
3
  //
4
4
  // Zero dependencies. Reads a triage JSON from a path arg or STDIN, enforces
5
5
  // the contract documented in pipeline/schemas/triage-output.schema.json AND
6
- // the edge-case rules from phase-4-review.md section3.3.
6
+ // the edge-case rules from phase-3-review.md section3.3.
7
7
  //
8
8
  // Unlike validate-schemas.mjs (which only checks schema files are well-formed),
9
9
  // this validates an ACTUAL triage run's output.
@@ -81,7 +81,7 @@ function validateRawFinding(f, label, errors) {
81
81
  }
82
82
  }
83
83
 
84
- // v3.2.0 verify-by-test outcome (Phase 4 Step 3.7). Optional; when present:
84
+ // v3.2.0 verify-by-test outcome (Phase 3 Step 3.7). Optional; when present:
85
85
  // result is required, and confirmed/not-reproduced additionally require
86
86
  // testRef + evidencePath (the empirical claims must be traceable).
87
87
  function validateVerification(v, label, errors) {
@@ -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
  }