bareloop 0.8.0 → 0.10.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 +625 -0
- package/README.md +101 -110
- package/bareloop.context.md +333 -25
- package/package.json +1 -1
- package/src/authorflow.js +1447 -0
- package/src/authoring.js +1486 -0
- package/src/authorjob.js +801 -0
- package/src/authorscout.js +767 -0
- package/src/bridges.js +16 -3
- package/src/declaredclose.js +519 -0
- package/src/index.js +75 -0
- package/src/job.js +48 -4
- package/src/kinds.js +1445 -0
- package/src/ledger.js +18 -1
- package/src/plan.js +26 -1
- package/src/planrun.js +448 -40
- package/src/ralph.js +24 -4
- package/src/reuse.js +120 -9
- package/src/root.js +8 -1
- package/src/run.js +12 -4
- package/src/text.js +199 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/src/workbranch.js +261 -0
- package/types/authorflow.d.ts +764 -0
- package/types/authoring.d.ts +559 -0
- package/types/authorjob.d.ts +242 -0
- package/types/authorscout.d.ts +320 -0
- package/types/declaredclose.d.ts +174 -0
- package/types/index.d.ts +9 -1
- package/types/job.d.ts +1 -0
- package/types/kinds.d.ts +395 -0
- package/types/plan.d.ts +18 -0
- package/types/planrun.d.ts +53 -3
- package/types/ralph.d.ts +1 -0
- package/types/reuse.d.ts +1 -1
- package/types/run.d.ts +10 -2
- package/types/text.d.ts +151 -0
- package/types/validate.d.ts +74 -9
- package/types/workbranch.d.ts +102 -0
package/types/planrun.d.ts
CHANGED
|
@@ -78,6 +78,42 @@
|
|
|
78
78
|
* @returns {string} the labelled block, or '' when there is nothing to show
|
|
79
79
|
*/
|
|
80
80
|
export function closeGapBlock(gap: string | null | undefined): string;
|
|
81
|
+
/**
|
|
82
|
+
* What the NEXT attempt is told about the bound that cut the previous one.
|
|
83
|
+
*
|
|
84
|
+
* Two different bounds used to set the same bare `attemptBounded = roundIteration`
|
|
85
|
+
* and therefore render the same sentence. For a round-bound cutoff that sentence is
|
|
86
|
+
* true. For BA-11's deny streak it is false twice over — the rounds did NOT run out
|
|
87
|
+
* (the fence short-circuited the attempt with budget left), and the count it quotes
|
|
88
|
+
* is the cap, not what was spent — and it aims the worker at the READ BUDGET when the
|
|
89
|
+
* thing that stopped it was the FENCE. The reason was already recorded on the spine;
|
|
90
|
+
* nothing carried it to the one reader who could act on it.
|
|
91
|
+
*
|
|
92
|
+
* The denial note is MECHANICAL in the strict sense: it quotes the terminal the
|
|
93
|
+
* provider actually returned and names which bound fired. It invents nothing —
|
|
94
|
+
* no streak count and no denied path, because bare-agent's return (`error:
|
|
95
|
+
* 'denied:<tool>'`, loop.js) carries neither, and a number nobody measured is a
|
|
96
|
+
* bug with a confident voice. Every other cause keeps the frozen wording byte for
|
|
97
|
+
* byte: a wall-bounded attempt reads as a round bound exactly as it did before
|
|
98
|
+
* (and is unreachable anyway — the step head refuses to fund an attempt past the
|
|
99
|
+
* deadline, W-2), so this change can only ever speak where it has something true
|
|
100
|
+
* to add.
|
|
101
|
+
*
|
|
102
|
+
* The reason arrives ALREADY SCRUBBED (the capture seam in `mkWorker` runs it
|
|
103
|
+
* through the one `SECRET_PATTERNS` inventory), so this renderer only bounds it —
|
|
104
|
+
* and announces the trim with the repo's ONE marker rather than a second spelling
|
|
105
|
+
* of "this text was cut" (F90.2).
|
|
106
|
+
*
|
|
107
|
+
* @param {{iteration?: number|string|undefined, cause: string, reason: string|null}|null|undefined} bounded
|
|
108
|
+
* the previous attempt's bound state, or nullish when it was not bounded
|
|
109
|
+
* @param {number} rounds the step's round bound, as quoted by the frozen sentence
|
|
110
|
+
* @returns {string|null} the note, or null when there is nothing to say
|
|
111
|
+
*/
|
|
112
|
+
export function boundedNote(bounded: {
|
|
113
|
+
iteration?: number | string | undefined;
|
|
114
|
+
cause: string;
|
|
115
|
+
reason: string | null;
|
|
116
|
+
} | null | undefined, rounds: number): string | null;
|
|
81
117
|
/**
|
|
82
118
|
* The plan-drafting prompt: a schema DESCRIPTION built from the live validator
|
|
83
119
|
* menus — never a copyable example (the drafter must author, not echo; the
|
|
@@ -252,12 +288,18 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
|
|
|
252
288
|
* default; a caller supplies this to drive time deterministically (the same seam `createClock`
|
|
253
289
|
* already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
|
|
254
290
|
* whose floor is one close timeout).
|
|
291
|
+
* @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed leg
|
|
292
|
+
* recorded on its own spine (`readResume`'s `restart.branch`, off the `work-branch`
|
|
293
|
+
* event). With it the resume returns to the branch its own work is sitting on; without
|
|
294
|
+
* it the deterministic name would collide with that very branch and the collision walk
|
|
295
|
+
* would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
|
|
296
|
+
* path. A recorded branch that no longer exists is a STOP, never a fresh start.
|
|
255
297
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
256
298
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
257
|
-
* 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
258
|
-
* `step-red:<id>`
|
|
299
|
+
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
300
|
+
* 'step-stalled' | `step-red:<id>`
|
|
259
301
|
*/
|
|
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 }: {
|
|
302
|
+
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 }: {
|
|
261
303
|
workdir: string;
|
|
262
304
|
provider: any;
|
|
263
305
|
nativeProvider?: ((o: {
|
|
@@ -289,7 +331,15 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
|
|
|
289
331
|
} | null | undefined;
|
|
290
332
|
spendComplete?: (() => boolean) | undefined;
|
|
291
333
|
now?: (() => number) | undefined;
|
|
334
|
+
resumeBranch?: string | null | undefined;
|
|
292
335
|
}): Promise<string>;
|
|
336
|
+
/**
|
|
337
|
+
* The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a
|
|
338
|
+
* provider string (`denied:<tool>` today, ~20 bytes), not a gap, so this is a
|
|
339
|
+
* backstop against a future terminal that arrives long — never an envelope the
|
|
340
|
+
* shipped path routinely rides through.
|
|
341
|
+
*/
|
|
342
|
+
export const BOUND_REASON_MAX: 200;
|
|
293
343
|
export type CategorizedError = Error & {
|
|
294
344
|
category?: string;
|
|
295
345
|
lib?: string;
|
package/types/ralph.d.ts
CHANGED
|
@@ -272,6 +272,7 @@ export const CLOSE_ENV_DENY: Readonly<{
|
|
|
272
272
|
prefixes: readonly string[];
|
|
273
273
|
shape: RegExp;
|
|
274
274
|
}>;
|
|
275
|
+
export function stageGap(name: string, gap: string): string;
|
|
275
276
|
export const GAP_KEEP_TRIM_MARKER: "more elided by the";
|
|
276
277
|
/**
|
|
277
278
|
* The forbidden zone, named (PRD v1.11). Each row is an outcome in which the
|
package/types/reuse.d.ts
CHANGED
|
@@ -221,7 +221,7 @@ export function selectBridge({ registry, job, ask, provider, pinned, shortlist,
|
|
|
221
221
|
* which is later and better evidence than the last event). Defaults to the last
|
|
222
222
|
* event's own timestamp — the last sign of life there is.
|
|
223
223
|
* @returns {any} `{ started, approvalHash, specHash, patient, job, bridgeTries,
|
|
224
|
-
* perTryBudgetUsd, perTryWallMs, ended, endOutcome, greened, completed, tried,
|
|
224
|
+
* perTryBudgetUsd, perTryWallMs, ended, endOutcome, wallDerivedHalt, greened, completed, tried,
|
|
225
225
|
* selectionCostUsd, selectionComplete, strayRounds, r1Missing, restart, deathAtKnown,
|
|
226
226
|
* spentUsd, spendComplete, carrySpentUsd, carrySpendComplete }`
|
|
227
227
|
*/
|
package/types/run.d.ts
CHANGED
|
@@ -83,6 +83,13 @@
|
|
|
83
83
|
* RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
|
|
84
84
|
* run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
|
|
85
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.
|
|
86
93
|
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
87
94
|
* territory, threaded to the plan flow. Defaults OFF
|
|
88
95
|
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
@@ -92,10 +99,10 @@
|
|
|
92
99
|
* binding.)
|
|
93
100
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
94
101
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
95
|
-
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
|
|
102
|
+
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
96
103
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
|
|
97
104
|
*/
|
|
98
|
-
export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans }: {
|
|
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 }: {
|
|
99
106
|
approvals: unknown;
|
|
100
107
|
workdir: string;
|
|
101
108
|
provider: any;
|
|
@@ -124,6 +131,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
|
|
|
124
131
|
count?: number;
|
|
125
132
|
grantUsed?: boolean;
|
|
126
133
|
} | null | undefined;
|
|
134
|
+
resumeBranch?: string | null | undefined;
|
|
127
135
|
layerRoot?: boolean | undefined;
|
|
128
136
|
}): Promise<string>;
|
|
129
137
|
export type Red = {
|
package/types/text.d.ts
CHANGED
|
@@ -33,3 +33,154 @@ export function priceOf(r: any): {
|
|
|
33
33
|
costUsd: number | null;
|
|
34
34
|
unpricedRounds: number;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* The F6 tally over a list of PRICED CALLS — `priceOf`'s per-call read, summed.
|
|
38
|
+
*
|
|
39
|
+
* `knownUsd` carries the priced half explicitly and `costUsd` is the honest
|
|
40
|
+
* total: null the moment anything went unpriced, never the priced half wearing a
|
|
41
|
+
* complete coat. An EMPTY list is a complete $0 and not an unknown — nothing
|
|
42
|
+
* spent is a number somebody can act on.
|
|
43
|
+
* @param {{costUsd: number|null, unpricedRounds?: number}[]|null|undefined} entries
|
|
44
|
+
* @returns {{costUsd: number|null, knownUsd: number, spendComplete: boolean,
|
|
45
|
+
* nullCostCalls: number, unpricedRounds: number}}
|
|
46
|
+
*/
|
|
47
|
+
export function tallyCalls(entries: {
|
|
48
|
+
costUsd: number | null;
|
|
49
|
+
unpricedRounds?: number;
|
|
50
|
+
}[] | null | undefined): {
|
|
51
|
+
costUsd: number | null;
|
|
52
|
+
knownUsd: number;
|
|
53
|
+
spendComplete: boolean;
|
|
54
|
+
nullCostCalls: number;
|
|
55
|
+
unpricedRounds: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* THE ONE MONEY-CEILING PREDICATE, for the same reason `priceOf` above is one:
|
|
59
|
+
* the authoring pipeline meters two populations of paid call in two different
|
|
60
|
+
* accumulators (the survey's `calls[]`, the declaration loop's cost book), and
|
|
61
|
+
* two hand-spelled answers to "am I over the ceiling?" is two instruments.
|
|
62
|
+
*
|
|
63
|
+
* It is asked BETWEEN metered calls and never inside one. A cap that binds
|
|
64
|
+
* mid-call kills the row before it can be graded — F45 measured that as 3 of 7
|
|
65
|
+
* launches lost and $5.82 unreadable.
|
|
66
|
+
*
|
|
67
|
+
* Three answers, and the order between the last two is load-bearing:
|
|
68
|
+
* - NO CEILING → null, always. An absent cap is UNBOUNDED, and unbounded is a
|
|
69
|
+
* visible operator choice, never a defaulted number nobody set (the
|
|
70
|
+
* `maxWallMs` precedent). There is nothing to enforce, so unpriced spend
|
|
71
|
+
* here is merely reported.
|
|
72
|
+
* - KNOWN SPEND AT OR OVER THE CEILING → `cap-halt`, even when the total is
|
|
73
|
+
* unknown. The breach is CERTAIN on the priced half alone — whatever the
|
|
74
|
+
* unpriced remainder turns out to be, it can only be larger — so naming this
|
|
75
|
+
* `pricing-red` would send the operator to fix a meter when the fact is that
|
|
76
|
+
* the money is gone. At-or-over rather than over: the next call is the one
|
|
77
|
+
* that breaches, and it is the one being decided.
|
|
78
|
+
* - SPEND THAT CANNOT BE KNOWN → `pricing-red`. `?? 0` launders unknown into
|
|
79
|
+
* $0 (F6) and a ceiling enforced against a laundered floor is not enforced
|
|
80
|
+
* at all. If the spend cannot be seen the cap cannot govern it, and that is
|
|
81
|
+
* its own stop rather than a silent pass.
|
|
82
|
+
*
|
|
83
|
+
* A MALFORMED ceiling is the one input it refuses rather than answers: `null` and
|
|
84
|
+
* an absent field mean UNBOUNDED, and they are the ONLY way to get there. Anything
|
|
85
|
+
* else that is not a finite number (`'2.50'`, `NaN`, `Infinity`) reading as
|
|
86
|
+
* unbounded would be F6's laundering in its ceiling form — a caller who set a cap
|
|
87
|
+
* getting a paid pipeline with nothing enforcing it, which is the exact collapse
|
|
88
|
+
* the ceiling exists to prevent, wearing the caller's own typo (PRD v1.62). The
|
|
89
|
+
* CLI's `parseCeiling` guards the FLAG; this guards the LIBRARY, and `authorClose`
|
|
90
|
+
* / `runAuthorScout` / `authorCloseForJob` are documented adopter seams. It THROWS
|
|
91
|
+
* rather than clamping, unlike the attempt caps beside it, because those axes have
|
|
92
|
+
* a safe direction to clamp toward and this one does not: every candidate default
|
|
93
|
+
* is either a number nobody set or the unbounded run the caller just declined.
|
|
94
|
+
* @param {{ceilingUsd: number|null|undefined, knownUsd: number, spendComplete: boolean}} o
|
|
95
|
+
* @returns {'cap-halt'|'pricing-red'|null}
|
|
96
|
+
* @throws {Error} when the ceiling is neither absent nor a finite number
|
|
97
|
+
*/
|
|
98
|
+
export function capStop({ ceilingUsd, knownUsd, spendComplete }: {
|
|
99
|
+
ceilingUsd: number | null | undefined;
|
|
100
|
+
knownUsd: number;
|
|
101
|
+
spendComplete: boolean;
|
|
102
|
+
}): "cap-halt" | "pricing-red" | null;
|
|
103
|
+
/**
|
|
104
|
+
* ONE raw model emission, ready to persist.
|
|
105
|
+
*
|
|
106
|
+
* Three things happen here and NONE of them is optional at a call site:
|
|
107
|
+
* - the text goes through the ONE redactor (`redactSecrets`, over the ONE
|
|
108
|
+
* `SECRET_PATTERNS` inventory). A raw is the single most likely record in
|
|
109
|
+
* this system to carry a live credential: it is whatever a model read out
|
|
110
|
+
* of a repository, verbatim, and it lands in a file that outlives the run;
|
|
111
|
+
* - it is BOUNDED, and the bound ANNOUNCES itself with the full size (F28 —
|
|
112
|
+
* a bound eats detail, never evidence). The cut is decoded rather than
|
|
113
|
+
* sliced so a multi-byte character is never split in half;
|
|
114
|
+
* - it carries its own address: which call produced it (`label`, the same
|
|
115
|
+
* spelling the cost book meters under) and which attempt (`attempt`), plus
|
|
116
|
+
* the typed failure that describes it when there is one.
|
|
117
|
+
*
|
|
118
|
+
* `cause`/`reason` are `null` when the emission was fine — an ABSENT failure is
|
|
119
|
+
* absent, never an empty string that reads like a diagnosis nobody wrote.
|
|
120
|
+
* @param {{label: string, attempt: number, text: unknown, cause?: string|null,
|
|
121
|
+
* reason?: string|null, cap?: number}} o
|
|
122
|
+
* @returns {{label: string, attempt: number, bytes: number, trimmed: boolean,
|
|
123
|
+
* text: string, cause: string|null, reason: string|null}}
|
|
124
|
+
*/
|
|
125
|
+
export function scrubRaw({ label, attempt, text, cause, reason, cap }: {
|
|
126
|
+
label: string;
|
|
127
|
+
attempt: number;
|
|
128
|
+
text: unknown;
|
|
129
|
+
cause?: string | null;
|
|
130
|
+
reason?: string | null;
|
|
131
|
+
cap?: number;
|
|
132
|
+
}): {
|
|
133
|
+
label: string;
|
|
134
|
+
attempt: number;
|
|
135
|
+
bytes: number;
|
|
136
|
+
trimmed: boolean;
|
|
137
|
+
text: string;
|
|
138
|
+
cause: string | null;
|
|
139
|
+
reason: string | null;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Stamp a persisted raw with the verdict that DESCRIBES it — through the same
|
|
143
|
+
* boundary the raw's own text rode.
|
|
144
|
+
*
|
|
145
|
+
* It exists because the verdict is not known when the raw is created: a writer
|
|
146
|
+
* records the emission the moment the call returns (so a metered call can never
|
|
147
|
+
* be missing its output), and only classifies it afterwards. The obvious
|
|
148
|
+
* shape — assigning `cause`/`reason` onto the stored object at that later
|
|
149
|
+
* point — writes an UNSCRUBBED string into a record that is already past the
|
|
150
|
+
* one redactor, and `reason` is the worst field to do it to: it quotes the
|
|
151
|
+
* transport's own error prose, which nobody in this repo wrote and nobody
|
|
152
|
+
* bounds. So the stamp is a function, not an assignment, and `scrubRaw` renders
|
|
153
|
+
* its own two fields through it: ONE place decides how a diagnosis becomes
|
|
154
|
+
* persistable, exactly as ONE place decides it for the text.
|
|
155
|
+
*
|
|
156
|
+
* `cause` is enumerated (`SURVEY_CAUSES`) and passes through untouched; a mask
|
|
157
|
+
* over a closed set would only hide a bug in the set.
|
|
158
|
+
* @template {{cause: string|null, reason: string|null}} T
|
|
159
|
+
* @param {T} raw @param {{cause?: string|null, reason?: string|null}} o @returns {T}
|
|
160
|
+
*/
|
|
161
|
+
export function stampRaw<T extends {
|
|
162
|
+
cause: string | null;
|
|
163
|
+
reason: string | null;
|
|
164
|
+
}>(raw: T, { cause, reason }: {
|
|
165
|
+
cause?: string | null;
|
|
166
|
+
reason?: string | null;
|
|
167
|
+
}): T;
|
|
168
|
+
/**
|
|
169
|
+
* The ceiling on ONE persisted raw. Sized to the survey's own prompt-ingredient
|
|
170
|
+
* cap (`AUTHOR_SCOUT_BLOB_MAX`, 8000) rather than picked: the largest raw this
|
|
171
|
+
* repository produces is a survey, the archive's real surveys ran 5991-8056
|
|
172
|
+
* bytes, and a record that cannot hold one whole specimen cannot answer the
|
|
173
|
+
* question it exists for. The two constants are deliberately independent
|
|
174
|
+
* numbers — one bounds a PROMPT, this one bounds a RECORD.
|
|
175
|
+
*/
|
|
176
|
+
export const RAW_PERSIST_MAX: 8000;
|
|
177
|
+
/**
|
|
178
|
+
* The announced-trim marker for a persisted raw. A NEW spelling rather than a
|
|
179
|
+
* reuse of `GAP_TRIM_MARKER` ('gap trimmed:'), and that is a decision, not
|
|
180
|
+
* decoration: `src/root.js` READS `GAP_TRIM_MARKER` to mean *this gap was cut,
|
|
181
|
+
* so Layer R must not compare it*, so a second producer of those bytes in a
|
|
182
|
+
* different population is exactly the F90 hazard — a reader must know every
|
|
183
|
+
* producer of its marker. Nothing reads this one yet; it exists so the trim
|
|
184
|
+
* announces itself (F28) and so a future reader has an unambiguous key.
|
|
185
|
+
*/
|
|
186
|
+
export const RAW_TRIM_MARKER: "raw trimmed:";
|
package/types/validate.d.ts
CHANGED
|
@@ -86,16 +86,40 @@ export function sweepSecretLiterals(root: any, red: (code: string, path: string,
|
|
|
86
86
|
* So the reject targets the dominant exponential class and fails it as a
|
|
87
87
|
* mechanical gap at the validation gate, before any tokens burn. A full ReDoS
|
|
88
88
|
* analyzer needs a real regex engine (an external native dep we do not take for
|
|
89
|
-
* a LOW issue)
|
|
90
|
-
*
|
|
89
|
+
* a LOW issue).
|
|
90
|
+
*
|
|
91
|
+
* SECOND CLASS, added 2026-08-12 — OVERLAPPING ALTERNATION under an unbounded
|
|
92
|
+
* repeat (`(a|aa)+`, `(a|a)+`, `(\d|\d\d)*`). F49 recorded this as a genuine
|
|
93
|
+
* false NEGATIVE of the shape approach and PARKED the widening, because changing
|
|
94
|
+
* the detector changes which SIGNED specs are admissible. It is closed here on
|
|
95
|
+
* measurement, one fresh process per point (a hot RegExp tiers up out of the V8
|
|
96
|
+
* interpreter, and a single-process sweep read a LONGER body as faster —
|
|
97
|
+
* backwards for an exponential, and the harness rather than the regex).
|
|
98
|
+
* `(a|aa)+$` against a run of `a` with one non-matching tail character:
|
|
99
|
+
*
|
|
100
|
+
* 27ch 218ms · 31ch 1,879ms · 35ch 12,082ms · 39ch 52,563ms · 41ch 189,420ms
|
|
101
|
+
*
|
|
102
|
+
* ~6.8× per 4 characters — the Fibonacci decomposition count, φ⁴. `(a|a)+$` (the
|
|
103
|
+
* 2ⁿ equal-branch case) took 52,256ms on 27ch, and `((a|aa))+$` — the redundant
|
|
104
|
+
* wrapper the `((a+))+` entry above exists for — 15,426ms on 35ch, which is why
|
|
105
|
+
* the overlap propagates up through a wrapper exactly as the inner repeat does.
|
|
106
|
+
* Input-bounding is theater here for F49's own reason: tens of characters suffice.
|
|
107
|
+
*
|
|
108
|
+
* The detected core is the PREFIX RELATION between top-level branches (one branch
|
|
109
|
+
* a prefix of another, equality included — `(a|a)+` is the 2ⁿ case). Whether a
|
|
110
|
+
* given prefix overlap can actually be DRIVEN exponentially depends on what
|
|
111
|
+
* follows the group, and deciding that needs the real engine this file declined
|
|
112
|
+
* to take as a dependency — so the reject is conservative by construction, which
|
|
113
|
+
* is the fail-safe direction.
|
|
114
|
+
*
|
|
115
|
+
* MONOTONIC, and that is the rule this change lives under: it only ever ADDS
|
|
116
|
+
* rejections. Nothing above is narrowed, and no previously-rejected shape became
|
|
117
|
+
* admissible (asserted over the whole corpus in tests/plan.test.js). F49's
|
|
118
|
+
* "never sharpen the detector" bars chasing false POSITIVES; closing a false
|
|
119
|
+
* NEGATIVE by adding rejections is the opposite, allowed direction — distinct
|
|
120
|
+
* rules, not in tension (the `((a+))+` widening set the precedent).
|
|
91
121
|
*
|
|
92
122
|
* 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
123
|
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
100
124
|
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
101
125
|
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
@@ -106,10 +130,51 @@ export function sweepSecretLiterals(root: any, red: (code: string, path: string,
|
|
|
106
130
|
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
107
131
|
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
108
132
|
*
|
|
133
|
+
* The alternation class has its OWN instance of the same limit, measured and
|
|
134
|
+
* locked in `REDOS_OVERREJECTED_ALT`: `(ab|abc)+$`, `(x|xy|xyz)+$` and
|
|
135
|
+
* `(x(a|aa))+$` carry the prefix relation and did NOT blow up on the bodies
|
|
136
|
+
* measured (0.3–0.4ms at 41–91 characters). "Did not blow up on the body
|
|
137
|
+
* measured" is the honest claim — no adversarial search was run for a worse
|
|
138
|
+
* input — and rejecting them is again the direction that never admits an
|
|
139
|
+
* exponential pattern.
|
|
140
|
+
*
|
|
109
141
|
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
110
142
|
* so this scan assumes valid, balanced JS regex syntax.
|
|
111
143
|
* @param {string} src a compiled regex source string
|
|
112
|
-
* @returns {boolean} true iff a nested unbounded quantifier
|
|
144
|
+
* @returns {boolean} true iff a nested unbounded quantifier, or an unboundedly
|
|
145
|
+
* repeated group with overlapping alternation branches, is present
|
|
113
146
|
*/
|
|
114
147
|
export function hasNestedQuantifier(src: string): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* The catastrophic-backtracking reject, applied to a WHOLE signed document by
|
|
150
|
+
* tree walk — `sweepSecretLiterals`'s twin, and for its reason: some signed
|
|
151
|
+
* documents carry their regexes at a depth no per-field check enumerates.
|
|
152
|
+
*
|
|
153
|
+
* WHY IT EXISTS (PRD v1.60 §1, an OPEN GAP now closed). v1.55 took F49's reject
|
|
154
|
+
* to the operator's own regex fields, and the admissibility sweep that backed it
|
|
155
|
+
* walked `jobs/*.json`. That is not the population of signed specs: a REGISTRY
|
|
156
|
+
* BRIDGE is signed the same way, ships its plans VERBATIM (reuse stores the
|
|
157
|
+
* whole plan, never a template), and lives outside `jobs/`, so a pathological
|
|
158
|
+
* pattern arriving through a bridge was never swept. Nothing had decided
|
|
159
|
+
* bridge-borne patterns were safe — the sweep was written against the directory
|
|
160
|
+
* that held every spec at the time and was never re-aimed.
|
|
161
|
+
*
|
|
162
|
+
* WHAT is rejected is UNCHANGED: `hasNestedQuantifier`, one inventory, shared
|
|
163
|
+
* with job.js and plan.js. Only WHERE it looks is wider. Widening the DETECTOR
|
|
164
|
+
* is arbiter-adjacent and stays parked (F49); widening the POPULATION is this.
|
|
165
|
+
*
|
|
166
|
+
* KEY-SCOPED, deliberately. `pattern` and `gapKeep` are the exactly-three regex
|
|
167
|
+
* fields F49 names (`artifact-written.pattern`, `judged.pattern`, `gapKeep`);
|
|
168
|
+
* every other string in a signed document is prose, a path or a command, and
|
|
169
|
+
* running a regex-shape scan over prose would red goals for containing `(a+)+`
|
|
170
|
+
* in a sentence. A field carrying a regex under a NEW key is not swept until
|
|
171
|
+
* this list names it — stated so the limit is read rather than rediscovered.
|
|
172
|
+
*
|
|
173
|
+
* COMPILE-FIRST, like both per-field callers: the scan's contract is that its
|
|
174
|
+
* input is valid regex syntax, and a string that does not compile is the OTHER
|
|
175
|
+
* validator's red. Two reds on one string bury the real one.
|
|
176
|
+
* @param {any} root
|
|
177
|
+
* @param {(code: string, path: string, detail?: string) => void} red
|
|
178
|
+
*/
|
|
179
|
+
export function sweepNestedQuantifiers(root: any, red: (code: string, path: string, detail?: string) => void): void;
|
|
115
180
|
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;
|