From e34082d79a0ff7c886545ce3a73c59e3034cf430 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 20 Jun 2026 21:31:07 +0200 Subject: [PATCH] feat(voyage): document recovery/retry iteration caps in trekexecute Co-Authored-By: Claude Opus 4.8 (1M context) --- commands/trekexecute.md | 39 +++++++++++++++++++++++++++++ tests/commands/trekexecute.test.mjs | 25 ++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/commands/trekexecute.md b/commands/trekexecute.md index fd6f537..36cc634 100644 --- a/commands/trekexecute.md +++ b/commands/trekexecute.md @@ -1298,6 +1298,38 @@ Status: partial (recovery_depth=2, escalated to user) Do NOT dispatch a third recovery. Report to the user. +### Iteration caps + global recovery/retry budget + +Every recovery/retry loop in this executor has a documented numeric bound. +These are **three distinct axes** — not one nested counter — plus a separate +child turn cap: + +- **Per-step retry cap** — `attempts ≤ 3` (initial + **maximum 2 retries**; + Hard Rule 5 / Sub-step E). Bounds the retries of a *single* step. +- **Per-session recovery cap** — `recovery_depth < 2` (Phase 7.6 hard cap). + Bounds how deep recovery *dispatch* nests. +- **Global recovery/retry budget** — a new env-overridable constant + `TREKEXECUTE_MAX_RECOVERY_ITERATIONS` (default **25**) — the aggregate ceiling + on the **total count of recovery + retry iterations across the whole + execution**. This is the case the two caps above do NOT bound: many steps each + retrying up to 3× can sum unbounded. It primarily backstops the **retry + aggregate** (recovery dispatch is already hard-capped at 2). Surfaced and + enforced via the `iterations_remaining` counter (Phase 3 schema; decremented + on every recovery/retry iteration; reconciled at the Phase 7.5 completion + gate). +- **Child turn cap** — `--max-turns` (`TREKEXECUTE_MAX_TURNS`, default 50) + bounds model turns inside a single `-p` child; a *different* axis from the + three above. + +Default **25** is an independent value from the cap research (practitioner range +10–50; OWASP Agentic ASI08 financial-DoS), tuned down from the research ~50 +starting point for a tighter backstop. It is **not** a reconciliation with +`--max-turns` (50) — the axes are incomparable. Operators may override: +`TREKEXECUTE_MAX_RECOVERY_ITERATIONS=10 /trekexecute --project ...`. The +`effort=='low'` single-foreground-loop path and the wave path are both subject to +this aggregate ceiling; it is an execution-spec ceiling (enforced via the +`iterations_remaining` counter), not a code-loop edit. + ## Phase 8 — Final report Always produce a final report. @@ -1698,3 +1730,10 @@ code-path. cleanup proceeds regardless. Rationale: this converts an unrecoverable failure (worktree removed, branch deleted, work lost) into a recoverable one (push succeeded, branch preserved on remote). Source: research/02 R3. + +20. **Three distinct iteration axes.** Per-step `attempts` (≤3), per-session + `recovery_depth` (<2), and the global `TREKEXECUTE_MAX_RECOVERY_ITERATIONS` + budget (default 25 — aggregate recovery+retry backstop) are independent + bounds, not one nested counter; child `--max-turns` (50) is a fourth, + separate axis. Every recovery/retry loop is bounded by at least one of them. + See "Iteration caps + global recovery/retry budget". diff --git a/tests/commands/trekexecute.test.mjs b/tests/commands/trekexecute.test.mjs index 69e534a..0569d0c 100644 --- a/tests/commands/trekexecute.test.mjs +++ b/tests/commands/trekexecute.test.mjs @@ -93,3 +93,28 @@ test('trekexecute — completion gate anchors result:completed to Phase 7.5 audi assert.match(section, /DONE/, 'gate must reference the DONE stop-signal token'); assert.match(section, /emitted AFTER/, 'gate must require DONE emitted AFTER the audit ran'); }); + +// --- S38 loop-discipline hardening: caps + global recovery budget (Step 3) --- + +test('trekexecute — global recovery/retry budget TREKEXECUTE_MAX_RECOVERY_ITERATIONS default 25 (SC b)', () => { + const text = read(); + assert.ok(text.includes('TREKEXECUTE_MAX_RECOVERY_ITERATIONS'), + 'global recovery/retry budget constant must be documented'); + assert.match( + text, + /TREKEXECUTE_MAX_RECOVERY_ITERATIONS[^\n]{0,40}\b25\b|default[^\n]{0,20}\b25\b[^\n]{0,40}aggregate/i, + 'global budget must document numeric default 25 adjacent to the constant', + ); +}); + +test('trekexecute — every recovery/retry loop bounded + 3-axis cap hierarchy (SC b)', () => { + const text = read(); + assert.match(text, /maximum 2 retries|Retry cap = 3 attempts/, 'per-step retry cap must be explicit'); + assert.match(text, /recovery_depth < 2/, 'recovery-dispatch cap must be explicit'); + const capIdx = text.indexOf('Iteration caps'); + assert.ok(capIdx >= 0, 'cap-hierarchy section ("Iteration caps") must exist'); + const section = text.slice(capIdx, capIdx + 1500); + assert.match(section, /attempts/, 'hierarchy must name per-step attempts axis'); + 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'); +});