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/CHANGELOG.md +704 -0
- package/README.md +13 -4
- package/bareloop.context.md +351 -32
- package/package.json +1 -1
- package/src/authorflow.js +1280 -0
- package/src/authoring.js +1296 -0
- package/src/authorjob.js +793 -0
- package/src/authorscout.js +706 -0
- package/src/bridges.js +2 -2
- package/src/declaredclose.js +519 -0
- package/src/exits.js +8 -2
- package/src/index.js +72 -0
- package/src/job.js +91 -10
- package/src/kinds.js +1354 -0
- package/src/ledger.js +70 -9
- package/src/plan.js +57 -101
- package/src/planrun.js +554 -75
- package/src/ralph.js +27 -5
- package/src/reuse.js +135 -11
- package/src/root.js +8 -1
- package/src/run.js +29 -3
- package/src/text.js +120 -0
- package/src/validate.js +121 -0
- package/src/workbranch.js +261 -0
- package/types/authorflow.d.ts +720 -0
- package/types/authoring.d.ts +447 -0
- package/types/authorjob.d.ts +241 -0
- package/types/authorscout.d.ts +307 -0
- package/types/declaredclose.d.ts +174 -0
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +9 -1
- package/types/job.d.ts +14 -2
- package/types/kinds.d.ts +343 -0
- package/types/plan.d.ts +41 -38
- package/types/planrun.d.ts +125 -18
- package/types/ralph.d.ts +17 -0
- package/types/reuse.d.ts +5 -1
- package/types/run.d.ts +23 -2
- package/types/text.d.ts +84 -0
- package/types/validate.d.ts +50 -0
- package/types/workbranch.d.ts +102 -0
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 };
|
package/types/planrun.d.ts
CHANGED
|
@@ -1,21 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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' |
|
|
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:";
|
package/types/validate.d.ts
CHANGED
|
@@ -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;
|