diff --git a/commands/trekexecute.md b/commands/trekexecute.md index 36cc634..024c4d4 100644 --- a/commands/trekexecute.md +++ b/commands/trekexecute.md @@ -850,12 +850,24 @@ progress file. "steps": { "1": { "status": "pending", "attempts": 0, "error": null, "completed_at": null, "commit": null } }, + "iterations_remaining": 25, "entry_condition_checked": false, "exit_condition_checked": false, "summary": null } ``` +**`iterations_remaining` (v1.7+, additive-optional):** the remaining +recovery/retry budget against the global hard cap (unit = **iterations**, per the +brief — token/cost budgeting is out of scope). Seeded to +`TREKEXECUTE_MAX_RECOVERY_ITERATIONS` (default 25) at start. **Single writer:** the +executor decrements it by 1 on **every recovery/retry iteration** — each step +attempt beyond the first, and each Phase 7.6 recovery dispatch. **Backfill-on-absent:** +when resuming a legacy `progress.json` that lacks the field, seed it to the cap +rather than hard-failing (forward/backward-compatible; shape-validated by +`progress-validator.mjs` — non-negative integer, never required-top). The field is +present in the spec at minimum — there is **no `(if wired)` conditional**. + ### Mode-specific behavior **mode = execute (fresh):** @@ -1246,6 +1258,14 @@ Record in progress file: > audit ran** (never before, never in place of it). A transcript that merely > "feels done" is NOT a stop-signal — victory-declaration bias is the exact > failure mode this gate exists to defeat. +> 3. **Counter liveness (deterministic, defeats "never decremented")** — the gate +> reconciles the budget counter against the ground-truth counters the Phase 7.5 +> audit already derives: +> `iterations_remaining == TREKEXECUTE_MAX_RECOVERY_ITERATIONS − (recovery_depth + Σ steps.*.attempts-beyond-first)`. +> A field left un-decremented (or tampered) fails this reconciliation and the +> result is OVERRIDDEN `completed → partial`, exactly as the audit-override +> already does (`:1221`). So the counter's liveness is auditable at the gate, +> not merely asserted in prose. > > Termination authority therefore rests on the deterministic audit + an > objective stop-signal, never on the model's narrative. Native `/goal` (when @@ -1523,6 +1543,7 @@ To resume: /trekexecute --resume {path} "drift_details": [], "recovery_dispatched": false, "recovery_depth": 0, + "iterations_remaining": 25, "legacy_plan": false, "progress_file": "{path}" } diff --git a/tests/commands/trekexecute.test.mjs b/tests/commands/trekexecute.test.mjs index 0569d0c..cb0c39d 100644 --- a/tests/commands/trekexecute.test.mjs +++ b/tests/commands/trekexecute.test.mjs @@ -118,3 +118,33 @@ test('trekexecute — every recovery/retry loop bounded + 3-axis cap hierarchy ( assert.match(section, /recovery_depth/, 'hierarchy must name per-session recovery_depth axis'); assert.match(section, /TREKEXECUTE_MAX_RECOVERY_ITERATIONS/, 'hierarchy must name the global budget axis'); }); + +// --- S38 loop-discipline hardening: iterations_remaining budget signal (Step 4) --- + +test('trekexecute — iterations_remaining surfaced in progress schema + summary JSON, no (if wired) (SC c)', () => { + const text = read(); + assert.ok(text.includes('iterations_remaining'), 'iterations_remaining must be present (SC c)'); + assert.ok(!/iterations_remaining[^\n]*\(if wired\)/.test(text), + 'no "(if wired)" conditional — field present in the spec at minimum'); + const schemaIdx = text.indexOf('### Progress file schema'); + assert.ok(schemaIdx >= 0, 'progress schema section missing'); + assert.match(text.slice(schemaIdx, schemaIdx + 1400), /iterations_remaining/, + 'progress schema must include iterations_remaining'); + const sumIdx = text.indexOf('"trekexecute_summary": {'); + assert.ok(sumIdx >= 0, 'summary JSON block missing'); + assert.match(text.slice(sumIdx, sumIdx + 900), /iterations_remaining/, + 'summary JSON must include iterations_remaining (observable)'); +}); + +test('trekexecute — iterations_remaining decrement + backfill + gate cross-check documented (SC c)', () => { + const text = read(); + assert.match(text, /decrement[^.\n]*iteration|every recovery\/retry iteration/i, + 'decrement-per-iteration rule must be documented'); + assert.match(text, /backfill|seed (it )?to (the )?cap/i, + 'backfill-on-absent (legacy resume) rule must be documented'); + const gateIdx = text.indexOf('machine-verifiable completion gate'); + const gate = text.slice(gateIdx, gateIdx + 2200); + assert.match(gate, /iterations_remaining/, 'gate must cross-check iterations_remaining'); + assert.match(gate, /recovery_depth/, 'gate cross-check must reconcile against recovery_depth'); + assert.match(gate, /attempts/, 'gate cross-check must reconcile against attempts'); +});