shapeup-sdlc 3.18.3 → 4.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 (141) hide show
  1. package/.claude/settings.local.example.json +2 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +115 -56
  4. package/README.md +101 -143
  5. package/SECURITY.md +16 -25
  6. package/agents/orient.md +14 -0
  7. package/agents/qa-edge-hunter.md +13 -0
  8. package/agents/spec-evaluator.md +14 -0
  9. package/agents/task-executor.md +14 -0
  10. package/bin/init.mjs +13 -16
  11. package/bin/lib/grant.mjs +8 -19
  12. package/commands/build.md +9 -14
  13. package/commands/eval.md +3 -3
  14. package/commands/hammer.md +9 -5
  15. package/commands/retro.md +12 -11
  16. package/commands/scopes.md +1 -12
  17. package/commands/ship.md +45 -88
  18. package/commands/wire.md +1 -8
  19. package/hooks/dispatch-receipt.mjs +94 -8
  20. package/hooks/hooks.json +0 -41
  21. package/hooks/lib/decision.mjs +6 -9
  22. package/hooks/lib/tier.mjs +50 -0
  23. package/hooks/sandbox-guard.mjs +140 -64
  24. package/kernel/gate.mjs +203 -22
  25. package/kernel/harness.mjs +20 -67
  26. package/kernel/init/run.mjs +175 -523
  27. package/kernel/lib/contract.mjs +9 -93
  28. package/kernel/lib/floor.mjs +64 -0
  29. package/kernel/lib/paths.mjs +39 -112
  30. package/kernel/order.mjs +157 -0
  31. package/kernel/probe/digest.mjs +2 -38
  32. package/kernel/schemas/domain.schema.json +146 -1125
  33. package/kernel/schemas/work-order.schema.json +8 -4
  34. package/kernel/schemas/work-result.schema.json +1 -1
  35. package/kernel/verify/envelope.mjs +2 -2
  36. package/kernel/verify/skills.mjs +27 -8
  37. package/kernel/verify/t0.mjs +2 -2
  38. package/package.json +4 -2
  39. package/skills/building/SKILL.md +144 -0
  40. package/skills/building/resources/gates.md +76 -0
  41. package/skills/building/resources/guidance.md +23 -0
  42. package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
  43. package/skills/building/resources/intake.md +157 -0
  44. package/skills/building/resources/orders.md +69 -0
  45. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
  46. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
  47. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
  48. package/skills/building/resources/plan-scopes.md +95 -0
  49. package/skills/building/resources/plan-spec.md +102 -0
  50. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
  51. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
  52. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
  53. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
  54. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
  55. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
  56. package/skills/building/resources/plan-wire.md +82 -0
  57. package/skills/building/resources/plan.md +98 -0
  58. package/skills/building/resources/retro-research.md +60 -0
  59. package/skills/building/resources/retro-scan.md +49 -0
  60. package/skills/building/resources/retro.md +225 -0
  61. package/skills/building/resources/rounds.md +78 -0
  62. package/skills/building/resources/stop.md +127 -0
  63. package/skills/orient/SKILL.md +37 -37
  64. package/skills/qa-edge-hunter/SKILL.md +14 -14
  65. package/skills/shapeup/SKILL.md +5 -5
  66. package/skills/shapeup/resources/breadboarding.md +1 -1
  67. package/skills/shapeup/resources/context-compaction.md +5 -5
  68. package/skills/shapeup/resources/shaping.md +1 -1
  69. package/skills/spec-evaluator/SKILL.md +17 -21
  70. package/skills/spec-evaluator/references/dimension-contract.md +1 -1
  71. package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
  72. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
  73. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
  74. package/skills/task-executor/SKILL.md +11 -15
  75. package/commands/hill.md +0 -10
  76. package/hooks/gate-intake.mjs +0 -109
  77. package/hooks/gate-zerowork.mjs +0 -349
  78. package/hooks/tier-guard.mjs +0 -162
  79. package/kernel/compile.mjs +0 -1419
  80. package/kernel/init/fit.mjs +0 -199
  81. package/kernel/init/run-args.mjs +0 -206
  82. package/kernel/lib/breadboard.mjs +0 -165
  83. package/kernel/probe/attempts.mjs +0 -161
  84. package/kernel/probe/concurrency.mjs +0 -535
  85. package/kernel/probe/eval.mjs +0 -405
  86. package/kernel/probe/hunt.mjs +0 -104
  87. package/kernel/probe/leg.mjs +0 -229
  88. package/kernel/probe/owner.mjs +0 -142
  89. package/kernel/probe/requirements.mjs +0 -328
  90. package/kernel/probe/resume.mjs +0 -1110
  91. package/kernel/probe/rounds.mjs +0 -115
  92. package/kernel/probe/staged.mjs +0 -83
  93. package/kernel/probe/stats.mjs +0 -477
  94. package/kernel/probe/t0.mjs +0 -115
  95. package/kernel/reduce/board.mjs +0 -350
  96. package/kernel/reduce/graph.mjs +0 -482
  97. package/kernel/reduce/hill.mjs +0 -322
  98. package/kernel/reduce/ingest.mjs +0 -767
  99. package/kernel/reduce/leftovers.mjs +0 -159
  100. package/kernel/reduce/ship.mjs +0 -529
  101. package/kernel/reduce/snapshot.mjs +0 -291
  102. package/kernel/reduce/verdict.mjs +0 -172
  103. package/kernel/report/export.mjs +0 -384
  104. package/kernel/report/facts.mjs +0 -234
  105. package/kernel/verify/budget.mjs +0 -153
  106. package/kernel/verify/build.mjs +0 -428
  107. package/kernel/verify/dispatch.mjs +0 -114
  108. package/kernel/verify/spec.mjs +0 -975
  109. package/kernel/verify/trace.mjs +0 -604
  110. package/skills/ba-pitch-analyzer/SKILL.md +0 -204
  111. package/skills/coach/SKILL.md +0 -397
  112. package/skills/hill-chart/SKILL.md +0 -154
  113. package/skills/hill-chart/assets/dashboard.template.html +0 -1432
  114. package/skills/scope-architect/SKILL.md +0 -174
  115. package/skills/scope-hammer/SKILL.md +0 -231
  116. package/skills/solution-architect/SKILL.md +0 -158
  117. package/skills/tech-lead/SKILL.md +0 -151
  118. package/skills/tech-lead/references/gates.md +0 -605
  119. package/skills/tech-lead/references/protocol.md +0 -889
  120. package/skills/tech-lead/references/tiny-lane.md +0 -53
  121. package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
  122. package/skills/translator/SKILL.md +0 -258
  123. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
  124. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
  125. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
  126. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
  127. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
  128. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
  129. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
  130. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
  131. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
  132. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
  133. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
  134. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
  135. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
  136. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
  137. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
  138. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
  139. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
  140. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
  141. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
@@ -1,529 +0,0 @@
1
- #!/usr/bin/env node
2
- // SHIP REPORT — freeze one reviewable markdown report at GATE L4 (ADR-0001).
3
- //
4
- // WHY THIS EXISTS.
5
- //
6
- // Everything a reviewer wants from a run is in the LOCAL tier and therefore gitignored: the
7
- // verdict and its refuted criteria, the QA findings, the T0 artifacts, the adjudicated decisions,
8
- // the cut list. A pull request showed the spec and the code and NONE of the evidence that the one
9
- // matched the other. The obvious fix — commit those artifacts — is the wrong one: they mutate
10
- // throughout a run, so the deliverable tier would churn on every attempt and every round.
11
- //
12
- // So the split is by TIME rather than by artifact. They stay local while they change; their
13
- // conclusions are frozen ONCE, when the run ends, into `shapeup/<slug>/REPORT.md`.
14
- //
15
- // EVERY NUMBER HERE IS DERIVED, NEVER PASSED IN. The report reduces over files the harness already
16
- // wrote — the trial ledger, the verdict artifacts, the board, the discovery ledger, the round
17
- // ledger. Nothing in it is a claim the orchestrator makes about its own run, which is the whole
18
- // discipline this harness exists to enforce; a ship report that accepted a summary as input would
19
- // be the "agent reports done" pathology wearing a deliverable's clothes.
20
- //
21
- // Zero dependencies, zero network.
22
- //
23
- // Usage:
24
- // node `harness reduce ship` --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated]
25
- // [--qa run|skipped] [--stdout]
26
- //
27
- // Exit: 0 written (path on stdout), 2 usage error.
28
-
29
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from "node:fs";
30
- import { join, dirname } from "node:path";
31
- import { runArgs } from "../lib/argv.mjs";
32
- import {
33
- report as reportPath, tasksDir, verdictsDir, trials, evaluationDir, qaDir,
34
- roundLedger, discoveryLedger, receipt as receiptPath, harnessRun, relShared,
35
- activeOrder, runArgsPath, readReceipt, runIdFromReceipt, hammerCensus, LOCAL,
36
- } from "../lib/paths.mjs";
37
- import { readTrials } from "../verify/t0.mjs";
38
- import { ratchetReport } from "../probe/stats.mjs";
39
- import { projectRequirements, summaryLine } from "../probe/requirements.mjs";
40
- import { deriveRounds } from "../probe/rounds.mjs";
41
- import { collectDiff, scanDiff, summarize } from "./leftovers.mjs";
42
- import { chartersRun } from "../probe/hunt.mjs";
43
-
44
- /** @returns {string} Today as `YYYY-MM-DD` (UTC). */
45
- const today = () => new Date().toISOString().slice(0, 10);
46
-
47
- /**
48
- * Read a file, tolerating absence — every section of this report is optional, because a run can
49
- * legitimately end at GATE H with no QA pass or no evaluation.
50
- * @param {string} p - Path.
51
- * @returns {(string|null)} Contents, or null.
52
- */
53
- function readIf(p) {
54
- try { return existsSync(p) ? readFileSync(p, "utf8") : null; } catch { return null; }
55
- }
56
-
57
- /**
58
- * Parse a Markdown frontmatter block into a flat scalar map.
59
- * @param {string} text - Document text.
60
- * @returns {Object<string,string>} Top-level `key: value` pairs.
61
- */
62
- export function frontmatter(text) {
63
- const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text || "");
64
- if (!m) return {};
65
- const fm = {};
66
- for (const line of m[1].split(/\r?\n/)) {
67
- const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line.trim());
68
- if (kv) fm[kv[1]] = kv[2].replace(/^["']|["']$/g, "");
69
- }
70
- return fm;
71
- }
72
-
73
- /**
74
- * Board census, from task frontmatter — the same two-source discipline the GATE L2 block uses, reduced to
75
- * the authoritative one.
76
- * @param {string} cwd - Project root.
77
- * @param {string} slug - Feature slug.
78
- * @returns {{total:number, done:number, unfinished:string[]}} Counts and the ids still open.
79
- */
80
- export function boardCensus(cwd, slug) {
81
- const dir = tasksDir(cwd, slug);
82
- const out = { total: 0, done: 0, unfinished: [], anchors: {} };
83
- if (!existsSync(dir)) return out;
84
- for (const f of readdirSync(dir)) {
85
- if (!/^TASK-[\w.-]+\.md$/i.test(f)) continue;
86
- const body = readIf(join(dir, f)) || "";
87
- const fm = frontmatter(body);
88
- const id = fm.id || f.replace(/\.md$/, "");
89
- out.total++;
90
- // The COMMITTED anchor for this board id. `use_case_refs` is the tier-direction rule's own
91
- // sanctioned direction (LOCAL names SHARED), and it is what the frozen report cites instead of
92
- // the id — boards renumber per machine, use cases do not.
93
- const ucs = String(fm.use_case_refs ?? "").replace(/^\[|\]$/g, "")
94
- .split(",").map((x) => x.trim()).filter(Boolean);
95
- out.anchors[id] = ucs;
96
- if (fm.status === "done") out.done++;
97
- else out.unfinished.push(id);
98
- }
99
- out.unfinished.sort();
100
- return out;
101
- }
102
-
103
- /**
104
- * Replace every board id in free prose with its committed anchor.
105
- *
106
- * THE WRITE BOUNDARY, not the column, and that is the whole point. Board ids reached the frozen
107
- * report three different ways — the unfinished-task callout, the covering-AC column's own prefix,
108
- * and INSIDE acceptance-criterion prose a planner wrote ("given the seeded todos (TASK-006)"). The
109
- * third is upstream free text, so a fix that only changes what the columns interpolate still
110
- * commits a file the next run's spec-lint reds. Everything written into the committed report passes
111
- * through here.
112
- *
113
- * An id with no resolvable use case becomes a neutral phrase rather than the id: the report loses a
114
- * pointer that never resolved off this machine anyway, and keeps the sentence around it.
115
- *
116
- * A path into the run trace is the same leak by another spelling: the QA findings section quotes
117
- * the hunt report, which points at the discovery ledger by its local path, and the next run's
118
- * spec-lint refused the report for it. The path becomes "the run trace", and the sentence stays.
119
- *
120
- * @param {*} text - Any value destined for the committed report.
121
- * @param {Record<string, string[]>} anchors - Board id → its `use_case_refs`.
122
- * @returns {string} The text with every `TASK-…` replaced by a stable anchor and every run-trace
123
- * path by a phrase.
124
- */
125
- export function deboard(text, anchors = {}) {
126
- const esc = LOCAL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
- return String(text ?? "")
128
- .replace(/\bTASK-[A-Za-z0-9][\w.-]*/g, (id) => {
129
- const ucs = anchors[id];
130
- if (ucs && ucs.length) return ucs.join("/");
131
- return "a board task";
132
- })
133
- .replace(new RegExp(`\`?${esc}/[^\\s\`]+\`?`, "g"), (m) => {
134
- const tail = (m.replace(/`/g, "").match(/[)\]},.;:'"]+$/) || [""])[0];
135
- return `the run trace${tail}`;
136
- });
137
- }
138
-
139
- /**
140
- * Per-scope T0 outcome, reduced from the trial ledger.
141
- *
142
- * The LAST trial per scope whose tree survived (`kept`/`rebased`) is the one describing the code
143
- * that actually shipped — a `reverted` trial's tree was thrown away, so reporting its score would
144
- * describe work nobody can see.
145
- *
146
- * @param {string} cwd - Project root.
147
- * @param {string} slug - Feature slug.
148
- * @returns {Array<{scope_id:string, score:object, status:string, delta:string, trials:number}>}
149
- * One row per scope, ordered by scope id.
150
- */
151
- export function t0Summary(cwd, slug) {
152
- const rows = readTrials(trials(cwd, slug));
153
- const byScope = new Map();
154
- for (const t of rows) {
155
- if (!t.scope_id) continue;
156
- const e = byScope.get(t.scope_id) || { scope_id: t.scope_id, trials: 0, last: null };
157
- e.trials++;
158
- if (t.status === "kept" || t.status === "rebased") e.last = t;
159
- byScope.set(t.scope_id, e);
160
- }
161
- return [...byScope.values()]
162
- .map((e) => ({
163
- scope_id: e.scope_id,
164
- trials: e.trials,
165
- score: e.last?.score ?? null,
166
- status: e.last?.status ?? "no surviving trial",
167
- delta: e.last?.delta ?? "",
168
- }))
169
- .sort((a, b) => a.scope_id.localeCompare(b.scope_id));
170
- }
171
-
172
- /**
173
- * Count the immutable verdict artifacts on disk — the evidence the evaluator had to cite.
174
- * @param {string} cwd - Project root.
175
- * @param {string} slug - Feature slug.
176
- * @returns {number} How many artifacts exist.
177
- */
178
- export function verdictArtifactCount(cwd, slug) {
179
- const dir = verdictsDir(cwd, slug);
180
- if (!existsSync(dir)) return 0;
181
- try { return readdirSync(dir).filter((f) => f.endsWith(".json")).length; } catch { return 0; }
182
- }
183
-
184
- /**
185
- * Lift a named section out of a markdown document.
186
- * @param {(string|null)} md - Document text.
187
- * @param {RegExp} heading - Matches the heading line that opens the section.
188
- * @returns {(string|null)} The section body, trimmed, or null when absent/empty.
189
- */
190
- export function section(md, heading) {
191
- if (!md) return null;
192
- const lines = md.split(/\r?\n/);
193
- const start = lines.findIndex((l) => heading.test(l));
194
- if (start === -1) return null;
195
- const level = (lines[start].match(/^#+/) || ["#"])[0].length;
196
- const out = [];
197
- for (let i = start + 1; i < lines.length; i++) {
198
- const h = lines[i].match(/^(#+)\s/);
199
- if (h && h[1].length <= level) break;
200
- out.push(lines[i]);
201
- }
202
- const body = out.join("\n").trim();
203
- return body || null;
204
- }
205
-
206
- /**
207
- * The report's opening line when the ship is not a passing one — null for a PASS.
208
- *
209
- * @param {string} verdict - The run's final verdict (`PASS`, `FAIL`, `not-evaluated`, …).
210
- * @param {({verdict?:string, cut_list?:Array}|null)} census - GATE H's census as data, or null.
211
- * @returns {(string|null)} One markdown line naming the verdict, and the cut list that cleared it
212
- * when a census is on record; null when the verdict is PASS.
213
- */
214
- export function shipHeadline(verdict, census) {
215
- if (verdict === "PASS") return null;
216
- const cuts = Array.isArray(census?.cut_list) ? census.cut_list.length : null;
217
- const why = census
218
- ? `GATE H's baseline comparison cleared it${census.verdict ? ` (census: ${census.verdict})` : ""}` +
219
- (cuts != null ? ` with ${cuts} item${cuts === 1 ? "" : "s"} cut` : "")
220
- : "no GATE H census was on record when it shipped";
221
- // A FAIL is a judgement that some criterion failed; anything else here means no judgement was
222
- // made at all, and the line must not claim criteria failed that nobody graded.
223
- if (verdict === "FAIL") {
224
- return `> **Shipped with a FAIL verdict.** Not every criterion passed; ${why}. Read the failed ` +
225
- "criteria and the cut list below before treating this feature as done.";
226
- }
227
- return `> **Shipped without a passing verdict (${verdict}).** No criterion was graded as passing; ${why}. ` +
228
- "Nothing below is evidence that this feature works.";
229
- }
230
-
231
- /**
232
- * Assemble the report. Pure given its inputs, so the structural tests can assert its shape
233
- * without a filesystem.
234
- * @param {object} facts - Derived facts (slug, verdict, board, t0, decisions, findings, …).
235
- * @returns {string} The markdown document.
236
- */
237
- export function buildReport(facts) {
238
- const {
239
- slug, at, verdict, qa, rounds, roundsJudged, board, t0, artifacts, ratchet,
240
- evalCriteria, evalBugs, qaFindings, decisions, discovered, intakeSha, leftovers, requirements,
241
- census = null,
242
- } = facts;
243
-
244
- const L = [];
245
- L.push("---", "type: ship-report", `feature: ${slug}`, `date: ${at}`,
246
- `verdict: ${verdict}`, `rounds_used: ${rounds ?? "~"}`, `rounds_judged: ${roundsJudged ?? "~"}`, `qa: ${qa}`,
247
- `intake_sha256: ${intakeSha ?? "~"}`, "---", "");
248
- L.push(`# ${slug} — ship report`, "");
249
- // A SHIP IS NOT A PASS, AND THE FIRST LINE SAYS WHICH ONE THIS IS. A run whose verdict FAILed can
250
- // still ship — GATE H compares against the baseline, not the ideal, and a cut list can clear it —
251
- // and its status then reads `shipped` like any other. The verdict sat in a table cell below the
252
- // fold, so a reader who stopped at the title took a shipped FAIL for a passing build.
253
- const headline = shipHeadline(verdict, census);
254
- if (headline) L.push(headline, "");
255
- L.push("Frozen at GATE L4. Every figure below is derived from run artifacts on disk — the trial",
256
- "ledger, the verdict artifacts, the board — never from a summary of the run.", "");
257
-
258
- L.push("## Outcome", "");
259
- L.push("| | |", "|---|---|");
260
- L.push(`| Verdict | **${verdict}** |`);
261
- L.push(`| Rounds used | ${rounds ?? "—"} |`);
262
- // A round built and a round judged are different facts — a round can die before EVAL ever sees
263
- // it, so this row is its own line rather than folded into "Rounds used" above. Omitted when EVAL
264
- // never ran at all, the same way the sections below it are.
265
- if (roundsJudged != null) L.push(`| Rounds judged | ${roundsJudged} |`);
266
- L.push(`| Board | ${board.done}/${board.total} tasks done |`);
267
- L.push(`| T0 artifacts | ${artifacts} |`);
268
- L.push(`| QA | ${qa} |`);
269
- L.push("");
270
-
271
- if (board.unfinished.length) {
272
- // Anchored, never enumerated by board id: the ids renumber per machine, and a committed file
273
- // carrying one reds the NEXT run of this pitch at L1b.
274
- const unfinishedAnchors = [...new Set(board.unfinished.flatMap((id) => board.anchors?.[id] ?? []))];
275
- L.push(`> **${board.unfinished.length} task(s) did not finish**${unfinishedAnchors.length ? ` — use cases: ${unfinishedAnchors.join(", ")}` : ""}.`,
276
- "> The verdict above grades what was built, not what was planned.", "");
277
- }
278
-
279
- // Leftovers — advisory, and a SECTION rather than a verdict. It was a Stop hook that printed once
280
- // into a transcript; here it lands in the artifact a human reads at GATE L4 and a teammate finds
281
- // on `git pull`, which is the difference between a note and a record.
282
- if (leftovers?.length) {
283
- L.push("## Leftovers (advisory)", "");
284
- L.push("Markers in lines this run ADDED. Not a gate and not part of the verdict — a cleanup list.", "");
285
- for (const line of leftovers) L.push(`- ${line}`);
286
- L.push("");
287
- }
288
-
289
- if (t0.length) {
290
- L.push("## Verification (T0)", "");
291
- L.push("The surviving trial per scope — the one describing code that is actually on the branch.", "");
292
- L.push("| scope | fixtures | regressions | trials | last status | delta |", "|---|---|---|---|---|---|");
293
- for (const s of t0) {
294
- const f = s.score ? `${s.score.fixtures_passed}/${s.score.fixtures_total}` : "—";
295
- const r = s.score?.regressions != null ? String(s.score.regressions) : "—";
296
- L.push(`| ${s.scope_id} | ${f} | ${r} | ${s.trials} | ${s.status} | ${s.delta || "—"} |`);
297
- }
298
- L.push("");
299
- }
300
-
301
- // The requirement matrix — the way back from a verdict to the clause the pitch asked for, frozen
302
- // at the one moment the run's local evidence still exists. Omitted entirely when the run has no
303
- // registry: a table of nothing reads as "no requirements", which is a different claim from "this
304
- // run predates the registry". Derived like every other figure here, by the same probe the L4 line
305
- // and GATE H's census read, so the three cannot disagree.
306
- if (requirements?.registry && requirements.rows.length) {
307
- L.push("## Requirements", "");
308
- L.push("One row per registered clause. A requirement has evidence when an acceptance criterion",
309
- "covers it AND a criterion grading it passed — `covers:` is the join, the judge's anchor is the",
310
- "path back. This is a projection, never a verdict: it never blocked this ship.", "");
311
- L.push(`**${summaryLine(requirements)}** · run \`${requirements.run_id ?? "unknown"}\``, "");
312
- // A clause, an AC and a criterion are all free prose, and a literal pipe in any of them breaks
313
- // the row into columns nobody wrote — a frozen report that misrenders its own evidence.
314
- /**
315
- * Escape a free-prose value for a Markdown table cell.
316
- * @param {string} s - The value.
317
- * @returns {string} The value with every literal pipe escaped.
318
- */
319
- const cell = (s) => String(s).replace(/\|/g, "\\|");
320
- L.push("| REQ | source | evidence | covering AC | criterion | T0 |", "|---|---|---|---|---|---|");
321
- for (const r of requirements.rows) {
322
- const ac = r.covering_acs.length
323
- ? `${deboard(r.covering_acs[0].task_id, facts.board?.anchors)}: ${deboard(r.covering_acs[0].ac, facts.board?.anchors)}${r.covering_acs.length > 1 ? ` (+${r.covering_acs.length - 1})` : ""}`
324
- : "—";
325
- const crit = r.criteria.length ? `${r.criteria[0].criterion}${r.criteria.length > 1 ? ` (+${r.criteria.length - 1})` : ""} → ${r.criteria.map((c) => c.verdict).join(",")}` : "—";
326
- const t0h = r.t0.length ? r.t0.map((h) => String(h).slice(0, 12)).join(", ") : "—";
327
- L.push(`| ${r.id} | ${cell(r.source || "—")} | ${r.evidence} | ${cell(ac)} | ${cell(crit)} | ${t0h} |`);
328
- }
329
- L.push("");
330
- if (requirements.inconsistencies.length) {
331
- L.push("Anchored to a requirement no acceptance criterion covers — reconcile, do not count as evidence:", "");
332
- for (const i of requirements.inconsistencies) L.push(`- ${i.requirement} ← "${i.criterion}" (${i.verdict})`);
333
- L.push("");
334
- }
335
- }
336
-
337
- // The ratchet aggregate is derived, ~10 scalars that do not grow with the run, which is why it
338
- // can live in the committed tier while `metrics/` correctly stays gitignored (ADR-0001: a
339
- // committed shard keyed on $HOSTNAME only grows). Without this the instrument existed and was
340
- // never read — the trial ledger it reduces is harvested at SHIP or lost with the local tier.
341
- if (ratchet && ratchet.trials > 0) {
342
- L.push("## Ratchet", "");
343
- L.push("Measured over this run's trial ledger. A monotone series is a ratchet working; a flat or",
344
- "sawtooth series says the loop is still a budgeted retry loop wearing a ratchet's shape.", "");
345
- L.push("| | |", "|---|---|");
346
- L.push(`| Trials | ${ratchet.trials} across ${ratchet.scopes} scope(s), ${ratchet.scopes_multi_trial} with more than one attempt |`);
347
- L.push(`| Improvement rate | ${ratchet.improvement_rate} — kept ÷ trials after the first |`);
348
- L.push(`| Monotone rate | ${ratchet.monotone_rate} — multi-trial scopes whose score never decreased |`);
349
- L.push(`| Sawtooth count | ${ratchet.sawtooth_count} — a revert immediately after a keep |`);
350
- L.push(`| Mean trials to green | ${ratchet.mean_trials_to_green ?? "— (no scope reached green)"} |`);
351
- const hist = Object.entries(ratchet.status_histogram).map(([k, v]) => `${k} ${v}`).join(", ");
352
- L.push(`| Statuses | ${hist || "—"} |`);
353
- L.push("");
354
- // A zero improvement_rate means one of two opposite things, and the number alone cannot say
355
- // which: the loop tried again and failed to improve, or nothing ever needed a second attempt.
356
- // Measured on the first two real runs — every scope went green on attempt 1 — so the reading a
357
- // reviewer meets by default is the degenerate one. `stats --ratchet` says so on the terminal;
358
- // the committed report has to say it too, or a 0 here is read as "the ratchet did not work".
359
- if (ratchet.scopes_multi_trial === 0) {
360
- L.push("> No scope needed a second attempt, so the rates above are vacuous rather than bad:",
361
- "> the ratchet was never asked to climb. The Day-1 question — does the loop measurably",
362
- "> improve across attempts — needs a run where at least one scope retries.", "");
363
- }
364
- }
365
-
366
- if (evalCriteria) L.push("## Evaluation", "", evalCriteria, "");
367
- if (evalBugs) L.push("### Refuted criteria and bugs", "", evalBugs, "");
368
- if (qaFindings) L.push("## QA findings", "", qaFindings, "");
369
- if (decisions) L.push("## Decisions adjudicated during the run", "", decisions, "");
370
- if (discovered) L.push("## Discovered, not built", "", discovered, "");
371
-
372
- L.push("---", "",
373
- "*Run state (board, orders, results, T0 artifacts, evaluation and QA reports) stays in the",
374
- "gitignored local tier (ADR-0001). This report",
375
- "is the frozen conclusion of it.*", "");
376
- // THE WHOLE REPORT, ONCE. Board ids were anchored in three places and leaked through a fourth: a
377
- // discovery-ledger entry copied verbatim into "Discovered, not built" carried `[TASK-005]`, this
378
- // kernel wrote it straight to the committed tier past the hook that guards the model's edits, and
379
- // the next run's L1b lint red'd the file the previous run had frozen. A committed report may not
380
- // name a board id anywhere, so the rule is applied to the finished text rather than section by
381
- // section.
382
- return deboard(L.join("\n"), board.anchors);
383
- }
384
-
385
- /**
386
- * The QA line the report may print. The run says whether it dispatched the hunt; the hunt's own
387
- * report says whether anything was hunted. A hunter that could not reach the app still returns, and
388
- * a report opening `charters: 0/…` over a dispatched hunt read as "QA: run" with no findings — which
389
- * a reader takes for an app with nothing wrong, not for an app nobody drove.
390
- * @param {string|undefined} passed - What the caller recorded (`run`, `skipped`), if anything.
391
- * @param {string|null} huntReport - The hunt report's text, or null when there is none.
392
- * @returns {string} `skipped`, `not-hunted` (dispatched, zero charters run), or `run`.
393
- */
394
- export function qaStatus(passed, huntReport) {
395
- if (passed === "skipped" || (!passed && !huntReport)) return passed || "skipped";
396
- if (huntReport && chartersRun(huntReport) === 0) return "not-hunted";
397
- return passed || "run";
398
- }
399
-
400
- /**
401
- * Gather every fact from disk and render the report.
402
- * @param {{cwd:string, slug:string, verdict?:string, qa?:string}} opts - Inputs.
403
- * @returns {{markdown:string, path:string, facts:object}} The document, its destination, the facts.
404
- */
405
- export function generate({ cwd, slug, verdict, qa }) {
406
- const run = frontmatter(readIf(harnessRun(cwd, slug)) || "");
407
- const receipt = (() => { try { return JSON.parse(readIf(receiptPath(cwd, slug)) || "{}"); } catch { return {}; } })();
408
-
409
- const evalReport = readIf(join(evaluationDir(cwd, slug), `EVAL-FEATURE-${slug}.md`));
410
- const huntReport = readIf(join(qaDir(cwd, slug), "hunt-report.md"));
411
- const ledger = readIf(roundLedger(cwd, slug));
412
- const discovery = readIf(discoveryLedger(cwd, slug));
413
-
414
- // Two numbers, not one: `rounds` (built — an order, a T0 verdict, a build-gate artifact or an
415
- // EVAL result) and `roundsJudged` (EVAL actually returned a verdict for), kept separate so a run
416
- // whose later rounds never reached EVAL still reports the rounds it built.
417
- const derivedRounds = deriveRounds(cwd, slug, run.rounds_used);
418
-
419
- const facts = {
420
- slug,
421
- at: today(),
422
- verdict: verdict || run.final_verdict || "not-evaluated",
423
- census: (() => { try { return JSON.parse(readIf(hammerCensus(cwd, slug)) || "null"); } catch { return null; } })(),
424
- qa: qaStatus(qa, huntReport),
425
- rounds: derivedRounds.rounds_used,
426
- roundsJudged: derivedRounds.rounds_judged,
427
- intakeSha: receipt.intake_sha256,
428
- board: boardCensus(cwd, slug),
429
- t0: t0Summary(cwd, slug),
430
- ratchet: ratchetReport(readTrials(trials(cwd, slug))),
431
- requirements: projectRequirements({ cwd, slug }),
432
- artifacts: verdictArtifactCount(cwd, slug),
433
- evalCriteria: section(evalReport, /^#+\s.*criteria/i) || section(evalReport, /^#+\s*spec-conformance/i),
434
- evalBugs: section(evalReport, /^#+\s*Bugs?\b/i),
435
- qaFindings: section(huntReport, /^#+\s*Findings?\b/i),
436
- decisions: section(ledger, /^#+\s*Decisions?\b/i),
437
- discovered: section(discovery, /^#+\s*Discovered\b/i),
438
- leftovers: (() => {
439
- // Advisory, and derived like everything else here: the run's own diff, added lines only.
440
- const diff = collectDiff(cwd, slug);
441
- return diff ? summarize(scanDiff(diff)) : [];
442
- })(),
443
- };
444
- return { markdown: buildReport(facts), path: reportPath(cwd, slug), facts };
445
- }
446
-
447
- // ---------------------------------------------------------------------------
448
- /** The typed argv contract (see `./lib/argv.mjs`). */
449
- export const ARGV_SPEC = {
450
- usage: "harness.mjs reduce ship --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated] " +
451
- "[--qa run|skipped] [--stdout]",
452
- _: { arity: 0, max: 0, name: "(no positional operands)" },
453
- slug: { type: "str", required: true },
454
- cwd: { type: "path" },
455
- verdict: { type: "str" },
456
- qa: { type: "str" },
457
- stdout: { type: "flag" },
458
- };
459
-
460
- /**
461
- * Generate the ship report from the artifacts on disk.
462
- *
463
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
464
- * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
465
- * call `process.exit()` with the subcommand's documented code rather than returning.
466
- */
467
- /**
468
- * Did THIS run skip evaluation? Read from the run's own recorded arguments, and believed only when
469
- * the record can be shown to belong to this run.
470
- *
471
- * WHY THE KERNEL ASKS AT ALL. A run launched with `--no-eval` verified nothing, and the protocol
472
- * has always said such a run ships as `not-evaluated`, "recorded plainly — never silently
473
- * upgraded". That promise lived entirely inside the orchestrator's control flow, where one
474
- * assignment downstream of the branch restores the old behaviour with every check still green —
475
- * measured, not supposed. The gate block tells the human the truth either way; the committed report
476
- * a teammate inherits on `git pull` is the artifact that was lying, so the refusal belongs at the
477
- * writer of that artifact, where a future orchestrator edit cannot reach it.
478
- *
479
- * POSITIVELY PROVEN OR NOT AT ALL. The record is written at launch, later than the receipt that
480
- * mints the run key, so a freshly opened run can still find the PREVIOUS run's record on disk. A
481
- * stale flag would refuse a ship that verified everything — a worse failure than the one this
482
- * closes. The flag therefore counts only when the record names the same run the receipt does;
483
- * a missing, unreadable or differently-keyed record proves nothing and permits.
484
- *
485
- * @param {string} cwd - Project root.
486
- * @param {string} slug - Feature slug.
487
- * @returns {boolean} True only when this run's own record says evaluation was skipped.
488
- */
489
- function evalWasSkipped(cwd, slug) {
490
- try {
491
- const record = JSON.parse(readFileSync(runArgsPath(cwd, slug), "utf8"));
492
- if (record?.noEval !== true) return false;
493
- const mine = runIdFromReceipt(readReceipt(receiptPath(cwd, slug)));
494
- return Boolean(mine) && record.runId === mine;
495
- } catch { return false; }
496
- }
497
-
498
- /** The verdict values that assert the feature was graded and passed. */
499
- const PASSING = new Set(["PASS", "pass"]);
500
-
501
- export async function cli(rawArgv) {
502
- const args = runArgs(ARGV_SPEC, rawArgv);
503
- const cwd = args.cwd || process.cwd();
504
- if (PASSING.has(String(args.verdict ?? "")) && evalWasSkipped(cwd, args.slug)) {
505
- console.error(
506
- "✋ reduce ship: this run was launched with --no-eval, so nothing graded it — refusing to freeze a report " +
507
- `that says ${args.verdict}. Ship it as --verdict not-evaluated, which the report, the ledger and the ` +
508
- "sign-off block all carry.",
509
- );
510
- process.exit(3);
511
- }
512
- const { markdown, path } = generate({ cwd, slug: args.slug, verdict: args.verdict, qa: args.qa });
513
- if (args.stdout) {
514
- process.stdout.write(markdown);
515
- } else {
516
- mkdirSync(dirname(path), { recursive: true });
517
- writeFileSync(path, markdown, "utf8");
518
-
519
- // THE RUN IS OVER, SO RETIRE ITS POINTER. `harness compile` publishes `.shapeup/active-order`
520
- // as it writes each order and nothing ever erased it, so the pointer outlived every run that
521
- // produced one. That is harmless to the guard now — liveness comes from the order set, and a
522
- // shipped run has no unanswered orders — but a pointer naming a finished run is a fact on disk
523
- // that is no longer true, and the next reader to trust it inherits the same class of bug.
524
- // Only on the writing path: `--stdout` is a preview of the report, not the end of the run.
525
- rmSync(activeOrder(cwd), { force: true });
526
-
527
- console.log(relShared(args.slug, "REPORT.md"));
528
- }
529
- }