shapeup-sdlc 1.7.0 → 3.0.1

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 (98) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +17 -3
  4. package/README.md +124 -103
  5. package/SECURITY.md +37 -30
  6. package/bin/init.mjs +77 -47
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +7 -6
  9. package/commands/ship.md +29 -16
  10. package/commands/wire.md +1 -1
  11. package/hooks/dispatch-receipt.mjs +195 -0
  12. package/hooks/gate-intake.mjs +3 -3
  13. package/hooks/gate-zerowork.mjs +25 -24
  14. package/hooks/hooks.json +9 -48
  15. package/hooks/lib/decision.mjs +19 -8
  16. package/hooks/safety-spine.mjs +3 -3
  17. package/hooks/sandbox-guard.mjs +84 -40
  18. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
  19. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
  20. package/kernel/harness.mjs +134 -0
  21. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
  22. package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
  23. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  24. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
  25. package/kernel/lib/paths.mjs +491 -0
  26. package/kernel/probe/concurrency.mjs +510 -0
  27. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +9 -10
  28. package/kernel/probe/eval.mjs +77 -0
  29. package/kernel/probe/leg.mjs +125 -0
  30. package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
  31. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +14 -9
  32. package/kernel/probe/t0.mjs +66 -0
  33. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
  34. package/kernel/reduce/graph.mjs +437 -0
  35. package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
  36. package/kernel/reduce/ingest.mjs +633 -0
  37. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  38. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +61 -13
  39. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
  40. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
  41. package/kernel/report/export.mjs +321 -0
  42. package/kernel/report/facts.mjs +209 -0
  43. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +15 -12
  44. package/kernel/verify/dispatch.mjs +114 -0
  45. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  46. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  47. package/kernel/verify/skills.mjs +125 -0
  48. package/kernel/verify/spec.mjs +559 -0
  49. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +134 -24
  50. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
  51. package/oracles/http-oracle.mjs +1 -1
  52. package/oracles/process-oracle.mjs +1 -1
  53. package/oracles/snapshot-oracle.mjs +1 -1
  54. package/oracles/test-oracle.mjs +1 -1
  55. package/package.json +11 -9
  56. package/skills/ba-pitch-analyzer/SKILL.md +5 -5
  57. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  58. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  59. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  60. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  61. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  62. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  63. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  64. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  65. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  66. package/skills/ba-pitch-analyzer/references/doc-schemas.md +2 -0
  67. package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
  68. package/skills/qa-edge-hunter/SKILL.md +7 -3
  69. package/skills/scope-architect/SKILL.md +40 -6
  70. package/skills/solution-architect/SKILL.md +19 -4
  71. package/skills/spec-evaluator/SKILL.md +12 -2
  72. package/skills/task-executor/SKILL.md +7 -6
  73. package/skills/tech-lead/SKILL.md +50 -38
  74. package/skills/tech-lead/references/gates.md +71 -20
  75. package/skills/tech-lead/references/protocol.md +832 -0
  76. package/skills/tech-lead/schemas/domain.schema.json +341 -66
  77. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  78. package/skills/tech-lead/schemas/work-result.schema.json +1 -1
  79. package/skills/tech-lead/workflows/shapeup-run.js +1327 -750
  80. package/hooks/anti-rationalization.mjs +0 -238
  81. package/hooks/compact-snapshot.mjs +0 -47
  82. package/hooks/gate-deadline.mjs +0 -147
  83. package/hooks/gate-l2.mjs +0 -161
  84. package/hooks/session-rehydrate.mjs +0 -108
  85. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  86. package/skills/spec-evaluator/README.md +0 -93
  87. package/skills/tech-lead/README.md +0 -71
  88. package/skills/tech-lead/references/delegation.md +0 -261
  89. package/skills/tech-lead/references/hard-rules.md +0 -34
  90. package/skills/tech-lead/references/invocation.md +0 -45
  91. package/skills/tech-lead/references/ledger-schema.md +0 -213
  92. package/skills/tech-lead/references/round-protocol.md +0 -234
  93. package/skills/tech-lead/references/state-model.md +0 -66
  94. package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
  95. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
  96. package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
  97. package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
  98. package/skills/translator/README.md +0 -66
@@ -1,27 +1,23 @@
1
- #!/usr/bin/env node
2
- // Slop cleaner — advisory Stop hook (v1.2).
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,21 +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";
38
- import { ratchetReport } from "./stats.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";
39
39
 
40
40
  /** @returns {string} Today as `YYYY-MM-DD` (UTC). */
41
41
  const today = () => new Date().toISOString().slice(0, 10);
@@ -67,7 +67,7 @@ export function frontmatter(text) {
67
67
  }
68
68
 
69
69
  /**
70
- * 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
71
71
  * the authoritative one.
72
72
  * @param {string} cwd - Project root.
73
73
  * @param {string} slug - Feature slug.
@@ -166,7 +166,7 @@ export function section(md, heading) {
166
166
  export function buildReport(facts) {
167
167
  const {
168
168
  slug, at, verdict, qa, rounds, board, t0, artifacts, ratchet,
169
- evalCriteria, evalBugs, qaFindings, decisions, discovered, intakeSha,
169
+ evalCriteria, evalBugs, qaFindings, decisions, discovered, intakeSha, leftovers,
170
170
  } = facts;
171
171
 
172
172
  const L = [];
@@ -191,6 +191,16 @@ export function buildReport(facts) {
191
191
  "> The verdict above grades what was built, not what was planned.", "");
192
192
  }
193
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
+
194
204
  if (t0.length) {
195
205
  L.push("## Verification (T0)", "");
196
206
  L.push("The surviving trial per scope — the one describing code that is actually on the branch.", "");
@@ -245,6 +255,32 @@ export function buildReport(facts) {
245
255
  return L.join("\n");
246
256
  }
247
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
+
248
284
  /**
249
285
  * Gather every fact from disk and render the report.
250
286
  * @param {{cwd:string, slug:string, verdict?:string, qa?:string}} opts - Inputs.
@@ -264,7 +300,7 @@ export function generate({ cwd, slug, verdict, qa }) {
264
300
  at: today(),
265
301
  verdict: verdict || run.final_verdict || "not-evaluated",
266
302
  qa: qa || (huntReport ? "run" : "skipped"),
267
- rounds: run.rounds_used,
303
+ rounds: roundsUsed(cwd, slug, run.rounds_used),
268
304
  intakeSha: receipt.intake_sha256,
269
305
  board: boardCensus(cwd, slug),
270
306
  t0: t0Summary(cwd, slug),
@@ -275,6 +311,11 @@ export function generate({ cwd, slug, verdict, qa }) {
275
311
  qaFindings: section(huntReport, /^#+\s*Findings?\b/i),
276
312
  decisions: section(ledger, /^#+\s*Decisions?\b/i),
277
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
+ })(),
278
319
  };
279
320
  return { markdown: buildReport(facts), path: reportPath(cwd, slug), facts };
280
321
  }
@@ -282,7 +323,7 @@ export function generate({ cwd, slug, verdict, qa }) {
282
323
  // ---------------------------------------------------------------------------
283
324
  /** The typed argv contract (see `./lib/argv.mjs`). */
284
325
  export const ARGV_SPEC = {
285
- 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] " +
286
327
  "[--qa run|skipped] [--stdout]",
287
328
  _: { arity: 0, max: 0, name: "(no positional operands)" },
288
329
  slug: { type: "str", required: true },
@@ -292,8 +333,15 @@ export const ARGV_SPEC = {
292
333
  stdout: { type: "flag" },
293
334
  };
294
335
 
295
- if (isMain(import.meta.url)) {
296
- 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);
297
345
  const cwd = args.cwd || process.cwd();
298
346
  const { markdown, path } = generate({ cwd, slug: args.slug, verdict: args.verdict, qa: args.qa });
299
347
  if (args.stdout) {
@@ -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)) {
@@ -237,16 +237,22 @@ function assertValid(snapshot) {
237
237
 
238
238
  /** The typed argv contract (see `./lib/argv.mjs`). */
239
239
  export const ARGV_SPEC = {
240
- usage: "run-snapshot.mjs [--cwd <dir>] [--format json|text] [--write]",
240
+ usage: "harness.mjs reduce snapshot [--cwd <dir>] [--format json|text] [--write]",
241
241
  _: { arity: 0, max: 0, name: "(no positional operands)" },
242
242
  cwd: { type: "path" },
243
243
  format: { type: "enum", values: ["json", "text"], default: "json" },
244
244
  write: { type: "flag" },
245
245
  };
246
246
 
247
- const isMainModule = isMain(import.meta.url);
248
- if (isMainModule) {
249
- 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);
250
256
  const cwd = resolve(args.cwd || process.cwd());
251
257
  const format = args.format;
252
258
 
@@ -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); }