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,21 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  // GATE ANSWER SET — cross a gate with a pre-recorded decision instead of a live human.
3
3
  //
4
- // WHY THIS EXISTS (measured, not theorized).
4
+ // WHY THIS EXISTS (observed, not theorized).
5
5
  //
6
6
  // This harness pauses at every ⏸ gate for PO sign-off by default. That is the point of it. But
7
- // on the SDD harness benchmark it produced two distinct failures, both of which look like the
8
- // harness being slow or broken rather than the harness being safe:
7
+ // unattended it produces two distinct failures, both of which look like the harness being slow or
8
+ // broken rather than the harness being safe:
9
9
  //
10
- // 1. STALL → TIMEOUT. F3 (Sonnet 5): the run was killed at the declared 1800s cap having
11
- // produced nothing scoreable, while the no-harness control finished the same feature in
12
- // 51 seconds. A run with no human at the keyboard sitting at a gate does not fail — it
13
- // waits, and a wait is indistinguishable from work until the budget runs out.
10
+ // 1. STALL → TIMEOUT. The run is killed at its time cap having produced nothing scoreable,
11
+ // on a feature that takes minutes to build by hand. A run with no human at the keyboard
12
+ // sitting at a gate does not fail it waits, and a wait is indistinguishable from work
13
+ // until the budget runs out.
14
14
  // 2. CONSENT-BY-PROSE. The workaround was a paragraph of English in the prompt ("treat this
15
- // message as advance sign-off for every gate"). On Sonnet that worked. On Haiku 4.5 the
16
- // model read the paragraph, read the 450-line gate list, and narrated the pipeline instead
17
- // of running it 29% acceptance, n=5, zero variance. Consent carried in prose is consent
18
- // that can be re-summarized instead of acted on.
15
+ // message as advance sign-off for every gate"). Some models act on it. Others read the
16
+ // paragraph, read the gate list, and narrate the pipeline instead of running it. Consent
17
+ // carried in prose is consent that can be re-summarized instead of acted on.
19
18
  //
20
19
  // The organising rule of this project is that every invariant that matters lives in the runtime,
21
20
  // not in a prompt. Gate sign-off was the last big one still living in a prompt. So it becomes a
@@ -33,10 +32,10 @@
33
32
  // pre-recorded answer") rather than a silent wait that gets reported as a slow harness.
34
33
  //
35
34
  // USAGE
36
- // node gate-answers.mjs --init [--preset ci|guarded|interactive] [--out <path>] [--by "<name>"]
37
- // node gate-answers.mjs --resolve <gate-id> [--file <path>|--preset <name>] [--slug <slug>]
38
- // node gate-answers.mjs --list [--file <path>|--preset <name>]
39
- // node gate-answers.mjs --verify [--file <path>|--preset <name>] [--auto-level unattended]
35
+ // node `harness gate` --init [--preset ci|guarded|interactive] [--out <path>] [--by "<name>"]
36
+ // node `harness gate` --resolve <gate-id> [--file <path>|--preset <name>] [--slug <slug>]
37
+ // node `harness gate` --list [--file <path>|--preset <name>]
38
+ // node `harness gate` --verify [--file <path>|--preset <name>] [--auto-level unattended]
40
39
  //
41
40
  // RESOLUTION ORDER for --resolve / --list / --verify, first hit wins:
42
41
  // 1. --file <path>
@@ -51,11 +50,10 @@
51
50
  // 5 no answer and on_missing=abort → the run aborts here, attributably
52
51
  // 2 usage / validation error
53
52
 
54
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
53
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
55
54
  import { join, dirname } from "node:path";
56
- import { isMain } from "./lib/is-main.mjs";
57
55
  import { runArgs } from "./lib/argv.mjs";
58
- import { gateAnswerCandidates, LOCAL } from "./lib/paths.mjs";
56
+ import { gateAnswerCandidates, gates as gatesPath, LOCAL } from "./lib/paths.mjs";
59
57
 
60
58
  export const GATE_IDS = ["L0", "L1a", "L1a.5", "L1b", "L2", "L3", "QA", "H", "L4", "COACH-1"];
61
59
 
@@ -79,7 +77,7 @@ export const VALID_BY_GATE = {
79
77
  const CI_NOTE = "Pre-approved for a headless lane. No human is present; the decision is recorded here so the ledger still names a source.";
80
78
 
81
79
  export const PRESETS = {
82
- // Everything pre-approved. The lane a CI step or a benchmark runs in.
80
+ // Everything pre-approved. The lane a CI step runs in.
83
81
  ci: {
84
82
  version: 1,
85
83
  preset: "ci",
@@ -205,6 +203,33 @@ export function resolve(set, gate, source) {
205
203
  };
206
204
  }
207
205
 
206
+ /**
207
+ * Append one row to the per-slug gate-crossing ledger — `.shapeup/<slug>/gates.jsonl` ({@link
208
+ * gatesPath}, from `lib/paths.mjs`).
209
+ *
210
+ * WRITER RULE, same shape as {@link appendTrial} in `verify/t0.mjs` and the dispatch-receipt hook's
211
+ * ownership of `decisions.jsonl`: one small file, one writer, append-only JSONL so concurrent
212
+ * scopes crossing gates in the same round cannot clobber each other's row.
213
+ *
214
+ * Records every resolution that reached a real status — a crossing (`ok`), a recorded pause
215
+ * (`ask`), or an abort (`abort`) — because "the run stopped here" is exactly the kind of fact this
216
+ * ledger exists to hold. Best-effort: a ledger write that could fail a gate crossing would cost more
217
+ * than the row is worth, matching {@link appendTrial}'s own trade-off.
218
+ *
219
+ * @param {string} cwd - Project root.
220
+ * @param {string} slug - Feature slug.
221
+ * @param {object} row - The ledger row (gate, decision, source, note, round, status).
222
+ * @returns {boolean} True when the row was written.
223
+ */
224
+ export function appendGateLedger(cwd, slug, row) {
225
+ try {
226
+ const p = gatesPath(cwd, slug);
227
+ mkdirSync(dirname(p), { recursive: true });
228
+ appendFileSync(p, JSON.stringify(row) + "\n");
229
+ return true;
230
+ } catch { return false; }
231
+ }
232
+
208
233
  /** Gates this lane will actually hit — used by --verify to catch a set that stalls halfway. */
209
234
  export function requiredGates({ autoLevel = "unattended", tiny = false, qa = true } = {}) {
210
235
  if (tiny) return ["L0", "L4"];
@@ -240,9 +265,9 @@ export function discover({ cwd = process.cwd(), file = null, preset = null, slug
240
265
 
241
266
  /** The typed argv contract (see `./lib/argv.mjs`). */
242
267
  export const ARGV_SPEC = {
243
- usage: "gate-answers.mjs (--init | --list | --verify | --resolve <gate-id>) [--preset <name>] " +
268
+ usage: "harness.mjs gate (--init | --list | --verify | --resolve <gate-id>) [--preset <name>] " +
244
269
  "[--file <path>] [--slug <slug>] [--cwd <dir>] [--out <path>] [--by <who>] " +
245
- "[--auto-level <level>] [--tiny] [--no-qa]",
270
+ "[--auto-level <level>] [--tiny] [--no-qa] [--round <n>]",
246
271
  _: { arity: 0, max: 0, name: "(no positional operands)" },
247
272
  cwd: { type: "path" },
248
273
  init: { type: "flag" },
@@ -257,6 +282,10 @@ export const ARGV_SPEC = {
257
282
  "auto-level": { type: "str" },
258
283
  tiny: { type: "flag" },
259
284
  "no-qa": { type: "flag" },
285
+ // The current BUILD round, when the caller has one — L2/L3 are crossed once per round, so the
286
+ // ledger row needs it to key a `GateDecision` node uniquely per crossing. Round-independent gates
287
+ // (L0, L1a, …) simply omit it and the row carries `round: null`.
288
+ round: { type: "int", min: 1 },
260
289
  };
261
290
 
262
291
  function out(obj, code = 0) {
@@ -268,8 +297,15 @@ function die(msg, code = 2) {
268
297
  process.exit(code);
269
298
  }
270
299
 
271
- export function main() {
272
- const args = runArgs(ARGV_SPEC);
300
+ /**
301
+ * Resolve, verify, list or initialise the gate answer set.
302
+ *
303
+ * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
304
+ * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
305
+ * call `process.exit()` with the subcommand's documented code rather than returning.
306
+ */
307
+ export function cli(rawArgv) {
308
+ const args = runArgs(ARGV_SPEC, rawArgv);
273
309
  const cwd = args.cwd || process.cwd();
274
310
 
275
311
  if (args.init) {
@@ -328,11 +364,18 @@ export function main() {
328
364
 
329
365
  const r = resolve(found.set, gate, found.source);
330
366
  if (r.status === "error") die(r.reason);
367
+ // A gate with no `--slug` (e.g. `--file` used ad hoc, outside any run) has nowhere to file a
368
+ // per-run ledger row — the same reasoning `resolveRunId` uses for "no run is active": absence is
369
+ // the correct answer, not an error, so the write is skipped rather than guessing a location.
370
+ if (args.slug) {
371
+ appendGateLedger(cwd, args.slug, {
372
+ gate: r.gate, status: r.status, decision: r.decision ?? null,
373
+ source: r.source ?? found.source, note: r.note ?? r.reason ?? null,
374
+ round: args.round ?? null,
375
+ });
376
+ }
331
377
  if (r.status === "ask") out({ ...r, ok: false }, 4);
332
378
  if (r.status === "abort") out({ ...r, ok: false }, 5);
333
379
  out({ ...r, ok: true }, 0);
334
380
  }
335
381
 
336
- if (isMain(import.meta.url)) {
337
- main();
338
- }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ // harness — the kernel's single entry point.
3
+ //
4
+ // WHAT THIS IS. Everything in the pipeline that must be DETERMINISTIC — measured rather than
5
+ // claimed, single-writer rather than negotiated, an answer file rather than a vibe — is a
6
+ // subcommand of this one script:
7
+ //
8
+ // node ${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs <verb> [<action>] [flags]
9
+ //
10
+ // WHY ONE ENTRY POINT. A permission rule is matched against a command string. With one executable
11
+ // the entire grant is two lines a person can read (`bin/lib/grant.mjs`); with twenty it is forty
12
+ // rules that must be regenerated on every add, rename or removal — and a rule that silently
13
+ // matches nothing is indistinguishable from a rule that works until the first dispatch fails.
14
+ // Subcommands are free; entry points are not.
15
+ //
16
+ // WHO CALLS IT. Worker skills, from their own shells, and the hooks. The control plane — the
17
+ // Workflow script — calls no shell at all: it branches on the structured return of an `agent()`
18
+ // and never parses stdout.
19
+ //
20
+ // THE VERBS, and the invariant each one exists to keep:
21
+ //
22
+ // verify t0 · budget · envelope · Measured, not claimed. A model verifying itself is
23
+ // trace · spec · skills · claiming; these read artifacts and re-hash them.
24
+ // dispatch `skills` reads the roster off disk; `dispatch` reads
25
+ // the hook layer's evidence that a skill really resolved
26
+ // in this session — the half a file check cannot answer.
27
+ // reduce ingest · hill · snapshot · Single writer. Shared state has exactly one author.
28
+ // ship · board · verdict · graph
29
+ // gate An answer file with a source, not a vibe.
30
+ // probe resume · t0 · stats · digest · Read-only queries over run state. `concurrency`
31
+ // concurrency · leg · eval answers how many legs ran at once and what the
32
+ // fan-out bought, and refuses a figure the record set
33
+ // cannot support rather than printing a plausible one.
34
+ // `leg` answers whether a scope's work reached the
35
+ // board — a green T0 says its fixtures passed, not that
36
+ // its result was ever applied, and a leg that skipped
37
+ // its own ingest step is indistinguishable from one that
38
+ // did not until something reads the writer's own record.
39
+ // `eval` answers what an EVAL round's WorkResult
40
+ // actually said, mechanically — the round-loop branch
41
+ // reads this instead of trusting a dispatching agent's
42
+ // own end-of-turn summary of its own verdict.
43
+ // init run · fit Opens a run, or refuses it (exit 3).
44
+ // report export Projects the run's records as fact tables.
45
+ // compile The WorkOrder: schema-valid or nothing is dispatched.
46
+ //
47
+ // EXIT CODES are the subcommand's own and are part of its contract: 0 success · 1 ran, answer is
48
+ // no · 2 malformed input, nothing ran (`lib/argv.mjs`) · 3 `init run` refused to open · 4/5 gate
49
+ // stop/abort · 6 budget tripped. This dispatcher adds exactly one of its own: 2 for an unknown
50
+ // verb, which is the same "the input was malformed" case.
51
+
52
+ import { isMain } from "./lib/argv.mjs";
53
+
54
+ /**
55
+ * verb → action → module, the whole routing table.
56
+ *
57
+ * A verb whose value is a string takes no action word (`gate`, `compile`); a verb whose value is an
58
+ * object requires one, and `_default` names the action taken when it is omitted.
59
+ *
60
+ * Exported so the structural suite enumerates the subcommands from THIS table rather than from a
61
+ * hand-kept list beside it — a second copy of the routing is a second thing to forget.
62
+ */
63
+ export const ROUTES = {
64
+ verify: {
65
+ t0: "./verify/t0.mjs", budget: "./verify/budget.mjs", envelope: "./verify/envelope.mjs",
66
+ trace: "./verify/trace.mjs", spec: "./verify/spec.mjs", skills: "./verify/skills.mjs",
67
+ dispatch: "./verify/dispatch.mjs",
68
+ },
69
+ reduce: {
70
+ ingest: "./reduce/ingest.mjs", hill: "./reduce/hill.mjs", snapshot: "./reduce/snapshot.mjs",
71
+ ship: "./reduce/ship.mjs", board: "./reduce/board.mjs", verdict: "./reduce/verdict.mjs",
72
+ graph: "./reduce/graph.mjs",
73
+ },
74
+ probe: {
75
+ resume: "./probe/resume.mjs", t0: "./probe/t0.mjs", stats: "./probe/stats.mjs",
76
+ digest: "./probe/digest.mjs", concurrency: "./probe/concurrency.mjs",
77
+ leg: "./probe/leg.mjs", eval: "./probe/eval.mjs",
78
+ },
79
+ init: { run: "./init/run.mjs", fit: "./init/fit.mjs" },
80
+ report: { export: "./report/export.mjs", _default: "export" },
81
+ gate: "./gate.mjs",
82
+ compile: "./compile.mjs",
83
+ };
84
+
85
+ /** Every `<verb> <action>` pair, for the usage text and the unknown-verb rejection. */
86
+ function usage() {
87
+ const lines = [];
88
+ for (const [verb, target] of Object.entries(ROUTES)) {
89
+ if (typeof target === "string") lines.push(` ${verb}`);
90
+ else lines.push(` ${verb} ${Object.keys(target).filter((a) => a !== "_default").join(" | ")}`);
91
+ }
92
+ return `usage: harness.mjs <verb> [<action>] [flags]\n${lines.join("\n")}\n\nEvery subcommand accepts --help through its own argv spec.`;
93
+ }
94
+
95
+ /**
96
+ * Reject an invocation before anything runs, the same way `lib/argv.mjs` rejects a bad flag.
97
+ * @param {object} detail - Machine-readable rejection record.
98
+ * @returns {never} Exits 2.
99
+ */
100
+ function reject(detail) {
101
+ process.stderr.write(JSON.stringify(detail) + "\n");
102
+ process.stderr.write(usage() + "\n");
103
+ process.exit(2);
104
+ }
105
+
106
+ /**
107
+ * Resolve `<verb> [<action>]` to a module path and the argv the subcommand should see.
108
+ *
109
+ * @param {string[]} argv - `process.argv.slice(2)`.
110
+ * @returns {{module: string, rest: string[]}} The module to import and its own arguments.
111
+ */
112
+ export function route(argv) {
113
+ const [verb, ...rest] = argv;
114
+ if (!verb || verb === "--help" || verb === "-h") { process.stdout.write(usage() + "\n"); process.exit(0); }
115
+ const target = ROUTES[verb];
116
+ if (!target) reject({ error: "unknown_verb", verb, expected: Object.keys(ROUTES).join(" | ") });
117
+ if (typeof target === "string") return { module: target, rest };
118
+
119
+ const action = rest[0] && !rest[0].startsWith("-") ? rest[0] : target._default;
120
+ const module = action ? target[action] : null;
121
+ if (!module) {
122
+ reject({
123
+ error: action ? "unknown_action" : "missing_action", verb, action: action ?? null,
124
+ expected: Object.keys(target).filter((a) => a !== "_default").join(" | "),
125
+ });
126
+ }
127
+ return { module, rest: rest[0] === action ? rest.slice(1) : rest };
128
+ }
129
+
130
+ if (isMain(import.meta.url)) {
131
+ const { module, rest } = route(process.argv.slice(2));
132
+ const mod = await import(module);
133
+ await mod.cli(rest);
134
+ }
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  // GATE L0.3 — FIT CHECK. Which lane does this change belong in? Decided by measurement.
3
3
  //
4
- // WHY THIS EXISTS (measured, and it is the root cause the other three fixes did not touch).
4
+ // WHY THIS EXISTS (and it is the root cause the other three fixes did not touch).
5
5
  //
6
- // On the SDD harness benchmark, F3 ("add a `summary` command" to a six-file CLI — one new module
7
- // plus one dispatcher wiring) was run through the full eleven-gate pipeline and never finished.
8
- // Not once, across four attempts. It was killed at the 1800s cap mid-build; then, with a
9
- // wall-clock breaker fitted, it reached ship-triage and was killed there; then it produced an
10
- // honest report saying its two must-haves were 0% started.
6
+ // A change of the size "add a `summary` command to a six-file CLI" — one new module plus one
7
+ // dispatcher wiring run through the full eleven-gate pipeline does not finish. Not once, across
8
+ // repeated attempts: killed at the time cap mid-build; then, with a wall-clock breaker fitted,
9
+ // killed at ship-triage; then producing an honest report saying its two must-haves were 0%
10
+ // started.
11
11
  //
12
12
  // Every one of those fixes made the FAILURE better. None of them made the RUN finish, because
13
13
  // none addressed why a three-file change was consuming half an hour: **the ceremony was not
14
14
  // sized to the change.**
15
15
  //
16
- // The harness already knew. In the pilot transcripts, tech-lead identified F1 at GATE L0 as
16
+ // The harness already knew. In those transcripts tech-lead identified the change at GATE L0 as
17
17
  // "about as small as they come… squarely inside the --tiny lane" — and then ran the full pipeline
18
18
  // anyway, because the lane was a judgment the model was free to talk itself out of. That is the
19
19
  // same class of defect as narration and as prose consent: an invariant living somewhere a model
@@ -34,17 +34,16 @@
34
34
  // symmetric, and the tie goes to the gates.
35
35
  //
36
36
  // USAGE
37
- // node fit-check.mjs --intake-file <path> [--cwd <root>] [--json]
38
- // node fit-check.mjs --intake-text "<requirement>" [--cwd <root>]
37
+ // node `harness init fit` --intake-file <path> [--cwd <root>] [--json]
38
+ // node `harness init fit` --intake-text "<requirement>" [--cwd <root>]
39
39
  //
40
40
  // Exit 0 always — this informs a decision, it does not deny a tool call. The orchestrator reads
41
- // `lane` and acts on it; `init-run.mjs` records it in the receipt.
41
+ // `lane` and acts on it; ``harness init run`` records it in the receipt.
42
42
 
43
43
  import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
44
44
  import { join } from "node:path";
45
- import { isMain } from "./lib/is-main.mjs";
46
- import { LOCAL } from "./lib/paths.mjs";
47
- import { runArgs } from "./lib/argv.mjs";
45
+ import { LOCAL } from "../lib/paths.mjs";
46
+ import { runArgs } from "../lib/argv.mjs";
48
47
 
49
48
  /** Directories that are never part of "the tree being changed". */
50
49
  const IGNORE_DIRS = new Set([".git", "node_modules", LOCAL, "dist", "build", ".next", "coverage", ".claude"]);
@@ -121,7 +120,7 @@ export function decideLane({ intake, files }) {
121
120
  };
122
121
 
123
122
  // CONJUNCTIVE, and it must be. The first version asked "is there evidence this is BIG?" and
124
- // defaulted to tiny when it found none — which classified all three benchmark features as tiny,
123
+ // defaulted to tiny when it found none — which classified every trial feature as tiny,
125
124
  // including the five-seam one that genuinely needs the pipeline and used two evaluation rounds
126
125
  // to pass. A router that confident and that wrong is worse than no router: it would skip review
127
126
  // on exactly the change that needed it.
@@ -152,7 +151,7 @@ export function decideLane({ intake, files }) {
152
151
  // Say it on every invocation rather than burying it in a doc. These thresholds are calibrated
153
152
  // against THREE features. That is enough to stop the router being obviously wrong and nowhere
154
153
  // near enough to trust it silently, so it recommends and never denies.
155
- calibration: "advisory — thresholds fitted on 3 benchmark features (n=3); `full` is the default and the PO may override with --lane",
154
+ calibration: "advisory — thresholds are coarse and `full` is the default; the PO may override with --lane",
156
155
  };
157
156
  }
158
157
 
@@ -160,15 +159,22 @@ export function decideLane({ intake, files }) {
160
159
 
161
160
  /** The typed argv contract (see `./lib/argv.mjs`). */
162
161
  export const ARGV_SPEC = {
163
- usage: 'fit-check.mjs (--intake-file <path> | --intake-text "<requirement>") [--cwd <dir>]',
162
+ usage: 'harness.mjs init fit (--intake-file <path> | --intake-text "<requirement>") [--cwd <dir>]',
164
163
  _: { arity: 0, max: 0, name: "(no positional operands)" },
165
164
  cwd: { type: "path" },
166
165
  "intake-text": { type: "str" },
167
166
  "intake-file": { type: "path" },
168
167
  };
169
168
 
170
- export function main() {
171
- const args = runArgs(ARGV_SPEC);
169
+ /**
170
+ * Decide the lane (full or tiny) for an intake and the tree it will build in.
171
+ *
172
+ * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
173
+ * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
174
+ * call `process.exit()` with the subcommand's documented code rather than returning.
175
+ */
176
+ export function cli(rawArgv) {
177
+ const args = runArgs(ARGV_SPEC, rawArgv);
172
178
  const cwd = args.cwd || process.cwd();
173
179
  let intake = args.intakeText ?? null;
174
180
  const f = args.intakeFile ?? null;
@@ -186,11 +192,8 @@ export function main() {
186
192
  ...result,
187
193
  // The sentence the orchestrator must act on, so the decision is not re-derived from the JSON.
188
194
  action: result.lane === "tiny"
189
- ? "TINY LANE. Run: orient (light) → single-task board → build → T0 → ⏸ L4. Skip WIRE, scope contracts, spec tree, EVAL and QA. The full pipeline on a change this size is the measured cause of a benchmark run that never finished, four attempts running."
195
+ ? "TINY LANE. Run: orient (light) → single-task board → build → T0 → ⏸ L4. Skip WIRE, scope contracts, spec tree, EVAL and QA. The full pipeline on a change this size is why a run burns its whole clock without finishing."
190
196
  : "FULL LANE. Run the complete pipeline: ORIENT → WIRE → MAP SCOPES → BUILD → EVAL → QA → GATE H → L4.",
191
197
  }, null, 2));
192
198
  }
193
199
 
194
- if (isMain(import.meta.url)) {
195
- main();
196
- }