@dzhechkov/harness-core 0.5.0 → 0.5.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 (96) hide show
  1. package/.dz-manifest.json +339 -75
  2. package/README.md +47 -4
  3. package/dist/codex-hooks-assets.d.ts.map +1 -1
  4. package/dist/codex-hooks-assets.js +39 -2
  5. package/dist/codex-hooks-assets.js.map +1 -1
  6. package/dist/codex-hooks-verify.d.ts +23 -2
  7. package/dist/codex-hooks-verify.d.ts.map +1 -1
  8. package/dist/codex-hooks-verify.js +29 -0
  9. package/dist/codex-hooks-verify.js.map +1 -1
  10. package/dist/codex-hooks.d.ts +90 -7
  11. package/dist/codex-hooks.d.ts.map +1 -1
  12. package/dist/codex-hooks.js +171 -21
  13. package/dist/codex-hooks.js.map +1 -1
  14. package/dist/feature-adr-routing.d.ts +22 -0
  15. package/dist/feature-adr-routing.d.ts.map +1 -1
  16. package/dist/feature-adr-routing.js +45 -0
  17. package/dist/feature-adr-routing.js.map +1 -1
  18. package/dist/index.d.ts +11 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +17 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/loop-blobs.generated.d.ts +1 -1
  23. package/dist/loop-blobs.generated.d.ts.map +1 -1
  24. package/dist/loop-blobs.generated.js +12 -3
  25. package/dist/loop-blobs.generated.js.map +1 -1
  26. package/dist/loop-plan.d.ts +70 -0
  27. package/dist/loop-plan.d.ts.map +1 -1
  28. package/dist/loop-plan.js +103 -0
  29. package/dist/loop-plan.js.map +1 -1
  30. package/dist/loop-render.d.ts.map +1 -1
  31. package/dist/loop-render.js +38 -130
  32. package/dist/loop-render.js.map +1 -1
  33. package/dist/loop-run-semantics.d.ts +130 -0
  34. package/dist/loop-run-semantics.d.ts.map +1 -0
  35. package/dist/loop-run-semantics.js +257 -0
  36. package/dist/loop-run-semantics.js.map +1 -0
  37. package/dist/loop-trace.d.ts +106 -5
  38. package/dist/loop-trace.d.ts.map +1 -1
  39. package/dist/loop-trace.js +151 -18
  40. package/dist/loop-trace.js.map +1 -1
  41. package/dist/managed-hooks.d.ts +10 -0
  42. package/dist/managed-hooks.d.ts.map +1 -1
  43. package/dist/managed-hooks.js +17 -5
  44. package/dist/managed-hooks.js.map +1 -1
  45. package/dist/named-lock.d.ts +57 -0
  46. package/dist/named-lock.d.ts.map +1 -0
  47. package/dist/named-lock.js +247 -0
  48. package/dist/named-lock.js.map +1 -0
  49. package/dist/operations.d.ts +81 -5
  50. package/dist/operations.d.ts.map +1 -1
  51. package/dist/operations.js +356 -38
  52. package/dist/operations.js.map +1 -1
  53. package/dist/parity.d.ts +70 -2
  54. package/dist/parity.d.ts.map +1 -1
  55. package/dist/parity.js +133 -2
  56. package/dist/parity.js.map +1 -1
  57. package/dist/qe-bridge.d.ts +291 -0
  58. package/dist/qe-bridge.d.ts.map +1 -0
  59. package/dist/qe-bridge.js +538 -0
  60. package/dist/qe-bridge.js.map +1 -0
  61. package/dist/score.d.ts.map +1 -1
  62. package/dist/score.js +43 -9
  63. package/dist/score.js.map +1 -1
  64. package/dist/trace-corroborate.d.ts +48 -0
  65. package/dist/trace-corroborate.d.ts.map +1 -0
  66. package/dist/trace-corroborate.js +172 -0
  67. package/dist/trace-corroborate.js.map +1 -0
  68. package/dist/workflow-run-dispatch.d.ts +230 -0
  69. package/dist/workflow-run-dispatch.d.ts.map +1 -0
  70. package/dist/workflow-run-dispatch.js +363 -0
  71. package/dist/workflow-run-dispatch.js.map +1 -0
  72. package/dist/workflow-run.d.ts +513 -0
  73. package/dist/workflow-run.d.ts.map +1 -0
  74. package/dist/workflow-run.js +1377 -0
  75. package/dist/workflow-run.js.map +1 -0
  76. package/package.json +2 -2
  77. package/sbom.json +740 -80
  78. package/src/codex-hooks-assets.ts +39 -2
  79. package/src/codex-hooks-verify.ts +55 -2
  80. package/src/codex-hooks.ts +172 -20
  81. package/src/feature-adr-routing.ts +55 -0
  82. package/src/index.ts +46 -1
  83. package/src/loop-blobs.generated.ts +12 -3
  84. package/src/loop-plan.ts +185 -0
  85. package/src/loop-render.ts +38 -128
  86. package/src/loop-run-semantics.ts +278 -0
  87. package/src/loop-trace.ts +207 -16
  88. package/src/managed-hooks.ts +26 -5
  89. package/src/named-lock.ts +277 -0
  90. package/src/operations.ts +441 -40
  91. package/src/parity.ts +177 -2
  92. package/src/qe-bridge.ts +737 -0
  93. package/src/score.ts +50 -9
  94. package/src/trace-corroborate.ts +205 -0
  95. package/src/workflow-run-dispatch.ts +459 -0
  96. package/src/workflow-run.ts +1773 -0
package/src/loop-plan.ts CHANGED
@@ -27,6 +27,10 @@
27
27
 
28
28
  import { createHash } from 'node:crypto';
29
29
  import { TRACE_KEY_RE } from './loop-trace.js';
30
+ // The ONE ceiling formula (ADR-004 Confirmation-2) — imported so the run projection's
31
+ // `budgetTotal` and the rendered script's `__budget` constant cannot drift into two ceilings.
32
+ // loop-run-semantics has no runtime import of its own, so this edge introduces no cycle.
33
+ import { computeBudgetTotal } from './loop-run-semantics.js';
30
34
 
31
35
  export const LOOP_PLAN_SCHEMA = 'loop-plan/1';
32
36
 
@@ -1437,3 +1441,184 @@ export function toTraceProjection(plan: LoopPlan): TraceProjection {
1437
1441
  expectedSteps: norm.steps.filter((s) => s.kind === 'agent' || s.kind === 'gate').map((s) => s.stepId),
1438
1442
  };
1439
1443
  }
1444
+
1445
+ // ─────────────────────────────────────────────────────────────────────────────
1446
+ // The FOURTH projection (feature dz-workflow-run, ADR-001 O1 + AM-3 doctrine).
1447
+ //
1448
+ // `dz workflow run` is a SECOND enactor of the same plan, and the AM-3 rule that kept the first
1449
+ // one honest applies to it unchanged: a consumer reads a PROJECTION, never `plan.steps`. The three
1450
+ // existing projections cannot serve it — `TraceProjection` is the invariant-checking view and
1451
+ // carries no prompts, models or budgets by design. So the scheduler gets its own view, added next
1452
+ // to the other three, and joins the same layer-1 consumer grep.
1453
+ //
1454
+ // The addition is ANALYTICAL: the `loop-plan/1` SCHEMA SURFACE (the LoopPlan/LoopStep types,
1455
+ // FIELD_DOMAINS, KNOWN_KEYS, REQUIRED_FIELDS, STEP_FIELD_KINDS, CROSS_REFS, parsePlan,
1456
+ // validatePlan, normalizePlan, planDigest) is byte-unchanged — nothing below can make a plan that
1457
+ // used to parse stop parsing, or change a digest.
1458
+ // ─────────────────────────────────────────────────────────────────────────────
1459
+
1460
+ /** One dispatching step as the scheduler needs it — everything it must know to build a prompt,
1461
+ * pick a model, spend budget, retry, and verify a deliverable landed. */
1462
+ export interface RunStepSpec {
1463
+ stepId: string;
1464
+ kind: 'agent' | 'gate';
1465
+ phase: string;
1466
+ /** USER prompt seed (`plan.steps[].prompt`); null means the plan declared none. */
1467
+ prompt: string | null;
1468
+ model: string | null;
1469
+ deliverable: Deliverable;
1470
+ reads: string[];
1471
+ writes: string[];
1472
+ tools: string[];
1473
+ /** INCLUDES the initial attempt (AM-4 semantics; 1 = no retry). */
1474
+ retryMaxAttempts: number;
1475
+ retryOn: FailureClass[];
1476
+ /** `budget.maxAgents ?? 1` — the per-step worst case a boundary reservation is built from. */
1477
+ maxAgents: number;
1478
+ /** `step['x-role'] === 'qe'` — the designed `x-` seam, not a schema field (fence: zero surgery). */
1479
+ qeRole: boolean;
1480
+ gate: { kind: string; failRoute: string | null; maxRedos: number } | null;
1481
+ }
1482
+
1483
+ /** One SCHEDULING BOUNDARY: the unit the runner reserves budget for, checkpoints, and may pause
1484
+ * BEFORE. A parallel region is ONE boundary (its worst case is reserved as a whole), which is what
1485
+ * makes "pause before the region, never mid-region" expressible. */
1486
+ export interface RunBoundary {
1487
+ /** The stepId of the stage / fanout / gate / pause this boundary enacts. */
1488
+ boundaryId: string;
1489
+ kind: 'stage' | 'region' | 'gate' | 'pause';
1490
+ /** Happens-before inputs, as BOUNDARY ids: a declared dep on a join stage or a fanout member is
1491
+ * carried as its REGION, because the member and the join are not boundaries of their own. */
1492
+ deps: string[];
1493
+ /** Present for kind 'stage' | 'gate'. */
1494
+ stage?: RunStepSpec;
1495
+ /** Present for kind 'region'. */
1496
+ region?: {
1497
+ fanout: string;
1498
+ join: string;
1499
+ joinPolicy: string;
1500
+ onInvalid: string;
1501
+ maxFanout: number;
1502
+ registry: string[];
1503
+ dedup: boolean;
1504
+ shape: ConcurrencyShape;
1505
+ chain: RunStepSpec[];
1506
+ };
1507
+ /** Present for kind 'pause'. */
1508
+ pause?: { state: string; resumeArg: string; payloadSchema: Record<string, unknown> | null };
1509
+ }
1510
+
1511
+ export interface RunProjection {
1512
+ kind: 'run-projection/1';
1513
+ /** Plan order — the render's enactment order, so both enactors walk the same sequence. */
1514
+ boundaries: RunBoundary[];
1515
+ /** `computeBudgetTotal(plan)` — the SAME number the rendered script spends against. */
1516
+ budgetTotal: number;
1517
+ /** Plan-declared `pauses[].resumeArg` keys — the enumerated argsHash exclusions (AM-13). */
1518
+ resumeArgKeys: string[];
1519
+ /** `plan.trace.emit === true` — the AM-6 gate input (a run with no trace plane is refused). */
1520
+ traceEmit: boolean;
1521
+ }
1522
+
1523
+ /** The ONLY plan view `workflow-run.ts` consumes (AM-3, fourth consumer). */
1524
+ export function toRunProjection(plan: LoopPlan): RunProjection {
1525
+ const norm = normalizePlan(plan);
1526
+ const byId = new Map(norm.steps.map((s) => [s.stepId, s]));
1527
+ const gatesById = new Map((norm.gates ?? []).map((g) => [g.stepId, g]));
1528
+ const pausesByState = new Map((norm.pauses ?? []).map((p) => [p.state, p]));
1529
+ const fanouts = norm.fanouts ?? [];
1530
+ const joinsByForStage = new Map((norm.joins ?? []).map((j) => [j.forStage, j]));
1531
+
1532
+ /** member stepId → its owning fanout stage; join stepId → its owning fanout stage. */
1533
+ const ownedByRegion = new Map<string, string>();
1534
+ for (const f of fanouts) {
1535
+ for (const c of f.chain ?? []) ownedByRegion.set(c, f.stage);
1536
+ const j = joinsByForStage.get(f.stage);
1537
+ if (j !== undefined) ownedByRegion.set(j.stage, f.stage);
1538
+ }
1539
+
1540
+ const specOf = (s: LoopStep): RunStepSpec => {
1541
+ const g = s.kind === 'gate' ? gatesById.get(s.stepId) : undefined;
1542
+ return {
1543
+ stepId: s.stepId,
1544
+ kind: s.kind === 'gate' ? 'gate' : 'agent',
1545
+ phase: s.phase,
1546
+ prompt: typeof s.prompt === 'string' ? s.prompt : null,
1547
+ model: typeof s.model === 'string' && s.model !== '' ? s.model : null,
1548
+ deliverable: s.deliverable ?? 'return-value',
1549
+ reads: [...(s.artifacts?.reads ?? [])],
1550
+ writes: [...(s.artifacts?.writes ?? [])],
1551
+ tools: [...(s.tools ?? [])],
1552
+ retryMaxAttempts: typeof s.retry?.maxAttempts === 'number' && s.retry.maxAttempts >= 1 ? Math.floor(s.retry.maxAttempts) : 1,
1553
+ retryOn: [...(s.retry?.retryableFailureClasses ?? [])],
1554
+ maxAgents: typeof s.budget?.maxAgents === 'number' ? s.budget.maxAgents : 1,
1555
+ qeRole: s['x-role'] === 'qe',
1556
+ gate:
1557
+ g === undefined
1558
+ ? null
1559
+ : {
1560
+ kind: typeof g.kind === 'string' ? g.kind : 'gate',
1561
+ failRoute: typeof g.failRoute === 'string' ? g.failRoute : null,
1562
+ maxRedos: typeof g.maxRedos === 'number' && Number.isFinite(g.maxRedos) && g.maxRedos > 0 ? Math.floor(g.maxRedos) : 0,
1563
+ },
1564
+ };
1565
+ };
1566
+
1567
+ /** A declared dep resolved to the BOUNDARY that must have settled before this one dispatches. */
1568
+ const depBoundary = (dep: string): string => ownedByRegion.get(dep) ?? dep;
1569
+
1570
+ const boundaries: RunBoundary[] = [];
1571
+ for (const s of norm.steps) {
1572
+ if (ownedByRegion.has(s.stepId)) continue; // members and joins are rendered inside their region
1573
+ const deps = [...new Set((s.deps ?? []).map(depBoundary).filter((d) => d !== s.stepId))];
1574
+ if (s.kind === 'fanout') {
1575
+ const f = fanouts.find((x) => x.stage === s.stepId);
1576
+ const j = f === undefined ? undefined : joinsByForStage.get(f.stage);
1577
+ boundaries.push({
1578
+ boundaryId: s.stepId,
1579
+ kind: 'region',
1580
+ deps,
1581
+ region: {
1582
+ fanout: s.stepId,
1583
+ join: j?.stage ?? '',
1584
+ joinPolicy: j?.joinPolicy ?? '',
1585
+ onInvalid: j?.onInvalid ?? 'named-failure',
1586
+ maxFanout: typeof f?.maxFanout === 'number' ? f.maxFanout : 0,
1587
+ registry: [...(f?.registry ?? [])],
1588
+ dedup: f?.dedup === true,
1589
+ shape: s.concurrency ?? 'barrier',
1590
+ chain: (f?.chain ?? []).map((c) => byId.get(c)).filter((c): c is LoopStep => c !== undefined).map(specOf),
1591
+ },
1592
+ });
1593
+ continue;
1594
+ }
1595
+ if (s.kind === 'pause') {
1596
+ const state = typeof s.pauseState === 'string' ? s.pauseState : '';
1597
+ const declared = pausesByState.get(state);
1598
+ boundaries.push({
1599
+ boundaryId: s.stepId,
1600
+ kind: 'pause',
1601
+ deps,
1602
+ pause: {
1603
+ state,
1604
+ resumeArg: declared?.resumeArg ?? '',
1605
+ payloadSchema: declared?.payloadSchema ?? null,
1606
+ },
1607
+ });
1608
+ continue;
1609
+ }
1610
+ if (s.kind === 'gate') {
1611
+ boundaries.push({ boundaryId: s.stepId, kind: 'gate', deps, stage: specOf(s) });
1612
+ continue;
1613
+ }
1614
+ boundaries.push({ boundaryId: s.stepId, kind: 'stage', deps, stage: specOf(s) });
1615
+ }
1616
+
1617
+ return {
1618
+ kind: 'run-projection/1',
1619
+ boundaries,
1620
+ budgetTotal: computeBudgetTotal(norm),
1621
+ resumeArgKeys: (norm.pauses ?? []).map((p) => p.resumeArg).filter((k): k is string => typeof k === 'string' && k !== ''),
1622
+ traceEmit: norm.trace?.emit === true,
1623
+ };
1624
+ }
@@ -34,6 +34,7 @@ import {
34
34
  type LoopStep,
35
35
  } from './loop-plan.js';
36
36
  import { BLOBS, type LoopBlob } from './loop-blobs.generated.js';
37
+ import { computeBudgetTotal, stepContractLines } from './loop-run-semantics.js';
37
38
 
38
39
  export const LOOP_RENDER_GENERATOR = 'loop-render/1';
39
40
 
@@ -76,6 +77,12 @@ function sha256(s: string): string {
76
77
  /** Which blobs a plan pulls in (opt-in subsystems + auto rules + requires closure). */
77
78
  export function selectBlobs(plan: LoopPlan): LoopBlob[] {
78
79
  const names = new Set<string>();
80
+ // The ENACTMENT-DECISION blob is UNCONDITIONAL (ADR-001 W4): the base runtime aliases
81
+ // errText/errSnap/classifyFailure in every script, so there is no plan that does not need it.
82
+ // gateVerdict/joinRegion are inert function declarations in a plan with no gate/fanout — the
83
+ // alternative (a conditional roster) would make the exec fingerprint's `tools` axis depend on
84
+ // plan shape twice, and would let a semantics-carrying script exist without the semantics.
85
+ names.add('loop-semantics');
79
86
  const sub = plan.subsystems ?? {};
80
87
  if (plan.checkpointing?.enabled === true || sub.checkpoints === true) names.add('checkpoints');
81
88
  // AM-9: training pairs are injected ONLY on an explicit opt-in — never by default (PHI lesson).
@@ -189,26 +196,22 @@ function stepUserLines(s: LoopStep): string[] {
189
196
  function stepPromptAssembly(s: LoopStep, plan: LoopPlan): string[] {
190
197
  const id = s.stepId;
191
198
  const lines: string[] = [];
192
- const reads = s.artifacts?.reads ?? [];
193
- const writes = s.artifacts?.writes ?? [];
194
199
  const gateCfg = s.kind === 'gate' ? (plan.gates ?? []).find((g) => g.stepId === id) : undefined;
195
200
  lines.push(`const P_${ident(id)} = [`);
196
201
  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
- // ENACTS LoopStep.tools (cfr-pipeline ADR-002): the declared perimeter is COMMUNICATED to the
203
- // agent in FIXED wording, so its presence is greppable by a layer-1 test. The second sentence is
204
- // not decoration — it is the honesty clause the whole feature rests on: a declaration is not
205
- // enforcement, and every environment this ships into today serves the tools as labeled stubs.
206
- const tools = s.tools ?? [];
207
- if (tools.length > 0) {
208
- lines.push(` ${jsString('declared MCP tool allowlist (plan tools): ' + tools.join(', ') + ' — use NOTHING outside it. In this environment every one of these is a labeled STUB, not a live integration; enforcement lives at the MCP server, not here.')},`);
209
- }
210
- if (s.kind === 'gate') {
211
- 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.')},`);
202
+ // The contract lines themselves come from ONE function shared with the dz runner
203
+ // (loop-run-semantics.stepContractLines, ADR-001 Confirmation-5) — the render's only remaining
204
+ // job is JS-quoting them. ENACTS artifacts.reads/writes/deliverable, LoopStep.tools and the gate
205
+ // protocol: the plan's declarations are COMMUNICATED to the agent in FIXED wording, so their
206
+ // presence is greppable by a layer-1 test and IDENTICAL under both enactors.
207
+ for (const line of stepContractLines({
208
+ reads: s.artifacts?.reads ?? [],
209
+ writes: s.artifacts?.writes ?? [],
210
+ deliverable: s.deliverable ?? 'return-value',
211
+ tools: s.tools ?? [],
212
+ gate: s.kind === 'gate' ? { kind: gateCfg?.kind ?? 'gate' } : null,
213
+ })) {
214
+ lines.push(` ${jsString(line)},`);
212
215
  }
213
216
  lines.push(`].join('\\n')`);
214
217
  return lines;
@@ -477,15 +480,10 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
477
480
  const traceOn = plan.trace?.emit === true;
478
481
  const ckptOn = plan.checkpointing?.enabled === true || plan.subsystems?.checkpoints === true;
479
482
  // budget: declared per-step budgets PLUS the declared gate-redo allowance (QE round-3 B1 — a
480
- // plan-declared redo must be affordable; an undeclared one still hits the guard loudly).
481
- const stepBudget = plan.steps.reduce((n, s) => n + (s.budget?.maxAgents ?? 1), 0);
482
- const byIdB = new Map(plan.steps.map((s) => [s.stepId, s]));
483
- const gateRedoBudget = (plan.gates ?? []).reduce((n, g) => {
484
- const redos = typeof g.maxRedos === 'number' && Number.isFinite(g.maxRedos) && g.maxRedos > 0 ? Math.floor(g.maxRedos) : 0;
485
- if (redos === 0 || typeof g.failRoute !== 'string' || g.failRoute.startsWith('terminal:')) return n;
486
- return n + redos * ((byIdB.get(g.failRoute)?.budget?.maxAgents ?? 1) + (byIdB.get(g.stepId)?.budget?.maxAgents ?? 1));
487
- }, 0);
488
- const budgetTotal = stepBudget + gateRedoBudget;
483
+ // plan-declared redo must be affordable; an undeclared one still hits the guard loudly). The
484
+ // FORMULA lives in loop-run-semantics (ADR-004 Confirmation-2) so the dz runner reserves against
485
+ // the same ceiling this script spends against — one formula, two enactors.
486
+ const budgetTotal = computeBudgetTotal(plan);
489
487
  const lines: string[] = [];
490
488
  lines.push(G('runtime'));
491
489
  lines.push(`const A = typeof args === 'string' ? JSON.parse(args) : (args || {})`);
@@ -518,82 +516,15 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
518
516
  lines.push(`function __settleSeqOf(stepId, itemKey, occ) { const v = __settled[__settleKey(stepId, itemKey, occ)]; return typeof v === 'number' ? v : -1 }`);
519
517
  lines.push(`let __invocationN = 0`);
520
518
  lines.push(`let __seqFallback = 0 // used only when the trace blob is opted out (hooks are no-ops)`);
521
- lines.push(`// __errText TOTAL error-to-text (QE round-3 B3, the ha-consilium 5b totality lesson): the`);
522
- lines.push(`// writer's own settle event must survive a hostile error object. String(err) throws on a`);
523
- lines.push(`// null-prototype object and a throwing .message getter throws on access — both are caught here,`);
524
- lines.push(`// so rendering a message can never replace the original failure or lose the settle. The`);
525
- lines.push(`// .message property is read ONCE into a local (round-5 B3: a one-shot getter answered the`);
526
- lines.push(`// typeof probe and vanished on the value read — snapshot-once defeats it).`);
527
- lines.push(`function __errText(err) {`);
528
- lines.push(` try {`);
529
- lines.push(` if (err !== null && typeof err === 'object') { const m = err.message; if (typeof m === 'string') return m }`);
530
- lines.push(` return String(err)`);
531
- lines.push(` } catch (_e) {`);
532
- lines.push(` try { return Object.prototype.toString.call(err) } catch (_e2) { return '[unrenderable error]' }`);
533
- lines.push(` }`);
534
- lines.push(`}`);
535
- lines.push(`// failure classification (G4, hardened in QE round-3 B3, round-4: cause-chain + word bounds) —`);
536
- lines.push(`// the CLOSED enum from loop-plan/1 (timeout | transport | malformed-output | policy-refusal).`);
537
- lines.push(`// The err.cause CHAIN is traversed (bounded depth 5, cycle-safe, getter-safe): the standard`);
538
- lines.push(`// Node fetch shape TypeError('fetch failed', {cause:{code:'ECONNRESET'}}) is TRANSPORT.`);
539
- lines.push(`// THREE TIERS over the whole chain, strongest first:`);
540
- lines.push(`// 1. error CODE (works on non-Error shapes like {code:'ECONNRESET'}; never message-dependent;`);
541
- lines.push(`// ETIMEDOUT is a TRANSPORT code — round-2's message regex captured it as 'timeout' first);`);
542
- lines.push(`// 2. error NAME (SyntaxError = parsing model output failed → malformed-output);`);
543
- lines.push(`// 3. message patterns, DISJOINT by precedence transport > policy-refusal > malformed-output >`);
544
- lines.push(`// timeout — every alternative WORD-BOUNDED (round-4 B3: 'rate.?limit' unbounded matched`);
545
- lines.push(`// 'delibeRATE LIMITation'/'corpoRATE LIMITation' — a substring can never smuggle a class).`);
546
- lines.push(`// An UNCLASSIFIABLE failure returns null and is NEVER retried (closed enum, fail-closed).`);
547
- lines.push(`function __causeChain(err) {`);
548
- lines.push(` const chain = []`);
549
- lines.push(` let cur = err`);
550
- lines.push(` for (let d = 0; d < 5; d++) {`);
551
- lines.push(` if (cur === null || cur === undefined) break`);
552
- lines.push(` if (chain.indexOf(cur) !== -1) break // cycle-safe`);
553
- lines.push(` chain.push(cur)`);
554
- lines.push(` try { cur = typeof cur === 'object' ? cur.cause : undefined } catch (_e) { cur = undefined } // getter-safe`);
555
- lines.push(` }`);
556
- lines.push(` return chain.length > 0 ? chain : [err]`);
557
- lines.push(`}`);
558
- lines.push(`// __errSnap — ONE snapshot PER FAILURE (round-6 B3; Codex round-5: .message was snapshot once`);
559
- lines.push(`// per __errText INVOCATION, not once per failure — logging read it, classification read it`);
560
- lines.push(`// AGAIN, so a one-shot .message getter answered the log and defeated classification: 2 getter`);
561
- lines.push(`// reads, 1 attempt MEASURED). The catch site builds this snapshot ONCE; the log line and the`);
562
- lines.push(`// classifier both consume the SNAPSHOT — .code/.name/.message are each read exactly once per`);
563
- lines.push(`// failure, getter-safe, over the whole cause chain.`);
564
- lines.push(`function __errSnap(err) {`);
565
- lines.push(` const chain = __causeChain(err)`);
566
- lines.push(` const snap = []`);
567
- lines.push(` for (let ci = 0; ci < chain.length; ci++) {`);
568
- lines.push(` let code = null`);
569
- 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 }`);
570
- lines.push(` let name = null`);
571
- 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 }`);
572
- lines.push(` snap.push({ code: code, name: name, text: __errText(chain[ci]) })`);
573
- lines.push(` }`);
574
- lines.push(` return snap`);
575
- lines.push(`}`);
576
- lines.push(`function __classifyFailure(outcome, snap) {`);
577
- 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']`);
578
- lines.push(` const links = Array.isArray(snap) ? snap : []`);
579
- lines.push(` for (let ci = 0; ci < links.length; ci++) {`);
580
- lines.push(` const code = links[ci].code`);
581
- lines.push(` if (code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EPIPE' || code === 'ECONNABORTED' || code === 'EAI_AGAIN') return 'transport'`);
582
- lines.push(` }`);
583
- lines.push(` for (let ci = 0; ci < links.length; ci++) {`);
584
- lines.push(` if (links[ci].name === 'SyntaxError') return 'malformed-output'`);
585
- lines.push(` }`);
586
- lines.push(` let msg = ''`);
587
- lines.push(` for (let ci = 0; ci < links.length; ci++) { msg += (ci > 0 ? '\\n' : '') + links[ci].text }`);
588
- lines.push(` msg = msg.toLowerCase()`);
589
- lines.push(` // rate[ -]?limit(ed|ing|s)? is RIGHT-BOUNDED (round-5 B3: the open 'rate.?limit' matched`);
590
- lines.push(` // 'rate limitation: invalid JSON' as transport — a malformed-output failure smuggled a class)`);
591
- 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'`);
592
- 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'`);
593
- 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'`);
594
- lines.push(` if (/\\btimeout\\b|\\btimed out\\b/.test(msg)) return 'timeout'`);
595
- lines.push(` return null`);
596
- lines.push(`}`);
519
+ lines.push(`// The enactment DECISIONS below are ALIASES onto the always-on \`loop-semantics\` blob`);
520
+ lines.push(`// (ADR-001 W4): errText/causeChain/errSnap/classifyFailure/gateVerdict/joinRegion have ONE`);
521
+ lines.push(`// implementation, sliced into this script from harness-core/src/loop-run-semantics.ts and`);
522
+ lines.push(`// IMPORTED (not copied) by the dz runner. The __-prefixed names are kept so every call site`);
523
+ lines.push(`// in this runtime reads exactly as it did when the bodies lived here.`);
524
+ lines.push(`const __errText = errText`);
525
+ lines.push(`const __causeChain = causeChain`);
526
+ lines.push(`const __errSnap = errSnap`);
527
+ lines.push(`const __classifyFailure = classifyFailure`);
597
528
  lines.push(`// __settleStep — THE single terminal exit of every step path (QE round-5 B3 class-kill;`);
598
529
  lines.push(`// round-6: SUCCESS-PATH PARITY — Codex round-5 showed __settleStep({outcome:'ok'}) returned`);
599
530
  lines.push(`// BEFORE flushing, leaving success durability on a naked phase-boundary await whose rejection`);
@@ -684,17 +615,8 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
684
615
  lines.push(` if (lastErr !== null) throw lastErr`);
685
616
  lines.push(` return null`);
686
617
  lines.push(`}`);
687
- lines.push(`// join helper — explicit policy from the closed set; a dispatched branch is never skippable`);
688
- lines.push(`function joinRegion(results, o) {`);
689
- lines.push(` const policy = o && o.policy ? o.policy : 'all-activated'`);
690
- lines.push(` const failures = []`);
691
- lines.push(` for (let i = 0; i < results.length; i++) { if (results[i] === null || results[i] === undefined) failures.push(i) }`);
692
- 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 } }`);
693
- lines.push(` const quorum = /^quorum:([1-9][0-9]*)$/.exec(policy)`);
694
- 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 } }`);
695
- 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') }`);
696
- lines.push(` return { ok: true, values: results, failures: [] }`);
697
- lines.push(`}`);
618
+ lines.push(`// join helper — explicit policy from the closed set; a dispatched branch is never skippable.`);
619
+ lines.push(`// The DECISION lives in the loop-semantics blob above (\`joinRegion\`), called directly here.`);
698
620
  if (plan.steps.some((s) => s.kind === 'fanout')) {
699
621
  // ── QUIESCENCE (QE round-7 B3, the round-6 reviewer's FOURTH CLASS: structured-concurrency /
700
622
  // quiescence ownership). MEASURED by Codex on the previous shape: the region awaited a
@@ -731,7 +653,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
731
653
  }
732
654
  if (traceOn) {
733
655
  lines.push(`// trace wiring (blob-provided emitter; hooks INSIDE runStep — ADR-003)`);
734
- lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP)`);
656
+ lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP, 'rendered-script')`);
735
657
  lines.push(`__hooks.onDispatch = function (e) { return traceOnDispatch(__traceState, e) }`);
736
658
  lines.push(`__hooks.onSettle = function (e) { return traceOnSettle(__traceState, e) }`);
737
659
  lines.push(`async function __traceFlushNow(phaseName, stepLabel) {`);
@@ -871,20 +793,8 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
871
793
  lines.push(`// must be the ONLY anchored verdict line. Embedded mid-reply "GATE: PASS" text never counts,`);
872
794
  lines.push(`// "GATE: PASS" followed by trailing prose is invalid, and "GATE: FAIL … GATE: PASS" is an`);
873
795
  lines.push(`// INVALID verdict (never a success) — routed like a failure (redo/fail route), never a pass.`);
874
- lines.push(`function __gateVerdict(r) {`);
875
- lines.push(` if (typeof r !== 'string') return 'invalid'`);
876
- lines.push(` const vLines = r.split('\\n')`);
877
- lines.push(` const vRe = /^\\s*GATE:\\s*(PASS|FAIL)\\s*$/`);
878
- lines.push(` let vCount = 0`);
879
- lines.push(` let vLast = ''`);
880
- lines.push(` for (let i = 0; i < vLines.length; i++) {`);
881
- lines.push(` if (vRe.test(vLines[i])) { vCount++ }`);
882
- lines.push(` if (vLines[i].trim() !== '') { vLast = vLines[i] }`);
883
- lines.push(` }`);
884
- lines.push(` const vEnd = vRe.exec(vLast)`);
885
- lines.push(` if (vCount !== 1 || vEnd === null) return 'invalid'`);
886
- lines.push(` return vEnd[1] === 'PASS' ? 'pass' : 'fail'`);
887
- lines.push(`}`);
796
+ lines.push(`// The grammar itself lives in the loop-semantics blob, shared with the dz runner.`);
797
+ lines.push(`const __gateVerdict = gateVerdict`);
888
798
  }
889
799
  lines.push(GE('runtime'));
890
800
  return lines.join('\n');