bareloop 0.6.0 → 0.7.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/types/clock.d.ts CHANGED
@@ -20,6 +20,14 @@
20
20
  * the same (raise `maxWallMs`), and the alternative — calling a governance stop
21
21
  * transport on the chance that it was — is the failure this closes.
22
22
  *
23
+ * BOTH derived bounds land here, because both are derived from the same wall:
24
+ * BA-18's idle timeout (`callTimeoutMs`) and BA-19's total-duration deadline
25
+ * (`callDeadlineMs`). Adding the second CODE does not widen the rule — the
26
+ * discriminator is still `clock.expired()`, never the code. That matters for the
27
+ * deadline specifically: a provider someone else constructed with its own
28
+ * `deadlineMs` could throw this code on a run with time left, and that is transport,
29
+ * exactly as it was before.
30
+ *
23
31
  * @param {any} err the throw as caught
24
32
  * @param {Clock} clock the run's clock
25
33
  * @returns {boolean}
@@ -35,22 +43,25 @@ export function isWallTimeout(err: any, clock: Clock): boolean;
35
43
  * @property {() => boolean} expired whether the deadline has passed (always false when unbounded)
36
44
  * @property {(closeTimeoutMs: number) => number|null} enforcedMs the HONEST worst case
37
45
  * @property {(providerDefaultMs?: number) => number} callTimeoutMs the derived per-call bound
46
+ * @property {() => number|null} callDeadlineMs the derived per-call TOTAL-duration ceiling; `null` when unbounded
38
47
  * @property {(closeTimeoutMs: number) => object} report one spine-safe shape carrying both numbers
39
48
  */
40
49
  /**
41
50
  * Start a wall clock for one run.
42
- * @param {{ maxWallMs?: number|null, closeStages?: number, now?: () => number }} [opts]
51
+ * @param {{ maxWallMs?: number|null, closeStages?: number, now?: () => number, priorElapsedMs?: number }} [opts]
43
52
  * `maxWallMs`: the signed operator cap, or absent/null for an honestly unbounded
44
53
  * run. `closeStages`: how many stages the job's close runs (W5) — each one gets
45
54
  * the FULL `closeTimeoutMs`, so this is the multiplier on the overshoot, not a
46
55
  * decoration. Defaults to 1, the single-predicate close. `now`: injected for
47
- * tests — the default is the real clock.
56
+ * tests — the default is the real clock. `priorElapsedMs`: time this attempt's
57
+ * PREDECESSOR already consumed (see below).
48
58
  * @returns {Clock}
49
59
  */
50
- export function createClock({ maxWallMs, closeStages, now }?: {
60
+ export function createClock({ maxWallMs, closeStages, now, priorElapsedMs }?: {
51
61
  maxWallMs?: number | null;
52
62
  closeStages?: number;
53
63
  now?: () => number;
64
+ priorElapsedMs?: number;
54
65
  }): Clock;
55
66
  /** BA-18's default request/idle timeout (`AnthropicProvider.timeoutMs`), the
56
67
  * ceiling every derived per-call timeout starts from. Kept in sync deliberately:
@@ -69,6 +80,13 @@ export const MIN_CALL_TIMEOUT_MS: 30000;
69
80
  * `category`, so every consumer that defaults an uncategorised throw would file
70
81
  * it as transport. */
71
82
  export const TIMEOUT_CODE: "ETIMEDOUT";
83
+ /** bare-agent's rejection code when its TOTAL-duration deadline trips (BA-19,
84
+ * `provider-http.js` → `applyRequestDeadline`). A DIFFERENT code from the idle
85
+ * bound's on purpose — upstream splits them precisely so a consumer routing
86
+ * governance stops apart from transport casualties can tell which timer fired. Like
87
+ * `TIMEOUT_CODE` it carries no `category`, so every default-to-transport catch site
88
+ * would file it as a casualty. */
89
+ export const DEADLINE_CODE: "EDEADLINE";
72
90
  export type Clock = {
73
91
  /**
74
92
  * whether the operator set a cap at all
@@ -102,6 +120,10 @@ export type Clock = {
102
120
  * the derived per-call bound
103
121
  */
104
122
  callTimeoutMs: (providerDefaultMs?: number) => number;
123
+ /**
124
+ * the derived per-call TOTAL-duration ceiling; `null` when unbounded
125
+ */
126
+ callDeadlineMs: () => number | null;
105
127
  /**
106
128
  * one spine-safe shape carrying both numbers
107
129
  */
package/types/index.d.ts CHANGED
@@ -6,4 +6,7 @@ export { globToPrefix, scanSecrets } from "./validate.js";
6
6
  export { validateJob, jobSpecHash, checkApproval, checkMenu, CLOSE_TYPES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, TOOL_MENU, LOCKED_TOOLS, STORE_VERBS, VERDICT_TYPES, LOCKED_VERDICTS } from "./job.js";
7
7
  export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from "./plan.js";
8
8
  export { snapshotScope, evalExits } from "./exits.js";
9
+ export { BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate, mintBridge, appendGreen, appendRed, loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists } from "./bridges.js";
10
+ export { renderListing, selectionPrompt } from "./selection.js";
11
+ export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate } from "./reuse.js";
9
12
  export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from "./ledger.js";
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Normalize a gap for comparison. Deliberately NARROW: only bytes a re-run changes
3
+ * on its own are removed. Error COUNTS, file names and line numbers all survive —
4
+ * a trajectory like "30 errors" → "22 errors" must read as two different gaps, and
5
+ * a normalizer that scrubbed numbers would turn the converging case this module
6
+ * exists to protect into an instant strike.
7
+ * @param {string} text @returns {string}
8
+ */
9
+ export function normalizeGap(text: string): string;
10
+ /**
11
+ * The comparable key for a gap: the normalized text, hashed. Hashed rather than
12
+ * kept whole because the seen-set lives for the whole step and a close's output
13
+ * can be tens of KB — and because the set holds no content, nothing here can leak
14
+ * model or file bytes into a record.
15
+ * @param {string} text @returns {string}
16
+ */
17
+ export function gapKey(text: string): string;
18
+ /**
19
+ * Build one ladder. ONE PER STEP: the seen-set and the strike count are the step's
20
+ * own history, and a ladder shared across steps would strike a fresh step for a gap
21
+ * an earlier one had already seen.
22
+ *
23
+ * @param {object} opts
24
+ * @param {number} [opts.limit] strikes that end the step (default `STRIKE_LIMIT`)
25
+ * @param {() => number} opts.writeCount the CUMULATIVE count of the worker's
26
+ * allow-decision write/edit records so far this step (planrun reads it off the
27
+ * gate audit). Required, and a throw when it is missing rather than a silent
28
+ * degradation to repeats-only: a strike rule running on one of its two signals
29
+ * with nothing on the record saying so is the blind-instrument class, and this
30
+ * is the BA-4 param-guard class where a throw is the right answer.
31
+ */
32
+ export function createLadder({ limit, writeCount }: {
33
+ limit?: number | undefined;
34
+ writeCount: () => number;
35
+ }): {
36
+ /**
37
+ * Read one RED iteration. Called by ralph after the judge has rendered a
38
+ * non-green, non-fault verdict — a green never reaches here, so every reading
39
+ * is a failing iteration by construction.
40
+ * @param {{iteration: number, gap?: string}} o
41
+ * @returns {{governor: string, iteration: number, strike: boolean, strikes: number,
42
+ * limit: number, wrote: boolean, repeatOf: number|null, distinctGaps: number}}
43
+ */
44
+ record({ iteration, gap }: {
45
+ iteration: number;
46
+ gap?: string;
47
+ }): {
48
+ governor: string;
49
+ iteration: number;
50
+ strike: boolean;
51
+ strikes: number;
52
+ limit: number;
53
+ wrote: boolean;
54
+ repeatOf: number | null;
55
+ distinctGaps: number;
56
+ };
57
+ /** has the step run out of strikes? */
58
+ struckOut: () => boolean;
59
+ /** the ladder's own books — counts and iteration numbers, never gap content */
60
+ report: () => {
61
+ iterations: number;
62
+ strikes: number;
63
+ limit: number;
64
+ distinctGaps: number;
65
+ repeats: {
66
+ at: number;
67
+ firstAt: number;
68
+ }[];
69
+ idle: number[];
70
+ };
71
+ /**
72
+ * The mechanism, in one sentence, for the replan brief. It is the only channel
73
+ * the redrafting planner adapts to, so it states WHICH shape ended the step and
74
+ * the evidence for it — never a label on its own, and never a culprit file
75
+ * beyond what the close itself reported (F28): the inputs here are iteration
76
+ * numbers and counts, so naming one is inexpressible.
77
+ * @returns {string}
78
+ */
79
+ brief(): string;
80
+ };
81
+ /**
82
+ * The strike ceiling. Shell-owned, exactly where the fixed count was: the runner
83
+ * sets it, the agent cannot express it, and it is arbiter territory — a step's own
84
+ * plan may not tighten it and may not raise it. Two is hamr's number.
85
+ */
86
+ export const STRIKE_LIMIT: 2;
package/types/plan.d.ts CHANGED
@@ -90,7 +90,7 @@ export function stageClose(close: any): any[] | null;
90
90
  * job spec. Never throws on JSON text or plain parsed data; every failure is
91
91
  * a named red. Returns the parsed plan on ok (single parse), null on any red.
92
92
  * @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
93
- * @param {{ job?: any, maxStepRounds?: number, scopes?: string[], capRuns?: number }} [opts] `job`: the
93
+ * @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[] }} [opts] `job`: the
94
94
  * validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
95
95
  * menu all come from it — a missing or non-plan-shape job fails CLOSED);
96
96
  * `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
@@ -98,13 +98,28 @@ export function stageClose(close: any): any[] | null;
98
98
  * `scopes`: the offered `tree-changed` menu from `legalScopes` (the SAME array
99
99
  * the drafting prompt enumerated). Omitted, it derives from the signed
100
100
  * writeScope — never a free-text fallback (F50).
101
+ *
102
+ * The two shape-lottery gate rules (2026-08-04) — both keyed on RECORDED
103
+ * facts, never an LLM judgment of "is this job small", and both inactive
104
+ * when their fact is omitted (resume/reuse callers stay byte-identical):
105
+ * `seedRed`: check names whose PREFLIGHT verdict was needs_revision. Rule
106
+ * A-v2 — a seed-red check is the goal itself, not a milestone; it may only
107
+ * sit on the plan's FINAL write step (`check-placement`). The $0 archive
108
+ * sweep: seed-red-check-on-an-early-step has 0 honest greens ever; the one
109
+ * wide closing step greens 7/7. Green-at-seed checks stay free mid-plan
110
+ * (the 20 TESTGEN mid-plan greens — a guard, not a goal).
111
+ * `priorChecks`: the check names the PREDECESSOR plan carried (replan gate
112
+ * only). Rule B — a redraft may not drop one (`check-shed`): exits without
113
+ * the check verify FORM alone, so a shed lets the run "green" unearned
114
+ * (u-msdsmkid mechanism b; the 3 archived sheds were all on step-red runs).
101
115
  * @returns {{ ok: boolean, reds: Red[], plan: object|null }}
102
116
  */
103
- export function validatePlan(input: object | string, { job, maxStepRounds, scopes, capRuns }?: {
117
+ export function validatePlan(input: object | string, { job, maxStepRounds, scopes, seedRed, priorChecks }?: {
104
118
  job?: any;
105
119
  maxStepRounds?: number;
106
120
  scopes?: string[];
107
- capRuns?: number;
121
+ seedRed?: string[];
122
+ priorChecks?: string[];
108
123
  }): {
109
124
  ok: boolean;
110
125
  reds: Red[];
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Files named in an exit gap that the step's worker never wrote — the mechanical
3
+ * fact behind the prose-prohibition trap (u-msdpuaej, 2026-08-03): a drafter wrote
4
+ * "do not modify any file outside X" into a step whose exit check judges the whole
5
+ * goal, the worker obeyed, and the replanner — handed the gap AND the stall note —
6
+ * rebuilt the same shape, because inferring "the exit reds on files this step never
7
+ * touched" is exactly the semantic leap that fails (F38: mechanical gaps convert,
8
+ * semantic ones stall; 3/3 across the mailbox class). This computes that fact from
9
+ * the two books that already exist: path-like tokens in the gap text (extension
10
+ * required — a bare `step/two` is not a file claim) suffix-matched against the F32
11
+ * write audit's absolute paths. Advisory-only and fail-safe: extraction that finds
12
+ * nothing yields no line, never a guess.
13
+ *
14
+ * @param {string | undefined} gap the step's last exit gap text
15
+ * @param {string[] | undefined} writes the step's cumulative write-audit paths
16
+ * @returns {string[]} gap-named files with no matching write, in gap order
17
+ */
18
+ export function gapFilesNeverWritten(gap: string | undefined, writes: string[] | undefined): string[];
1
19
  /**
2
20
  * The plan-drafting prompt: a schema DESCRIPTION built from the live validator
3
21
  * menus — never a copyable example (the drafter must author, not echo; the
@@ -13,16 +31,26 @@
13
31
  * @param {{ balanceUsd?: number|null, remainingMs?: number|null, progress?: string }} [materials]
14
32
  * what the run has LEFT (T/A). Omitted → no materials block at all, which is the
15
33
  * pre-T behaviour and keeps every existing caller byte-identical.
16
- * @param {number} [capRuns] the shell's attempt cap — the ceiling `attempts` may only
17
- * TIGHTEN. Interpolated the way every sibling bound is: `validatePlan` reds outside
18
- * `1..capRuns`, and a prompt that says only "integer" spends a round teaching it.
19
- * Defaults to validatePlan's own default so existing callers are unchanged.
34
+ * @param {object|null} [startingDraft] Layer 3 (D4) — a loaded bridge's plan, handed over
35
+ * as the draft to TWEAK. Omitted/null is the COLD path and renders byte-identically to
36
+ * the pre-Layer-3 prompt: the block is additive, never surgery on the prompt's interior,
37
+ * so the two paths cannot drift (the F47 works-both-ways rule). Only the runner passes
38
+ * it, and only for the FIRST draft phase — a replan drafts from the run's own state.
39
+ * @param {{ seedRed?: string[], priorChecks?: string[] }} [checkFacts] the shape-lottery
40
+ * gate rules, STATED as well as enforced (the mailbox precedent: a rule only the
41
+ * validator knows costs a redraft per draft). `seedRed`: preflight-red check names —
42
+ * Rule A-v2's law (final write step only, framed wide). `priorChecks`: the predecessor
43
+ * plan's carried checks, replan phase only — Rule B's law (keep every one). Empty/omitted
44
+ * renders byte-identically to the pre-rules prompt.
20
45
  */
21
46
  export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxStepRounds: number, failure: string | null, scopes: string[] | undefined, materials?: {
22
47
  balanceUsd?: number | null;
23
48
  remainingMs?: number | null;
24
49
  progress?: string;
25
- }, capRuns?: number): string;
50
+ }, startingDraft?: object | null, checkFacts?: {
51
+ seedRed?: string[];
52
+ priorChecks?: string[];
53
+ }): string;
26
54
  /**
27
55
  * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close).
28
56
  * Called by runJob after the approval gate, validation, and the smoke — this
@@ -48,7 +76,17 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
48
76
  * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier factory
49
77
  * (tier ∈ STEP_MODELS → a provider). A plan naming a tier with no factory supplied is an
50
78
  * interpreter-red STOP — never silently run the default tier as if the choice was honoured.
51
- * @param {number} [opts.capRuns] shell-owned per-step attempt cap
79
+ * @param {number} [opts.capRuns] shell-owned iteration cap, RETIRED as a governor
80
+ * (v1.46 §4). It stopped bounding a plan step when the strike ladder landed, and it
81
+ * stopped bounding the close-fix loop when the close trend rule did. It survives as
82
+ * that loop's fallback for the ONE case the trend cannot read — a close whose output
83
+ * carries no number at all — because a governor that cannot see the variable must
84
+ * not be the governor, and the honest fallback is the cruder bound it replaced
85
+ * rather than "unbounded". It lifts the moment a stage reports a comparable number.
86
+ * @param {number} [opts.strikeLimit] shell-owned STRIKE ceiling for a plan step's
87
+ * ladder (src/ladder.js). Arbiter territory exactly as the count it replaced was —
88
+ * the runner sets it, the plan cannot express it, and no step may tighten or raise
89
+ * it.
52
90
  * @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
53
91
  * @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
54
92
  * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
@@ -63,15 +101,67 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
63
101
  * first plan-flow job to emit `root-injected` runs the pre-registered ON-vs-OFF acceptance
64
102
  * read (the Layer R default-flip, LAYERS.md ⚠). Excluded on native (clipipe): the native
65
103
  * worker has no onToolResult seam, so the tee cannot settle and same-path rewrites are blind.
104
+ * @param {unknown} [opts.bridge] Layer 3 (D3/D4) — a bridge-v1 registry entry to REUSE as the
105
+ * drafter's starting point. Absent is the COLD path and is byte-identical to the pre-Layer-3
106
+ * flow. Present, the D2-split LOAD GATE runs at the door, before the close precheck and
107
+ * before any token: on a pass the newest version's plan (the run-as-executed artifact)
108
+ * rides into the FIRST draft prompt only; on a fail the run returns the distinct terminal
109
+ * `recipe-stale` having spent nothing. Selecting the entry — the listing, the pin, the
110
+ * LLM's pick — is the CALLER's, and so is falling back to cold: a silent automatic
111
+ * fall-back would spend a run's budget on a decision nobody made.
112
+ * @param {number} [opts.priorWallMs=0] RESUME (module C) — wall time a PREVIOUS, killed
113
+ * attempt of this same try already consumed, folded into the run clock (see
114
+ * `createClock`'s `priorElapsedMs`). The SIGNED `maxWallMs` is never rewritten: it is in
115
+ * the spec hash, and a resume that edited it would need a signature nobody typed. The
116
+ * restarted attempt simply starts already partly spent, so a kill can never buy a fresh
117
+ * allotment of the operator's wall.
118
+ * @param {any} [opts.resumeSeed] RESUME (module C v2) — the FINEST honest checkpoint of a
119
+ * KILLED run of this same try, read off its own spine by `readResume` (src/reuse.js) and
120
+ * by nothing else: `{ phase, plan, completedSteps: [{id, seq, by}] }`. hamr's ruling is
121
+ * the whole contract — *"it should allow resume and start last step instead from the
122
+ * beginning, why would i want to waste more money on something i already started"* — so
123
+ * with a seed this flow re-pays for NO paid unit the dead run already bought:
124
+ *
125
+ * - the SCOUT does not run (`scout-skipped`, never silence). Its survey is not on the
126
+ * spine (only its byte count is), so the ONE consequence is stated rather than
127
+ * discovered: a replan after a resume drafts from an empty survey plus the failure
128
+ * brief. Re-scouting to refill it would re-pay the exact call this exists to save.
129
+ * - the PLAN is not re-drafted. It is reloaded from the seed and RE-VALIDATED against
130
+ * the current signed spec; a plan that no longer validates is `plan-red` by name
131
+ * (`resume-plan-red`), never run. It is then emitted as this leg's own
132
+ * `plan-accepted` (`phase: 'resume'`) — R1 mints a bridge version from the plan AS
133
+ * EXECUTED, read back off the spine, so a leg that greens without one would mint
134
+ * nothing.
135
+ * - a step whose exit was already SATISFIED is skipped, in prefix order, each with its
136
+ * own `step-skipped` record naming the event that proves it. Never a fake
137
+ * step-start/step-end pair, and never silence: a reader must be able to tell skipped
138
+ * from run (the resume-to-cap invariant).
139
+ *
140
+ * The worker's conversation is deliberately NOT replayed — a transcript is not a
141
+ * checkpoint, and every attempt is fresh by the loop's own design. Absent is the
142
+ * ordinary path and is byte-identical to a run that was never killed.
143
+ * @param {{stage?: string|null, value?: number|null}[]} [opts.resumeGrades] RESUME — the
144
+ * close GRADES the dead leg recorded, oldest first (`readResume`'s `restart.grades`).
145
+ * A resumed leg is the same run continuing, so its halt readouts must judge the whole
146
+ * chain: without this the trend restarts at this leg's first close and can report
147
+ * `flat` — "revise the goal" — on a run that was converging when its allowance ran
148
+ * out. Baselines only (src/trend.js's THE SEED): it spends none of this leg's own
149
+ * bounds and mints none of its strikes. Empty/omitted is the cold path.
150
+ * @param {() => boolean} [opts.spendComplete] is the run's spend EXACT, or a floor? The
151
+ * ledger one level up owns the answer (unpriced rounds, a self-healed stall, a mid-call
152
+ * cut, an inherited resume floor); the money-halt readout states it beside the remaining
153
+ * it quotes, because `budget − floor` is a CEILING and printing it bare reads as exact
154
+ * (F6). Defaults to exact, which is what every pre-record caller was implicitly saying.
66
155
  * @param {() => number} [opts.now] the wall clock's time source, injected. The real clock is the
67
156
  * default; a caller supplies this to drive time deterministically (the same seam `createClock`
68
157
  * already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
69
158
  * whose floor is one close timeout).
70
159
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
71
- * 'check-red' | 'close-red' | 'close-unsupported' | 'pricing-red' | 'cap-halt' |
72
- * 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' | `step-red:<id>`
160
+ * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
161
+ * 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' |
162
+ * `step-red:<id>`
73
163
  */
74
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, now }: {
164
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades }: {
75
165
  workdir: string;
76
166
  provider: any;
77
167
  nativeProvider?: ((o: {
@@ -85,10 +175,19 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
85
175
  isUnpriced?: (() => boolean) | undefined;
86
176
  providerFor?: ((tier: string) => any) | undefined;
87
177
  capRuns?: number | undefined;
178
+ strikeLimit?: number | undefined;
88
179
  closeTimeoutMs?: number | undefined;
89
180
  maxStepRounds?: number | undefined;
90
181
  scoutRounds?: number | undefined;
91
182
  layerRoot?: boolean | undefined;
183
+ bridge?: unknown;
184
+ priorWallMs?: number | undefined;
185
+ resumeSeed?: any;
186
+ resumeGrades?: {
187
+ stage?: string | null;
188
+ value?: number | null;
189
+ }[] | undefined;
190
+ spendComplete?: (() => boolean) | undefined;
92
191
  now?: (() => number) | undefined;
93
192
  }): Promise<string>;
94
193
  export type CategorizedError = Error & {
package/types/ralph.d.ts CHANGED
@@ -111,6 +111,26 @@ export function runStages(stages: any[], redact?: (s: string) => string, { timeo
111
111
  timeoutMs?: number;
112
112
  cwd?: string;
113
113
  }): Promise<any>;
114
+ /**
115
+ * The exhaustion-by-PROGRESS seam, as a contract rather than "whatever one module
116
+ * happens to return". Two governors implement it and they read different signals —
117
+ * the step ladder's repeats-and-writes (src/ladder.js) and the close-fix loop's
118
+ * per-stage graded numbers (src/trend.js) — which is the whole reason ralph takes an
119
+ * interface here and interprets nothing itself.
120
+ *
121
+ * `record` returns the governor's own reading, which ralph emits verbatim and never
122
+ * inspects; each reading names its `governor` so two instruments under one spine type
123
+ * can never be averaged into one number. `report` is the only shape ralph itself
124
+ * reads. `terminal` is optional and overrides the exhaustion PROSE only — never the
125
+ * category, never the outcome.
126
+ *
127
+ * @typedef {object} LoopGovernor
128
+ * @property {(o: {iteration: number, gap?: string}) => object} record
129
+ * @property {() => boolean} struckOut
130
+ * @property {() => {strikes: number, limit: number, iterations: number, distinctGaps?: number}} report
131
+ * @property {() => string} brief
132
+ * @property {() => {decision: string, options: string[]}} [terminal]
133
+ */
114
134
  /**
115
135
  * The loop: `while close-red and under-cap: run the middle`. Stops at first
116
136
  * green (within-run tuning past a visible close is the fit-to-pass surface,
@@ -136,7 +156,28 @@ export function runStages(stages: any[], redact?: (s: string) => string, { timeo
136
156
  * zone, worker-crash routing (F32), and the cap taxonomy apply unchanged.
137
157
  * Injected by run.js only; inexpressible in any config or plan — the agent
138
158
  * never authors its judge any more than its close.
139
- * @param {number} opts.capRuns budget: max middle runs
159
+ * @param {number} [opts.capRuns] budget: max middle runs. Required UNLESS `ladder`
160
+ * is supplied — the two exhaustion rules are alternatives, never both at once.
161
+ * The "unless" is ENFORCED at entry (a param guard that throws), not merely
162
+ * documented: an optional-in-the-contract bound with nothing checking it made a
163
+ * loop that never ran report a governance stop that never happened.
164
+ * @param {LoopGovernor} [opts.ladder]
165
+ * the PROGRESS governor (src/ladder.js) that replaces the fixed count for a plan
166
+ * STEP's micro-loop. With it, the loop is bounded by strikes rather than by a
167
+ * number of runs: iterations float, and money, the wall, the variance meter and
168
+ * the stall fuse keep every bit of their current authority (each still ends the
169
+ * loop through the middle's own throw). Ralph still interprets nothing — it hands
170
+ * the ladder the iteration's gap and reads back a boolean.
171
+ *
172
+ * The CLOSE-FIX loop now supplies one too (PRD v1.46 §4, once its own $0 replay
173
+ * over all 8 archived fix loops came back clean: 0 greens harmed, 1 waste case
174
+ * caught). Its governor reads a different signal — the close's own per-stage
175
+ * graded numbers (src/trend.js), not repeats and writes — which is exactly why
176
+ * this is a SEAM and not a hard-coded rule: ralph still interprets nothing.
177
+ * An optional `terminal()` on the governor overrides the exhaustion prose, because
178
+ * the step ladder's copy offers the human a replan and there is no planner at the
179
+ * close. The category and the outcome are NOT overridable: one exhaustion terminal,
180
+ * two triggers.
140
181
  * @param {(type: string, data?: object) => object} opts.emit a spine emitter
141
182
  * @param {(s: string) => string} [opts.redact] source scrubber for close output
142
183
  * (secrets never enter the spine); injected so the shell stays stdlib-only
@@ -155,7 +196,7 @@ export function runStages(stages: any[], redact?: (s: string) => string, { timeo
155
196
  * with zero writes, a crash stays what it always was: an instrument stop.
156
197
  * @returns {Promise<'green'|'escalated'>}
157
198
  */
158
- export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
199
+ export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
159
200
  middle: (iteration: number, gap?: string) => void | Promise<void>;
160
201
  close?: string[] | undefined;
161
202
  judge?: (() => Promise<{
@@ -167,7 +208,8 @@ export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeou
167
208
  gap?: string;
168
209
  detail?: string;
169
210
  }) | undefined;
170
- capRuns: number;
211
+ capRuns?: number | undefined;
212
+ ladder?: LoopGovernor | undefined;
171
213
  emit: (type: string, data?: object) => object;
172
214
  redact?: ((s: string) => string) | undefined;
173
215
  closeTimeoutMs?: number | undefined;
@@ -194,7 +236,12 @@ export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeou
194
236
  * Two rules, deliberately kept BOTH ways round:
195
237
  * - `names` / `prefixes` — the knowns. A literal denylist is a chase (every
196
238
  * new provider mints a new variable), so it is not load-bearing on its own;
197
- * it exists for names the shape rule cannot see (`AWS_ACCESS_KEY_ID`).
239
+ * it exists for names the shape rule STRUCTURALLY cannot see — either no
240
+ * `_`-bounded credential suffix at all (`AWS_ACCESS_KEY_ID`, `PGPASSWORD`,
241
+ * `MYSQL_PWD`), a `*_KEY` that is not `*_API_KEY` (`SSH_PRIVATE_KEY`), or a
242
+ * BARE spelling with nothing in front of the suffix (`TOKEN`, `SECRET`).
243
+ * Names are matched EXACTLY, which is what keeps connection config out of
244
+ * the strip (`PGHOST`, `SECRETS_DIR`, `TOKENIZERS_PARALLELISM` all survive).
198
245
  * - `shape` — the NAME-SHAPE rule, which is what bounds the chase: anything
199
246
  * whose name ends in the credential suffixes goes, whoever minted it and
200
247
  * whether or not anyone here has heard of it. Case-insensitive; no `g` flag,
@@ -228,3 +275,34 @@ export const CLOSE_FAULTS: Record<string, {
228
275
  decision: string;
229
276
  options: string[];
230
277
  }>;
278
+ /**
279
+ * The exhaustion-by-PROGRESS seam, as a contract rather than "whatever one module
280
+ * happens to return". Two governors implement it and they read different signals —
281
+ * the step ladder's repeats-and-writes (src/ladder.js) and the close-fix loop's
282
+ * per-stage graded numbers (src/trend.js) — which is the whole reason ralph takes an
283
+ * interface here and interprets nothing itself.
284
+ *
285
+ * `record` returns the governor's own reading, which ralph emits verbatim and never
286
+ * inspects; each reading names its `governor` so two instruments under one spine type
287
+ * can never be averaged into one number. `report` is the only shape ralph itself
288
+ * reads. `terminal` is optional and overrides the exhaustion PROSE only — never the
289
+ * category, never the outcome.
290
+ */
291
+ export type LoopGovernor = {
292
+ record: (o: {
293
+ iteration: number;
294
+ gap?: string;
295
+ }) => object;
296
+ struckOut: () => boolean;
297
+ report: () => {
298
+ strikes: number;
299
+ limit: number;
300
+ iterations: number;
301
+ distinctGaps?: number;
302
+ };
303
+ brief: () => string;
304
+ terminal?: (() => {
305
+ decision: string;
306
+ options: string[];
307
+ }) | undefined;
308
+ };