bareloop 0.6.0 → 0.8.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.
@@ -1,3 +1,83 @@
1
+ /**
2
+ * This step's own last EXIT results, rendered for the REPLAN brief.
3
+ *
4
+ * The label says exits and not "the close", and the distinction is the whole of one
5
+ * fix: `res.gap` is `lastGap`, the join of EVERY failing exit's detail in exit order.
6
+ * With `MAX_EXITS_PER_STEP` at 2 and the mandatory `tree-changed` pairing on a write
7
+ * step, a step that wrote nothing leads the block with the EVALUATOR's own prose
8
+ * (`0 files changed under src/** …`) — captured verbatim from a real replan prompt,
9
+ * under a label reading "the verification's own output". The close stage's output is
10
+ * one of the details here, never reliably all of them. The payload is left alone: "the
11
+ * step wrote nothing" is exactly what a replanner should be told, and stripping it to
12
+ * make an old label true would trade a real fact for a tidy sentence.
13
+ *
14
+ * u-mshcpdg4: a run one strict error from done, killed with $1.10 and 82 seconds
15
+ * unspent. The close named the remaining work exactly — `src/recurse.js(978,115)`
16
+ * — and the worker read that every attempt. The REPLANNER, which is the component
17
+ * that chooses which files the next plan targets, was handed only the trend line
18
+ * (`still progressing — typecheck 30 → 15 → 15 → 1`): converging, and silent about
19
+ * WHERE. It re-targeted `src/loop.js`, already at zero errors; that step wrote
20
+ * nothing and struck out. F85 gave the brief the trajectory; this gives it the
21
+ * artifact the trajectory is a summary OF.
22
+ *
23
+ * Handed over as TEXT, never as a parsed file list, and the trap is worth naming
24
+ * because the parse is the obvious-looking fix: line 1 of that very gap reads
25
+ * `reports 1 error(s) in src/recurse.js, src/loop.js` — every file in SCOPE — and
26
+ * only line 2 names the culprit. The `never wrote` advisory that used to sit here
27
+ * did exactly that parse, and on this gap it resolved to `src/loop.js` — the file
28
+ * already CLEAN — then said so as a directive beside the artifact that said the
29
+ * opposite. It was deleted with this change (hamr, 2026-08-05): two readers of one
30
+ * question, the parsed one wrong, is the same failure the variance meter just had.
31
+ * Nothing generic can tell a summary line from a detail line, and the
32
+ * shapes differ per close anyway (tsc file:line, pytest test ids, a count close
33
+ * that names no file at all). A model reading the artifact can make that
34
+ * distinction; a regex reproduces the bug. So: no extraction, no digest, no
35
+ * inference — the artifact, bounded and scrubbed.
36
+ *
37
+ * Two boundaries, both reused rather than respelled:
38
+ * - SCRUB. A prompt is an egress point and this repo has ONE secret inventory
39
+ * (`SECRET_PATTERNS` via `redactSecrets`). The gap arrives already scrubbed
40
+ * from `judge`, which makes this defense in depth rather than the only guard —
41
+ * and defense in depth at the egress is exactly where it belongs, because the
42
+ * next caller of this helper will not remember the upstream one.
43
+ * - BOUND, and it is a BACKSTOP rather than an envelope. What arrives here has
44
+ * ALREADY been bounded: `runClose` ran `boundGap` with the stage's own `gapKeep`,
45
+ * which deliberately rescues the `not ok`/`FAILED` names out of the elided middle
46
+ * — the mechanical gap F46's conversion mechanism feeds the worker. Re-bounding
47
+ * that with a fresh 400/1500 envelope deletes exactly those names a second time,
48
+ * which is F28 reintroduced; measured on a 120-red-line close output, the first
49
+ * bound rescued 78 names and this one dropped 12 of them. src/exits.js states the
50
+ * same rule for the same artifact one seam earlier, and this now uses ITS number:
51
+ * `CHECK_GAP_MAX`, imported rather than respelled, so the two seams cannot drift
52
+ * on how big a check gap may be. Under it the gap passes through VERBATIM.
53
+ *
54
+ * A backstop is still needed, and the headroom is why the ceiling is not smaller.
55
+ * `res.gap` is `lastGap`, the JOIN of up to `MAX_EXITS_PER_STEP` failing exit
56
+ * details, and the check-passes branch slices EACH at `CHECK_GAP_MAX` — so what
57
+ * arrives is bounded per detail, never in total. Measured, the real maximum is
58
+ * comfortably under: `runClose`'s own envelope tops out at ~10.1KB (400 head +
59
+ * 8192 keep + 1500 tail + labels), and joined with a `tree-changed` line that is
60
+ * ~10.2KB, so the shipped path passes through untouched with ~1.8KB to spare. The
61
+ * backstop therefore fires only where src/exits.js says it should: a seam that
62
+ * returned something never bounded at all.
63
+ *
64
+ * Over the line `boundGap` runs with `REPLAN_GAP_KEEP`, the same envelope the
65
+ * close path uses, and every trim announces itself (F28). Note what that keep-set
66
+ * cannot do: `\S` matches every non-blank line, so the keep block degenerates into
67
+ * "the first 50 lines" and re-surfaces nothing from the middle. That is the honest
68
+ * limit of a red-set that cannot know the stage's own pattern (see
69
+ * `REPLAN_GAP_KEEP`), and it is affordable precisely because it now applies only
70
+ * to a gap nothing upstream ever bounded — on that gap ANY envelope loses names,
71
+ * and the choice is a bounded prompt over an unbounded one.
72
+ *
73
+ * No gap → the empty string, so the brief renders byte-identically to the pre-F86
74
+ * one. A labelled empty section would be an invitation to explain an absence the
75
+ * run never observed (a stall never judged its exits at all).
76
+ *
77
+ * @param {string | null | undefined} gap the step's last exit gap text
78
+ * @returns {string} the labelled block, or '' when there is nothing to show
79
+ */
80
+ export function closeGapBlock(gap: string | null | undefined): string;
1
81
  /**
2
82
  * The plan-drafting prompt: a schema DESCRIPTION built from the live validator
3
83
  * menus — never a copyable example (the drafter must author, not echo; the
@@ -13,16 +93,26 @@
13
93
  * @param {{ balanceUsd?: number|null, remainingMs?: number|null, progress?: string }} [materials]
14
94
  * what the run has LEFT (T/A). Omitted → no materials block at all, which is the
15
95
  * 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.
96
+ * @param {object|null} [startingDraft] Layer 3 (D4) — a loaded bridge's plan, handed over
97
+ * as the draft to TWEAK. Omitted/null is the COLD path and renders byte-identically to
98
+ * the pre-Layer-3 prompt: the block is additive, never surgery on the prompt's interior,
99
+ * so the two paths cannot drift (the F47 works-both-ways rule). Only the runner passes
100
+ * it, and only for the FIRST draft phase — a replan drafts from the run's own state.
101
+ * @param {{ seedRed?: string[], priorChecks?: string[] }} [checkFacts] the shape-lottery
102
+ * gate rules, STATED as well as enforced (the mailbox precedent: a rule only the
103
+ * validator knows costs a redraft per draft). `seedRed`: preflight-red check names —
104
+ * Rule A-v2's law (final write step only, framed wide). `priorChecks`: the predecessor
105
+ * plan's carried checks, replan phase only — Rule B's law (keep every one). Empty/omitted
106
+ * renders byte-identically to the pre-rules prompt.
20
107
  */
21
108
  export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxStepRounds: number, failure: string | null, scopes: string[] | undefined, materials?: {
22
109
  balanceUsd?: number | null;
23
110
  remainingMs?: number | null;
24
111
  progress?: string;
25
- }, capRuns?: number): string;
112
+ }, startingDraft?: object | null, checkFacts?: {
113
+ seedRed?: string[];
114
+ priorChecks?: string[];
115
+ }): string;
26
116
  /**
27
117
  * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close).
28
118
  * Called by runJob after the approval gate, validation, and the smoke — this
@@ -48,7 +138,17 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
48
138
  * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier factory
49
139
  * (tier ∈ STEP_MODELS → a provider). A plan naming a tier with no factory supplied is an
50
140
  * 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
141
+ * @param {number} [opts.capRuns] shell-owned iteration cap, RETIRED as a governor
142
+ * (v1.46 §4). It stopped bounding a plan step when the strike ladder landed, and it
143
+ * stopped bounding the close-fix loop when the close trend rule did. It survives as
144
+ * that loop's fallback for the ONE case the trend cannot read — a close whose output
145
+ * carries no number at all — because a governor that cannot see the variable must
146
+ * not be the governor, and the honest fallback is the cruder bound it replaced
147
+ * rather than "unbounded". It lifts the moment a stage reports a comparable number.
148
+ * @param {number} [opts.strikeLimit] shell-owned STRIKE ceiling for a plan step's
149
+ * ladder (src/ladder.js). Arbiter territory exactly as the count it replaced was —
150
+ * the runner sets it, the plan cannot express it, and no step may tighten or raise
151
+ * it.
52
152
  * @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
53
153
  * @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
54
154
  * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
@@ -63,15 +163,101 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
63
163
  * first plan-flow job to emit `root-injected` runs the pre-registered ON-vs-OFF acceptance
64
164
  * read (the Layer R default-flip, LAYERS.md ⚠). Excluded on native (clipipe): the native
65
165
  * worker has no onToolResult seam, so the tee cannot settle and same-path rewrites are blind.
166
+ * @param {unknown} [opts.bridge] Layer 3 (D3/D4) — a bridge-v1 registry entry to REUSE as the
167
+ * drafter's starting point. Absent is the COLD path and is byte-identical to the pre-Layer-3
168
+ * flow. Present, the D2-split LOAD GATE runs at the door, before the close precheck and
169
+ * before any token: on a pass the newest version's plan (the run-as-executed artifact)
170
+ * rides into the FIRST draft prompt only; on a fail the run returns the distinct terminal
171
+ * `recipe-stale` having spent nothing. Selecting the entry — the listing, the pin, the
172
+ * LLM's pick — is the CALLER's, and so is falling back to cold: a silent automatic
173
+ * fall-back would spend a run's budget on a decision nobody made.
174
+ * @param {number} [opts.priorWallMs=0] RESUME (module C) — wall time a PREVIOUS, killed
175
+ * attempt of this same try already consumed, folded into the run clock (see
176
+ * `createClock`'s `priorElapsedMs`). The SIGNED `maxWallMs` is never rewritten: it is in
177
+ * the spec hash, and a resume that edited it would need a signature nobody typed. The
178
+ * restarted attempt simply starts already partly spent, so a kill can never buy a fresh
179
+ * allotment of the operator's wall.
180
+ * @param {any} [opts.resumeSeed] RESUME (module C v2) — the FINEST honest checkpoint of a
181
+ * KILLED run of this same try, read off its own spine by `readResume` (src/reuse.js) and
182
+ * by nothing else: `{ phase, plan, completedSteps: [{id, seq, by}] }`. hamr's ruling is
183
+ * the whole contract — *"it should allow resume and start last step instead from the
184
+ * beginning, why would i want to waste more money on something i already started"* — so
185
+ * with a seed this flow re-pays for NO paid unit the dead run already bought:
186
+ *
187
+ * - the SCOUT does not run (`scout-skipped`, never silence). Its survey is not on the
188
+ * spine (only its byte count is), so the ONE consequence is stated rather than
189
+ * discovered: a replan after a resume drafts from an empty survey plus the failure
190
+ * brief. Re-scouting to refill it would re-pay the exact call this exists to save.
191
+ * - the PLAN is not re-drafted. It is reloaded from the seed and RE-VALIDATED against
192
+ * the current signed spec; a plan that no longer validates is `plan-red` by name
193
+ * (`resume-plan-red`), never run. It is then emitted as this leg's own
194
+ * `plan-accepted` (`phase: 'resume'`) — R1 mints a bridge version from the plan AS
195
+ * EXECUTED, read back off the spine, so a leg that greens without one would mint
196
+ * nothing.
197
+ * - a step whose exit was already SATISFIED is skipped, in prefix order, each with its
198
+ * own `step-skipped` record naming the event that proves it. Never a fake
199
+ * step-start/step-end pair, and never silence: a reader must be able to tell skipped
200
+ * from run (the resume-to-cap invariant).
201
+ *
202
+ * The worker's conversation is deliberately NOT replayed — a transcript is not a
203
+ * checkpoint, and every attempt is fresh by the loop's own design. Absent is the
204
+ * ordinary path and is byte-identical to a run that was never killed.
205
+ * @param {{stage?: string|null, value?: number|null}[]} [opts.resumeGrades] RESUME — the
206
+ * close GRADES the dead leg recorded, oldest first (`readResume`'s `restart.grades`).
207
+ * A resumed leg is the same run continuing, so its halt readouts must judge the whole
208
+ * chain: without this the trend restarts at this leg's first close and can report
209
+ * `flat` — "revise the goal" — on a run that was converging when its allowance ran
210
+ * out. Baselines only (src/trend.js's THE SEED): it spends none of this leg's own
211
+ * bounds and mints none of its strikes. Empty/omitted is the cold path.
212
+ * @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
213
+ * replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
214
+ * `restart.replanGrantUsed`), seeding the ceiling below. `resumeGrades`' sibling and
215
+ * deliberately NOT its twin: a grade seed feeds a READOUT, this one feeds a BOUND.
216
+ *
217
+ * Without it the ceiling is reborn on every call, and a resume is another call:
218
+ * measured, a leg that spent both replans and step-redded handed its checkpoint to a
219
+ * second leg that replanned twice more. Two became four by being killed once, which is
220
+ * the creep the latch exists to make impossible ("unlimited replanning launders thrash
221
+ * as adaptation", PRD v1.12).
222
+ *
223
+ * WHY THIS SEED SPENDS THE LEG'S BOUND WHERE `resumeGrades` DOES NOT. src/trend.js's
224
+ * THE SEED refuses to seed the fix loop's ITERATIONS from history "because the leg's
225
+ * own bounds must not be spent by history" — and that is right, because an attempt
226
+ * allowance is a LEG bound: a restarted leg buys its own attempts with its own money.
227
+ * The replan ceiling is a RUN bound by doctrine. It bounds how many times the WORKFLOW
228
+ * may be redrawn before redrawing it is thrash, and that question spans exactly the
229
+ * chain the halt readout already spans. The two seeds therefore disagree on purpose;
230
+ * they are not the same rule applied inconsistently.
231
+ *
232
+ * CHAIN SEMANTICS, and the one place this differs from the grade seed's mechanism.
233
+ * `readGradeSeed`'s documented KNOWN LIMIT is that it reads ONE spine, so a resume of a
234
+ * resume inherits leg 2's grades and not leg 1's — the chain shortens by one leg per
235
+ * resume, which is fail-safe for a READOUT (a shorter chain can only under-claim a
236
+ * direction). It is the DANGEROUS direction for a ceiling: an under-claimed ledger is a
237
+ * refilled allowance, and every kill would buy one. So this seed does NOT reuse that
238
+ * mechanism. It follows the MONEY fold instead (`priorSpentUsd`, `try-start`/
239
+ * `job-start`): each leg DECLARES the ledger it inherited on its own spine, and the
240
+ * next reader adds only that leg's own `replan` records to the declared number. Leg 3
241
+ * therefore inherits the whole chain, not leg 2's slice.
242
+ *
243
+ * `count` seeds `replans` (and, above zero, latches `replanned`); `grantUsed` seeds the
244
+ * F85-C variance latch, so a killed leg cannot re-earn an extra the arbiter has already
245
+ * granted. Null/omitted is the cold path and is byte-identical to a run nobody resumed.
246
+ * @param {() => boolean} [opts.spendComplete] is the run's spend EXACT, or a floor? The
247
+ * ledger one level up owns the answer (unpriced rounds, a self-healed stall, a mid-call
248
+ * cut, an inherited resume floor); the money-halt readout states it beside the remaining
249
+ * it quotes, because `budget − floor` is a CEILING and printing it bare reads as exact
250
+ * (F6). Defaults to exact, which is what every pre-record caller was implicitly saying.
66
251
  * @param {() => number} [opts.now] the wall clock's time source, injected. The real clock is the
67
252
  * default; a caller supplies this to drive time deterministically (the same seam `createClock`
68
253
  * already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
69
254
  * whose floor is one close timeout).
70
255
  * @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>`
256
+ * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
257
+ * 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' |
258
+ * `step-red:<id>`
73
259
  */
74
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, now }: {
260
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans }: {
75
261
  workdir: string;
76
262
  provider: any;
77
263
  nativeProvider?: ((o: {
@@ -85,10 +271,23 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
85
271
  isUnpriced?: (() => boolean) | undefined;
86
272
  providerFor?: ((tier: string) => any) | undefined;
87
273
  capRuns?: number | undefined;
274
+ strikeLimit?: number | undefined;
88
275
  closeTimeoutMs?: number | undefined;
89
276
  maxStepRounds?: number | undefined;
90
277
  scoutRounds?: number | undefined;
91
278
  layerRoot?: boolean | undefined;
279
+ bridge?: unknown;
280
+ priorWallMs?: number | undefined;
281
+ resumeSeed?: any;
282
+ resumeGrades?: {
283
+ stage?: string | null;
284
+ value?: number | null;
285
+ }[] | undefined;
286
+ resumeReplans?: {
287
+ count?: number;
288
+ grantUsed?: boolean;
289
+ } | null | undefined;
290
+ spendComplete?: (() => boolean) | undefined;
92
291
  now?: (() => number) | undefined;
93
292
  }): Promise<string>;
94
293
  export type CategorizedError = Error & {
package/types/ralph.d.ts CHANGED
@@ -111,6 +111,42 @@ export function runStages(stages: any[], redact?: (s: string) => string, { timeo
111
111
  timeoutMs?: number;
112
112
  cwd?: string;
113
113
  }): Promise<any>;
114
+ /**
115
+ * Bound an over-long close stream to a head sample + elided middle + tail. When
116
+ * `keepPattern` is set (F28), lines of the WHOLE stream matching it are also
117
+ * carried in a clearly-delimited block between head and tail — capped — so the
118
+ * failing-test names buried in the middle still reach the worker.
119
+ * Exported for the ONE reason `GAP_KEEP_TRIM_MARKER` above is: a second consumer
120
+ * that needs a gap bounded must reuse THIS envelope, not spell its own. A private
121
+ * copy would be a second truncation scheme, and the two would drift on exactly the
122
+ * property that matters — whether the red lines survive, and whether the trim
123
+ * announces itself (F28: silent truncation is the disease this function cures).
124
+ * The close path is unchanged: `runClose` still calls it with the stage's own
125
+ * `gapKeep`, byte-identically.
126
+ * @param {string} s the (redacted) close output
127
+ * @param {string} [keepPattern] regex source; matching lines survive the elision
128
+ */
129
+ export function boundGap(s: string, keepPattern?: string): string;
130
+ /**
131
+ * The exhaustion-by-PROGRESS seam, as a contract rather than "whatever one module
132
+ * happens to return". Two governors implement it and they read different signals —
133
+ * the step ladder's repeats-and-writes (src/ladder.js) and the close-fix loop's
134
+ * per-stage graded numbers (src/trend.js) — which is the whole reason ralph takes an
135
+ * interface here and interprets nothing itself.
136
+ *
137
+ * `record` returns the governor's own reading, which ralph emits verbatim and never
138
+ * inspects; each reading names its `governor` so two instruments under one spine type
139
+ * can never be averaged into one number. `report` is the only shape ralph itself
140
+ * reads. `terminal` is optional and overrides the exhaustion PROSE only — never the
141
+ * category, never the outcome.
142
+ *
143
+ * @typedef {object} LoopGovernor
144
+ * @property {(o: {iteration: number, gap?: string}) => object} record
145
+ * @property {() => boolean} struckOut
146
+ * @property {() => {strikes: number, limit: number, iterations: number, distinctGaps?: number}} report
147
+ * @property {() => string} brief
148
+ * @property {() => {decision: string, options: string[]}} [terminal]
149
+ */
114
150
  /**
115
151
  * The loop: `while close-red and under-cap: run the middle`. Stops at first
116
152
  * green (within-run tuning past a visible close is the fit-to-pass surface,
@@ -136,7 +172,28 @@ export function runStages(stages: any[], redact?: (s: string) => string, { timeo
136
172
  * zone, worker-crash routing (F32), and the cap taxonomy apply unchanged.
137
173
  * Injected by run.js only; inexpressible in any config or plan — the agent
138
174
  * never authors its judge any more than its close.
139
- * @param {number} opts.capRuns budget: max middle runs
175
+ * @param {number} [opts.capRuns] budget: max middle runs. Required UNLESS `ladder`
176
+ * is supplied — the two exhaustion rules are alternatives, never both at once.
177
+ * The "unless" is ENFORCED at entry (a param guard that throws), not merely
178
+ * documented: an optional-in-the-contract bound with nothing checking it made a
179
+ * loop that never ran report a governance stop that never happened.
180
+ * @param {LoopGovernor} [opts.ladder]
181
+ * the PROGRESS governor (src/ladder.js) that replaces the fixed count for a plan
182
+ * STEP's micro-loop. With it, the loop is bounded by strikes rather than by a
183
+ * number of runs: iterations float, and money, the wall, the variance meter and
184
+ * the stall fuse keep every bit of their current authority (each still ends the
185
+ * loop through the middle's own throw). Ralph still interprets nothing — it hands
186
+ * the ladder the iteration's gap and reads back a boolean.
187
+ *
188
+ * The CLOSE-FIX loop now supplies one too (PRD v1.46 §4, once its own $0 replay
189
+ * over all 8 archived fix loops came back clean: 0 greens harmed, 1 waste case
190
+ * caught). Its governor reads a different signal — the close's own per-stage
191
+ * graded numbers (src/trend.js), not repeats and writes — which is exactly why
192
+ * this is a SEAM and not a hard-coded rule: ralph still interprets nothing.
193
+ * An optional `terminal()` on the governor overrides the exhaustion prose, because
194
+ * the step ladder's copy offers the human a replan and there is no planner at the
195
+ * close. The category and the outcome are NOT overridable: one exhaustion terminal,
196
+ * two triggers.
140
197
  * @param {(type: string, data?: object) => object} opts.emit a spine emitter
141
198
  * @param {(s: string) => string} [opts.redact] source scrubber for close output
142
199
  * (secrets never enter the spine); injected so the shell stays stdlib-only
@@ -155,7 +212,7 @@ export function runStages(stages: any[], redact?: (s: string) => string, { timeo
155
212
  * with zero writes, a crash stays what it always was: an instrument stop.
156
213
  * @returns {Promise<'green'|'escalated'>}
157
214
  */
158
- export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
215
+ export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
159
216
  middle: (iteration: number, gap?: string) => void | Promise<void>;
160
217
  close?: string[] | undefined;
161
218
  judge?: (() => Promise<{
@@ -167,7 +224,8 @@ export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeou
167
224
  gap?: string;
168
225
  detail?: string;
169
226
  }) | undefined;
170
- capRuns: number;
227
+ capRuns?: number | undefined;
228
+ ladder?: LoopGovernor | undefined;
171
229
  emit: (type: string, data?: object) => object;
172
230
  redact?: ((s: string) => string) | undefined;
173
231
  closeTimeoutMs?: number | undefined;
@@ -194,7 +252,12 @@ export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeou
194
252
  * Two rules, deliberately kept BOTH ways round:
195
253
  * - `names` / `prefixes` — the knowns. A literal denylist is a chase (every
196
254
  * 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`).
255
+ * it exists for names the shape rule STRUCTURALLY cannot see — either no
256
+ * `_`-bounded credential suffix at all (`AWS_ACCESS_KEY_ID`, `PGPASSWORD`,
257
+ * `MYSQL_PWD`), a `*_KEY` that is not `*_API_KEY` (`SSH_PRIVATE_KEY`), or a
258
+ * BARE spelling with nothing in front of the suffix (`TOKEN`, `SECRET`).
259
+ * Names are matched EXACTLY, which is what keeps connection config out of
260
+ * the strip (`PGHOST`, `SECRETS_DIR`, `TOKENIZERS_PARALLELISM` all survive).
198
261
  * - `shape` — the NAME-SHAPE rule, which is what bounds the chase: anything
199
262
  * whose name ends in the credential suffixes goes, whoever minted it and
200
263
  * whether or not anyone here has heard of it. Case-insensitive; no `g` flag,
@@ -228,3 +291,34 @@ export const CLOSE_FAULTS: Record<string, {
228
291
  decision: string;
229
292
  options: string[];
230
293
  }>;
294
+ /**
295
+ * The exhaustion-by-PROGRESS seam, as a contract rather than "whatever one module
296
+ * happens to return". Two governors implement it and they read different signals —
297
+ * the step ladder's repeats-and-writes (src/ladder.js) and the close-fix loop's
298
+ * per-stage graded numbers (src/trend.js) — which is the whole reason ralph takes an
299
+ * interface here and interprets nothing itself.
300
+ *
301
+ * `record` returns the governor's own reading, which ralph emits verbatim and never
302
+ * inspects; each reading names its `governor` so two instruments under one spine type
303
+ * can never be averaged into one number. `report` is the only shape ralph itself
304
+ * reads. `terminal` is optional and overrides the exhaustion PROSE only — never the
305
+ * category, never the outcome.
306
+ */
307
+ export type LoopGovernor = {
308
+ record: (o: {
309
+ iteration: number;
310
+ gap?: string;
311
+ }) => object;
312
+ struckOut: () => boolean;
313
+ report: () => {
314
+ strikes: number;
315
+ limit: number;
316
+ iterations: number;
317
+ distinctGaps?: number;
318
+ };
319
+ brief: () => string;
320
+ terminal?: (() => {
321
+ decision: string;
322
+ options: string[];
323
+ }) | undefined;
324
+ };