bareloop 0.9.0 → 0.10.0

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.
@@ -294,6 +294,14 @@ N−1 are enforced only at the close, at the end of the run. That is a COST shap
294
294
  hole — the close still refuses a tree that fails any stage, and nothing green is minted mid-run
295
295
  — but a stage first tested at the tail is a stage the run pays full price to discover.
296
296
 
297
+ **The ceiling of 2 is RULED, not provisional (2026-08-13, PRD v1.63 §3).** One check per write
298
+ step is the contract: a step passing its own in-run check is never a verdict, only the close is
299
+ truth, and widening the ceiling so a step's own gate could approximate the close would move
300
+ judgement into the part the agent authors. Plan around one check; do not plan for a second one
301
+ arriving. It is raised only on a demonstrated starvation case (a real run where a step provably
302
+ cannot express its progress gate within one check and stalls for it), never for wording comfort,
303
+ and the change is the operator's — arbiter territory.
304
+
297
305
  Red vocabulary (both validators): `parse-error`, `unknown-field`, `missing-required`,
298
306
  `invalid-value`, `bounds`, `duplicate-id`, `close-type`, `close-hierarchy`,
299
307
  `secret-literal`, `scope-escape`,
@@ -480,10 +488,10 @@ a step, or test it without a provider.
480
488
  | step | call | what it is |
481
489
  |---|---|---|
482
490
  | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), and the interview asks NOTHING about a genre. The green set is six questions, answers keyed by number; the soft-green and hitl sets are named but LOCKED (`questions: null`, absent rather than empty) and their selection refuses before they run. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
483
- | survey | `runAuthorScout({ workdir, provider, attempts? })` → `{ state, facts, reason, meta, raw, raws, calls }` | a bounded READ-ONLY LLM survey. Read-only by MENU CONSTRUCTION (`AUTHOR_SCOUT_VERBS` = the full menu minus write-class and store-class verbs), 8 rounds, F59's reserved toolless final round. **`state: 'ABSENT'` means the scout did not complete — never "no special facts are needed"**, and a parsed `{}` is one of its five ABSENT routes. Up to `SCOUT_ATTEMPTS` (**3**, hardcoded — PRD v1.58) attempts: `attempts` is TIGHTEN-ONLY and CLAMPS rather than throws (floor **1**, because a scout that never ran is an ABSENT nobody can act on), the direction every cap in this system runs in. **A retry fires on the typed MALFORMED class alone** (`SCOUT_RETRY_CAUSES` = `empty` \| `unparseable`); what is excluded is excluded for a reason — `call-failed` covers transport and `truncated:max_tokens`, both provider-red with NO redraft; `short` is F59's cut-off population and already has its own instrument INSIDE the attempt; `non-object` and `empty-object` are valid JSON with wrong or vacuous content, which is a SEMANTIC failure, and F38/F39 measured what re-asking one buys (the same distribution, sampled twice) — that is the self-healing line, and it is not crossed here. The re-ask is TOOLLESS over the conversation the survey already produced (the repository was already read; only the emission was unreadable) and names the mechanical parse error and nothing else. **There is no JSON repair behind it and there never will be** — a repairer decides what the model MEANT and writes it down as though the model had said it, in the one artefact whose whole job is to be honest about what a repository contains. `meta.attempts` / `meta.attemptsAllowed` record what was paid for; every attempt is metered under its own label (`author-scout`, `author-scout#2`, …) and leaves its raw behind |
491
+ | survey | `runAuthorScout({ workdir, provider, attempts?, ceilingUsd? })` → `{ state, facts, reason, meta, raw, raws, calls, budgetStop }` | a bounded READ-ONLY LLM survey. Read-only by MENU CONSTRUCTION (`AUTHOR_SCOUT_VERBS` = the full menu minus write-class and store-class verbs), 8 rounds, F59's reserved toolless final round. **`state: 'ABSENT'` means the scout did not complete — never "no special facts are needed"**, and a parsed `{}` is one of its five ABSENT routes. Up to `SCOUT_ATTEMPTS` (**3**, hardcoded — PRD v1.58) attempts: `attempts` is TIGHTEN-ONLY and CLAMPS rather than throws (floor **1**, because a scout that never ran is an ABSENT nobody can act on), the direction every cap in this system runs in. **A retry fires on the typed MALFORMED class alone** (`SCOUT_RETRY_CAUSES` = `empty` \| `unparseable`); what is excluded is excluded for a reason — `call-failed` covers transport and `truncated:max_tokens`, both provider-red with NO redraft; `short` is F59's cut-off population and already has its own instrument INSIDE the attempt; `non-object` and `empty-object` are valid JSON with wrong or vacuous content, which is a SEMANTIC failure, and F38/F39 measured what re-asking one buys (the same distribution, sampled twice) — that is the self-healing line, and it is not crossed here. The re-ask is TOOLLESS over the conversation the survey already produced (the repository was already read; only the emission was unreadable) and names the mechanical parse error and nothing else. **There is no JSON repair behind it and there never will be** — a repairer decides what the model MEANT and writes it down as though the model had said it, in the one artefact whose whole job is to be honest about what a repository contains. `meta.attempts` / `meta.attemptsAllowed` record what was paid for; every attempt is metered under its own label (`author-scout`, `author-scout#2`, …) and leaves its raw behind. **`ceilingUsd` is the operator's money ceiling** (PRD v1.62) — checked BETWEEN attempts and before F59's reserved round, so no paid call escapes it; `null`/absent is UNBOUNDED, and a non-null non-finite value THROWS (the shared `capStop` seam — see `authorClose` below). When it ends the ladder, `budgetStop` names which stop (`cap-halt` \| `pricing-red`) and a survey that was never asked for carries the typed cause `not-funded` — deliberately OUTSIDE `SCOUT_RETRY_CAUSES`, because a retry is precisely what the cap forbade |
484
492
  | listing | `buildSeedListing({ workdir, seedRef, sourcePaths, testPaths })` | mechanical, `$0`, no model. `files` is the WHOLE tree (what the validator judges paths against); `block` is scoped to the survey's own paths and capped in ANNOUNCED tiers (what the prompt carries). Handing the validator the scoped half would make a job scoped to `src/` read as whole-tree and silently disarm the one-population law |
485
- | authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second |
486
- | everything above, composed | `authorCloseForJob({ verdictType, answers, repoPath, lang, generate, ... })` → `{ ok, closeDecl, verdictType, refusal, cost, ... }` | refuses at the cheapest gate that can refuse: an interview refusal costs **zero**. THE GENRE REFUSAL LIVES HERE (not in the interview): a language the catalogue owns no data for, and a LOCKED KIND the model reached for, both come back as counted `request-red` demand |
493
+ | authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate, ceilingUsd? })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second. **`ceilingUsd` is the same operator ceiling the survey took** — the scout's spend is ABSORBED into this loop's cost book before its first call is weighed, so spend already incurred folds in and re-entering cannot silently widen it. It is checked immediately before EVERY paid call (the author call, each revise, and each malformed-emission retry). **A MALFORMED ceiling is an ERROR, not a silent unbounded run** (v1.64 §3): the seam every ceiling read goes through (`capStop`, `src/text.js`) THROWS on a non-null non-finite value — `'2.50'`, `NaN`, `Infinity`, `true`, `{}` — before the first paid call, and the throw propagates uncaught rather than being caught into an ABSENT survey, because a caught one would hand you the very silent-unbounded run it exists to prevent. `null`/omitted is the STATED operator choice for unbounded; `0` and negatives are finite, legal, and cap-halt immediately. The advertised ceiling and the enforced ceiling can therefore never be different numbers. A `cap-halt` here can arrive with `ok:true` — a close that was already validated and measured survives the money stop, exactly as a late provider casualty does |
494
+ | everything above, composed | `authorCloseForJob({ verdictType, answers, repoPath, lang, generate, ceilingUsd?, ... })` → `{ ok, closeDecl, verdictType, refusal, cost, ... }` | refuses at the cheapest gate that can refuse: an interview refusal costs **zero**. THE GENRE REFUSAL LIVES HERE (not in the interview): a language the catalogue owns no data for, and a LOCKED KIND the model reached for, both come back as counted `request-red` demand. **`ceilingUsd` is ONE number handed to BOTH paid seams** (the survey's and the declaration loop's) — the advertised ceiling and the enforced ceiling are the same ceiling. It travels as an EXPLICIT `null` when unset, because unbounded is a stated choice and an absent field read as falsy is that state reached by accident — and anything that is neither a finite number nor `null` throws at the seam rather than becoming that state by typo |
487
495
  | assembly | `assembleSpec(specDraft, { closeDecl, verdictType })` | folds the authored half into the OPERATOR's half. Budgets, the fence, cadence, escalation and the provider are never authored by anything here. The GOAL is passed through, not generated. **A draft that ALREADY carries the authored half is REFUSED — it THROWS, never merges and never overwrites.** `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType`) names the fields this fold WRITES, as data, so the refusal and the fold cannot disagree about which half of the spec is which; the rule is `job.js`'s one step earlier (two closes are two arbiters, and picking one silently is how a signed artefact stops meaning what the signer read). `scripts/run-author.mjs` asks the same question at **$0 BEFORE the scout**, so the answer never arrives after the model has been paid |
488
496
  | the three gates | `prepareSigning({ spec, workdir, seedRef? })` → `{ ok, specHash, gates, work, guards, refusal }` | D9, and it NEVER signs |
489
497
 
@@ -510,6 +518,26 @@ declaring one is a counted `locked-kind` red rather than an unknown-kind typo; `
510
518
  tool mode a locked kind is INEXPRESSIBLE — the schema carries one branch per live kind — so
511
519
  that demand arrives through the interview layer instead (`refuseLockedKind(kind)`).
512
520
 
521
+ **A red `count-not-worse` gap carries the LINES it counted, not only how many (F98).** Each
522
+ term's **KEPT** matched lines nest under that term's own breakdown row — the row that already
523
+ prints its `contributes +n`/`-n`, so a line's arithmetic role travels with it. Kept means the
524
+ lines that survived the scope filter and were actually read by the aggregate (a `first` term
525
+ echoes only the line that produced its value), which is to say the ones the count is made of.
526
+ A scope-DROPPED line is never among them (it was never counted, so naming it would aim the
527
+ worker outside its own population, F84's one-population law), duplicates collapse within a
528
+ term (a line feeding two terms plays two roles and appears under each), and the value is
529
+ still computed from the parsed values and never from this list.
530
+ They ride the **existing** gap channel and invent none: the declared stage's `gapKeep` prefix
531
+ on every line (Layer R's `redKeep` is DERIVED from that prefix), the same `GAP_LINE_CAP`, and
532
+ the same announced trim on overflow — so a consumer parsing the gap sees more lines under the
533
+ same shape, never a new one. `parseValue` returns them as `matched` alongside `value`,
534
+ per-term, index-aligned with the breakdown. The
535
+ licence is the one `pattern-absent-in-diff` already runs under: the instrument NAMED these
536
+ lines, and echoing what it named is not the barred move of naming a culprit it never
537
+ reported. **Nothing here can flip a signed spec hash** — the verdict logic, the baselines,
538
+ the counting, the `closeDecl` schema and `detail` are untouched; only what the executor TELLS
539
+ the worker changed.
540
+
513
541
  **The three gates, and the signature (D9).** Nothing LLM-judges a close.
514
542
  1. `validateCloseDecl(closeDecl, { verdictType, ... })` — schema, kinds, params, the F84
515
543
  one-population law, F49's static nested-quantifier reject, the D5 guard equality, the
@@ -524,12 +552,44 @@ that demand arrives through the interview layer instead (`refuseLockedKind(kind)
524
552
  **A close with no WORK stage red at seed is refused decision-ready** — it grades nothing,
525
553
  and an instrument that scans nothing reads clean exactly like one that measures correctly.
526
554
 
555
+ **KNOWN LIMIT: gate 1 does not check a parser's FORMAT, and never will (F94, ruled WON'T-BUILD
556
+ 2026-08-13, PRD v1.63 §1).** A declaration can name the right stage, over the right population,
557
+ with the right aggregate, and carry a `lineMatch` for a shape the tool never prints. Detecting
558
+ that at draft time needs stage → tool attribution, and `npm run typecheck` defeats it
559
+ mechanically: the declared command names npm, what prints is `tsc`, and the mapping lives in a
560
+ `package.json` the validator does not read. A detector that resolved some spellings and passed
561
+ the rest would read as coverage while being blind exactly where patients differ. **Gates 2 and 3
562
+ are the instrument for this class, and they work** — a term matching nothing over a non-empty
563
+ population reads unknown-not-zero (F6/F45/F93), so the seed-verdict read refuses it at $0 before
564
+ a signature, which is how the real case was caught. Formats are checked by RUNNING the tool,
565
+ never by reading a string. The complementary defence is upstream of the gate: for the tools the
566
+ genre owns facts about, `genreInstruments` hands the author the measured pattern (with its real
567
+ captured line) instead of asking, so fewer declarations reach gate 1 carrying an invented format
568
+ at all. Reopens only if a wrong-format parser ever reaches a SIGNED close.
569
+
527
570
  Then the human signs `specHash`, unchanged: the approvals array and the human's word are
528
571
  the arbiter relocating to the user, not disappearing. **Re-authoring is a spec edit (D6)** —
529
572
  a different declaration is different bytes, a different resolved hash, and a new signature.
530
573
  The guards are stored ENUMERATED and every short-form parser is expanded, so no
531
574
  omittable-with-a-default field can change what runs without changing what was signed.
532
575
 
576
+ **A signing surface must show the GOAL and the judged stages as one reading (F87).** The
577
+ goal has to state everything the close will judge, and **nothing derives one from the other
578
+ or checks them against each other** — the only derivation runs close-stages → check menu, one
579
+ hop, one direction. So the sole defence against an unstated stage is that the person signing
580
+ reads both halves at once, and any surface you build around `prepareSigning` inherits that
581
+ obligation. Both reference runners now do it: `scripts/run-u.mjs --approve` names every close
582
+ stage (name + kind) under the goal — for **BOTH spec forms**, through `closeStagesOf`, since
583
+ gating that block on `closeDecl` reached exactly one of the eleven shipped specs and left the
584
+ other ten (command `close[]`) showing the goal with no stages under it, which is the same
585
+ half-reading in the guard against it (F100); a command stage prints `[command]` and NOT the
586
+ catalogue kind name it does not carry — and `scripts/run-author.mjs` prints the goal above the
587
+ declaration via `declarationLines(spec)` in `scripts/author-readout.mjs`, rendered from the
588
+ **RESOLVED** spec — the bytes that get hashed, never the draft and never the authored half
589
+ alone. An absent goal renders as absent rather than as a bare label. This is a READING, not a
590
+ validator: nothing compares the goal to the declaration, because deriving one from the other
591
+ is exactly what F87 forbids.
592
+
533
593
  **Refusals are COUNTED (D13).** `refusalEvents(refusal)` returns the spine events: a
534
594
  `job-red` carrying `{ code: 'request-red', verb, lib: 'bareloop' }` — the `lib` stamped at
535
595
  the EMIT SITE, so `classifyIncidents` files it against bareloop's own catalogue and its
@@ -661,9 +721,37 @@ three times on one call — each stall silently abandons the hung call and reiss
661
721
  laundered into `provider-red`, and carries `spendComplete:false` (an abandoned call may
662
722
  already have been billed). **The reissue is wall-aware:** past the run's deadline the fuse does
663
723
  not self-heal — self-heal is what a run does with time left — and it gives up as `wall-halt`,
664
- not `step-stalled`, since a replan has nothing left to re-allocate. Every one of them is a
724
+ not `step-stalled`, since a replan has nothing left to re-allocate. **A `step-stalled` run is
725
+ a CHECKPOINT, not a verdict** (hamr's ruling, 2026-08-13 — PRD v1.64 §1): it is resumable on
726
+ the reference runner alongside the two governance halts, the work on disk stands, and its
727
+ `spendComplete:false` carries forward as a floor. Every one of them is a
665
728
  decision-ready escalation with a terminal `job-end`: the spine never dangles.
666
729
 
730
+ **`gate-red` is not in that list, and the plan flow no longer mints it (F98).** A fence
731
+ DENIAL STREAK — bare-agent's BA-11 deny-spin guard returning `denied:*` after consecutive
732
+ refusals — is a **BOUNDED ATTEMPT, never a terminal**, on BOTH worker surfaces: the attempt
733
+ ends, `attempt-bounded` is emitted with the `reason`, the close judges the partial work, the
734
+ gap feeds forward, the caps are unchanged and the loop continues.
735
+ **The NEXT attempt is told which bound fired** (v1.64 §2): the bound carries
736
+ `{iteration, cause, reason}`, and where the cause is `denied` the note says the GATE cut the
737
+ attempt and quotes what the gate recorded, verbatim — instead of the round-cutoff sentence,
738
+ which was false on cause and on count and aimed the worker at its read budget when the thing
739
+ that stopped it was the fence. **Nothing is invented**: bare-agent returns `denied:<tool>`
740
+ and nothing else — no path, no streak count — so neither is claimed. Every other cause keeps
741
+ the round-bound wording byte for byte. The reason is scrubbed ONCE at capture through the one
742
+ `SECRET_PATTERNS` inventory and shared by prompt and spine, and trimmed under the repo's one
743
+ `GAP_TRIM_MARKER` (F90.2). It is the same lane
744
+ `max_turns` and `loop.stop()` take, for the same reason — the attempt ran, produced audited
745
+ work, and was cut short by a governance bound doing its job. **The fence holding is the
746
+ system working, which is the opposite of a fault**; routing it as an instrument stop killed a
747
+ converging run with $1.77 and 21.6 wall-minutes still on it. Nothing about the gate changed:
748
+ the same actions are denied, the same audit rows are written, a genuine scope escape produces
749
+ exactly what it did before — it now costs one bounded attempt instead of the whole run. The
750
+ category survives only as a passthrough (`ralph`'s decision table still answers to it if a
751
+ caller's own middle throws it, and `EXCLUDED_ESCALATIONS` still lists it so a future emission
752
+ is filed as governance rather than re-read as a capability gap), but no site in this library
753
+ raises it.
754
+
667
755
  **Every `job-end` carries the money, on every path**: `{ outcome, spentUsd, spendComplete }`
668
756
  (plus `step`/`cause`/`detail` where the outcome has them). `spentUsd` is the accumulated sum
669
757
  of PRICED rounds ONLY — never an estimate from token counts or averages (cap-not-estimate) —
@@ -1000,7 +1088,9 @@ learning credit and runs no on-green retention. A close that cannot RUN is a `br
1000
1088
  escalation before any provider call. So the resume story is: a `cap-halt` or `wall-halt`
1001
1089
  stop IS the checkpoint (the workdir + the close), the human raises `budgetUsd` or
1002
1090
  `maxWallMs` (a new spec hash — re-sign), and the rerun picks up where the budget died,
1003
- paying zero provider calls for anything already green.
1091
+ paying zero provider calls for anything already green. (Those two are the *allowance* stops,
1092
+ the ones with a lever to raise. They are not the whole resumable set — `step-stalled` is also
1093
+ a checkpoint, with nothing to re-sign; see `resumableOutcomes` under the resume reader.)
1004
1094
 
1005
1095
  **Unpriced is never free (F6/F12):** the ledger meters **per ROUND** (`worker-round`, at
1006
1096
  bare-agent's `onLlmResult` seam) — money is counted as it is spent, never at the end of an
@@ -1022,7 +1112,14 @@ the loop contract (F16 — it is ONE attempt inside `while close-red and under-c
1022
1112
  re-run with the close's verdict; without this a model one-shots and can eat the budget in
1023
1113
  reads before ever writing). It is NOT allowed to read the run's own machinery (F14): the gate
1024
1114
  audit, the smoke store and the litectx store are denied — the agent neither authors the
1025
- arbiter nor reads its books. N2 bounds (honest): `gold`/`rubric` closes refuse
1115
+ arbiter nor reads its books. **That denial is now REGISTERED in the persona, not left to be
1116
+ discovered (F98):** `PERSONA_TOOLS` names `gate-audit.jsonl`, `.smoke`, `.litectx` and the
1117
+ run's spine as always-denied, records of how the worker is being judged and never anything
1118
+ about its task, beside the absolute-path law it is the twin of — both are fence facts a worker
1119
+ cannot infer. It renders for EVERY worker on every grant (a worker granted only `write` gets
1120
+ no component strategy paragraph at all and must still be told). The fence is unchanged; what
1121
+ changed is that the rule is stated instead of learned one refusal at a time, which cost a real
1122
+ run the rounds of a bounded attempt. N2 bounds (honest): `gold`/`rubric` closes refuse
1026
1123
  `close-unsupported` (execution lands at N4).
1027
1124
 
1028
1125
  ### The reuse registry (Layer 3) — `src/bridges.js`, `src/selection.js`
@@ -1242,11 +1339,33 @@ semantics are byte-unchanged (PRD v1.46 §3):
1242
1339
  is `$0`: a leg whose every round came back unpriced folds nothing countable and is still not
1243
1340
  exact, so the declaration is emitted to carry that unknown forward rather than letting it read
1244
1341
  as an exact zero (F6).
1245
- - **`resumableOutcomes: ['cap-halt', 'wall-halt']`** reclassifies a landed `job-end` whose
1246
- outcome is a GOVERNANCE HALT as a restart rather than a completed row. "A landed job-end is
1247
- complete" is true of a VERDICT; a halt means an operator-owned allowance ran out with the
1248
- work on disk and the plan on the spine. A green or any red stays non-resumable under every
1249
- setting — a verdict already rendered is never re-bought.
1342
+ - **`resumableOutcomes: [...]`** reclassifies a landed `job-end` whose outcome is a
1343
+ CHECKPOINT rather than a verdict as a restart rather than a completed row. "A landed
1344
+ job-end is complete" is true of a VERDICT; a checkpoint means the work is on disk and the
1345
+ plan is on the spine with nothing decided against them. **The set is the CALLER's**, and
1346
+ the reference runner's is `['cap-halt', 'wall-halt', 'step-stalled']` (`run-u`'s
1347
+ `RESUMABLE_HALTS`). The first two are governance halts — an operator-owned allowance ran
1348
+ out. `step-stalled` joined them 2026-08-13 on hamr's ruling (PRD v1.64 §1): it is the one
1349
+ terminal whose OWN escalation already offers *"retry the run"*, nothing about the work on
1350
+ disk is wrong, and the shape that named the fix is a stall that trips with time left and
1351
+ then rides out past the wall — losing a checkpoint a `wall-halt` would have kept. Its NAME
1352
+ deliberately stays `step-stalled` because `runJob` keys the **F44 spend FLOOR** on that
1353
+ outcome; renaming it would report unknown spend as exact. The floor travels: a stalled
1354
+ leg's `spendComplete:false` reaches the preview as `≥$x` and the resumed leg as
1355
+ `priorSpendComplete: false`, so the next terminal stays a floor rather than healing an
1356
+ unknown by inheriting it. A green or any red stays non-resumable under every setting — a
1357
+ verdict already rendered is never re-bought.
1358
+ **One exception, and it is a DERIVATION rather than a widening (D4a, F96).** A recorded
1359
+ `step-red` is re-read as a wall-halt — and becomes resumable — when the run's OWN spine
1360
+ carries a `wall-bounded {bounded: true}` record STRICTLY BEFORE its terminal, and only when
1361
+ `'wall-halt'` is itself in `resumableOutcomes`. It fires on the run's own primary evidence,
1362
+ which no caller can assert; a `step-red` with no wall record in front of it is untouched, so
1363
+ "a red is an answer" survives intact. It touches no close verdict — the grades stand and the
1364
+ resume seeds from them. The readout surfaces `wallDerivedHalt` so an operator resuming a spine
1365
+ that SAYS `step-red` is told which record turned it into a checkpoint. **This is a PERMANENT
1366
+ safety net, not a migration shim (ruled 2026-08-13, PRD v1.63 §2):** the defect that minted
1367
+ the shape is fixed at source, but spines are append-only forever, so the population it protects
1368
+ can never shrink — and on a healthy spine it is a branch that is never taken.
1250
1369
 
1251
1370
  `restart.branch` is the WORK BRANCH the dead leg was working on, READ off its `work-branch`
1252
1371
  record and never re-derived: the name is deterministic from the signed spec, so a restart that
@@ -1328,6 +1447,12 @@ and is re-signed.
1328
1447
  fold — a garbage, non-finite or negative `count` reads as `0` rather than widening the ceiling.
1329
1448
  Null/omitted is the cold path. The disarm control is pinned: a resumed leg with an UNSPENT
1330
1449
  ceiling still replans, so the bound is bounded, not switched off.
1450
+ **NEVER-REFILL is now the RULING, not just the behaviour (2026-08-13, PRD v1.63 §4).** A resume
1451
+ inherits the spent replan ledger, entire; a refill would let every kill buy an allowance, which
1452
+ is unlimited replanning through the side door, and unlimited replanning launders thrash as
1453
+ adaptation. The consequence for an adopter is a real one and is deliberate: a resume whose
1454
+ ledger is already spent re-enters its plan with no channel to replace it. That is not funded —
1455
+ it is SHOWN, by the resume banner, before you sign.
1331
1456
  - **Under the REMAINDER, never a fresh allotment.** The killed attempt's spend and wall
1332
1457
  FOLD IN (`runJob`'s `priorSpentUsd`/`priorSpendComplete`/`priorWallMs`, `createClock`'s
1333
1458
  `priorElapsedMs`), so
@@ -1405,7 +1530,9 @@ seeding an upstream ask. Deliberate exclusions
1405
1530
  counted as unmapped, never dropped): `cap-halt`/`wall-halt` (budget stories, money and
1406
1531
  time), `step-stalled` (the stall fuse firing is our governance, not an observed provider
1407
1532
  failure), `step-variance` (a planning story), `gate-red`/`smoke-red` (governance working
1408
- as intended / already counted), `hitl-close`/`close-unsupported`/`close-unauthorable` (by design — and the last one
1533
+ as intended / already counted — and `gate-red` is no longer minted anywhere in this library,
1534
+ kept listed because this set is EXECUTABLE: dropping a name would not delete the category, it
1535
+ would re-file any future emission of it as a counted capability gap), `hitl-close`/`close-unsupported`/`close-unauthorable` (by design — and the last one
1409
1536
  is excluded for a SECOND reason: its demand is already counted once as the `request-red`
1410
1537
  the same refusal emits, so counting the escalation too would double every refusal),
1411
1538
  `close-timeout`/`close-killed`/`close-crashed` (the arbiter's own named terminals, F17);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/authorflow.js CHANGED
@@ -78,11 +78,11 @@ import { join, relative, isAbsolute } from 'node:path';
78
78
  import { seedRead as runSeedReadStages, makeSeedTrees, AGGREGATES, SIGNS, MAX_TERMS } from './kinds.js';
79
79
  import {
80
80
  KIND_CATALOGUE, MAX_STAGES, DIRECTIONS, BASELINES,
81
- GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames,
81
+ GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
82
82
  validateDeclaration, envCapableKind, VERDICT_CLASSES, LIVE_CLASSES,
83
83
  } from './authoring.js';
84
- import { buildSeedListing, cleanEntry } from './authorscout.js';
85
- import { extractArtifact, priceOf, scrubRaw } from './text.js';
84
+ import { buildSeedListing, cleanEntry, SURVEY_CAUSES } from './authorscout.js';
85
+ import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop } from './text.js';
86
86
  import { redactSecrets } from './validate.js';
87
87
 
88
88
  const require = createRequire(import.meta.url);
@@ -496,6 +496,60 @@ THE PARSER reads ONE number out of a command's output, as signed arithmetic over
496
496
  not actually happen.`;
497
497
  }
498
498
 
499
+ /**
500
+ * HOW THE GENRE'S OWN TOOLS PRINT — rendered FROM `genreInstruments`, so the
501
+ * pattern the author is handed and the pattern the tree already grades with are
502
+ * one string.
503
+ *
504
+ * This block is the fix for run msmbpjk6: a parser for a tool WE OWN THE FACTS
505
+ * ABOUT was left to the model to compose, and it composed tsc's terminal-only
506
+ * shape against piped output — 0 matches on 67 real error lines. The delivery is
507
+ * deliberately the MANDATORY-GUARD delivery, not advice: the pattern arrives
508
+ * spelled out, marked genre property, with the real captured line beside it, in
509
+ * the same register as the guard battery that arrives already parameterised.
510
+ *
511
+ * It states the MECHANISM (pipe, not a terminal) rather than only the answer,
512
+ * because the same trap is waiting behind every tool that pretty-prints for a
513
+ * human, and only some of them are in the table yet. That prediction has since
514
+ * been paid off by measurement: `node --test` prints TAP down a pipe and the
515
+ * spec reporter on a terminal, the identical flip, on a second tool.
516
+ *
517
+ * Each row also carries its `capture`, because a count term and a tally term are
518
+ * read by DIFFERENT aggregates and the wrong pairing is a silent misreading, not
519
+ * an error — `first` over a line the tool omits on a good run reports a broken
520
+ * instrument. The block states that mapping once rather than per row.
521
+ * @param {string} lang
522
+ */
523
+ export function instrumentsBlock(lang) {
524
+ const rows = genreInstruments(lang).map((i) => `- ${i.id} — ${i.what}\n`
525
+ + ` lineMatch: ${i.lineMatch}\n`
526
+ + ` capture: ${i.capture === null ? 'null' : i.capture}\n`
527
+ + ` a REAL captured line: ${i.example}\n`
528
+ + ` why this spelling: ${i.why}`);
529
+ return `KNOWN INSTRUMENTS — how this genre's own tools PRINT, fixed by the genre, not yours to decide
530
+
531
+ A close captures output through a PIPE, never a terminal. Tools that pretty-print for a human
532
+ print something else entirely down a pipe, and the piped shape is the only one a close ever reads.
533
+ The patterns below are MEASURED against real captured output of this genre's tools.
534
+
535
+ Where a stage of yours counts one of these, use its lineMatch EXACTLY as written — a pattern you
536
+ compose yourself for one of these tools is a pattern for output that never arrives, and a term that
537
+ matches nothing does not report a small number, it reports a broken instrument.
538
+
539
+ Each row states its own capture, and that choice decides the aggregate:
540
+ capture: null the term TALLIES one per matching line. Use aggregate "sum" — over no matching
541
+ lines that is a legitimate counted zero.
542
+ capture: <n> the term reads the FIGURE the tool printed, out of that group. Use aggregate
543
+ "first" — and note that no match at all then means the tool never reported the
544
+ number, which is a broken instrument, never a zero. A count is listed this way
545
+ only where the tool prints the line even when the number is zero.
546
+
547
+ ${rows.join('\n\n')}
548
+
549
+ This covers only the tools listed. For anything else, you still compose the pattern yourself from
550
+ the facts object — and say in your notes when you had to.`;
551
+ }
552
+
499
553
  /** The three laws. Stated to the author; enforced, where checkable, by M2. */
500
554
  export function lawsBlock() {
501
555
  return `THE LAWS YOUR DECLARATION MUST OBEY
@@ -611,6 +665,11 @@ measure anything.`;
611
665
  + 'This job\'s genre carries a fixed close SHAPE. The following policy is law for your declaration. It names\n'
612
666
  + 'no command, no path, no environment value and no pattern for this repository — finding those is your work,\n'
613
667
  + `from the facts object and the interview:\n\n${TYPES_GENRE_TEMPLATE}`,
668
+ // The template deliberately names no regex for this repository — but the
669
+ // shape a KNOWN tool prints is not "for this repository", it is a fact about
670
+ // an instrument the genre already owns and already grades with. It sits here,
671
+ // immediately after the policy that mandates the typecheck stage.
672
+ instrumentsBlock(lang),
614
673
  'MANDATORY GUARDS — already placed and already parameterised\n\n'
615
674
  + 'This kind of job carries stages the person did not ask for. They are fixed by the job\'s genre and they are\n'
616
675
  + 'already in your declaration, with their pattern battery and extensions already filled in — those are genre\n'
@@ -853,7 +912,8 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }) {
853
912
  * run could not be read afterwards. A cost entry with no raw beside it is that
854
913
  * run again.
855
914
  */
856
- export function makeCostBook() {
915
+ /** @param {{ceilingUsd?: number|null}} [o] the OPERATOR's ceiling; `null`/absent is UNBOUNDED */
916
+ export function makeCostBook({ ceilingUsd = null } = {}) {
857
917
  /** @type {{label: string, costUsd: number|null, unpricedRounds: number}[]} */
858
918
  const entries = [];
859
919
  /** @type {ReturnType<typeof scrubRaw>[]} */
@@ -872,19 +932,19 @@ export function makeCostBook() {
872
932
  * `report().calls`, because both are readings of one list */
873
933
  raws() { return raws.map((w) => ({ ...w })); },
874
934
  report() {
875
- const known = entries.reduce((a, e) => a + (typeof e.costUsd === 'number' ? e.costUsd : 0), 0);
876
- const nullCostCalls = entries.filter((e) => e.costUsd === null).length;
877
- const unpricedRounds = entries.reduce((a, e) => a + (e.unpricedRounds || 0), 0);
878
- const spendComplete = nullCostCalls === 0 && unpricedRounds === 0;
879
- return {
880
- costUsd: spendComplete ? Number(known.toFixed(6)) : null,
881
- knownUsd: Number(known.toFixed(6)),
882
- spendComplete,
883
- nullCostCalls,
884
- unpricedRounds,
885
- calls: entries.map((e) => ({ ...e })),
886
- };
935
+ return { ...tallyCalls(entries), calls: entries.map((e) => ({ ...e })) };
887
936
  },
937
+ /**
938
+ * THE CEILING'S OWN READING, asked between metered calls. The book is the one
939
+ * place this pipeline's spend accumulates, so it is the one place that can
940
+ * answer honestly — and it answers through the shared predicate rather than a
941
+ * second hand-spelled comparison (`src/text.js`, beside `priceOf`).
942
+ * @returns {'cap-halt'|'pricing-red'|null}
943
+ */
944
+ capStop() { return capStop({ ceilingUsd, ...tallyCalls(entries) }); },
945
+ /** what was set, stated — `null` is "nobody set one", which is UNBOUNDED and
946
+ * a visible operator choice, never a defaulted number */
947
+ ceilingUsd,
888
948
  };
889
949
  }
890
950
 
@@ -939,6 +999,16 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
939
999
  let red = null;
940
1000
 
941
1001
  for (let attempt = 0; attempt <= retries; attempt += 1) {
1002
+ // THE CEILING, immediately before the paid call and nowhere else. Sited HERE
1003
+ // rather than in the revise loop because this is where every declaration call
1004
+ // is actually made: the author call, each revise, AND each malformed-emission
1005
+ // retry. The retry axis is the one that matters most — it fires exactly when
1006
+ // the model is malforming, which is the worst moment for a paid loop to be
1007
+ // unbounded. `attempts` is returned as-is so the caller can tell a cap that
1008
+ // tripped before this ask began (0) from one that cut a retry ladder short.
1009
+ const halt = book.capStop();
1010
+ if (halt) return { declaration: null, attempts, convo, raw: '', providerError: null, red: null, budget: halt };
1011
+
942
1012
  /** @type {{calls: any[]}} */
943
1013
  const box = { calls: [] };
944
1014
  const tools = mode === 'tool' ? [declarationTool(box, { catalogue })] : [];
@@ -948,11 +1018,11 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
948
1018
  const raw = redactSecrets(String(r?.text ?? ''));
949
1019
 
950
1020
  if (r?.error) {
951
- return { declaration: null, attempts, convo, raw, providerError: String(r.error), red: null };
1021
+ return { declaration: null, attempts, convo, raw, providerError: String(r.error), red: null, budget: null };
952
1022
  }
953
1023
 
954
1024
  if (mode === 'tool') {
955
- if (box.calls.length === 1) return { declaration: box.calls[0], attempts, convo, raw, providerError: null, red: null };
1025
+ if (box.calls.length === 1) return { declaration: box.calls[0], attempts, convo, raw, providerError: null, red: null, budget: null };
956
1026
  red = {
957
1027
  code: 'artifact-red',
958
1028
  path: DECLARATION_TOOL_NAME,
@@ -966,7 +1036,7 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
966
1036
  // parser, never a second one.
967
1037
  const { code, red: extractRed } = extractArtifact(raw);
968
1038
  if (code) {
969
- try { return { declaration: JSON.parse(code), attempts, convo, raw, providerError: null, red: null }; }
1039
+ try { return { declaration: JSON.parse(code), attempts, convo, raw, providerError: null, red: null, budget: null }; }
970
1040
  catch (e) { red = { code: 'artifact-red', path: 'declaration', detail: `the declaration did not parse as JSON: ${String(/** @type {any} */ (e)?.message ?? e)}`, axis: 'unparseable-json' }; }
971
1041
  } else {
972
1042
  red = { code: 'artifact-red', path: 'declaration', detail: `no artifact in the reply: ${extractRed}`, axis: 'no-artifact' };
@@ -979,7 +1049,7 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
979
1049
  convo.push({ role: 'user', content: `${block}\n\n${mode === 'tool' ? STRUCTURE_INSTRUCTION_TOOL : STRUCTURE_INSTRUCTION_TEXT}` });
980
1050
  }
981
1051
  }
982
- return { declaration: null, attempts, convo, raw: '', providerError: null, red };
1052
+ return { declaration: null, attempts, convo, raw: '', providerError: null, red, budget: null };
983
1053
  }
984
1054
 
985
1055
  /**
@@ -1010,9 +1080,11 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
1010
1080
  *
1011
1081
  * @param {{workdir: string, seedRef: string, lang: string, verdictType: string,
1012
1082
  * answers: Record<string|number, string>, questions?: Record<string|number, string>,
1013
- * scout: {state: string, facts: any, reason?: string|null, calls?: any[], raws?: any[]},
1083
+ * scout: {state: string, facts: any, reason?: string|null, cause?: string|null, calls?: any[], raws?: any[],
1084
+ * budgetStop?: 'cap-halt'|'pricing-red'|null},
1014
1085
  * listing?: {stop: string|null, files: string[]|null, block: string|null, meta: any}|null,
1015
1086
  * generate: Function, seedReadFn?: Function, closeCtx?: any,
1087
+ * ceilingUsd?: number|null,
1016
1088
  * maxRevisions?: number, structureRetries?: number,
1017
1089
  * structuredMode?: 'tool'|'text', catalogue?: Record<string, any>}} o
1018
1090
  */
@@ -1021,6 +1093,7 @@ export async function authorClose({
1021
1093
  answers, questions = GREEN_QUESTIONS,
1022
1094
  scout, listing = null,
1023
1095
  generate, seedReadFn = runSeedReadStages, closeCtx = {},
1096
+ ceilingUsd = null,
1024
1097
  maxRevisions = MAX_REVISIONS,
1025
1098
  structureRetries = MAX_STRUCTURE_RETRIES,
1026
1099
  structuredMode = 'tool',
@@ -1039,7 +1112,13 @@ export async function authorClose({
1039
1112
  // ceiling. Floor 0, exactly as above: `askDeclaration` loops `attempt <= retries`,
1040
1113
  // so 0 is still ONE attempt with no retry — a legal tighter ask, not an absent one.
1041
1114
  const retryCap = Math.max(0, Math.min(Math.trunc(Number(structureRetries)) || 0, MAX_STRUCTURE_RETRIES));
1042
- const book = makeCostBook();
1115
+ // THE OPERATOR'S CEILING — NO DEFAULT, and none is invented here. `null` means
1116
+ // nobody set one, which is UNBOUNDED: a defaulted cap is a silent second
1117
+ // ceiling, and the run path already paid for that lesson twice (`maxWallMs` has
1118
+ // no default for exactly this reason, and `shellCapUsd` left off judged against
1119
+ // a number nobody chose). An unbounded authoring run is legal; it is just never
1120
+ // an accident, which is why the driver PRINTS it.
1121
+ const book = makeCostBook({ ceilingUsd });
1043
1122
  // The scout's calls AND its raws, together. The raws matter most on the path
1044
1123
  // that spends nothing more: run mslhn707 refused at the $0 preflight below,
1045
1124
  // and the survey text that would have said WHY died with the process.
@@ -1057,6 +1136,40 @@ export async function authorClose({
1057
1136
  // special facts are needed". This one lands in a SIGNED artefact.
1058
1137
  if (scout?.state !== 'PRESENT' || !scout.facts || typeof scout.facts !== 'object'
1059
1138
  || Object.keys(scout.facts).length === 0) {
1139
+ // A SURVEY THE CEILING STOPPED IS NOT A SURVEY THAT FAILED. The scout carries
1140
+ // its own governance stop, and when it is set that is the honest name for why
1141
+ // the facts are missing — the same rule W-2 settled for the clock (a time-stop
1142
+ // is NAMED a time-stop). `scout-absent` sends the operator to the scout; a
1143
+ // cap-halt sends them to the number they set, which is the one that moves.
1144
+ if (scout?.budgetStop) {
1145
+ // A CONCURRENT CAUSE IS CONCEDED, NEVER DENIED. The ceiling can refuse the
1146
+ // reserved recovery round for a call that ALSO died on transport, and both
1147
+ // are then true at once. "not on anything it read" would deny the very
1148
+ // error the same sentence goes on to quote, and this red carries
1149
+ // `path: budgetUsd` — so the operator is steered to raise a ceiling into a
1150
+ // dead socket, which is the one lever that cannot repair it. The stop
1151
+ // itself does not move: the ceiling did bind, and naming the second cause
1152
+ // is attribution, not routing.
1153
+ //
1154
+ // The transport death is not the only second cause. The ceiling also
1155
+ // refuses the recovery round behind a SHORT/EMPTY reply, and the retry
1156
+ // behind an EMPTY/UNPARSEABLE one — every cause except NOT_FUNDED means a
1157
+ // real call produced the reason being quoted, so "not on anything it
1158
+ // read" is honest ONLY when no call was ever made.
1159
+ const alsoDied = scout?.cause === SURVEY_CAUSES.CALL_FAILED;
1160
+ const alsoIncomplete = !alsoDied && scout?.cause != null && scout.cause !== SURVEY_CAUSES.NOT_FUNDED;
1161
+ return refuse([{
1162
+ code: scout.budgetStop,
1163
+ path: 'budgetUsd',
1164
+ detail: alsoDied
1165
+ ? 'the authoring ceiling bound here AND the survey call itself failed — both are true, and raising the '
1166
+ + `ceiling will not repair the second: ${redactSecrets(String(scout.reason ?? ''))}`
1167
+ : alsoIncomplete
1168
+ ? 'the authoring ceiling bound here AND the survey came back incomplete — both are true: the ceiling '
1169
+ + `refused the round that could have repaired it: ${redactSecrets(String(scout.reason ?? ''))}`
1170
+ : `the survey stopped on the authoring ceiling, not on anything it read: ${redactSecrets(String(scout.reason ?? ''))}`,
1171
+ }], scout.budgetStop);
1172
+ }
1060
1173
  return refuse([{
1061
1174
  code: 'scout-absent',
1062
1175
  path: 'scout',
@@ -1158,12 +1271,55 @@ export async function authorClose({
1158
1271
  /** @type {{stage: string, name: string}[]} */
1159
1272
  let droppedEnv = [];
1160
1273
 
1274
+ /**
1275
+ * THE GOVERNANCE STOP'S OWN RED. It names the CAP and the SPEND, because the
1276
+ * operator's next move is a NUMBER — resume-to-cap is the shipped lever (the
1277
+ * stop IS the checkpoint), and a stop that reports neither side of the
1278
+ * comparison cannot be acted on. It is a governance stop, never an error: the
1279
+ * partial artifacts stay exactly where they are, nothing retries, and nothing
1280
+ * about the close itself is being judged here.
1281
+ * @param {'cap-halt'|'pricing-red'} code
1282
+ * @returns {Red}
1283
+ */
1284
+ const capRed = (code) => {
1285
+ const c = book.report();
1286
+ const spent = `$${c.knownUsd.toFixed(6)} across ${c.calls.length} metered call(s)`;
1287
+ return code === 'pricing-red'
1288
+ ? {
1289
+ code,
1290
+ path: 'budgetUsd',
1291
+ detail: `the authoring ceiling cannot be enforced: ${c.nullCostCalls} unpriced call(s) and ${c.unpricedRounds} `
1292
+ + `unpriced round(s) make the total unknown, against a $${ceilingUsd} ceiling (${spent} is the KNOWN half). `
1293
+ + 'Unpriced is never free (F6) — a cap that cannot see the spend cannot govern it, so the run stops rather '
1294
+ + 'than spending against a floor that reads as exact.',
1295
+ }
1296
+ : {
1297
+ code,
1298
+ path: 'budgetUsd',
1299
+ detail: `the authoring ceiling is spent: ${spent} against a $${ceilingUsd} ceiling. The next call was never `
1300
+ + 'made — the cap binds BETWEEN calls, so nothing was cut off mid-flight and everything paid for is on '
1301
+ + 'disk. The stop IS the checkpoint: raise the ceiling and re-run, or read what is here and stop.',
1302
+ };
1303
+ };
1304
+
1161
1305
  const seedTrees = makeSeedTrees();
1162
1306
  try {
1163
1307
  for (let i = 0; i <= revisionCap; i += 1) {
1164
1308
  const label = i === 0 ? 'author' : `revise-${i}`;
1165
1309
  const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue });
1166
1310
  messages = ask.convo;
1311
+
1312
+ // The cap tripped BEFORE this ask made any call at all, so there is no
1313
+ // iteration to record: an iteration row for a call that never happened is
1314
+ // the blind-instrument class one field wide (`ralph` already pays for the
1315
+ // sibling of this — a governance stop narrated for a loop it never
1316
+ // entered). A ladder cut SHORT is the other case and falls through: those
1317
+ // calls did happen, so the row stays and the stop is read after it.
1318
+ if (ask.budget && ask.attempts === 0) {
1319
+ fatal = [capRed(ask.budget)];
1320
+ stop = ask.budget;
1321
+ break;
1322
+ }
1167
1323
  /** @type {any} */
1168
1324
  const iter = {
1169
1325
  call: label, attempts: ask.attempts, declaration: ask.declaration ?? null,
@@ -1172,6 +1328,17 @@ export async function authorClose({
1172
1328
  };
1173
1329
  iterations.push(iter);
1174
1330
 
1331
+ // A retry ladder cut short by the cap. The money stop OUTRANKS the artifact
1332
+ // red that was in flight: the artifact red says the model malformed, the cap
1333
+ // says we stopped paying to find out — and only the second one is why the
1334
+ // loop ended. Reporting the first would send the operator to the model when
1335
+ // the fact is the wallet.
1336
+ if (ask.budget) {
1337
+ fatal = [capRed(ask.budget)];
1338
+ stop = ask.budget;
1339
+ break;
1340
+ }
1341
+
1175
1342
  if (ask.providerError) {
1176
1343
  // a transport failure is a casualty, never evidence about the model — and
1177
1344
  // the transport's own prose (a proxy URL, an auth echo) is scrubbed on the