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
@@ -1,2187 +0,0 @@
1
- // shapeup-run — the BUILD-phase pipeline as one script on the native Dynamic Workflow runtime.
2
- //
3
- // WHAT THIS FILE OWNS.
4
- // ORIENT → GATE L1a → ANALYZE → WIRE → GATE L1a.5 → MAP SCOPES → GATE L1b →
5
- // rounds of (BUILD → build gate → GATE L2 → EVAL → GATE L3) bounded by budgets.maxRounds →
6
- // QA → GATE H → ship report → RunReturn.
7
- //
8
- // THE THREE PLANES THIS FILE RESPECTS, because collapsing them is what the previous version cost:
9
- // CONTROL (here) — sequences phases and branches on structured returns. Owns NO I/O: no shell,
10
- // no filesystem, no stdout parsing. Nothing in this file reads a model's prose.
11
- // EXECUTION — the sub-agents. Each has a real shell and a real filesystem, runs the kernel
12
- // itself, and returns a SCHEMA-VALIDATED object. The runtime enforces the
13
- // shape, so every branch below reads a real typed field.
14
- // ARTIFACT — the kernel. `harness compile` writes the order and `harness reduce ingest`
15
- // applies the result; those remain the only writers of shared state.
16
- //
17
- // BOTH LANES ARE THIS CODE. The unattended lane launches it headlessly; the interactive lane pauses
18
- // at each gate a human answers and relaunches. There is no prose runbook that can drift from what
19
- // executes.
20
- //
21
- // EVERY PHASE HAS A POST-CONDITION. A phase is complete when its ARTIFACT is on disk, never when a
22
- // result record says so — `requirePhase()` below. A WorkResult may legitimately report `escalated`
23
- // with an empty artifacts list, which satisfies ingest; without the post-condition the run walks to
24
- // the next gate as though the phase landed, and every relaunch re-dispatches it.
25
- //
26
- // DELIBERATE OMISSIONS, stated rather than hidden:
27
- // - No mid-round ESCALATE resolution and no discovered-task reconciliation mid-BUILD. Both stay
28
- // on the prose path in references/round-protocol.md and references/delegation.md.
29
- // - QA runs once after the first PASS; its findings are a count for GATE H's census, never
30
- // re-probed inside this run. Promoting a finding to a fix round is a live PO's decision.
31
- // - The `tiny` lane and pre-scope-contract specs are out of scope here; SKILL.md's tiny-lane
32
- // reference keeps that path.
33
- //
34
- // args — RunArgs (domain.schema.json $defs/RunArgs):
35
- // slug, autoLevel (interactive|auto|unattended), answers (preset name or path),
36
- // models {exec, eval, qa?}, budgets {maxRounds, attemptBudget}, pluginRoot,
37
- // startedAt, and the optional switches noEval / noQa / adversarialVerify /
38
- // maxParallelScopes (default 4).
39
- //
40
- // return — RunReturn (domain.schema.json $defs/RunReturn), the full union:
41
- // { status: "shipped", verdict, rounds_used, dims_not_evaluated, qa_findings, qa, report }
42
- // { status: "paused", paused_at, block, valid_decisions, context }
43
- // { status: "aborted", aborted_at, reason }
44
- // { status: "gate_h", breaker: "outer"|"attempt_budget"|"none"|"deadline", hammer_proposals, green_scopes,
45
- // tripped_scopes?, unapplied_results?, census?, cut_list?, l4? }
46
- // — returned only when the census said cannot-ship, L4 said hold, or the ship report was
47
- // refused; a breaker whose census clears the run ends as `shipped` with `after: "gate_h"`.
48
-
49
- // meta must be a PURE LITERAL — the runtime parses it statically, before the body ever runs, and
50
- // rejects the whole script on anything it has to evaluate. A `+`-joined description is a
51
- // BinaryExpression, so the concatenation that reads better in source costs the file its ability to
52
- // load at all. Keep every value here a plain literal, however long the line gets.
53
- export const meta = {
54
- name: "shapeup-run",
55
- description: "BUILD-phase pipeline: ORIENT → ANALYZE → WIRE → MAP SCOPES → rounds of BUILD/EVAL → QA → GATE H → ship. Gates resolve by the kernel's exit code; every dispatch is WorkOrder in / WorkResult out; the fast-forward is derived from artifacts on disk.",
56
- phases: [
57
- { title: "Preflight", detail: "one canary dispatch — can this session resolve a worker skill; then the project's build and launch probes, run once and recorded nowhere" },
58
- { title: "Orient" }, { title: "Analyze" }, { title: "Wire" }, { title: "MapScopes" },
59
- { title: "Build" }, { title: "Eval" }, { title: "Refute" }, { title: "QA" }, { title: "Ship" },
60
- ],
61
- };
62
-
63
- // Some callers hand args as a JSON string.
64
- if (typeof args === "string") { try { args = JSON.parse(args); } catch { args = {}; } }
65
-
66
- // ---------------------------------------------------------------------------------------------
67
- // ARGS — validated here because nothing validates the C1 boundary for us. A run that starts with a
68
- // below-floor model or a missing budget produces work nobody can trust, and finding that out at
69
- // GATE L3 costs the whole run.
70
- // ---------------------------------------------------------------------------------------------
71
- // ---- MODEL FLOOR REGION START ------------------------------------------------------------------
72
- // This used to be an ALLOWLIST — exact spelling against {"sonnet","opus"} — which is backwards
73
- // from fail-open: every real model id this repo actually ships with (`claude-opus-5`,
74
- // `claude-sonnet-4-5`, `opusplan`) failed the exact-spelling test and aborted the run before
75
- // Preflight. Fixed as a DENYLIST of tiers this repo never certifies eval or build with: anything
76
- // NOT provably below the floor passes, matching this repo's own fail-open rule (an unrecognized
77
- // string is evidence this repo hasn't seen it, not evidence it's too cheap). Only an empty/absent
78
- // model string is rejected outright — that is a missing model, not an unrecognized one.
79
- //
80
- // The cheap/fast Claude tier is matched by PATTERN, never spelled out as a literal string here —
81
- // a literal name in this file would trip the structural suite's own floor check, which scans every
82
- // workflow script for exactly that spelling and is deliberately phrased the same indirect way.
83
- const BELOW_FLOOR_PATTERNS = [/ha[i1]ku/i, /\binstant\b/i, /\bmini\b/i, /\bnano\b/i, /\blite\b/i, /\bsmall\b/i];
84
- const belowFloor = (m) => {
85
- const s = String(m || "").trim();
86
- if (!s) return true; // no model named at all — missing, not merely unrecognized
87
- return BELOW_FLOOR_PATTERNS.some((re) => re.test(s));
88
- };
89
- // ---- MODEL FLOOR REGION END --------------------------------------------------------------------
90
-
91
- function validateArgs(a) {
92
- const problems = [];
93
- for (const k of ["slug", "autoLevel", "models", "budgets", "pluginRoot"]) {
94
- if (a?.[k] === undefined || a[k] === null || a[k] === "") problems.push(`missing args.${k}`);
95
- }
96
- if (a && !["interactive", "auto", "unattended"].includes(a.autoLevel)) {
97
- problems.push(`args.autoLevel="${a.autoLevel}" must be interactive|auto|unattended`);
98
- }
99
- if (a?.budgets && (!a.budgets.maxRounds || !a.budgets.attemptBudget)) {
100
- problems.push("args.budgets must carry maxRounds and attemptBudget");
101
- }
102
- if (a?.models) {
103
- for (const role of ["exec", "eval"]) {
104
- if (role === "eval" && a.noEval) continue;
105
- if (belowFloor(a.models[role])) problems.push(`args.models.${role} is below the model floor (sonnet or above)`);
106
- }
107
- }
108
- return problems;
109
- }
110
-
111
- const argProblems = validateArgs(args);
112
- if (argProblems.length) return { status: "aborted", aborted_at: "args", reason: argProblems.join("; ") };
113
-
114
- const slug = args.slug;
115
- // The report's path, never the ship command's one-line `detail`: that is a sub-agent's sentence, and
116
- // it reached the RunReturn's `report` field in place of the path the orchestrator opens. The
117
- // committed root is a fixed name, so the path is known without reading anything.
118
- const REPORT_PATH = `shapeup/${slug}/REPORT.md`;
119
- const KERNEL = `${args.pluginRoot}/kernel/harness.mjs`;
120
- const execModel = args.models.exec;
121
- const evalModel = args.models.eval;
122
- const qaModel = args.models.qa || args.models.exec;
123
- const maxRounds = args.budgets.maxRounds;
124
- const attemptBudget = args.budgets.attemptBudget;
125
-
126
- // How many scopes may build at once. A dial rather than a constant because concurrency is a COST
127
- // question before it is a speed one: every extra leg is another worker's full context. 4 is the
128
- // default because a feature is rarely cut into more independent slices than that; 1 restores the
129
- // sequential behaviour for a project whose workers are not safe to run side by side.
130
- //
131
- // It is a CAP, not a group size. The window refills the instant any leg settles, so the dial bounds
132
- // spend without ever quantising the schedule — see the scheduler below for what the difference cost.
133
- const maxParallelScopes = Math.max(1, Number(args.maxParallelScopes ?? 4) || 1);
134
-
135
- // ---- SCHEDULER REGION START ------------------------------------------------------------------
136
- // BUILD's fan-out, as three separable decisions instead of one list operation:
137
- //
138
- // WAVES ORDER the scopes. EDGES RELEASE them. THE DIAL CAPS them.
139
- //
140
- // Fusing the three into `waves.flatMap((w) => chunk(w, maxParallelScopes))` cost wall-clock twice
141
- // over, and both costs are structural rather than incidental:
142
- //
143
- // 1. `chunk` turns the dial from a CAP into a QUANTUM. A wave of six at a dial of four runs four,
144
- // then idles three workers until the SLOWEST of those four lands, then runs two. Nothing about
145
- // the scopes asked for that; it falls out of splitting a list instead of scheduling it.
146
- // 2. A wave is a LEVEL, and a level says when a scope is definitely safe to start, never when it
147
- // BECAME safe. The scope that wires the others to the entry point waits for the slowest command
148
- // scope even when the one scope it actually consumes went green minutes earlier.
149
- //
150
- // Everything the previous loop bought with blood survives, and each is named at the line that keeps
151
- // it: a scope never starts beside a scope it consumes; no scope is ever dropped; a dead builder is a
152
- // spent attempt rather than a dead run; the dial is honoured exactly.
153
- //
154
- // This region is self-contained on purpose. The scheduling decision is PURE — no dispatch, no clock,
155
- // no randomness — so it is the one part of this file that can be executed against a fixture instead
156
- // of reasoned about, and the round loop below injects the real dispatcher as `launch`.
157
-
158
- /**
159
- * The dependency relation the fan-out releases on, as `scope_id → the scope_ids it waits for`.
160
- *
161
- * Three rungs, each a strict non-regression on the next. The edge list is used only when it
162
- * re-levels to the SAME waves: both fields are projections of one graph the kernel parsed once, so
163
- * each validates the other, and an edge dropped or invented in transit lands on the wave rung rather
164
- * than releasing a scope early — which is the one direction this must never fail in.
165
- *
166
- * @param {*} rawDeps - `probe resume`'s `scope_deps`: `[dependant, dependency]` path pairs.
167
- * @param {Array[]} waves - Validated dependency waves, arrays of scope objects.
168
- * @param {Array} list - Every scope in the round.
169
- * @returns {Map<string,Set<string>>} One entry per scope; an empty set means "ready immediately".
170
- */
171
- function scopeEdges(rawDeps, waves, list) {
172
- const known = new Set(list.map((s) => s.scope_id));
173
- const idOf = (p) => String(p).split("/").pop().replace(/\.(md|json)$/, "");
174
- const blank = () => new Map(list.map((s) => [s.scope_id, new Set()]));
175
-
176
- // Kahn. A CYCLE IS THE ONE INPUT THAT TURNS THIS SCHEDULER INTO A HANG rather than a failure: two
177
- // scopes awaiting each other produce no error, no log line and no artifact, which a list-chunking
178
- // loop could not do. So an edge set is used only when every node peels.
179
- const levelsOf = (m) => {
180
- const done = new Set(), out = [];
181
- for (;;) {
182
- const ready = list.filter((s) => !done.has(s.scope_id) && [...m.get(s.scope_id)].every((d) => done.has(d)));
183
- if (!ready.length) break;
184
- out.push(ready.map((s) => s.scope_id).sort());
185
- for (const s of ready) done.add(s.scope_id);
186
- }
187
- return done.size === list.length ? out : undefined;
188
- };
189
- const shape = (levels) => (levels ? JSON.stringify(levels) : "");
190
-
191
- if (Array.isArray(rawDeps) && rawDeps.length) {
192
- const m = blank();
193
- let edges = 0;
194
- for (const pair of rawDeps) {
195
- if (!Array.isArray(pair) || pair.length < 2) continue;
196
- const to = idOf(pair[0]), from = idOf(pair[1]);
197
- if (to === from || !known.has(to) || !known.has(from)) continue;
198
- m.get(to).add(from);
199
- edges++;
200
- }
201
- const want = shape(waves.map((w) => w.map((s) => s.scope_id).sort()));
202
- if (edges && shape(levelsOf(m)) === want) return m;
203
- log(`BUILD order — the dependency edge list does not re-derive the wave order (${edges} usable ` +
204
- `edge(s)); releasing per wave instead, which is what a wave-stepped fan-out did.`);
205
- }
206
-
207
- // WAVE RUNG. "Everything in wave i waits for everything in wave i-1" — the exact release points a
208
- // wave-stepped loop had, and acyclic by construction. An absent or unusable edge list costs the
209
- // per-edge release and nothing else; a single wave yields no edges at all, which is the fully
210
- // unscheduled fan-out. A scheduler that refuses to run is worse than one that runs unscheduled.
211
- const m = blank();
212
- for (let i = 1; i < waves.length; i++) {
213
- for (const to of waves[i]) for (const from of waves[i - 1]) m.get(to.scope_id).add(from.scope_id);
214
- }
215
- return m;
216
- }
217
-
218
- /**
219
- * Forbid two scopes that may write the same path from building at the same time.
220
- *
221
- * WHY THIS IS THE SCHEDULER'S JOB AND NOBODY ELSE'S. `shared_substrate` is the declared escape hatch
222
- * from the disjointness rule: the spec lint passes an overlap both contracts declare, and the
223
- * sandbox guard permits that path to every live order naming it. Both layers are individually
224
- * correct and the join is wrong — concurrent writers to one shared entry point lose each other's
225
- * work, measured in every trial, with every check green. No lint can catch it because nothing is
226
- * mis-declared; the only place the fact "these two are running RIGHT NOW" exists is here.
227
- *
228
- * AN EXCLUSION IS NOT A DEPENDENCY. Neither scope has to go first — they only have to not overlap —
229
- * so each pair is oriented by BUILD POSITION, later waits for earlier. That orientation is what
230
- * makes it safe to mix with real dependency edges: every dependency edge already points backwards
231
- * in this list (it is the waves flattened, so a scope's dependencies are strictly earlier), and an
232
- * exclusion oriented the same way keeps the whole set pointing backwards, which cannot cycle.
233
- *
234
- * DEGRADES LIKE EVERYTHING ELSE HERE, but note which way. Absent or unreadable pairs mean no
235
- * exclusions and today's scheduling — a scheduler that refuses to run is worse than one that runs
236
- * unscheduled. What is NOT acceptable is the opposite reading: when the pairs ARE there, the edge is
237
- * enforced rather than warned about, because a run that quietly loses work is worse than both.
238
- *
239
- * @param {Map<string,Set<string>>} edges - Release edges so far; mutated and returned.
240
- * @param {*} rawPairs - `probe resume`'s `scope_exclusions`: unordered path pairs.
241
- * @param {Array} order - The build order. Position in it decides which side of a pair waits.
242
- * @returns {{edges: Map<string,Set<string>>, added: number}} The composed set and how many landed.
243
- */
244
- function withExclusions(edges, rawPairs, order) {
245
- if (!Array.isArray(rawPairs) || !rawPairs.length) return { edges, added: 0 };
246
- const idOf = (p) => String(p).split("/").pop().replace(/\.(md|json)$/, "");
247
- const at = new Map(order.map((s, i) => [s.scope_id, i]));
248
- let added = 0;
249
- for (const pair of rawPairs) {
250
- if (!Array.isArray(pair) || pair.length < 2) continue;
251
- const a = idOf(pair[0]), b = idOf(pair[1]);
252
- if (a === b || !at.has(a) || !at.has(b)) continue;
253
- const [first, later] = at.get(a) < at.get(b) ? [a, b] : [b, a];
254
- if (edges.get(later)?.has(first)) continue; // already ordered by a real edge
255
- edges.get(later).add(first);
256
- added++;
257
- }
258
- return { edges, added };
259
- }
260
-
261
- /**
262
- * The widest set of scopes this release graph can have open at one moment.
263
- *
264
- * WHY A RUN NEEDS THIS BEFORE IT DISPATCHES ANYTHING. The dial says how many legs are PAID FOR; this
265
- * says how many the constraints actually permit, and the two are not the same number. A feature
266
- * whose entry point sits in five scopes' substrate has a ceiling of ONE however wide the dial is
267
- * opened — that is a scope-cutting fact, decided at the board review, and without this line it shows
268
- * up hours later as a slow round that reads exactly like slow workers.
269
- *
270
- * It also splits the concurrency shortfall into its two causes, which no single observation can. A
271
- * peak below the CEILING is a dispatch or runtime limit; a ceiling below the DIAL is the scope cut.
272
- * One number each, and the repair is different.
273
- *
274
- * The measure is the widest level of the release DAG, which is a guarantee rather than an estimate:
275
- * every member of a level has all its predecessors in earlier levels, so a level's whole width is
276
- * simultaneously admissible.
277
- *
278
- * @param {Map<string,Set<string>>} edges - The composed release set.
279
- * @param {Array} items - The scopes in the round.
280
- * @returns {number} Widest simultaneously-admissible set; 1 when nothing may overlap.
281
- */
282
- function releaseCeiling(edges, items) {
283
- const done = new Set();
284
- let widest = 1;
285
- for (;;) {
286
- const ready = items.filter((s) => !done.has(s.scope_id) && [...(edges.get(s.scope_id) || [])].every((d) => done.has(d)));
287
- if (!ready.length) break;
288
- widest = Math.max(widest, ready.length);
289
- for (const s of ready) done.add(s.scope_id);
290
- }
291
- return widest;
292
- }
293
-
294
- /**
295
- * Run `launch` over every item, at most `width` at once, releasing each item as soon as ITS OWN
296
- * dependencies have settled rather than when its whole wave has.
297
- *
298
- * Release is on SETTLED, not on green, which is deliberate and matches what a wave-stepped loop did.
299
- * Gating on green is strictly stronger and costs more than it buys: a dependency that fails would
300
- * starve every dependant of any attempt at all, and those scopes would leave the round's census
301
- * entirely — an outcome class the gate, the hill projection and the ledger have no reading for. What
302
- * must never happen is a scope building CONCURRENTLY with a scope it consumes, and that is what the
303
- * dependency await forbids.
304
- *
305
- * No race, no timer, no clock — deliberately. `Promise.race` over the in-flight legs would tighten
306
- * the schedule marginally and is disqualified for the same reason a clock read is: it makes the
307
- * schedule a function of real time, so a relaunch reschedules differently.
308
- *
309
- * @param {Array} items - The scopes, in the order the waves put them.
310
- * @param {Map<string,Set<string>>} edges - scope_id → the scope_ids it waits for.
311
- * @param {number} width - Concurrency cap.
312
- * @param {function(object): Promise<object>} launch - Dispatches one scope. May reject; may not be
313
- * trusted to return anything.
314
- * @returns {Promise<object[]>} Exactly one settled record per item, in input order. Never rejects.
315
- */
316
- async function scheduleScopes(items, edges, width, launch) {
317
- let free = Math.max(1, Number(width) || 1);
318
- const waiting = [];
319
- const acquire = () => (free > 0 ? (free--, Promise.resolve()) : new Promise((r) => waiting.push(r)));
320
- const release = () => { const next = waiting.shift(); if (next) next(); else free++; };
321
-
322
- // TWO COLLECTIONS, and the split is the difference between "one entry per item" and "one entry per
323
- // id". `runs` is POSITIONAL, so two scopes that somehow carry the same id are two legs and two
324
- // records; keying the settle by id instead would silently run one of them and report it twice,
325
- // which is a dropped scope wearing the right name. `started` is by id because that is how an edge
326
- // names its dependency, and it keeps the first promise for an id — a duplicate cannot make a
327
- // dependant wait on the wrong one.
328
- const runs = [];
329
- const started = new Map();
330
-
331
- async function runOne(item) {
332
- // THE DEPENDENCY WAIT HAPPENS BEFORE THE SLOT ACQUIRE, and that ordering is the whole reason
333
- // the window cannot deadlock against the edges: a waiting scope occupies no capacity, so the
334
- // window can never fill with scopes that are all waiting on one another.
335
- const deps = [...(edges.get(item.scope_id) || [])].map((d) => started.get(d)).filter(Boolean);
336
- if (deps.length) await Promise.all(deps);
337
- await acquire();
338
- try {
339
- const res = await launch(item);
340
- // The runtime drops an item whose stage returned no value and leaves an empty slot in the
341
- // settled list. Name that by itself: an empty slot and a dead worker are different repairs.
342
- return res || { scope_id: item.scope_id, __failed: "the runtime dropped this leg — a pipeline stage returned no value" };
343
- } catch (e) {
344
- // A DEAD BUILDER IS A SPENT ATTEMPT, NOT A DEAD RUN. Rethrowing would reject every other
345
- // scope's promise through the settle below and discard a whole round of green work.
346
- return { scope_id: item.scope_id, __failed: `${item.scope_id}: ${(e && e.message) || String(e)}` };
347
- } finally {
348
- // In `finally`, so a leg that dies before returning does not narrow the window permanently.
349
- release();
350
- }
351
- }
352
-
353
- // TWO PHASES, because one phase is a silently dropped edge. A body reads its dependencies' promises
354
- // out of `started` on its first line; built in a single pass, a dependency declared LATER in the
355
- // list is not there yet, its edge is discarded as unknown, and the scope that edge was protecting
356
- // starts early — the exact failure the edge exists to prevent, with no diagnostic anywhere.
357
- const begin = [];
358
- for (const item of items) {
359
- let open;
360
- const gate = new Promise((r) => { open = r; });
361
- const leg = gate.then(() => runOne(item));
362
- runs.push(leg);
363
- if (!started.has(item.scope_id)) started.set(item.scope_id, leg);
364
- begin.push(open);
365
- }
366
- for (const open of begin) open();
367
-
368
- // ONE ENTRY PER ITEM, IN INPUT ORDER, structurally — no edge, no failure, no dial setting and no
369
- // repeated id can drop a scope out of the round's census.
370
- return Promise.all(runs);
371
- }
372
- // ---- SCHEDULER REGION END --------------------------------------------------------------------
373
-
374
- // ---------------------------------------------------------------------------------------------
375
- // SCHEMAS — the only contract between this control script and a sub-agent. The runtime forces the
376
- // agent's final message to validate against these, which is the entire reason no stdout is parsed
377
- // anywhere in this file.
378
- //
379
- // A SCHEMA HERE IS ONE HALF OF A CONTRACT, and the other half is a kernel subcommand's stdout. The
380
- // runtime validates the agent's report against the shape declared BELOW, never against the shape the
381
- // kernel actually prints, so the two can disagree indefinitely: the courier sub-agent sits between
382
- // them and quietly coerces whatever it was handed into whatever it was asked for. That is not a
383
- // hypothetical — `scope_files` was declared here as an array of strings against a kernel that emits
384
- // `{scope_id, path}` objects, and it "worked" for the life of the file. The region markers are the
385
- // seam the structural suite reads these declarations through, so the two halves can be compared by
386
- // something other than a reader's eye.
387
- // ---- SCHEMA REGION START -----------------------------------------------------------------------
388
- const nullable = (t) => ({ type: [t, "null"] });
389
-
390
- /** A kernel subcommand a sub-agent ran in its own shell, reported as data. */
391
- const CMD = {
392
- type: "object",
393
- properties: {
394
- exit_code: { type: "integer" },
395
- ok: { type: "boolean" },
396
- detail: { type: "string" },
397
- // The machine value, copied CHARACTER FOR CHARACTER out of the command's own JSON. Separate
398
- // from `detail` on purpose: `detail` is prose for a human to read, this is a token the control
399
- // plane branches on, and collapsing the two is what made every gate comparison silently false.
400
- decision: { type: "string" },
401
- // A close's own advisory failure (kernel/probe/resume.mjs's `exportOnClose`) — copied verbatim, the same discipline as `decision`, so "the export failed" is a
402
- // fact `closeIfTerminal` can act on rather than a line buried inside free-text `detail`.
403
- export_warning: { type: "string" },
404
- },
405
- required: ["exit_code", "ok"],
406
- };
407
-
408
- /** `harness probe resume` — the fast-forward derivation, every field artifact-derived. */
409
- const RESUME = {
410
- type: "object",
411
- properties: {
412
- intake_path: nullable("string"), spec_folder: nullable("string"), orient_dir: nullable("string"),
413
- breadboard_path: nullable("string"), breadboard_source: nullable("string"),
414
- project_profile_path: nullable("string"), status: nullable("string"), closed_status: nullable("string"),
415
- lens: nullable("string"), stack: nullable("string"),
416
- run_cmd: nullable("string"), app_url: nullable("string"),
417
- eval_dimensions: { type: "array", items: { type: "string" } },
418
- has_orient_artifacts: { type: "boolean" },
419
- has_spec_tree: { type: "boolean" },
420
- has_board: { type: "boolean" },
421
- staged_workflow: {
422
- type: "object",
423
- properties: {
424
- checked: { type: "boolean" },
425
- drift: { type: "array", items: { type: "string" } },
426
- installed_version: nullable("string"),
427
- run_version: nullable("string"),
428
- warning: { type: "string" },
429
- },
430
- },
431
- // The requirements registry — a fact, not a phase. See the COVERAGE block below for why it is
432
- // guarded on this bare boolean and never asked about through `probe resume --require`.
433
- has_requirements: { type: "boolean" },
434
- has_wiring_map: { type: "boolean" },
435
- has_project_profile: { type: "boolean" },
436
- // THE SHAPE THE KERNEL WRITES, not a convenient one. `probe resume` emits `{scope_id, path}` per
437
- // contract and `ResumeState` declares exactly that; this declared an array of strings and the
438
- // call site below split each entry as a path. Nothing failed and nothing could: the courier
439
- // sub-agent sits between the kernel's stdout and this schema, so it coerced the objects into
440
- // whatever it was asked for, on every run, for the life of the file. The failure that shape
441
- // invites is silent in the other direction — a coercion that DROPS the entries reads as zero
442
- // scopes, which this file cannot distinguish from a run whose contracts were never written, so
443
- // MAP SCOPES is re-dispatched over contracts already on disk.
444
- scope_files: {
445
- type: "array",
446
- items: {
447
- type: "object",
448
- properties: { scope_id: { type: "string" }, path: { type: "string" } },
449
- required: ["scope_id", "path"],
450
- },
451
- },
452
- // Additive: the same scopes grouped into dependency waves. Absent or unusable → one wave.
453
- scope_waves: { type: "array", items: { type: "array", items: { type: "string" } } },
454
- // The same relation as edges — `[dependant, dependency]` pairs. A wave says when a scope is
455
- // definitely safe to start; an edge says when it BECAME safe. Absent or unusable → the waves'
456
- // own release points, which is what a wave-stepped fan-out had.
457
- scope_deps: { type: "array", items: { type: "array", items: { type: "string" } } },
458
- // Pairs that may write the same declared-shared path, so they may not build at the same time.
459
- // Not an ordering — either may go first. Absent or unusable → no exclusion, today's scheduling.
460
- scope_exclusions: { type: "array", items: { type: "array", items: { type: "string" } } },
461
- eval_rounds_done: { type: "array", items: { type: "integer" } },
462
- next_phase: nullable("string"),
463
- },
464
- required: ["has_orient_artifacts", "has_spec_tree", "has_wiring_map", "scope_files", "eval_rounds_done"],
465
- };
466
- // ---- SCHEMA REGION END -------------------------------------------------------------------------
467
-
468
- /** `harness reduce graph --subgraph run` — the bounded read model a round opens with. */
469
- const SUBGRAPH = {
470
- type: "object",
471
- properties: {
472
- run: nullable("string"),
473
- scopes: { type: "array", items: { type: "string" } },
474
- requirements: { type: "array", items: { type: "string" } },
475
- orders: { type: "integer" },
476
- pending_orders: { type: "array", items: { type: "string" } },
477
- rounds_with_green: { type: "array", items: { type: "integer" } },
478
- green_scopes_by_round: { type: "object" },
479
- trials: { type: "integer" },
480
- edges: { type: "integer" },
481
- },
482
- required: ["scopes", "rounds_with_green", "green_scopes_by_round"],
483
- };
484
-
485
- /** `harness probe t0` — has this scope already gone green in this round? */
486
- const T0CHECK = {
487
- type: "object",
488
- properties: { green: { type: "boolean" }, path: nullable("string") },
489
- required: ["green"],
490
- };
491
-
492
- /** `probe leg --order` — did one named order's result reach the single writer? Any phase. */
493
- const ORDERLEG = {
494
- type: "object",
495
- properties: {
496
- closed: { type: "boolean" },
497
- found: { type: "boolean" },
498
- order: nullable("string"),
499
- has_receipt: { type: "boolean" },
500
- has_result: { type: "boolean" },
501
- applied: { type: "boolean" },
502
- },
503
- required: ["closed", "found", "has_result", "applied"],
504
- };
505
-
506
- /** `probe attempts` — the attested census: what was spent, and whether the breaker really tripped. */
507
- const ATTEMPTS = {
508
- type: "object",
509
- properties: {
510
- scope_id: { type: "string" },
511
- spent: { type: "integer" },
512
- in_flight: { type: "integer" },
513
- green: { type: "boolean" },
514
- tripped: { type: "boolean" },
515
- },
516
- required: ["scope_id", "spent", "tripped"],
517
- };
518
-
519
- /** `probe leg` — did this scope's result reach the board, or is it finished work nothing applied? */
520
- const LEGCHECK = {
521
- type: "object",
522
- properties: {
523
- closed: { type: "boolean" },
524
- orders_total: { type: "integer" },
525
- results_total: { type: "integer" },
526
- applied_total: { type: "integer" },
527
- unapplied: { type: "array", items: { type: "string" } },
528
- },
529
- required: ["closed"],
530
- };
531
-
532
- const ORIENT = {
533
- type: "object",
534
- properties: {
535
- ok: { type: "boolean" }, artifact_written: { type: "boolean" },
536
- spiked_area: { type: "string" }, spike_result: { type: "string" },
537
- riskiest_unknowns: { type: "array", items: { type: "string" } },
538
- },
539
- required: ["ok", "artifact_written", "spiked_area", "spike_result"],
540
- };
541
-
542
- /** coverage / analyze / wire — "did the artifact land?" is all a gate needs from them. */
543
- const PHASE_OK = {
544
- type: "object",
545
- properties: { ok: { type: "boolean" }, artifact_written: { type: "boolean" }, detail: { type: "string" } },
546
- required: ["ok", "artifact_written"],
547
- };
548
-
549
- const MAPSCOPES = {
550
- type: "object",
551
- properties: {
552
- ok: { type: "boolean" }, artifact_written: { type: "boolean" },
553
- scopes: {
554
- type: "array",
555
- items: {
556
- type: "object",
557
- properties: { scope_id: { type: "string" }, path: { type: "string" } },
558
- required: ["scope_id", "path"],
559
- },
560
- },
561
- },
562
- required: ["ok", "artifact_written", "scopes"],
563
- };
564
-
565
- /** One scope's whole attempt ratchet, run inside the worker leg (see buildScope). */
566
- const SCOPE_RESULT = {
567
- type: "object",
568
- properties: {
569
- scope_id: { type: "string" },
570
- green: { type: "boolean" },
571
- t0_artifact: nullable("string"),
572
- attempts_used: { type: "integer" },
573
- breaker: { type: "string", enum: ["none", "stagnation", "attempt_budget"] },
574
- reason: { type: "string" },
575
- },
576
- required: ["scope_id", "green", "attempts_used", "breaker"],
577
- };
578
-
579
- const EVAL = {
580
- type: "object",
581
- properties: {
582
- ok: { type: "boolean" },
583
- overall: { type: "string", enum: ["PASS", "FAIL"] },
584
- findings: {
585
- type: "array",
586
- items: {
587
- type: "object",
588
- properties: { id: { type: "string" }, criterion: { type: "string" }, evidence: { type: "string" } },
589
- required: ["id", "criterion", "evidence"],
590
- },
591
- },
592
- },
593
- required: ["ok", "overall"],
594
- };
595
-
596
- // The round-loop branch reads THIS, not `EVAL` above, for the pass/fail decision. `EVAL` shapes
597
- // the dispatching agent's own end-of-turn summary of a 3-step dispatch (compile, dispatch,
598
- // ingest) — schema-checked for SHAPE only, never re-verified against the WorkResult `reduce
599
- // ingest` just wrote. Measured live (2026-08-19, todo-cli): that summary reported the round as
600
- // passing while `results/evaluate-r1.json`'s own `verdict.overall` said `"FAIL"` (2 cited bugs),
601
- // and the round loop broke straight to QA/GATE H on the strength of the summary alone — the same
602
- // self-report-instead-of-artifact-read shape this file already fixed once for gate DECISIONS
603
- // (see the comment above `crossGate`), just never extended to the EVAL verdict itself. `probe
604
- // eval` is `harness probe eval`'s own output — a narrow, single-purpose "transcribe this JSON
605
- // verbatim" query over the WorkResult on disk, the same pattern `probe t0`/`probe resume` already
606
- // use everywhere else a fact has to cross the no-filesystem boundary.
607
- const EVAL_VERDICT = {
608
- type: "object",
609
- properties: {
610
- ok: { type: "boolean" },
611
- overall: nullable("string"),
612
- bug_count: nullable("integer"),
613
- report_path: nullable("string"),
614
- round: { type: "integer" },
615
- // The dimensions round N's evaluate order named — what "PASS" actually covers.
616
- dimensions: { type: ["array", "null"], items: { type: "string" } },
617
- // Why the round holds no verdict it may act on — the evaluator's own first deviation when it
618
- // refused, or what is structurally wrong with the verdict it returned. Null when `ok`.
619
- status: nullable("string"),
620
- reason: nullable("string"),
621
- },
622
- required: ["ok", "round"],
623
- };
624
-
625
- const REFUTATION = {
626
- type: "object",
627
- properties: { id: { type: "string" }, refuted: { type: "boolean" }, why: { type: "string" } },
628
- required: ["id", "refuted"],
629
- };
630
-
631
- const QA_REPORT = {
632
- type: "object",
633
- properties: { ok: { type: "boolean" }, findings_count: { type: "integer" } },
634
- required: ["ok", "findings_count"],
635
- };
636
-
637
- const BOARD_CHECK = {
638
- type: "object",
639
- properties: { ok: { type: "boolean" }, reason: { type: ["string", "null"] } },
640
- required: ["ok"],
641
- };
642
-
643
- const HUNT_OUTCOME = {
644
- type: "object",
645
- properties: {
646
- ok: { type: "boolean" }, status: { type: ["string", "null"] },
647
- qa: { type: "string", enum: ["run", "not-hunted", "missing"] },
648
- charters_run: { type: ["integer", "null"] }, findings: { type: "integer" }, reason: { type: ["string", "null"] },
649
- },
650
- required: ["ok", "qa"],
651
- };
652
-
653
- const HAMMER = {
654
- type: "object",
655
- properties: {
656
- ok: { type: "boolean" },
657
- verdict: { type: "string", enum: ["ship-now", "ship-after-fixes", "cannot-ship"] },
658
- cut_list: { type: "array", items: { type: "string" } },
659
- },
660
- required: ["ok", "verdict", "cut_list"],
661
- };
662
-
663
- /** What every hammer dispatch is told, on the PASS path and the breaker path alike. */
664
- const HAMMER_EXTRA =
665
- "Run the census, compare against the BASELINE and never the ideal, and produce the cut list. " +
666
- "Write the census as data to `reports/hammer-census.json` under the run's local root — the " +
667
- "`reports/**` entry in your order's substrate names the directory — with schema_version 1, " +
668
- "order_id, verdict (ship-now | ship-after-fixes | cannot-ship), cut_list, ship_blocking, breaker, " +
669
- "baseline — beside the human-readable report. GATE L4's resolver reads that file and nothing " +
670
- "else before it lets any answer set say `ship`; a census that lives only in your printed blocks " +
671
- "cannot be read by a gate.";
672
-
673
- /**
674
- * A breaker routed the run to GATE H. This used to be a terminal return: the loop handed back
675
- * `{status: "gate_h"}`, the close-out stamped the ledger `escalated` and retired the pointers, and
676
- * the census, GATE H, the ship report and GATE L4 all happened AFTER the close, in the tech
677
- * lead's prose — so the census reached no artifact, `gates.jsonl` held no H and no L4, and a later
678
- * `--close shipped` was refused over the `escalated` fact already on the ledger. Measured on three
679
- * consumer runs. AGENTS.md has always said what a breaker means: "ship what's green, never kill the
680
- * run from outside". So the run does that itself, in this launch: the hammer census (an artifact),
681
- * GATE H (a row), the ship report (with the verdict as it is, FAIL or not-evaluated included), and
682
- * GATE L4 (a row) — and only then a close, `shipped` or `escalated`, that names the census.
683
- *
684
- * QA never ran on this path (it sits after a PASS), so the census is told so plainly. `qaFindings`
685
- * is declared after the round loop and must not be read from here.
686
- *
687
- * @param {object} ret - The gate_h return the round loop built.
688
- * @returns {Promise<object>} The RunReturn that actually ends the run.
689
- */
690
- async function settleAtGateH(ret) {
691
- phase("Ship");
692
- const h = await worker({
693
- skill: "scope-hammer", operation: "hammer", schema: HAMMER, phase: "Ship", label: "hammer",
694
- payload: { feature: slug, qa_findings: 0, hammer_proposals: ret.hammer_proposals || [], breaker: ret.breaker ?? null },
695
- extra: HAMMER_EXTRA,
696
- });
697
- if (h.__failed) return await withWarnings(diedAt("H", h));
698
- {
699
- const g = await crossGate("H", "Ship", ["accept-cut-list", "ship-all", "ask"],
700
- { verdict: h.verdict, cut_list: h.cut_list, breaker: ret.breaker ?? null, green_scopes: ret.green_scopes, hammer_proposals: ret.hammer_proposals });
701
- if (g.stop) return await withWarnings(g.stop);
702
- }
703
- if (h.verdict === "cannot-ship") {
704
- return await withWarnings({ ...ret, census: "cannot-ship", cut_list: h.cut_list });
705
- }
706
- const hVerdict = verdict === "pass" ? "PASS" : verdict === "fail" ? "FAIL" : "not-evaluated";
707
- const ship = await cmd(`reduce ship --slug ${slug} --verdict ${hVerdict} --qa skipped`, "Ship", "ship-report");
708
- if (!ship.ok) {
709
- return await withWarnings({ ...ret, census: h.verdict, cut_list: h.cut_list, ship_report: `refused: ${ship.detail || `exit ${ship.exit_code}`}` });
710
- }
711
- {
712
- const g = await crossGate("L4", "Ship", ["ship", "hold", "ask"], { verdict: hVerdict, census: h.verdict, cut_list: h.cut_list, breaker: ret.breaker ?? null });
713
- if (g.stop) return await withWarnings(g.stop);
714
- if (g.decision === "hold") return await withWarnings({ ...ret, census: h.verdict, cut_list: h.cut_list, l4: "hold" });
715
- }
716
- await advisory(`report export --slug ${slug}`, "Ship", "export-run");
717
- await setRunStatus("shipped", "Ship");
718
- return await withWarnings({
719
- status: "shipped", verdict, rounds_used: round, after: "gate_h", breaker: ret.breaker ?? null,
720
- census: h.verdict, cut_list: h.cut_list, green_scopes: ret.green_scopes,
721
- unapplied_results: ret.unapplied_results || [], qa_findings: 0,
722
- report: REPORT_PATH,
723
- });
724
- }
725
-
726
- // ---------------------------------------------------------------------------------------------
727
- // DISPATCH — three shapes, and none of them parses text.
728
- //
729
- // A sub-agent can be skipped by the operator or die on a terminal API error, in which case the
730
- // runtime hands back `null`. A null is a NAMED failure here, never a crash: `status: "failed"` is
731
- // not a member of the RunReturn union, so every call site converts it to one that is.
732
- // ---------------------------------------------------------------------------------------------
733
- const nullFail = (label) => ({ __failed: `${label}: sub-agent skipped, blocked, or died after retries` });
734
-
735
- /**
736
- * Run one kernel subcommand in a sub-agent's own shell and get its outcome as data.
737
- *
738
- * @param {string} verbs - Kernel verb words plus flags, e.g. `reduce hill --slug x`.
739
- * @param {string} phaseName - Progress group.
740
- * @param {string} label - Display label.
741
- * @returns {Promise<{exit_code:number, ok:boolean, detail?:string}>} Never rejects.
742
- */
743
- async function cmd(verbs, phaseName, label) {
744
- const r = await agent(
745
- `Run exactly this command and nothing else:\n\n node "${KERNEL}" ${verbs}\n\n` +
746
- `Report its exit code as exit_code, ok=true if and only if exit_code is 0, and one line of ` +
747
- `detail. If the command printed JSON carrying a top-level "decision" key, copy that value into ` +
748
- `decision EXACTLY as it appears — one bare token, no sentence, no quotes, no rephrasing. ` +
749
- `Otherwise omit decision. If the command printed JSON carrying a top-level "export_warning" ` +
750
- `key with a non-empty string value, copy that string into export_warning verbatim. Otherwise ` +
751
- `omit export_warning. Do not interpret, summarise or act on the command's output beyond that.\n\n` +
752
- `If the tool call itself is refused or blocked before the command ever runs — a permission or ` +
753
- `policy denial, not the command's own exit — that is NOT an exit code, and you must never invent ` +
754
- `one to fill the field: report exit_code as -1 and put the denial's own wording verbatim in ` +
755
- `detail. -1 is not a real process exit and this file treats it as "the check could not run", never ` +
756
- `as evidence about what the command would have said.`,
757
- { model: "sonnet", effort: "low", phase: phaseName, label, schema: CMD },
758
- );
759
- return (r && typeof r === "object") ? r : { exit_code: -1, ok: false, detail: `${label}: no result` };
760
- }
761
-
762
- /**
763
- * Run a kernel subcommand whose outcome is advisory — a projection or a lint that informs but does
764
- * not stop the run.
765
- *
766
- * It still LOOKS at the exit code. A command whose result nobody reads is a command whose failure
767
- * is indistinguishable from its success, which is the defect class this whole file is arranged
768
- * against; "advisory" means the run continues, not that nothing is recorded.
769
- *
770
- * @param {string} verbs - Kernel verb words plus flags.
771
- * @param {string} phaseName - Progress group.
772
- * @param {string} label - Display label.
773
- * @returns {Promise<void>} Settles when the command has run.
774
- */
775
- async function advisory(verbs, phaseName, label) {
776
- const r = await cmd(verbs, phaseName, label);
777
- if (!r.ok) log(`${label} — did not complete (${r.detail || `exit ${r.exit_code}`}). The run continues; this output is a projection, not a gate.`);
778
- }
779
-
780
- /**
781
- * Run a kernel subcommand whose stdout is a JSON document this script needs the fields of.
782
- *
783
- * The sub-agent re-reports the document against `schema`, so what reaches this file is validated by
784
- * the runtime rather than parsed here.
785
- *
786
- * @param {string} verbs - Kernel verb words plus flags.
787
- * @param {object} schema - The shape the caller branches on.
788
- * @param {string} phaseName - Progress group.
789
- * @param {string} label - Display label.
790
- * @returns {Promise<(object|null)>} The validated document, or null when the agent produced none.
791
- */
792
- async function query(verbs, schema, phaseName, label) {
793
- const r = await agent(
794
- `Run exactly this command and nothing else:\n\n node "${KERNEL}" ${verbs}\n\n` +
795
- `It prints one JSON document on stdout. Return that document's fields as the schema names ` +
796
- `them, verbatim — do not add, rename, summarise or infer any value.`,
797
- { model: "sonnet", effort: "low", phase: phaseName, label, schema },
798
- );
799
- return (r && typeof r === "object") ? r : null;
800
- }
801
-
802
- // A payload field that is not known yet is ABSENT, never `null`.
803
- //
804
- // The two contracts either side of this line disagree about how to say "unknown", and the
805
- // disagreement is load-bearing: `probe resume` declares `stack`, `lens`, `run_cmd` and `app_url`
806
- // nullable — null is its legitimate answer on a fresh run — while `WorkOrderPayload` types them
807
- // `string` and marks every one of them OPTIONAL. So the contract already models unknown as an
808
- // absent key, and forwarding the probe's `null` into it produces an order that fails its own schema
809
- // before a worker ever sees it. Four of the seven operations this file dispatches — orient, analyze,
810
- // evaluate, hunt — carry such a field, ORIENT among them, so the first dispatch of every fresh run
811
- // was refused at compile. The three that carry none (wire, map-scopes, hammer) always compiled,
812
- // which is why the fault looked intermittent rather than total.
813
- //
814
- // Dropping the key is the correct direction. Widening the schema to accept null would make
815
- // `"stack": null` a valid order and push the null downstream into every worker's prompt, where each
816
- // one would have to re-decide what a null stack means.
817
- const compact = (payload) =>
818
- Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== null && v !== undefined));
819
-
820
- /**
821
- * Dispatch one worker through the envelope port and get back the fields a gate needs.
822
- *
823
- * The sub-agent compiles its own WorkOrder with `harness compile`, calls the Skill against it, and
824
- * applies its WorkResult with `harness reduce ingest` — all in its own shell. This script never
825
- * couriers those commands, and the `verify envelope` hook still denies a dispatch whose order is
826
- * missing or schema-invalid, because that hook fires on the Skill call the sub-agent makes.
827
- *
828
- * @param {object} spec - `{skill, operation, payload, schema, phase, label, model, extra}`.
829
- * @returns {Promise<object>} The validated worker report, or a `__failed` marker.
830
- */
831
- async function worker({ skill, operation, payload, schema, phase: phaseName, label, model = execModel, extra = "", compile, round }) {
832
- // The compile line is overridable because not every dispatch can be addressed the same way.
833
- // `--operation <op> --slug <slug>` resolves its worker from `compile`'s OP_OWNER table, which
834
- // covers the planner/judge/QA operations and NOT `execute`: a build order is addressed by scope,
835
- // round and attempt, so the generic form exits 2 ("could not resolve --worker/--operation") for
836
- // exactly the leg that runs most often. It used to be emitted for build legs anyway, contradicted
837
- // one line later by `extra`'s correct `--scope …` instruction — a prompt arguing with itself.
838
- // `--round` when the dispatch belongs to one. `compile` already suffixes the order id with it
839
- // (`evaluate-r2`), and `probe resume` already reads `evaluate-r<N>.json` back to learn which EVAL
840
- // rounds are done — writer and reader agreed all along and only this caller omitted the flag. The
841
- // cost of that omission is two facts, both silent: every relaunch restarted the round counter at
842
- // 1, because `eval_rounds_done` could never match `evaluate.json`; and round 2's evaluate result
843
- // OVERWROTE round 1's, so a run kept only its last round's envelope.
844
- const roundFlag = round ? ` --round ${round}` : "";
845
- // A CALLER-SUPPLIED PAYLOAD ONLY REACHES THE ORDER ON THE GENERIC LINE. Overriding `compile`
846
- // replaces the whole command, `--payload` included, so a caller that passes both is writing a
847
- // field nobody will ever read — which is exactly how `payload.bugs` came to be built, filtered,
848
- // and discarded on every fix round for the life of this file. Loud rather than fatal: the order
849
- // is still valid, and killing a build leg over a discarded field would cost more than it saves.
850
- if (compile && payload && Object.keys(compact(payload)).length) {
851
- log(`DISPATCH — ${label}: payload dropped (${Object.keys(compact(payload)).join(", ")}). This ` +
852
- `dispatch overrides its compile line, so --payload is not emitted; the order must derive ` +
853
- `those fields itself (see harness compile).`);
854
- }
855
- const compileCmd = compile
856
- || `compile --operation ${operation} --slug ${slug}${roundFlag} --payload '${JSON.stringify(compact(payload))}'`;
857
- const r = await agent(
858
- `You are running one step of an orchestrated build over feature slug "${slug}".\n\n` +
859
- `1. Compile the WorkOrder:\n` +
860
- ` node "${KERNEL}" ${compileCmd}\n` +
861
- ` It prints the order path on stdout.\n` +
862
- `2. Dispatch the worker against that order:\n` +
863
- ` Skill(shapeup-sdlc-plugin:${skill}) --order "<the path from step 1>"\n` +
864
- ` If the Skill dispatch fails or returns an error, STOP and report the error. Never work ` +
865
- `around a failed dispatch by doing the craft yourself — the run has a receipt gate and an ` +
866
- `improvised result is refused at step 3 anyway.\n` +
867
- ` ${extra}\n` +
868
- `3. Apply its WorkResult, naming the ORDER from step 1 — not any path the worker reports:\n` +
869
- ` node "${KERNEL}" reduce ingest --order <the path from step 1>\n\n` +
870
- `Then report ONLY the fields the schema names. Nothing else crosses this boundary — no ` +
871
- `narration, no file contents, no summary of the work.`,
872
- { model, phase: phaseName, label, schema, effort: "medium" },
873
- );
874
- return (r && typeof r === "object") ? r : nullFail(label);
875
- }
876
-
877
- /**
878
- * Dispatch a worker, check its result mechanically, and send it back ONCE with the refusal.
879
- *
880
- * A worker's result can be well-formed and still not what the run asked for — a verdict that grades
881
- * the surface as a group, a hunt that reached the app and ran no charter. The kernel says so; this
882
- * turns that into one more dispatch carrying the kernel's own reason, and only a second refusal
883
- * stands. Measured: a rule in the skill made the judge grade row by row on its first dispatch, and
884
- * the refusal is what holds when the model takes the loose path anyway.
885
- *
886
- * @param {function(string): Promise<object>} dispatch - Dispatches the worker; takes a note to append.
887
- * @param {function(string): Promise<(object|null)>} check - Runs the kernel's check; takes a label suffix.
888
- * @param {function(object): boolean} refused - Whether a check's answer is a correctable refusal.
889
- * @param {string} what - What is being checked, for the log line.
890
- * @returns {Promise<{r: object, v: (object|null)}>} The last dispatch and the last check.
891
- */
892
- async function sendBackOnce(dispatch, check, refused, what) {
893
- let r = await dispatch("");
894
- if (r.__failed) return { r, v: null };
895
- let v = await check("");
896
- if (v && refused(v)) {
897
- log(`${what} — refused (${v.reason}); sent back once`);
898
- r = await dispatch(` Your previous result was refused: ${v.reason}. Correct that and return again.`);
899
- if (r.__failed) return { r, v: null };
900
- v = await check(":again");
901
- }
902
- return { r, v };
903
- }
904
-
905
- /**
906
- * Dispatch a board writer (analyze, or the board-only regeneration) and send it back once when the
907
- * board carries no `covers:` clause while a requirements registry exists.
908
- *
909
- * Advisory past the second dispatch: the requirements matrix never blocks a ship, so a board that
910
- * still carries none continues with a state warning naming what the matrix will read, rather than
911
- * aborting a run whose plan is otherwise sound.
912
- *
913
- * @param {function(string): Promise<object>} dispatch - The board writer's dispatch; takes a note.
914
- * @param {string} what - The operation, for the log and the warning.
915
- * @returns {Promise<{r: object, v: (object|null)}>} The last dispatch and the last check.
916
- */
917
- async function boardSentBack(dispatch, what) {
918
- const out = await sendBackOnce(dispatch,
919
- (suffix) => query(`probe requirements --slug ${slug} --board-check`, BOARD_CHECK, "Analyze", `board-covers${suffix}`),
920
- (v) => !v.ok && !!v.reason, `ANALYZE ${what}`);
921
- if (!out.r.__failed && out.v && !out.v.ok && out.v.reason) {
922
- const msg = `ANALYZE ${what}: ${out.v.reason} (after one send-back; the requirements matrix will read no evidence)`;
923
- log(msg);
924
- stateWarnings.push(msg);
925
- }
926
- return out;
927
- }
928
-
929
- // ---------------------------------------------------------------------------------------------
930
- // GATES — the kernel's exit-code convention, unchanged: 0 cross · 4 pause · 5 abort. The resolved
931
- // decision travels in `decision`, copied verbatim out of the kernel's own JSON.
932
- //
933
- // IT USED TO BE READ OUT OF `detail`, which is free prose a sub-agent writes. What came back was a
934
- // sentence — "Command exited 0; gate QA resolved decision=run from …" — and every comparison
935
- // downstream is against a token, so `decision === "run"` and `decision === "stop"` were false on
936
- // every run that has ever executed. The observable effect was a documented phase that silently
937
- // never ran and a PO answer at L3 that did nothing, with no error and no artifact to notice the
938
- // absence by. This file's own header says nothing here reads a model's prose; this was the one
939
- // place that did, and every gate decision passed through it.
940
- //
941
- // An unreadable decision now ABORTS rather than defaulting. A default is what made the original
942
- // defect invisible: "proceed" is a plausible answer at four of these gates, so the run continued
943
- // and read as healthy. Stopping converts a silent skip into a first-run error.
944
- // ---------------------------------------------------------------------------------------------
945
- const PRESETS = new Set(["ci", "guarded", "interactive"]);
946
- // The non-preset branch is an operator-supplied PATH, so it is quoted: every kernel call in this file
947
- // is spelled into a sub-agent's prompt, and the PreToolUse envelope gate scans that prompt as well as
948
- // a `Skill` call's args. A bare path is read together with the trailing quote of the literal around
949
- // it, and the whole dispatch is denied.
950
- const answersFlag = (a) => (!a ? "" : PRESETS.has(a) ? `--preset ${a}` : `--file "${a}"`);
951
-
952
- const TITLES = {
953
- L1a: "Orient Review", "L1a.5": "Wiring Review", L1b: "Board Review",
954
- L2: "Build Round Complete", L3: "Verdict & Loop", QA: "QA Edge Hunt", H: "Decide When to Stop",
955
- };
956
- const gateBlock = (g, ctx) =>
957
- [`⏸ GATE ${g} — ${TITLES[g] || g}`, ...Object.entries(ctx).map(([k, v]) => `${k}: ${JSON.stringify(v)}`)].join("\n");
958
- const paused = (g, valid, ctx) => ({ status: "paused", paused_at: g, block: gateBlock(g, ctx), valid_decisions: valid, context: ctx });
959
- const aborted = (g, why) => ({ status: "aborted", aborted_at: g, reason: why });
960
- const diedAt = (g, r) => ({ status: "aborted", aborted_at: g, reason: r.__failed });
961
-
962
- /**
963
- * Resolve one gate: cross, pause, or abort.
964
- *
965
- * @param {string} gateId - Gate id (L1a, L2, …).
966
- * @param {string} phaseName - Progress group.
967
- * @param {string[]} validDecisions - What a human may answer if this pauses.
968
- * @param {object} ctx - The facts the gate block shows.
969
- * @returns {Promise<{stop?: object, decision?: string}>} `stop` carries a terminal RunReturn.
970
- */
971
- async function crossGate(gateId, phaseName, validDecisions, ctx) {
972
- // The gate ledger keys a per-round crossing (L2, L3) on gate id + round, so it needs the round
973
- // whenever the caller already has one to show in the block — the same value `ctx.round` carries
974
- // for display, threaded through rather than re-derived.
975
- const roundFlag = (ctx?.round != null ? ` --round ${ctx.round}` : "") + (ctx?.verdict ? ` --verdict ${ctx.verdict}` : "");
976
- const g = await cmd(`gate --resolve ${gateId} --slug ${slug}${roundFlag} ${answersFlag(args.answers)}`.trim(), phaseName, `gate:${gateId}`);
977
- if (g.exit_code === 4) return { stop: paused(gateId, validDecisions, ctx) };
978
- if (g.exit_code === 5) return { stop: aborted(gateId, g.detail || `GATE ${gateId} aborted`) };
979
- const decision = String(g.decision ?? "").trim();
980
- if (!validDecisions.includes(decision)) {
981
- return {
982
- stop: aborted(gateId,
983
- `GATE ${gateId} exited 0 but its decision did not come back as one of ` +
984
- `${validDecisions.join(" | ")} — got ${JSON.stringify(g.decision ?? null)}. The kernel prints ` +
985
- `the decision as a top-level JSON key; a run must not guess it. Re-run this gate, or answer ` +
986
- `it directly with --answers.`),
987
- };
988
- }
989
- return { decision };
990
- }
991
-
992
- // Ask the completion oracle about ONE phase, as a leg in that phase's progress group. Declared
993
- // ahead of both callers rather than between them: a `const` is not hoisted, and this file has paid
994
- // for a temporal-dead-zone binding once already (see `findings` at the round loop).
995
- const attest = (phaseKey, phaseName, label) =>
996
- cmd(`probe resume --slug ${slug} --require ${phaseKey}`, phaseName, label);
997
-
998
- /**
999
- * The other half of a phase post-condition: the single writer ran.
1000
- *
1001
- * The artifact check asks whether the worker wrote its product. It cannot ask whether the
1002
- * WorkResult was applied — the leg row, the discoveries, the board — because the product is written
1003
- * by the worker directly and the envelope is applied by `reduce ingest`, a separate act the leg's
1004
- * script names as its last step. Measured on a live run: a planning phase landed a result naming
1005
- * seventeen artifacts and no leg row, the artifact check passed, and the run walked on with the
1006
- * result's discoveries never reaching the ledger. So this asks the leg ledger by order name. A
1007
- * result on disk that nothing applied is ingested here — the same repair the build round makes,
1008
- * for the same reason: the single writer is the invariant, not which step invokes it — and if it
1009
- * still is not applied after that, the run stops with a cause naming the writer that did not run,
1010
- * rather than folding the gap into a phase that "completed".
1011
- *
1012
- * @param {string} gate - The gate name to report the abort under.
1013
- * @param {string} phaseKey - The phase.
1014
- * @param {string} phaseName - Progress group.
1015
- * @param {string} [orderStem=phaseKey] - The order's file stem, when the phase was dispatched
1016
- * under a different operation (a board-only ANALYZE compiles `board.json`).
1017
- * @returns {Promise<(object|null)>} An aborted RunReturn, or null when the leg closed (or the
1018
- * question could not be asked — a probe that did not run proves nothing, and is logged as such).
1019
- */
1020
- async function requireLeg(gate, phaseKey, phaseName, orderStem = phaseKey, again = null) {
1021
- const ask = () => query(`probe leg --slug ${slug} --order "${orderStem}"`, ORDERLEG, phaseName, `legcheck:${orderStem}`);
1022
- let leg = await ask();
1023
- if (!leg || !leg.found) { log(`${gate} — could not ask the leg ledger about "${phaseKey}" (probe returned ${leg ? "no order" : "nothing"}); proceeding on the artifact alone.`); return null; }
1024
- // SENT BACK ONCE FOR THE ENVELOPE. A dispatched leg that wrote its artifacts and no WorkResult has
1025
- // done the craft and skipped the last step, and it recurs on the same phase run after run. The
1026
- // worker is dispatched again with the reason, told its artifacts stand; only a second miss is named
1027
- // at the close.
1028
- if (!leg.has_result && leg.has_receipt && again) {
1029
- log(`${gate} — "${phaseKey}" was dispatched and returned no WorkResult; sending it back once for the envelope.`);
1030
- await again(` Your previous dispatch of this order wrote the phase's artifacts and returned NO WorkResult. ` +
1031
- `The artifacts are on disk and stand: do not redo them. Write the WorkResult the order names ` +
1032
- `(its substrate's own results path), then ingest it as the steps above say.`);
1033
- leg = await ask();
1034
- if (!leg || !leg.found) return null;
1035
- }
1036
- if (!leg.has_result) {
1037
- // The artifact is on disk and the envelope never came back. Measured live: the first dispatch of
1038
- // a run wrote its four artifacts and no WorkResult, every later phase read the artifacts, and
1039
- // the run closed `shipped` over an order still open by construction. The phase stands on its
1040
- // artifact — that is what the post-condition checks — but the order is named at the close, and
1041
- // the next run over this slug will find it unanswered rather than be surprised by it.
1042
- log(`${gate} — "${phaseKey}" ${leg.has_receipt ? "was dispatched (receipt on disk) and" : "has an order but no receipt, and"} never answered: no WorkResult at all. ` +
1043
- `Its artifacts are on disk and the phase proceeds on them; the order stays unanswered and is named at the close.`);
1044
- if (leg.order && !unansweredOrders.includes(leg.order)) unansweredOrders.push(leg.order);
1045
- return null;
1046
- }
1047
- if (leg.applied) return null;
1048
- log(`${gate} — "${phaseKey}" came back with a result nothing applied (no leg row). Ingesting it here: ${leg.order}.`);
1049
- await advisory(`reduce ingest --order "${leg.order}"`, phaseName, `late-ingest:${phaseKey}`);
1050
- leg = await ask();
1051
- if (leg?.applied) return null;
1052
- return aborted(gate,
1053
- `${gate}: the single writer did not run for "${phaseKey}" — its WorkResult is on disk and no leg row ` +
1054
- `records it being applied, and a late \`reduce ingest\` did not take either. The phase's product exists; ` +
1055
- `what it discovered and reported never reached the ledger or the board. Read the result, run ` +
1056
- `\`reduce ingest --order "${leg?.order ?? "<its order>"}"\` by hand to see why it refuses, then relaunch.`);
1057
- }
1058
-
1059
- /**
1060
- * The phase post-condition: the artifact is on disk, or the run stops here.
1061
- *
1062
- * Deliberately the SAME derivation the fast-forward uses, asked about one phase. Two readings of
1063
- * "is this phase done" that can disagree is the defect class, not a safeguard against it.
1064
- *
1065
- * @param {string} gate - The gate name to report the abort under.
1066
- * @param {string} phaseKey - The phase `probe resume --require` knows.
1067
- * @param {string} phaseName - Progress group.
1068
- * @returns {Promise<(object|null)>} An aborted RunReturn, or null when the artifact is there.
1069
- */
1070
- async function requirePhase(gate, phaseKey, phaseName, orderStem = phaseKey, again = null) {
1071
- const r = await attest(phaseKey, phaseName, `require:${phaseKey}`);
1072
- if (r.exit_code === 0) return await requireLeg(gate, phaseKey, phaseName, orderStem, again);
1073
- // Exit 6 is `probe resume --require`'s OWN documented code for "the artifact really is not on
1074
- // disk" (kernel/probe/resume.mjs banner). Any other value — including -1, the courier's sentinel
1075
- // for a tool call that never ran — is not that predicate answering "no"; it is the predicate never
1076
- // being asked. Collapsing both into one message manufactured a false "worker escalated" diagnosis
1077
- // the one time this was measured live: the artifacts existed, and the check itself had been denied
1078
- // by a layer above this plugin (workspace trust, or Claude Code's own auto-mode classifier —
1079
- // neither leaves a row in decisions.jsonl, because neither is this plugin's hook).
1080
- if (r.exit_code === 6) {
1081
- return aborted(gate,
1082
- `${gate} produced no artifact: the ${phaseKey} artifact is not on disk after the phase ran and its ` +
1083
- `result was ingested. The phase did not complete — its worker most likely escalated (a WorkResult ` +
1084
- `may report "escalated" with an empty artifacts list) — and because completion is derived from the ` +
1085
- `artifact, every relaunch would re-dispatch this phase and escalate again. Read the phase's result ` +
1086
- `to see what it could not complete, resolve it, then relaunch.`);
1087
- }
1088
- return aborted(gate,
1089
- `${gate}'s completion check for "${phaseKey}" did not run to a verdict — exit_code ${r.exit_code}, ` +
1090
- `not the 0 (satisfied) or 6 (artifact absent) \`probe resume --require\` documents.` +
1091
- `${r.detail ? ` Courier reported: ${r.detail}.` : ""} This is NOT evidence the phase failed — it means ` +
1092
- `the check itself could not run, most often a Bash call denied above this plugin's own hooks and ` +
1093
- `permission grant (an untrusted workspace, or Claude Code's auto-mode classifier). Verify the ` +
1094
- `${phaseKey} artifact by hand before assuming it is missing, then either relaunch or drive the ` +
1095
- `remaining phase's kernel commands directly.`);
1096
- }
1097
-
1098
- /**
1099
- * A phase this launch did not run, said where the run is actually watched — and proved.
1100
- *
1101
- * A GATE PAUSE IS A RETURN, so the PO's answer is followed by a fresh launch of this file, and the
1102
- * runtime's progress panel is rebuilt from THAT launch's dispatches: this repo resumes off the
1103
- * artifacts under `.shapeup/`, deliberately, rather than off the runtime's own `resumeFromRunId`.
1104
- * A phase that fast-forwards used to dispatch nothing, so its box rendered `0 agents · Not started
1105
- * yet` — identical to a phase that never ran, on the one screen an operator has to decide whether a
1106
- * paused run came back correctly. Measured on a relaunch: ORIENT, WIRE and MAP SCOPES looked right
1107
- * only by accident, because a gate leg lands in each of their progress groups and earns them a tick,
1108
- * while ANALYZE — reviewed at L1b, in another group — showed as never started with its spec tree
1109
- * sitting on disk. There is no runtime call for "mark this phase complete"; a leg is the only thing
1110
- * that puts a phase in the record, so the skip has to cost one.
1111
- *
1112
- * WHICH MAKES THE LEG WORTH SPENDING ON THE ONE QUESTION THE SKIP NEVER ASKED. Every fast-forward
1113
- * decision in this file branches on a SINGLE `probe resume` snapshot taken at the top of the run,
1114
- * before ORIENT dispatches. Re-asking `--require` at the phase itself is the same oracle the
1115
- * post-condition uses, one phase over, and it converts "the state probe said so a few legs ago" into
1116
- * an attestation made where it is acted on.
1117
- *
1118
- * @param {string} gate - The gate name to report an abort under.
1119
- * @param {string} phaseKey - The phase `probe resume --require` knows.
1120
- * @param {string} phaseName - Progress group.
1121
- * @param {string} what - What is already on disk, for the narrator line.
1122
- * @returns {Promise<(object|null)>} An aborted RunReturn, or null when the artifact is really there.
1123
- */
1124
- async function fastForward(gate, phaseKey, phaseName, what) {
1125
- log(`${gate} — ${what}, fast-forwarding past it`);
1126
- const r = await attest(phaseKey, phaseName, `ff:${phaseKey}`);
1127
- if (r.exit_code === 0) return null;
1128
- // Same distinction as requirePhase: only exit 6 is the predicate genuinely answering "not there".
1129
- // Anything else means this re-attestation call itself never executed, most likely denied above
1130
- // this plugin's hooks and permission grant — not a real disagreement between the two probes.
1131
- if (r.exit_code === 6) {
1132
- return aborted(gate,
1133
- `${gate} was fast-forwarded on a state probe that reported its artifact present, and re-asking ` +
1134
- `\`probe resume --require ${phaseKey}\` at the phase itself says it is not on disk. Two readings ` +
1135
- `of "is this done" disagree, and the one taken AT the phase is the later of the two — so the run ` +
1136
- `stops here rather than building on a phase nothing can attest. Check whether the ${phaseKey} ` +
1137
- `artifact was moved or removed since this run last touched it, then relaunch.`);
1138
- }
1139
- return aborted(gate,
1140
- `${gate}'s re-attestation of "${phaseKey}" did not run to a verdict — exit_code ${r.exit_code}, not ` +
1141
- `the 0 (satisfied) or 6 (artifact absent) \`probe resume --require\` documents.` +
1142
- `${r.detail ? ` Courier reported: ${r.detail}.` : ""} The two probes are not actually in disagreement — ` +
1143
- `this one never executed, most often a Bash call denied above this plugin's own hooks and ` +
1144
- `permission grant (an untrusted workspace, or Claude Code's auto-mode classifier). Verify the ` +
1145
- `${phaseKey} artifact by hand before assuming it regressed, then relaunch.`);
1146
- }
1147
-
1148
- /**
1149
- * The positive enforcer for GATE L0.9b's launch record: the RunArgs object this run was
1150
- * configured with, kernel-written to the run's own local root, is on disk before this file
1151
- * dispatches anything past Preflight.
1152
- *
1153
- * Nothing upstream of this call can be trusted to have written it. `SKILL.md` Step 2 tells the
1154
- * orchestrating session to run `harness init run-args` right before `Workflow(...)` is invoked, but
1155
- * that is prose the session reads, not a check anything runs — and a session that launches this
1156
- * script without having done it left no trace anywhere else: the receipt, `intake.md` and
1157
- * `harness-run.md` all exist regardless, and `dialFrom()` (`kernel/probe/concurrency.mjs`) answers
1158
- * every reader with a silent default rather than an error. So the record's existence would depend
1159
- * entirely on whether a prior step of prose was followed — exactly the shape this file's own header
1160
- * exists to retire (no rule with no enforcer). `probe concurrency --require-run-args` is a presence
1161
- * check, not a measurement: it shares `dialFrom()`'s run-root resolution and file path so there is
1162
- * one definition of where the record lives, not two.
1163
- *
1164
- * @returns {Promise<(object|null)>} An aborted RunReturn, or null when the record is there.
1165
- */
1166
- async function requireLaunchRecord() {
1167
- const r = await cmd(`probe concurrency --slug ${slug} --require-run-args`, "Preflight", "launch-record");
1168
- if (r.exit_code === 0) return null;
1169
- // Same distinction requirePhase/fastForward draw: only exit 6 is the predicate genuinely
1170
- // answering "not there". Anything else means the check itself did not run.
1171
- if (r.exit_code === 6) {
1172
- return aborted("preflight",
1173
- `the run's launch record does not exist — GATE L0.9b's RunArgs (the model matrix, budgets and ` +
1174
- `every operator switch this run was typed with) was never written to disk before this launch, ` +
1175
- `so nothing downstream can be attested against what the operator actually configured. Run ` +
1176
- `\`harness init run-args\` (references/gates.md L0.9b names every flag it resolves — \`SKILL.md\` ` +
1177
- `Step 2 runs it right before this workflow launches) and relaunch.`);
1178
- }
1179
- return aborted("preflight",
1180
- `the launch-record check did not run to a verdict — exit_code ${r.exit_code}, not the 0 (present) ` +
1181
- `or 6 (absent) \`probe concurrency --require-run-args\` documents.` +
1182
- `${r.detail ? ` Courier reported: ${r.detail}.` : ""} This is NOT evidence the record is missing — ` +
1183
- `most often a Bash call denied above this plugin's own hooks and permission grant (an untrusted ` +
1184
- `workspace, or Claude Code's auto-mode classifier). Verify the run's launch record by hand — ` +
1185
- `\`harness probe concurrency --slug ${slug} --require-run-args\` — before assuming it is absent, ` +
1186
- `then relaunch.`);
1187
- }
1188
-
1189
- // The ledger's `status` field is bookkeeping, not this file's resume oracle — the fast-forward reads
1190
- // artifacts. It survives because `reduce snapshot` and the the ship report's census hook read it to tell
1191
- // a run in flight from a finished one. A lost write is a degraded digest, not a corrupted build, so
1192
- // it warns and continues — and the warning travels in the RunReturn, because a headless stdout
1193
- // carries only the final message and a diagnostic on a channel nobody reads is not a diagnostic.
1194
- const stateWarnings = [];
1195
- // Orders a receipt says were dispatched and no WorkResult ever answered — a leg that did its craft
1196
- // and never came back. Named at every close, whatever the close; never folded into "complete".
1197
- const unansweredOrders = [];
1198
- async function setRunStatus(status, phaseName) {
1199
- const r = await cmd(`probe resume --slug ${slug} --set-status ${status}`, phaseName, `status:${status}`);
1200
- if (!r.ok) {
1201
- const why = (r.detail || `exit ${r.exit_code}`).trim();
1202
- log(`RUN STATE — status="${status}" did not take: ${why}. The run continues (resume is derived from ` +
1203
- `artifacts, not from this field), but the snapshot and the the ship report's census hook will read ` +
1204
- `this run as unfinished.`);
1205
- stateWarnings.push(`status="${status}" did not take: ${why}`);
1206
- } else if (r.decision === "reopened") {
1207
- // The ledger now carries the earlier close under `prior_closes`; say so where a reader of the
1208
- // run's own log will see it, because resuming a closed run changes what its close will say.
1209
- log(`RUN STATE — this run had been closed; moving it to "${status}" reopened it. The earlier close is ` +
1210
- `kept in the ledger under prior_closes, and the run's next terminal close is recorded as its own.`);
1211
- }
1212
- }
1213
-
1214
- // A free-text reason, made safe to spell into a sub-agent's shell instruction: no quotes, no
1215
- // newlines, no backticks or `$` — every one of those risks breaking the command the sub-agent
1216
- // itself constructs from this file's own instruction text (see `cmd()`'s banner: every kernel call
1217
- // in this script is spelled into a prompt, not spawned directly). Truncated, not elided: the exit
1218
- // code and the ledger's own close_cause line are still the source of record — this is only what
1219
- // crosses the prompt boundary.
1220
- const causeArg = (s) => (String(s ?? "").replace(/[`"'$\\\n\r]/g, " ").replace(/\s+/g, " ").trim().slice(0, 300) || "no reason recorded");
1221
-
1222
- // A terminal RunReturn closes the run's own ledger — a terminal status, its cause, and a close
1223
- // timestamp, in one write. WHICH arms are terminal, and what status each closes as, is no longer
1224
- // decided here: `probe resume --close-arm <ret.status>` hands the kernel the RunReturn arm itself
1225
- // and it answers from `RUN_RETURN_CLOSE` (kernel/probe/resume.mjs), derived from the schema's own
1226
- // enum rather than a pair of statuses this file used to compare `ret.status` against by hand — a
1227
- // literal comparison that could not see a new arm go unhandled, because it never consulted the
1228
- // kernel's own list of terminal statuses at all. `gate_h` now closes the run as `escalated` — the
1229
- // breaker that tripped travels in the close's cause text, never as a status of its own; the
1230
- // tech-lead skill's own GATE H → L4 orchestration still runs the census and the ship decision, it
1231
- // just no longer finds the ledger's close fields unset when it gets there. `paused` and `ok` remain
1232
- // explicitly non-terminal — the kernel says so, this call site no longer needs to know why.
1233
- // Best-effort, the same discipline as `setRunStatus` above: a lost write degrades the trace's own
1234
- // record of why the run ended, it does not change what this return reports.
1235
- //
1236
- // A close can also come back `ok:true` and still be a degraded outcome: `closeRun`
1237
- // (kernel/probe/resume.mjs) refuses a DIFFERENT terminal status outright (that failure already hits
1238
- // the `!r.ok` branch below, exit 3), but the SAME status with a DIFFERENT cause — a run_id closed
1239
- // more than once across relaunches, e.g. `aborted` at Preflight then `aborted` again for a different
1240
- // reason after a later relaunch — is *superseded* rather than refused: `ok:true`, the new cause
1241
- // folded together with the old one on disk, and `decision:"superseded"` (the one bare token `cmd()`
1242
- // relays verbatim across the courier boundary — see its own banner) naming that outcome. Reporting
1243
- // that as a clean, silent success would be a state fact the trace needs with nothing surfacing it —
1244
- // the same shape as leaving a run's ledger open after it ended. It costs nothing this return itself
1245
- // reports — the ledger already folded the prior cause in — but the run's own RunReturn must say the
1246
- // trace is degraded.
1247
- async function closeIfTerminal(ret) {
1248
- const cause = ret.status === "aborted"
1249
- ? `${ret.aborted_at || "?"}: ${ret.reason || "no reason recorded"}`
1250
- : ret.status === "gate_h"
1251
- ? (ret.stalled ? `stalled=${ret.stalled} ` : "")
1252
- + `breaker=${ret.breaker ?? "?"} green_scopes=${Array.isArray(ret.green_scopes) ? ret.green_scopes.length : "?"} hammer_proposals=${Array.isArray(ret.hammer_proposals) ? ret.hammer_proposals.length : "?"}`
1253
- + (Array.isArray(ret.tripped_scopes) ? ` tripped_scopes=${ret.tripped_scopes.length}` : "")
1254
- // A result the single writer never applied is named at the close, not folded into "not green".
1255
- + (Array.isArray(ret.unapplied_results) && ret.unapplied_results.length ? ` unapplied_results=${ret.unapplied_results.length}` : "")
1256
- + (ret.census ? ` census=${ret.census}` : "") + (ret.l4 ? ` l4=${ret.l4}` : "") + (ret.ship_report ? ` ship_report=${ret.ship_report}` : "")
1257
- : `verdict=${ret.verdict ?? "?"} rounds=${ret.rounds_used ?? "?"} qa_findings=${ret.qa_findings ?? "?"}` + (ret.qa ? ` qa=${ret.qa}` : "")
1258
- + (ret.after ? ` after=${ret.after} breaker=${ret.breaker ?? "?"} census=${ret.census ?? "?"} cut_list=${Array.isArray(ret.cut_list) ? ret.cut_list.length : "?"}` : "");
1259
- // `--close-arm` hands the kernel the arm itself (not a status this file decided was terminal) —
1260
- // a non-terminal arm (`paused`, `ok`) still exits 0 with no "decision" key, so the branches below
1261
- // stay silent for it exactly as they did when this file's own guard returned early.
1262
- const unanswered = unansweredOrders.length ? ` unanswered_orders=${unansweredOrders.length}` : "";
1263
- const r = await cmd(`probe resume --slug ${slug} --close-arm ${ret.status} --cause "${causeArg(cause + unanswered)}"`, "Ship", `close:${ret.status}`);
1264
- if (!r.ok) {
1265
- const why = (r.detail || `exit ${r.exit_code}`).trim();
1266
- log(`RUN STATE — close(${ret.status}) did not take: ${why}. This return's own status and reason still ` +
1267
- `stand; only the ledger's own closed_at/close_cause record is degraded.`);
1268
- stateWarnings.push(`close(${ret.status}) did not take: ${why}`);
1269
- return;
1270
- }
1271
- if (String(r.decision ?? "").trim() === "superseded") {
1272
- log(`RUN STATE — close(${ret.status}) superseded an earlier close recorded under the same status ` +
1273
- `but a different cause — this run_id was closed more than once. Both causes are on the ledger's ` +
1274
- `own close_cause line; this return's trace is degraded, not corrupted.`);
1275
- stateWarnings.push(`close(${ret.status}) superseded an earlier close of this run_id — see harness-run.md's close_cause for both reasons`);
1276
- }
1277
- // The close itself took (r.ok above) — an export_warning here is `exportOnClose`
1278
- // (kernel/probe/resume.mjs) reporting it could not project this run's
1279
- // fact tables. Advisory, same as every other line in this function: the close stands, the run's
1280
- // own return says the trace is short one export rather than swallowing the fact.
1281
- if (r.export_warning) {
1282
- log(`RUN STATE — close(${ret.status}) took, but its export did not: ${r.export_warning}`);
1283
- stateWarnings.push(`close(${ret.status}): ${r.export_warning}`);
1284
- }
1285
- }
1286
- const withWarnings = async (ret) => {
1287
- await closeIfTerminal(ret);
1288
- return stateWarnings.length ? { ...ret, state_warnings: stateWarnings } : ret;
1289
- };
1290
-
1291
- // =============================================================================================
1292
- // THE RUN
1293
- // =============================================================================================
1294
- phase("Preflight");
1295
-
1296
- // CANARY — can THIS SESSION resolve a worker skill at all?
1297
- //
1298
- // `init run` already refused if the SKILL.md files are not on disk. That is the cheap half and it
1299
- // is not the failure: in the session that produced this defect the files were sitting right there
1300
- // in the repo, and the dispatch still returned "Unknown skill" because the plugin was not loaded.
1301
- // A file check passes green on both states that actually happen — installed-but-disabled, and a
1302
- // different version loaded — because those copies have all ten SKILL.md files too.
1303
- //
1304
- // So one real dispatch, before any worker is paid for. Deliberately with NO `--order`: this is
1305
- // testing name resolution, not doing work, and an order would leave a compiled order with no result
1306
- // in `orders/` that every reader of that directory would then have to know about. A canary that
1307
- // perturbs the run it is clearing is not a preflight.
1308
- //
1309
- // The evidence is not the sub-agent's report — it is the hook layer's. A Skill call whose name does
1310
- // not resolve fires no hook at all, so a decision row naming the skill is proof the name resolved,
1311
- // and `verify dispatch` reads that row. The sub-agent cannot write it, so it cannot fake it.
1312
- const canarySkill = "orient";
1313
- await agent(
1314
- `Make exactly ONE tool call and nothing else: Skill(shapeup-sdlc-plugin:${canarySkill}).\n\n` +
1315
- `Pass no arguments. Do NOT act on anything the skill returns — this is a preflight that checks ` +
1316
- `the skill can be reached, not a request to do its work. Do not use any other tool.\n\n` +
1317
- `Then report whether the call returned or errored, in one line.`,
1318
- { model: "sonnet", effort: "low", phase: "Preflight", label: `canary:${canarySkill}` },
1319
- );
1320
- const canary = await cmd(`verify dispatch --skill ${canarySkill} --within 900`, "Preflight", "canary-evidence");
1321
- if (!canary.ok) {
1322
- return await withWarnings(aborted("preflight",
1323
- `the ${canarySkill} skill did not resolve in this session — no dispatch reached the hook layer. ` +
1324
- `A run would report phases completing while the sub-agents improvised every worker's craft. ` +
1325
- `Load the plugin (\`claude --plugin-dir <repo>\`, or install and enable it) and relaunch. ` +
1326
- `(${canary.detail || `exit ${canary.exit_code}`})`));
1327
- }
1328
-
1329
- // ENVIRONMENT CANARY — do the project's own build and launch probes run in THIS session?
1330
- //
1331
- // The skill canary proves the workers resolve; it says nothing about whether the commands the round
1332
- // gate will run can. A missing package install or local SDK pointer fails the build in a second, a
1333
- // probe outside the grant is refused, a device that is not attached leaves every `[ui]` row
1334
- // ungraded — and each used to surface only at the first round gate, after planning was paid for.
1335
- // `verify build --preflight` runs the same declared steps from a sub-agent, through the same grant,
1336
- // and writes nothing. It WARNS rather than aborts: a baseline can be red for a reason the feature is
1337
- // meant to fix, and an unattended run should still say so up front rather than stop.
1338
- //
1339
- // The evidence is this command's exit code. Whether a grant "looks present" in a settings file is
1340
- // not evidence of anything: a grant can arrive by other routes, and one in the file can still be
1341
- // refused above it.
1342
- const envCanary = await cmd(`verify build --slug ${slug} --preflight`, "Preflight", "canary-env");
1343
- if (envCanary.exit_code === 1 || envCanary.exit_code === 4) {
1344
- const kind = envCanary.exit_code === 4 ? "a probe could not run (exit 2 — typically no device attached, or a missing artifact)" : "a declared step is red";
1345
- const msg = `ENVIRONMENT — preflight: ${kind}${envCanary.detail ? `: ${envCanary.detail}` : ""}. The same step runs in every ` +
1346
- `round's build gate, so fix the environment now if it is not the feature's to fix.`;
1347
- log(msg);
1348
- stateWarnings.push(msg);
1349
- } else if (envCanary.exit_code !== 0 && envCanary.exit_code !== 3) {
1350
- const msg = `ENVIRONMENT — preflight did not run (exit ${envCanary.exit_code}${envCanary.detail ? `: ${envCanary.detail}` : ""}); ` +
1351
- `nothing is known about the build or launch probes until the first round gate.`;
1352
- log(msg);
1353
- stateWarnings.push(msg);
1354
- }
1355
-
1356
- // GATE L0.9b's launch record must exist before anything past Preflight dispatches; see
1357
- // requireLaunchRecord()'s own banner for why this cannot be left to Step 2's prose alone.
1358
- const launchRecordAbort = await requireLaunchRecord();
1359
- if (launchRecordAbort) return await withWarnings(launchRecordAbort);
1360
-
1361
- phase("Orient");
1362
-
1363
- const rs = await query(`probe resume --slug ${slug} --plugin-root "${args.pluginRoot}"`, RESUME, "Orient", "resume-state");
1364
- // WHICH ORCHESTRATOR IS RUNNING. A run keeps the workflow copy it opened with — correct for a run
1365
- // in flight, and silent: a relaunch after an upgrade executes the old script, reports normally and
1366
- // closes normally, so every observation is of the previous release. Said out loud, never enforced.
1367
- if (rs.staged_workflow?.warning) {
1368
- log(`RUN STATE — ${rs.staged_workflow.warning}`);
1369
- stateWarnings.push(rs.staged_workflow.warning);
1370
- }
1371
- // A probe that produced nothing is not an EMPTY run — it is an unknown one. Treating it as empty
1372
- // would re-dispatch every phase from the top, over a run that may be in progress.
1373
- if (!rs) {
1374
- return await withWarnings(aborted("probe", "the fast-forward derivation returned no state — refusing to re-dispatch a run that may already be in progress"));
1375
- }
1376
-
1377
- // A RELAUNCH OVER A CLOSED RUN REOPENS IT FIRST. The first status write is what takes a close back,
1378
- // and a launch that fast-forwards every planning phase made that write only at BUILD, so the gates
1379
- // it crossed on the way were recorded against a run whose ledger still read closed. Bookkeeping,
1380
- // not a phase decision: the phases below still branch on artifacts alone.
1381
- if (rs.closed_status) await setRunStatus("orienting", "Orient");
1382
-
1383
- const specFolder = rs.spec_folder || `shapeup/${slug}/spec/`;
1384
- // A set the PO named at L0.5, or null: `harness compile` then resolves it from the spec for each
1385
- // evaluate order. A fixed fallback here would travel as an explicit list and switch off the judge's
1386
- // own auto-enable, which is how every run once graded spec-conformance alone.
1387
- const evalDims = rs.eval_dimensions?.length ? rs.eval_dimensions : null;
1388
- // What the judge was actually asked to grade, read back off the newest evaluate order.
1389
- let gradedDims = evalDims || [];
1390
-
1391
- // ---- ORIENT + GATE L1a ----------------------------------------------------------------------
1392
- let spikedArea = "~", spikeResult = "~", riskiest = [];
1393
- if (!rs.has_orient_artifacts) {
1394
- log(`ORIENT — dispatching (slug ${slug})`);
1395
- await setRunStatus("orienting", "Orient");
1396
- const dispatchOrient = (note = "") => worker({
1397
- skill: "orient", operation: "orient", schema: ORIENT, phase: "Orient", label: note ? "orient:again" : "orient",
1398
- payload: { pitch: rs.intake_path, breadboard: rs.breadboard_path, spec_folder: specFolder, feature: slug, stack: rs.stack },
1399
- // NAME THE FILES. "write the orient/ artifacts" was the whole instruction, while completion is
1400
- // decided by four exact filenames — so a leg that did the work and called its output
1401
- // `code-surface-map.md` and `discovered-tasks.md` aborted the run at the post-condition, having
1402
- // spiked real code and written four genuinely useful files. The skill doc states these names
1403
- // four times over; the dispatch prose stated them zero. Cheap to say, and the phase's completion
1404
- // contract belongs where the worker reads it, not only where it is enforced.
1405
- extra:
1406
- "Read and spike real code before any board exists. The phase is COMPLETE only when the run's " +
1407
- "orient directory — the one this order's substrate permits — contains all four of these, " +
1408
- "named exactly: `code-surface.md`, " +
1409
- "`discovered-seed.md`, `hill-signal.md`, and one `spike-<area>.md` (or `spike-not-needed.md` " +
1410
- "when the risk scan came back rank 0). Any other filename leaves the phase incomplete and " +
1411
- "the run aborts, however good the contents are." + note,
1412
- });
1413
- const o = await dispatchOrient();
1414
- if (o.__failed) return await withWarnings(diedAt("ORIENT", o));
1415
- const post = await requirePhase("ORIENT", "orient", "Orient", "orient", dispatchOrient);
1416
- if (post) return await withWarnings(post);
1417
- await advisory(`reduce graph --slug ${slug}`, "Orient", "graph:orient");
1418
- spikedArea = o.spiked_area; spikeResult = o.spike_result; riskiest = o.riskiest_unknowns || [];
1419
- } else {
1420
- const post = await fastForward("ORIENT", "orient", "Orient", "artifacts already on disk");
1421
- if (post) return await withWarnings(post);
1422
- }
1423
-
1424
- {
1425
- // `breadboard` travels in the block because a missing one is invisible anywhere later: every
1426
- // downstream artifact reads the same whether or not the pitch's second half reached the run.
1427
- const g = await crossGate("L1a", "Orient", ["proceed", "ask", "abort"],
1428
- { breadboard: rs.breadboard_source ?? "none", spiked_area: spikedArea, spike_result: spikeResult, riskiest_unknowns: riskiest });
1429
- if (g.stop) return await withWarnings(g.stop);
1430
- }
1431
-
1432
- // ---- COVERAGE (the requirements registry) — ahead of ANALYZE, whose ACs cite its ids ----------
1433
- //
1434
- // WHY IT RUNS AT ALL, and why here. The pitch's own requirement list is the one statement of what
1435
- // the run was asked for, and until it is extracted into `shapeup/<slug>/requirements.md` there is
1436
- // no stable key an acceptance criterion, a scope contract or a verdict can point back to. Measured
1437
- // on a full run: the planner produced the requirement edge on the board and the judge never saw
1438
- // it, because nothing on either side shared a key space. So the registry is written BEFORE the
1439
- // board, not beside it — ANALYZE's acceptance criteria cite `REQ-<n>` ids, which have to exist
1440
- // before they can be cited.
1441
- //
1442
- // IT IS NOT A PHASE, AND THAT IS THE WHOLE DESIGN OF THIS BLOCK.
1443
- // · No `phase("Coverage")`: the dispatch belongs to the planning stretch the Analyze group
1444
- // already covers (`setRunStatus("mapping")` spans it), so it never renders as an empty group
1445
- // on a relaunch — the failure a per-phase progress box would otherwise have to pay a leg to
1446
- // avoid, and there is no leg to spend here (see the next point).
1447
- // · No `requirePhase()` / no `fastForward()`: both route to `probe resume --require`, whose
1448
- // `--require` is an ENUM over `PHASE_ARTIFACT`'s keys. `coverage` is not one, so the call exits
1449
- // 2, and this file reads any exit other than 6 as "the predicate was never asked" — a dispatch
1450
- // that worked would abort the run. The skip is therefore narrated, and guarded on the bare
1451
- // `has_requirements` boolean the resume state carries.
1452
- // · Not in `PHASE_ARTIFACT` either: that map is also `nextPhase()`'s ordered list, so adding it
1453
- // would fast-forward every pre-registry run to the registry instead of to `build`.
1454
- phase("Analyze");
1455
- if (!rs.has_requirements) {
1456
- log(`COVERAGE — dispatching (slug ${slug})`);
1457
- await setRunStatus("mapping", "Analyze");
1458
- const c = await worker({
1459
- skill: "ba-pitch-analyzer", operation: "coverage", schema: PHASE_OK, phase: "Analyze", label: "coverage",
1460
- payload: { requirements: rs.intake_path, feature: slug },
1461
- extra:
1462
- "Extract the pitch's requirement clauses into the SHARED requirements registry, one atomic " +
1463
- "clause per row. A clause carrying an R-id keeps its number as REQ-<n> and records the R-id " +
1464
- "verbatim in its source cell; ids are assigned once and never renumbered.",
1465
- });
1466
- if (c.__failed) return await withWarnings(diedAt("COVERAGE", c));
1467
- await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:coverage");
1468
- } else {
1469
- log(`COVERAGE — a requirements registry is already on disk; not re-dispatching it`);
1470
- }
1471
-
1472
- // ---- ANALYZE (spec tree + board) — ahead of WIRE, which reads its use cases -------------------
1473
- if (!rs.has_spec_tree) {
1474
- log(`ANALYZE — dispatching (slug ${slug})`);
1475
- // "mapping", not "analyzing": the kernel's RUN_STATUSES enum is deliberately COARSER than this
1476
- // file's phases — one value covers ANALYZE through MAP SCOPES, the stretch where the run is
1477
- // working out the shape. `analyzing` is not a member and never was, so this call failed on every
1478
- // single run since the cutover. It is advisory, so nothing stopped; the ledger simply stayed on
1479
- // the previous phase and the snapshot under-reported where the run had got to.
1480
- await setRunStatus("mapping", "Analyze");
1481
- const { r: a } = await boardSentBack((note) => worker({
1482
- skill: "ba-pitch-analyzer", operation: "analyze", schema: PHASE_OK, phase: "Analyze", label: note ? "analyze:again" : "analyze",
1483
- payload: { pitch: rs.intake_path, breadboard: rs.breadboard_path, spec_folder: specFolder, feature: slug, lens: rs.lens, orient_dir: rs.orient_dir },
1484
- extra: "Write the spec tree and the board from the orient artifacts — do not re-scan the code." + note,
1485
- }), "analyze");
1486
- if (a.__failed) return await withWarnings(diedAt("ANALYZE", a));
1487
- const post = await requirePhase("ANALYZE", "analyze", "Analyze");
1488
- if (post) return await withWarnings(post);
1489
- await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:analyze");
1490
- } else if (!rs.has_board) {
1491
- // THE HALF THAT DOES NOT SURVIVE. The spec tree is committed; the board is per-machine and
1492
- // gitignored, and ANALYZE writes both. A run after the first on a machine — and every run in a
1493
- // fresh checkout — used to fast-forward on the committed half alone and build over no board:
1494
- // GATE L2 crossed 0/0 tasks, the evaluator could read no `covers:` clause, and the requirements
1495
- // projection said "no evidence" for a round whose static criteria all passed. The board-only
1496
- // operation regenerates it from the tree without re-deriving the tree.
1497
- log(`ANALYZE — spec tree on disk, no board: dispatching the board-only operation (slug ${slug})`);
1498
- await setRunStatus("mapping", "Analyze");
1499
- const { r: b } = await boardSentBack((note) => worker({
1500
- skill: "ba-pitch-analyzer", operation: "board", schema: PHASE_OK, phase: "Analyze", label: note ? "board:again" : "board",
1501
- payload: { spec_folder: specFolder, feature: slug, lens: rs.lens },
1502
- extra: "The spec tree is committed and FROZEN for this dispatch. Regenerate the per-machine board " +
1503
- "under the run's tasks/ directory from the use cases on disk — every acceptance criterion " +
1504
- "carrying its `(covers: REQ-…)` clause — and write nothing under the spec folder." + note,
1505
- }), "board");
1506
- if (b.__failed) return await withWarnings(diedAt("ANALYZE", b));
1507
- // The board a worker writes carries `depends_on`; its inverse (`unlocks`) and a new task's
1508
- // `status: todo` are mechanical, so the kernel writes them rather than trusting every regeneration
1509
- // to remember — a board that came back without both failed spec-lint on every task at L1b.
1510
- await advisory(`reduce board --slug ${slug} --write`, "Analyze", "board:derive");
1511
- const post = await requirePhase("ANALYZE", "analyze", "Analyze", "board");
1512
- if (post) return await withWarnings(post);
1513
- await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:board");
1514
- } else {
1515
- const post = await fastForward("ANALYZE", "analyze", "Analyze", "spec tree and board already on disk");
1516
- if (post) return await withWarnings(post);
1517
- }
1518
-
1519
- // ---- WIRE + GATE L1a.5 ------------------------------------------------------------------------
1520
- phase("Wire");
1521
- if (!rs.has_wiring_map) {
1522
- // FAIL FAST, at the orchestrator: `rs` already carries has_project_profile from the resume
1523
- // snapshot taken before ORIENT — the SAME fact solution-architect would have to discover for
1524
- // itself. Dispatching without it spends a full worker turn only for the worker to hit its own
1525
- // documented rule ("profile absent ⇒ ESCALATE, do not invent an entry point" — solution-
1526
- // architect/SKILL.md) and escalate; an escalation writes no wiring-map.md, so `has_wiring_map`
1527
- // stays false and every relaunch re-dispatches and re-escalates identically. The orchestrator
1528
- // holds the state a gate needs; it should not hand the check to the LLM it is about to pay for.
1529
- if (!rs.has_project_profile) {
1530
- return await withWarnings(aborted("WIRE",
1531
- `missing SHARED project-profile.md at ${rs.project_profile_path} — GATE L0 writes it ` +
1532
- `({schema_version:1, archetype, entry_point}; references/gates.md GATE L0 §PROFILE) before ` +
1533
- `this workflow launches. WIRE cannot resolve an entry_call_site without an entry_point to ` +
1534
- `resolve against. Write the profile, then relaunch.`));
1535
- }
1536
- log(`WIRE — dispatching (slug ${slug})`);
1537
- const dispatchWire = (note = "") => worker({
1538
- skill: "solution-architect", operation: "wire", schema: PHASE_OK, phase: "Wire", label: note ? "wire:again" : "wire",
1539
- payload: { feature: slug, spec_folder: specFolder, project_profile: rs.project_profile_path, breadboard: rs.breadboard_path },
1540
- extra: "Write the wiring map: per use case, engine → seam → entry-point call site → affordance." + note,
1541
- });
1542
- const w = await dispatchWire();
1543
- if (w.__failed) return await withWarnings(diedAt("WIRE", w));
1544
- const post = await requirePhase("WIRE", "wire", "Wire", "wire", dispatchWire);
1545
- if (post) return await withWarnings(post);
1546
- await advisory(`reduce graph --slug ${slug}`, "Wire", "graph:wire");
1547
- } else {
1548
- const post = await fastForward("WIRE", "wire", "Wire", "wiring map already on disk");
1549
- if (post) return await withWarnings(post);
1550
- }
1551
-
1552
- {
1553
- const g = await crossGate("L1a.5", "Wire", ["proceed", "ask", "abort"], { wiring_map: "written" });
1554
- if (g.stop) return await withWarnings(g.stop);
1555
- }
1556
-
1557
- // ---- MAP SCOPES + GATE L1b --------------------------------------------------------------------
1558
- phase("MapScopes");
1559
- // Both ways this list is produced now carry the same shape: `probe resume`'s `scope_files` and
1560
- // `MAPSCOPES.scopes` are each `{scope_id, path}`, so nothing downstream has to know which branch
1561
- // filled it. An entry missing either half is dropped rather than turned into a scope whose path is
1562
- // `undefined` — `compile --scope undefined` exits 2, and the attempt loop reads a non-zero exit as
1563
- // the stagnation breaker, which is a dead scope reported as a hard one.
1564
- let scopes = (rs.scope_files || []).filter((s) => s?.scope_id && s?.path)
1565
- .map((s) => ({ path: s.path, scope_id: s.scope_id }));
1566
- const mappedThisRun = scopes.length === 0;
1567
- if (scopes.length === 0) {
1568
- log(`MAP SCOPES — dispatching (slug ${slug})`);
1569
- const m = await worker({
1570
- skill: "scope-architect", operation: "map-scopes", schema: MAPSCOPES, phase: "MapScopes", label: "map-scopes",
1571
- payload: { feature: slug, breadboard: rs.breadboard_path },
1572
- // SAY THE PASS RULE, for the same reason ORIENT's filenames are named above: the rule lives in
1573
- // `verify t0` (a fixture passes iff it exits 0) and the architect never saw it. Given a contract
1574
- // that said only "commands that drive this scope end-to-end", it wrote the scope's error paths
1575
- // as bare invocations — `todo done abc # E_INVALID_INDEX, exit 1` — which cannot pass by
1576
- // construction, so four of six scopes could never go T0-green however correct their code was.
1577
- // The two that did go green were the two whose fixtures happened to be `node --test …`.
1578
- extra:
1579
- "Write the scope contracts (substrate whitelists, verification fixtures) and report the " +
1580
- "riskiest-first sequence. EVERY e2e_verification_fixture MUST EXIT 0 when the scope is " +
1581
- "correct — T0 scores any non-zero exit as a failure, so a fixture written as a bare " +
1582
- "error-path invocation can never pass and its scope can never go green. Put expected " +
1583
- "non-zero exits INSIDE a test file that itself exits 0, and name that test file as the " +
1584
- "fixture. Write `e2e_verification_fixtures` as a FRONTMATTER key holding bare command " +
1585
- "strings — a `## e2e_verification_fixtures` markdown section is not parsed, and a command " +
1586
- "with prose appended to it is not runnable. A scope whose fixtures do not parse has nothing " +
1587
- "to verify it and is refused at the board review.",
1588
- });
1589
- if (m.__failed) return await withWarnings(diedAt("MAP SCOPES", m));
1590
- const post = await requirePhase("MAP SCOPES", "map-scopes", "MapScopes");
1591
- if (post) return await withWarnings(post);
1592
- await advisory(`reduce graph --slug ${slug}`, "MapScopes", "graph:map-scopes");
1593
- scopes = m.scopes;
1594
- } else {
1595
- const post = await fastForward("MAP SCOPES", "map-scopes", "MapScopes",
1596
- `${scopes.length} scope contract(s) already on disk`);
1597
- if (post) return await withWarnings(post);
1598
- }
1599
-
1600
- // DEPENDENCY ORDER — a scope is never built beside a scope it consumes.
1601
- //
1602
- // The fan-out used to chunk scopes by a fixed width over the directory's alphabetical order. On the
1603
- // criterion-1 run that scheduled `cli-integration` — the scope that wires every other scope's module
1604
- // to the entry point — SECOND, beside the very scopes it consumes. It failed both rounds, and its
1605
- // failure was the whole of the run's failure: five of six scopes went green, `bin/todo.js` stayed a
1606
- // placeholder, and four individually T0-green command modules were unreachable.
1607
- //
1608
- // All three facts below are derived by the kernel from the contracts, so nothing here declares any
1609
- // of them. The waves fix the ORDER scopes are considered in; the dependency edges fix when each one
1610
- // is RELEASED; the exclusions forbid two scopes that may write the same declared-shared path from
1611
- // running at the same time. Everything else fans out.
1612
- const wavesFrom = (raw, list) => {
1613
- const byId = new Map(list.map((s) => [s.scope_id, s]));
1614
- const idOf = (p) => String(p).split("/").pop().replace(/\.(md|json)$/, "");
1615
- const w = (raw || []).map((g) => g.map((p) => byId.get(idOf(p))).filter(Boolean)).filter((g) => g.length);
1616
- // Every scope must appear exactly once, or the grouping is not trustworthy and one wave — today's
1617
- // behavior — is the safe answer. A scheduler that drops a scope is worse than an unscheduled one.
1618
- return w.length && w.reduce((a, g) => a + g.length, 0) === list.length ? w : [list];
1619
- };
1620
- let waves = wavesFrom(rs.scope_waves, scopes);
1621
- let rawDeps = rs.scope_deps;
1622
- let rawExclusions = rs.scope_exclusions;
1623
- if (mappedThisRun && scopes.length > 1) {
1624
- // The contracts were written THIS run, so the state probed before MAP SCOPES could not have seen
1625
- // them. One cheap re-derivation, on the fresh-run path only.
1626
- const rs2 = await query(`probe resume --slug ${slug}`, RESUME, "MapScopes", "scope-waves");
1627
- if (rs2?.scope_waves?.length) {
1628
- waves = wavesFrom(rs2.scope_waves, scopes);
1629
- rawDeps = rs2.scope_deps;
1630
- rawExclusions = rs2.scope_exclusions;
1631
- }
1632
- }
1633
- // The waves flattened ARE the build order: dependencies first, input order within a level. At a dial
1634
- // of 1 the window degrades to exactly this sequence, which is the sequential lane unchanged.
1635
- const buildOrder = waves.flat();
1636
- const excluded = withExclusions(scopeEdges(rawDeps, waves, scopes), rawExclusions, buildOrder);
1637
- const scopeReleases = excluded.edges;
1638
- if (excluded.added) {
1639
- log(`BUILD order — ${excluded.added} pair(s) of scopes may write the same declared-shared path and are ` +
1640
- `serialised against each other. Concurrent writers to one shared file lose each other's work, and ` +
1641
- `nothing downstream reports it: every fixture, lint and hook stays green over the surviving copy.`);
1642
- }
1643
- // THE CEILING IS REPORTED WHETHER OR NOT IT IS THE DIAL, and it is reported BEFORE anything is
1644
- // dispatched. A ceiling under the dial is a scope-cutting fact — the substrate the contracts declared
1645
- // does not admit the concurrency the run is paying for — and it is fixed by re-cutting scopes at the
1646
- // board review, not by anything BUILD can do. Said here, it costs a line; found later, it is a slow
1647
- // round indistinguishable from slow workers.
1648
- const ceiling = Math.min(releaseCeiling(scopeReleases, buildOrder), maxParallelScopes);
1649
- if (waves.length > 1 || excluded.added || ceiling < maxParallelScopes) {
1650
- const edgeCount = [...scopeReleases.values()].reduce((a, s) => a + s.size, 0);
1651
- log(`BUILD order — ${waves.length} dependency wave(s): ${waves.map((w) => w.map((s) => s.scope_id).join("+")).join(" → ")}` +
1652
- ` · ${edgeCount} release edge(s) · at most ${ceiling} scope(s) can be open at once` +
1653
- `${ceiling < maxParallelScopes ? ` (the window is ${maxParallelScopes}; the substrate the contracts declared is what caps it, not the dial)` : ""}`);
1654
- }
1655
-
1656
- // Advisory lints at L1b. spec-lint is hard — a substrate overlap makes parallel builds unsafe, and
1657
- // a breadboard Place with no screen builds the wrong thing; trace-lint stays advisory until
1658
- // `covers:` is populated; hill-derive is a projection. The abort names no cause of its own: spec-lint
1659
- // has more than one kind of red, and the detail says which.
1660
- const specLint = await cmd(`verify spec --slug ${slug}`, "MapScopes", "spec-lint");
1661
- if (!specLint.ok) {
1662
- return await withWarnings(aborted("L1b", `spec-lint reported red findings before BUILD: ${specLint.detail || `exit ${specLint.exit_code}`}`));
1663
- }
1664
- await advisory(`verify trace --slug ${slug} --quiet`, "MapScopes", "trace-lint");
1665
- await advisory(`reduce hill --slug ${slug}`, "MapScopes", "hill-derive");
1666
-
1667
- {
1668
- const g = await crossGate("L1b", "MapScopes", ["proceed", "ask", "abort"], { scopes: scopes.map((s) => s.scope_id) });
1669
- if (g.stop) return await withWarnings(g.stop);
1670
- }
1671
-
1672
- // =============================================================================================
1673
- // ROUNDS of BUILD → GATE L2 → EVAL → GATE L3, bounded by maxRounds.
1674
- //
1675
- // The round loop is inlined here rather than dispatched round-by-round because a round dispatched
1676
- // once can attempt every scope from attempt 1, while an outer loop must survive a mid-BUILD kill
1677
- // and resume without re-work. So before opening a scope's attempt loop this file asks whether THIS
1678
- // ROUND already has a green T0 verdict for it on disk, and skips the scope entirely when it does.
1679
- // =============================================================================================
1680
- const lastEval = rs.eval_rounds_done?.length ? Math.max(...rs.eval_rounds_done) : 0;
1681
- let round = lastEval + 1;
1682
- let verdict = null;
1683
- // DERIVED FROM DISK AT EVERY LAUNCH, never accumulated only in memory. These two lists ARE GATE H's
1684
- // census: `scope-hammer` is dispatched with `hammer_proposals`, and AGENTS.md makes a scope that
1685
- // exhausted its attempt budget a queued GATE H proposal. As bare `const []` they were emptied by the
1686
- // one event that most needs them intact — a gate PAUSE is a `return`, so the PO's answer is followed
1687
- // by a FRESH LAUNCH whose accumulators start empty and whose round loop is then fast-forwarded past
1688
- // the rounds that filled them. The PO was handed an empty cut list for a run that had genuinely
1689
- // exhausted scopes. Unattended (`ci`) runs never pause, which is why no archived trace shows it.
1690
- //
1691
- // This file has now paid for the same class three times — `findings` in the temporal dead zone and
1692
- // `payload.bugs` "lived in a variable, which a relaunch between two rounds resets to empty" are the
1693
- // other two. The cure is the same each time and it is not a bigger variable: re-derive the fact.
1694
- const allGreen = [];
1695
- const allHammer = [];
1696
- const allUnapplied = []; // results on disk the single writer never applied, run-wide
1697
- // OUTSIDE the loop, because its whole purpose is to cross a round boundary: round r's verdict is
1698
- // what round r+1 has to act on. Declared inside, it was in the temporal dead zone at the BUILD that
1699
- // needed it — a runtime error no static check can see, since nothing but a real second round ever
1700
- // reaches that line.
1701
- let findings = [];
1702
-
1703
- // THE ROUND THE RELAUNCH IS RESUMING INTO MAY ALREADY HAVE PASSED, and until this leg existed the
1704
- // run had no way to find out. `round` above opens at the round AFTER the last EVAL — correct when
1705
- // that EVAL returned FAIL, and the only reading available. But a pause at GATE L3, at QA or at
1706
- // GATE H happens strictly AFTER `results/evaluate-r<N>.json` is written, so the relaunch opened
1707
- // round N+1, asked the graph which scopes were green IN ROUND N+1, was told none, and re-dispatched
1708
- // every scope through the attempt ratchet before running a second EVAL — over a verdict of PASS
1709
- // sitting on disk. It cost a round of `maxRounds`, a full build fan-out, and a second judgement free
1710
- // to return FAIL where the first passed. The PO answered a gate and the run went backwards.
1711
- //
1712
- // The verdict is an ARTIFACT, and `probe eval` reads it out of the WorkResult `reduce ingest` wrote
1713
- // — the same probe, and deliberately not the dispatching agent's account of it, that L3 branches on
1714
- // below. This is the fast-forward the planning phases have always had, applied to the one phase that
1715
- // did not have it: a completed EVAL is skipped exactly like a completed ANALYZE.
1716
- if (lastEval) {
1717
- phase("Eval");
1718
- const prior = await query(`probe eval --slug ${slug} --round ${lastEval}`, EVAL_VERDICT, "Eval", `ff:eval-r${lastEval}`);
1719
- if (prior?.ok && prior.overall === "PASS") {
1720
- verdict = "pass";
1721
- if (Array.isArray(prior.dimensions) && prior.dimensions.length) gradedDims = prior.dimensions;
1722
- round = lastEval;
1723
- log(`EVAL — round ${lastEval} already returned PASS on disk, fast-forwarding past the build/eval loop`);
1724
- }
1725
- }
1726
-
1727
- // `verdict !== "pass"` is the same exit the `break` below takes when a round passes — stated here as
1728
- // well so a RESUMED run reaches it, since the break belongs to a loop iteration a relaunch never ran.
1729
- while (verdict !== "pass" && round <= maxRounds) {
1730
- phase("Build");
1731
- await setRunStatus("building", "Build");
1732
-
1733
- // The wall-clock breaker, opt-in, checked at each round boundary. Exit 6 is "tripped": route to
1734
- // GATE H and ship what is green, never kill the run from outside.
1735
- const budget = await cmd(`verify budget --slug ${slug} --strict`, "Build", `budget:r${round}`);
1736
- if (budget.exit_code === 6) {
1737
- await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
1738
- return await settleAtGateH({ status: "gate_h", breaker: "deadline", unapplied_results: allUnapplied, hammer_proposals: allHammer, green_scopes: allGreen });
1739
- }
1740
-
1741
- log(`BUILD round ${round} — ${scopes.length} scope(s), up to ${maxParallelScopes} at once, attempt budget ${attemptBudget}`);
1742
- const roundGreen = [], roundHammer = [];
1743
-
1744
- // ONE bounded query opens the round, instead of one probe per scope. The graph is a projection of
1745
- // the same verdict artifacts `probe t0` reads, so this is the identical fact asked once — which is
1746
- // the difference between a read model and a directory walk.
1747
- const g = await query(`reduce graph --slug ${slug} --subgraph run`, SUBGRAPH, "Build", `graph:r${round}`);
1748
- const alreadyGreen = new Set(g?.green_scopes_by_round?.[String(round)] || []);
1749
- if (alreadyGreen.size) log(`BUILD r${round} — ${alreadyGreen.size} scope(s) already green in the graph, skipping them`);
1750
-
1751
- // REBUILD THE CENSUS FROM THE GRAPH THIS QUERY JUST RETURNED. Everything a prior launch learned is
1752
- // already on disk: a scope green in ANY round is green work, and a scope the run has touched but
1753
- // never got green is what GATE H has to be shown. One query, already made, re-read — so a relaunch
1754
- // after a paused gate carries the same census the launch that paused it would have.
1755
- const greenEver = new Set(Object.values(g?.green_scopes_by_round || {}).flat());
1756
- for (const sid of greenEver) if (!allGreen.includes(sid)) allGreen.push(sid);
1757
- for (const sid of (g?.scopes || [])) {
1758
- if (!greenEver.has(sid) && !allHammer.includes(sid) && scopes.some((x) => x.scope_id === sid)) allHammer.push(sid);
1759
- }
1760
-
1761
- // SCOPES FAN OUT. A scope contract is the definition of an independent subtask — disjoint
1762
- // substrate, own fixtures, own ratchet — so the loop that ran them one at a time was leaving the
1763
- // whole point of the contract on the floor. `pipeline()` has NO barrier between its stages: a
1764
- // fast scope is being confirmed while a slow one is still on attempt 3. One pipeline per scope,
1765
- // so that property is now GLOBAL rather than per group — nothing waits on a group boundary, and
1766
- // the window refills the instant any leg settles.
1767
- //
1768
- // Three stages, because each answers a different question about the same scope:
1769
- // check — is it already green on disk from a killed round? (resume, no re-work)
1770
- // build — the attempt ratchet, inside the worker's own shell
1771
- // confirm — MEASURED, NOT CLAIMED: the worker says green; the T0 artifact has to agree.
1772
- // A green with no artifact on disk is a claim, and the evaluator that must cite that
1773
- // artifact would find nothing.
1774
- // NEVER RETURN `null` FROM A STAGE TO MEAN "carry on". The runtime reads a null stage result as
1775
- // DROP THIS ITEM and skips its remaining stages — measured directly, not inferred:
1776
- //
1777
- // stage1 → null ⇒ stage2 ran 0/3 times, settled = [NULL, NULL, NULL]
1778
- // stage1 → {…} ⇒ stage2 ran 3/3 times
1779
- //
1780
- // This stage used to return `null` for "not green yet — stage 2, please build it", which meant
1781
- // every scope that was not ALREADY green was dropped before `buildScope` could run. On a fresh
1782
- // run that is every scope, so the round ended with 0 green and 6 queued, the inner breaker tripped
1783
- // and the run returned `gate_h` having dispatched no builder at all. BUILD could never dispatch;
1784
- // the failure looked exactly like six genuinely hard scopes.
1785
- const settled = await scheduleScopes(buildOrder, scopeReleases, maxParallelScopes, async (scope) => {
1786
- const done = await pipeline(
1787
- [scope],
1788
- async (s) => (alreadyGreen.has(s.scope_id)
1789
- ? { scope_id: s.scope_id, green: true, resumed: true }
1790
- : { scope_id: s.scope_id, pending: true }), // not green yet → stage 2 builds it
1791
- async (pre, s) => (pre?.pending ? buildScope(s, round) : pre),
1792
- async (res, s) => {
1793
- if (!res) return res;
1794
- // THE SINGLE WRITER IS ASKED FIRST, WHATEVER THE LEG SAID. This question used to sit behind
1795
- // the green checks below, and the state it exists to catch — a result on disk that nothing
1796
- // applied — was reachable only for a scope that was already fully green. Measured live: a
1797
- // leg reported green with no T0 verdict at all, the T0 re-read correctly returned "not
1798
- // green", the round returned before this line, and the result — six tasks, two discoveries
1799
- // — was never read by anyone. A dead leg (`__failed`) can have left a result too. So every
1800
- // settled scope is asked, and what it answers travels on the result to the close, where an
1801
- // unapplied result is named rather than folded into "not green". Application stays gated
1802
- // on the green checks: ingest ticks acceptance boxes, and a result T0 never measured must
1803
- // not mark work green — the repair below is for a scope that IS green.
1804
- const applied = await query(`probe leg --slug ${slug} --scope ${s.scope_id} --round ${round}`,
1805
- LEGCHECK, "Build", `legcheck:${s.scope_id}-r${round}`);
1806
- const unapplied = applied?.closed ? [] : (applied?.unapplied || []);
1807
- if (res.__failed) return unapplied.length ? { ...res, unapplied } : res;
1808
- if (!res.green) return unapplied.length ? { ...res, unapplied } : res;
1809
- // THE T0 RE-READ IS SKIPPED FOR A RESUMED SCOPE; THE LEG CHECK BELOW IS NOT.
1810
- //
1811
- // `resumed` means the graph already reported this scope green for this round, so re-reading
1812
- // its T0 artifact would only confirm what the resume derivation just read. But these two
1813
- // stages answer DIFFERENT questions, and the second one is precisely the question a resumed
1814
- // scope is most likely to fail: a relaunch happens because the previous launch DIED, and a
1815
- // leg that died between writing its result and running `reduce ingest` leaves exactly this
1816
- // state — green T0 on disk, result never applied, board still `pending`. The short-circuit
1817
- // used to cover both stages, so the one scope class known to be at risk was the one class
1818
- // nobody asked, and the late-ingest repair nine lines below could never fire for it.
1819
- if (!res.resumed) {
1820
- const confirmed = await query(`probe t0 --slug ${slug} --scope ${s.scope_id} --round ${round}`,
1821
- T0CHECK, "Build", `t0confirm:${s.scope_id}-r${round}`);
1822
- if (!confirmed?.green) {
1823
- log(`BUILD r${round} — ${s.scope_id} reported green but no T0 verdict is on disk for this ` +
1824
- `round; treating it as not green (the evaluator cites that artifact, and it is not there).`);
1825
- return { ...res, green: false, reason: "reported green with no T0 verdict artifact on disk", ...(unapplied.length ? { unapplied } : {}) };
1826
- }
1827
- }
1828
- // AND ITS RESULT HAS TO HAVE REACHED THE BOARD. A green T0 says the worker's fixtures ran and
1829
- // passed; it says nothing about whether the WorkResult was applied, and this stage used to ask
1830
- // only the first question. Measured on a live run: a leg wrote its code, a green verdict, a
1831
- // kept trial row and its WorkResult, then skipped step 3 of its own script. It reported green,
1832
- // this stage re-verified the T0 artifact, agreed, and the round walked on with the scope's task
1833
- // still `pending` and zero acceptance criteria ticked — the board GATE L2 reads as 100%
1834
- // disagreeing with a scope that was genuinely finished.
1835
- //
1836
- // The evidence is the leg-completion row, because `reduce ingest` writes it: its presence
1837
- // proves the writer ran, and it is not something the leg can assert about itself.
1838
- for (const orderPath of unapplied) {
1839
- // INGESTED HERE RATHER THAN FAILED. The result is on disk and valid — re-running the leg
1840
- // would pay a whole attempt again for work already done. Only the single writer writes
1841
- // shared state, and that writer is this command; which step invokes it is not the invariant.
1842
- log(`BUILD r${round} — ${s.scope_id} finished without applying its own result. Ingesting it ` +
1843
- `here: ${orderPath}. The leg skipped its ingest step, so the board did not see work that ` +
1844
- `is on disk and T0-green.`);
1845
- // QUOTED, and that is not cosmetic. The PreToolUse envelope gate scans the `Agent` prompt as
1846
- // well as `Skill` args, so a bare order path interpolated into a sub-agent's prompt is read
1847
- // together with the trailing quote of the surrounding literal and the whole dispatch is
1848
- // denied — measured once already, on a path threaded through a prompt exactly like this one.
1849
- await advisory(`reduce ingest --order "${orderPath}"`, "Build", `late-ingest:${s.scope_id}-r${round}`);
1850
- }
1851
- if (applied && !applied.closed && !(applied.unapplied || []).length && applied.results_total === 0) {
1852
- // Green T0, no result envelope at all: the two records disagree about whether a leg ran, and
1853
- // the round must not resolve that by preferring the one that says yes.
1854
- log(`BUILD r${round} — ${s.scope_id} has a green T0 verdict and no WorkResult on disk for ` +
1855
- `this round; treating it as not green.`);
1856
- return { ...res, green: false, reason: "green T0 verdict with no WorkResult envelope" };
1857
- }
1858
- return res;
1859
- },
1860
- );
1861
- return done[0];
1862
- });
1863
-
1864
- for (const [i, res] of settled.entries()) {
1865
- const scopeId = buildOrder[i].scope_id;
1866
- for (const p of (res?.unapplied || [])) {
1867
- if (!allUnapplied.includes(p)) allUnapplied.push(p);
1868
- log(`BUILD r${round} — ${scopeId} has a result on disk nothing applied: ${p}`);
1869
- }
1870
- // A dead builder is a SPENT ATTEMPT, not a dead run: the scope goes to GATE H's census and
1871
- // the round continues. Killing the run here would discard every other scope's green work.
1872
- if (!res || res.__failed) {
1873
- log(`BUILD r${round} — ${scopeId} lost its worker: ${res?.__failed || "no result"}`);
1874
- roundHammer.push(scopeId);
1875
- } else if (res.green) {
1876
- roundGreen.push(res.scope_id || scopeId);
1877
- } else {
1878
- roundHammer.push(res.scope_id || scopeId);
1879
- }
1880
- }
1881
-
1882
- for (const sid of roundGreen) if (!allGreen.includes(sid)) allGreen.push(sid);
1883
- // A scope that went green this round is no longer a cut candidate, however it was queued earlier.
1884
- for (const sid of roundGreen) { const i = allHammer.indexOf(sid); if (i !== -1) allHammer.splice(i, 1); }
1885
- for (const sid of roundHammer) if (!allHammer.includes(sid) && !allGreen.includes(sid)) allHammer.push(sid);
1886
-
1887
- // NOTHING GREEN AND SOMETHING QUEUED → GATE H. This used to return the literal `inner` as its breaker, and the
1888
- // protocol's INNER breaker is the per-scope attempt budget, which "queues a proposal, never blocks
1889
- // the round" — so the close named a breaker the attested census flatly denied (one attempt spent
1890
- // of five, `tripped: false`), and the operator was told a scope had exhausted its attempts after
1891
- // it used one. The word is now earned: `probe attempts` — the one derivation built so the census
1892
- // and the breaker cannot disagree — is asked for every queued scope, and the return names
1893
- // `attempt_budget` only for the scopes it says tripped, `none` when the round simply stalled.
1894
- if (roundGreen.length === 0 && roundHammer.length > 0) {
1895
- const tripped = [];
1896
- for (const sid of roundHammer) {
1897
- const census = await query(`probe attempts --slug ${slug} --scope "${sid}" --round ${round} --attempt-budget ${attemptBudget}`,
1898
- ATTEMPTS, "Build", `census:${sid}-r${round}`);
1899
- if (census?.tripped) tripped.push(sid);
1900
- }
1901
- await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
1902
- return await settleAtGateH({
1903
- status: "gate_h", breaker: tripped.length ? "attempt_budget" : "none", stalled: "no_green",
1904
- tripped_scopes: tripped, unapplied_results: allUnapplied,
1905
- hammer_proposals: allHammer, green_scopes: allGreen,
1906
- });
1907
- }
1908
-
1909
- // ---- ROUND BUILD GATE — the feature builds and launches, measured before anyone is asked --------
1910
- //
1911
- // T0 is per scope and inside the scope's substrate; nothing in it proves the FEATURE compiles or
1912
- // starts. Measured on a live mobile run: 30/30 T0 trials green on the first try (TypeScript
1913
- // stand-ins, structural greps, a suite wrapper that never compiled its sources) while the
1914
- // ledger's own `run_cmd` failed, and three rounds of EVAL then graded a blank screen because
1915
- // nothing in the loop had ever installed or launched the app. `verify build` runs the ledger's
1916
- // `run_cmd`, then the profile's `build_probe` and `launch_probe`, and writes one artifact
1917
- // `reduce hill` and `harness compile` both read — so a red gate moves no dot downhill and becomes
1918
- // the next round's bug list without this script carrying anything across the round boundary.
1919
- //
1920
- // Exit 3 is "nothing declared" — the honest state of a run whose L0 pinned no run command and
1921
- // whose profile names no probe — and it is logged, not treated as green. Any other non-0/1 exit
1922
- // means the gate itself did not run, and the run says so rather than inferring a verdict.
1923
- const gate = await cmd(`verify build --slug ${slug} --round ${round}`, "Build", `build-gate:r${round}`);
1924
- let buildGate = "green";
1925
- if (gate.exit_code === 1) {
1926
- buildGate = "red";
1927
- log(`BUILD GATE r${round} — RED${gate.detail ? `: ${gate.detail}` : ""}. EVAL will not run over a feature ` +
1928
- `that does not build or launch; the failing step is compiled into round ${round + 1}'s orders as bugs.`);
1929
- } else if (gate.exit_code === 3) {
1930
- buildGate = "undeclared";
1931
- log(`BUILD GATE r${round} — nothing declared: no run_cmd in the run ledger and no build_probe or ` +
1932
- `launch_probe in project-profile.md. EVAL runs over an unproven build; pin them at GATE L0.`);
1933
- } else if (gate.exit_code !== 0) {
1934
- buildGate = "unknown";
1935
- log(`BUILD GATE r${round} — the gate itself did not run (exit ${gate.exit_code}` +
1936
- `${gate.detail ? `: ${gate.detail}` : ""}). Treated as undeclared, not as green.`);
1937
- }
1938
-
1939
- // Trace-lint runs a SECOND time here, and this is the pass that can see anything. At L1b the
1940
- // scopes' files do not exist yet, so per-scope reachability measures an empty set and its warning
1941
- // cannot fire. After the round's build gate the code is on disk, which is exactly when "this
1942
- // scope's fixture is green and none of its code is in the compiled set" becomes a checkable fact
1943
- // — the shape that once left three scopes green while their files never compiled. Advisory, like
1944
- // the L1b pass: it overwrites the same projection with a later and better-informed one.
1945
- await advisory(`verify trace --slug ${slug} --quiet`, "Build", `trace-lint:r${round}`);
1946
- await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
1947
- {
1948
- const g = await crossGate("L2", "Build", ["proceed", "ask", "abort"],
1949
- { round, green_scopes: roundGreen, hammer_proposals: roundHammer, build_gate: buildGate });
1950
- if (g.stop) return await withWarnings(g.stop);
1951
- }
1952
-
1953
- // ---- EVAL — exactly one feature-level pass per round (the single-judge invariant) ------------
1954
- phase("Eval");
1955
- await setRunStatus("evaluating", "Eval");
1956
- if (buildGate === "red") {
1957
- // A red gate outranks --no-eval: skipping the judge is the operator's call, but the build failing
1958
- // is a measured fact, and a round that does not compile has nothing for anyone to pass.
1959
- verdict = "fail";
1960
- findings = [];
1961
- log(`EVAL r${round} — not dispatched: the round build gate is red. The judge grades a running ` +
1962
- `feature; this one does not build or launch. Round ${round + 1} fixes the gate's failing step.`);
1963
- } else if (args.noEval) {
1964
- log("EVAL — skipped (--no-eval)");
1965
- // references/protocol.md's own words for this, twice: a run --no-eval ships records
1966
- // `not-evaluated` — "recorded plainly — never silently upgraded [to `pass`]". Nothing verified
1967
- // the feature beyond task-executor's own per-AC evidence checks, and the report this run
1968
- // freezes at GATE L4 has to say that as plainly as the gate block already does.
1969
- verdict = "not-evaluated";
1970
- } else {
1971
- const { r: e, v: ev } = await sendBackOnce(
1972
- (note) => worker({
1973
- skill: "spec-evaluator", operation: "evaluate", schema: EVAL, phase: "Eval",
1974
- label: note ? `eval:r${round}:again` : `eval:r${round}`, model: evalModel, round,
1975
- // No `t0_artifacts` here, deliberately: `harness compile` derives them from the round's green
1976
- // T0 verdicts on disk, for every lane — this script could only name paths it was told about.
1977
- // The same goes for `build_gate` and `launch_cmd`: compile reads them off the round's gate
1978
- // artifact and the project profile, so the judge gets the launch evidence without this script
1979
- // having to carry it.
1980
- payload: { ...(evalDims ? { dimensions: evalDims } : {}), run_cmd: rs.run_cmd, round },
1981
- extra: "Evaluate the running feature against every acceptance criterion and Done-when. One feature-level pass; cite every artifact the order lists under t0_artifacts, re-hashing each yourself." + note,
1982
- }),
1983
- // The pass/fail branch is decided from the WorkResult on disk, not from the dispatching
1984
- // agent's own summary of it (`e.overall`) — see EVAL_VERDICT's comment for why.
1985
- (suffix) => query(`probe eval --slug ${slug} --round ${round}`, EVAL_VERDICT, "Eval", `verdict:r${round}${suffix}`),
1986
- // A verdict the kernel refused — grouped rows, a missing citation — is correctable, not a dead judge.
1987
- (v) => !v.ok && !!v.overall && !!v.reason,
1988
- `EVAL r${round}`,
1989
- );
1990
- if (e.__failed) return await withWarnings(diedAt("L3", e));
1991
- if (!ev) return await withWarnings(diedAt("L3", nullFail(`verdict:r${round}`)));
1992
- // A round with no verdict to act on is NOT a dead worker. An evaluator that refused the round
1993
- // wrote a result saying why, and `probe eval` carries it as `reason`; reported as "died after
1994
- // retries", the one sentence naming the cause stayed in a file nobody was pointed at.
1995
- if (!ev.ok || !ev.overall) {
1996
- return await withWarnings(diedAt("L3", { __failed: `verdict:r${round}: no verdict this round can act on — ${ev.reason || `status ${ev.status || "unknown"}`}` }));
1997
- }
1998
- verdict = ev.overall === "PASS" ? "pass" : "fail";
1999
- if (Array.isArray(ev.dimensions) && ev.dimensions.length) gradedDims = ev.dimensions;
2000
- findings = e.findings || [];
2001
- }
2002
-
2003
- // ---- REFUTE WAVE (opt-in) — one independent skeptic per FAIL finding, BEFORE it costs a whole
2004
- // fix round. Perspective-diverse by prompt: the refuter is told to try to refute and to default to
2005
- // "real" when it cannot. A pure addition; off unless args.adversarialVerify.
2006
- if (verdict === "fail" && args.adversarialVerify && findings.length) {
2007
- phase("Refute");
2008
- const checks = await parallel(findings.map((f) => () =>
2009
- agent(
2010
- `A single judge marked this acceptance criterion FAILED for slug "${slug}":\n` +
2011
- ` criterion: ${f.criterion}\n evidence: ${f.evidence}\n\n` +
2012
- `Independently try to REFUTE the failure by exercising the running feature yourself. ` +
2013
- `Return refuted=true ONLY if you can show it actually passes; otherwise refuted=false.`,
2014
- { model: evalModel, phase: "Refute", label: `refute:${f.id}`, schema: REFUTATION },
2015
- )));
2016
- const overturned = new Set(checks.filter(Boolean).filter((c) => c.refuted).map((c) => c.id));
2017
- if (overturned.size) log(`REFUTE — ${overturned.size}/${findings.length} FAIL finding(s) overturned by an independent skeptic`);
2018
- if (overturned.size === findings.length) {
2019
- verdict = "pass";
2020
- log("REFUTE — every finding was overturned; treating this round as PASS");
2021
- }
2022
- }
2023
-
2024
- await advisory(`reduce graph --slug ${slug}`, "Eval", `graph:eval-r${round}`);
2025
- await advisory(`reduce hill --slug ${slug}`, "Eval", "hill-derive");
2026
- const g3 = await crossGate("L3", "Eval", ["loop", "stop", "ask"], { round, verdict, build_gate: buildGate });
2027
- if (g3.stop) return await withWarnings(g3.stop);
2028
-
2029
- // "not-evaluated" (--no-eval) ships exactly like "pass" — protocol.md: the run "goes straight to
2030
- // SHIP" once EVAL is skipped, never spends another round waiting on a verdict nobody is producing.
2031
- if (verdict === "pass" || verdict === "not-evaluated") break; // → QA → GATE H → ship
2032
- if (g3.decision === "stop" || round >= maxRounds) {
2033
- return await settleAtGateH({ status: "gate_h", breaker: "outer", unapplied_results: allUnapplied, hammer_proposals: allHammer, green_scopes: allGreen });
2034
- }
2035
- round += 1;
2036
- }
2037
-
2038
- if (verdict !== "pass" && verdict !== "not-evaluated") {
2039
- return await settleAtGateH({ status: "gate_h", breaker: "outer", unapplied_results: allUnapplied, hammer_proposals: allHammer, green_scopes: allGreen });
2040
- }
2041
-
2042
- // ---- QA (post-PASS, pre-ship) — a level-up, never a gate. `--no-qa` answers it "skip". --------
2043
- phase("QA");
2044
- let qaFindings = 0;
2045
- const qaG = await crossGate("QA", "QA", ["run", "skip", "ask"], { round, verdict });
2046
- if (qaG.stop) return await withWarnings(qaG.stop);
2047
- const qaRan = !args.noQa && qaG.decision === "run";
2048
- // What QA amounted to, from the hunt's own record: `run` only when a charter ran. The QA gate's `run`
2049
- // is the decision to dispatch, and a dispatched hunt that drove nothing is `not-hunted`.
2050
- let qaState = "skipped";
2051
- if (qaRan) {
2052
- const { r: q, v: hv } = await sendBackOnce(
2053
- (note) => worker({
2054
- skill: "qa-edge-hunter", operation: "hunt", schema: QA_REPORT, phase: "QA", label: note ? "hunt:again" : "hunt", model: qaModel,
2055
- payload: { feature: slug, spec_folder: specFolder, app_url: rs.app_url, round },
2056
- extra: "Exploratory hunt over the shipped feature. No verdict and no score — findings only, each with a repro." + note,
2057
- }),
2058
- (suffix) => query(`probe hunt --slug ${slug}`, HUNT_OUTCOME, "QA", `hunt-outcome${suffix}`),
2059
- (v) => !v.ok && v.status === "done" && !!v.reason,
2060
- "QA hunt",
2061
- );
2062
- // QA is a level-up: losing its worker costs the findings, not the run.
2063
- if (q.__failed) log(`QA — the hunt lost its worker: ${q.__failed}. Shipping without QA findings.`);
2064
- else {
2065
- qaFindings = hv && Number.isInteger(hv.findings) ? hv.findings : q.findings_count;
2066
- qaState = hv?.qa === "run" ? "run" : "not-hunted";
2067
- }
2068
- }
2069
-
2070
- // ---- GATE H — delegated to scope-hammer (census, baseline comparison, cut list) ----------------
2071
- phase("Ship");
2072
- const h = await worker({
2073
- skill: "scope-hammer", operation: "hammer", schema: HAMMER, phase: "Ship", label: "hammer",
2074
- payload: { feature: slug, qa_findings: qaFindings, hammer_proposals: allHammer },
2075
- extra: HAMMER_EXTRA,
2076
- });
2077
- if (h.__failed) return await withWarnings(diedAt("H", h));
2078
- if (h.verdict === "cannot-ship") {
2079
- return await withWarnings(aborted("H", `scope-hammer: CANNOT SHIP — ${h.cut_list.join(", ") || "a must-have failed"}`));
2080
- }
2081
-
2082
- {
2083
- const g = await crossGate("H", "Ship", ["accept-cut-list", "ship-all", "ask"], { verdict: h.verdict, cut_list: h.cut_list });
2084
- if (g.stop) return await withWarnings(g.stop);
2085
- }
2086
-
2087
- // The frozen report's own verdict line has to say what actually happened — a `--no-eval` run
2088
- // verified nothing beyond task-executor's per-AC checks, and `reduce ship` already accepts
2089
- // "not-evaluated" as a real verdict (its own usage string, and `generate()`'s no-artifact
2090
- // default). Hardcoding PASS here is exactly the silent upgrade protocol.md's Rules forbid.
2091
- const shipVerdict = verdict === "not-evaluated" ? "not-evaluated" : "PASS";
2092
- const ship = await cmd(`reduce ship --slug ${slug} --verdict ${shipVerdict} --qa ${qaState}`, "Ship", "ship-report");
2093
- // GATE L4 HAS A CALL SITE. It used to be a line of prose in the tech lead's references — "resolve
2094
- // the gate itself before any of the above" — and a run reached its ship decision with no ledger row
2095
- // for it. The resolver narrows `ship` on the census artifact the hammer just wrote.
2096
- {
2097
- const g = await crossGate("L4", "Ship", ["ship", "hold", "ask"], { verdict: shipVerdict, census: h.verdict, cut_list: h.cut_list });
2098
- if (g.stop) return await withWarnings(g.stop);
2099
- if (g.decision === "hold") return await withWarnings({ status: "aborted", aborted_at: "L4", reason: `GATE L4 answered "hold" (census ${h.verdict}, verdict ${shipVerdict})` });
2100
- }
2101
- await advisory(`report export --slug ${slug}`, "Ship", "export-run");
2102
- // The run's own concurrency, printed once where the records are complete and before the next run
2103
- // supersedes the trace. It is a projection over `receipts/dispatch.jsonl` and `legs.jsonl`, so it
2104
- // asserts nothing and gates nothing — and when those records cannot support a figure it says so
2105
- // rather than printing a plausible one.
2106
- await advisory(`probe concurrency --slug ${slug} --format table`, "Ship", "concurrency");
2107
- await setRunStatus("shipped", "Ship");
2108
-
2109
- // The dimensions this run did NOT evaluate, so GATE L4 can say what "shipped" does not cover.
2110
- const ALL_DIMS = ["spec-conformance", "tdd-surface", "integration", "completeness",
2111
- "test-surface-conformance", "security", "performance"];
2112
-
2113
- return await withWarnings({
2114
- status: "shipped",
2115
- // The real verdict this run reached — "pass" or, over a --no-eval run, "not-evaluated". GATE L4's
2116
- // own sign-off block reads this field verbatim (SKILL.md Step 4); hardcoding "pass" here told a
2117
- // human answering that gate the run was graded when it never was.
2118
- verdict,
2119
- rounds_used: round,
2120
- dims_evaluated: gradedDims,
2121
- dims_not_evaluated: ALL_DIMS.filter((d) => !gradedDims.includes(d)),
2122
- qa_findings: qaFindings,
2123
- qa: qaState,
2124
- report: REPORT_PATH,
2125
- });
2126
-
2127
- // =============================================================================================
2128
- // buildScope — one scope's full attempt ratchet, as a single worker leg.
2129
- //
2130
- // The ratchet lives inside the worker's own shell rather than in this control script because each
2131
- // attempt is implement → `harness verify t0`, and both halves need a real filesystem and a real
2132
- // git. The worker reports only the outcome the round loop branches on.
2133
- // =============================================================================================
2134
- async function buildScope(scope, roundNo) {
2135
- // THE VERDICT'S BUGS REACH THIS LEG THROUGH ITS ORDER, NOT THROUGH THIS FUNCTION.
2136
- //
2137
- // `WorkOrderPayload.bugs` is "the EVAL report's bug entries for this task — touch nothing else",
2138
- // and AGENTS.md states the regression rule as "bugs + full Test Surface of touched UC". Nothing
2139
- // populated it, so a round r+1 leg was dispatched with no idea the judge had cited anything: it
2140
- // re-ran T0, found the scope still green, and reported done. Measured — EVAL returned FAIL naming
2141
- // five defects at file:line; round 2 kept all six trees and changed none of them, and every bug
2142
- // re-probed identically.
2143
- //
2144
- // Threading them from here was tried first and cannot work, twice over: a build order overrides
2145
- // the compile line (below), and only the GENERIC line serialises `--payload`, so the field was
2146
- // built and dropped; and the value lived in a variable, which a relaunch between two rounds
2147
- // resets to empty. `harness compile` reads them off the ledgered verdict instead — see
2148
- // `verdictBugs` — so a fresh round and a resumed one take the identical path.
2149
- //
2150
- // `digested_errors` could not cover this: it carries AEGIS triples from a RED T0, and a scope
2151
- // whose fixtures pass while its behaviour contradicts the spec has no red trial to digest. That
2152
- // gap is why EVAL is a separate layer, so the channel out of it has to be separate too.
2153
- return worker({
2154
- // No `operation` either, and for the same reason as the payload: the compile override below
2155
- // replaces the whole command, so a declared operation here would be discarded — and it would
2156
- // now be WRONG as well as dead. `harness compile` derives it: a round carrying cited defects
2157
- // compiles as `fix`, which is the operation task-executor's own contract binds `payload.bugs`
2158
- // to, and a round with nothing cited stays `execute`.
2159
- skill: "task-executor", schema: SCOPE_RESULT, phase: "Build",
2160
- label: `build:${scope.scope_id}-r${roundNo}`,
2161
- // A build order is addressed by scope + round + attempt, never by operation: the attempt number
2162
- // is part of its identity, so there is one order per attempt and the generic slug form cannot
2163
- // express it. NOTE this override is why no `payload` is passed — it would be discarded.
2164
- compile: `compile --scope "${scope.path}" --round ${roundNo} --attempt 1`,
2165
- extra:
2166
- (roundNo > 1
2167
- ? `THIS MAY BE A FIX ROUND. If your compiled order carries \`payload.bugs\`, the evaluator ` +
2168
- `returned FAIL last round and those are the defects it cited against files you own — each ` +
2169
- `with the criterion it broke and a file:line. Fix exactly those and touch nothing else. ` +
2170
- `They are spec-conformance defects, so T0 already passes and will keep passing whether or ` +
2171
- `not you fix them: a green T0 is NOT evidence you are done this round, and re-running the ` +
2172
- `fixtures cannot tell you. Read the cited lines against the committed spec, change them, ` +
2173
- `and keep T0 green. An entry marked \`unowned\` cites no file any scope owns — fix it only ` +
2174
- `if it falls inside your substrate. `
2175
- : "") +
2176
- `Re-compile the order for every attempt after the first, with --attempt <n>. An attempt will ` +
2177
- `be REFUSED (exit 3) while the previous one is unanswered — dispatched with no leg row and no ` +
2178
- `WorkResult — because grading a tree the previous attempt may still be writing counts an ` +
2179
- `attempt nobody ran. Let it come back rather than opening the next one. ` +
2180
- `Run the attempt ratchet for THIS scope only: up to ${attemptBudget} attempts of implement → ` +
2181
- `\`node "${KERNEL}" verify t0 "${scope.path}" --round ${roundNo} --attempt <n>\`, each scored against ` +
2182
- `the last kept trial. Stop on the first green T0, or when the attempt budget or the stagnation ` +
2183
- `breaker trips. Write only inside this scope's substrate whitelist — the sandbox hook enforces it. ` +
2184
- `Report green, attempts_used, which breaker (if any) tripped, and the T0 artifact path — ` +
2185
- `attempts_used is what the attested channels carry, not a count of the orders you compiled.`,
2186
- });
2187
- }