shapeup-sdlc 3.18.2 → 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 +29 -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
package/kernel/gate.mjs CHANGED
@@ -50,11 +50,11 @@
50
50
  // 5 no answer and on_missing=abort → the run aborts here, attributably
51
51
  // 2 usage / validation error
52
52
 
53
- import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
54
- import { parseBoard } from "./reduce/board.mjs";
53
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
55
54
  import { join, dirname } from "node:path";
56
55
  import { runArgs } from "./lib/argv.mjs";
57
- import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir, tasksDir, hammerCensus, readRunId, harnessRun } from "./lib/paths.mjs";
56
+ import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir, ordersDir, qaDir, tasksDir, hammerCensus, readRunId, harnessRun, insideRunTrace, runArgsPath, dispatchReceipts } from "./lib/paths.mjs";
57
+ import { belowFloor } from "./lib/floor.mjs";
58
58
 
59
59
  export const GATE_IDS = ["L0", "L1a", "L1a.5", "L1b", "L2", "L3", "QA", "H", "L4", "COACH-1"];
60
60
 
@@ -93,7 +93,7 @@ export const PRESETS = {
93
93
  "L2": { decision: "proceed", note: CI_NOTE },
94
94
  "L3": { decision: "loop", max_rounds: 3, note: "FAIL → fix round r+1, up to the run's max_rounds. The circuit breaker, not this file, ends the loop." },
95
95
  "QA": { decision: "run", note: "QA is a level-up, not a gate — it discovers, it cannot block." },
96
- "H": { decision: "accept-cut-list", note: "Scope-hammer's cut list is accepted as proposed; baseline comparison still runs and is still recorded." },
96
+ "H": { decision: "accept-cut-list", note: "The census's cut list is accepted as proposed; baseline comparison still runs and is still recorded." },
97
97
  "L4": { decision: "ship", note: "Ship sign-off pre-approved. THIS is the one a reviewer should look at first when auditing a headless run." },
98
98
  "COACH-1": { decision: "skip", note: "No live PO to categorize feedback; retro rules are not filed from an unattended run." },
99
99
  },
@@ -109,7 +109,7 @@ export const PRESETS = {
109
109
  answers: {
110
110
  "L0": { decision: "proceed", note: "Config gate — mechanical." },
111
111
  "L1a": { decision: "proceed", note: "Orient review — advisory read." },
112
- "L1a.5": { decision: "proceed", note: "Wiring review — checked by trace-lint." },
112
+ "L1a.5": { decision: "proceed", note: "Wiring review." },
113
113
  "L1b": { decision: "ask", note: "Board review is where scope is actually decided. Not pre-approvable." },
114
114
  "L2": { decision: "proceed", note: "The board facts travel in the gate block itself — green_scopes and hammer_proposals — so a preset answering here is not answering blind. A board with zero tasks is refused here regardless of the answer — the resolver narrows `proceed` to `ask` over an empty board." },
115
115
  "L3": { decision: "loop", max_rounds: 3, note: "Loop on FAIL; the breaker ends it." },
@@ -169,17 +169,16 @@ export function validate(set) {
169
169
  * Returns { gate, decision, source, note, status } where status ∈ ok | ask | abort.
170
170
  * Pure: the CLI turns `status` into the exit code, nothing here exits.
171
171
  */
172
- /** The three verdicts `scope-hammer` may return (shapeup-run.js's HAMMER schema). */
172
+ /** The three verdicts the stop census may carry (`HammerCensus.verdict` in the domain registry). */
173
173
  export const HAMMER_VERDICTS = ["ship-now", "ship-after-fixes", "cannot-ship"];
174
174
 
175
175
  /**
176
176
  * The census verdict on disk for one run, or null when no census has run.
177
177
  *
178
- * Derived from the hammer's own WorkResult rather than taken from a caller: the verdict is the
179
- * product of a dispatch, and a gate that accepts it as an argument accepts whatever the caller
180
- * believes. `null` is a real answer here — "scope-hammer has not run" — and is the state a run that
181
- * returned `gate_h` from a circuit breaker is in, because the breaker returns long before the
182
- * hammer is dispatched.
178
+ * Derived from the census file the stop step wrote rather than taken from a caller: the verdict is the
179
+ * product of a step, and a gate that accepts it as an argument accepts whatever the caller
180
+ * believes. `null` is a real answer here — "the census has not run" — and is the state a run that
181
+ * routed to GATE H from a circuit breaker is in, until the stop step has written one.
183
182
  *
184
183
  * @param {string} cwd - Project root.
185
184
  * @param {string} slug - Feature slug.
@@ -187,9 +186,9 @@ export const HAMMER_VERDICTS = ["ship-now", "ship-after-fixes", "cannot-ship"];
187
186
  */
188
187
  export function censusVerdict(cwd, slug) {
189
188
  if (!slug) return null;
190
- // The census artifact — written by the hammer at a path its order's substrate permits. The
191
- // WorkResult fallback below is kept for a lane that wrote one the old way; a WorkResult may not
192
- // carry a verdict under the schema, so in practice the artifact is the only source.
189
+ // The census artifact — written by the stop step. The WorkResult fallback below is kept for a lane
190
+ // that wrote one the old way; a WorkResult may not carry a verdict under the schema, so in practice
191
+ // the artifact is the only source.
193
192
  try {
194
193
  const c = hammerCensus(cwd, slug);
195
194
  if (existsSync(c)) {
@@ -207,6 +206,135 @@ export function censusVerdict(cwd, slug) {
207
206
  } catch { return null; }
208
207
  }
209
208
 
209
+ /**
210
+ * The number of charters a hunt report says it ran.
211
+ *
212
+ * @param {(string|null)} text - The hunt report's text.
213
+ * @returns {(number|null)} The run count from its `charters: <run>/<approved>` line, or null.
214
+ */
215
+ export function chartersRun(text) {
216
+ const m = String(text ?? "").match(/^charters:\s*(\d+)/m);
217
+ return m ? Number(m[1]) : null;
218
+ }
219
+
220
+ const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
221
+
222
+ /**
223
+ * Why a run that owes a QA hunt cannot ship, or null when it owes none or paid it.
224
+ *
225
+ * Derived from files, never from the census (the loop writes that itself). A hunt is owed when the
226
+ * evaluator passed a round (or the round was not evaluated) and the run was not launched with
227
+ * `noQa` and no QA row of this run says `skip`. It is paid when `results/hunt.json` exists and, for
228
+ * a `done` result over an app its order could reach, the hunt report ran at least one charter. A
229
+ * `failed` hunt says it could not hunt, which is honest and accepted. A run that never reached a
230
+ * passing round owes nothing: a build that has not passed is not worth edge-hunting.
231
+ *
232
+ * The case this exists for is the one nothing else sees: QA answered `run` and the hunter never
233
+ * dispatched, so no result exists to be wrong. An explicit `skip` answer is the recorded way out.
234
+ *
235
+ * @param {string} cwd - Project root.
236
+ * @param {string} slug - Feature slug.
237
+ * @returns {(string|null)} A reason phrased for the PO, or null.
238
+ */
239
+ export function qaProblem(cwd, slug) {
240
+ if (!slug) return null;
241
+ if (readJson(runArgsPath(cwd, slug))?.noQa) return null;
242
+ const runId = readRunId(cwd, slug);
243
+ let rows = [];
244
+ try {
245
+ rows = readFileSync(gatesPath(cwd, slug), "utf8").split("\n").filter(Boolean)
246
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } })
247
+ .filter((r) => r && r.run_id === runId && r.status === "ok");
248
+ } catch { return null; }
249
+ const qa = rows.filter((r) => r.gate === "QA").at(-1);
250
+ if (qa?.decision === "skip") return null;
251
+ const owed = rows.some((r) => r.gate === "L3" && ["pass", "not-evaluated"].includes(r.verdict));
252
+ if (!qa) return owed ? "the round passed but GATE QA was never crossed" : null;
253
+ if (qa.decision !== "run") return null;
254
+ const hunt = join(resultsDir(cwd, slug), "hunt.json");
255
+ if (!existsSync(hunt)) return "GATE QA was answered run and no hunt was dispatched (no results/hunt.json)";
256
+ const result = readJson(hunt);
257
+ if (!result) return "the hunt result cannot be read";
258
+ if (result.status !== "done") return null;
259
+ const payload = readJson(join(ordersDir(cwd, slug), "hunt.json"))?.payload || {};
260
+ if (!payload.launch_cmd && !payload.app_url) return null;
261
+ let report = null;
262
+ try { report = readFileSync(join(qaDir(cwd, slug), "hunt-report.md"), "utf8"); } catch { /* no report */ }
263
+ const n = chartersRun(report);
264
+ if (n === null) return "the hunt returned done with no hunt report carrying a `charters:` line";
265
+ return n > 0 ? null : "the hunt returned done over an app its order could reach, having run 0 charters";
266
+ }
267
+
268
+ /** The launch-record role each worker runs under; `qa` is never held to the floor. */
269
+ export const WORKER_ROLE = { orient: "exec", "task-executor": "exec", "spec-evaluator": "eval", "qa-edge-hunter": "qa" };
270
+
271
+ /**
272
+ * Whether a launch record's model names the model a receipt says ran.
273
+ *
274
+ * The record carries what the launcher typed — an alias (`sonnet`) or an id — and the receipt the id the
275
+ * host resolved (`claude-sonnet-5-5`), so equality is too strict and the alias is looked for inside the
276
+ * id. Nothing to compare on either side is not a mismatch: a mismatch is a claim, and it needs both names.
277
+ *
278
+ * @param {*} asked - The launch record's model for the role.
279
+ * @param {*} ran - The receipt's `usage.model`.
280
+ * @returns {boolean} True when they name the same model, or when one side is missing.
281
+ */
282
+ export function sameModel(asked, ran) {
283
+ const a = String(asked ?? "").trim().toLowerCase();
284
+ const r = String(ran ?? "").trim().toLowerCase();
285
+ if (!a || !r) return true;
286
+ return a === r || r.includes(a) || a.includes(r);
287
+ }
288
+
289
+ /**
290
+ * The model each role ran on, read off this run's dispatch receipts, against the launch record.
291
+ *
292
+ * WHY THE GATE READS THIS. The floor is checked when the run opens, on the launch record — a request.
293
+ * The agents are defined `model: inherit`, so the model that ran is the launching session's unless the
294
+ * dispatch passed one, and the receipts are the only record of it. Across every archived run the record
295
+ * asked for `sonnet` and two runs that were compared against each other had run their workers on two
296
+ * different models, which no reader of the record could see. So the ship gate reads the receipts: an
297
+ * `exec` or `eval` worker that ran below the floor is the floor's own refusal, applied to what ran rather
298
+ * than to what was asked; any other difference is written down, because a cost or a verdict read against
299
+ * the wrong model is a figure about a different run.
300
+ *
301
+ * A receipt with no model (a background launch reports none) is counted as `unreported` and claims
302
+ * nothing. Rows of another run are ignored.
303
+ *
304
+ * @param {string} cwd - Project root.
305
+ * @param {string} slug - Feature slug.
306
+ * @returns {{asked: object, ran: object, unreported: number, mismatch: string[], below_floor: string[]}}
307
+ * `ran` maps role → model id → receipt count; `mismatch` and `below_floor` are sentences for the PO.
308
+ */
309
+ export function modelEvidence(cwd, slug) {
310
+ const asked = readJson(runArgsPath(cwd, slug))?.models ?? {};
311
+ const runId = readRunId(cwd, slug);
312
+ let rows = [];
313
+ try {
314
+ rows = readFileSync(dispatchReceipts(cwd, slug), "utf8").split("\n").filter(Boolean)
315
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } })
316
+ .filter((r) => r && (!runId || r.run_id === runId));
317
+ } catch { rows = []; }
318
+ const ran = { exec: {}, eval: {}, qa: {} };
319
+ let unreported = 0;
320
+ for (const r of rows) {
321
+ const role = WORKER_ROLE[r.skill_invoked] ?? null;
322
+ if (!role) continue;
323
+ const m = typeof r.usage?.model === "string" && r.usage.model ? r.usage.model : null;
324
+ if (!m) { unreported++; continue; }
325
+ ran[role][m] = (ran[role][m] ?? 0) + 1;
326
+ }
327
+ const n = (k) => `${k} receipt${k === 1 ? "" : "s"}`;
328
+ const mismatch = [], below = [];
329
+ for (const role of ["exec", "eval", "qa"]) {
330
+ for (const [m, k] of Object.entries(ran[role])) {
331
+ if (!sameModel(asked[role], m)) mismatch.push(`${role}: the launch record asked for ${asked[role] ?? "no model"}, ${n(k)} name${k === 1 ? "s" : ""} ${m}`);
332
+ if (role !== "qa" && belowFloor(m)) below.push(`${role} ran on ${m} in ${n(k)}, below the model floor (sonnet or above)`);
333
+ }
334
+ }
335
+ return { asked, ran, unreported, mismatch, below_floor: below };
336
+ }
337
+
210
338
  /**
211
339
  * Narrow a resolved gate answer to what the run's own evidence supports.
212
340
  *
@@ -215,10 +343,10 @@ export function censusVerdict(cwd, slug) {
215
343
  * census cleared the run, so `ship` is only on the menu when a census exists and did clear it.
216
344
  *
217
345
  * Measured on a consumer: a run whose dispatch receipts carried `orient` and `task-executor` and
218
- * nothing else — `scope-hammer` never dispatched — recorded `H → accept-cut-list` and `L4 → ship`
346
+ * nothing else — no census was ever written — recorded `H → accept-cut-list` and `L4 → ship`
219
347
  * from the `ci` preset, while its own ledger read `status: escalated`, `final_verdict: ~`, every
220
- * requirement at `no evidence`. `shapeup-run.js` does guard this, but only after the hammer
221
- * dispatch, so a run that returns `gate_h` from a breaker reaches L4 by a route the guard does not
348
+ * requirement at `no evidence`. The run script did guard this, but only after the census
349
+ * step, so a run that returns `gate_h` from a breaker reaches L4 by a route the guard does not
222
350
  * cover. A check on one call site is not a property.
223
351
  *
224
352
  * Absence of a census disqualifies `ship` on its own: "no one looked" and "someone looked and it
@@ -237,7 +365,7 @@ export function narrowToEvidence(r, cwd, slug) {
237
365
  // among allowed answers and cannot supply the evidence that makes one allowed.
238
366
  if (r?.gate === "L2" && r?.status === "ok" && r?.decision === "proceed" && slug) {
239
367
  let tasks = 0;
240
- try { tasks = parseBoard(tasksDir(cwd, slug)).length; } catch { tasks = 0; }
368
+ try { tasks = readdirSync(tasksDir(cwd, slug)).filter((f) => /^TASK-[\w.-]+\.md$/i.test(f)).length; } catch { tasks = 0; }
241
369
  if (tasks === 0) {
242
370
  return {
243
371
  gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
@@ -252,10 +380,36 @@ export function narrowToEvidence(r, cwd, slug) {
252
380
  }
253
381
  if (r?.gate !== "L4" || r?.status !== "ok" || r?.decision !== "ship") return r;
254
382
  const verdict = censusVerdict(cwd, slug);
255
- if (verdict === "ship-now" || verdict === "ship-after-fixes") return r;
383
+ if (verdict === "ship-now" || verdict === "ship-after-fixes") {
384
+ const qa = qaProblem(cwd, slug);
385
+ if (!qa) {
386
+ // The floor, applied to what ran. Opening the run checked the launch record; the receipts say
387
+ // which model each worker actually ran on, and `ship` over a build or a verdict produced below
388
+ // the floor is the same refusal one step later. A model that merely differs from the record is
389
+ // carried on the result and into the row, not refused: the PO may well have meant it.
390
+ const models = modelEvidence(cwd, slug);
391
+ if (models.below_floor.length) {
392
+ return {
393
+ gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
394
+ refused: "ship", census: verdict, models,
395
+ reason: `GATE L4 cannot be answered "ship": ${models.below_floor.join("; ")}. The floor was checked on ` +
396
+ `the launch record when the run opened; the dispatch receipts say what ran. Re-run the role on a ` +
397
+ `model at or above the floor, or put the block to the PO.`,
398
+ };
399
+ }
400
+ return { ...r, models };
401
+ }
402
+ return {
403
+ gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
404
+ refused: "ship", census: verdict, qa,
405
+ reason: `GATE L4 cannot be answered "ship": ${qa}. Dispatch the hunt, or answer GATE QA \`skip\` ` +
406
+ `(a recorded decision) and resolve L4 again. An answer chooses among the answers a gate ` +
407
+ `allows; it cannot supply the evidence that makes one allowed.`,
408
+ };
409
+ }
256
410
  const why = verdict === "cannot-ship"
257
- ? "scope-hammer's census returned CANNOT SHIP"
258
- : "scope-hammer has not run, so no census exists";
411
+ ? "the census returned CANNOT SHIP"
412
+ : "the census has not been written, so none exists";
259
413
  return {
260
414
  gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
261
415
  refused: "ship", census: verdict,
@@ -415,6 +569,11 @@ export const ARGV_SPEC = {
415
569
  // before any verdict exists — "loop" on a FAIL — and its note says so; recorded without the
416
570
  // verdict, a PASS round's row read "FAIL → fix round", the opposite of what happened.
417
571
  verdict: { type: "str" },
572
+ // A decision the PO just gave in the session that reached the gate. It resolves THIS crossing with
573
+ // the source `PO (live)` and needs no answer file: the ledger row records who decided, and the
574
+ // evidence narrowing (`ship` needs a clearing census) still applies — an answer chooses among the
575
+ // answers a gate allows, never the evidence that makes one allowed.
576
+ answer: { type: "str" },
418
577
  };
419
578
 
420
579
  /**
@@ -434,6 +593,11 @@ export function gateNote(r, verdict) {
434
593
  if (r.gate === "L3" && verdict === "pass") {
435
594
  return `verdict PASS — the "${r.decision}" answer applies only to a failed round; the run goes on to QA and GATE H`;
436
595
  }
596
+ // A ship over workers that ran on a model other than the launch record's says so in the row itself,
597
+ // where whoever reads the ledger for a cost or a verdict will see it.
598
+ if (r.gate === "L4" && r.decision === "ship" && r.models?.mismatch?.length) {
599
+ return `${own ?? ""}${own ? " — " : ""}model differs from the launch record: ${r.models.mismatch.join("; ")}`;
600
+ }
437
601
  return own;
438
602
  }
439
603
 
@@ -456,6 +620,10 @@ function die(msg, code = 2) {
456
620
  export function cli(rawArgv) {
457
621
  const args = runArgs(ARGV_SPEC, rawArgv);
458
622
  const cwd = args.cwd || process.cwd();
623
+ if (args.resolve && insideRunTrace(cwd)) {
624
+ console.error(`✋ gate: ${cwd} is inside a run trace (${LOCAL}/); run it from the project root, or its row lands in a nested ledger.`);
625
+ process.exit(2);
626
+ }
459
627
 
460
628
  if (args.init) {
461
629
  const presetName = args.preset ?? "ci";
@@ -472,7 +640,18 @@ export function cli(rawArgv) {
472
640
  out({ ok: true, wrote: dest, preset: presetName, authorized_by: set.authorized_by, gates: Object.keys(set.answers).length });
473
641
  }
474
642
 
475
- const found = discover({ cwd, file: args.file ?? null, preset: args.preset ?? null, slug: args.slug ?? null });
643
+ // A live answer builds a one-gate set instead of discovering one. `ask` is not an answer.
644
+ let found;
645
+ if (args.answer !== undefined && args.resolve) {
646
+ const valid = VALID_BY_GATE[args.resolve];
647
+ if (!valid) die(`unknown gate "${args.resolve}" — known: ${GATE_IDS.join(", ")}`);
648
+ if (args.answer === "ask" || !valid.includes(args.answer)) {
649
+ die(`"${args.answer}" is not a decision GATE ${args.resolve} takes — valid: ${valid.filter((d) => d !== "ask").join(", ")}`);
650
+ }
651
+ found = { set: { version: 1, preset: "custom", answers: { [args.resolve]: { decision: args.answer, note: "answered live by the PO" } } }, source: "PO (live)" };
652
+ } else {
653
+ found = discover({ cwd, file: args.file ?? null, preset: args.preset ?? null, slug: args.slug ?? null });
654
+ }
476
655
  if (found.error) die(found.error);
477
656
  const errs = validate(found.set);
478
657
  if (errs.length) die(`invalid gate answer set (${found.source}):\n - ${errs.join("\n - ")}`);
@@ -531,6 +710,8 @@ export function cli(rawArgv) {
531
710
  source: r.source ?? found.source, note: gateNote(r, verdict),
532
711
  round: args.round ?? null,
533
712
  ...(verdict ? { verdict } : {}),
713
+ // What ran, per role, beside what was asked — the machine-readable half of the note above.
714
+ ...(r.models ? { models: { asked: r.models.asked, ran: r.models.ran, unreported: r.models.unreported, mismatch: r.models.mismatch } } : {}),
534
715
  });
535
716
  }
536
717
  if (r.status === "ask") out({ ...r, ok: false }, 4);
@@ -2,8 +2,7 @@
2
2
  // harness — the kernel's single entry point.
3
3
  //
4
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:
5
+ // claimed, an answer file rather than a vibe — is a subcommand of this one script:
7
6
  //
8
7
  // node ${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs <verb> [<action>] [flags]
9
8
  //
@@ -13,66 +12,34 @@
13
12
  // matches nothing is indistinguishable from a rule that works until the first dispatch fails.
14
13
  // Subcommands are free; entry points are not.
15
14
  //
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.
15
+ // WHO CALLS IT. The loop that drives a run (the `building` skill, in the session), worker skills
16
+ // from their own shells, and the hooks.
19
17
  //
20
18
  // THE VERBS, and the invariant each one exists to keep:
21
19
  //
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 · build `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
- // `build` runs the feature's build and launch probes once
28
- // per round before EVAL — T0 is per scope and proves
29
- // nothing about whether the whole compiles or starts.
30
- // reduce ingest · hill · snapshot · Single writer. Shared state has exactly one author.
31
- // ship · board · verdict · graph
32
- // gate An answer file with a source, not a vibe.
33
- // probe resume · t0 · stats · digest · Read-only queries over run state. `concurrency`
34
- // concurrency · leg · eval · answers how many legs ran at once and what the
35
- // owner · requirements · attempts ·
36
- // hunt
37
- // fan-out bought, and refuses a figure the record set
38
- // cannot support rather than printing a plausible one.
39
- // `leg` answers whether a scope's work reached the
40
- // board — a green T0 says its fixtures passed, not that
41
- // its result was ever applied, and a leg that skipped
42
- // its own ingest step is indistinguishable from one that
43
- // did not until something reads the writer's own record.
44
- // `eval` answers what an EVAL round's WorkResult
45
- // actually said, mechanically — the round-loop branch
46
- // reads this instead of trusting a dispatching agent's
47
- // own end-of-turn summary of its own verdict. `owner`
48
- // answers which scope may write a path, elected from
49
- // the contracts — so a census cites it instead of
50
- // asserting ownership from memory. `requirements`
51
- // answers which pitch clause a verdict reached, joined
52
- // through the plan's own covers: edge — the L4 line and
53
- // GATE H's census cite it for the same reason.
54
- // `attempts` answers how many of a scope's attempts are
55
- // ATTESTED (a dispatch receipt AND a leg row or a
56
- // WorkResult), never the order set or the T0 verdict
57
- // set alone — the round loop's inner breaker and
58
- // scope-hammer's census both cite it, so they cannot
59
- // disagree about the same exhaustion again.
60
- // init run · fit · run-args Opens a run, or refuses it (exit 3). `run-args`
61
- // writes GATE L0.9b's launch record and echoes it.
62
- // report export Projects the run's records as fact tables.
63
- // compile The WorkOrder: schema-valid or nothing is dispatched.
20
+ // verify t0 · envelope · skills Measured, not claimed. A model verifying itself is claiming;
21
+ // these read artifacts and re-hash them. `t0` runs a scope's
22
+ // fixtures and keeps or reverts the tree; `envelope` checks an
23
+ // order or a result against its schema (and is the dispatch
24
+ // gate when called as a hook); `skills` reads the worker roster
25
+ // off disk.
26
+ // gate An answer file with a source, not a vibe.
27
+ // init run Opens a run, or refuses it (exit 3). Writes the receipt and
28
+ // the launch record, and refuses a model below the floor.
29
+ // order open · close Arms and retires the write fence's pointer, and records how a
30
+ // run ended.
64
31
  //
65
32
  // EXIT CODES are the subcommand's own and are part of its contract: 0 success · 1 ran, answer is
66
33
  // no · 2 malformed input, nothing ran (`lib/argv.mjs`) · 3 `init run` refused to open · 4/5 gate
67
- // stop/abort · 6 budget tripped. This dispatcher adds exactly one of its own: 2 for an unknown
68
- // verb, which is the same "the input was malformed" case.
34
+ // stop/abort. This dispatcher adds exactly one of its own: 2 for an unknown verb, which is the same
35
+ // "the input was malformed" case.
69
36
 
70
37
  import { isMain } from "./lib/argv.mjs";
71
38
 
72
39
  /**
73
40
  * verb → action → module, the whole routing table.
74
41
  *
75
- * A verb whose value is a string takes no action word (`gate`, `compile`); a verb whose value is an
42
+ * A verb whose value is a string takes no action word (`gate`, `order`); a verb whose value is an
76
43
  * object requires one, and `_default` names the action taken when it is omitted.
77
44
  *
78
45
  * Exported so the structural suite enumerates the subcommands from THIS table rather than from a
@@ -80,25 +47,11 @@ import { isMain } from "./lib/argv.mjs";
80
47
  */
81
48
  export const ROUTES = {
82
49
  verify: {
83
- t0: "./verify/t0.mjs", budget: "./verify/budget.mjs", envelope: "./verify/envelope.mjs",
84
- trace: "./verify/trace.mjs", spec: "./verify/spec.mjs", skills: "./verify/skills.mjs",
85
- dispatch: "./verify/dispatch.mjs", build: "./verify/build.mjs",
50
+ t0: "./verify/t0.mjs", envelope: "./verify/envelope.mjs", skills: "./verify/skills.mjs",
86
51
  },
87
- reduce: {
88
- ingest: "./reduce/ingest.mjs", hill: "./reduce/hill.mjs", snapshot: "./reduce/snapshot.mjs",
89
- ship: "./reduce/ship.mjs", board: "./reduce/board.mjs", verdict: "./reduce/verdict.mjs",
90
- graph: "./reduce/graph.mjs",
91
- },
92
- probe: {
93
- resume: "./probe/resume.mjs", t0: "./probe/t0.mjs", stats: "./probe/stats.mjs",
94
- digest: "./probe/digest.mjs", concurrency: "./probe/concurrency.mjs",
95
- leg: "./probe/leg.mjs", eval: "./probe/eval.mjs", owner: "./probe/owner.mjs",
96
- requirements: "./probe/requirements.mjs", attempts: "./probe/attempts.mjs", hunt: "./probe/hunt.mjs",
97
- },
98
- init: { run: "./init/run.mjs", fit: "./init/fit.mjs", "run-args": "./init/run-args.mjs" },
99
- report: { export: "./report/export.mjs", _default: "export" },
100
52
  gate: "./gate.mjs",
101
- compile: "./compile.mjs",
53
+ init: { run: "./init/run.mjs" },
54
+ order: "./order.mjs",
102
55
  };
103
56
 
104
57
  /** Every `<verb> <action>` pair, for the usage text and the unknown-verb rejection. */