shapeup-sdlc 1.6.3 → 3.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 (118) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +54 -107
  4. package/README.md +165 -151
  5. package/SECURITY.md +49 -27
  6. package/bin/init.mjs +93 -108
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +20 -0
  9. package/commands/eval.md +5 -4
  10. package/commands/scopes.md +5 -4
  11. package/commands/shape.md +1 -1
  12. package/commands/ship.md +53 -7
  13. package/commands/wire.md +1 -1
  14. package/hooks/dispatch-receipt.mjs +195 -0
  15. package/hooks/gate-intake.mjs +16 -17
  16. package/hooks/gate-zerowork.mjs +107 -25
  17. package/hooks/hooks.json +9 -48
  18. package/hooks/lib/decision.mjs +38 -19
  19. package/hooks/safety-spine.mjs +4 -4
  20. package/hooks/sandbox-guard.mjs +130 -50
  21. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
  22. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
  23. package/kernel/harness.mjs +134 -0
  24. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
  25. package/kernel/init/run.mjs +489 -0
  26. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  27. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
  28. package/kernel/lib/paths.mjs +491 -0
  29. package/kernel/probe/concurrency.mjs +510 -0
  30. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
  31. package/kernel/probe/eval.mjs +77 -0
  32. package/kernel/probe/leg.mjs +125 -0
  33. package/kernel/probe/resume.mjs +528 -0
  34. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
  35. package/kernel/probe/t0.mjs +66 -0
  36. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
  37. package/kernel/reduce/graph.mjs +437 -0
  38. package/kernel/reduce/hill.mjs +152 -0
  39. package/kernel/reduce/ingest.mjs +633 -0
  40. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  41. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
  42. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
  43. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
  44. package/kernel/report/export.mjs +325 -0
  45. package/kernel/report/facts.mjs +347 -0
  46. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
  47. package/kernel/verify/dispatch.mjs +114 -0
  48. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  49. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  50. package/kernel/verify/skills.mjs +125 -0
  51. package/kernel/verify/spec.mjs +559 -0
  52. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
  53. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
  54. package/oracles/_shared.mjs +1 -1
  55. package/oracles/http-oracle.mjs +2 -2
  56. package/oracles/index.mjs +1 -1
  57. package/oracles/process-oracle.mjs +2 -2
  58. package/oracles/snapshot-oracle.mjs +2 -2
  59. package/oracles/test-oracle.mjs +2 -2
  60. package/package.json +11 -13
  61. package/skills/ba-pitch-analyzer/SKILL.md +24 -19
  62. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  63. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  64. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  65. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  66. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  67. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  68. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  69. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  70. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  71. package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
  72. package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
  73. package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
  74. package/skills/coach/SKILL.md +3 -3
  75. package/skills/orient/SKILL.md +2 -1
  76. package/skills/qa-edge-hunter/SKILL.md +15 -41
  77. package/skills/scope-architect/SKILL.md +57 -18
  78. package/skills/scope-hammer/SKILL.md +5 -5
  79. package/skills/shapeup/SKILL.md +3 -3
  80. package/skills/shapeup/resources/context-compaction.md +4 -3
  81. package/skills/solution-architect/SKILL.md +37 -15
  82. package/skills/spec-evaluator/SKILL.md +24 -7
  83. package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
  84. package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
  85. package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
  86. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
  87. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
  88. package/skills/spec-evaluator/references/probing.md +42 -23
  89. package/skills/spec-evaluator/references/report-schema.md +2 -2
  90. package/skills/task-executor/SKILL.md +32 -18
  91. package/skills/tech-lead/SKILL.md +127 -438
  92. package/skills/tech-lead/references/gates.md +140 -49
  93. package/skills/tech-lead/references/protocol.md +832 -0
  94. package/skills/tech-lead/schemas/domain.schema.json +645 -198
  95. package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
  96. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  97. package/skills/tech-lead/schemas/work-result.schema.json +56 -18
  98. package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
  99. package/skills/translator/SKILL.md +2 -2
  100. package/hooks/anti-rationalization.mjs +0 -244
  101. package/hooks/compact-snapshot.mjs +0 -47
  102. package/hooks/gate-deadline.mjs +0 -151
  103. package/hooks/gate-l2.mjs +0 -161
  104. package/hooks/session-rehydrate.mjs +0 -109
  105. package/skills/advisor-protocol/SKILL.md +0 -171
  106. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  107. package/skills/spec-evaluator/README.md +0 -93
  108. package/skills/tech-lead/README.md +0 -71
  109. package/skills/tech-lead/references/delegation.md +0 -254
  110. package/skills/tech-lead/references/invocation.md +0 -45
  111. package/skills/tech-lead/references/ledger-schema.md +0 -214
  112. package/skills/tech-lead/references/round-protocol.md +0 -184
  113. package/skills/tech-lead/references/state-model.md +0 -66
  114. package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
  115. package/skills/tech-lead/scripts/init-run.mjs +0 -326
  116. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
  117. package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
  118. package/skills/translator/README.md +0 -66
@@ -1,27 +1,23 @@
1
- #!/usr/bin/env node
2
- // Slop cleaner — advisory Stop hook (v1.2, absorb-audit P2).
1
+ // leftovers — what the run's own diff still carries that nobody meant to ship.
3
2
  //
4
- // Scans what actually changed this session (git diff of the working tree; fallback: the
5
- // newest WorkResult's files_touched) for the classic leftovers TODO/FIXME markers,
6
- // console.log/debugger, blocks of commented-out code, one file swallowing hundreds of added
7
- // lines — and mentions them to the user on stop.
3
+ // TODO/FIXME markers, `console.log`/`debugger`, blocks of commented-out code, one file swallowing
4
+ // hundreds of added lines. Added lines only: a marker the feature did not introduce is somebody
5
+ // else's, and a report that lists those is a report people learn to skim.
8
6
  //
9
- // ADVISORY ONLY, same contract as anti-rationalization.mjs: exit 0 always, at most
10
- // { systemMessage }, never { decision:"block" }, never exit 2 ("QA is a level-up, not a
11
- // gate"). Harness-scoped: silent unless a run is active.
7
+ // WHY IT IS PART OF THE SHIP REPORT AND NOT A STOP HOOK. It used to be an advisory Stop hook that
8
+ // printed once, into a transcript, at the moment a session ended the channel least likely to be
9
+ // read and impossible to check later. The ship report is the artifact a human actually reads at
10
+ // GATE L4 and the one a teammate finds on `git pull`, so a leftover recorded there is a leftover
11
+ // somebody can act on. Nothing about the check changed; only where its answer lands.
12
12
  //
13
- // Contract: Stop stdin JSON { cwd, stop_hook_active }.
13
+ // Advisory by construction: it is a SECTION, never a verdict. QA is a level-up, not a gate.
14
14
 
15
15
  import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { spawnSync } from "node:child_process";
18
- import { activeSlug } from "./anti-rationalization.mjs";
19
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
20
- import { LOCAL, SHARED, resultsDir } from "../skills/tech-lead/scripts/lib/paths.mjs";
18
+ import { LOCAL, SHARED, resultsDir } from "../lib/paths.mjs";
21
19
 
22
- import { runHook, readStdin, settle } from "./lib/decision.mjs";
23
-
24
- // Harness bookkeeping is never "slop" — skip both storage roots, whichever names they carry.
20
+ // Harness bookkeeping is never a leftover — skip both storage roots, whichever names they carry.
25
21
  const SKIP_PATH = new RegExp(`^(${[LOCAL, SHARED].map((r) => r.replace(/[.\\]/g, "\\$&")).join("|")})/`);
26
22
  const MAX_FILES = 30;
27
23
  const MAX_ADDED_LINES_PER_FILE = 400;
@@ -32,8 +28,16 @@ const MARKERS = [
32
28
  { name: "debugger", re: /^\s*debugger\b/ },
33
29
  ];
34
30
 
35
- /** Scan a unified diff for slop in ADDED lines only.
36
- * Returns [{ file, markers: {name: count}, added, big, commented_code }]. */
31
+ /**
32
+ * Scan a unified diff for leftovers in ADDED lines only.
33
+ *
34
+ * Added lines only, deliberately: a TODO the feature did not introduce is somebody else's, and a
35
+ * report that lists it is a report people learn to skim.
36
+ *
37
+ * @param {string} diffText - A unified diff.
38
+ * @returns {Array<{file:string, markers:Object<string,number>, added:number, big:boolean, commented_code:boolean}>}
39
+ * One entry per dirty file, capped at MAX_FILES.
40
+ */
37
41
  export function scanDiff(diffText) {
38
42
  const findings = [];
39
43
  let current = null;
@@ -78,7 +82,11 @@ export function scanDiff(diffText) {
78
82
  return findings;
79
83
  }
80
84
 
81
- /** One human-readable fragment per finding. */
85
+ /**
86
+ * One human-readable fragment per finding.
87
+ * @param {Array} findings - The result of {@link scanDiff}.
88
+ * @returns {string[]} At most six lines, each naming a file and what it carries.
89
+ */
82
90
  export function summarize(findings) {
83
91
  return findings.slice(0, 6).map((f) => {
84
92
  const bits = Object.entries(f.markers).map(([name, n]) => `${name} ×${n}`);
@@ -88,12 +96,24 @@ export function summarize(findings) {
88
96
  });
89
97
  }
90
98
 
99
+ /**
100
+ * Run git, returning stdout on success and null otherwise.
101
+ * @param {string} cwd - Working directory.
102
+ * @param {string[]} args - Arguments after `git`.
103
+ * @returns {(string|null)} stdout, or null when git failed or is absent.
104
+ */
91
105
  function git(cwd, args) {
92
106
  const r = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
93
107
  return r.status === 0 ? r.stdout : null;
94
108
  }
95
109
 
96
- /** The session's change set as a unified diff — git first, WorkResult fallback. */
110
+ /**
111
+ * The run's change set as a unified diff — git first, WorkResult fallback.
112
+ * @param {string} cwd - Project root.
113
+ * @param {string} slug - Feature slug.
114
+ * @returns {(string|null)} A unified diff, or null when there is nothing to scan.
115
+ */
116
+ export
97
117
  function collectDiff(cwd, slug) {
98
118
  const tracked = git(cwd, ["diff", "HEAD"]);
99
119
  if (tracked !== null) {
@@ -137,40 +157,3 @@ function collectDiff(cwd, slug) {
137
157
  } catch { return null; }
138
158
  return diff || null;
139
159
  }
140
-
141
- async function main() {
142
- await runHook("slop-cleaner", async () => {
143
- const raw = await readStdin();
144
- let p;
145
- /** Stay silent — with the reason on the record (hooks/lib/decision.mjs). */
146
- const defer = (reason, rule) => settle({ verdict: "allow", event: "Stop", cwd: p?.cwd, reason, rule });
147
- try { p = JSON.parse(raw || "{}"); }
148
- catch (e) { settle({ verdict: "error", event: "Stop", reason: `unparseable payload: ${e.message}` }); }
149
-
150
- if (p.stop_hook_active) defer("stop_hook_active — never participate in a stop-hook loop", "loop-guard");
151
-
152
- const cwd = p.cwd || process.cwd();
153
- const slug = activeSlug(cwd);
154
- if (!slug) defer("no active run — stay silent", "no-run");
155
-
156
- const diff = collectDiff(cwd, slug);
157
- if (!diff) defer("no diff to scan", "no-diff");
158
-
159
- const findings = scanDiff(diff);
160
- if (findings.length === 0) defer("diff scanned, no leftovers found — inspected and permitted", "diff-clean");
161
-
162
- return {
163
- verdict: "allow", event: "Stop", cwd, subject: slug, rule: "slop-found", emit: true,
164
- reason: `${findings.length} leftover(s) in the recent diff — advisory note emitted, not a block`,
165
- payload: {
166
- systemMessage:
167
- `slop-cleaner (advisory): recent edits carry leftovers — ${summarize(findings).join("; ")}. ` +
168
- `Not blocking — consider a cleanup pass.`,
169
- },
170
- };
171
- });
172
- }
173
-
174
- if (isMain(import.meta.url)) {
175
- main();
176
- }
@@ -21,20 +21,21 @@
21
21
  // Zero dependencies, zero network.
22
22
  //
23
23
  // Usage:
24
- // node ship-report.mjs --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated]
24
+ // node `harness reduce ship` --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated]
25
25
  // [--qa run|skipped] [--stdout]
26
26
  //
27
27
  // Exit: 0 written (path on stdout), 2 usage error.
28
28
 
29
29
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
30
30
  import { join, dirname } from "node:path";
31
- import { isMain } from "./lib/is-main.mjs";
32
- import { runArgs } from "./lib/argv.mjs";
31
+ import { runArgs } from "../lib/argv.mjs";
33
32
  import {
34
33
  report as reportPath, tasksDir, verdictsDir, trials, evaluationDir, qaDir,
35
- roundLedger, discoveryLedger, receipt as receiptPath, harnessRun, relShared,
36
- } from "./lib/paths.mjs";
37
- import { readTrials } from "./t0-verify.mjs";
34
+ roundLedger, discoveryLedger, receipt as receiptPath, harnessRun, relShared, resultsDir,
35
+ } from "../lib/paths.mjs";
36
+ import { readTrials } from "../verify/t0.mjs";
37
+ import { ratchetReport } from "../probe/stats.mjs";
38
+ import { collectDiff, scanDiff, summarize } from "./leftovers.mjs";
38
39
 
39
40
  /** @returns {string} Today as `YYYY-MM-DD` (UTC). */
40
41
  const today = () => new Date().toISOString().slice(0, 10);
@@ -66,7 +67,7 @@ export function frontmatter(text) {
66
67
  }
67
68
 
68
69
  /**
69
- * Board census, from task frontmatter — the same two-source discipline `gate-l2` uses, reduced to
70
+ * Board census, from task frontmatter — the same two-source discipline the GATE L2 block uses, reduced to
70
71
  * the authoritative one.
71
72
  * @param {string} cwd - Project root.
72
73
  * @param {string} slug - Feature slug.
@@ -164,8 +165,8 @@ export function section(md, heading) {
164
165
  */
165
166
  export function buildReport(facts) {
166
167
  const {
167
- slug, at, verdict, qa, rounds, board, t0, artifacts,
168
- evalCriteria, evalBugs, qaFindings, decisions, discovered, intakeSha,
168
+ slug, at, verdict, qa, rounds, board, t0, artifacts, ratchet,
169
+ evalCriteria, evalBugs, qaFindings, decisions, discovered, intakeSha, leftovers,
169
170
  } = facts;
170
171
 
171
172
  const L = [];
@@ -190,6 +191,16 @@ export function buildReport(facts) {
190
191
  "> The verdict above grades what was built, not what was planned.", "");
191
192
  }
192
193
 
194
+ // Leftovers — advisory, and a SECTION rather than a verdict. It was a Stop hook that printed once
195
+ // into a transcript; here it lands in the artifact a human reads at GATE L4 and a teammate finds
196
+ // on `git pull`, which is the difference between a note and a record.
197
+ if (leftovers?.length) {
198
+ L.push("## Leftovers (advisory)", "");
199
+ L.push("Markers in lines this run ADDED. Not a gate and not part of the verdict — a cleanup list.", "");
200
+ for (const line of leftovers) L.push(`- ${line}`);
201
+ L.push("");
202
+ }
203
+
193
204
  if (t0.length) {
194
205
  L.push("## Verification (T0)", "");
195
206
  L.push("The surviving trial per scope — the one describing code that is actually on the branch.", "");
@@ -202,6 +213,35 @@ export function buildReport(facts) {
202
213
  L.push("");
203
214
  }
204
215
 
216
+ // The ratchet aggregate is derived, ~10 scalars that do not grow with the run, which is why it
217
+ // can live in the committed tier while `metrics/` correctly stays gitignored (ADR-0001: a
218
+ // committed shard keyed on $HOSTNAME only grows). Without this the instrument existed and was
219
+ // never read — the trial ledger it reduces is harvested at SHIP or lost with the local tier.
220
+ if (ratchet && ratchet.trials > 0) {
221
+ L.push("## Ratchet", "");
222
+ L.push("Measured over this run's trial ledger. A monotone series is a ratchet working; a flat or",
223
+ "sawtooth series says the loop is still a budgeted retry loop wearing a ratchet's shape.", "");
224
+ L.push("| | |", "|---|---|");
225
+ L.push(`| Trials | ${ratchet.trials} across ${ratchet.scopes} scope(s), ${ratchet.scopes_multi_trial} with more than one attempt |`);
226
+ L.push(`| Improvement rate | ${ratchet.improvement_rate} — kept ÷ trials after the first |`);
227
+ L.push(`| Monotone rate | ${ratchet.monotone_rate} — multi-trial scopes whose score never decreased |`);
228
+ L.push(`| Sawtooth count | ${ratchet.sawtooth_count} — a revert immediately after a keep |`);
229
+ L.push(`| Mean trials to green | ${ratchet.mean_trials_to_green ?? "— (no scope reached green)"} |`);
230
+ const hist = Object.entries(ratchet.status_histogram).map(([k, v]) => `${k} ${v}`).join(", ");
231
+ L.push(`| Statuses | ${hist || "—"} |`);
232
+ L.push("");
233
+ // A zero improvement_rate means one of two opposite things, and the number alone cannot say
234
+ // which: the loop tried again and failed to improve, or nothing ever needed a second attempt.
235
+ // Measured on the first two real runs — every scope went green on attempt 1 — so the reading a
236
+ // reviewer meets by default is the degenerate one. `stats --ratchet` says so on the terminal;
237
+ // the committed report has to say it too, or a 0 here is read as "the ratchet did not work".
238
+ if (ratchet.scopes_multi_trial === 0) {
239
+ L.push("> No scope needed a second attempt, so the rates above are vacuous rather than bad:",
240
+ "> the ratchet was never asked to climb. The Day-1 question — does the loop measurably",
241
+ "> improve across attempts — needs a run where at least one scope retries.", "");
242
+ }
243
+ }
244
+
205
245
  if (evalCriteria) L.push("## Evaluation", "", evalCriteria, "");
206
246
  if (evalBugs) L.push("### Refuted criteria and bugs", "", evalBugs, "");
207
247
  if (qaFindings) L.push("## QA findings", "", qaFindings, "");
@@ -210,11 +250,37 @@ export function buildReport(facts) {
210
250
 
211
251
  L.push("---", "",
212
252
  "*Run state (board, orders, results, T0 artifacts, evaluation and QA reports) stays in the",
213
- "gitignored local tier — see `docs/design/adr/0001-consumer-file-organization.md`. This report",
253
+ "gitignored local tier (ADR-0001). This report",
214
254
  "is the frozen conclusion of it.*", "");
215
255
  return L.join("\n");
216
256
  }
217
257
 
258
+ /**
259
+ * How many BUILD/EVAL rounds this run actually completed.
260
+ *
261
+ * `harness-run.md`'s `rounds_used` frontmatter field is written ONCE, at GATE L0.1 (`init run`),
262
+ * as `0` — nothing in the round loop ever rewrites it as rounds complete. The orchestrator's own
263
+ * `RunReturn` carries the real count (`shapeup-run.js`'s `rounds_used: round`), but that value
264
+ * never reaches `reduce ship`, so every real run's report printed "Rounds used | 0" beside its own
265
+ * `results/evaluate-r1.json` — a claim the frontmatter makes about the run, contradicted by the
266
+ * artifact sitting next to it. Derived instead, the same way `probe resume`'s `eval_rounds_done`
267
+ * already does: the highest `evaluate-r<N>.json` result on disk. Falls back to the frontmatter
268
+ * value only when no EVAL round ever ran (the `--tiny` lane has no round concept at all), so a
269
+ * bare or tiny run's reporting is unchanged.
270
+ * @param {string} cwd - Project root.
271
+ * @param {string} slug - Feature slug.
272
+ * @param {(string|undefined)} fallback - `run.rounds_used` from the frontmatter.
273
+ * @returns {(number|string|undefined)} The derived round count, or the fallback.
274
+ */
275
+ function roundsUsed(cwd, slug, fallback) {
276
+ const dir = resultsDir(cwd, slug);
277
+ const done = (existsSync(dir) ? readdirSync(dir) : [])
278
+ .map((f) => f.match(/^evaluate-r(\d+)\.json$/))
279
+ .filter(Boolean)
280
+ .map((m) => Number(m[1]));
281
+ return done.length ? Math.max(...done) : fallback;
282
+ }
283
+
218
284
  /**
219
285
  * Gather every fact from disk and render the report.
220
286
  * @param {{cwd:string, slug:string, verdict?:string, qa?:string}} opts - Inputs.
@@ -234,16 +300,22 @@ export function generate({ cwd, slug, verdict, qa }) {
234
300
  at: today(),
235
301
  verdict: verdict || run.final_verdict || "not-evaluated",
236
302
  qa: qa || (huntReport ? "run" : "skipped"),
237
- rounds: run.rounds_used,
303
+ rounds: roundsUsed(cwd, slug, run.rounds_used),
238
304
  intakeSha: receipt.intake_sha256,
239
305
  board: boardCensus(cwd, slug),
240
306
  t0: t0Summary(cwd, slug),
307
+ ratchet: ratchetReport(readTrials(trials(cwd, slug))),
241
308
  artifacts: verdictArtifactCount(cwd, slug),
242
309
  evalCriteria: section(evalReport, /^#+\s.*criteria/i) || section(evalReport, /^#+\s*spec-conformance/i),
243
310
  evalBugs: section(evalReport, /^#+\s*Bugs?\b/i),
244
311
  qaFindings: section(huntReport, /^#+\s*Findings?\b/i),
245
312
  decisions: section(ledger, /^#+\s*Decisions?\b/i),
246
313
  discovered: section(discovery, /^#+\s*Discovered\b/i),
314
+ leftovers: (() => {
315
+ // Advisory, and derived like everything else here: the run's own diff, added lines only.
316
+ const diff = collectDiff(cwd, slug);
317
+ return diff ? summarize(scanDiff(diff)) : [];
318
+ })(),
247
319
  };
248
320
  return { markdown: buildReport(facts), path: reportPath(cwd, slug), facts };
249
321
  }
@@ -251,7 +323,7 @@ export function generate({ cwd, slug, verdict, qa }) {
251
323
  // ---------------------------------------------------------------------------
252
324
  /** The typed argv contract (see `./lib/argv.mjs`). */
253
325
  export const ARGV_SPEC = {
254
- usage: "ship-report.mjs --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated] " +
326
+ usage: "harness.mjs reduce ship --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated] " +
255
327
  "[--qa run|skipped] [--stdout]",
256
328
  _: { arity: 0, max: 0, name: "(no positional operands)" },
257
329
  slug: { type: "str", required: true },
@@ -261,8 +333,15 @@ export const ARGV_SPEC = {
261
333
  stdout: { type: "flag" },
262
334
  };
263
335
 
264
- if (isMain(import.meta.url)) {
265
- const args = runArgs(ARGV_SPEC);
336
+ /**
337
+ * Generate the ship report from the artifacts on disk.
338
+ *
339
+ * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
340
+ * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
341
+ * call `process.exit()` with the subcommand's documented code rather than returning.
342
+ */
343
+ export async function cli(rawArgv) {
344
+ const args = runArgs(ARGV_SPEC, rawArgv);
266
345
  const cwd = args.cwd || process.cwd();
267
346
  const { markdown, path } = generate({ cwd, slug: args.slug, verdict: args.verdict, qa: args.qa });
268
347
  if (args.stdout) {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Run snapshot — the compaction-resilience derivation (v1.2, absorb-audit P4).
2
+ // Run snapshot — the compaction-resilience derivation (v1.2).
3
3
  //
4
4
  // Derives a RunSnapshot (domain.schema.json#/$defs/RunSnapshot) from FILES ONLY: the
5
5
  // active-scope pointer, harness-run.md frontmatter, board task frontmatter, t0/verdicts
@@ -9,22 +9,22 @@
9
9
  // re-dispatching an already-ingested order, miscounting attempts (breaking the inner circuit
10
10
  // breaker), or "remembering" a hill phase instead of re-deriving it.
11
11
  //
12
- // Consumers (both in hooks/): compact-snapshot.mjs persists it before compaction (audit
13
- // anchor); session-rehydrate.mjs re-derives it fresh after compaction and injects the
14
- // rehydrate_hint as additionalContext.
12
+ // Consumers: `--write` persists it as an audit anchor; `--format text` prints the rehydrate_hint
13
+ // for whoever asks. Nothing fires it automatically the orchestrator re-derives after a context
14
+ // loss by running it, which answers whenever the question is asked rather than only at the two
15
+ // moments a lifecycle hook happened to see.
15
16
  //
16
17
  // Output is self-validated against the registry before it is emitted — the same
17
- // refuse-to-emit-schema-drift discipline as compile-order.mjs.
18
+ // refuse-to-emit-schema-drift discipline as `harness compile`.
18
19
  //
19
- // Usage: node run-snapshot.mjs [--cwd <dir>] [--format json|text] [--write]
20
+ // Usage: node `harness reduce snapshot` [--cwd <dir>] [--format json|text] [--write]
20
21
  // exit 0 with empty stdout when no run is active (fail-open), 1 on schema drift.
21
22
 
22
23
  import { readFileSync, readdirSync, existsSync, writeFileSync } from "node:fs";
23
24
  import { resolve, join } from "node:path";
24
- import { validate } from "./validate-envelope.mjs";
25
- import { isMain } from "./lib/is-main.mjs";
26
- import { runArgs } from "./lib/argv.mjs";
27
- import { localDir, localRoot, relLocal, globLocal, runSnapshot as runSnapshotPath } from "./lib/paths.mjs";
25
+ import { validate } from "../verify/envelope.mjs";
26
+ import { runArgs } from "../lib/argv.mjs";
27
+ import { localDir, localRoot, relLocal, globLocal, runSnapshot as runSnapshotPath } from "../lib/paths.mjs";
28
28
 
29
29
  /**
30
30
  * Read a JSON file, tolerating absence/parse errors.
@@ -84,13 +84,13 @@ function findRun(cwd) {
84
84
  // The pointer names the run the sandbox guard is scoping, NOT necessarily a run still open.
85
85
  // `.shapeup/active-scope` has existed since v0.3 and nothing has ever cleared it, because
86
86
  // its original reader (sandbox-guard) fails OPEN on a stale one — a pointer at a finished run
87
- // simply stops matching any substrate. session-rehydrate is the opposite kind of reader: it
87
+ // simply stops matching any substrate. `harness reduce graph --subgraph run` is the opposite kind of reader: it
88
88
  // turns a pointer into "a run is ALREADY OPEN in this workspace … do NOT open a new run", and
89
89
  // it now fires on `startup`/`clear`. Following the pointer without checking the run's status
90
90
  // therefore made every cold session in every repo that had EVER run the harness open with a
91
91
  // false claim about its own workspace — including sessions with nothing to do with the harness,
92
92
  // and including the case where the user's next act is to open the run the injection forbids.
93
- // This function's own contract (and session-rehydrate's header) always said "a run only for an
93
+ // This function's own contract (and `harness reduce graph --subgraph run`'s header) always said "a run only for an
94
94
  // active-scope pointer or a mid-run harness-run.md"; the status check is what makes that true.
95
95
  const pointer = readJSON(join(root, "active-scope"));
96
96
  if (pointer?.slug && isMidRun(root, pointer.slug)) {
@@ -176,11 +176,6 @@ export function deriveSnapshot(cwd) {
176
176
  if (board.total > 0) snapshot.board = board;
177
177
  }
178
178
 
179
- const escDir = join(root, "escalates");
180
- snapshot.open_escalates = existsSync(escDir)
181
- ? readdirSync(escDir).filter((f) => f.endsWith(".json")).length
182
- : 0;
183
-
184
179
  const ordersDir = join(root, "orders");
185
180
  const resultsDir = join(root, "results");
186
181
  const results = new Set(existsSync(resultsDir) ? readdirSync(resultsDir) : []);
@@ -242,16 +237,22 @@ function assertValid(snapshot) {
242
237
 
243
238
  /** The typed argv contract (see `./lib/argv.mjs`). */
244
239
  export const ARGV_SPEC = {
245
- usage: "run-snapshot.mjs [--cwd <dir>] [--format json|text] [--write]",
240
+ usage: "harness.mjs reduce snapshot [--cwd <dir>] [--format json|text] [--write]",
246
241
  _: { arity: 0, max: 0, name: "(no positional operands)" },
247
242
  cwd: { type: "path" },
248
243
  format: { type: "enum", values: ["json", "text"], default: "json" },
249
244
  write: { type: "flag" },
250
245
  };
251
246
 
252
- const isMainModule = isMain(import.meta.url);
253
- if (isMainModule) {
254
- const args = runArgs(ARGV_SPEC);
247
+ /**
248
+ * Derive the file-based run snapshot, and optionally freeze it to disk.
249
+ *
250
+ * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
251
+ * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
252
+ * call `process.exit()` with the subcommand's documented code rather than returning.
253
+ */
254
+ export async function cli(rawArgv) {
255
+ const args = runArgs(ARGV_SPEC, rawArgv);
255
256
  const cwd = resolve(args.cwd || process.cwd());
256
257
  const format = args.format;
257
258
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Verdict-ledger reference implementation (audit Stage D1) — spec-evaluator's own skill-local
2
+ // Verdict-ledger reference implementation — spec-evaluator's own skill-local
3
3
  // reference impl (ships beside SKILL.md; not invoked at runtime).
4
4
  //
5
5
  // The `spec-evaluator` skill performs verdict re-probe / confidence / flip-detection as a
@@ -12,12 +12,11 @@
12
12
  // { run, task, dimension, criterion, verdict:"PASS"|"FAIL", confidence, reprobed, flip, evidence, at }
13
13
  //
14
14
  // Library use:
15
- // import { reconcile, detectFlips, stability } from "skills/spec-evaluator/scripts/verdict-ledger.mjs";
15
+ // import { reconcile, detectFlips, stability } from "kernel/reduce/verdict.mjs";
16
16
  // const { records, summary } = reconcile(priorLines, currentRecords);
17
17
 
18
18
  // Most recent prior line for a (dimension, criterion), by highest run number.
19
- import { isMain } from "../../tech-lead/scripts/lib/is-main.mjs";
20
- import { runArgs } from "../../tech-lead/scripts/lib/argv.mjs";
19
+ import { runArgs } from "../lib/argv.mjs";
21
20
 
22
21
  /**
23
22
  * Find the most recent prior ledger line for a record's (dimension, criterion), by highest run.
@@ -139,15 +138,22 @@ export function parseLedger(text) {
139
138
  }
140
139
 
141
140
  // --- CLI entry: summarize a ledger file -------------------------------------
142
- /** The typed argv contract (see `skills/tech-lead/scripts/lib/argv.mjs`). */
141
+ /** The typed argv contract (see `kernel/lib/argv.mjs`). */
143
142
  export const ARGV_SPEC = {
144
- usage: "verdict-ledger.mjs <.verdicts-TASK.jsonl>",
143
+ usage: "harness.mjs reduce verdict <.verdicts-TASK.jsonl>",
145
144
  _: { arity: 1, max: 1, name: ".verdicts-TASK.jsonl" },
146
145
  };
147
146
 
148
- if (isMain(import.meta.url)) {
147
+ /**
148
+ * Reconcile a verdict ledger and report its flips and stability.
149
+ *
150
+ * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
151
+ * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
152
+ * call `process.exit()` with the subcommand's documented code rather than returning.
153
+ */
154
+ export async function cli(rawArgv) {
149
155
  const { readFileSync } = await import("node:fs");
150
- const path = runArgs(ARGV_SPEC)._[0];
156
+ const path = runArgs(ARGV_SPEC, rawArgv)._[0];
151
157
  let lines;
152
158
  try { lines = parseLedger(readFileSync(path, "utf8")); }
153
159
  catch (e) { console.error(`cannot read ledger ${path}: ${e.message}`); process.exit(2); }