bareloop 0.11.0 → 0.13.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/src/planrun.js CHANGED
@@ -21,7 +21,7 @@ import { Gate } from 'bareguard';
21
21
  import { LiteCtx } from 'litectx';
22
22
  import { runClose, runStages, ralph, CLOSE_FAULTS, boundGap } from './ralph.js';
23
23
  import { validatePlan, legalScopes, closeStagesOf } from './plan.js';
24
- import { WRITE_VERBS, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, MAX_SCOPE_MENU } from './plan.js';
24
+ import { WRITE_VERBS, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, MAX_SCOPE_MENU, RETRIEVAL_PAIR } from './plan.js';
25
25
  import { snapshotScope, evalExits, CHECK_GAP_MAX } from './exits.js';
26
26
  import { createRoot } from './root.js';
27
27
  import { createStallWatch, STALL_MS, MAX_STALLS } from './stall.js';
@@ -29,14 +29,17 @@ import { createLadder, STRIKE_LIMIT } from './ladder.js';
29
29
  import { createTrend, FIX_STRIKE_LIMIT } from './trend.js';
30
30
  import { TOOL_MENU, STORE_VERBS, checkMenu } from './job.js';
31
31
  import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, PERSONA_TOOLS, strategyFor } from './tools.js';
32
+ import { createReadShim, readShimArm, readShimStrategy } from './readshim.js';
32
33
  import { globToPrefix, redactSecrets, SECRET_PATTERNS } from './validate.js';
33
- import { validateBridge, loadGate } from './bridges.js';
34
+ import { validateBridge, loadGate, newestEligibleVersion, reuseEligibility, quarantinesCredit, QUARANTINED_CODE } from './bridges.js';
34
35
  import { extractArtifact } from './text.js';
36
+ import { defaultJudgeLoop } from './judged.js';
35
37
  import { createClock, isWallTimeout } from './clock.js';
36
38
  import { isDeclaredClose, runDeclaredStages, validateCloseDecl, closeGrade, HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED } from './declaredclose.js';
37
39
  import {
38
40
  seedAtHead, seedListing, changedSet, GAP_TRIM_MARKER, GATE_AUDIT_FILE, ARBITER_BOOK_STORES,
39
- HUMAN_KIND, HUMAN_DECISIONS, normalizeHumanRuling,
41
+ HUMAN_KIND, HUMAN_DECISIONS, normalizeHumanRuling, resolveHumanRuling,
42
+ REVIEW_DOOR, doorOpens, mechanicalStages,
40
43
  } from './kinds.js';
41
44
  import { workBranchName, prepareWorkBranch } from './workbranch.js';
42
45
 
@@ -133,6 +136,24 @@ function materialsBlock(m) {
133
136
  return `\nWhat this run has left to spend — plan within it:\n${lines.join('\n')}\nThese are totals for the run, not per-step allowances. Nothing here is a rate: a step's cost depends on what it does.\n`;
134
137
  }
135
138
 
139
+ /**
140
+ * MODULE 8 — the signer's words, when this leg is a RERUN taken at a previous run's
141
+ * review door (PRD v1.71 §3.4: *"redo/rerun comes with new authoring"*).
142
+ *
143
+ * It is stated as a REQUIREMENT beside the goal, not as a replacement for it: the
144
+ * signed goal and the signed close are unchanged, and what the person added is the
145
+ * half a machine could not see. The block also says what the previous run's verdict
146
+ * WAS, because the planner would otherwise draft against a tree that already passes
147
+ * the close with no idea why it is being asked to do more.
148
+ *
149
+ * Empty/absent renders '' — every existing prompt is byte-identical.
150
+ * @param {string|null|undefined} note
151
+ */
152
+ function doorNoteBlock(note) {
153
+ if (typeof note !== 'string' || !note.trim()) return '';
154
+ return `\nA previous run of this job satisfied the close, and the person who owns it REVIEWED the\nresult and asked for another pass. Their words, which are a requirement this plan must\nmeet on top of the goal above (the goal and the close are unchanged):\n${note.trim()}\nThe close will judge the result exactly as it judged the last one — passing it again is\nnecessary and is not sufficient.\n`;
155
+ }
156
+
136
157
  /**
137
158
  * Layer 3 (D4) — the MECHANICAL START. A loaded bridge is handed to the drafter as a
138
159
  * STARTING DRAFT, not as a contract: it is appended to the ordinary drafting prompt and
@@ -276,6 +297,26 @@ export function closeGapBlock(gap) {
276
297
  */
277
298
  export const BOUND_REASON_MAX = 200;
278
299
 
300
+ /**
301
+ * BA-21 — the WRITE side of the pricing-provenance signal (read side: `rateProvenance` /
302
+ * `spendProvenance` in src/ledger.js). bare-agent >=0.37 rides `rateSource` beside
303
+ * `pricing` on every metering payload; both metering callbacks below forward it onto the
304
+ * spine through THIS one helper, because two sites spelling one rule are two instruments
305
+ * (the ripgrep fix that landed in ci.yml but not publish.yml).
306
+ *
307
+ * Forwarded VERBATIM — the write side reports what upstream said, and src/ledger.js is
308
+ * the one place that decides what counts as a guess. And an ABSENT provenance stays
309
+ * absent: a payload carrying no `rateSource` (bare-agent <0.37, and every round already
310
+ * in the archive) emits no field at all, so a reader sees UNKNOWN rather than a label we
311
+ * invented. Never defaulted to `null` here — `null` is upstream saying "nothing was
312
+ * priced", which is a different fact from "nobody told us".
313
+ * @param {any} arg an `onLlmResult` / `onTurn` payload
314
+ * @returns {{rateSource?: string|null}} the field to spread into the spine record
315
+ */
316
+ export function rateSourceFields(arg) {
317
+ return arg && typeof arg === 'object' && 'rateSource' in arg ? { rateSource: arg.rateSource } : {};
318
+ }
319
+
279
320
  /**
280
321
  * What the NEXT attempt is told about the bound that cut the previous one.
281
322
  *
@@ -348,8 +389,18 @@ export function boundedNote(bounded, rounds) {
348
389
  * Rule A-v2's law (final write step only, framed wide). `priorChecks`: the predecessor
349
390
  * plan's carried checks, replan phase only — Rule B's law (keep every one). Empty/omitted
350
391
  * renders byte-identically to the pre-rules prompt.
392
+ * @param {string|null} [doorNote] MODULE 8 — the signer's own words, when a previous run
393
+ * of this spec ended at the review door and they took the RERUN door. Additive and
394
+ * omitted-by-default, so every existing caller renders byte-identically. It is a
395
+ * REQUIREMENT the plan must meet, never a replacement for the goal: the signed goal and
396
+ * the signed close are unchanged, and what the person added is what the machine could
397
+ * not see.
398
+ * @param {boolean|'cap'|'diff'} [readShim] the read shim's ARM. Does it carry G1? Then G1
399
+ * is STATED as well as enforced (the mailbox precedent again: a law only the validator
400
+ * knows costs a redraft per draft). The arms without it — `false` and `'diff'` — render
401
+ * byte-identically to the pre-shim prompt.
351
402
  */
352
- export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes, materials, startingDraft = null, checkFacts = {}) {
403
+ export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes, materials, startingDraft = null, checkFacts = {}, doorNote = null, readShim = false) {
353
404
  const scopeMenu = Array.isArray(scopes) && scopes.length ? scopes : legalScopes(job.writeScope ?? []);
354
405
  const ceiling = Array.isArray(job.tools) ? job.tools : [...TOOL_MENU];
355
406
  // The shape-lottery laws, stated where the check menu is offered (the mailbox
@@ -373,6 +424,16 @@ export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes,
373
424
  // (`?? []`: a close naming no command stages nothing, so it offers nothing)
374
425
  const closeStages = closeStagesOf(job) ?? [];
375
426
  const checkNames = checkMenu(closeStages).map((m) => m.name);
427
+ // G1, stated where the grant is offered. Only under an arm that CARRIES G1 —
428
+ // with the shim off, or under the diff-only arm that caps nothing, the rule
429
+ // does not exist and neither does this sentence (the A0 and A2 renders). The
430
+ // sentence also states the 24KB bound as its reason, which is the second
431
+ // reason it cannot render for A2: that bound is not in force there.
432
+ const readShimLaw = readShimArm(readShim).g1
433
+ ? `\n A step granting "read" must also grant "recall" and "get": a read of a large file
434
+ returns only its next 24KB, and those two verbs are the only way to read a symbol in
435
+ full. A step that grants "read" without both is rejected.`
436
+ : '';
376
437
  const doc = `DRAFT-PLAN
377
438
  You are planning how to accomplish a goal in a repository, as an ordered list of bounded
378
439
  steps (schema "plan-v1"). The plan is pure declarative JSON validated by a strict schema;
@@ -383,7 +444,7 @@ Shape: { "schema": "plan-v1", "steps": [ ... 1..${MAX_PLAN_STEPS} steps ... ] }
383
444
  strictly in array order. Each step (no other fields exist):
384
445
  - "id": kebab-case slug, unique
385
446
  - "action": the step's task, precise enough for a worker that sees ONLY this step
386
- - "tools": non-empty unique subset of ${JSON.stringify(ceiling)} (read/grep are the worker's ONLY way to see the tree — there is no shell; write/edit change the tree; recall/get/impact/related/recent search and navigate the repository index; compress/peek read cheaply; stash/remember/forget park and record notes across steps)
447
+ - "tools": non-empty unique subset of ${JSON.stringify(ceiling)} (read/grep are the worker's ONLY way to see the tree — there is no shell; write/edit change the tree; recall/get/impact/related/recent search and navigate the repository index; compress/peek read cheaply; stash/remember/forget park and record notes across steps)${readShimLaw}
387
448
  - "rounds": integer 1..${maxStepRounds} — the step's per-attempt tool-round bound
388
449
  - "target": the step's deliverable path (REQUIRED when tools include write/edit), inside ${JSON.stringify(job.writeScope)}
389
450
  - "scope" (optional): narrow this step's WRITE fence — copy one value from the offered scopes below.
@@ -429,7 +490,7 @@ ${scopeMenu.map((s) => ` ${JSON.stringify(s)}`).join('\n')}
429
490
 
430
491
  Goal:
431
492
  ${job.goal}
432
- ${materialsBlock(materials)}
493
+ ${doorNoteBlock(doorNote)}${materialsBlock(materials)}
433
494
  Repository survey (from a read-only scout):
434
495
  ${scoutBlob || '(no scout notes)'}`;
435
496
  let p = doc;
@@ -462,6 +523,13 @@ ${scoutBlob || '(no scout notes)'}`;
462
523
  * `false` → the drafter has no tools, so a native session would report NO cost — return a
463
524
  * metered claude-json TEXT provider (`--output-format json`, `parse:'claude-json'`) instead,
464
525
  * so its spend is never invisible. The Loop path (`anthropic-api`) never touches this.
526
+ * @param {any} [opts.judgeProvider] softgreen — the provider a JUDGED close stage runs its
527
+ * locate call through, pinned by the operator to `JUDGE_MODEL` (src/judged.js). Separate
528
+ * from `provider`/`providerFor` on purpose: the judge tier is not a step knob and never
529
+ * agent-selectable, and the worker's binding is not a legal stand-in for it. Absent is not
530
+ * a fall-back — a judged stage with no seam instrument-STOPS as a wiring gap, the same
531
+ * answer a native job with no `nativeProvider` gets. A close with no judged stage never
532
+ * reads it.
465
533
  * @param {(type: string, data?: object) => object} opts.emit spine emitter (the caller's METERED emit)
466
534
  * @param {() => number} opts.remainingUsd the one wallet: what is left of the signed budget right now
467
535
  * @param {() => boolean} [opts.isUnpriced] has any round come back with a null cost? (F6) — the
@@ -485,6 +553,17 @@ ${scoutBlob || '(no scout notes)'}`;
485
553
  * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
486
554
  * reserved — a scout that spends every round on tools gets one toolless round to write its
487
555
  * survey, because the bound halts it mid-tool-use and text is its only deliverable)
556
+ * @param {boolean|'cap'|'diff'} [opts.readShim=false] THE READ SHIM (src/readshim.js) — the
557
+ * ARM to run, one of the Phase 2 pre-registration's four: `false` (A0, off — the default),
558
+ * `'cap'` (A1: cap + pointer + next-unseen-slice + G1), `'diff'` (A2: the diff lever alone,
559
+ * no cap, no pointer, no G1), `true` (A3: every lever). Anything else THROWS at the entry
560
+ * below rather than coercing into a truthy shim — a mis-spelled arm that silently ran A3
561
+ * is a corrupted battery row, invisible afterwards.
562
+ * OFF is byte-identical to the pre-shim run in every observable, G1 included: the frozen
563
+ * A0 baseline arm must be exactly today's behaviour, and a guard firing under a disabled
564
+ * shim would quietly make the baseline a treatment arm. The default-flip waits on a paid
565
+ * contrast that has not been approved (the `layerRoot` precedent, F41 — an unproven lever
566
+ * ships OFF and earns its default).
488
567
  * @param {boolean} [opts.layerRoot=false] Layer R — the within-run ratchet (src/root.js),
489
568
  * scoped PER STEP's ralph loop (each micro-wheel is the Layer-1 atom). Shell-assembled from
490
569
  * the step's own books: per-attempt write-sets from the F32 workerWrites audit (teed for
@@ -601,13 +680,70 @@ ${scoutBlob || '(no scout notes)'}`;
601
680
  * is, before anything is read at all. After the fix loop opens the ruling is gone, so once the mechanical
602
681
  * stages pass again the run PAUSES for a second review rather than converting the same
603
682
  * sentence forever.
683
+ * @param {{decision: string, text?: string|null, receivedAt?: string|null}|null} [opts.heldRuling]
684
+ * F102 — the decision the CHECKPOINT carries (`readResume`'s `restart.pendingDecision`):
685
+ * an answer a person already gave, on a leg that stopped before a single round was
686
+ * bought for it. It is applied exactly as a fresh one is, through the same seam, so
687
+ * the person is never asked the same question twice; the spine records that it came
688
+ * from a record rather than from a person, and `receivedAt` says when they said it.
689
+ *
690
+ * A leg handed BOTH is refused (`hitl-decision-red`, naming the held decision): two
691
+ * answers to one question is ambiguity, and a merge would pick a winner nobody chose.
692
+ * Absent is every ordinary run and every leg the operator answers directly.
693
+ * @param {number} [opts.priorSpentUsd=0] F103 — money the CHAIN spent before this leg,
694
+ * for the `engagement` record's money half ONLY. It bounds nothing here: the wallet
695
+ * is the ledger's one level up, and `remainingUsd` already nets this fold against the
696
+ * signed budget on every path (the signed budget is the chain's ceiling, and a rerun
697
+ * spends what remains of it — never a refill). Reporting, never governance.
698
+ * @param {boolean|null} [opts.reviewDoor] MODULE 8 — does this run END at the review
699
+ * door (PRD v1.71 §3)? `true`/`false` is the operator's explicit choice in either
700
+ * direction; absent is the CLASS default (`doorOpens`): always for soft-green,
701
+ * whose credit is held until a person accepts, and never for green-class unless
702
+ * asked. It changes what the run RECORDS and never what it returns — the door is
703
+ * a disposition, and the loop's verdict is not the door's to touch.
704
+ * @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
705
+ * MODULE 8 — the signer took the RERUN door on a previous run: a FRESH ENGAGEMENT
706
+ * (§3.4) against the same signed spec, carrying their words. Two things follow, and
707
+ * only two: the already-green shortcut is refused (the tree passing the close is
708
+ * precisely the state the person rejected), and the words reach the PLANNER as new
709
+ * authoring. It is never a `humanRuling` — that answers a close's human STAGE, and
710
+ * a green-class job has none. Empty words are refused at the same seam every door is.
604
711
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
605
712
  * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
606
713
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
607
714
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
608
715
  */
609
- export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, maxStepRounds = 40, layerRoot = false, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null }) {
716
+ export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, maxStepRounds = 40, layerRoot = false, readShim = false, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null }) {
610
717
  workdir = resolve(workdir);
718
+ // The read shim's ARM, resolved at the door — before the workdir is read, before
719
+ // the scout, before a single token. An unrecognised spelling throws HERE, where
720
+ // it costs nothing, rather than coercing into a truthy shim and running A3 under
721
+ // an A2 label for the rest of a paid row (the blind-instrument class: the result
722
+ // would be wrong and would look fine). Only the guard runs here; the flag itself
723
+ // is still threaded onward as written.
724
+ readShimArm(readShim);
725
+ // G1's OTHER half, refused at the same $0 door. `validatePlan` reds `read-blind`
726
+ // when a step grants `read` without the retrieval pair — but a step cannot grant
727
+ // what the SIGNED CEILING does not offer, so against a spec whose `tools` lack
728
+ // `recall`/`get` EVERY draft reds identically, and the drafter is paid for each
729
+ // doomed cycle before the run fails. The condition is knowable here, before a
730
+ // token: the arm is the operator's argument and the ceiling is already signed.
731
+ //
732
+ // Same class as the unknown-arm throw above and deliberately NOT a red: a red is
733
+ // a verdict on the AGENT's work, and nothing the agent authored is wrong here —
734
+ // the operator asked for a shim the signature cannot satisfy. Tighten-only, and
735
+ // it narrows nothing that works: the only configuration it rejects is one that
736
+ // already fails 100% of the time, just later and for money.
737
+ //
738
+ // Keyed on `arm.g1`, not on "is the shim on", for the same reason `plan.js` is:
739
+ // the diff-only arm caps nothing, so there is nothing to be blind about.
740
+ if (readShimArm(readShim).g1) {
741
+ const signedCeiling = Array.isArray(job?.tools) ? job.tools : [...TOOL_MENU];
742
+ const short = RETRIEVAL_PAIR.filter((v) => !signedCeiling.includes(v));
743
+ if (short.length) {
744
+ throw new TypeError(`readShim: this arm caps reads, so a step granting "read" must also grant ${RETRIEVAL_PAIR.join(' and ')} — but the signed ceiling [${signedCeiling.join(', ')}] offers no ${short.join('/')}, so no legal plan exists and every draft would red as "read-blind". Re-sign the spec with ${short.join(' and ')}, or run with the shim off.`);
745
+ }
746
+ }
611
747
  // ONE spelling of the redaction, housed next to the inventory it reads
612
748
  // (src/validate.js) — the same helper the isolate verbs scrub the litectx store
613
749
  // with. Two hand-spelled copies of "what a secret looks like" is exactly how
@@ -670,7 +806,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
670
806
  // answered. It is a terminal of its own rather than an `interpreter-red` — no
671
807
  // library failed, the operator's own input is what could not be read.
672
808
  const hasHumanStage = stagedClose.some((/** @type {any} */ s) => s?.kind === HUMAN_KIND);
673
- const ruling = normalizeHumanRuling(humanRuling);
809
+ // …and the DOOR's rerun rides into the same resolver (module 8): it is not a
810
+ // ruling and is never read as one, but "is this leg a fresh engagement" has ONE
811
+ // spelling, and the clock split below reads it. Resolved HERE, before anything is
812
+ // spent, so a door-rerun leg can never be recorded as `cold` or opened onto the
813
+ // wall of the leg the person just rejected (F103).
814
+ const ruling = resolveHumanRuling(humanRuling, heldRuling, doorRerun);
674
815
  const decisionRed = (/** @type {string} */ detail) => {
675
816
  emit('escalation', {
676
817
  category: HITL_DECISION_RED, decisionReady: true,
@@ -689,11 +830,92 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
689
830
  + 'nothing to rule on — the verdict class is a promise about the CLOSE, and only a close that asks a person '
690
831
  + 'can be answered by one');
691
832
  }
833
+ // ── THE RERUN DOOR, one level out (module 8). A previous run ended at the
834
+ // review door, the person said "I don't like it, go again", and this leg IS the
835
+ // fresh engagement (§3.4). It is validated HERE, at the same seam and against the
836
+ // same rulebook the close's own doors use — empty words are refused before a
837
+ // branch is cut, before a scout is bought, before anything costs anything, because
838
+ // the whole of what a rerun gives the run is the words.
839
+ //
840
+ // Deliberately NOT a `humanRuling`: that answers a close's human STAGE and is
841
+ // refused for a close that has none, which is every green-class job. The two
842
+ // decisions look alike on a screen and are different facts.
843
+ /** @type {{text: string, fromRunid: string|null, receivedAt: string|null}|null} */
844
+ let doorWords = null;
845
+ if (doorRerun != null) {
846
+ const dn = normalizeHumanRuling({ decision: 'rerun', text: /** @type {any} */ (doorRerun)?.text ?? null });
847
+ if (!dn.ok) return decisionRed(`the rerun door was taken on a previous run, and ${dn.why}`);
848
+ doorWords = {
849
+ text: /** @type {string} */ (/** @type {any} */ (dn.ruling).text),
850
+ fromRunid: typeof (/** @type {any} */ (doorRerun).fromRunid) === 'string' ? /** @type {any} */ (doorRerun).fromRunid : null,
851
+ receivedAt: typeof (/** @type {any} */ (doorRerun).receivedAt) === 'string' ? /** @type {any} */ (doorRerun).receivedAt : null,
852
+ };
853
+ emit('door-rerun', {
854
+ // the person's words are the gap, so they ride the same scrub every other
855
+ // operator-authored string on this append-only log does
856
+ text: scrub(doorWords.text),
857
+ fromRunid: doorWords.fromRunid,
858
+ ...(doorWords.receivedAt === null ? {} : { receivedAt: doorWords.receivedAt }),
859
+ meaning: 'a FRESH ENGAGEMENT against the same signed spec — the previous run\'s verdict is untouched, and this leg buys its own clock',
860
+ });
861
+ }
862
+
692
863
  /** the ruling still to be spent. Cleared the moment the fix loop opens: the
693
864
  * human's words are what OPENS that loop, and once the worker holds them the
694
865
  * next machine-clean tree is a new question for the person, not the old one. */
695
866
  let liveRuling = ruling.ruling;
696
867
 
868
+ // ── F102: THE DECISION IS WRITTEN DOWN, because a wall is a serialization
869
+ // boundary and a decision that lives only in this process dies with it.
870
+ //
871
+ // Two records, and they answer two DIFFERENT questions:
872
+ // `human-decision` — what this leg was handed, and by whom
873
+ // `human-decision-spent` — what actually BOUGHT it
874
+ //
875
+ // The split is the whole finding. `liveRuling` is cleared when the fix loop
876
+ // OPENS (N4 §1.5, unchanged below) — that is a rule about this leg's close
877
+ // readings. Whether the person must be asked AGAIN is a different question, and
878
+ // F102's incident is exactly where the two answers differ: the rerun opened the
879
+ // fix loop and then wall-halted with `iterationsUsed: 0`, so the loop had
880
+ // "spent" a decision no worker ever saw and the resume re-asked the byte-
881
+ // identical question. So the spend marker fires on the WORK: a round bought for
882
+ // the fix, an accept that greened, a pause honoured. Anything short of that and
883
+ // the words are still owed to the person, and the checkpoint says so.
884
+ //
885
+ // The source travels with it (`operator` | `checkpoint`): "a person answered
886
+ // this leg" and "a record answered this leg" are different facts, and one
887
+ // spelling for both is F101's class in a decision's coat.
888
+ /** the decision this leg still OWES the person — the door, held until work buys
889
+ * it. @type {string|null} */
890
+ let owedRuling = null;
891
+ if (liveRuling !== null) {
892
+ owedRuling = liveRuling.decision;
893
+ emit('human-decision', {
894
+ decision: liveRuling.decision,
895
+ // the human's words are the gap, so they ride the same scrub every other
896
+ // model- or operator-authored string on this spine does (append-only: a log
897
+ // that captures a key captures it forever)
898
+ text: liveRuling.text === null ? null : scrub(liveRuling.text),
899
+ source: ruling.source,
900
+ // WHEN THE PERSON SAID IT, carried across the boundary rather than re-stamped.
901
+ // Omitted on a fresh answer, where this record's own `ts` IS the receipt —
902
+ // a decorative copy of `ts` would be indistinguishable from a carried one.
903
+ ...(ruling.receivedAt === null ? {} : { receivedAt: ruling.receivedAt }),
904
+ meaning: ruling.source === 'checkpoint'
905
+ ? 'held by the checkpoint and applied directly — the person is not asked again (F102)'
906
+ : 'answered on this invocation',
907
+ });
908
+ }
909
+ /** the decision is PAID FOR — by the work it commissioned, never by the loop
910
+ * merely opening. Idempotent: the first payment is the one that settles it.
911
+ * @param {string} reason */
912
+ const spendRuling = (reason) => {
913
+ if (owedRuling === null) return;
914
+ const decision = owedRuling;
915
+ owedRuling = null;
916
+ emit('human-decision-spent', { decision, reason });
917
+ };
918
+
697
919
  // ── PAUSE: the third door, and it costs nothing at all (2026-08-12 §1, doors
698
920
  // re-cut 2026-08-17). Before the branch, the seed, the precheck and the clock,
699
921
  // because a person who answered "not now" has asked for nothing to be done: no
@@ -716,6 +938,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
716
938
  // the leg that asked, and this leg measured nothing, so it states nothing (F6:
717
939
  // absent, never an empty list dressed as a reading).
718
940
  if (liveRuling?.decision === 'pause') {
941
+ // ANSWERED IN FULL, here. A pause commissions nothing, so there is nothing left
942
+ // owing and nothing for a later leg to hold over the person — the same three
943
+ // doors are open the next time somebody looks (doors addendum, v1.73 ruling 5:
944
+ // the pause door is allowance-free in EVERY state, including a wall-exhausted
945
+ // one, which is why this sits above the clock as well as above the wallet).
946
+ spendRuling('paused');
719
947
  emit(HITL_PAUSE, {
720
948
  stage: stagedClose.find((/** @type {any} */ s) => s?.kind === HUMAN_KIND)?.name ?? null,
721
949
  humanDecision: 'pause',
@@ -760,7 +988,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
760
988
  //
761
989
  // The entry is re-validated here even though the caller is contracted to pass a valid
762
990
  // one. Not defensive decoration: the very next thing this code does is reach into
763
- // `versions.at(-1).plan`, and a half-written file that reached a reader anyway must come
991
+ // the newest eligible version's `plan`, and a half-written file that reached a reader anyway must come
764
992
  // back as named reds, never as a TypeError out of the runner (`loadRegistry` already
765
993
  // skips-and-reports for the same reason).
766
994
  let startingDraft = null;
@@ -779,8 +1007,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
779
1007
  return 'recipe-stale';
780
1008
  }
781
1009
  // `versions` is oldest-first and non-empty by the validator's entry bar, so the newest
782
- // green's plan-as-executed is the one that inherits (R1) — never the founding one.
783
- const newest = bv.bridge.versions.at(-1);
1010
+ // RELEASED green's plan-as-executed is the one that inherits (R1) — never the founding
1011
+ // one, and never a HELD one (softgreen module 6: a judged green earns no reuse until a
1012
+ // person accepts it). Selection already skips a wholly-held entry with a stated reason;
1013
+ // this is the same rule at the point of CONSUMPTION, so a second caller handing a
1014
+ // bridge in directly cannot spend credit no signer granted.
1015
+ const newest = newestEligibleVersion(bv.bridge);
1016
+ if (newest === null) {
1017
+ const reds = [{ code: QUARANTINED_CODE, path: 'versions', detail: reuseEligibility(bv.bridge).reason }];
1018
+ emit('bridge-gate', { outcome: QUARANTINED_CODE, name, reds });
1019
+ emit('escalation', {
1020
+ category: 'recipe-stale', decisionReady: true,
1021
+ decision: `The selected workflow${name ? ` "${name}"` : ''} is HELD: its green was rendered by the judged floor and nobody has accepted it yet, so it has earned no reuse. Nothing was spent.`,
1022
+ options: ['accept that run at the review door, then rerun', 'rerun COLD (draft a new plan from scratch)', 'select a different workflow from the registry'],
1023
+ detail: reds.map((r) => `${r.code}:${r.path} — ${r.detail}`).join('\n'),
1024
+ });
1025
+ return 'recipe-stale';
1026
+ }
784
1027
  startingDraft = newest.plan;
785
1028
  emit('bridge-loaded', { name: bv.bridge.name, versions: bv.bridge.versions.length, runid: newest.runid });
786
1029
  }
@@ -809,10 +1052,55 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
809
1052
  // RESUME (module C): `priorWallMs` is time a killed attempt of this same try already
810
1053
  // consumed. It folds into the clock's start, never into `maxWallMs` — the cap stays the
811
1054
  // signed one in the record and in the hash, and only the REMAINDER is left to run.
812
- const clock = createClock({ maxWallMs: job.maxWallMs ?? null, closeStages: stagedClose.length, priorElapsedMs: priorWallMs, ...(now ? { now } : {}) });
1055
+ //
1056
+ // F103 — TWO COUNTERS, AND NEITHER DOES THE OTHER'S JOB. hamr, verbatim:
1057
+ // *"redo/rerun comes with new authoring for money+time and keeps accounting of
1058
+ // this far and this session separate counters"*.
1059
+ //
1060
+ // CHAIN — every leg of this job added up. It is what the halt readout
1061
+ // reports and what a person reads, and it is never lost.
1062
+ // ENGAGEMENT — what BOUNDS this leg. A resume of the same engagement folds
1063
+ // (W-2 exactly as it was: a kill may never buy a second wall). A
1064
+ // RERUN does not, because the person did not decide on the run's
1065
+ // clock: `msx7a3rj` took the door and inherited 87 seconds from a
1066
+ // leg that had already ended, on a worker measured to read nine
1067
+ // rounds before its first write. That engagement was not short, it
1068
+ // was structurally impossible, and it paid real money to find out.
1069
+ //
1070
+ // The cap itself is untouched on both paths — `maxWallMs` is the signed number,
1071
+ // in the record and in the hash, and only a re-sign changes what a rerun can buy.
1072
+ // MONEY IS DELIBERATELY NOT SYMMETRIC and is folded on every path (`remainingUsd`,
1073
+ // one level up): the signed budget is the CHAIN's ceiling, a signature for $5
1074
+ // never silently authorizes $10, and the fresh engagement spends what remains of
1075
+ // it. The asymmetry is the point — time was the axis F103 measured, and a refilled
1076
+ // wallet is the failure F45 measured.
1077
+ const chainWallMs = typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? priorWallMs : 0;
1078
+ const engagementPriorWallMs = ruling.fresh ? 0 : chainWallMs;
1079
+ const clock = createClock({ maxWallMs: job.maxWallMs ?? null, closeStages: stagedClose.length, priorElapsedMs: engagementPriorWallMs, ...(now ? { now } : {}) });
813
1080
  const closeTimeoutForReport = closeTimeoutMs ?? 120_000;
1081
+ emit('engagement', {
1082
+ kind: ruling.fresh ? 'rerun' : (chainWallMs > 0 || resumeSeed !== null ? 'resume' : 'cold'),
1083
+ chainWallMs,
1084
+ engagementPriorWallMs,
1085
+ wallCapMs: job.maxWallMs ?? null,
1086
+ // the money half of the same pair, stated where the time half is so a reader
1087
+ // never has to assemble one leg's accounting from two records. The BOUND lives
1088
+ // one level up (the ledger owns the wallet); these are its two readings.
1089
+ // belted like every other number entering a record: a garbage fold reads 0
1090
+ // rather than putting a NaN where a reader expects money (F6's arithmetic half)
1091
+ chainSpentUsd: typeof priorSpentUsd === 'number' && Number.isFinite(priorSpentUsd) && priorSpentUsd > 0 ? priorSpentUsd : 0,
1092
+ budgetUsd: job.budgetUsd,
1093
+ meaning: ruling.fresh
1094
+ ? 'a rerun is a FRESH ENGAGEMENT: its own clock, the same signed wallet (F103)'
1095
+ : 'the same engagement continuing: the wall folds, as it always has (W-2)',
1096
+ });
814
1097
  emit('wall-clock', {
815
1098
  ...clock.report(closeTimeoutForReport),
1099
+ // the CHAIN view, beside the engagement's own elapsed rather than folded into
1100
+ // it: on a rerun leg `elapsedMs` is this engagement's and this is the job's,
1101
+ // and one number meaning both is exactly how a decision came to inherit 87
1102
+ // seconds.
1103
+ chainWallMs,
816
1104
  meaning: clock.bounded
817
1105
  ? 'a between-round deadline; a close already in flight when it trips runs to completion'
818
1106
  : 'NO time cap was set — time-unbounded by explicit operator choice, never by a default',
@@ -927,6 +1215,26 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
927
1215
  // arbiter-owned fact reaches a stage: through the ctx, never through the
928
1216
  // declaration. A command close has no human stage to hand it to.
929
1217
  humanRuling: liveRuling,
1218
+ // SOFTGREEN — the judged stage's paid seam and its meter, the same way: through
1219
+ // the ctx, never the declaration. The seam is null unless the operator wired a
1220
+ // pinned judge provider (a judged stage then STOPS as a wiring gap rather than
1221
+ // grading on the worker's model). The meter emits a distinct `judge-round`
1222
+ // record carrying the call's own honest cost — the SAME emit the worker rounds
1223
+ // ride, so `runJob`'s ONE ledger accounts a close's spend exactly as it accounts
1224
+ // an attempt's (a budget funds the attempt PLUS its close), and a null cost trips
1225
+ // the same F6 pricing halt rather than being laundered into $0.
1226
+ judgeLoop: judgeProvider ? (/** @type {{system: string}} */ o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system }) : null,
1227
+ onJudgeCost: (/** @type {any} */ c) => emit('judge-round', {
1228
+ stage: c.stage, path: c.path, attempt: c.attempt, label: c.label, model: c.model,
1229
+ costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
1230
+ // BA-21 provenance, through the SAME helper the worker rounds use — this record
1231
+ // is on `spendProvenance`'s read list (src/ledger.js), so a judge-round that
1232
+ // never carried the field would read UNKNOWN forever inside the readout built to
1233
+ // expose exactly this seam. NAMED, not papered over: the judge cost payload
1234
+ // (src/kinds.js `onJudgeCost({...})`) does not carry `rateSource` today, so it
1235
+ // still reads UNKNOWN — correctly — until that payload forwards it too.
1236
+ ...rateSourceFields(c),
1237
+ }),
930
1238
  })
931
1239
  : runStages(stages, scrub, closeOpts));
932
1240
  /** @type {string|undefined} the stage that rendered the LAST close verdict (Layer R's red-set source) */
@@ -984,6 +1292,18 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
984
1292
  ...extra,
985
1293
  });
986
1294
  };
1295
+ /** WHAT THIS RUN MOVED, read once and read the same way for both screens a
1296
+ * person can arrive at (the pause and the review door). An unreadable set comes
1297
+ * back as its own `stop` rather than an empty list, so the caller can say
1298
+ * UNKNOWN instead of "nothing changed" (F6), and the cap is applied here so the
1299
+ * two screens cannot drift apart on how much they show.
1300
+ * @returns {Promise<{cs: any, paths: string[], shown: string[]}>} */
1301
+ const changedEvidence = async () => {
1302
+ const cs = declaredCtx ? await changedSet(workdir, declaredCtx.seedRef) : { stop: 'no seed', paths: [] };
1303
+ const paths = cs.stop === null ? cs.paths : [];
1304
+ return { cs, paths, shown: paths.slice(0, PAUSE_CHANGED_CAP) };
1305
+ };
1306
+
987
1307
  /**
988
1308
  * THE PAUSE (N4 §1.3/§1.4, rulings 1 and 2) — a decision-ready checkpoint, and
989
1309
  * never a bare "approve?". What the person is shown is assembled here rather
@@ -997,9 +1317,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
997
1317
  * @param {any} v the close verdict that paused @returns {Promise<void>}
998
1318
  */
999
1319
  const emitHitlPause = async (v) => {
1000
- const cs = declaredCtx ? await changedSet(workdir, declaredCtx.seedRef) : { stop: 'no seed', paths: [] };
1001
- const paths = cs.stop === null ? cs.paths : [];
1002
- const shown = paths.slice(0, PAUSE_CHANGED_CAP);
1320
+ const { cs, paths, shown } = await changedEvidence();
1003
1321
  emit(HITL_PAUSE, {
1004
1322
  stage: v.stage ?? null,
1005
1323
  ask: v.ask ?? null,
@@ -1019,6 +1337,55 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1019
1337
  });
1020
1338
  };
1021
1339
 
1340
+ /**
1341
+ * THE REVIEW DOOR (softgreen module 8, PRD v1.71 §3) — the pause machinery one
1342
+ * level out: a run that reaches a VERDICT-BEARING terminal offers the person the
1343
+ * same three doors, off the same evidence package, at the end of every run the
1344
+ * door is open for.
1345
+ *
1346
+ * THE LAW, hamr verbatim: *"it's important not to change the loop self verdict."*
1347
+ * This writes a RECORD and returns nothing. The caller's very next statement is
1348
+ * `return 'green'` / `return 'already-green'` — unchanged, unconditional, and
1349
+ * pinned by test. The close mints the verdict; the door records a disposition,
1350
+ * and `src/reviewdoor.js` is where a person's answer is applied, later, off this
1351
+ * record. Nothing here waits, blocks, or spends: a green run that is never
1352
+ * answered is still a green run.
1353
+ *
1354
+ * WHICH RUNS: always the classes whose credit is HELD (soft-green — module 6's
1355
+ * quarantine has no other release), and otherwise only when the operator asks
1356
+ * (`reviewDoor`). `doorOpens` owns that question, in one place.
1357
+ * @param {string} outcome the verdict the close minted @param {any} v that close verdict
1358
+ */
1359
+ const emitReviewDoor = async (outcome, v) => {
1360
+ if (!doorOpens(job, reviewDoor)) return;
1361
+ const cls = job?.verdictType ?? 'green';
1362
+ const held = quarantinesCredit(cls);
1363
+ // the same evidence a pause carries, assembled by the same helper — one
1364
+ // assembly of one run's facts, however the person arrives at it
1365
+ const { cs, paths, shown } = await changedEvidence();
1366
+ emit(REVIEW_DOOR, {
1367
+ outcome,
1368
+ verdictType: cls,
1369
+ // …and whether this run's credit is being HELD pending the answer. Read from
1370
+ // the ONE place that decides it (module 6), never re-spelled here.
1371
+ quarantined: held,
1372
+ stages: v?.stages ?? [],
1373
+ // the stages an `accept` will RE-RUN, named up front: the person is told what
1374
+ // their answer is about to be checked against before they give it
1375
+ mechanical: mechanicalStages(stagedClose).map((/** @type {any} */ s) => s?.name ?? null),
1376
+ changed: {
1377
+ paths: shown,
1378
+ ...(paths.length > shown.length ? { more: paths.length - shown.length } : {}),
1379
+ ...(cs.stop === null ? {} : { unreadable: scrub(String(cs.stop)) }),
1380
+ },
1381
+ decisionReady: true,
1382
+ decision: `The close minted ${outcome} and it STANDS. What happens to the work is yours: accept it, rerun it with your own words as the gap, or pause and come back.`
1383
+ + (held ? ' This green was rendered by the judged floor, so it has earned no reuse until you accept it.' : ''),
1384
+ options: [...HUMAN_DECISIONS],
1385
+ meaning: 'not a verdict — the verdict is already minted and nothing an answer does can change it; this records a disposition',
1386
+ });
1387
+ };
1388
+
1022
1389
  /** the close, as ONE verdict: every stage in order, first red wins */
1023
1390
  const judgeClose = async () => {
1024
1391
  const v = await runCloseStages(stagedClose);
@@ -1191,7 +1558,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1191
1558
  // record, zero tokens; a forbidden-zone verdict escalates before any spend
1192
1559
  const pre = await judgeClose();
1193
1560
  emit('close-precheck', { ...pre });
1194
- if (pre.verdict === 'satisfied') return 'already-green';
1561
+ // …and if a signer's `accept` is what greened the human stage, it is PAID FOR:
1562
+ // the answer was applied to the tree they were shown, and no later leg may hold
1563
+ // it over a tree they have not seen (F102's persistence must never become a
1564
+ // decision that outlives its evidence).
1565
+ if (pre.verdict === 'satisfied') {
1566
+ // ── THE RERUN DOOR REFUSES THIS SHORTCUT (module 8). A tree that already
1567
+ // passes the close is EXACTLY the state the person rejected when they took the
1568
+ // rerun door: returning `already-green` here would answer "go again" with "there
1569
+ // is nothing to do", spend nothing, and hand back the verdict they just declined
1570
+ // to take. So the run proceeds — a plan is drafted with their words in it, the
1571
+ // work happens, and the close judges it again at the end, as it judges everything.
1572
+ if (doorWords === null) { spendRuling('accepted'); await emitReviewDoor('already-green', pre); return 'already-green'; }
1573
+ emit('door-rerun-open', {
1574
+ fromRunid: doorWords.fromRunid,
1575
+ meaning: 'the close is already satisfied and the run continues anyway — the signer asked for different work, not for a re-grade of the same work',
1576
+ });
1577
+ }
1195
1578
  // …and the pause's own arm of the same gate. Reaching a human stage HERE means
1196
1579
  // every mechanical stage already passes on the untouched tree: the machine half
1197
1580
  // of this job is done and the only thing left is a person. Pausing costs $0 and
@@ -1554,11 +1937,47 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1554
1937
  };
1555
1938
  const grantedNames = new Set(granted.map((v) => /** @type {Record<string, string>} */ (TOOL_BY_VERB)[v]));
1556
1939
  const shell = createShellTools().tools.filter((/** @type {{name: string}} */ t) => grantedNames.has(t.name));
1940
+ // THE READ SHIM (arm-gated, default OFF): a per-worker delivery ledger over the
1941
+ // read tool — cap at READ_SHIM_CAP, pointer only once the CURRENT content has been
1942
+ // delivered whole, next-unseen-slice otherwise, diff on a changed re-read. WHICH of
1943
+ // those are live is the arm (`readShimArm`, Phase 2's A0/A1/A2/A3); off is no wrapper at
1944
+ // all. Same seam and the same per-worker lifetime argument as the native cap below:
1945
+ // `createShellTools()` above builds fresh tool objects for THIS worker, so the ledger
1946
+ // dies with the worker, which is the reset the archive replay segmented on.
1947
+ //
1948
+ // COMPOSITION ON NATIVE, decided: a CAPPING arm REPLACES the native wrapper, never layers
1949
+ // over it. Both bound the same seam at the same 24 KB (READ_SHIM_CAP === NATIVE_READ_CAP,
1950
+ // deliberately one number) and both hand back a trusted steer at the same two verbs, so
1951
+ // stacking them would truncate twice and append two notices — and worse, the shim on the
1952
+ // OUTSIDE would ledger the native wrapper's already-truncated text as the whole file and
1953
+ // then answer a re-read with a pointer, which is precisely the lie this module exists to
1954
+ // prevent. The shim is the strictly stronger wrapper (it keeps the CLI display bound AND
1955
+ // continues where the worker left off), so with a CAPPING arm ON it takes the seam alone.
1956
+ // An arm that brings NO cap (A2) is the other case and is handled at the block below —
1957
+ // it must not take the native bound away, because losing a bound is not one of its levers.
1958
+ const shimArm = readShimArm(readShim);
1959
+ // ONE shim per worker, holding ONE ledger, wrapping BOTH seams the cap touches:
1960
+ // the read tool below, and `ctx_get`'s stale-pointer answer further down. They
1961
+ // are one shim because the cap creates the second problem — its strategy line
1962
+ // sends a capped worker to ctx_recall/ctx_get for a whole function, and a stale
1963
+ // pointer there is the cap's own dead end, not litectx's.
1964
+ const shim = createReadShim({ arm: shimArm });
1965
+ const rdTool = shell.find((/** @type {{name: string}} */ t) => t.name === TOOL_BY_VERB.read);
1557
1966
  // F48: on native, bound shell_read below the CLI's tool-result display cap and hand back a
1558
1967
  // TRUSTED truncation notice steering to ctx_get — the CLI's own truncation blinds the worker
1559
1968
  // (spilled + injection-flagged). Fresh tool objects per mkWorker call, so mutating is per-worker.
1560
- if (native) {
1561
- const rd = shell.find((/** @type {{name: string}} */ t) => t.name === TOOL_BY_VERB.read);
1969
+ //
1970
+ // The condition is the shim's CAP, not the shim: an arm that caps nothing (A2, the diff)
1971
+ // takes the native wrapper's cap away from a native run and hands the seam back to the CLI's
1972
+ // silent truncation — a REGRESSION against A0 on that surface, produced by a lever that is
1973
+ // not about capping at all. So the native bound stays whenever the shim brings none, and it
1974
+ // is applied FIRST, INSIDE the shim: the shim's own doctrine is that it ledgers the text the
1975
+ // worker actually received (the tool's own maxBytes bound is already handled this way), so a
1976
+ // diff against a natively-truncated delivery is a diff against bytes the worker really holds.
1977
+ // Outside, it would truncate the shim's diff mid-hunk. Under A1/A3 this block does not run at
1978
+ // all, so the order is unobservable there and the composition decision below is unchanged.
1979
+ if (native && !shimArm.cap) {
1980
+ const rd = rdTool;
1562
1981
  if (rd) {
1563
1982
  const inner = rd.execute;
1564
1983
  rd.execute = async (/** @type {any} */ args) => {
@@ -1571,8 +1990,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1571
1990
  };
1572
1991
  }
1573
1992
  }
1993
+ // …and the shim goes on last, so it is the OUTER wrapper wherever both apply.
1994
+ if (shimArm.on && rdTool) shim.wrapRead(rdTool);
1995
+ // The stale-pointer serve rides ONLY with a capping arm — `serveStale` refuses
1996
+ // on any other arm anyway, and passing the hook regardless would still change
1997
+ // A0's `ctx_get` object, which has to stay the untouched baseline.
1574
1998
  const ctx = [...CTX_TOOLS].some((t) => grantedNames.has(t))
1575
- ? createCtxTools(lc, workdir, emitCtx).filter((t) => grantedNames.has(t.name))
1999
+ ? createCtxTools(lc, workdir, emitCtx, shimArm.cap ? { onStalePointer: shim.serveStale } : {}).filter((t) => grantedNames.has(t.name))
1576
2000
  : [];
1577
2001
  // LC-3 (litectx 0.31.0): cooperative yield — index() is async but its work is sync CPU,
1578
2002
  // and the default pass holds THIS event loop (the fuse's timers, the wall clock, the lag
@@ -1587,8 +2011,21 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1587
2011
  // isolate/retrieval paragraphs named other components' verbs outright. The
1588
2012
  // assembly lives in tools.js beside the prose it composes, and is asserted
1589
2013
  // byte-identical to the full-component paragraphs on a full grant.
1590
- const system = PERSONA_TOOLS + strategyFor(granted)
1591
- + (native && grantedNames.has(TOOL_BY_VERB.read) ? NATIVE_READ_STRATEGY : '');
2014
+ // One bound, one strategy line: with a CAPPING arm on, the shim OWNS the read seam on
2015
+ // both surfaces (it replaced the native wrapper above), so it speaks for the cap and
2016
+ // the native line stands down — two notices describing one bound is how a worker learns
2017
+ // to distrust both. Off, this renders exactly as before.
2018
+ //
2019
+ // The lines are assembled from the SAME two facts the wrappers were: whichever bound is
2020
+ // actually installed speaks for itself, and the arm's own line rides along. A2 on the API
2021
+ // surface therefore gets the diff sentence and NOTHING about a 24KB limit — a persona
2022
+ // describing machinery that is off is a lie to the worker, and a worker rationing reads
2023
+ // against an imaginary cap is a different treatment than the arm names. A2 on native gets
2024
+ // both, because on native both bounds really are installed.
2025
+ const readLines = grantedNames.has(TOOL_BY_VERB.read)
2026
+ ? (native && !shimArm.cap ? NATIVE_READ_STRATEGY : '') + readShimStrategy(shimArm)
2027
+ : '';
2028
+ const system = PERSONA_TOOLS + strategyFor(granted) + readLines;
1592
2029
  /** @param {any} u @returns {{inputTokens: number, outputTokens: number, cacheReadTokens: number, cacheCreationTokens: number}} */
1593
2030
  const usageOf = (u) => ({ inputTokens: u?.inputTokens ?? 0, outputTokens: u?.outputTokens ?? 0, cacheReadTokens: u?.cacheReadTokens ?? 0, cacheCreationTokens: u?.cacheCreationTokens ?? 0 });
1594
2031
 
@@ -1602,7 +2039,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1602
2039
  // WITH tools take this path: a native session with NO tools fires no
1603
2040
  // onTurn and reports no cost (live-verified), so the toolless drafter runs
1604
2041
  // the metered claude-json TEXT path below instead (never unmetered spend).
1605
- /** @param {{costUsd?: number|null, pricing?: string|null, usage?: any, kind?: string}} arg */
2042
+ /** @param {{costUsd?: number|null, pricing?: string|null, rateSource?: string|null, usage?: any, kind?: string}} arg */
1606
2043
  const nativeMetered = async (arg) => {
1607
2044
  const session = (arg?.kind ?? 'turn') === 'session';
1608
2045
  // per-turn events are ATTRIBUTION ONLY (`worker-turn`, never accounted —
@@ -1613,6 +2050,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1613
2050
  emit(session ? 'worker-round' : 'worker-turn', {
1614
2051
  phase, iteration: roundIteration, kind: arg?.kind ?? 'turn',
1615
2052
  costUsd: session ? (arg?.costUsd ?? null) : null, pricing: arg?.pricing ?? null,
2053
+ // BA-21 provenance. The session event forwards whatever upstream reported; a
2054
+ // per-turn event says `null` — the same deliberate statement `costUsd` makes
2055
+ // one line up. A native turn is unpriced BY DESIGN (the CLI prices the SESSION),
2056
+ // so no rate was consulted and there is no guess to have made. Stated here
2057
+ // rather than forwarded because it is OUR fact about this surface, not one the
2058
+ // provider payload carries.
2059
+ ...(session ? rateSourceFields(arg) : { rateSource: null }),
1616
2060
  tokens: (arg?.usage?.inputTokens ?? 0) + (arg?.usage?.outputTokens ?? 0),
1617
2061
  usage: usageOf(arg?.usage),
1618
2062
  });
@@ -1711,16 +2155,25 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1711
2155
  const newLoop = (gen) => {
1712
2156
  /** @type {any} */
1713
2157
  let self = null;
1714
- /** @param {{costUsd?: number|null, pricing?: string|null, usage?: any, kind?: string}} arg */
2158
+ /** @param {{costUsd?: number|null, pricing?: string|null, rateSource?: string|null, usage?: any, kind?: string}} arg */
1715
2159
  const metered = async (arg) => {
1716
2160
  // The meter fires for EVERY generation, live or abandoned. An orphaned round
1717
2161
  // was really billed, and spend that no instrument sees is the one thing this
1718
2162
  // repo never ships (F6/F44 — unknown is reported as unknown, never as zero;
1719
2163
  // stall.js says it plainly: a reissue can pay twice, and the wallet is what
1720
2164
  // caps that).
2165
+ // F102 — the decision is spent BY THE WORK IT COMMISSIONED. This is the
2166
+ // moment a round is really bought for the fix, which is the only moment the
2167
+ // human's words have actually reached a worker. A wall or a cap that lands
2168
+ // before it leaves the words owed, and the checkpoint carries them on.
2169
+ if (phase === 'fix') spendRuling('fix-round');
1721
2170
  emit('worker-round', {
1722
2171
  phase, iteration: roundIteration, kind: arg?.kind ?? 'turn',
1723
2172
  costUsd: arg?.costUsd ?? null, pricing: arg?.pricing ?? null,
2173
+ // BA-21 provenance, forwarded verbatim: `pricing` says WHETHER the round was
2174
+ // priced, `rateSource` says who stood behind the rate. Reporting only — the
2175
+ // pricing-red halt still keys on cost alone (src/run.js).
2176
+ ...rateSourceFields(arg),
1724
2177
  tokens: (arg?.usage?.inputTokens ?? 0) + (arg?.usage?.outputTokens ?? 0),
1725
2178
  usage: usageOf(arg?.usage),
1726
2179
  });
@@ -1970,7 +2423,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1970
2423
  };
1971
2424
  const draftPlan = async (/** @type {any[]|null} */ reds) => {
1972
2425
  drafter.setIteration(reds ? 'redraft' : 'draft');
1973
- const r = await drafter.ask(planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopeMenu, materials, starting, checkFacts), []);
2426
+ const r = await drafter.ask(planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopeMenu, materials, starting, checkFacts, doorWords?.text ?? null, readShim), []);
1974
2427
  return extractArtifact(r.text).code ?? '';
1975
2428
  };
1976
2429
  // Scrubbed HERE, once, before any consumer reads them — the judge() precedent
@@ -1983,7 +2436,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1983
2436
  // emit sites: scrubbing here makes the leak inexpressible, scrubbing each
1984
2437
  // consumer makes it something the next consumer has to remember.
1985
2438
  const validate = (/** @type {string} */ t) => {
1986
- const v = validatePlan(t, { job, maxStepRounds, scopes: scopeMenu, ...checkFacts });
2439
+ const v = validatePlan(t, { job, maxStepRounds, scopes: scopeMenu, readShim, ...checkFacts });
1987
2440
  return { ...v, reds: v.reds.map((r) => (typeof r.detail === 'string' ? { ...r, detail: scrub(r.detail) } : r)) };
1988
2441
  };
1989
2442
  let text = await draftPlan(null);
@@ -2014,7 +2467,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
2014
2467
  // burn the very work resume exists to keep (hamr: "why would i want to waste
2015
2468
  // more money on something i already started"). Omission = rules inactive, by
2016
2469
  // the validator's contract.
2017
- const pv = validatePlan(resumeSeed.plan, { job, maxStepRounds, scopes: scopeMenu });
2470
+ // `readShim` IS passed, unlike the shape-lottery opts above: G1 is not a drafting
2471
+ // law about plan quality but a precondition of the machinery that is about to run —
2472
+ // a capped worker with no retrieval verb is blind, whether its plan is fresh or
2473
+ // reloaded. A plan drafted under the shim already passed it, so the only way this
2474
+ // refuses paid work is an operator flipping the flag between legs, which is a spec
2475
+ // change deserving the refusal. Off, it is inert, and this line renders as before.
2476
+ const pv = validatePlan(resumeSeed.plan, { job, maxStepRounds, scopes: scopeMenu, readShim });
2018
2477
  // scrubbed at the boundary for `validate()`'s own reason: a `parse-error` detail
2019
2478
  // quotes a window of the SOURCE, and the spine is append-only forever
2020
2479
  const reds = pv.reds.map((r) => (typeof r.detail === 'string' ? { ...r, detail: scrub(r.detail) } : r));
@@ -2718,7 +3177,9 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
2718
3177
  const post = await judgeClose();
2719
3178
  emit('outer-close', { ...post });
2720
3179
  if (post.verdict === 'satisfied') {
3180
+ spendRuling('accepted');
2721
3181
  planExecuted();
3182
+ await emitReviewDoor('green', post);
2722
3183
  return 'green';
2723
3184
  }
2724
3185
  // ── THE THIRD BRANCH (N4 §1.3): between `satisfied` and the fix loop, because
@@ -3023,5 +3484,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
3023
3484
  // an exact-looking total for a call that never billed back.
3024
3485
  return lastEscalation.category;
3025
3486
  }
3487
+ // …and the fix loop's own green reaches the SAME door. A green the loop had to
3488
+ // work for is exactly as much a green as one the close minted first time, and a
3489
+ // door that opened on only one of them would be a door that quietly graded how
3490
+ // the verdict was arrived at.
3491
+ if (fixOutcome === 'green') await emitReviewDoor('green', lastCloseVerdict);
3026
3492
  return fixOutcome === 'green' ? 'green' : 'escalated';
3027
3493
  }