@dzhechkov/harness-core 0.3.150 → 0.4.2

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 (111) hide show
  1. package/.dz-manifest.json +410 -62
  2. package/README.md +81 -3
  3. package/dist/agentdb-index.d.ts.map +1 -1
  4. package/dist/agentdb-index.js +10 -2
  5. package/dist/agentdb-index.js.map +1 -1
  6. package/dist/backlog-embed.d.ts +94 -0
  7. package/dist/backlog-embed.d.ts.map +1 -0
  8. package/dist/backlog-embed.js +138 -0
  9. package/dist/backlog-embed.js.map +1 -0
  10. package/dist/backlog.d.ts +180 -7
  11. package/dist/backlog.d.ts.map +1 -1
  12. package/dist/backlog.js +429 -26
  13. package/dist/backlog.js.map +1 -1
  14. package/dist/challenge-panel.d.ts +3 -0
  15. package/dist/challenge-panel.d.ts.map +1 -1
  16. package/dist/challenge-panel.js +3 -0
  17. package/dist/challenge-panel.js.map +1 -1
  18. package/dist/export-holdout.d.ts +149 -0
  19. package/dist/export-holdout.d.ts.map +1 -0
  20. package/dist/export-holdout.js +198 -0
  21. package/dist/export-holdout.js.map +1 -0
  22. package/dist/feature-adr-checkpoints.d.ts +127 -0
  23. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  24. package/dist/feature-adr-checkpoints.js +199 -1
  25. package/dist/feature-adr-checkpoints.js.map +1 -1
  26. package/dist/feature-adr-routing.d.ts +3 -0
  27. package/dist/feature-adr-routing.d.ts.map +1 -1
  28. package/dist/feature-adr-routing.js +3 -0
  29. package/dist/feature-adr-routing.js.map +1 -1
  30. package/dist/guard.d.ts +42 -0
  31. package/dist/guard.d.ts.map +1 -1
  32. package/dist/guard.js +73 -1
  33. package/dist/guard.js.map +1 -1
  34. package/dist/index.d.ts +16 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +28 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/loop-blobs.generated.d.ts +33 -0
  39. package/dist/loop-blobs.generated.d.ts.map +1 -0
  40. package/dist/loop-blobs.generated.js +101 -0
  41. package/dist/loop-blobs.generated.js.map +1 -0
  42. package/dist/loop-lint.d.ts +63 -0
  43. package/dist/loop-lint.d.ts.map +1 -0
  44. package/dist/loop-lint.js +606 -0
  45. package/dist/loop-lint.js.map +1 -0
  46. package/dist/loop-plan.d.ts +416 -0
  47. package/dist/loop-plan.d.ts.map +1 -0
  48. package/dist/loop-plan.js +1151 -0
  49. package/dist/loop-plan.js.map +1 -0
  50. package/dist/loop-render.d.ts +104 -0
  51. package/dist/loop-render.d.ts.map +1 -0
  52. package/dist/loop-render.js +1068 -0
  53. package/dist/loop-render.js.map +1 -0
  54. package/dist/loop-trace.d.ts +229 -0
  55. package/dist/loop-trace.d.ts.map +1 -0
  56. package/dist/loop-trace.js +614 -0
  57. package/dist/loop-trace.js.map +1 -0
  58. package/dist/mutation-gate.d.ts +247 -0
  59. package/dist/mutation-gate.d.ts.map +1 -0
  60. package/dist/mutation-gate.js +535 -0
  61. package/dist/mutation-gate.js.map +1 -0
  62. package/dist/no-stubs.d.ts +53 -0
  63. package/dist/no-stubs.d.ts.map +1 -0
  64. package/dist/no-stubs.js +190 -0
  65. package/dist/no-stubs.js.map +1 -0
  66. package/dist/package-skill-layouts.d.ts +67 -0
  67. package/dist/package-skill-layouts.d.ts.map +1 -0
  68. package/dist/package-skill-layouts.js +81 -0
  69. package/dist/package-skill-layouts.js.map +1 -0
  70. package/dist/patterns.d.ts.map +1 -1
  71. package/dist/patterns.js +156 -75
  72. package/dist/patterns.js.map +1 -1
  73. package/dist/recall-domain-boost.d.ts.map +1 -1
  74. package/dist/recall-domain-boost.js +6 -0
  75. package/dist/recall-domain-boost.js.map +1 -1
  76. package/dist/statusline.d.ts +10 -2
  77. package/dist/statusline.d.ts.map +1 -1
  78. package/dist/statusline.js +122 -36
  79. package/dist/statusline.js.map +1 -1
  80. package/dist/store-lock.d.ts +108 -0
  81. package/dist/store-lock.d.ts.map +1 -0
  82. package/dist/store-lock.js +231 -0
  83. package/dist/store-lock.js.map +1 -0
  84. package/dist/workflows.d.ts +16 -22
  85. package/dist/workflows.d.ts.map +1 -1
  86. package/dist/workflows.js +17 -98
  87. package/dist/workflows.js.map +1 -1
  88. package/package.json +6 -4
  89. package/sbom.json +1073 -203
  90. package/src/agentdb-index.ts +10 -1
  91. package/src/backlog-embed.ts +156 -0
  92. package/src/backlog.ts +536 -28
  93. package/src/challenge-panel.ts +4 -0
  94. package/src/export-holdout.ts +235 -0
  95. package/src/feature-adr-checkpoints.ts +291 -1
  96. package/src/feature-adr-routing.ts +4 -0
  97. package/src/guard.ts +106 -1
  98. package/src/index.ts +62 -2
  99. package/src/loop-blobs.generated.ts +114 -0
  100. package/src/loop-lint.ts +643 -0
  101. package/src/loop-plan.ts +1419 -0
  102. package/src/loop-render.ts +1126 -0
  103. package/src/loop-trace.ts +727 -0
  104. package/src/mutation-gate.ts +701 -0
  105. package/src/no-stubs.ts +204 -0
  106. package/src/package-skill-layouts.ts +107 -0
  107. package/src/patterns.ts +135 -60
  108. package/src/recall-domain-boost.ts +6 -0
  109. package/src/statusline.ts +117 -30
  110. package/src/store-lock.ts +258 -0
  111. package/src/workflows.ts +18 -117
@@ -0,0 +1,1126 @@
1
+ /**
2
+ * loop-render — the schema-driven GENERATOR of loop-designer (ADR-002): `loop-plan/1` plan →
3
+ * ONE region-delimited, self-contained Workflow script + a sidecar `<name>.plan.json` (the plan is
4
+ * written BEFORE and independently of the script — FR-4.1: the oracle diff compares against an
5
+ * artifact the renderer has not touched).
6
+ *
7
+ * Region contract (architecture §3.1):
8
+ * BLOB — verbatim bytes from the blob registry; replaced wholesale on re-render (INV-10).
9
+ * GENERATED — derived from the plan; replaced wholesale (lint rule `plan-binding`).
10
+ * USER — the ONLY hand-editable regions; preserved BYTE-FOR-BYTE on re-render (INV-11).
11
+ *
12
+ * The exec fingerprint (FR-1.6 / AM-10) hashes ALL FOUR axes independently-sensitively:
13
+ * topology (structural plan shape) + prompts (per-step prompt text ONLY) + models (per-step
14
+ * declared model ONLY) + tools (the declared blob set with content hashes). The axis inputs are
15
+ * NON-REDUNDANT by construction (QE round-2 G2): no axis embeds rendered text that would let it
16
+ * subsume another. Changing ANY ONE axis alone changes the fingerprint, so a resume against a
17
+ * stale fingerprint is REFUSED (the generated resume-guard call site supplies this hash where the
18
+ * legacy feature-adr call site supplies inputHash alone — physical duplication only, no canonical-
19
+ * file change).
20
+ *
21
+ * Merge is propose-never-clobber (§3.2): a target with no markers is refused (write
22
+ * `<script>.proposed.js` + require --force); a USER region whose step vanished from the plan is a
23
+ * NAMED conflict, never silently dropped.
24
+ *
25
+ * Pure: no fs (the CLI does the writes), no clock, no randomness.
26
+ */
27
+
28
+ import { createHash } from 'node:crypto';
29
+ import {
30
+ normalizePlan,
31
+ planDigest,
32
+ stepIdent,
33
+ type LoopPlan,
34
+ type LoopStep,
35
+ } from './loop-plan.js';
36
+ import { BLOBS, type LoopBlob } from './loop-blobs.generated.js';
37
+
38
+ export const LOOP_RENDER_GENERATOR = 'loop-render/1';
39
+
40
+ /** The checkpoint schema stamp — PINNED in v1 (QE round-6 narrowing: `checkpointing.schemaVersion`
41
+ * is validated-away by ENACT-CKPT-OPT; the omitted-vs-explicit-default distinction false-flipped
42
+ * the topology axis in round 3 and is now unrepresentable). Both the rendered runtime and the
43
+ * fingerprint axis input carry this one constant. */
44
+ export const CKPT_SCHEMA_DEFAULT = 'loop-ckpt-1';
45
+
46
+ export interface RenderManifest {
47
+ planDigest: string;
48
+ execFingerprint: string;
49
+ blobs: { name: string; version: string; contentHash: string }[];
50
+ steps: string[];
51
+ userRegions: string[];
52
+ }
53
+
54
+ export interface RenderResult {
55
+ /** The full script text. */
56
+ text: string;
57
+ /** The sidecar plan JSON — write this FIRST, independently of the script (FR-4.1). */
58
+ planJson: string;
59
+ manifest: RenderManifest;
60
+ execFingerprint: string;
61
+ }
62
+
63
+ export interface MergeResult {
64
+ text: string;
65
+ /** USER regions in the old file with no counterpart in the new plan — reported, never dropped. */
66
+ conflicts: { stepId: string; reason: string }[];
67
+ /** True when the target had no markers and was refused (use --force to overwrite). */
68
+ refused: boolean;
69
+ proposedText?: string;
70
+ }
71
+
72
+ function sha256(s: string): string {
73
+ return createHash('sha256').update(s, 'utf8').digest('hex');
74
+ }
75
+
76
+ /** Which blobs a plan pulls in (opt-in subsystems + auto rules + requires closure). */
77
+ export function selectBlobs(plan: LoopPlan): LoopBlob[] {
78
+ const names = new Set<string>();
79
+ const sub = plan.subsystems ?? {};
80
+ if (plan.checkpointing?.enabled === true || sub.checkpoints === true) names.add('checkpoints');
81
+ // AM-9: training pairs are injected ONLY on an explicit opt-in — never by default (PHI lesson).
82
+ if (sub.trainingPairs === true) names.add('training-pairs');
83
+ if (sub.usageAdaptive === true) names.add('usage-probes');
84
+ if (sub.challengePanel === true) names.add('challenge-panel');
85
+ if (sub.codexDispatch === true) names.add('codex-dispatch');
86
+ // model-resolver is NOT a user-facing opt-in: it auto-includes whenever any step.model is set.
87
+ if (plan.steps.some((s) => typeof s.model === 'string' && s.model !== '')) names.add('model-resolver');
88
+ if (plan.trace?.emit === true) names.add('trace');
89
+ // requires closure (e.g. training-pairs → checkpoints for the shared fnv helpers)
90
+ let grew = true;
91
+ while (grew) {
92
+ grew = false;
93
+ for (const n of [...names]) {
94
+ for (const req of BLOBS[n]?.requires ?? []) {
95
+ if (!names.has(req)) {
96
+ names.add(req);
97
+ grew = true;
98
+ }
99
+ }
100
+ }
101
+ }
102
+ // stable roster order
103
+ const order = Object.keys(BLOBS);
104
+ return order.filter((n) => names.has(n)).map((n) => BLOBS[n] as LoopBlob);
105
+ }
106
+
107
+ const B = (name: string, version: string, hash: string, src: string): string =>
108
+ `// ── BEGIN BLOB ${name}@${version} sha256:${hash} src=${src} ──`;
109
+ const BE = (name: string, version: string): string => `// ── END BLOB ${name}@${version} ──`;
110
+ const G = (label: string): string => `// ── BEGIN GENERATED ${label} ──`;
111
+ const GE = (label: string): string => `// ── END GENERATED ${label} ──`;
112
+ const U = (label: string): string => `// ── BEGIN USER ${label} ──`;
113
+ const UE = (label: string): string => `// ── END USER ${label} ──`;
114
+
115
+ function jsString(s: string): string {
116
+ return JSON.stringify(String(s));
117
+ }
118
+
119
+ interface StepPlanView {
120
+ step: LoopStep;
121
+ depSettles: string[];
122
+ }
123
+
124
+ /** Render-time context renderStep needs to ENACT the plan (QE round-3 B1). */
125
+ interface RenderEnv {
126
+ ckptOn: boolean;
127
+ tpOn: boolean;
128
+ /** stepIds that are some gate's failRoute — rendered re-dispatchable. */
129
+ gateTargets: Set<string>;
130
+ /** fanout chain members — rendered inside their region, never checkpointed here. */
131
+ memberIds: Set<string>;
132
+ }
133
+
134
+ /** Render-time single-quote shell escaping for PLAN-LITERAL paths (writes are plan data). */
135
+ function shqRender(s: string): string {
136
+ return "'" + String(s).replace(/'/g, "'\\''") + "'";
137
+ }
138
+
139
+ /** The landed-barrier block (QE round-3 B1): a step with declared artifacts.writes polls until
140
+ * every declared write EXISTS (relative to TRACE_DIR), then proceeds — or fails LOUDLY. This is
141
+ * the generic form of feature-adr's Step-7.5 codex-landed barrier: a settled dispatch is not a
142
+ * delivered file. */
143
+ function landedBarrier(id: string, phase: string, writes: string[], pad: string): string[] {
144
+ const testExpr = writes.map((w) => `[ -e ${shqRender(w)} ]`).join(' && ');
145
+ return [
146
+ `${pad}// landed barrier: the declared write(s) must EXIST before the loop proceeds (a stub or`,
147
+ `${pad}// out-of-band writer settling early must not read as a delivered file)`,
148
+ `${pad}{`,
149
+ `${pad} const probeCmd = 'cd ' + shqRt(TRACE_DIR === null ? '.' : TRACE_DIR) + ${jsString(' && ' + testExpr + ' && echo LANDED || echo NOT-LANDED')}`,
150
+ `${pad} let landed = false`,
151
+ `${pad} for (let p = 0; p < 5 && !landed; p++) {`,
152
+ `${pad} __agentCalls++`,
153
+ `${pad} const probe = await agent('Run EXACTLY this one shell command via your Bash tool and reply with ONLY its raw stdout: ' + probeCmd, { label: ${jsString('landed:' + id)}, phase: ${jsString(phase)}, effort: 'low' }) // loop-lint: infra-agent`,
154
+ `${pad} landed = typeof probe === 'string' && probe.indexOf('NOT-LANDED') === -1 && probe.indexOf('LANDED') !== -1`,
155
+ `${pad} }`,
156
+ `${pad} if (!landed) { await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(phase)}, outcome: 'failed', error: new Error(${jsString(`step ${id}: declared write(s) never landed (${writes.join(', ')}) — failing LOUDLY (deliverable contract)`)}) }) }`,
157
+ `${pad}}`,
158
+ ];
159
+ }
160
+
161
+ /** Item binding — the ONLY member-vs-top-level difference the emitter knows (QE round-5 B1
162
+ * class-kill): identity (`label + ':' + it`, `itemKey: it`), the per-item prompt suffix, and the
163
+ * per-item CHAIN-predecessor causedBy entry. Everything else a step declares — including declared
164
+ * deps, whose settles ride causedBy through the SAME depSettles path as top-level steps (round
165
+ * 6) — renders through THE SAME code path. */
166
+ interface MemberBinding {
167
+ /** The per-item chain-predecessor settle expr ('__settleSeqOf("prev", it, __ix)') or null (first
168
+ * chain position — its causedBy is exactly the declared-deps settles, like a top-level step). */
169
+ chainCausedBy: string | null;
170
+ /** The upstream per-item value variable piped into the prompt, or null (first chain position). */
171
+ inputExpr: string | null;
172
+ }
173
+
174
+ /** THE single USER-region emitter — top-level steps and fanout members alike. */
175
+ function stepUserLines(s: LoopStep): string[] {
176
+ const id = s.stepId;
177
+ return [
178
+ U(`step:${id}/body`),
179
+ `const USER_PROMPT_${ident(id)} = ${jsString(s.prompt ?? `TODO: prompt for step ${id} (phase ${s.phase})`)}`, // no-stubs: rendered-script default prompt sentinel the author replaces (authoring cue)
180
+ UE(`step:${id}/body`),
181
+ ];
182
+ }
183
+
184
+ /** THE single prompt-assembly emitter: USER prompt + GENERATED artifact-contract/gate-protocol
185
+ * lines — the plan's reads/writes/deliverable/gate declarations are COMMUNICATED to the agent
186
+ * (enacted, not decorative) wherever the step renders. Round-4's B1 counterexample (a fanout
187
+ * member's `artifacts.reads` silently dropped) is unrepresentable here BY CONSTRUCTION: members
188
+ * have no separate assembly path. */
189
+ function stepPromptAssembly(s: LoopStep, plan: LoopPlan): string[] {
190
+ const id = s.stepId;
191
+ const lines: string[] = [];
192
+ const reads = s.artifacts?.reads ?? [];
193
+ const writes = s.artifacts?.writes ?? [];
194
+ const gateCfg = s.kind === 'gate' ? (plan.gates ?? []).find((g) => g.stepId === id) : undefined;
195
+ lines.push(`const P_${ident(id)} = [`);
196
+ lines.push(` USER_PROMPT_${ident(id)},`);
197
+ if (reads.length > 0) lines.push(` ${jsString('declared inputs (plan artifacts.reads): ' + reads.join(', '))},`);
198
+ if (writes.length > 0) {
199
+ const fileNote = (s.deliverable ?? 'return-value') === 'file' ? '; your deliverable is the written file(s), not your reply' : '';
200
+ lines.push(` ${jsString('declared outputs (plan artifacts.writes): ' + writes.join(', ') + ' — write them' + fileNote + '. The loop verifies they land.')},`);
201
+ }
202
+ if (s.kind === 'gate') {
203
+ lines.push(` ${jsString('GATE PROTOCOL (kind: ' + (gateCfg?.kind ?? 'gate') + '): end your reply with exactly one line "GATE: PASS" or "GATE: FAIL" — the loop PARSES this verdict and never synthesizes one.')},`);
204
+ }
205
+ lines.push(`].join('\\n')`);
206
+ return lines;
207
+ }
208
+
209
+ /** THE single dispatch-call emitter (QE round-5 B1 class-kill; NARROWED round 6): agent opts,
210
+ * the ONE inline thunk, and runStep opts — the v1 retry profile (maxAttempts + classes; the
211
+ * timing family is VALIDATED-AWAY, ENACT-RETRY-TIMING), model, and the causedBy settles of the
212
+ * step's declared deps — are built HERE for top-level steps AND fanout members. A member is the
213
+ * same step with an item binding, never a reduced projection that carries only what someone
214
+ * remembered to copy (the round-2 G5 → round-4 initialDelayMs/reads B1 family). Codex dispatch
215
+ * routes are validated-away (ENACT-DISPATCH), so exactly ONE thunk form exists. The
216
+ * member-parity tests + the single-emission source guard keep this the only path. */
217
+ function stepCallExpr(s: LoopStep, depSettles: string[], member: MemberBinding | null): string {
218
+ const id = s.stepId;
219
+ const optsParts: string[] = [
220
+ member === null ? `label: ${jsString(id)}` : `label: ${jsString(id)} + ':' + it`,
221
+ `phase: ${jsString(s.phase)}`,
222
+ ];
223
+ if (typeof s.model === 'string' && s.model !== '') optsParts.push(`model: ${jsString(s.model)}`);
224
+ const promptExpr =
225
+ member === null
226
+ ? `P_${ident(id)}`
227
+ : `P_${ident(id)} + '\\nitem: ' + it${member.inputExpr === null ? '' : ` + '\\ninput: ' + JSON.stringify(${member.inputExpr})`}`;
228
+ const thunk = `() => agent(${promptExpr}, { ${optsParts.join(', ')} })`;
229
+ // causedBy = the declared deps' settles (ONE path for both placements — round 6: member deps
230
+ // used to be silently replaced by the positional chain entry) + the member's chain predecessor.
231
+ const causedByEntries = depSettles.map((d) => `__settleSeqOf(${jsString(d)})`);
232
+ if (member !== null && member.chainCausedBy !== null) causedByEntries.push(member.chainCausedBy);
233
+ const runOptsParts = [
234
+ `itemKey: ${member === null ? 'null' : 'it'}`,
235
+ // PER-OCCURRENCE branch identity (QE round-7; Codex round-6 R2 MEASURED: with `dedup:false` and
236
+ // registry ["x","x"], BOTH pipeline branches shared the settle slot `__settled["a\0x"]`, the
237
+ // second settle overwrote the first, and both downstream `b` dispatches recorded causedBy=[5] —
238
+ // the first should point at seq 4). Identity is the OCCURRENCE (index-qualified), never the
239
+ // VALUE: two branches over the same registry value are two branches.
240
+ `occurrence: ${member === null ? 'null' : '__ix'}`,
241
+ // the EFFECTIVE model rides runStep opts too (QE round-7; Codex round-6 R2 MEASURED: a step
242
+ // declaring model:"sonnet" dispatched with that agent option but traced `model:null`, because
243
+ // the trace hook reads opts.model and only the agent opts carried it).
244
+ `model: ${typeof s.model === 'string' && s.model !== '' ? jsString(s.model) : 'null'}`,
245
+ `retryMaxAttempts: ${s.retry?.maxAttempts ?? 1}`,
246
+ `retryOn: ${JSON.stringify(s.retry?.retryableFailureClasses ?? [])}`,
247
+ `causedBy: [${causedByEntries.join(', ')}]`,
248
+ ];
249
+ return `runStep(${jsString(id)}, ${jsString(s.phase)}, ${thunk}, { ${runOptsParts.join(', ')} })`;
250
+ }
251
+
252
+ /** Emit the per-step GENERATED wiring + its USER region — ENACTING the plan (QE round-3 B1):
253
+ * checkpoint consult + resume-skip + persist, gate verdict parsing with redo/fail routing, the
254
+ * landed barrier for declared writes, dispatch routes, and artifact-contract prompt lines. A plan
255
+ * field renderStep cannot enact is REJECTED by validatePlan (ENACT-x / KIND-1 / XREF-1 diagnostics),
256
+ * never a silent no-op — the loop-plan-honesty enumeration test machine-checks the closure.
257
+ * All dispatch surfaces come from the SHARED emitters above (round-5 B1). */
258
+ function renderStep(v: StepPlanView, plan: LoopPlan, env: RenderEnv): string {
259
+ const s = v.step;
260
+ const id = s.stepId;
261
+ const lines: string[] = [];
262
+
263
+ // USER region FIRST: `const` is block-scoped (TDZ) — the GENERATED wiring below reads it.
264
+ lines.push(...stepUserLines(s));
265
+ lines.push(G(`step:${id} kind=${s.kind} phase=${s.phase}`));
266
+ lines.push(...stepPromptAssembly(s, plan));
267
+ const writes = s.artifacts?.writes ?? [];
268
+ const gateCfg = s.kind === 'gate' ? (plan.gates ?? []).find((g) => g.stepId === id) : undefined;
269
+
270
+ if (s.kind === 'agent' || s.kind === 'gate') {
271
+ const runExpr = stepCallExpr(s, v.depSettles, null);
272
+ const ckpt = env.ckptOn && s.kind === 'agent' && !env.memberIds.has(id);
273
+ const tp = env.tpOn;
274
+ const isTarget = env.gateTargets.has(id);
275
+ const needsFn = isTarget || s.kind === 'gate';
276
+ // upstream results in the checkpoint input hash: a re-run upstream invalidates downstream
277
+ // (only deps with a rendered r_ variable — region results are covered transitively via their
278
+ // member steps and EXEC_FP).
279
+ const depVars = (s.deps ?? [])
280
+ .filter((d) => {
281
+ const ds = plan.steps.find((x) => x.stepId === d);
282
+ return ds !== undefined && (ds.kind === 'agent' || ds.kind === 'gate' || ds.kind === 'pause') && !env.memberIds.has(d);
283
+ })
284
+ .map((d) => `r_${ident(d)}`);
285
+ const hashParts = `[P_${ident(id)}${depVars.length > 0 ? ', ' + depVars.join(', ') : ''}]`;
286
+ const artifactRel = writes.length > 0 ? JSON.stringify(writes) : 'null';
287
+ const tpLine = (pad: string, resumed: boolean): string =>
288
+ `${pad}await __tpCapture(${jsString(id)}, ${jsString(s.phase)}, P_${ident(id)}, r_${ident(id)}, ${typeof s.model === 'string' && s.model !== '' ? jsString(s.model) : 'null'}, ${resumed ? 'true' : 'false'})`;
289
+
290
+ // ROUND-6 B3 SHAPE: `let r_x` + (optional) dispatch-fn declaration form the await-free
291
+ // preamble; then exactly ONE settle-routed try wraps EVERY await this step performs —
292
+ // dispatch, landed probes, redo loop, verdict settles. A REJECTED await (the round-5 landed
293
+ // probe hole: `await agent(...)` outside any settle-routed try ⇒ 0 flushes, 0 durable events)
294
+ // now routes into __settleStep structurally; the shape is asserted by the structural source
295
+ // guard in loop-render.test.ts, which beats the retired six-token list.
296
+ lines.push(`let r_${ident(id)} = null`);
297
+ if (needsFn) {
298
+ // re-dispatchable form: this step is a gate, or a gate's failRoute target — the redo loop
299
+ // re-invokes __dispatch_<id>(true) (live: a redo NEVER resumes from the checkpoint it just
300
+ // wrote). The DECLARATION executes no await; every CALL site sits inside the try below.
301
+ lines.push(`async function __dispatch_${ident(id)}(__live) {`);
302
+ if (ckpt) {
303
+ lines.push(` const __h = __ckptInputHash(${jsString(id)}, ${hashParts})`);
304
+ lines.push(` if (__live !== true && __ckptResume(${jsString(id)}, __h, ${artifactRel})) {`);
305
+ lines.push(` r_${ident(id)} = __ckptEntries[${jsString(id)}].result`);
306
+ lines.push(` log(${jsString(`checkpoint: step ${id} RESUMED (fingerprint+artifact match) — dispatch skipped`)})`);
307
+ if (tp) lines.push(tpLine(' ', true));
308
+ lines.push(` return r_${ident(id)}`);
309
+ lines.push(` }`);
310
+ }
311
+ lines.push(` r_${ident(id)} = await ${runExpr}`);
312
+ if (writes.length > 0) lines.push(...landedBarrier(id, s.phase, writes, ' '));
313
+ if (ckpt) lines.push(` await __ckptAppend(${jsString(id)}, ${jsString(s.phase)}, __h, r_${ident(id)})`);
314
+ if (tp) lines.push(tpLine(' ', false));
315
+ lines.push(` return r_${ident(id)}`);
316
+ lines.push(`}`);
317
+ }
318
+ lines.push(`try {`);
319
+ if (!needsFn) {
320
+ if (ckpt) {
321
+ lines.push(` const __h_${ident(id)} = __ckptInputHash(${jsString(id)}, ${hashParts})`);
322
+ lines.push(` if (__ckptResume(${jsString(id)}, __h_${ident(id)}, ${artifactRel})) {`);
323
+ lines.push(` r_${ident(id)} = __ckptEntries[${jsString(id)}].result`);
324
+ lines.push(` log(${jsString(`checkpoint: step ${id} RESUMED (fingerprint+artifact match) — dispatch skipped`)})`);
325
+ if (tp) lines.push(tpLine(' ', true));
326
+ lines.push(` } else {`);
327
+ lines.push(` r_${ident(id)} = await ${runExpr}`);
328
+ if (writes.length > 0) lines.push(...landedBarrier(id, s.phase, writes, ' '));
329
+ lines.push(` await __ckptAppend(${jsString(id)}, ${jsString(s.phase)}, __h_${ident(id)}, r_${ident(id)})`);
330
+ if (tp) lines.push(tpLine(' ', false));
331
+ lines.push(` }`);
332
+ } else {
333
+ lines.push(` r_${ident(id)} = await ${runExpr}`);
334
+ if (writes.length > 0) lines.push(...landedBarrier(id, s.phase, writes, ' '));
335
+ if (tp) lines.push(tpLine(' ', false));
336
+ }
337
+ } else {
338
+ lines.push(` await __dispatch_${ident(id)}()`);
339
+ if (s.kind === 'gate') {
340
+ // gate redo/fail routing (enacts plan gates[] EXACTLY: failRoute + maxRedos; absent config
341
+ // means no redo and a loud failure — kind:'gate' is never a decorative label)
342
+ const redos = typeof gateCfg?.maxRedos === 'number' && Number.isFinite(gateCfg.maxRedos) && gateCfg.maxRedos > 0 ? Math.floor(gateCfg.maxRedos) : 0;
343
+ const route = typeof gateCfg?.failRoute === 'string' ? gateCfg.failRoute : null;
344
+ const routeIsTerminal = route !== null && route.startsWith('terminal:');
345
+ lines.push(` let __v_${ident(id)} = __gateVerdict(r_${ident(id)})`);
346
+ if (redos > 0 && route !== null && !routeIsTerminal) {
347
+ lines.push(` let __redo_${ident(id)} = 0`);
348
+ lines.push(` while (__v_${ident(id)} !== 'pass' && __redo_${ident(id)} < ${redos}) {`);
349
+ lines.push(` __redo_${ident(id)}++`);
350
+ lines.push(` log('gate ${id}: verdict ' + __v_${ident(id)} + ${jsString(` — redo `)} + __redo_${ident(id)} + ${jsString(`/${redos} re-dispatches failRoute ${route} (plan gates[]; checkpoints bypassed on redo)`)})`);
351
+ lines.push(` await __dispatch_${ident(route)}(true)`);
352
+ lines.push(` await __dispatch_${ident(id)}(true)`);
353
+ lines.push(` __v_${ident(id)} = __gateVerdict(r_${ident(id)})`);
354
+ lines.push(` }`);
355
+ }
356
+ if (routeIsTerminal) {
357
+ lines.push(` if (__v_${ident(id)} !== 'pass') {`);
358
+ lines.push(` // typed terminal failure route (plan gates[].failRoute) — a NAMED phase, never a silent pass; settled durably through the single exit (round-5 B3)`);
359
+ lines.push(` await __ledgerAppend(${jsString(s.phase)}, ${jsString((route as string).slice('terminal:'.length))})`);
360
+ lines.push(` return await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'terminal', value: { phase: ${jsString(route as string)}, gate: ${jsString(id)}, verdict: __v_${ident(id)} } })`);
361
+ lines.push(` }`);
362
+ } else {
363
+ lines.push(` if (__v_${ident(id)} !== 'pass') {`);
364
+ lines.push(` // the flush rides INSIDE __settleStep now — a flush rejection can no longer replace the gate error (Codex R2, round-5 B3)`);
365
+ lines.push(` await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'failed', error: new Error('gate ${id} FAILED (verdict ' + __v_${ident(id)} + ') — failing the run LOUDLY (a failed or unparseable gate verdict is never a silent pass)') })`);
366
+ lines.push(` }`);
367
+ }
368
+ }
369
+ }
370
+ lines.push(`} catch (__stepErr) { await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'failed', error: __stepErr }) }`);
371
+ } else if (s.kind === 'pause') {
372
+ const pauseState = s.pauseState ?? id;
373
+ const pause = (plan.pauses ?? []).find((p) => p.state === pauseState);
374
+ const resumeArg = pause?.resumeArg ?? 'resume';
375
+ lines.push(`if (A[${jsString(resumeArg)}] === undefined) {`);
376
+ lines.push(` // typed pause (checkpoint-return/re-invoke — never a generic interrupt): re-invoke with args.${resumeArg}; settled durably through the single exit (round-5 B3)`);
377
+ if (pause?.payloadSchema !== undefined) {
378
+ // enacts pauses[].payloadSchema: the pause return CARRIES the declared payload shape, so the
379
+ // re-invoking caller sees what the resume arg must contain.
380
+ lines.push(` await __ledgerAppend(${jsString(s.phase)}, ${jsString(pauseState)})`);
381
+ lines.push(` return await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'terminal', value: { phase: ${jsString(pauseState)}, resumeArg: ${jsString(resumeArg)}, payloadSchema: ${JSON.stringify(pause.payloadSchema)} } })`);
382
+ } else {
383
+ lines.push(` await __ledgerAppend(${jsString(s.phase)}, ${jsString(pauseState)})`);
384
+ lines.push(` return await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'terminal', value: { phase: ${jsString(pauseState)}, resumeArg: ${jsString(resumeArg)} } })`);
385
+ }
386
+ lines.push(`}`);
387
+ lines.push(`const r_${ident(id)} = A[${jsString(resumeArg)}]`);
388
+ }
389
+ lines.push(GE(`step:${id}`));
390
+ return lines.join('\n');
391
+ }
392
+
393
+ /** Identifier lowering is loop-plan's COLLISION-RESISTANT stepIdent (QE round-6; the round-5 third
394
+ * class: `a-b` and `a.b` both lowered to `a_b` and the rendered script died with a redeclaration
395
+ * SyntaxError). One implementation, shared with the IDENT-1 parse check — and IDENT-1, not the
396
+ * lowering, is what REJECTS a colliding plan (the 8-hex suffix is not injective; QE round-7). */
397
+ const ident = stepIdent;
398
+
399
+ /** Fanout region render (QE round-5 B1 class-kill): members are STEPS with an item binding —
400
+ * their USER region, prompt assembly (reads-contract lines included) and dispatch call all ride
401
+ * the SAME emitters as top-level steps (stepUserLines/stepPromptAssembly/stepCallExpr). The old
402
+ * reduced member projection (per-member agent opts + retry copied by hand) — the root cause of the
403
+ * whole B1 family (G5 → round-4 initialDelayMs/reads) — no longer exists; the source guard in
404
+ * loop-render.test.ts asserts no second member-emission path can reappear. Chain shape (non-empty,
405
+ * one member under barrier, no repeats) is MEMBER-2's; member kinds are XREF-1's. */
406
+ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
407
+ const f = (plan.fanouts ?? []).find((x) => x.stage === fanoutStep.stepId);
408
+ const j = (plan.joins ?? []).find((x) => x.forStage === fanoutStep.stepId);
409
+ const chain = f?.chain ?? [];
410
+ if (!f || !j || chain.length === 0) return `// (fanout ${fanoutStep.stepId}: missing fanouts/joins/chain config — validatePlan rejects this plan)`;
411
+ const id = fanoutStep.stepId;
412
+ const shape = fanoutStep.concurrency ?? 'barrier';
413
+ const byId = new Map(plan.steps.map((s) => [s.stepId, s]));
414
+ const members = chain.map((c) => byId.get(c)).filter((s): s is LoopStep => s !== undefined);
415
+ // a member's DECLARED deps ride its causedBy through the same depSettles path as a top-level
416
+ // step (round 6 — they used to be silently replaced by the positional chain entry; deps
417
+ // targeting members and member-to-member deps are validated-away, so these are outside-region).
418
+ const memberDeps = (ms: LoopStep): string[] => (ms.deps ?? []).filter((d) => byId.has(d) && !chain.includes(d));
419
+ const lines: string[] = [];
420
+ lines.push(G(`step:${id} kind=fanout shape=${shape} maxFanout=${f.maxFanout}`));
421
+ for (const ms of members) lines.push(...stepPromptAssembly(ms, plan));
422
+ lines.push(`const REGISTRY_${ident(id)} = ${JSON.stringify(f.registry)}`);
423
+ // dedup (enacted — QE round-3 B1): a declared dedup DEDUPLICATES the registry before the cap.
424
+ const registryExpr = f.dedup === true ? `REGISTRY_${ident(id)}.filter(function (x, i) { return REGISTRY_${ident(id)}.indexOf(x) === i })` : `REGISTRY_${ident(id)}`;
425
+ lines.push(`const MEMBERS_${ident(id)} = ${registryExpr}.slice(0, ${f.maxFanout}) // bounded fanout (INV-2): never args-derived, never uncapped`);
426
+ // ROUND-6 B3 SHAPE: the region's awaits (member dispatches via parallel + the join) ride ONE
427
+ // settle-routed try, mirroring the per-step shape. Member runStep failures settle themselves
428
+ // durably before rejecting; the outer catch is the structural belt for the region as a whole.
429
+ lines.push(`let R_${ident(id)} = null`);
430
+ lines.push(`let J_${ident(id)} = null`);
431
+ lines.push(`try {`);
432
+ if (shape === 'pipeline') {
433
+ // PIPELINED per-item chains inside ONE __drainAll — the discriminating render shape (§3.3):
434
+ // dispatch(B:item1) is allocated a seq before settle(A:item3). Predecessor by POSITION (G15),
435
+ // qualified by the branch OCCURRENCE (round 7 — duplicate registry values are distinct
436
+ // branches, so a chain predecessor is looked up per occurrence, never per value).
437
+ lines.push(` R_${ident(id)} = await __drainAll(MEMBERS_${ident(id)}.map((it, __ix) => async () => {`);
438
+ let prev: string | null = null;
439
+ for (let ci = 0; ci < members.length; ci++) {
440
+ const cs = members[ci] as LoopStep;
441
+ const call = stepCallExpr(cs, memberDeps(cs), {
442
+ chainCausedBy: ci === 0 ? null : `__settleSeqOf(${jsString((members[ci - 1] as LoopStep).stepId)}, it, __ix)`,
443
+ inputExpr: prev,
444
+ });
445
+ lines.push(` const v_${ident(cs.stepId)} = await ${call}`);
446
+ prev = `v_${ident(cs.stepId)}`;
447
+ }
448
+ lines.push(` return ${prev ?? 'null'}`);
449
+ lines.push(` }))`);
450
+ } else {
451
+ // BARRIER shape: all members dispatched, one join closes the region (exactly one member step —
452
+ // MEMBER-2; extra barrier chain entries used to be silently never dispatched).
453
+ const ms = members[0] as LoopStep;
454
+ const call = stepCallExpr(ms, memberDeps(ms), { chainCausedBy: null, inputExpr: null });
455
+ lines.push(` R_${ident(id)} = await __drainAll(MEMBERS_${ident(id)}.map((it, __ix) => () => ${call}))`);
456
+ }
457
+ lines.push(` J_${ident(id)} = await __joinSettled(${jsString(j.stage)}, ${jsString(fanoutStep.phase)}, R_${ident(id)}, { policy: ${jsString(j.joinPolicy)}, onInvalid: ${jsString(j.onInvalid ?? 'named-failure')}, region: ${jsString(id)} })`);
458
+ lines.push(`} catch (__stepErr) { await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(fanoutStep.phase)}, outcome: 'failed', error: __stepErr }) }`);
459
+ lines.push(GE(`step:${id}`));
460
+ const userLines: string[] = [];
461
+ for (const ms of members) userLines.push(...stepUserLines(ms));
462
+ return [...userLines, ...lines].join('\n');
463
+ }
464
+
465
+ /** The base runtime — emitted UNCONDITIONALLY in every rendered script (the plan's DECIDED note:
466
+ * `runStep` is part of the base GENERATED region, never an opt-in blob; the trace/checkpoints blobs
467
+ * attach hooks INSIDE it, and when opted out the hook sites are no-ops). */
468
+ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: LoopBlob[]): string {
469
+ const traceOn = plan.trace?.emit === true;
470
+ const ckptOn = plan.checkpointing?.enabled === true || plan.subsystems?.checkpoints === true;
471
+ // budget: declared per-step budgets PLUS the declared gate-redo allowance (QE round-3 B1 — a
472
+ // plan-declared redo must be affordable; an undeclared one still hits the guard loudly).
473
+ const stepBudget = plan.steps.reduce((n, s) => n + (s.budget?.maxAgents ?? 1), 0);
474
+ const byIdB = new Map(plan.steps.map((s) => [s.stepId, s]));
475
+ const gateRedoBudget = (plan.gates ?? []).reduce((n, g) => {
476
+ const redos = typeof g.maxRedos === 'number' && Number.isFinite(g.maxRedos) && g.maxRedos > 0 ? Math.floor(g.maxRedos) : 0;
477
+ if (redos === 0 || typeof g.failRoute !== 'string' || g.failRoute.startsWith('terminal:')) return n;
478
+ return n + redos * ((byIdB.get(g.failRoute)?.budget?.maxAgents ?? 1) + (byIdB.get(g.stepId)?.budget?.maxAgents ?? 1));
479
+ }, 0);
480
+ const budgetTotal = stepBudget + gateRedoBudget;
481
+ const lines: string[] = [];
482
+ lines.push(G('runtime'));
483
+ lines.push(`const A = typeof args === 'string' ? JSON.parse(args) : (args || {})`);
484
+ lines.push(`const PLAN_DIGEST = ${jsString(planDig)}`);
485
+ lines.push(`const EXEC_FP = ${jsString(execFp)} // full execution fingerprint: topology+prompts+models+tools (FR-1.6)`);
486
+ lines.push(`function shqRt(s) { return "'" + String(s).replace(/'/g, "'\\\\''") + "'" }`);
487
+ lines.push(`const RUN_ID = (typeof A.runId === 'string' && /^[a-z0-9-]{1,40}$/.test(A.runId)) ? A.runId : 'run-1'`);
488
+ lines.push(`const TRACE_DIR = (typeof A.traceDir === 'string' && A.traceDir.charAt(0) === '/') ? A.traceDir.replace(/\\/+$/, '') : null`);
489
+ lines.push(`const TRACE_FILE = TRACE_DIR === null ? null : TRACE_DIR + '/trace.jsonl'`);
490
+ lines.push(`const REPO_DIR = (typeof A.repo === 'string' && A.repo.charAt(0) === '/') ? A.repo.replace(/\\/+$/, '') : null`);
491
+ lines.push(`const DZ_BIN = (typeof A.dz === 'string' && A.dz !== '') ? A.dz : 'dz'`);
492
+ lines.push(`const LOOP_SLUG = ${jsString(plan.name)}`);
493
+ lines.push(`// budget guard — spent BEFORE every spawn; retries consume budget (lint: budget-before-spawn)`);
494
+ lines.push(`const __budget = { left: ${budgetTotal} }`);
495
+ lines.push(`// Total agent invocations this run made — model dispatches AND infra agents. The ledger's`);
496
+ lines.push(`// \`agents\` column means agent_count from the completion notification (ALL subagents), so the`);
497
+ lines.push(`// automated row must count every dispatch, never the trace's model-dispatch subset (QE F1).`);
498
+ lines.push(`let __agentCalls = 0`);
499
+ lines.push(`let __ledgerDone = false`);
500
+ if (traceOn) lines.push(`let __faLegWarned = false`);
501
+ lines.push(`function __spendBudget(stepId) { if (__budget.left <= 0) { throw new Error('loop budget exhausted before ' + stepId) } __budget.left-- }`);
502
+ lines.push(`const __hooks = { onDispatch: null, onSettle: null }`);
503
+ lines.push(`const __settled = {}`);
504
+ lines.push(`// settle identity is PER-OCCURRENCE (round-7; Codex round-6 R2: with dedup:false and a`);
505
+ lines.push(`// duplicated registry value, two branches shared one (stepId,itemKey) slot — the second`);
506
+ lines.push(`// settle overwrote the first and BOTH downstream dispatches recorded the LATER seq as`);
507
+ lines.push(`// their causedBy). The occurrence INDEX (the branch's position in the capped member`);
508
+ lines.push(`// list) qualifies the key, so each branch's causedBy points at its OWN upstream settle.`);
509
+ lines.push(`function __settleKey(stepId, itemKey, occ) { return stepId + '\\u0000' + (itemKey == null ? '' : itemKey) + '\\u0000' + (occ == null ? '' : occ) }`);
510
+ lines.push(`function __settleSeqOf(stepId, itemKey, occ) { const v = __settled[__settleKey(stepId, itemKey, occ)]; return typeof v === 'number' ? v : -1 }`);
511
+ lines.push(`let __invocationN = 0`);
512
+ lines.push(`let __seqFallback = 0 // used only when the trace blob is opted out (hooks are no-ops)`);
513
+ lines.push(`// __errText — TOTAL error-to-text (QE round-3 B3, the ha-consilium 5b totality lesson): the`);
514
+ lines.push(`// writer's own settle event must survive a hostile error object. String(err) throws on a`);
515
+ lines.push(`// null-prototype object and a throwing .message getter throws on access — both are caught here,`);
516
+ lines.push(`// so rendering a message can never replace the original failure or lose the settle. The`);
517
+ lines.push(`// .message property is read ONCE into a local (round-5 B3: a one-shot getter answered the`);
518
+ lines.push(`// typeof probe and vanished on the value read — snapshot-once defeats it).`);
519
+ lines.push(`function __errText(err) {`);
520
+ lines.push(` try {`);
521
+ lines.push(` if (err !== null && typeof err === 'object') { const m = err.message; if (typeof m === 'string') return m }`);
522
+ lines.push(` return String(err)`);
523
+ lines.push(` } catch (_e) {`);
524
+ lines.push(` try { return Object.prototype.toString.call(err) } catch (_e2) { return '[unrenderable error]' }`);
525
+ lines.push(` }`);
526
+ lines.push(`}`);
527
+ lines.push(`// failure classification (G4, hardened in QE round-3 B3, round-4: cause-chain + word bounds) —`);
528
+ lines.push(`// the CLOSED enum from loop-plan/1 (timeout | transport | malformed-output | policy-refusal).`);
529
+ lines.push(`// The err.cause CHAIN is traversed (bounded depth 5, cycle-safe, getter-safe): the standard`);
530
+ lines.push(`// Node fetch shape TypeError('fetch failed', {cause:{code:'ECONNRESET'}}) is TRANSPORT.`);
531
+ lines.push(`// THREE TIERS over the whole chain, strongest first:`);
532
+ lines.push(`// 1. error CODE (works on non-Error shapes like {code:'ECONNRESET'}; never message-dependent;`);
533
+ lines.push(`// ETIMEDOUT is a TRANSPORT code — round-2's message regex captured it as 'timeout' first);`);
534
+ lines.push(`// 2. error NAME (SyntaxError = parsing model output failed → malformed-output);`);
535
+ lines.push(`// 3. message patterns, DISJOINT by precedence transport > policy-refusal > malformed-output >`);
536
+ lines.push(`// timeout — every alternative WORD-BOUNDED (round-4 B3: 'rate.?limit' unbounded matched`);
537
+ lines.push(`// 'delibeRATE LIMITation'/'corpoRATE LIMITation' — a substring can never smuggle a class).`);
538
+ lines.push(`// An UNCLASSIFIABLE failure returns null and is NEVER retried (closed enum, fail-closed).`);
539
+ lines.push(`function __causeChain(err) {`);
540
+ lines.push(` const chain = []`);
541
+ lines.push(` let cur = err`);
542
+ lines.push(` for (let d = 0; d < 5; d++) {`);
543
+ lines.push(` if (cur === null || cur === undefined) break`);
544
+ lines.push(` if (chain.indexOf(cur) !== -1) break // cycle-safe`);
545
+ lines.push(` chain.push(cur)`);
546
+ lines.push(` try { cur = typeof cur === 'object' ? cur.cause : undefined } catch (_e) { cur = undefined } // getter-safe`);
547
+ lines.push(` }`);
548
+ lines.push(` return chain.length > 0 ? chain : [err]`);
549
+ lines.push(`}`);
550
+ lines.push(`// __errSnap — ONE snapshot PER FAILURE (round-6 B3; Codex round-5: .message was snapshot once`);
551
+ lines.push(`// per __errText INVOCATION, not once per failure — logging read it, classification read it`);
552
+ lines.push(`// AGAIN, so a one-shot .message getter answered the log and defeated classification: 2 getter`);
553
+ lines.push(`// reads, 1 attempt MEASURED). The catch site builds this snapshot ONCE; the log line and the`);
554
+ lines.push(`// classifier both consume the SNAPSHOT — .code/.name/.message are each read exactly once per`);
555
+ lines.push(`// failure, getter-safe, over the whole cause chain.`);
556
+ lines.push(`function __errSnap(err) {`);
557
+ lines.push(` const chain = __causeChain(err)`);
558
+ lines.push(` const snap = []`);
559
+ lines.push(` for (let ci = 0; ci < chain.length; ci++) {`);
560
+ lines.push(` let code = null`);
561
+ lines.push(` try { const c = chain[ci] !== null && typeof chain[ci] === 'object' ? chain[ci].code : null; code = typeof c === 'string' ? c.toUpperCase() : null } catch (_e) { code = null }`);
562
+ lines.push(` let name = null`);
563
+ lines.push(` try { const n = chain[ci] !== null && typeof chain[ci] === 'object' ? chain[ci].name : null; name = typeof n === 'string' ? n : null } catch (_e) { name = null }`);
564
+ lines.push(` snap.push({ code: code, name: name, text: __errText(chain[ci]) })`);
565
+ lines.push(` }`);
566
+ lines.push(` return snap`);
567
+ lines.push(`}`);
568
+ lines.push(`function __classifyFailure(outcome, snap) {`);
569
+ lines.push(` if (outcome === 'null') return 'transport' // a dead/empty agent is a delivery failure — the "agent died" case is retryable ONLY under retryOn: ['transport']`);
570
+ lines.push(` const links = Array.isArray(snap) ? snap : []`);
571
+ lines.push(` for (let ci = 0; ci < links.length; ci++) {`);
572
+ lines.push(` const code = links[ci].code`);
573
+ lines.push(` if (code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EPIPE' || code === 'ECONNABORTED' || code === 'EAI_AGAIN') return 'transport'`);
574
+ lines.push(` }`);
575
+ lines.push(` for (let ci = 0; ci < links.length; ci++) {`);
576
+ lines.push(` if (links[ci].name === 'SyntaxError') return 'malformed-output'`);
577
+ lines.push(` }`);
578
+ lines.push(` let msg = ''`);
579
+ lines.push(` for (let ci = 0; ci < links.length; ci++) { msg += (ci > 0 ? '\\n' : '') + links[ci].text }`);
580
+ lines.push(` msg = msg.toLowerCase()`);
581
+ lines.push(` // rate[ -]?limit(ed|ing|s)? is RIGHT-BOUNDED (round-5 B3: the open 'rate.?limit' matched`);
582
+ lines.push(` // 'rate limitation: invalid JSON' as transport — a malformed-output failure smuggled a class)`);
583
+ lines.push(` if (/\\btransport\\b|\\beconnreset\\b|\\beconnrefused\\b|\\benotfound\\b|\\bepipe\\b|\\betimedout\\b|\\bsocket hang up\\b|\\bnetwork error\\b|\\brate[ -]?limit(ed|ing|s)?\\b|\\boverloaded\\b|\\bhttp 5[0-9][0-9]\\b/.test(msg)) return 'transport'`);
584
+ lines.push(` if (/\\bpolicy\\b|\\brefus(e|ed|es|al|ing)\\b|\\bdeclin(e|ed|es|ing)\\b|\\bcontent filter\\b|\\bsafety block\\b/.test(msg)) return 'policy-refusal'`);
585
+ lines.push(` if (/\\bmalformed\\b|\\bunparseable\\b|\\bparse error\\b|\\binvalid json\\b|\\bunexpected token\\b|\\bunexpected end of json\\b|\\bschema mismatch\\b/.test(msg)) return 'malformed-output'`);
586
+ lines.push(` if (/\\btimeout\\b|\\btimed out\\b/.test(msg)) return 'timeout'`);
587
+ lines.push(` return null`);
588
+ lines.push(`}`);
589
+ lines.push(`// __settleStep — THE single terminal exit of every step path (QE round-5 B3 class-kill;`);
590
+ lines.push(`// round-6: SUCCESS-PATH PARITY — Codex round-5 showed __settleStep({outcome:'ok'}) returned`);
591
+ lines.push(`// BEFORE flushing, leaving success durability on a naked phase-boundary await whose rejection`);
592
+ lines.push(`// REPLACED the successful result with 'Error: phase flush down'). EVERY outcome now FLUSHES`);
593
+ lines.push(`// FIRST: ok and 'terminal' (gate terminal route, pause, run epilogue) flush then return the`);
594
+ lines.push(`// value; a failure flushes then throws the ORIGINAL error. A flush failure is ALWAYS a logged`);
595
+ lines.push(`// SECONDARY event; it never replaces the primary outcome — success included (the ha-consilium`);
596
+ lines.push(`// totality lesson at the flush layer).`);
597
+ lines.push(`async function __settleStep(o) {`);
598
+ lines.push(` try { await __traceFlushNow(o.phase, o.stepId) } catch (_fe) { log('settle flush for ' + o.stepId + ' threw: ' + __errText(_fe) + ' — primary outcome preserved') }`);
599
+ if (plan.subsystems?.trainingPairs === true) {
600
+ lines.push(` // The ONE producer of the captureFailures channel on terminal values — the four terminal call sites never carry the key, so future routes inherit it.`);
601
+ lines.push(` if (o.outcome === 'terminal' && o.value !== null && typeof o.value === 'object') { o.value.captureFailures = __captureFailures }`);
602
+ }
603
+ lines.push(` if (o.outcome === 'failed') { throw o.error }`);
604
+ lines.push(` return o.value`);
605
+ lines.push(`}`);
606
+ lines.push(`// __phaseFlush — the TOTAL phase-boundary flush (round-6 B3): its rejection is a logged`);
607
+ lines.push(`// secondary event, never a replaced outcome (the naked await __traceFlushNow at phase`);
608
+ lines.push(`// boundaries was the round-5 success-replacement hole).`);
609
+ lines.push(`async function __phaseFlush(phaseName) {`);
610
+ lines.push(` try { await __traceFlushNow(phaseName, null) } catch (_fe) { log('phase flush threw: ' + __errText(_fe) + ' — outcome preserved (flush failure is secondary)') }`);
611
+ lines.push(`}`);
612
+ lines.push(`// join failures route through the single exit too (joinRegion throws; the wrapper settles)`);
613
+ lines.push(`async function __joinSettled(joinStepId, phaseName, results, o) {`);
614
+ lines.push(` try { return joinRegion(results, o) } catch (err) { return await __settleStep({ stepId: joinStepId, phase: phaseName, outcome: 'failed', error: err }) }`);
615
+ lines.push(`}`);
616
+ lines.push(`// runStep — the SINGLE choke point (every agent() call rides through here; lint:`);
617
+ lines.push(`// no-agent-outside-runstep). Round-5 B3: a THIN wrapper — every outcome (the settled value`);
618
+ lines.push(`// OR any throw out of the attempt loop, __spendBudget included) routes through __settleStep;`);
619
+ lines.push(`// no step path can exit around the durable settle.`);
620
+ lines.push(`async function runStep(stepId, phaseName, thunk, o) {`);
621
+ lines.push(` let __v`);
622
+ lines.push(` try { __v = await __runStepAttempts(stepId, phaseName, thunk, o) }`);
623
+ lines.push(` catch (err) { return await __settleStep({ stepId: stepId, phase: phaseName, outcome: 'failed', error: err }) }`);
624
+ lines.push(` return await __settleStep({ stepId: stepId, phase: phaseName, outcome: 'ok', value: __v })`);
625
+ lines.push(`}`);
626
+ lines.push(`// the attempt loop. Order per invocation attempt: budget → seq(dispatch) → call`);
627
+ lines.push(`// → seq(settle) → retry decision. seq allocation and the call are the SAME synchronous`);
628
+ lines.push(`// statement pair — never separable by an async write (AM-2).`);
629
+ lines.push(`async function __runStepAttempts(stepId, phaseName, thunk, o) {`);
630
+ lines.push(` const opts = o || {}`);
631
+ lines.push(` const maxAttempts = typeof opts.retryMaxAttempts === 'number' && opts.retryMaxAttempts >= 1 ? opts.retryMaxAttempts : 1 // INCLUDES the initial attempt (parse-layer posInt is the real gate; this is defense-in-depth)`);
632
+ lines.push(` let lastErr = null`);
633
+ lines.push(` let lastSnap = []`);
634
+ lines.push(` for (let attempt = 1; attempt <= maxAttempts; attempt++) {`);
635
+ lines.push(` __spendBudget(stepId)`);
636
+ lines.push(` const invocationId = stepId + (opts.itemKey == null ? '' : ':' + opts.itemKey) + '#' + (++__invocationN)`);
637
+ lines.push(` // dispatch transition — seq allocated synchronously, immediately before the call`);
638
+ lines.push(` if (__hooks.onDispatch) { __hooks.onDispatch({ invocationId: invocationId, stepId: stepId, itemKey: opts.itemKey == null ? null : String(opts.itemKey), attempt: attempt, phase: phaseName, model: opts.model == null ? null : String(opts.model), causedBy: (opts.causedBy || []).filter(function (n) { return typeof n === 'number' && n > 0 }) }) } else { __seqFallback++ }`);
639
+ lines.push(` let value = null`);
640
+ lines.push(` let outcome = 'ok'`);
641
+ lines.push(` try {`);
642
+ lines.push(` __agentCalls++`);
643
+ lines.push(` value = await thunk()`);
644
+ lines.push(` if (value === null || value === undefined) outcome = 'null'`);
645
+ lines.push(` } catch (err) {`);
646
+ lines.push(` outcome = 'error'`);
647
+ lines.push(` lastErr = err`);
648
+ lines.push(` lastSnap = __errSnap(err) // ONE snapshot per failure — log AND classify consume it (round-6 B3)`);
649
+ lines.push(` }`);
650
+ lines.push(` // settle transition — seq allocated synchronously in the continuation, BEFORE any message`);
651
+ lines.push(` // rendering (QE round-3 B3: a null-prototype throw must not lose the settle event — the`);
652
+ lines.push(` // round-2 catch logged via String(err) FIRST, which itself threw and replaced the failure)`);
653
+ lines.push(` let settleSeq = -1`);
654
+ lines.push(` if (__hooks.onSettle) { settleSeq = __hooks.onSettle({ invocationId: invocationId, outcome: outcome }) } else { settleSeq = ++__seqFallback }`);
655
+ lines.push(` __settled[__settleKey(stepId, opts.itemKey, opts.occurrence)] = settleSeq`);
656
+ lines.push(` if (outcome === 'error') { log('runStep ' + stepId + ' attempt ' + attempt + '/' + maxAttempts + ' threw: ' + (lastSnap.length > 0 ? lastSnap[0].text : '[no failure snapshot]')) }`);
657
+ lines.push(` if (outcome === 'ok') return value`);
658
+ lines.push(` // retry decision (G4): the failure is CLASSIFIED against the closed enum and retried ONLY`);
659
+ lines.push(` // when its class is in this step's retryOn list — retryOn: [] means ONE attempt, always.`);
660
+ lines.push(` // A non-array retryOn is treated as [] (schema validation upstream owns the shape; the`);
661
+ lines.push(` // runtime never lets a string's indexOf smuggle a class in — QE round-3 B3 hardening).`);
662
+ lines.push(` const failureClass = __classifyFailure(outcome, lastSnap)`);
663
+ lines.push(` const retryList = Array.isArray(opts.retryOn) ? opts.retryOn : []`);
664
+ lines.push(` const retryable = attempt < maxAttempts && failureClass !== null && retryList.indexOf(failureClass) !== -1`);
665
+ lines.push(` if (!retryable) {`);
666
+ lines.push(` // the durable flush of a terminal failure now lives in __settleStep (round-5 B3): this`);
667
+ lines.push(` // throw — like the budget guard's — is caught by the runStep wrapper and settled there.`);
668
+ lines.push(` if (outcome === 'error') { throw lastErr }`);
669
+ lines.push(` return null // a dead agent is a named null, never a fake result`);
670
+ lines.push(` }`);
671
+ lines.push(` // v1 retries are IMMEDIATE (round-6 narrowing): the retry-timing family`);
672
+ lines.push(` // (initialDelayMs/backoffMultiplier/maxDelayMs/jitter) is validated-away at the plan`);
673
+ lines.push(` // layer (ENACT-RETRY-TIMING) — no delay code exists here to drift, mis-copy, or skip.`);
674
+ lines.push(` log('runStep ' + stepId + ': attempt ' + attempt + ' ' + outcome + ' (class ' + failureClass + ') — retrying IMMEDIATELY (idempotent step, closed failure classes; v1 has no retry timing)')`);
675
+ lines.push(` }`);
676
+ lines.push(` if (lastErr !== null) throw lastErr`);
677
+ lines.push(` return null`);
678
+ lines.push(`}`);
679
+ lines.push(`// join helper — explicit policy from the closed set; a dispatched branch is never skippable`);
680
+ lines.push(`function joinRegion(results, o) {`);
681
+ lines.push(` const policy = o && o.policy ? o.policy : 'all-activated'`);
682
+ lines.push(` const failures = []`);
683
+ lines.push(` for (let i = 0; i < results.length; i++) { if (results[i] === null || results[i] === undefined) failures.push(i) }`);
684
+ lines.push(` if (policy === 'any') { if (failures.length === results.length) { throw new Error('join ' + o.region + ': every branch failed (policy any)') } return { ok: true, values: results, failures: failures } }`);
685
+ lines.push(` const quorum = /^quorum:([1-9][0-9]*)$/.exec(policy)`);
686
+ lines.push(` if (quorum) { const okN = results.length - failures.length; if (okN < Number(quorum[1])) { throw new Error('join ' + o.region + ': quorum ' + quorum[1] + ' not met (' + okN + ' ok)') } return { ok: true, values: results, failures: failures } }`);
687
+ lines.push(` if (failures.length > 0) { throw new Error('join ' + o.region + ': ' + failures.length + ' dispatched branch(es) failed under policy ' + policy + ' — a dispatched branch is never skippable') }`);
688
+ lines.push(` return { ok: true, values: results, failures: [] }`);
689
+ lines.push(`}`);
690
+ if (plan.steps.some((s) => s.kind === 'fanout')) {
691
+ // ── QUIESCENCE (QE round-7 B3, the round-6 reviewer's FOURTH CLASS: structured-concurrency /
692
+ // quiescence ownership). MEASURED by Codex on the previous shape: the region awaited a
693
+ // FAIL-FAST parallel(...), so when member `m:i1` rejected, `m:i2` was still PENDING — the
694
+ // workflow reached a terminal rejection with a dispatched invocation still LIVE and able to
695
+ // settle (and write trace) AFTER terminal exit. "The structural source guard passes because
696
+ // every lexical await is inside a try; containment is not quiescence."
697
+ //
698
+ // __drainAll gives the region ALL-SETTLED semantics: every branch thunk is wrapped so it can
699
+ // never reject, so the underlying parallel() awaits EVERY activated branch to settlement (each
700
+ // one recording its own durable settle through runStep/__settleStep) before this function
701
+ // returns. Only then does the PRIMARY failure propagate — and "primary" is the branch that
702
+ // settled its failure FIRST (the exact error fail-fast would have raised), not the
703
+ // lowest-indexed one. On the success path the values array is byte-for-byte what parallel()
704
+ // returned, so the join sees the same input it always did.
705
+ lines.push(`async function __drainAll(thunks) {`);
706
+ lines.push(` const __out = []`);
707
+ lines.push(` let __order = 0`);
708
+ lines.push(` const __wrapped = thunks.map(function (t, i) {`);
709
+ lines.push(` return async function () {`);
710
+ lines.push(` try { const v = await t(); __out[i] = { ok: true, value: v, at: __order++ }; return v }`);
711
+ lines.push(` catch (e) { __out[i] = { ok: false, error: e, at: __order++ }; return null }`);
712
+ lines.push(` }`);
713
+ lines.push(` })`);
714
+ lines.push(` const __results = await parallel(__wrapped) // no branch can reject ⇒ every activated branch is awaited to SETTLEMENT`);
715
+ lines.push(` let __primary = null`);
716
+ lines.push(` for (let i = 0; i < __out.length; i++) {`);
717
+ lines.push(` const o = __out[i]`);
718
+ lines.push(` if (o && o.ok !== true && (__primary === null || o.at < __primary.at)) { __primary = o }`);
719
+ lines.push(` }`);
720
+ lines.push(` if (__primary !== null) { throw __primary.error } // drained FIRST, then the primary failure propagates`);
721
+ lines.push(` return __results`);
722
+ lines.push(`}`);
723
+ }
724
+ if (traceOn) {
725
+ lines.push(`// trace wiring (blob-provided emitter; hooks INSIDE runStep — ADR-003)`);
726
+ lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP)`);
727
+ lines.push(`__hooks.onDispatch = function (e) { return traceOnDispatch(__traceState, e) }`);
728
+ lines.push(`__hooks.onSettle = function (e) { return traceOnSettle(__traceState, e) }`);
729
+ lines.push(`async function __traceFlushNow(phaseName, stepLabel) {`);
730
+ lines.push(` if (TRACE_FILE === null) { return }`);
731
+ lines.push(` // cmd must be let: the trace payload stays LEFT and must never be replaced by the fa-record panel leg; both ride the SAME writer agent.`);
732
+ lines.push(` let cmd = traceFlushCmd(__traceState, TRACE_FILE)`);
733
+ lines.push(` if (cmd === null) { return }`);
734
+ lines.push(` const fa = traceFaRecordCmd(DZ_BIN, LOOP_SLUG, (typeof stepLabel === 'string' && stepLabel !== '') ? stepLabel : phaseName, REPO_DIR)`);
735
+ lines.push(` if (fa !== null) { cmd = cmd + ' && { ' + fa + ' || true; }' }`);
736
+ lines.push(` else if (REPO_DIR === null && !__faLegWarned) { __faLegWarned = true; log('fa-record leg skipped — the live panel was not updated because no args.repo was given (the trace flush still runs)') }`);
737
+ lines.push(` // the flush agent is infra, not a step (it would otherwise recurse) // loop-lint: infra-agent`);
738
+ lines.push(` __agentCalls++`);
739
+ lines.push(` await agent('Run EXACTLY this one shell command via your Bash tool and reply with only OK: ' + cmd, { label: 'trace:flush', phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
740
+ lines.push(`}`);
741
+ lines.push(`async function __ledgerAppend(phaseName, outcome) {`);
742
+ lines.push(` if (__ledgerDone) { return } __ledgerDone = true`);
743
+ lines.push(` // Ledger telemetry is SECONDARY: this whole body is total and can never fail the run.`);
744
+ lines.push(` try {`);
745
+ lines.push(` if (REPO_DIR === null) { log('ledger:append skipped — ledger row was not written because no args.repo was given'); return }`);
746
+ lines.push(` // + 1 is THIS ledger writer, which is about to be invoked and not yet counted.`);
747
+ lines.push(` const line = traceLedgerLine({ slug: LOOP_SLUG, runId: RUN_ID, planDigest: PLAN_DIGEST, agents: __agentCalls + 1, outcome: outcome, date: A.date })`);
748
+ lines.push(` if (line === null) { log('ledger:append skipped — traceLedgerLine returned null'); return }`);
749
+ lines.push(` const cmd = traceLedgerAppendCmd(REPO_DIR, line)`);
750
+ lines.push(` if (cmd === null) { log('ledger:append skipped — traceLedgerAppendCmd returned null'); return }`);
751
+ lines.push(` __agentCalls++`);
752
+ lines.push(` const reply = await agent('Run EXACTLY this one shell command via your Bash tool and reply with only its stdout: ' + cmd, { label: 'ledger:append', phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
753
+ lines.push(` if (!/LEDGER-OK/.test(String(reply))) { log('ledger:append UNVERIFIED — ledger row write was not confirmed; run continues') }`);
754
+ lines.push(` } catch (_le) { log('ledger:append failed as a SECONDARY event: ' + __errText(_le) + ' — run continues') }`);
755
+ lines.push(`}`);
756
+ } else {
757
+ lines.push(`async function __traceFlushNow(phaseName, stepLabel) { /* trace.emit=false — no trace plane; fitness-suite verification is NOT claimable for this loop */ }`);
758
+ lines.push(`async function __ledgerAppend(phaseName, outcome) { /* trace off — no agents counted, no ledger row */ }`);
759
+ }
760
+ if (ckptOn) {
761
+ lines.push(`// checkpoint wiring (blob-provided pure half; the read/write agents are infra) — the resume`);
762
+ lines.push(`// guard hashes the FULL exec fingerprint (EXEC_FP), not inputHash alone (AM-10), plus the`);
763
+ lines.push(`// plan-declared checkpoint schema version (a schema bump invalidates every prior resume).`);
764
+ lines.push(`// QE round-3 B1: these helpers are INVOKED by every checkpointed step's generated wiring —`);
765
+ lines.push(`// __ckptLoad reads the store once at run start, __ckptResume decides skip-vs-run per step`);
766
+ lines.push(`// (fingerprint + artifact match via the blob's decideCheckpointResume), __ckptAppend persists`);
767
+ lines.push(`// after settle. Round 2 defined them and never called them — the schema promised a resume the`);
768
+ lines.push(`// workflow did not perform.`);
769
+ lines.push(`const CKPT_DIR = TRACE_DIR === null ? null : TRACE_DIR + '/.fa-state'`);
770
+ lines.push(`const CKPT_SCHEMA = ${jsString(CKPT_SCHEMA_DEFAULT)} // v1 pins the schema stamp (checkpointing.schemaVersion is validated-away — ENACT-CKPT-OPT)`);
771
+ lines.push(`const __ckptMode = resumeMode(A.resume)`);
772
+ lines.push(`let __ckptEntries = {}`);
773
+ lines.push(`let __ckptListing = new Set()`);
774
+ lines.push(`function __ckptInputHash(stage, parts) { return checkpointInputHash(stage, [EXEC_FP, CKPT_SCHEMA].concat(parts)) }`);
775
+ lines.push(`// __ckptLoad is TOTAL (round-6 B3: a rejected infra await must not exit around the settle`);
776
+ lines.push(`// discipline): a failed read is NAMED and the run continues LIVE — the safe direction`);
777
+ lines.push(`// (nothing resumes; nothing is falsely resumed).`);
778
+ lines.push(`async function __ckptLoad(phaseName) {`);
779
+ lines.push(` if (CKPT_DIR === null) { log('checkpointing enabled but no traceDir given — running LIVE; nothing resumes, nothing persists (named, never silent)'); return }`);
780
+ lines.push(` try {`);
781
+ lines.push(` const cmd = checkpointReadCmd(TRACE_DIR)`);
782
+ lines.push(` __agentCalls++`);
783
+ lines.push(` const out = await agent('Run EXACTLY this one shell command via your Bash tool and reply with ONLY its raw stdout: ' + cmd, { label: 'ckpt:read', phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
784
+ lines.push(` const parsed = parseCheckpointRead(typeof out === 'string' ? out : '')`);
785
+ lines.push(` __ckptEntries = parsed.entries`);
786
+ lines.push(` __ckptListing = parsed.listing`);
787
+ lines.push(` if (parsed.malformedLines > 0) { log('checkpoint read: ' + parsed.malformedLines + ' malformed line(s) — counted, never silently ignored') }`);
788
+ lines.push(` } catch (_ce) { log('checkpoint read threw: ' + __errText(_ce) + ' — running LIVE (named, never silent; nothing resumes)') }`);
789
+ lines.push(`}`);
790
+ lines.push(`function __ckptResume(stage, inputHash, artifactRel) {`);
791
+ lines.push(` if (CKPT_DIR === null) { return false }`);
792
+ lines.push(` const d = decideCheckpointResume({ mode: __ckptMode, entry: __ckptEntries[stage], inputHash: inputHash, artifactRel: artifactRel, listing: __ckptListing })`);
793
+ lines.push(` if (!d.resume && d.reason !== 'no-checkpoint' && d.reason !== 'mode-never') { log('checkpoint: ' + stage + ' NOT resumed (' + d.reason + ') — running live') }`);
794
+ lines.push(` return d.resume`);
795
+ lines.push(`}`);
796
+ lines.push(`// __ckptAppend is TOTAL (round-6 B3): a failed append is NAMED and the run continues — the`);
797
+ lines.push(`// step itself succeeded, and an infra-write failure must never replace that outcome (the`);
798
+ lines.push(`// next run simply re-runs the un-checkpointed step).`);
799
+ lines.push(`async function __ckptAppend(stage, phaseName, inputHash, result) {`);
800
+ lines.push(` if (CKPT_DIR === null) { return }`);
801
+ lines.push(` const line = serializeCheckpoint(stage, inputHash, result)`);
802
+ lines.push(` if (line === null) { log('checkpoint: ' + stage + ' not persisted (null/oversize/unserializable — named, never silent)'); return }`);
803
+ lines.push(` try {`);
804
+ lines.push(` const cmd = checkpointAppendCmd(TRACE_DIR, line)`);
805
+ lines.push(` __agentCalls++`);
806
+ lines.push(` await agent('Run EXACTLY this one shell command via your Bash tool and reply with only OK: ' + cmd, { label: 'ckpt:write:' + stage, phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
807
+ lines.push(` } catch (_ce) { log('checkpoint append for ' + stage + ' threw: ' + __errText(_ce) + ' — run continues (the step outcome stands; the next run re-runs this step)') }`);
808
+ lines.push(`}`);
809
+ }
810
+ if (plan.subsystems?.trainingPairs === true) {
811
+ lines.push(`// training-pair capture wiring (AM-9: reached ONLY via the explicit opt-in; QE round-3 B1:`);
812
+ lines.push(`// the blob helpers are now INVOKED — one pair per settled top-level agent step, per-stage`);
813
+ lines.push(`// granularity). ts is null (the sandbox has no clock — honest, never a fake timestamp).`);
814
+ lines.push(`// __tpCapture is TOTAL (round-6 B3 + the capture contract: a capture failure never fails`);
815
+ lines.push(`// the run) — a failed write is NAMED and the run continues. ROUND-7 (Codex round-6 R2`);
816
+ lines.push(`// MEASURED): totality used to start at the WRITE — buildTrainingPair/serializeTrainingPair`);
817
+ lines.push(`// ran BEFORE the try, so an agent returning a null-prototype object carrying a BigInt threw`);
818
+ lines.push(`// 'TypeError: Cannot convert object to primitive value' out of the serializer and REPLACED a`);
819
+ lines.push(`// SUCCESSFUL step (the step's own catch settled it as failed). The whole capture — pair`);
820
+ lines.push(`// construction, serialization and write — now rides ONE catch, the same discipline as`);
821
+ lines.push(`// __errText/__phaseFlush: a capture failure is a SECONDARY logged event, never an outcome.`);
822
+ lines.push(`const __captureFailures = []`);
823
+ lines.push(`async function __tpCapture(stage, phaseName, input, output, model, resumed) {`);
824
+ lines.push(` if (TRACE_DIR === null) { return }`);
825
+ lines.push(` let __captureMode = null`);
826
+ lines.push(` try {`);
827
+ lines.push(` // enabled is true because this entire wiring block is gated at render time by the subsystem opt-in.`);
828
+ lines.push(` const recordCount = output === null || output === undefined ? 0 : 1`);
829
+ lines.push(` const mode = decideCaptureMode({ enabled: true, resumed: resumed === true, recordCount: recordCount })`);
830
+ lines.push(` __captureMode = mode`);
831
+ lines.push(` if (mode === 'skip-disabled') { return }`);
832
+ lines.push(` if (mode === 'skip-empty') { log('training-pair: ' + stage + ' not captured (null/undefined output — named, never silent)'); __captureFailures.push(captureFailureRecord(stage, mode, 'empty-output', null)); return }`);
833
+ lines.push(` const pair = buildTrainingPair({ slug: RUN_ID, stage: stage, ts: null, input: input, output: output, evaluation: null, provenance: { model: model === null ? 'unknown' : model, role: stage }, captureMode: mode === 'backfill' ? 'backfill' : 'capture', resumed: resumed === true })`);
834
+ lines.push(` const line = serializeTrainingPair(pair)`);
835
+ lines.push(` if (line === null) { log('training-pair: ' + stage + ' not captured (unserializable) — named, never silent'); __captureFailures.push(captureFailureRecord(stage, mode, 'unserializable', null)); return }`);
836
+ lines.push(` if (mode === 'capture') {`);
837
+ lines.push(` const cmd = trainingPairAppendCmd(TRACE_DIR, RUN_ID, stage, line)`);
838
+ lines.push(` __agentCalls++`);
839
+ lines.push(` await agent('Run EXACTLY this one shell command via your Bash tool and reply with only OK: ' + cmd, { label: 'tp:write:' + stage, phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
840
+ lines.push(` return`);
841
+ lines.push(` }`);
842
+ lines.push(` if (mode === 'backfill') {`);
843
+ lines.push(` // Exclude pair.slug (RUN_ID) and pair.ts (null) from the mark key: normalized input/output`);
844
+ lines.push(` // identify the pair across runIds, which is the cross-run at-most-once property.`);
845
+ lines.push(` const markKey = fnv1a64(stage + '\\0' + pair.input + '\\0' + pair.output)`);
846
+ lines.push(` const cmd = trainingPairBackfillCmd(TRACE_DIR, RUN_ID, stage, [line], markKey)`);
847
+ lines.push(` __agentCalls++`);
848
+ lines.push(` const readback = await agent('Run EXACTLY this one shell command via your Bash tool and reply with ONLY its raw stdout: ' + cmd, { label: 'tp:backfill:' + stage, phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
849
+ lines.push(` const status = typeof readback === 'string' ? readback.trim() : ''`);
850
+ lines.push(` if (status === TP_BACKFILL_OK) { log('training-pair: ' + stage + ' backfilled from the checkpoint'); return }`);
851
+ lines.push(` if (status === TP_BACKFILL_SKIP) { log('training-pair: ' + stage + ' pair file already existed; nothing written'); return }`);
852
+ lines.push(` if (status === TP_BACKFILL_DUP) { log('training-pair: ' + stage + ' another run already captured this pair; nothing written'); return }`);
853
+ lines.push(` log('training-pair: ' + stage + ' checkpoint backfill UNVERIFIED: ' + __errText(readback))`);
854
+ lines.push(` __captureFailures.push(captureFailureRecord(stage, mode, 'backfill-unverified', readback))`);
855
+ lines.push(` }`);
856
+ lines.push(` } catch (_ce) { log('training-pair capture for ' + stage + ' threw: ' + __errText(_ce) + ' — run continues (capture is never load-bearing)'); __captureFailures.push(captureFailureRecord(stage, __captureMode, 'threw', __errText(_ce))) }`);
857
+ lines.push(`}`);
858
+ }
859
+ if (plan.steps.some((s) => s.kind === 'gate')) {
860
+ lines.push(`// gate verdict parsing (QE round-3 B1, tightened round-4) — parse-NEVER-synthesize, with the`);
861
+ lines.push(`// EXACTLY-ONE-ENDING-LINE protocol enforced: the verdict must be an ANCHORED line ("GATE: PASS"`);
862
+ lines.push(`// or "GATE: FAIL" alone on its line), it must be the LAST non-empty line of the reply, and it`);
863
+ lines.push(`// must be the ONLY anchored verdict line. Embedded mid-reply "GATE: PASS" text never counts,`);
864
+ lines.push(`// "GATE: PASS" followed by trailing prose is invalid, and "GATE: FAIL … GATE: PASS" is an`);
865
+ lines.push(`// INVALID verdict (never a success) — routed like a failure (redo/fail route), never a pass.`);
866
+ lines.push(`function __gateVerdict(r) {`);
867
+ lines.push(` if (typeof r !== 'string') return 'invalid'`);
868
+ lines.push(` const vLines = r.split('\\n')`);
869
+ lines.push(` const vRe = /^\\s*GATE:\\s*(PASS|FAIL)\\s*$/`);
870
+ lines.push(` let vCount = 0`);
871
+ lines.push(` let vLast = ''`);
872
+ lines.push(` for (let i = 0; i < vLines.length; i++) {`);
873
+ lines.push(` if (vRe.test(vLines[i])) { vCount++ }`);
874
+ lines.push(` if (vLines[i].trim() !== '') { vLast = vLines[i] }`);
875
+ lines.push(` }`);
876
+ lines.push(` const vEnd = vRe.exec(vLast)`);
877
+ lines.push(` if (vCount !== 1 || vEnd === null) return 'invalid'`);
878
+ lines.push(` return vEnd[1] === 'PASS' ? 'pass' : 'fail'`);
879
+ lines.push(`}`);
880
+ }
881
+ lines.push(GE('runtime'));
882
+ return lines.join('\n');
883
+ }
884
+
885
+ export interface ExecAxisInputs { topology: string; prompts: string; models: string; tools: string }
886
+
887
+ /** The four axis INPUT strings, built so no axis subsumes another (QE round-2 G2: the round-1
888
+ * prompts axis embedded the full rendered step text, which already carried dep wiring and per-step
889
+ * models — three of the four "independent" axes were mutually redundant, and deleting two of them
890
+ * left every test green while a real resume-guard hole opened):
891
+ * topology — structural plan shape (ids, kinds, phases, deps, retry/budget/pause config,
892
+ * fanouts/joins/pauses) with prompt and model EXCLUDED;
893
+ * prompts — per-step prompt text ONLY;
894
+ * models — per-step declared model ONLY (covers fanout chain members too);
895
+ * tools — the selected blob roster with content hashes.
896
+ */
897
+ export function computeExecAxisInputs(plan: LoopPlan): ExecAxisInputs {
898
+ const norm = normalizePlan(plan);
899
+ const blobs = selectBlobs(norm);
900
+ const ckptOn = norm.checkpointing?.enabled === true || norm.subsystems?.checkpoints === true;
901
+ const traceOn = norm.trace?.emit === true;
902
+ const axMemberIds = new Set<string>();
903
+ for (const f of norm.fanouts ?? []) for (const c of f.chain ?? []) axMemberIds.add(c);
904
+ return {
905
+ topology: JSON.stringify({
906
+ steps: norm.steps.map((s) => ({
907
+ id: s.stepId,
908
+ kind: s.kind,
909
+ phase: s.phase,
910
+ deps: s.deps ?? [],
911
+ concurrency: s.concurrency ?? null,
912
+ retry: s.retry ?? null,
913
+ budget: s.budget ?? null,
914
+ pauseState: s.pauseState ?? null,
915
+ idempotent: s.idempotent ?? false,
916
+ // QE round-3 B1/B2: these fields now DRIVE generated wiring (landed barrier, checkpoint
917
+ // consult, dispatch route, prompt artifact-contract lines) — effective values, so the
918
+ // omitted-vs-explicit-default collapse the round-2 reviewer confirmed stays intact.
919
+ deliverable: s.deliverable ?? 'return-value',
920
+ dispatch: s.dispatch ?? 'inline',
921
+ // QE round-6 (narrowing): the per-step checkpoint OPT-OUT field is validated-away
922
+ // (ENACT-CKPT-OPT), so the axis records the pure DERIVED value — a top-level agent step
923
+ // checkpoints iff checkpointing is on. The round-4/5 false-flip family (omitted vs
924
+ // explicit `false`) is unrepresentable: there is no field left to disagree with the
925
+ // effective value.
926
+ checkpoint: ckptOn && s.kind === 'agent' && !axMemberIds.has(s.stepId),
927
+ writes: s.artifacts?.writes ?? [],
928
+ reads: s.artifacts?.reads ?? [],
929
+ })),
930
+ gates: norm.gates ?? [],
931
+ fanouts: norm.fanouts ?? [],
932
+ joins: norm.joins ?? [],
933
+ pauses: norm.pauses ?? [],
934
+ // QE round-3 B2 (reviewer R1, CONFIRMED): enacted-WIRING flags that can change the generated
935
+ // runtime WITHOUT changing the blob roster. checkpointing.enabled flips checkpoint wiring even
936
+ // when the checkpoints blob was already selected through the training-pairs requires-closure —
937
+ // the round-2 fingerprint missed that (changedAxes=[] on a real wiring change). The selection
938
+ // REASON is fingerprinted here, not only the resulting roster.
939
+ // QE round-6 (narrowing): schemaVersion is validated-away (ENACT-CKPT-OPT) — the stamp is
940
+ // PINNED, so the axis records the pin, and the round-3 false-flip is unrepresentable.
941
+ wiring: { checkpoints: ckptOn, ckptSchema: ckptOn ? CKPT_SCHEMA_DEFAULT : null, trace: traceOn },
942
+ }),
943
+ // QE round-3 B2 (reviewer R1, CONFIRMED): JSON-encoded per-step prompts — a LENGTH-SAFE
944
+ // encoding. The round-2 `${id}:${prompt}` newline join let two different prompt sets serialize
945
+ // identically across step boundaries (a.prompt="A\nb:B",b.prompt="C" vs a.prompt="A",
946
+ // b.prompt="B\nb:C") — a genuine resume-guard hole.
947
+ prompts: JSON.stringify(norm.steps.map((s) => ({ id: s.stepId, prompt: s.prompt ?? null }))),
948
+ models: JSON.stringify(norm.steps.map((s) => ({ id: s.stepId, model: s.model ?? null }))),
949
+ tools: JSON.stringify(blobs.map((b) => ({ name: b.name, version: b.version, contentHash: b.contentHash }))),
950
+ };
951
+ }
952
+
953
+ /** Per-axis hashes — exposed so the AM-10 test can assert each axis INDEPENDENTLY (a single-axis
954
+ * plan change must flip exactly its own axis hash), not only the aggregate. */
955
+ export function execFingerprintAxisHashes(input: ExecAxisInputs): ExecAxisInputs {
956
+ return {
957
+ topology: sha256(input.topology),
958
+ prompts: sha256(input.prompts),
959
+ models: sha256(input.models),
960
+ tools: sha256(input.tools),
961
+ };
962
+ }
963
+
964
+ /** Independent-axes execution fingerprint (FR-1.6/AM-10): each axis hashed separately, then the
965
+ * four axis hashes hashed together — a change in ANY ONE axis flips the fingerprint. */
966
+ export function computeExecFingerprint(input: ExecAxisInputs): string {
967
+ const h = execFingerprintAxisHashes(input);
968
+ const axes = [
969
+ 'topology:' + h.topology,
970
+ 'prompts:' + h.prompts,
971
+ 'models:' + h.models,
972
+ 'tools:' + h.tools,
973
+ ];
974
+ return sha256(axes.join('\n'));
975
+ }
976
+
977
+ /** Render a plan to a full script. Deterministic. */
978
+ export function renderPlan(plan: LoopPlan): RenderResult {
979
+ const norm = normalizePlan(plan);
980
+ const digest = planDigest(plan);
981
+ const blobs = selectBlobs(norm);
982
+
983
+ // meta (GENERATED from the plan; INV-7: phases in first-reference order)
984
+ const phaseOrder: string[] = [];
985
+ for (const s of norm.steps) if (!phaseOrder.includes(s.phase)) phaseOrder.push(s.phase);
986
+ const metaLines: string[] = [];
987
+ metaLines.push(`export const meta = {`);
988
+ metaLines.push(` name: ${jsString(norm.name)},`);
989
+ metaLines.push(` description: ${jsString(norm.description)},`);
990
+ metaLines.push(` whenToUse: ${jsString(norm.whenToUse)},`);
991
+ metaLines.push(` phases: [`);
992
+ for (const p of phaseOrder) metaLines.push(` { title: ${jsString(p)}, detail: ${jsString('phase ' + p)} },`);
993
+ metaLines.push(` ],`);
994
+ metaLines.push(`}`);
995
+
996
+ // step bodies in plan order; fanout steps render their region, members render inside it
997
+ const fanoutMembers = new Set<string>();
998
+ for (const f of norm.fanouts ?? []) for (const c of f.chain ?? []) fanoutMembers.add(c);
999
+ const joinSteps = new Set((norm.joins ?? []).map((j) => j.stage));
1000
+ const env: RenderEnv = {
1001
+ ckptOn: norm.checkpointing?.enabled === true || norm.subsystems?.checkpoints === true,
1002
+ tpOn: norm.subsystems?.trainingPairs === true,
1003
+ gateTargets: new Set(
1004
+ (norm.gates ?? [])
1005
+ .filter((g) => typeof g.failRoute === 'string' && !g.failRoute.startsWith('terminal:'))
1006
+ .map((g) => g.failRoute as string),
1007
+ ),
1008
+ memberIds: fanoutMembers,
1009
+ };
1010
+ const stepChunks: string[] = [];
1011
+ const phaseCalls = new Set<string>();
1012
+ // QE round-3 B1: the checkpoint store is CONSULTED — loaded once at run start, before any
1013
+ // dispatch. __ckptLoad is TOTAL (round-6), so this top-level await cannot reject.
1014
+ if (env.ckptOn) stepChunks.push(`await __ckptLoad(${jsString(norm.steps[0]?.phase ?? 'Start')})`);
1015
+ for (const s of norm.steps) {
1016
+ if (fanoutMembers.has(s.stepId)) continue; // rendered inside their fanout region
1017
+ if (joinSteps.has(s.stepId)) continue; // the join is rendered by its fanout region (joinRegion call)
1018
+ if (!phaseCalls.has(s.phase)) {
1019
+ phaseCalls.add(s.phase);
1020
+ stepChunks.push(`phase(${jsString(s.phase)})`);
1021
+ }
1022
+ if (s.kind === 'fanout') stepChunks.push(renderFanout(norm, s));
1023
+ else stepChunks.push(renderStep({ step: s, depSettles: (s.deps ?? []).filter((d) => norm.steps.some((x) => x.stepId === d)) }, norm, env));
1024
+ // flush at each step boundary via the TOTAL __phaseFlush (round-6 B3: the naked
1025
+ // `await __traceFlushNow` here was the hole whose rejection REPLACED a successful outcome;
1026
+ // zero extra agent calls when the buffer is empty)
1027
+ stepChunks.push(`await __phaseFlush(${jsString(s.phase)})`);
1028
+ }
1029
+
1030
+ const blobChunks = blobs.map((b) => [B(b.name, b.version, b.contentHash, b.sourcePath), b.code, BE(b.name, b.version)].join('\n'));
1031
+
1032
+ // fingerprint axes (G2: non-redundant inputs — computed from the PLAN, never the rendered text,
1033
+ // so no axis subsumes another; the runtime is rendered with the final value)
1034
+ const execFp = computeExecFingerprint(computeExecAxisInputs(norm));
1035
+
1036
+ const header = `// ── LOOP-PLAN plan=loop-plan/1 digest=sha256:${digest} exec-fp=sha256:${execFp} generator=${LOOP_RENDER_GENERATOR} ──`;
1037
+ const runtime = renderRuntime(norm, digest, execFp, blobs);
1038
+ const completedValue = `{ phase: 'COMPLETED', runId: RUN_ID, planDigest: PLAN_DIGEST, execFp: EXEC_FP }`;
1039
+
1040
+ const ending = [
1041
+ G('epilogue'),
1042
+ `traceCloseIfOn()`,
1043
+ `function traceCloseIfOn() { ${norm.trace?.emit === true ? 'traceClose(__traceState)' : '/* trace off */'} }`,
1044
+ `await __phaseFlush(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')})`,
1045
+ `await __ledgerAppend(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, 'completed')`,
1046
+ `// the COMPLETED return rides the single exit too (round-5 B3): the epilogue flush happens inside`,
1047
+ `// __settleStep, so a flush rejection is a logged secondary event, never a replaced COMPLETED.`,
1048
+ `return await __settleStep({ stepId: '__epilogue__', phase: ${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, outcome: 'terminal', value: ${completedValue} })`,
1049
+ GE('epilogue'),
1050
+ ].join('\n');
1051
+
1052
+ const text = [
1053
+ metaLines.join('\n'),
1054
+ header,
1055
+ ...blobChunks,
1056
+ runtime,
1057
+ `try {`,
1058
+ ...stepChunks,
1059
+ ending,
1060
+ norm.subsystems?.trainingPairs === true
1061
+ ? `} catch (__runErr) { await __ledgerAppend(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, 'failed'); if (__captureFailures.length > 0) { log('training-pair capture failures this run: ' + __captureFailures.length + ' — ' + __captureFailures.map(function (f) { return f.stage + ':' + f.reason }).join(', ')) } throw __runErr }`
1062
+ : `} catch (__runErr) { await __ledgerAppend(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, 'failed'); throw __runErr }`,
1063
+ '',
1064
+ ].join('\n\n');
1065
+
1066
+ const userRegions = [...text.matchAll(/BEGIN USER (\S+)/g)].map((m) => m[1] as string);
1067
+ return {
1068
+ text,
1069
+ planJson: JSON.stringify(norm, null, 2) + '\n',
1070
+ execFingerprint: execFp,
1071
+ manifest: {
1072
+ planDigest: digest,
1073
+ execFingerprint: execFp,
1074
+ blobs: blobs.map((b) => ({ name: b.name, version: b.version, contentHash: b.contentHash })),
1075
+ steps: norm.steps.map((s) => s.stepId),
1076
+ userRegions,
1077
+ },
1078
+ };
1079
+ }
1080
+
1081
+ const USER_RE = /\/\/ ── BEGIN USER (\S+) ──\n([\s\S]*?)\/\/ ── END USER \1 ──/g;
1082
+
1083
+ /** Extract USER regions keyed by label. */
1084
+ export function extractUserRegions(text: string): Map<string, string> {
1085
+ const out = new Map<string, string>();
1086
+ for (const m of text.matchAll(USER_RE)) out.set(m[1] as string, m[2] as string);
1087
+ return out;
1088
+ }
1089
+
1090
+ /**
1091
+ * Merge a fresh render over an existing target (propose-never-clobber, §3.2):
1092
+ * - target has markers → splice: new BLOB/GENERATED + OLD USER bytes (byte-for-byte, INV-11);
1093
+ * a USER region with no counterpart in the new render is a NAMED conflict, never dropped.
1094
+ * - target has NO markers (hand-written) → refuse; return proposedText for `<script>.proposed.js`;
1095
+ * `--force` (the caller's flag) overwrites explicitly.
1096
+ */
1097
+ export function mergeRender(prevText: string, next: RenderResult, opts?: { force?: boolean }): MergeResult {
1098
+ const prevUsers = extractUserRegions(prevText);
1099
+ if (prevUsers.size === 0 && prevText.trim() !== '') {
1100
+ if (opts?.force === true) return { text: next.text, conflicts: [], refused: false };
1101
+ return {
1102
+ text: prevText,
1103
+ conflicts: [],
1104
+ refused: true,
1105
+ proposedText: next.text,
1106
+ };
1107
+ }
1108
+ const nextUsers = extractUserRegions(next.text);
1109
+ let text = next.text;
1110
+ for (const [label, body] of nextUsers) {
1111
+ const prev = prevUsers.get(label);
1112
+ if (prev !== undefined && prev !== body) {
1113
+ text = text.replace(
1114
+ `// ── BEGIN USER ${label} ──\n${body}// ── END USER ${label} ──`,
1115
+ `// ── BEGIN USER ${label} ──\n${prev}// ── END USER ${label} ──`,
1116
+ );
1117
+ }
1118
+ }
1119
+ const conflicts: MergeResult['conflicts'] = [];
1120
+ for (const [label] of prevUsers) {
1121
+ if (!nextUsers.has(label)) {
1122
+ conflicts.push({ stepId: label.replace(/^step:/, '').replace(/\/body$/, ''), reason: `USER region ${label} has no counterpart step in the new plan — its content was NOT carried over; recover it from the previous file` });
1123
+ }
1124
+ }
1125
+ return { text, conflicts, refused: false };
1126
+ }