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.
- package/CHANGELOG.md +204 -0
- package/README.md +101 -117
- package/bareloop.context.md +139 -12
- package/package.json +1 -1
- package/src/authorflow.js +189 -22
- package/src/authoring.js +190 -0
- package/src/authorjob.js +11 -3
- package/src/authorscout.js +70 -9
- package/src/bridges.js +14 -1
- package/src/index.js +3 -0
- package/src/kinds.js +102 -11
- package/src/ledger.js +5 -1
- package/src/planrun.js +210 -25
- package/src/ralph.js +5 -0
- package/src/reuse.js +52 -2
- package/src/run.js +2 -1
- package/src/text.js +79 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/types/authorflow.d.ts +52 -8
- package/types/authoring.d.ts +112 -0
- package/types/authorjob.d.ts +3 -2
- package/types/authorscout.d.ts +16 -3
- package/types/index.d.ts +1 -1
- package/types/kinds.d.ts +58 -6
- package/types/planrun.d.ts +43 -0
- package/types/reuse.d.ts +1 -1
- package/types/text.d.ts +67 -0
- package/types/validate.d.ts +74 -9
package/bareloop.context.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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: [
|
|
1246
|
-
|
|
1247
|
-
complete" is true of a VERDICT; a
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|