bareloop 0.12.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/tools.js CHANGED
@@ -21,7 +21,7 @@ const require = createRequire(import.meta.url);
21
21
  const { createShellTools } = require('bare-agent/tools');
22
22
  // litectx's rank-tiered render — a free function, not a ctx method (its contract's
23
23
  // own recipe: the caller slices the chunk body, compress renders the signature tier)
24
- const { compress: compressNode } = require('litectx');
24
+ const { compress: compressNode, StalePointerError } = require('litectx');
25
25
 
26
26
  import { extractArtifact, priceOf } from './text.js';
27
27
  // The isolate verbs write MODEL-AUTHORED text into `<workdir>/.litectx` — a file
@@ -243,8 +243,13 @@ const LINE_SPACE = 'Line numbers here are 0-BASED index positions from the repos
243
243
  * RESULT is invisible cannot be judged: a `ctx_get` that silently reds (stale pointer,
244
244
  * bad range) looks exactly like one that worked, and the worker's fallback to a
245
245
  * whole-file read looks like a free choice instead of a forced one.
246
+ * @param {{onStalePointer?: ((req: {file: string, path: string, startLine: number, endLine: number, detail: string}) => {text: string, outcome: string}|null)|null}} [opts]
247
+ * `onStalePointer`: the read shim's stale-pointer serve, wired ONLY by a capping
248
+ * arm (see `createReadShim`). Absent — every caller today except that one — this
249
+ * whole seam is byte-identical to what it was, which is what keeps A0 the
250
+ * untouched baseline an experiment has already run against.
246
251
  */
247
- export function createCtxTools(lc, workdir, emit) {
252
+ export function createCtxTools(lc, workdir, emit, { onStalePointer = null } = {}) {
248
253
  // ctx_get's failure contract, generalised to the palette (review 2026-07-31).
249
254
  // `emit` exists in this file because a verb whose RESULT is invisible cannot be
250
255
  // judged — and a THROWN call is the most invisible result there is: it looks
@@ -337,6 +342,33 @@ export function createCtxTools(lc, workdir, emit) {
337
342
  // StalePointerError: the file changed after indexing, so these lines now describe
338
343
  // DIFFERENT code. Its message IS the recovery instruction — hand it to the worker.
339
344
  const detail = String(/** @type {Error} */ (e)?.message || e);
345
+ // …and under a CAPPING read-shim arm, the recovery instruction is not
346
+ // enough. That arm's own strategy line steers the worker HERE for a whole
347
+ // function, and a worker that just edited the file it is working on — the
348
+ // normal case — gets a stale pointer and zero bytes for a paid round
349
+ // (measured: 5 of 10 ctx_get calls across the A1 arm's three green runs).
350
+ // So the shim serves the requested LINE RANGE from disk, labelled; see
351
+ // `serveStale`. The branch is on the TYPED error, never on the message
352
+ // prose — a prose sniff would fire on any failure whose wording happens
353
+ // to match, and litectx exports the class precisely so it need not.
354
+ if (onStalePointer && e instanceof StalePointerError) {
355
+ // The gate judged `resolve(workdir, expandHome(path))` for this call
356
+ // (toolAction, 'ctx_get'); the serve reads that same absolute path and
357
+ // no other, so it can never reach a byte the fence did not allow.
358
+ const served = onStalePointer({
359
+ file: resolve(workdir, expandHome(String(p))),
360
+ path: rel,
361
+ startLine: Number(startLine),
362
+ endLine: Number(endLine),
363
+ detail,
364
+ });
365
+ if (served && typeof served.text === 'string') {
366
+ // No `detail` on the event: the served TEXT is file content and the
367
+ // spine is append-only. Bytes and the outcome are what a reader needs.
368
+ emit('ctx-tool', { tool: 'ctx_get', path: rel, startLine, endLine, outcome: served.outcome, bytes: Buffer.byteLength(served.text) });
369
+ return served.text;
370
+ }
371
+ }
340
372
  emit('ctx-tool', { tool: 'ctx_get', path: rel, startLine, endLine, outcome: 'stale', bytes: 0, detail });
341
373
  return `stale pointer: ${detail}`;
342
374
  }
package/types/index.d.ts CHANGED
@@ -9,8 +9,9 @@ export { runJob, ACCOUNTED_ROUND_TYPES } from "./run.js";
9
9
  export { BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate, mintBridge, appendGreen, appendRed, loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists, QUARANTINED_VERDICTS, QUARANTINED_CODE, quarantinesCredit, newestEligibleVersion, reuseEligibility, recordDoor } from "./bridges.js";
10
10
  export { renderListing, selectionPrompt } from "./selection.js";
11
11
  export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate, applyDoorDecision, writeRunGreenRow } from "./reuse.js";
12
- export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from "./ledger.js";
12
+ export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES, VOUCHED_RATE_SOURCES, rateProvenance, spendProvenance } from "./ledger.js";
13
13
  export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE } from "./workbranch.js";
14
+ export { createReadShim, wrapReadTool, readShimArm, readShimStrategy, READ_SHIM_CAP, READ_SHIM_STRATEGY, READ_SHIM_DIFF_STRATEGY, READ_SHIM_ARMS } from "./readshim.js";
14
15
  export { runDeclaredClose, runStage, seedRead, seedAtHead, seedListing, changedSet, makeSeedTrees, LIVE_KINDS, STOP_FAULTS, JUDGED_MARKER, EXIT_GREEN, EXIT_RED, EXIT_STOP, HUMAN_DECISIONS, SEED_EXEMPT_KINDS, JUDGED_FLOOR_KIND, normalizeHumanRuling, resolveHumanRuling, JUDGE_ATTEMPTS, MAX_JUDGED_PATHS, REVIEW_DOOR, DOOR_OPEN_OUTCOMES, REVIEW_DOOR_CLASSES, doorOpens, mechanicalStages } from "./kinds.js";
15
16
  export { answerReviewDoor, doorRecordOf, doorAgeGate } from "./reviewdoor.js";
16
17
  export { JUDGE_MODEL, JUDGE_MAX_TOKENS, JUDGE_RULES, JUDGE_RULE_IDS, LOCATE_AXES, LOCATE_LABEL, validateCard, validateFacts, locatePrompt, runLocate, decide, defaultJudgeLoop, CALIBRATION_SIZE, CASE_VERDICTS, validateCalibrationSet, validateJudgedArtifacts, expectedOf } from "./judged.js";
package/types/ledger.d.ts CHANGED
@@ -48,8 +48,66 @@ export function updateLedger({ ledgerFile, spineFiles }: {
48
48
  appended: object[];
49
49
  fold: Record<string, FoldEntry>;
50
50
  };
51
+ /**
52
+ * Read one spend record's rate PROVENANCE. Four-way, and the two absent-ish cases are
53
+ * deliberately distinct facts:
54
+ * - `'vouched'` — a `VOUCHED_RATE_SOURCES` value: somebody stands behind the rate.
55
+ * - `'guessed'` — a rate was used and nobody vouched for it (`'tier'`, `'default'`, or
56
+ * any future value we do not recognise).
57
+ * - `'unpriced'` — `rateSource: null`: nothing was priced, so there was no rate to have
58
+ * guessed. Upstream said so; we are not inferring it.
59
+ * - `'unknown'` — the record carries no `rateSource` at all. Every round archived before
60
+ * this signal existed lands here and is NEVER backfilled: an absent label
61
+ * is reported as unknown, never reconstructed, and never rounded up to
62
+ * vouched (F6's unknown-is-never-zero, applied to provenance).
63
+ * @param {any} record a `worker-round` / `judge-round` / `worker-turn` spine record
64
+ * @returns {'vouched'|'guessed'|'unpriced'|'unknown'}
65
+ */
66
+ export function rateProvenance(record: any): "vouched" | "guessed" | "unpriced" | "unknown";
67
+ /** @typedef {{rounds: number, usd: number, unpricedRounds: number}} ProvenanceBucket */
68
+ /**
69
+ * How much of a run's spend was priced by a guess. Pure fold over one spine's events,
70
+ * REPORTING ONLY — it mints nothing, halts nothing and refuses nothing.
71
+ *
72
+ * `usd` sums only FINITE costs; a round whose cost is not a finite number is counted in
73
+ * `unpricedRounds` instead of being summed as $0 (F6 — unpriced is never free), so a
74
+ * bucket's `usd` is a FLOOR whenever its `unpricedRounds` is non-zero.
75
+ * @param {any[]} events one spine's parsed events
76
+ * @returns {{vouched: ProvenanceBucket, guessed: ProvenanceBucket, unpriced: ProvenanceBucket, unknown: ProvenanceBucket}}
77
+ */
78
+ export function spendProvenance(events: any[]): {
79
+ vouched: ProvenanceBucket;
80
+ guessed: ProvenanceBucket;
81
+ unpriced: ProvenanceBucket;
82
+ unknown: ProvenanceBucket;
83
+ };
51
84
  /** severity order, worst-first (the fold's display order; frequency ranks within) */
52
85
  export const LEDGER_CLASSES: readonly string[];
86
+ /**
87
+ * The two rate sources somebody VOUCHED for: `'provider'` (the provider reported its own
88
+ * authoritative cost, e.g. the claude CLI's `total_cost_usd`) and `'caller'` (we passed
89
+ * the rate in ourselves). Everything else bare-agent can report is a built-in guess.
90
+ *
91
+ * An ALLOW-LIST of vouched values, never a deny-list of guessed ones — this is the
92
+ * load-bearing line of the whole signal, for two reasons.
93
+ *
94
+ * First: BOTH of bareloop's production models resolve to `'tier'`, not to `'default'`
95
+ * (`claude-sonnet-5` and `claude-haiku-4-5-…` each match a recognized Claude tier by
96
+ * model-id substring, so bare-agent prices them off a hardcoded tier rate that could
97
+ * have drifted from Anthropic's). A "is this a guess?" test spelled
98
+ * `rateSource === 'default'` would therefore read EVERY normal bareloop round as
99
+ * confidently priced and see none of the population it exists to measure — this repo's
100
+ * blind-instrument class, seven recorded instances, and not becoming the eighth.
101
+ *
102
+ * Second: written this way, any value bare-agent adds later that we do not recognise
103
+ * reads as a GUESS rather than as vouched. The fail-safe direction, by construction.
104
+ */
105
+ export const VOUCHED_RATE_SOURCES: readonly string[];
106
+ export type ProvenanceBucket = {
107
+ rounds: number;
108
+ usd: number;
109
+ unpricedRounds: number;
110
+ };
53
111
  export type Sample = {
54
112
  spine: string;
55
113
  seq: number;
package/types/plan.d.ts CHANGED
@@ -72,7 +72,7 @@ export function closeStagesOf(job: any): any[] | null;
72
72
  * job spec. Never throws on JSON text or plain parsed data; every failure is
73
73
  * a named red. Returns the parsed plan on ok (single parse), null on any red.
74
74
  * @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
75
- * @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[] }} [opts] `job`: the
75
+ * @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[], readShim?: boolean|'cap'|'diff' }} [opts] `job`: the
76
76
  * validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
77
77
  * menu all come from it — a missing or non-plan-shape job fails CLOSED);
78
78
  * `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
@@ -94,14 +94,23 @@ export function closeStagesOf(job: any): any[] | null;
94
94
  * only). Rule B — a redraft may not drop one (`check-shed`): exits without
95
95
  * the check verify FORM alone, so a shed lets the run "green" unearned
96
96
  * (u-msdsmkid mechanism b; the 3 archived sheds were all on step-red runs).
97
+ * `readShim` (G1): the read shim's ARM for this run — `false` (default), `'cap'`,
98
+ * `'diff'` or `true`. The arms that CAP (`'cap'`, `true`) add one rule: a step
99
+ * granting `read` must also grant `recall` and `get` (`read-blind`), because a
100
+ * capped read is only navigable with them. The arms that do not cap (`false`,
101
+ * `'diff'`) render byte-identically to the pre-shim validator, so the frozen A0
102
+ * baseline stays a baseline and A2 isolates the diff instead of importing an
103
+ * admission rule with it. An unrecognised value THROWS (`readShimArm`) — a param
104
+ * guard on the caller's own argument, never a plan red.
97
105
  * @returns {{ ok: boolean, reds: Red[], plan: object|null }}
98
106
  */
99
- export function validatePlan(input: object | string, { job, maxStepRounds, scopes, seedRed, priorChecks }?: {
107
+ export function validatePlan(input: object | string, { job, maxStepRounds, scopes, seedRed, priorChecks, readShim }?: {
100
108
  job?: any;
101
109
  maxStepRounds?: number;
102
110
  scopes?: string[];
103
111
  seedRed?: string[];
104
112
  priorChecks?: string[];
113
+ readShim?: boolean | "cap" | "diff";
105
114
  }): {
106
115
  ok: boolean;
107
116
  reds: Red[];
@@ -122,6 +131,14 @@ export const MAX_PLAN_STEPS: 8;
122
131
  * count. Shallow entries survive the cap (see `legalScopes`) because they are
123
132
  * the useful ones — a deep leaf is reachable via its parent. */
124
133
  export const MAX_SCOPE_MENU: 24;
134
+ /** G1's pair (read shim only): the two verbs that can reach INTO a file the cap
135
+ * cut short — `recall` names where a symbol lives, `get` reads that symbol in
136
+ * full. Named here because the rule that needs them lives here; the cap that
137
+ * makes them mandatory lives in src/readshim.js.
138
+ * EXPORTED because `runPlan` refuses the same pair at $0 (see the G1 ceiling
139
+ * guard there): the rule and its pre-flight must read ONE spelling of which
140
+ * verbs count, never two that can drift apart. */
141
+ export const RETRIEVAL_PAIR: readonly string[];
125
142
  /** P (design record 2026-07-28): the signed per-step model-tier menu. Closed by
126
143
  * construction — the agent picks a tier, never names an arbitrary model string
127
144
  * (the tier→model mapping and any effort params are the RUNNER's, per provider).
@@ -78,6 +78,25 @@
78
78
  * @returns {string} the labelled block, or '' when there is nothing to show
79
79
  */
80
80
  export function closeGapBlock(gap: string | null | undefined): string;
81
+ /**
82
+ * BA-21 — the WRITE side of the pricing-provenance signal (read side: `rateProvenance` /
83
+ * `spendProvenance` in src/ledger.js). bare-agent >=0.37 rides `rateSource` beside
84
+ * `pricing` on every metering payload; both metering callbacks below forward it onto the
85
+ * spine through THIS one helper, because two sites spelling one rule are two instruments
86
+ * (the ripgrep fix that landed in ci.yml but not publish.yml).
87
+ *
88
+ * Forwarded VERBATIM — the write side reports what upstream said, and src/ledger.js is
89
+ * the one place that decides what counts as a guess. And an ABSENT provenance stays
90
+ * absent: a payload carrying no `rateSource` (bare-agent <0.37, and every round already
91
+ * in the archive) emits no field at all, so a reader sees UNKNOWN rather than a label we
92
+ * invented. Never defaulted to `null` here — `null` is upstream saying "nothing was
93
+ * priced", which is a different fact from "nobody told us".
94
+ * @param {any} arg an `onLlmResult` / `onTurn` payload
95
+ * @returns {{rateSource?: string|null}} the field to spread into the spine record
96
+ */
97
+ export function rateSourceFields(arg: any): {
98
+ rateSource?: string | null;
99
+ };
81
100
  /**
82
101
  * What the NEXT attempt is told about the bound that cut the previous one.
83
102
  *
@@ -146,6 +165,10 @@ export function boundedNote(bounded: {
146
165
  * REQUIREMENT the plan must meet, never a replacement for the goal: the signed goal and
147
166
  * the signed close are unchanged, and what the person added is what the machine could
148
167
  * not see.
168
+ * @param {boolean|'cap'|'diff'} [readShim] the read shim's ARM. Does it carry G1? Then G1
169
+ * is STATED as well as enforced (the mailbox precedent again: a law only the validator
170
+ * knows costs a redraft per draft). The arms without it — `false` and `'diff'` — render
171
+ * byte-identically to the pre-shim prompt.
149
172
  */
150
173
  export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxStepRounds: number, failure: string | null, scopes: string[] | undefined, materials?: {
151
174
  balanceUsd?: number | null;
@@ -154,7 +177,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
154
177
  }, startingDraft?: object | null, checkFacts?: {
155
178
  seedRed?: string[];
156
179
  priorChecks?: string[];
157
- }, doorNote?: string | null): string;
180
+ }, doorNote?: string | null, readShim?: boolean | "cap" | "diff"): string;
158
181
  /**
159
182
  * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close).
160
183
  * Called by runJob after the approval gate, validation, and the smoke — this
@@ -203,6 +226,17 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
203
226
  * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
204
227
  * reserved — a scout that spends every round on tools gets one toolless round to write its
205
228
  * survey, because the bound halts it mid-tool-use and text is its only deliverable)
229
+ * @param {boolean|'cap'|'diff'} [opts.readShim=false] THE READ SHIM (src/readshim.js) — the
230
+ * ARM to run, one of the Phase 2 pre-registration's four: `false` (A0, off — the default),
231
+ * `'cap'` (A1: cap + pointer + next-unseen-slice + G1), `'diff'` (A2: the diff lever alone,
232
+ * no cap, no pointer, no G1), `true` (A3: every lever). Anything else THROWS at the entry
233
+ * below rather than coercing into a truthy shim — a mis-spelled arm that silently ran A3
234
+ * is a corrupted battery row, invisible afterwards.
235
+ * OFF is byte-identical to the pre-shim run in every observable, G1 included: the frozen
236
+ * A0 baseline arm must be exactly today's behaviour, and a guard firing under a disabled
237
+ * shim would quietly make the baseline a treatment arm. The default-flip waits on a paid
238
+ * contrast that has not been approved (the `layerRoot` precedent, F41 — an unproven lever
239
+ * ships OFF and earns its default).
206
240
  * @param {boolean} [opts.layerRoot=false] Layer R — the within-run ratchet (src/root.js),
207
241
  * scoped PER STEP's ralph loop (each micro-wheel is the Layer-1 atom). Shell-assembled from
208
242
  * the step's own books: per-attempt write-sets from the F32 workerWrites audit (teed for
@@ -352,7 +386,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
352
386
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
353
387
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
354
388
  */
355
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun }: {
389
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, readShim, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun }: {
356
390
  workdir: string;
357
391
  provider: any;
358
392
  nativeProvider?: ((o: {
@@ -371,6 +405,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
371
405
  closeTimeoutMs?: number | undefined;
372
406
  maxStepRounds?: number | undefined;
373
407
  scoutRounds?: number | undefined;
408
+ readShim?: boolean | "cap" | "diff" | undefined;
374
409
  layerRoot?: boolean | undefined;
375
410
  bridge?: unknown;
376
411
  priorWallMs?: number | undefined;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Resolve the `readShim` flag into its arm. THROWS on anything else — the BA-4
3
+ * param-guard class, and deliberately not a red.
4
+ *
5
+ * An unrecognised value is OPERATOR input, arriving before a single token is
6
+ * spent, and there is exactly one safe way to fail on it: loudly, at once. The
7
+ * failure this guard exists to prevent is `readShim: 'diff '` (or 'Diff', or
8
+ * 'cap+diff') coerced by truthiness into A3 — a battery row that RAN the wrong
9
+ * arm, recorded the arm it was asked for, and is invisible in the results
10
+ * afterwards. That is the blind-instrument class this programme keeps paying
11
+ * for, and it would corrupt the contrast rather than merely fail a run.
12
+ *
13
+ * A red was the alternative and is wrong here twice over: a red is a verdict on
14
+ * the AGENT's work (the plan, the artifact), and this is the operator's own
15
+ * argument; and a red would need a terminal outcome name, which is arbiter
16
+ * territory and out of this change's scope.
17
+ * @param {boolean|'cap'|'diff'|undefined} [flag]
18
+ * @returns {ReadShimArm}
19
+ */
20
+ export function readShimArm(flag?: boolean | "cap" | "diff" | undefined): ReadShimArm;
21
+ /**
22
+ * The persona line this arm is entitled to — empty when the arm changes nothing
23
+ * the worker can perceive. One arm, one line: the two are never concatenated,
24
+ * because a cap arm has no diff and a diff arm has no cap.
25
+ * @param {ReadShimArm} arm
26
+ * @returns {string}
27
+ */
28
+ export function readShimStrategy(arm: ReadShimArm): string;
29
+ /** @typedef {{file: string, path: string, startLine: number, endLine: number, detail: string}} StaleReq */
30
+ /** @typedef {{text: string, outcome: 'stale-served'|'stale-absent'|'stale-past-eof'}} StaleServe */
31
+ /**
32
+ * Create the shim: one delivery ledger, and the two seams that speak for it.
33
+ *
34
+ * `wrapRead` is the read tool's wrapper (the ledger's only writer). `serveStale`
35
+ * is the answer to a stale `ctx_get` pointer, and it deliberately holds NO
36
+ * reference to the ledger — see its own note. Both live in one object because
37
+ * they are one worker's shim: the read seam and the retrieval seam the cap
38
+ * steers a capped worker towards.
39
+ * @param {{cap?: number, arm?: ReadShimArm}} [opts] `cap`: delivery bound in bytes (the
40
+ * shipped value is `READ_SHIM_CAP`; the option exists so the native path can pin the
41
+ * two bounds to one number rather than two constants drifting apart). `arm`: which
42
+ * levers are live (`readShimArm`). Defaults to ALL of them, so every caller written
43
+ * before the arms existed keeps the behaviour it was written against.
44
+ */
45
+ export function createReadShim({ cap, arm }?: {
46
+ cap?: number;
47
+ arm?: ReadShimArm;
48
+ }): {
49
+ arm: ReadShimArm;
50
+ cap: number;
51
+ wrapRead: (tool: {
52
+ name: string;
53
+ execute: (args: any) => Promise<any>;
54
+ }) => {
55
+ name: string;
56
+ execute: (args: any) => Promise<any>;
57
+ };
58
+ serveStale: ({ file, path, startLine, endLine, detail }: StaleReq) => StaleServe | null;
59
+ };
60
+ /**
61
+ * The one-tool spelling, kept for every caller written before the shim became an
62
+ * object: wrap this read tool with its own private ledger and hand it back.
63
+ * @param {{name: string, execute: (args: any) => Promise<any>}} tool
64
+ * @param {{cap?: number, arm?: ReadShimArm}} [opts]
65
+ * @returns {{name: string, execute: (args: any) => Promise<any>}} the same tool
66
+ */
67
+ export function wrapReadTool(tool: {
68
+ name: string;
69
+ execute: (args: any) => Promise<any>;
70
+ }, opts?: {
71
+ cap?: number;
72
+ arm?: ReadShimArm;
73
+ }): {
74
+ name: string;
75
+ execute: (args: any) => Promise<any>;
76
+ };
77
+ /** The delivery cap, in bytes. Equal to `NATIVE_READ_CAP` by construction — the
78
+ * native path's CLI display bound and this cap bound the SAME seam, so one
79
+ * number keeps them from double-truncating each other (see planrun's wiring). */
80
+ export const READ_SHIM_CAP: number;
81
+ /** The legal spellings, in one place so an error message cannot drift from the
82
+ * set it describes. */
83
+ export const READ_SHIM_ARMS: readonly string[];
84
+ /** F19 at the bound rather than the verb (the NATIVE_READ_STRATEGY precedent):
85
+ * a cap the worker does not know about is a worker that reads the same file
86
+ * three times wondering why it keeps starting over. It states the bound, what a
87
+ * re-read does, and the two verbs that read a symbol whole — the capability the
88
+ * cap makes mandatory (G1). */
89
+ export const READ_SHIM_STRATEGY: "\nREAD LIMIT: a shell_read of a file over 24KB returns only the next 24KB of it, followed by a notice saying which bytes you got. This is NOT the whole file. Reading the same path again continues where the last read stopped, and once you hold all of it a re-read says so instead of re-sending the bytes. To read a function IN FULL, use ctx_recall(<symbol>) then ctx_get(<pointer>) \u2014 that returns the entire function however large its file is. To locate a line, use shell_grep(<pattern>). Never try to understand a large file by reading it whole.";
90
+ /** The DIFF arm's own line. It says nothing about a 24KB limit and nothing about
91
+ * continuation reads, because under A2 neither exists — a persona describing
92
+ * machinery that is off is a lie to the worker, and a worker rationing its reads
93
+ * against an imaginary cap is a different treatment than the arm names.
94
+ *
95
+ * It does not repeat the steer at ctx_recall/ctx_get either: that steer is the
96
+ * CAP's compensation (G1's other half), and offering it here would import half
97
+ * of A1 into A2. The delivery itself carries `diffNotice`'s framing inline; this
98
+ * line exists so the first diff is not a surprise. */
99
+ export const READ_SHIM_DIFF_STRATEGY: "\nRE-READS: if you shell_read a file you have already been shown in full and it has changed since, the result is a unified DIFF against the copy you already hold \u2014 not the file itself. It is labelled as one. Lines starting '-' were removed, '+' were added, ' ' are unchanged context; apply it to the copy you have and you hold the current file.";
100
+ export type StaleReq = {
101
+ file: string;
102
+ path: string;
103
+ startLine: number;
104
+ endLine: number;
105
+ detail: string;
106
+ };
107
+ export type StaleServe = {
108
+ text: string;
109
+ outcome: "stale-served" | "stale-absent" | "stale-past-eof";
110
+ };
111
+ export type ReadShimArm = {
112
+ on: boolean;
113
+ cap: boolean;
114
+ pointer: boolean;
115
+ diff: boolean;
116
+ g1: boolean;
117
+ };
package/types/reuse.d.ts CHANGED
@@ -364,6 +364,7 @@ export function resumeTreeGate({ head, seed, dirty }: {
364
364
  * @param {number} [opts.strikeLimit] forwarded (a plan step's strike ceiling)
365
365
  * @param {number} [opts.closeTimeoutMs] forwarded
366
366
  * @param {boolean} [opts.layerRoot] forwarded
367
+ * @param {boolean|'cap'|'diff'} [opts.readShim] the read shim's ARM (A0/A1/A2/A3), forwarded verbatim
367
368
  * @param {any} [opts.resume] RESUME (module C) — a `readResume` reading of the KILLED
368
369
  * run's own spine. Its `approvalHash` must match this run's, or the run refuses:
369
370
  * a resume continues ONE signed run, not any run.
@@ -397,6 +398,7 @@ export function runReuse(opts: {
397
398
  strikeLimit?: number | undefined;
398
399
  closeTimeoutMs?: number | undefined;
399
400
  layerRoot?: boolean | undefined;
401
+ readShim?: boolean | "cap" | "diff" | undefined;
400
402
  resume?: any;
401
403
  runJob?: typeof shippedRunJob | undefined;
402
404
  now?: (() => number) | undefined;
package/types/run.d.ts CHANGED
@@ -97,6 +97,19 @@
97
97
  * collision walk would mint a `-2` beside the work the resume exists to continue. The
98
98
  * fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
99
99
  * verbatim to the plan flow; absent is the cold path.
100
+ * @param {boolean|'cap'|'diff'} [opts.readShim=false] THE READ SHIM (src/readshim.js) —
101
+ * WHICH ARM to run, threaded to the plan flow. The Phase 2 pre-registration's four:
102
+ * `false` A0 — off (the default)
103
+ * `'cap'` A1 — cap + pointer + next-unseen-slice + G1, no diff
104
+ * `'diff'` A2 — the diff lever alone: no cap, no pointer, no G1
105
+ * `true` A3 — every lever
106
+ * Anything else THROWS below, before a token is spent: a mis-spelled arm coerced
107
+ * by truthiness into A3 would run one treatment under another's label and be
108
+ * invisible in the results afterwards.
109
+ * OFF is byte-identical to the pre-shim run in every observable (G1 included): the
110
+ * frozen A0 baseline arm has to be exactly today, so a guard firing under a disabled
111
+ * shim would make the baseline a treatment arm. The default-flip belongs to a
112
+ * paid contrast nobody has approved yet (the `layerRoot` precedent, F41).
100
113
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
101
114
  * territory, threaded to the plan flow. Defaults OFF
102
115
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -128,7 +141,7 @@
128
141
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
129
142
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
130
143
  */
131
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun }: {
144
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, readShim, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun }: {
132
145
  approvals: unknown;
133
146
  workdir: string;
134
147
  provider: any;
@@ -159,6 +172,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
159
172
  grantUsed?: boolean;
160
173
  } | null | undefined;
161
174
  resumeBranch?: string | null | undefined;
175
+ readShim?: boolean | "cap" | "diff" | undefined;
162
176
  layerRoot?: boolean | undefined;
163
177
  humanRuling?: {
164
178
  decision: string;
package/types/tools.d.ts CHANGED
@@ -19,8 +19,24 @@ export function strategyFor(granted: string[]): string;
19
19
  * RESULT is invisible cannot be judged: a `ctx_get` that silently reds (stale pointer,
20
20
  * bad range) looks exactly like one that worked, and the worker's fallback to a
21
21
  * whole-file read looks like a free choice instead of a forced one.
22
+ * @param {{onStalePointer?: ((req: {file: string, path: string, startLine: number, endLine: number, detail: string}) => {text: string, outcome: string}|null)|null}} [opts]
23
+ * `onStalePointer`: the read shim's stale-pointer serve, wired ONLY by a capping
24
+ * arm (see `createReadShim`). Absent — every caller today except that one — this
25
+ * whole seam is byte-identical to what it was, which is what keeps A0 the
26
+ * untouched baseline an experiment has already run against.
22
27
  */
23
- export function createCtxTools(lc: any, workdir: string, emit: (type: string, data: object) => void): ({
28
+ export function createCtxTools(lc: any, workdir: string, emit: (type: string, data: object) => void, { onStalePointer }?: {
29
+ onStalePointer?: ((req: {
30
+ file: string;
31
+ path: string;
32
+ startLine: number;
33
+ endLine: number;
34
+ detail: string;
35
+ }) => {
36
+ text: string;
37
+ outcome: string;
38
+ } | null) | null;
39
+ }): ({
24
40
  name: string;
25
41
  description: string;
26
42
  parameters: {