@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.
- package/.dz-manifest.json +339 -75
- package/README.md +47 -4
- package/dist/codex-hooks-assets.d.ts.map +1 -1
- package/dist/codex-hooks-assets.js +39 -2
- package/dist/codex-hooks-assets.js.map +1 -1
- package/dist/codex-hooks-verify.d.ts +23 -2
- package/dist/codex-hooks-verify.d.ts.map +1 -1
- package/dist/codex-hooks-verify.js +29 -0
- package/dist/codex-hooks-verify.js.map +1 -1
- package/dist/codex-hooks.d.ts +90 -7
- package/dist/codex-hooks.d.ts.map +1 -1
- package/dist/codex-hooks.js +171 -21
- package/dist/codex-hooks.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +22 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +45 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.d.ts +1 -1
- package/dist/loop-blobs.generated.d.ts.map +1 -1
- package/dist/loop-blobs.generated.js +12 -3
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan.d.ts +70 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +103 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +38 -130
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-run-semantics.d.ts +130 -0
- package/dist/loop-run-semantics.d.ts.map +1 -0
- package/dist/loop-run-semantics.js +257 -0
- package/dist/loop-run-semantics.js.map +1 -0
- package/dist/loop-trace.d.ts +106 -5
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +151 -18
- package/dist/loop-trace.js.map +1 -1
- package/dist/managed-hooks.d.ts +10 -0
- package/dist/managed-hooks.d.ts.map +1 -1
- package/dist/managed-hooks.js +17 -5
- package/dist/managed-hooks.js.map +1 -1
- package/dist/named-lock.d.ts +57 -0
- package/dist/named-lock.d.ts.map +1 -0
- package/dist/named-lock.js +247 -0
- package/dist/named-lock.js.map +1 -0
- package/dist/operations.d.ts +81 -5
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +356 -38
- package/dist/operations.js.map +1 -1
- package/dist/parity.d.ts +70 -2
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +133 -2
- package/dist/parity.js.map +1 -1
- package/dist/qe-bridge.d.ts +291 -0
- package/dist/qe-bridge.d.ts.map +1 -0
- package/dist/qe-bridge.js +538 -0
- package/dist/qe-bridge.js.map +1 -0
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +43 -9
- package/dist/score.js.map +1 -1
- package/dist/trace-corroborate.d.ts +48 -0
- package/dist/trace-corroborate.d.ts.map +1 -0
- package/dist/trace-corroborate.js +172 -0
- package/dist/trace-corroborate.js.map +1 -0
- package/dist/workflow-run-dispatch.d.ts +230 -0
- package/dist/workflow-run-dispatch.d.ts.map +1 -0
- package/dist/workflow-run-dispatch.js +363 -0
- package/dist/workflow-run-dispatch.js.map +1 -0
- package/dist/workflow-run.d.ts +513 -0
- package/dist/workflow-run.d.ts.map +1 -0
- package/dist/workflow-run.js +1377 -0
- package/dist/workflow-run.js.map +1 -0
- package/package.json +2 -2
- package/sbom.json +740 -80
- package/src/codex-hooks-assets.ts +39 -2
- package/src/codex-hooks-verify.ts +55 -2
- package/src/codex-hooks.ts +172 -20
- package/src/feature-adr-routing.ts +55 -0
- package/src/index.ts +46 -1
- package/src/loop-blobs.generated.ts +12 -3
- package/src/loop-plan.ts +185 -0
- package/src/loop-render.ts +38 -128
- package/src/loop-run-semantics.ts +278 -0
- package/src/loop-trace.ts +207 -16
- package/src/managed-hooks.ts +26 -5
- package/src/named-lock.ts +277 -0
- package/src/operations.ts +441 -40
- package/src/parity.ts +177 -2
- package/src/qe-bridge.ts +737 -0
- package/src/score.ts +50 -9
- package/src/trace-corroborate.ts +205 -0
- package/src/workflow-run-dispatch.ts +459 -0
- 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
|
+
}
|
package/src/loop-render.ts
CHANGED
|
@@ -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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
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(`//
|
|
522
|
-
lines.push(`//
|
|
523
|
-
lines.push(`//
|
|
524
|
-
lines.push(`//
|
|
525
|
-
lines.push(`//
|
|
526
|
-
lines.push(
|
|
527
|
-
lines.push(`
|
|
528
|
-
lines.push(`
|
|
529
|
-
lines.push(`
|
|
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(
|
|
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(
|
|
875
|
-
lines.push(`
|
|
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');
|