Summary / 01. Coding harness (REPAIR and primitives) / Document
Spec 2026-09-04
Shared copy: the operator's name and machine names are replaced by roles. Nothing else is changed.
Rendering…
# Build Spec 01 — Agentless-First Coding Harness (the ≈80% fraction) > Derived from `papers/01-coding-harness.md`. This is the document that drives the build: a > reversible replacement coding harness plus its A/B test. It translates the paper's design > into exact signatures, exact return shapes, exact caps, and an exact build sequence — no > further research required. Where the paper says "≈80%", this spec pins *which* 80%. --- ## 1. Purpose & Scope Build a **reversible, agentless-first coding harness** — a deterministic `localize → repair → validate` pipeline with exactly six primitives, one LLM step, and three gates — as a drop-in *replacement* for the current coding loop (the live pi tool surface), not an accretion onto it. The overall target is the paper's full 100% spec. **Round 1 of this build** lands the expected ≈80% of it first: everything deterministic (AST/symbol/fault-localization analysis, test execution, validation, gates, caps, observation policy) plus the minimal string-replace edit primitive and the single-shot repair call. The agentic layer (interactive debugger, role decomposition, multi-step repair loop) is not built in Round 1 but is *not* excluded — it is the content of later rounds, which climb from that ~80% landing toward 100% (see §2). Round 1 is kept only if it wins a measured A/B test against the current harness on resolve rate, cost, and tokens (§9); later rounds climb from there. --- ## 2. Round 1 vs later rounds (toward the 100% target) The goal is the paper's full **100%** spec, not 80%. **~80%** is only the expected **first-round landing**; later rounds climb from there toward 100%. Every component from the paper is classified below by *which round builds it*. - **BUILD-NOW = the Round 1 build** — the expected ≈80% deterministic landing this spec implements and A/B-tests (rows 1–14). - **DEFERRED = later rounds** — the agentic layer (rows 15–17) that Round 1 intentionally does not build *yet*, because it needs the deterministic core underneath it first. These are **not excluded from the project** — they are the content of later rounds, the climb from ~80% toward the 100% target, gated by the same §9 A/B protocol once the Round 1 core is in place. The table below carries the classification; each row's classification label names its round. **BUILD-NOW** / **DEFERRED** are shorthand for "Round 1 build" and "later rounds" respectively. | # | Component (paper §) | Classification | |---|---|---| | 1 | `read_entity(selector)` — signature + windowed body | **BUILD-NOW** | | 2 | `search_symbol(query, keywords)` — signature-only symbol search | **BUILD-NOW** | | 3 | `search_code(query, cap)` — summarized + capped embedding retrieval | **BUILD-NOW** | | 4 | `edit_entity(selector, search→replace)` — exact-string-replace, linter-gated | **BUILD-NOW** | | 5 | `run_test(target)` — pass/fail + truncated traceback | **BUILD-NOW** | | 6 | `execute(code)` — vetted deterministic commands (lint / test / compile), bounded time + temp working copy; arbitrary code DEFERRED to an OS sandbox (bubblewrap/firejail) | **BUILD-NOW** | | 7 | Stage 1 Localization (tree-sitter skeleton → embedding + SBFL → top-k) | **BUILD-NOW** | | 8 | Stage 2 Repair (single LLM call, temperature-sampled N patches) | **BUILD-NOW** | | 9 | Stage 3 Validation (repro + regression + AST-normalized majority vote) | **BUILD-NOW** | | 10 | Linter gate | **BUILD-NOW** | | 11 | Validation gate | **BUILD-NOW** | | 12 | Evidence gate (ECLoop, weakened deterministic form) | **BUILD-NOW (cheap)** — see decision below | | 13 | Observation policy (windowed / summarized / skeletonized / collapsed) | **BUILD-NOW** | | 14 | Hard caps (iterations / context / tokens) | **BUILD-NOW** | | 15 | EnIGMA interactive debugger (`debug_start`/`debug_exec`/`debug_continue`, `connect`/`sendline`) | **DEFERRED** | | 16 | HyperAgent role decomposition (Planner / Navigator / Editor / Executor) | **DEFERRED** | | 17 | Iterative multi-step repair loop | **DEFERRED** | **Evidence-gate decision: BUILD-NOW-CHEAP** (a Round 1 build item). It is a deterministic monotone barrier (a commit is rejected unless the observation ledger already contains a read/search record citing the target entity). It can only *block* a premature edit, never *cause* a bad one, and costs a single set-membership check. It is implemented in Phase 2 as a flag on `edit_entity`, and is the **first thing deleted** if the §9 ablation shows no measured delta. --- ## 3. Primitives Six primitives in four categories (read / search / edit / execute). Every primitive returns a single JSON envelope: ```json { "ok": bool, "error": str|null, "data": <per-primitive shape> } ``` **Model-facing observations never carry line numbers or whole files** (P2/P3: positional localization is rejected for weak models). The harness keeps line/span *internally* for `edit_entity` scoping, but strips them from any JSON returned to the model. ### 3.1 `read_entity` — (D) ```python def read_entity(selector: str, body: bool = False, window: int = 100) -> dict: """ selector: a code-graph node name "{repo}.{module}.{qualname}" (matches memory.db code_nodes.name, e.g. "repo.memstore.MemStore.remember"). body: request a windowed body slice (the model asks and pays for it). window: body slice size in lines (clamped to 1..100). """ ``` `data` shape: ```json { "entity": { "name": "repo.memstore.MemStore.remember", "kind": "method", "signature": "def remember(self, text, kind=None, importance=0.5):", "path": "memstore.py", "lang": "python", "repo": "repo" }, "body": "<windowed slice, ≤100 lines, only when body=True; else null>" } ``` - Deterministic: reads `code_nodes` + slices the file. - Cap: `body` ≤ 100 lines (hard); `window` clamped to `[1, 100]`. - Never returns a whole file; never returns a line number. ### 3.2 `search_symbol` — (D) ```python def search_symbol(query: str, keywords: list[str] | None = None, k: int = 10) -> dict: """Signature-only AST symbol search over code_nodes (name + signature).""" ``` `data` shape: ```json { "matches": [ { "name": "...", "kind": "function", "signature": "def f(...):", "path": "...", "lang": "python", "repo": "..." } ], "truncated": false } ``` - Deterministic: case-insensitive substring/fuzzy match over `name` and `signature`. - `keywords` (if given): AND-filter — every keyword must appear in `name + signature` (P5). - Cap: `k` clamped to `[1, 10]`; signatures only, never bodies, never line numbers. ### 3.3 `search_code` — (D) ```python def search_code(query: str, cap: int = 8) -> dict: """Embedding retrieval over indexed entities, returned summarized + capped (never raw streams).""" ``` `data` shape: ```json { "results": [ { "name": "...", "signature": "def f(...):", "summary": "one-line docstring/first-comment digest or ''" } ], "truncated": false } ``` - Deterministic: `fastembed` `BAAI/bge-small-en-v1.5` (384-dim) embedding + numpy cosine over the entity index; each result summarized to `signature + first docstring line`, never a body. - Cap: `cap` clamped to `[1, 8]`. ### 3.4 `edit_entity` — (D + L) ```python def edit_entity(selector: str, search: str, replace: str) -> dict: """ The single edit primitive. The model supplies two SHORT strings; the harness performs exact-string substitution scoped to the entity's own span, then linter-gates the result. No diffs, no line numbers. (L = model supplies search/replace; D = substitution + gate.) """ ``` `data` shape: ```json { "committed": false, "lint_clean": false, "lint": [ { "kind": "syntax|lint", "code": "F821|...", "msg": "..." } ], "evidence_ok": false, "error": null } ``` Behavior, in order: 1. **Evidence gate** (before any write): `committed` requires `evidence_ok == True` — the observation ledger must already contain a read/search record citing `selector`. No evidence → return `{"committed": false, "evidence_ok": false, "error": "no evidence for selector"}`. (This is the §4 evidence gate, build-now-cheap.) 2. **Scoping**: `search` must match **exactly once** inside the entity's own span (its def, not the whole file). Zero matches or >1 matches → `{"committed": false, "error": "search matched 0 times"` (or `"... matched 2 times"`). 3. **Substitute**: replace that one occurrence with `replace`; write the candidate buffer (never the file yet). 4. **Linter gate**: run `py_compile` + `ruff --select E9,F` on the affected file. On **failure**: reject (discard the buffer, do not commit) and return `{"committed": false, "lint_clean": false, "lint": [...], "error": null}` — the model sees the lint error as data. 5. **Commit** only on lint-clean: write the buffer to disk and return `{"committed": true, "lint_clean": true, "lint": [], "evidence_ok": true}`. Caps: `search` length ≤ 400 chars; `replace` length ≤ 2,000 chars; `search` must be a literal exact substring (no regex, no line numbers). ### 3.5 `run_test` — (D) ```python def run_test(target: str, timeout: int = 120) -> dict: """Execute a test; return pass/fail + truncated traceback. Deterministic.""" ``` `data` shape: ```json { "passed": false, "exit_code": 1, "output": "<traceback, ≤50 lines / ≤2000 chars, truncated>" } ``` - `target` is a pytest node id (e.g. `tests/test_x.py::TestY::test_z`) or a shell invocation string; run in the repo's venv. - Cap: `timeout` clamped to `[1, 300]`s (default 120); traceback truncated to 50 lines / 2,000 chars. ### 3.6 `execute` — (D) ```python def execute(cmd: list[str], timeout: int = 10) -> dict: """Run a vetted, deterministic command (lint / test / compile) with bounded time and a temp working copy. No arbitrary code.""" ``` `data` shape: ```json { "stdout": "<≤100 lines>", "stderr": "<≤100 lines>", "exit_code": 0, "timed_out": false } ``` **Round-1 scope (hard constraint):** `execute` runs **only vetted, deterministic commands** (lint / test / compile) with bounded time and a temporary working copy. It does **not** run arbitrary model-generated Python; arbitrary code execution is **DEFERRED** until an OS-level sandbox (bubblewrap / firejail or equivalent) is in place. **Isolation is NOT a Python-level security boundary.** The mechanical limits below (temp cwd, `open`/`socket` monkey-patching, rlimits) are *hygiene and resource bounding*, not security: they are bypassable via `subprocess`, alternate APIs (`os.open`, `ctypes`, `io.open`), native libraries, `/proc`, and inherited file descriptors. Real isolation requires an **OS boundary** — namespaces, seccomp, bubblewrap, firejail, or a container. Sandbox mechanics (concrete): - Run in a fresh `tempfile.mkdtemp()` cwd; `os.chdir(tempdir)` before executing. - **Network hygiene (not a security boundary)**: a preamble is prepended that replaces `socket.socket` and `socket.create_connection` with raising stubs; the subprocess also runs with no proxy env. - **Filesystem hygiene (not a security boundary)**: cwd is the temp dir; `RLIMIT_FSIZE = 16 MB`; the preamble wraps `open`/`os.open` so absolute paths outside the temp dir raise `PermissionError`. - **Bounded time/memory**: `resource.setrlimit(RLIMIT_CPU, (timeout, timeout))`, `RLIMIT_AS = (512 MB, 512 MB)` via `preexec_fn`; wall-clock `timeout` = 10 s default, clamped `[1, 30]`. - Cap: stdout/stderr each truncated to 100 lines. --- ## 4. Pipeline Three stages; the LLM column contains exactly one step (Stage 2). Handoff is via plain dicts (JSON-serializable). Internal spans/line numbers are allowed **inside** the pipeline and are stripped only at the model boundary. ### Stage 1 — Localization (D) Inputs: `issue_text: str`, `repo_root: str`, optional `repro_target: str | None` (a failing test node id). Steps: 1. **Skeleton**: ingest the repo with `memory/codegraph.py` `ingest([repo_root], repo=..., clear=False)` → `code_nodes` (signatures only; bodies never enter the index). If the repo is already ingested and `mtime` is current, skip. 2. **Embed**: `memstore.embed([issue_text] + [n.signature for n in entities])` (fastembed bge-small-en-v1.5, 384-dim) → numpy cosine ranking of every entity against the issue text. 3. **SBFL** (only if `repro_target` is set): run the repro test under stdlib `trace` to collect line-level coverage; score each entity by **Ochiai** `s = failed_covered(entity) / sqrt(total_failed * (failed_covered(entity) + passed_covered(entity)))` aggregated over the entity's lines, where `failed_covered(entity)` = number of FAILING tests that execute the entity, `passed_covered(entity)` = number of PASSING tests that execute it, and `total_failed` = total number of failing tests. (Passed coverage must NOT be the numerator.) Entities with no coverage get `sbfl_score = null`. 4. **Rank + cap**: `combined = 0.7 * sim_norm + 0.3 * sbfl_norm` (sbfl term = 0 if absent; sim-only otherwise). Output the **top-k = 10** entities, `truncated` if there were more. Output: `LocalizationResult`: ```json { "candidates": [ { "name": "...", "kind": "...", "signature": "...", "path": "...", "similarity": 0.83, "sbfl_score": 0.41 | null } ], "truncated": false } ``` Caps: `k = 10` (hard). No LLM involvement. ### Stage 2 — Repair (L) Inputs (assembled into one prompt, in this exact order): 1. `issue_text` (the task statement, verbatim). 2. top-k candidate signatures from Stage 1 (signatures only — no bodies, no line numbers). 3. failing-test output from `run_test(repro_target)` if a repro exists (truncated per §3.5). 4. optional windowed bodies — only if the model requested them via `read_entity(body=True)` during the same run (≤100 lines each, appended after the signatures). Prompt composition is **deterministic** (a fixed template); only the content the model emits is LLM. The model emits a JSON list of `edit_entity` operations: ```json { "edits": [ { "selector": "...", "search": "...", "replace": "..." } ] } ``` **Temperature sampling**: the same prompt is sampled **N = 5** times at `temperature = 0.7` (`max_completion_tokens = 4096` per call). Each sample's edits are applied to a *fresh* copy of the repo (so samples never contaminate each other), each edit gated by `edit_entity`'s linter + evidence gates. A sample whose edits all commit is a **candidate patch**; a sample with any rejected edit is discarded. Output: `RepairResult = { "patches": [ { "patch_id": 0, "edits": [ {...} ], "committed": true }, ... ] }` with `0 ≤ len(patches) ≤ N`. Caps: `N = 5`; `max_completion_tokens = 4096`; single-shot (no re-prompt, no loop — §5). ### Stage 3 — Validation (D) For each candidate patch, in a *clean checkout* of the repo with only that patch applied: 1. **Reproduction**: run `repro_target`; patch must turn it from fail → pass. 2. **Regression**: run the repo's full test suite (or the instance's `PASS_TO_PASS` set); record pass/fail and which tests broke. 3. **AST-normalized majority vote** over patches that passed BOTH 1 and 2: - normalize each patch's edited entity text: strip docstrings + comment nodes, then `ast.dump(ast.parse(edited_src), annotate_fields=False)`. - two patches are **equivalent** iff their normalized dumps are byte-identical; group accordingly. - **select** the largest equivalence class; tie-break = the member with the smallest `difflib` line-delta vs the pre-edit entity. 4. **HALT**: if no patch passes both repro and regression → the run **stops** with `verdict = "halt"`. There is **no escalation** into any open-ended loop (no re-prompt, no retry, no "ask the model to fix the fix"). Output: `ValidationResult`: ```json { "verdict": "resolved" | "halt", "selected_patch_id": 2 | null, "repro_passed": true, "regression": { "passed": 38, "failed": 0 }, "vote_groups": [ { "ids": [0, 2], "count": 2 } ] } ``` --- ## 5. Gates & Caps | Gate / Cap | Value | Behavior on breach | |---|---|---| | **Linter gate** (Stage 2, per edit) | `py_compile` + `ruff --select E9,F` | Reject the edit, return the lint error; the file is never written. | | **Evidence gate** (Stage 2, per edit) | ≥1 read/search record citing the selector | Reject the edit; return `evidence_ok: false`. Monotone (can only block). | | **Validation gate** (Stage 3) | repro **and** regression pass | No pass → `verdict = "halt"`. Never escalates to a loop. | | **max repair iterations** | **1** | Single-shot by design; the cap is structural, not a loop counter. | | **max context length** | **32,768 tokens** | Observation budget §6; overflow → drop oldest collapsed observations first. | | **max total tokens / issue** | **80,000** (≈ Agentless ~78K) | Exceeded → deterministic halt with `verdict = "halt", "reason": "token_cap"`. | | **max completion tokens / repair call** | **4,096** | Truncate the completion; no re-prompt. | | **max candidates N** | **5** | Stage 2 sampling cap. | | **max observations kept** | **20** (one line each) | Oldest dropped first (§6). | A cap hit is always a **deterministic halt** with a structured reason — it is **never** fed back to the model as a prompt, and never triggers another LLM call. --- ## 6. Observation Policy The observation stream is a compressed, structured view of the repo, built deterministically and refreshed only on commit: - **Windowed**: bodies ≤ 100 lines (`read_entity` window). - **Summarized + capped**: `search_code` ≤ 8 results, each a signature + one-line summary. - **Skeletonized**: `search_symbol` / localization return signatures only. - **Collapsed**: old observations are truncated to the **last 20 entries, one line each** (`kind, target, result-in-≤1-line`). Nothing verbatim beyond the window. - **No line numbers, no whole files, no diffs** anywhere in model-facing text. - The ledger that feeds the evidence gate is a separate append-only record (not the collapsed view), so collapse never voids an already-earned evidence entry. --- ## 7. Implementation Order Seven phases, each independently testable and reversible. Each phase lands in `build/01-coding-harness/` and is a standalone importable module. Rollback = delete/flag-off that phase's module; nothing earlier is broken because phases only add modules and never mutate live harness code. | Phase | Delivers | Isolated test | Rollback | |---|---|---|---| | **1. Symbol index + read/search** | `index.py` (reuses `memory/codegraph.py` `ingest`), `primitives.py::read_entity` + `search_symbol` | Ingest a fixture repo; assert `read_entity("x.y")` returns signature-only, body=None, and `search_symbol("mem")` returns ≤10 signature-only matches with `keywords` AND-filtering. | `git rm`/flag-off `index.py` + the two functions; `codegraph` tables untouched (read-only reuse). | | **2. edit_entity + linter gate + evidence gate** | `edit.py` (`edit_entity` + `code_check` gating + evidence ledger) | Unit test: bad syntax → `committed:false` + lint returned; no-evidence → `evidence_ok:false`; unique-match + lint-clean → `committed:true`. File bytes unchanged on any reject. | Delete `edit.py`; no files were committed in tests (temp fixtures only). | | **3. Stage 1 localization** | `localize.py` (skeleton → embed → SBFL → top-10) | Fixture repo with a known fault + repro test; assert the faulting entity ranks in top-10 by combined score; SBFL-only run matches `trace`-derived Ochiai scores. | Delete `localize.py`. | | **4. Stage 2 repair loop** | `repair.py` (deterministic prompt assembly + N=5 temperature sampling) | Mock the LLM client (returns scripted edits); assert 5 samples, each on a fresh repo copy, edits gated, `len(patches) ≤ 5`. | Delete `repair.py`. | | **5. Stage 3 validation** | `validate.py` (repro + regression + AST-normalized majority vote) | Two candidate patches that are AST-identical modulo comments/docstrings must group into one class; a failing patch is excluded; `halt` when none pass. | Delete `validate.py`. | | **6. search_code + run_test + execute sandbox** | `retrieval.py`, `testrun.py`, `sandbox.py` | `execute` probes: network call raises; absolute-path open raises; `while True` hits CPU rlimit; output truncated to 100 lines. `search_code` returns ≤8 summarized results. | Delete the three modules; sandbox runs in temp dirs, no host state. | | **7. Pipeline + A/B harness** | `pipeline.py` (end-to-end), `ab.py` (feature-flag router + metric capture) | End-to-end on 3 warm-up tasks; `ab.py` runs old-vs-new on the §9 sample and emits the joint-metric report. | Feature flag `off` → the live harness is untouched; `ab.py` is a read-only driver. | The A/B harness (Phase 7) is the only phase that ever *invokes* the current harness — and only to measure it, never to modify it. --- ## 8. Dependencies & Supply-Chain Per Piper's rule: nothing is installed unless >72h old AND (≥10 stars OR deep-scrutinized). This build **requires zero new installs** — everything is stdlib, already-installed, or in-repo: | Dependency | Why | Status / age-stars | Install needed? | |---|---|---|---| | `tree_sitter` + `tree_sitter_{python,javascript,typescript,go,rust,php}` | AST skeleton (Stage 1, `read_entity`, `search_symbol`) | Already installed in `~/venvs/memory`; mature lib (years old, ≫10 stars). Reused by `memory/codegraph.py`. | **No** | | `fastembed` + `BAAI/bge-small-en-v1.5` (384-dim ONNX) | embedding retrieval (Stage 1, `search_code`) | Already installed + model cached in `~/memory/models`; ONNX, no torch. Reused via `memory/memstore.py`. | **No** | | `numpy` (1.26.4) | cosine ranking | Installed (system + venv). | **No** | | `ruff` | linter gate | Installed in `~/venvs/memory` (used by `coding-cortex/code_check.py`). | **No** | | `pytest` (9.1.1) | `run_test` / regression | Installed in venv. | **No** | | stdlib `ast`, `difflib`, `trace`, `subprocess`, `resource`, `tempfile`, `sqlite3`, `signal` | SBFL coverage, AST-normalized vote, sandbox rlimits, index storage | CPython stdlib. | **No** | | `memory/codegraph.py`, `memory/memstore.py`, `coding-cortex/code_check.py` | in-repo reuse (skeleton index, embeddings, lint gate) | In-repo, already vetted. | **No** | | `coverage.py` | *optional* richer SBFL | **NOT installed.** stdlib `trace` substitutes for the 80% build. Only adopted later if SBFL needs branch coverage. | **Flagged (optional, deferred)** — not required | | SWE-bench-Verified dataset | A/B sample (§9) | Not a pip package; a data fetch. Not installed locally. | **Flagged (data, not code)** — see §9 fallback | **Preferred path**: stdlib + in-repo components everywhere; nothing pip-installed. The only installable flagged is `coverage.py` (optional), and the only other flagged item is the SWE-bench dataset fetch, both avoidable for the 80% build. --- ## 9. Acceptance Criteria — the A/B Protocol **Sample (concrete).** 20 held-out **SWE-bench-Verified** instances, filtered to Python-only, single-repo, no-Docker reproduction (repro = running the instance's `FAIL_TO_PASS` test through `run_test`). Held out = never seen by any tuning, prompt, or ranking pass. Seeded RNG for reproducibility. **Fallback** (if the dataset cannot be fetched locally): `coding-cortex/piperbench` sampled to 20 tasks, held out identically. **Measures (joint — all three recorded, no single-number score).** 1. **Resolve rate** = `resolved / total` (the score). 2. **Cost** = `$ / instance` (model spend only; deterministic stages are ~free). 3. **Tokens** = `tokens / instance`. **Decision rule (explicit thresholds).** Run candidate-vs-current on the *identical* sample with the *identical* model (`deepseek-v4-pro`) and hardware (this box). The candidate **wins iff**: ``` resolve_candidate > resolve_current (strictly; ties do NOT adopt) AND cost_candidate ≤ 1.5 × cost_current (a worse cost is a disqualifying regression) AND tokens_candidate ≤ 1.5 × tokens_current (a worse token spend is a disqualifying regression) ``` A candidate that wins resolve but fails either cost/token bound is **not adopted**; the tradeoff is recorded in the report and the decision is explicit. If the candidate wins, it replaces the current harness; if it loses, the build tree is kept for the record and the live harness is unchanged. **Ablation rule (mechanical).** Each primitive is removed in isolation, everything else held fixed, and the delta measured. A primitive is **deleted** iff its removal changes none of the three measures beyond noise: `Δresolve = 0` AND `|Δcost| ≤ 5%` AND `|Δtokens| ≤ 5%`. This is applied to `search_code` and the evidence gate first (the two weakest a-priori cases), then to any other primitive, regardless of how sensible it looks. --- ## 10. Guardrails & Reversibility **Revocability.** - The build lives entirely in `build/01-coding-harness/` (a separate tree); it never writes into the live harness's source. - A **feature flag** (env var `HARNESS=agentless|current`, default `current`) routes the A/B; the flag alone switches between old and new — no code deletion required to revert. - `ab.py` is a read-only driver: it *measures* the current harness, it never modifies it. **Invariants (must hold, checked at the end of every phase).** 1. **No new `.md` files outside this folder** (`projects/self-improvement/{papers,specs,build}`). Everything else is code or DB rows. 2. **No change to the live harness** until the §9 A/B gate passes. The current tool surface stays byte-identical during the entire build. 3. **execute() runs only vetted deterministic commands in round 1; arbitrary code execution requires an OS boundary (namespaces/seccomp/bubblewrap/firejail/container) and is deferred until then; Python-level interception is hygiene, not a security boundary.** 4. **Deterministic stages stay deterministic**: no LLM call may be introduced into Stage 1 or Stage 3; the LLM column holds exactly Stage 2. 5. **No escalation**: a cap hit or a validation failure is a deterministic halt, never a prompt to the model and never a new agentic loop. --- *End of spec. This document is the build contract; the next document records the A/B outcome.*