bareloop 0.7.0 → 0.9.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/plan.d.ts CHANGED
@@ -24,42 +24,6 @@
24
24
  * @returns {string[]} the offerable scopes, deduped, shallowest-first, capped
25
25
  */
26
26
  export function legalScopes(writeScope: string[], dirs?: string[], cap?: number): string[];
27
- /**
28
- * F49 — a heuristic reject for the catastrophic-backtracking footgun: an
29
- * unbounded quantifier applied to a group whose body itself repeats unboundedly
30
- * (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
31
- * group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
32
- * and is caught by propagating the inner repeat up through the wrapper. Such a
33
- * pattern can hang `RegExp.test` for
34
- * seconds on a short crafted body, and `evalExits` runs the agent-authored
35
- * `artifact-written` pattern with NO timeout. This is self-DoS only — the agent
36
- * authors both the pattern and (via the worker) the artifact, so a hang burns
37
- * only its own run's wall-clock; there is NO arbiter compromise (it cannot
38
- * escape the fence, forge a green, or leak a secret). So the reject targets the
39
- * dominant exponential class and fails it as a mechanical gap at the validation
40
- * gate, before any tokens burn. A full ReDoS analyzer needs a real regex engine
41
- * (an external native dep we do not take for a LOW issue); exotic
42
- * overlapping-alternation blowup is out of scope by decision.
43
- *
44
- * SCOPE IS ASYMMETRIC, both directions named on purpose:
45
- * - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
46
- * detected — out of scope (self-DoS only, no arbiter compromise).
47
- * - false POSITIVE: a group whose repetitions are disambiguated by a literal
48
- * anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
49
- * though a real engine runs it linearly — the scan sees the nested-quantifier
50
- * SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
51
- * never admits an unsafe pattern), and the cost is bounded: the plan drafter
52
- * gets a mechanical gap and rewrites (drop the outer `+`, or match once).
53
- * Detecting "safe because anchored" needs the same real engine we declined,
54
- * and guessing it wrong would ADMIT an exponential pattern — so the shape
55
- * reject stands, and the over-rejection is a named, accepted limitation.
56
- *
57
- * The input is guaranteed to compile as a RegExp (checked first by the caller),
58
- * so this scan assumes valid, balanced JS regex syntax.
59
- * @param {string} src a compiled regex source string
60
- * @returns {boolean} true iff a nested unbounded quantifier is present
61
- */
62
- export function hasNestedQuantifier(src: string): boolean;
63
27
  /**
64
28
  * The close, as the ordered STAGE LIST the plan flow actually executes (PRD
65
29
  * v1.28). A staged close is already that list; the legacy object form is the
@@ -84,6 +48,24 @@ export function hasNestedQuantifier(src: string): boolean;
84
48
  * @returns {any[]|null} the stage list, or null when the close names no command
85
49
  */
86
50
  export function stageClose(close: any): any[] | null;
51
+ /**
52
+ * THE ONE STAGING, one level up: a job's close stages, from EITHER of the two
53
+ * fields a spec may carry.
54
+ *
55
+ * `stageClose` reads a command close; `declaredStages` reads an authored
56
+ * declaration (M4). A spec declares exactly one of them — the job validator reds
57
+ * `close-duplicated` on both — so this is a selection, never a merge.
58
+ *
59
+ * Every consumer calls THIS rather than `stageClose` directly, for the reason
60
+ * W4 wrote `stageClose` in the first place: the drafting prompt, the plan
61
+ * validator, the runner, the bridge registry and the reuse envelope must all see
62
+ * the same stage list, and a consumer that reads only `close` sees a declared
63
+ * job as having no close at all — an empty check menu offered to a drafter whose
64
+ * plan the validator would then red for referencing a stage nobody offered.
65
+ * @param {any} job the job spec, any shape
66
+ * @returns {any[]|null} the stage list, or null when the job names no close
67
+ */
68
+ export function closeStagesOf(job: any): any[] | null;
87
69
  /** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red */
88
70
  /**
89
71
  * Validate an agent-authored plan (`schema: "plan-v1"`) against the SIGNED
@@ -135,7 +117,6 @@ export const MAX_EXITS_PER_STEP: 2;
135
117
  /** plan size bound — per-step rounds bound the spend, but an unbounded step
136
118
  * COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
137
119
  export const MAX_PLAN_STEPS: 8;
138
- export { WRITE_VERBS };
139
120
  /** scope-menu bound: the menu is a PROMPT ingredient on every draft and
140
121
  * redraft, so an unbounded repo would price the drafting call by its directory
141
122
  * count. Shallow entries survive the cap (see `legalScopes`) because they are
@@ -145,7 +126,27 @@ export const MAX_SCOPE_MENU: 24;
145
126
  * construction — the agent picks a tier, never names an arbitrary model string
146
127
  * (the tier→model mapping and any effort params are the RUNNER's, per provider).
147
128
  * opus is deliberately absent: reserved for fresh builds hamr explicitly assigns,
148
- * never plan-selectable. */
129
+ * never plan-selectable.
130
+ *
131
+ * 2026-08-06 — haiku dropped, on hamr's order: "haiku should be dropped to see if
132
+ * it passes, if it's agent poor work or harness, like always." This is a REVERSIBLE
133
+ * ATTRIBUTION PROBE, not a finding that haiku is incapable. What the $0 archive read
134
+ * (186 spines / 71 accepted plans / 255 steps) actually shows is a CONFOUND, in both
135
+ * directions: only 20 steps ever declared a tier at all — haiku 12 (9 of them on a
136
+ * REPLANNED plan, 6 the LAST step of their plan, bounds 3–12 rounds, 2 ever green)
137
+ * against explicitly-declared sonnet 8 (bounds 14–32, 5 green). The planner picks
138
+ * haiku for steps it judges mechanical, and replanned steps sit on runs already in
139
+ * trouble, so neither column is a controlled read. All three bareagent-u runs of
140
+ * 2026-08-06 tiered the REPLANNED step down (haiku/6, haiku/8, haiku/12) and all
141
+ * three failed, against sonnet initial steps at 30–40 rounds — a confound sitting
142
+ * on the programme's most recent capability read. Removing the tier is how it gets
143
+ * attributed: with only one tier expressible, a still-failing replan is agent or
144
+ * harness, not the tier.
145
+ *
146
+ * The `model` FIELD and its validator branch stay: a one-entry menu means a plan (or
147
+ * a stored bridge) declaring `model:"sonnet"` still validates, and restoring haiku is
148
+ * a one-token edit. This narrows only what the AGENT may express — `--model haiku`
149
+ * remains the OPERATOR's probe knob (PRD v1.36), untouched in the runner scripts. */
149
150
  export const STEP_MODELS: readonly string[];
150
151
  export type Red = {
151
152
  code: string;
@@ -154,3 +155,5 @@ export type Red = {
154
155
  verb?: string;
155
156
  };
156
157
  import { WRITE_VERBS } from './job.js';
158
+ import { hasNestedQuantifier } from './validate.js';
159
+ export { WRITE_VERBS, hasNestedQuantifier };
@@ -1,21 +1,83 @@
1
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.
2
+ * This step's own last EXIT results, rendered for the REPLAN brief.
13
3
  *
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
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
17
79
  */
18
- export function gapFilesNeverWritten(gap: string | undefined, writes: string[] | undefined): string[];
80
+ export function closeGapBlock(gap: string | null | undefined): string;
19
81
  /**
20
82
  * The plan-drafting prompt: a schema DESCRIPTION built from the live validator
21
83
  * menus — never a copyable example (the drafter must author, not echo; the
@@ -147,6 +209,40 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
147
209
  * `flat` — "revise the goal" — on a run that was converging when its allowance ran
148
210
  * out. Baselines only (src/trend.js's THE SEED): it spends none of this leg's own
149
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.
150
246
  * @param {() => boolean} [opts.spendComplete] is the run's spend EXACT, or a floor? The
151
247
  * ledger one level up owns the answer (unpriced rounds, a self-healed stall, a mid-call
152
248
  * cut, an inherited resume floor); the money-halt readout states it beside the remaining
@@ -156,12 +252,18 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
156
252
  * default; a caller supplies this to drive time deterministically (the same seam `createClock`
157
253
  * already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
158
254
  * whose floor is one close timeout).
255
+ * @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed leg
256
+ * recorded on its own spine (`readResume`'s `restart.branch`, off the `work-branch`
257
+ * event). With it the resume returns to the branch its own work is sitting on; without
258
+ * it the deterministic name would collide with that very branch and the collision walk
259
+ * would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
260
+ * path. A recorded branch that no longer exists is a STOP, never a fresh start.
159
261
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
160
262
  * '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>`
263
+ * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
264
+ * 'step-stalled' | `step-red:<id>`
163
265
  */
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 }: {
266
+ 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, resumeBranch }: {
165
267
  workdir: string;
166
268
  provider: any;
167
269
  nativeProvider?: ((o: {
@@ -187,8 +289,13 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
187
289
  stage?: string | null;
188
290
  value?: number | null;
189
291
  }[] | undefined;
292
+ resumeReplans?: {
293
+ count?: number;
294
+ grantUsed?: boolean;
295
+ } | null | undefined;
190
296
  spendComplete?: (() => boolean) | undefined;
191
297
  now?: (() => number) | undefined;
298
+ resumeBranch?: string | null | undefined;
192
299
  }): Promise<string>;
193
300
  export type CategorizedError = Error & {
194
301
  category?: string;
package/types/ralph.d.ts CHANGED
@@ -111,6 +111,22 @@ 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;
114
130
  /**
115
131
  * The exhaustion-by-PROGRESS seam, as a contract rather than "whatever one module
116
132
  * happens to return". Two governors implement it and they read different signals —
@@ -256,6 +272,7 @@ export const CLOSE_ENV_DENY: Readonly<{
256
272
  prefixes: readonly string[];
257
273
  shape: RegExp;
258
274
  }>;
275
+ export function stageGap(name: string, gap: string): string;
259
276
  export const GAP_KEEP_TRIM_MARKER: "more elided by the";
260
277
  /**
261
278
  * The forbidden zone, named (PRD v1.11). Each row is an outcome in which the
package/types/reuse.d.ts CHANGED
@@ -174,7 +174,11 @@ export function selectBridge({ registry, job, ask, provider, pinned, shortlist,
174
174
  * **The declared fold, not a re-derivation.** A restarted try's own `try-start`
175
175
  * carries the fold it inherited (`priorSpentUsd`/`priorWallMs`), so a resume OF a
176
176
  * resume adds only its own new rounds. Re-deriving from the whole file would bill an
177
- * abandoned attempt twice.
177
+ * abandoned attempt twice. The REPLAN ledger (`priorReplans`/`priorReplanGrantUsed`)
178
+ * folds on this same mechanism and deliberately not on `readGradeSeed`'s: a grade seed
179
+ * feeds a readout, so its documented one-leg-per-resume shortening can only under-claim
180
+ * a direction, while an under-claimed CEILING is a refilled allowance and every kill
181
+ * would buy one (PRD v1.12 — the replan bound is the RUN's).
178
182
  *
179
183
  * **KNOWN LIMIT, stated rather than discovered later.** A call that was killed BEFORE it
180
184
  * returned left no event at all — no `worker-round`, no `selection-result` — so money it
package/types/run.d.ts CHANGED
@@ -74,6 +74,22 @@
74
74
  * chain, not just the leg: without them a leg that re-grades an unchanged tree reports
75
75
  * `flat` — "revise the goal" — on a run that was converging when the money ran out.
76
76
  * Counts and stage names only; no close bytes cross this seam. Absent is the cold path.
77
+ * @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
78
+ * replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
79
+ * `restart.replanGrantUsed`), forwarded to the plan flow, which seeds its ceiling with
80
+ * it. Its sibling above feeds a READOUT; this one feeds a BOUND, and that is the whole
81
+ * difference: `replanned`/`varianceGrantUsed` are locals in `runPlan`, so without this
82
+ * every kill hands the next leg a fresh allowance of an allowance PRD v1.12 makes the
83
+ * RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
84
+ * run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
85
+ * already sets, so a resume OF a resume folds once. Absent is the cold path.
86
+ * @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed attempt
87
+ * created and recorded on its own spine (`readResume`'s `restart.branch`). The branch
88
+ * name is DERIVED from the signed spec and is therefore identical on every leg, so
89
+ * without this the restart would collide with its own predecessor's branch and the
90
+ * collision walk would mint a `-2` beside the work the resume exists to continue. The
91
+ * fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
92
+ * verbatim to the plan flow; absent is the cold path.
77
93
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
78
94
  * territory, threaded to the plan flow. Defaults OFF
79
95
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -83,10 +99,10 @@
83
99
  * binding.)
84
100
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
85
101
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
86
- * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
102
+ * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
87
103
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
88
104
  */
89
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades }: {
105
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch }: {
90
106
  approvals: unknown;
91
107
  workdir: string;
92
108
  provider: any;
@@ -111,6 +127,11 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
111
127
  stage?: string | null;
112
128
  value?: number | null;
113
129
  }[] | undefined;
130
+ resumeReplans?: {
131
+ count?: number;
132
+ grantUsed?: boolean;
133
+ } | null | undefined;
134
+ resumeBranch?: string | null | undefined;
114
135
  layerRoot?: boolean | undefined;
115
136
  }): Promise<string>;
116
137
  export type Red = {
package/types/text.d.ts CHANGED
@@ -33,3 +33,87 @@ export function priceOf(r: any): {
33
33
  costUsd: number | null;
34
34
  unpricedRounds: number;
35
35
  };
36
+ /**
37
+ * ONE raw model emission, ready to persist.
38
+ *
39
+ * Three things happen here and NONE of them is optional at a call site:
40
+ * - the text goes through the ONE redactor (`redactSecrets`, over the ONE
41
+ * `SECRET_PATTERNS` inventory). A raw is the single most likely record in
42
+ * this system to carry a live credential: it is whatever a model read out
43
+ * of a repository, verbatim, and it lands in a file that outlives the run;
44
+ * - it is BOUNDED, and the bound ANNOUNCES itself with the full size (F28 —
45
+ * a bound eats detail, never evidence). The cut is decoded rather than
46
+ * sliced so a multi-byte character is never split in half;
47
+ * - it carries its own address: which call produced it (`label`, the same
48
+ * spelling the cost book meters under) and which attempt (`attempt`), plus
49
+ * the typed failure that describes it when there is one.
50
+ *
51
+ * `cause`/`reason` are `null` when the emission was fine — an ABSENT failure is
52
+ * absent, never an empty string that reads like a diagnosis nobody wrote.
53
+ * @param {{label: string, attempt: number, text: unknown, cause?: string|null,
54
+ * reason?: string|null, cap?: number}} o
55
+ * @returns {{label: string, attempt: number, bytes: number, trimmed: boolean,
56
+ * text: string, cause: string|null, reason: string|null}}
57
+ */
58
+ export function scrubRaw({ label, attempt, text, cause, reason, cap }: {
59
+ label: string;
60
+ attempt: number;
61
+ text: unknown;
62
+ cause?: string | null;
63
+ reason?: string | null;
64
+ cap?: number;
65
+ }): {
66
+ label: string;
67
+ attempt: number;
68
+ bytes: number;
69
+ trimmed: boolean;
70
+ text: string;
71
+ cause: string | null;
72
+ reason: string | null;
73
+ };
74
+ /**
75
+ * Stamp a persisted raw with the verdict that DESCRIBES it — through the same
76
+ * boundary the raw's own text rode.
77
+ *
78
+ * It exists because the verdict is not known when the raw is created: a writer
79
+ * records the emission the moment the call returns (so a metered call can never
80
+ * be missing its output), and only classifies it afterwards. The obvious
81
+ * shape — assigning `cause`/`reason` onto the stored object at that later
82
+ * point — writes an UNSCRUBBED string into a record that is already past the
83
+ * one redactor, and `reason` is the worst field to do it to: it quotes the
84
+ * transport's own error prose, which nobody in this repo wrote and nobody
85
+ * bounds. So the stamp is a function, not an assignment, and `scrubRaw` renders
86
+ * its own two fields through it: ONE place decides how a diagnosis becomes
87
+ * persistable, exactly as ONE place decides it for the text.
88
+ *
89
+ * `cause` is enumerated (`SURVEY_CAUSES`) and passes through untouched; a mask
90
+ * over a closed set would only hide a bug in the set.
91
+ * @template {{cause: string|null, reason: string|null}} T
92
+ * @param {T} raw @param {{cause?: string|null, reason?: string|null}} o @returns {T}
93
+ */
94
+ export function stampRaw<T extends {
95
+ cause: string | null;
96
+ reason: string | null;
97
+ }>(raw: T, { cause, reason }: {
98
+ cause?: string | null;
99
+ reason?: string | null;
100
+ }): T;
101
+ /**
102
+ * The ceiling on ONE persisted raw. Sized to the survey's own prompt-ingredient
103
+ * cap (`AUTHOR_SCOUT_BLOB_MAX`, 8000) rather than picked: the largest raw this
104
+ * repository produces is a survey, the archive's real surveys ran 5991-8056
105
+ * bytes, and a record that cannot hold one whole specimen cannot answer the
106
+ * question it exists for. The two constants are deliberately independent
107
+ * numbers — one bounds a PROMPT, this one bounds a RECORD.
108
+ */
109
+ export const RAW_PERSIST_MAX: 8000;
110
+ /**
111
+ * The announced-trim marker for a persisted raw. A NEW spelling rather than a
112
+ * reuse of `GAP_TRIM_MARKER` ('gap trimmed:'), and that is a decision, not
113
+ * decoration: `src/root.js` READS `GAP_TRIM_MARKER` to mean *this gap was cut,
114
+ * so Layer R must not compare it*, so a second producer of those bytes in a
115
+ * different population is exactly the F90 hazard — a reader must know every
116
+ * producer of its marker. Nothing reads this one yet; it exists so the trim
117
+ * announces itself (F28) and so a future reader has an unambiguous key.
118
+ */
119
+ export const RAW_TRIM_MARKER: "raw trimmed:";
@@ -62,4 +62,54 @@ export function redactSecrets(raw: unknown): string;
62
62
  * @param {(code: string, path: string, detail?: string) => void} red
63
63
  */
64
64
  export function sweepSecretLiterals(root: any, red: (code: string, path: string, detail?: string) => void): void;
65
+ /**
66
+ * F49 — a heuristic reject for the catastrophic-backtracking footgun: an
67
+ * unbounded quantifier applied to a group whose body itself repeats unboundedly
68
+ * (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
69
+ * group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
70
+ * and is caught by propagating the inner repeat up through the wrapper. Such a
71
+ * pattern can hang `RegExp.test` for seconds on a short crafted body, and THREE
72
+ * fields feed it to an evaluator with NO timeout — one per document:
73
+ * - the AGENT's `artifact-written.pattern`, run by `evalExits` (F49, the
74
+ * original site). Self-DoS only: the agent authors both the pattern and
75
+ * (via the worker) the artifact, so a hang burns only its own run's
76
+ * wall-clock, and there is no arbiter compromise — it cannot escape the
77
+ * fence, forge a green, or leak a secret.
78
+ * - the OPERATOR's `judged.pattern` (`runClose`) and `gapKeep` (`boundGap`),
79
+ * compiled and run against close output in BARELOOP'S OWN PROCESS. These
80
+ * are worse than an operator self-DoS, and the reason is F67's lesson: the
81
+ * regex blocks the MAIN EVENT LOOP, so the in-process stall fuse — a timer
82
+ * in that same loop — cannot fire. The run dies to the OUTSIDE watchdog and
83
+ * is read as a model stall, so a bad regex in a SIGNED spec presents as a
84
+ * provider problem. MEASURED: `boundGap(stream, '(a+)+$')` on a 40-char
85
+ * body does not finish in 20s.
86
+ * So the reject targets the dominant exponential class and fails it as a
87
+ * mechanical gap at the validation gate, before any tokens burn. A full ReDoS
88
+ * analyzer needs a real regex engine (an external native dep we do not take for
89
+ * a LOW issue); exotic overlapping-alternation blowup is out of scope by
90
+ * decision (PARKED to the close-authoring rung — PRD v1.55).
91
+ *
92
+ * SCOPE IS ASYMMETRIC, both directions named on purpose:
93
+ * - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
94
+ * detected — there is no inner QUANTIFIER to find, so the shape scan cannot
95
+ * see it; the blowup comes from overlapping alternation BRANCHES under a
96
+ * repeat. A genuine limit of the approach, not a bug in it. Widening the
97
+ * detector changes which SIGNED specs are admissible, which is
98
+ * arbiter-adjacent — parked, not chased (PRD v1.55).
99
+ * - false POSITIVE: a group whose repetitions are disambiguated by a literal
100
+ * anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
101
+ * though a real engine runs it linearly — the scan sees the nested-quantifier
102
+ * SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
103
+ * never admits an unsafe pattern), and the cost is bounded: the plan drafter
104
+ * gets a mechanical gap and rewrites (drop the outer `+`, or match once).
105
+ * Detecting "safe because anchored" needs the same real engine we declined,
106
+ * and guessing it wrong would ADMIT an exponential pattern — so the shape
107
+ * reject stands, and the over-rejection is a named, accepted limitation.
108
+ *
109
+ * The input is guaranteed to compile as a RegExp (checked first by the caller),
110
+ * so this scan assumes valid, balanced JS regex syntax.
111
+ * @param {string} src a compiled regex source string
112
+ * @returns {boolean} true iff a nested unbounded quantifier is present
113
+ */
114
+ export function hasNestedQuantifier(src: string): boolean;
65
115
  export const SECRET_PATTERNS: RegExp[];
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The branch name a job's run works on, DERIVED from the signed spec.
3
+ *
4
+ * **The source is `spec.job`, and the choice is load-bearing.** It is the one
5
+ * human-authored, REQUIRED, kebab-case name the spec carries; it is inside the
6
+ * spec hash, so the branch name cannot drift from what the operator signed; and
7
+ * it is stable across a resume and across a cold re-run of the same spec, which
8
+ * is precisely why the collision walk below has to exist. In practice it already
9
+ * reads as tool-and-problem (`bareagent-u-types`, `litectx-u-types`), which is
10
+ * the shape hamr's ruling asks for.
11
+ *
12
+ * **The goal prose was REJECTED as a source**, and not for tidiness: a goal is
13
+ * long, is edited freely between runs without changing what the job IS, and
14
+ * opens on boilerplate ("An existing pytest test suite for the SOAR
15
+ * orchestrator…"). Deriving from it would make the branch name unstable under
16
+ * edits that change nothing, and unreadable when it was stable. A run id was
17
+ * rejected by the ruling itself.
18
+ *
19
+ * Deterministic, pure, and total: any input yields a name matching
20
+ * `WORK_BRANCH_RE`, so a caller never has to check.
21
+ * @param {any} spec a job-v1 spec (validated or raw)
22
+ * @returns {string}
23
+ */
24
+ export function workBranchName(spec: any): string;
25
+ /**
26
+ * Put the run on its OWN branch, before it can touch any code.
27
+ *
28
+ * The three shapes, and each one's honest answer:
29
+ *
30
+ * - **Cold.** `name` is free → create it at HEAD and switch. The handed branch
31
+ * keeps its ref and its history; a new branch at HEAD moves no commit, so this
32
+ * costs the patient nothing and changes nothing it can measure (the declared
33
+ * close's seed is HEAD either way).
34
+ * - **Cold, and the name is taken.** Suffix `-2`, `-3`… until one is free.
35
+ * Deterministic given the repository, and it NEVER reuses another run's
36
+ * branch — a second run of the same spec is a second blast radius.
37
+ * - **Resume.** `resume` names the branch the killed leg recorded on its own
38
+ * spine. It is checked out, never re-created and never suffixed: the work the
39
+ * operator already paid for is sitting on it, and minting `-2` beside it would
40
+ * strand exactly the progress a resume exists to keep. A `resume` branch that
41
+ * is GONE is a STOP, never a silent fresh start — the run would be grading a
42
+ * tree whose history it does not own.
43
+ *
44
+ * **Every failure is a stop with a name.** Not a git repository, no git on PATH,
45
+ * a checkout git refuses, a crowded namespace, a resume branch a human deleted:
46
+ * each comes back as `stop` and `branch: null`. There is no fallback to "just
47
+ * work on whatever was handed to us" anywhere in this function, because that
48
+ * fallback IS the thing the hard rule forbids.
49
+ *
50
+ * A DETACHED head is not a failure. Creating a branch from it is exactly the
51
+ * bounding the rule asks for (and it attaches a head that was floating), so it
52
+ * succeeds with `from: null` — there was no branch to be handed, and the record
53
+ * says so rather than inventing one.
54
+ *
55
+ * @param {string} workdir the patient (the fence's root)
56
+ * @param {object} o
57
+ * @param {string} o.name the derived name (`workBranchName`); refused unless it
58
+ * matches `WORK_BRANCH_RE`, which is what makes `main` unreachable even from a
59
+ * hand-passed argument
60
+ * @param {string|null} [o.resume] RESUME — the branch this run already owns
61
+ * @returns {Promise<{stop: string, branch: null}
62
+ * | {stop: null, branch: string, created: boolean, resumed: boolean, from: string|null, base: string|null,
63
+ * repo: string, collided: number}>}
64
+ */
65
+ export function prepareWorkBranch(workdir: string, { name, resume }: {
66
+ name: string;
67
+ resume?: string | null | undefined;
68
+ }): Promise<{
69
+ stop: string;
70
+ branch: null;
71
+ } | {
72
+ stop: null;
73
+ branch: string;
74
+ created: boolean;
75
+ resumed: boolean;
76
+ from: string | null;
77
+ base: string | null;
78
+ repo: string;
79
+ collided: number;
80
+ }>;
81
+ /** Every work branch carries it. It is what makes `main`, `master` and every
82
+ * branch a human could plausibly have handed the run INEXPRESSIBLE as a work
83
+ * branch name — a structural guarantee rather than a denylist somebody has to
84
+ * keep current. */
85
+ export const WORK_BRANCH_PREFIX: "bareloop-";
86
+ /** The full legal shape of a work branch. Deliberately narrower than git's own
87
+ * `check-ref-format`: the kebab alphabet the job validator already holds every
88
+ * spec slug to (`src/job.js` SLUG_RE), plus the prefix. Everything git forbids —
89
+ * `..`, `@{`, a trailing `.lock`, a leading `-`, a `/` component, control
90
+ * characters — is unreachable inside it, so ref-safety is a property of the
91
+ * alphabet and never a sanitiser somebody has to remember to call. */
92
+ export const WORK_BRANCH_RE: RegExp;
93
+ /** The slug's ceiling, before any collision suffix. A ref component has a
94
+ * filesystem-length limit and a human scanning `git branch` has a much smaller
95
+ * one; 60 characters is longer than every job slug in `jobs/` and short enough
96
+ * to read in a column. */
97
+ export const MAX_SLUG_LEN: 60;
98
+ /** How far the collision walk goes before it STOPS and says so. A run that
99
+ * cannot find a free name in this many attempts is looking at a patient nobody
100
+ * has swept in a very long time, and silently walking to `-500` would be the
101
+ * runner papering over an operator problem. */
102
+ export const MAX_BRANCH_COLLISIONS: 50;