bareloop 0.9.0 → 0.11.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 +555 -0
- package/README.md +111 -117
- package/bareloop.context.md +314 -24
- package/package.json +3 -3
- package/src/authorflow.js +300 -35
- package/src/authoring.js +290 -33
- package/src/authorjob.js +59 -7
- package/src/authorscout.js +83 -10
- package/src/bridges.js +15 -2
- package/src/clock.js +21 -3
- package/src/declaredclose.js +88 -11
- package/src/index.js +11 -1
- package/src/job.js +7 -2
- package/src/kinds.js +292 -20
- package/src/ledger.js +29 -2
- package/src/planrun.js +404 -29
- package/src/ralph.js +14 -0
- package/src/reuse.js +247 -9
- package/src/root.js +1 -1
- package/src/run.js +12 -5
- package/src/selection.js +1 -1
- package/src/text.js +79 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/types/authorflow.d.ts +135 -17
- package/types/authoring.d.ts +161 -35
- package/types/authorjob.d.ts +11 -2
- package/types/authorscout.d.ts +22 -3
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +64 -6
- package/types/index.d.ts +4 -4
- package/types/job.d.ts +5 -0
- package/types/kinds.d.ts +134 -10
- package/types/planrun.d.ts +61 -2
- package/types/reuse.d.ts +75 -1
- package/types/run.d.ts +12 -2
- package/types/text.d.ts +67 -0
- package/types/validate.d.ts +74 -9
package/src/validate.js
CHANGED
|
@@ -177,6 +177,69 @@ function unboundedQuantLen(src, i) {
|
|
|
177
177
|
return len;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** How many characters of `(`-prologue sit at `src[i]` before the group's BODY:
|
|
181
|
+
* 1 for a plain or capturing group, 3 for `(?:` `(?=` `(?!`, 4 for `(?<=` `(?<!`,
|
|
182
|
+
* and past the `>` of a named group `(?<name>`. The body is read back when the
|
|
183
|
+
* group closes, and a prologue counted as body text would make `(?:a|aa)` split
|
|
184
|
+
* as `?:a` / `aa` — no prefix relation, and the hazard read as safe.
|
|
185
|
+
* @param {string} src @param {number} i index of the `(` */
|
|
186
|
+
function groupPrologueLen(src, i) {
|
|
187
|
+
if (src[i + 1] !== '?') return 1;
|
|
188
|
+
const c = src[i + 2];
|
|
189
|
+
if (c === ':' || c === '=' || c === '!') return 3;
|
|
190
|
+
if (c === '<') {
|
|
191
|
+
if (src[i + 3] === '=' || src[i + 3] === '!') return 4; // lookbehind
|
|
192
|
+
const close = src.indexOf('>', i + 3); // named group
|
|
193
|
+
return close === -1 ? 3 : close - i + 1;
|
|
194
|
+
}
|
|
195
|
+
return 1; // modifier groups etc — read the lot as body (fail-safe)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Do a group body's TOP-LEVEL alternation branches overlap — is one a prefix of
|
|
200
|
+
* another, equality included? That is the practical detectable core of the
|
|
201
|
+
* overlapping-alternation blowup (`(a|aa)`, `(a|a)`, `(\d|\d\d)`, `(a|a?)`):
|
|
202
|
+
* two branches that can consume the same text give the engine two ways to reach
|
|
203
|
+
* the same position, and an unbounded repeat multiplies them.
|
|
204
|
+
*
|
|
205
|
+
* The comparison is over the branches' SOURCE TEXT, which is what makes it
|
|
206
|
+
* conservative rather than exact — `(ab|abc)+` carries the prefix relation and
|
|
207
|
+
* did not blow up on the body measured. Exactness needs the real regex engine
|
|
208
|
+
* F49 declined to take as a dependency, and guessing it wrong would ADMIT an
|
|
209
|
+
* exponential pattern, so the over-rejection stands as a named limit.
|
|
210
|
+
*
|
|
211
|
+
* Splitting respects escapes, character classes and nested groups: a `|` inside
|
|
212
|
+
* `[a|b]` or inside `(x|y)` is not this group's alternation, and treating it as
|
|
213
|
+
* one would invent branches nobody wrote.
|
|
214
|
+
* @param {string} body the text between a group's prologue and its `)`
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
217
|
+
function altBranchesOverlap(body) {
|
|
218
|
+
/** @type {string[]} */
|
|
219
|
+
const branches = [];
|
|
220
|
+
let start = 0;
|
|
221
|
+
let depth = 0;
|
|
222
|
+
for (let i = 0; i < body.length; i++) {
|
|
223
|
+
const c = body[i];
|
|
224
|
+
if (c === '\\') { i++; continue; }
|
|
225
|
+
if (c === '[') { i++; if (body[i] === '^') i++; while (i < body.length && body[i] !== ']') { if (body[i] === '\\') i++; i++; } continue; }
|
|
226
|
+
if (c === '(') { depth++; continue; }
|
|
227
|
+
if (c === ')') { depth--; continue; }
|
|
228
|
+
if (c === '|' && depth === 0) { branches.push(body.slice(start, i)); start = i + 1; }
|
|
229
|
+
}
|
|
230
|
+
if (branches.length === 0) return false; // no top-level alternation at all
|
|
231
|
+
branches.push(body.slice(start));
|
|
232
|
+
for (let i = 0; i < branches.length; i++) {
|
|
233
|
+
for (let j = 0; j < branches.length; j++) {
|
|
234
|
+
// `startsWith` covers EQUALITY, which is the 2ⁿ case: `(a|a)+$` took 52.3s
|
|
235
|
+
// on a 27-character body. Skipping i===j is what keeps that from being
|
|
236
|
+
// every pattern's own reflexive match.
|
|
237
|
+
if (i !== j && branches[j].startsWith(branches[i])) return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
180
243
|
/**
|
|
181
244
|
* F49 — a heuristic reject for the catastrophic-backtracking footgun: an
|
|
182
245
|
* unbounded quantifier applied to a group whose body itself repeats unboundedly
|
|
@@ -201,16 +264,40 @@ function unboundedQuantLen(src, i) {
|
|
|
201
264
|
* So the reject targets the dominant exponential class and fails it as a
|
|
202
265
|
* mechanical gap at the validation gate, before any tokens burn. A full ReDoS
|
|
203
266
|
* analyzer needs a real regex engine (an external native dep we do not take for
|
|
204
|
-
* a LOW issue)
|
|
205
|
-
*
|
|
267
|
+
* a LOW issue).
|
|
268
|
+
*
|
|
269
|
+
* SECOND CLASS, added 2026-08-12 — OVERLAPPING ALTERNATION under an unbounded
|
|
270
|
+
* repeat (`(a|aa)+`, `(a|a)+`, `(\d|\d\d)*`). F49 recorded this as a genuine
|
|
271
|
+
* false NEGATIVE of the shape approach and PARKED the widening, because changing
|
|
272
|
+
* the detector changes which SIGNED specs are admissible. It is closed here on
|
|
273
|
+
* measurement, one fresh process per point (a hot RegExp tiers up out of the V8
|
|
274
|
+
* interpreter, and a single-process sweep read a LONGER body as faster —
|
|
275
|
+
* backwards for an exponential, and the harness rather than the regex).
|
|
276
|
+
* `(a|aa)+$` against a run of `a` with one non-matching tail character:
|
|
277
|
+
*
|
|
278
|
+
* 27ch 218ms · 31ch 1,879ms · 35ch 12,082ms · 39ch 52,563ms · 41ch 189,420ms
|
|
279
|
+
*
|
|
280
|
+
* ~6.8× per 4 characters — the Fibonacci decomposition count, φ⁴. `(a|a)+$` (the
|
|
281
|
+
* 2ⁿ equal-branch case) took 52,256ms on 27ch, and `((a|aa))+$` — the redundant
|
|
282
|
+
* wrapper the `((a+))+` entry above exists for — 15,426ms on 35ch, which is why
|
|
283
|
+
* the overlap propagates up through a wrapper exactly as the inner repeat does.
|
|
284
|
+
* Input-bounding is theater here for F49's own reason: tens of characters suffice.
|
|
285
|
+
*
|
|
286
|
+
* The detected core is the PREFIX RELATION between top-level branches (one branch
|
|
287
|
+
* a prefix of another, equality included — `(a|a)+` is the 2ⁿ case). Whether a
|
|
288
|
+
* given prefix overlap can actually be DRIVEN exponentially depends on what
|
|
289
|
+
* follows the group, and deciding that needs the real engine this file declined
|
|
290
|
+
* to take as a dependency — so the reject is conservative by construction, which
|
|
291
|
+
* is the fail-safe direction.
|
|
292
|
+
*
|
|
293
|
+
* MONOTONIC, and that is the rule this change lives under: it only ever ADDS
|
|
294
|
+
* rejections. Nothing above is narrowed, and no previously-rejected shape became
|
|
295
|
+
* admissible (asserted over the whole corpus in tests/plan.test.js). F49's
|
|
296
|
+
* "never sharpen the detector" bars chasing false POSITIVES; closing a false
|
|
297
|
+
* NEGATIVE by adding rejections is the opposite, allowed direction — distinct
|
|
298
|
+
* rules, not in tension (the `((a+))+` widening set the precedent).
|
|
206
299
|
*
|
|
207
300
|
* SCOPE IS ASYMMETRIC, both directions named on purpose:
|
|
208
|
-
* - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
|
|
209
|
-
* detected — there is no inner QUANTIFIER to find, so the shape scan cannot
|
|
210
|
-
* see it; the blowup comes from overlapping alternation BRANCHES under a
|
|
211
|
-
* repeat. A genuine limit of the approach, not a bug in it. Widening the
|
|
212
|
-
* detector changes which SIGNED specs are admissible, which is
|
|
213
|
-
* arbiter-adjacent — parked, not chased (PRD v1.55).
|
|
214
301
|
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
215
302
|
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
216
303
|
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
@@ -221,13 +308,28 @@ function unboundedQuantLen(src, i) {
|
|
|
221
308
|
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
222
309
|
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
223
310
|
*
|
|
311
|
+
* The alternation class has its OWN instance of the same limit, measured and
|
|
312
|
+
* locked in `REDOS_OVERREJECTED_ALT`: `(ab|abc)+$`, `(x|xy|xyz)+$` and
|
|
313
|
+
* `(x(a|aa))+$` carry the prefix relation and did NOT blow up on the bodies
|
|
314
|
+
* measured (0.3–0.4ms at 41–91 characters). "Did not blow up on the body
|
|
315
|
+
* measured" is the honest claim — no adversarial search was run for a worse
|
|
316
|
+
* input — and rejecting them is again the direction that never admits an
|
|
317
|
+
* exponential pattern.
|
|
318
|
+
*
|
|
224
319
|
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
225
320
|
* so this scan assumes valid, balanced JS regex syntax.
|
|
226
321
|
* @param {string} src a compiled regex source string
|
|
227
|
-
* @returns {boolean} true iff a nested unbounded quantifier
|
|
322
|
+
* @returns {boolean} true iff a nested unbounded quantifier, or an unboundedly
|
|
323
|
+
* repeated group with overlapping alternation branches, is present
|
|
228
324
|
*/
|
|
229
325
|
export function hasNestedQuantifier(src) {
|
|
230
|
-
/**
|
|
326
|
+
/** `quant`: the body repeats unboundedly. `alt`: the body (or a group beneath
|
|
327
|
+
* it) carries overlapping alternation branches. `at`: where the body starts,
|
|
328
|
+
* so the branches can be read back when the group closes. The two hazard flags
|
|
329
|
+
* are tracked separately and propagate separately — merging them would let a
|
|
330
|
+
* plain nested repeat be reported as an alternation overlap and vice versa,
|
|
331
|
+
* and the two are different rewrites for the author. */
|
|
332
|
+
/** @type {{ quant: boolean, alt: boolean, at: number }[]} */
|
|
231
333
|
const stack = [];
|
|
232
334
|
for (let i = 0; i < src.length; i++) {
|
|
233
335
|
const c = src[i];
|
|
@@ -254,11 +356,19 @@ export function hasNestedQuantifier(src) {
|
|
|
254
356
|
while (i < src.length && src[i] !== ']') { if (src[i] === '\\') i++; i++; }
|
|
255
357
|
continue;
|
|
256
358
|
}
|
|
257
|
-
if (c === '(') { stack.push({ quant: false }); continue; }
|
|
359
|
+
if (c === '(') { stack.push({ quant: false, alt: false, at: i + groupPrologueLen(src, i) }); continue; }
|
|
258
360
|
if (c === ')') {
|
|
259
361
|
const g = stack.pop();
|
|
260
362
|
const qlen = unboundedQuantLen(src, i + 1);
|
|
261
363
|
if (g && g.quant && qlen) return true; // group repeats unboundedly AND its body did too
|
|
364
|
+
// …and the SECOND class: the body's own top-level branches overlap, so the
|
|
365
|
+
// engine has more than one way to consume the same text and an unbounded
|
|
366
|
+
// repeat multiplies those ways (MEASURED: `(a|aa)+$` 12.1s on a 35-char
|
|
367
|
+
// body). `g.alt` carries an overlap found in a group BENEATH this one, for
|
|
368
|
+
// the redundant-wrapper reason one comment down.
|
|
369
|
+
const overlap = !!g && (g.alt || altBranchesOverlap(src.slice(g.at, i)));
|
|
370
|
+
if (overlap && qlen) return true;
|
|
371
|
+
if (overlap && stack.length) stack[stack.length - 1].alt = true;
|
|
262
372
|
// The group is an unbounded-repeated atom of its parent when it is directly
|
|
263
373
|
// re-quantified (qlen) OR its own body already repeats unboundedly (g.quant):
|
|
264
374
|
// a redundant wrapper — ((a+))+ , (?:(a+))* , (((\d*)))+ — is the SAME
|
|
@@ -276,3 +386,50 @@ export function hasNestedQuantifier(src) {
|
|
|
276
386
|
}
|
|
277
387
|
return false;
|
|
278
388
|
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The catastrophic-backtracking reject, applied to a WHOLE signed document by
|
|
392
|
+
* tree walk — `sweepSecretLiterals`'s twin, and for its reason: some signed
|
|
393
|
+
* documents carry their regexes at a depth no per-field check enumerates.
|
|
394
|
+
*
|
|
395
|
+
* WHY IT EXISTS (PRD v1.60 §1, an OPEN GAP now closed). v1.55 took F49's reject
|
|
396
|
+
* to the operator's own regex fields, and the admissibility sweep that backed it
|
|
397
|
+
* walked `jobs/*.json`. That is not the population of signed specs: a REGISTRY
|
|
398
|
+
* BRIDGE is signed the same way, ships its plans VERBATIM (reuse stores the
|
|
399
|
+
* whole plan, never a template), and lives outside `jobs/`, so a pathological
|
|
400
|
+
* pattern arriving through a bridge was never swept. Nothing had decided
|
|
401
|
+
* bridge-borne patterns were safe — the sweep was written against the directory
|
|
402
|
+
* that held every spec at the time and was never re-aimed.
|
|
403
|
+
*
|
|
404
|
+
* WHAT is rejected is UNCHANGED: `hasNestedQuantifier`, one inventory, shared
|
|
405
|
+
* with job.js and plan.js. Only WHERE it looks is wider. Widening the DETECTOR
|
|
406
|
+
* is arbiter-adjacent and stays parked (F49); widening the POPULATION is this.
|
|
407
|
+
*
|
|
408
|
+
* KEY-SCOPED, deliberately. `pattern` and `gapKeep` are the exactly-three regex
|
|
409
|
+
* fields F49 names (`artifact-written.pattern`, `judged.pattern`, `gapKeep`);
|
|
410
|
+
* every other string in a signed document is prose, a path or a command, and
|
|
411
|
+
* running a regex-shape scan over prose would red goals for containing `(a+)+`
|
|
412
|
+
* in a sentence. A field carrying a regex under a NEW key is not swept until
|
|
413
|
+
* this list names it — stated so the limit is read rather than rediscovered.
|
|
414
|
+
*
|
|
415
|
+
* COMPILE-FIRST, like both per-field callers: the scan's contract is that its
|
|
416
|
+
* input is valid regex syntax, and a string that does not compile is the OTHER
|
|
417
|
+
* validator's red. Two reds on one string bury the real one.
|
|
418
|
+
* @param {any} root
|
|
419
|
+
* @param {(code: string, path: string, detail?: string) => void} red
|
|
420
|
+
*/
|
|
421
|
+
export function sweepNestedQuantifiers(root, red) {
|
|
422
|
+
(/** @type {(node: any, at: string, key: string) => void} */
|
|
423
|
+
function sweep(node, at, key) {
|
|
424
|
+
if (typeof node === 'string') {
|
|
425
|
+
if (key !== 'pattern' && key !== 'gapKeep') return;
|
|
426
|
+
try { new RegExp(node, 'm'); } catch { return; }
|
|
427
|
+
if (hasNestedQuantifier(node)) {
|
|
428
|
+
red('invalid-value', at, `catastrophic-backtracking risk: a nested unbounded quantifier (${node}) can hang the untimed evaluator on the MAIN event loop, where the in-process stall fuse cannot fire (F49). Rewrite the pattern — drop the outer repeat, or match once.`);
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (Array.isArray(node)) node.forEach((v, i) => sweep(v, `${at}.${i}`, key));
|
|
433
|
+
else if (isObj(node)) for (const [k, v] of Object.entries(node)) sweep(v, at ? `${at}.${k}` : k, k);
|
|
434
|
+
})(root, '', '');
|
|
435
|
+
}
|
package/types/authorflow.d.ts
CHANGED
|
@@ -149,6 +149,31 @@ export function declarationTool(box: {
|
|
|
149
149
|
* @param {Record<string, any>} [catalogue]
|
|
150
150
|
*/
|
|
151
151
|
export function catalogueBlock(catalogue?: Record<string, any>): string;
|
|
152
|
+
/**
|
|
153
|
+
* HOW THE GENRE'S OWN TOOLS PRINT — rendered FROM `genreInstruments`, so the
|
|
154
|
+
* pattern the author is handed and the pattern the tree already grades with are
|
|
155
|
+
* one string.
|
|
156
|
+
*
|
|
157
|
+
* This block is the fix for run msmbpjk6: a parser for a tool WE OWN THE FACTS
|
|
158
|
+
* ABOUT was left to the model to compose, and it composed tsc's terminal-only
|
|
159
|
+
* shape against piped output — 0 matches on 67 real error lines. The delivery is
|
|
160
|
+
* deliberately the MANDATORY-GUARD delivery, not advice: the pattern arrives
|
|
161
|
+
* spelled out, marked genre property, with the real captured line beside it, in
|
|
162
|
+
* the same register as the guard battery that arrives already parameterised.
|
|
163
|
+
*
|
|
164
|
+
* It states the MECHANISM (pipe, not a terminal) rather than only the answer,
|
|
165
|
+
* because the same trap is waiting behind every tool that pretty-prints for a
|
|
166
|
+
* human, and only some of them are in the table yet. That prediction has since
|
|
167
|
+
* been paid off by measurement: `node --test` prints TAP down a pipe and the
|
|
168
|
+
* spec reporter on a terminal, the identical flip, on a second tool.
|
|
169
|
+
*
|
|
170
|
+
* Each row also carries its `capture`, because a count term and a tally term are
|
|
171
|
+
* read by DIFFERENT aggregates and the wrong pairing is a silent misreading, not
|
|
172
|
+
* an error — `first` over a line the tool omits on a good run reports a broken
|
|
173
|
+
* instrument. The block states that mapping once rather than per row.
|
|
174
|
+
* @param {string} lang
|
|
175
|
+
*/
|
|
176
|
+
export function instrumentsBlock(lang: string): string;
|
|
152
177
|
/** The three laws. Stated to the author; enforced, where checkable, by M2. */
|
|
153
178
|
export function lawsBlock(): string;
|
|
154
179
|
/**
|
|
@@ -321,7 +346,26 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }: {
|
|
|
321
346
|
* run could not be read afterwards. A cost entry with no raw beside it is that
|
|
322
347
|
* run again.
|
|
323
348
|
*/
|
|
324
|
-
|
|
349
|
+
/**
|
|
350
|
+
* @param {{ceilingUsd?: number|null,
|
|
351
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void}} [o]
|
|
352
|
+
* `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
|
|
353
|
+
* `onCall` is a REPORTING seam fired as each metered call lands — the shell's
|
|
354
|
+
* only chance to know what has been spent while the run is still alive. It is
|
|
355
|
+
* never consulted and never governs: `capStop` below is still the one
|
|
356
|
+
* predicate, still asked between calls, and still reads the same `entries`.
|
|
357
|
+
* `absorb` deliberately does NOT fire it: those calls were metered (and
|
|
358
|
+
* reported) by whoever made them, and reporting them twice would double the
|
|
359
|
+
* scout's spend in every reader downstream.
|
|
360
|
+
*/
|
|
361
|
+
export function makeCostBook({ ceilingUsd, onCall }?: {
|
|
362
|
+
ceilingUsd?: number | null;
|
|
363
|
+
onCall?: (call: {
|
|
364
|
+
label: string;
|
|
365
|
+
costUsd: number | null;
|
|
366
|
+
unpricedRounds: number;
|
|
367
|
+
}) => void;
|
|
368
|
+
}): {
|
|
325
369
|
/** @param {string} label @param {any} r a bare-agent run result @param {number} [attempt] */
|
|
326
370
|
add(label: string, r: any, attempt?: number): {
|
|
327
371
|
label: string;
|
|
@@ -347,17 +391,28 @@ export function makeCostBook(): {
|
|
|
347
391
|
reason: string | null;
|
|
348
392
|
}[];
|
|
349
393
|
report(): {
|
|
350
|
-
costUsd: number | null;
|
|
351
|
-
knownUsd: number;
|
|
352
|
-
spendComplete: boolean;
|
|
353
|
-
nullCostCalls: number;
|
|
354
|
-
unpricedRounds: number;
|
|
355
394
|
calls: {
|
|
356
395
|
label: string;
|
|
357
396
|
costUsd: number | null;
|
|
358
397
|
unpricedRounds: number;
|
|
359
398
|
}[];
|
|
399
|
+
costUsd: number | null;
|
|
400
|
+
knownUsd: number;
|
|
401
|
+
spendComplete: boolean;
|
|
402
|
+
nullCostCalls: number;
|
|
403
|
+
unpricedRounds: number;
|
|
360
404
|
};
|
|
405
|
+
/**
|
|
406
|
+
* THE CEILING'S OWN READING, asked between metered calls. The book is the one
|
|
407
|
+
* place this pipeline's spend accumulates, so it is the one place that can
|
|
408
|
+
* answer honestly — and it answers through the shared predicate rather than a
|
|
409
|
+
* second hand-spelled comparison (`src/text.js`, beside `priceOf`).
|
|
410
|
+
* @returns {'cap-halt'|'pricing-red'|null}
|
|
411
|
+
*/
|
|
412
|
+
capStop(): "cap-halt" | "pricing-red" | null;
|
|
413
|
+
/** what was set, stated — `null` is "nobody set one", which is UNBOUNDED and
|
|
414
|
+
* a visible operator choice, never a defaulted number */
|
|
415
|
+
ceilingUsd: number | null;
|
|
361
416
|
};
|
|
362
417
|
/**
|
|
363
418
|
* The default model boundary: one bare-agent Loop per call, with the declaration
|
|
@@ -406,13 +461,17 @@ export function makeLoopGenerate(provider: any, { system, maxTokens }?: {
|
|
|
406
461
|
*
|
|
407
462
|
* @param {{workdir: string, seedRef: string, lang: string, verdictType: string,
|
|
408
463
|
* answers: Record<string|number, string>, questions?: Record<string|number, string>,
|
|
409
|
-
* scout: {state: string, facts: any, reason?: string|null, calls?: any[], raws?: any[]
|
|
464
|
+
* scout: {state: string, facts: any, reason?: string|null, cause?: string|null, calls?: any[], raws?: any[],
|
|
465
|
+
* budgetStop?: 'cap-halt'|'pricing-red'|null},
|
|
410
466
|
* listing?: {stop: string|null, files: string[]|null, block: string|null, meta: any}|null,
|
|
411
467
|
* generate: Function, seedReadFn?: Function, closeCtx?: any,
|
|
468
|
+
* ceilingUsd?: number|null,
|
|
469
|
+
* onPhase?: (phase: string, data?: any) => void,
|
|
470
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
412
471
|
* maxRevisions?: number, structureRetries?: number,
|
|
413
472
|
* structuredMode?: 'tool'|'text', catalogue?: Record<string, any>}} o
|
|
414
473
|
*/
|
|
415
|
-
export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, scout, listing, generate, seedReadFn, closeCtx, maxRevisions, structureRetries, structuredMode, catalogue, }: {
|
|
474
|
+
export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, scout, listing, generate, seedReadFn, closeCtx, ceilingUsd, onPhase, onCall, maxRevisions, structureRetries, structuredMode, catalogue, }: {
|
|
416
475
|
workdir: string;
|
|
417
476
|
seedRef: string;
|
|
418
477
|
lang: string;
|
|
@@ -423,8 +482,10 @@ export function authorClose({ workdir, seedRef, lang, verdictType, answers, ques
|
|
|
423
482
|
state: string;
|
|
424
483
|
facts: any;
|
|
425
484
|
reason?: string | null;
|
|
485
|
+
cause?: string | null;
|
|
426
486
|
calls?: any[];
|
|
427
487
|
raws?: any[];
|
|
488
|
+
budgetStop?: "cap-halt" | "pricing-red" | null;
|
|
428
489
|
};
|
|
429
490
|
listing?: {
|
|
430
491
|
stop: string | null;
|
|
@@ -435,6 +496,13 @@ export function authorClose({ workdir, seedRef, lang, verdictType, answers, ques
|
|
|
435
496
|
generate: Function;
|
|
436
497
|
seedReadFn?: Function;
|
|
437
498
|
closeCtx?: any;
|
|
499
|
+
ceilingUsd?: number | null;
|
|
500
|
+
onPhase?: (phase: string, data?: any) => void;
|
|
501
|
+
onCall?: (call: {
|
|
502
|
+
label: string;
|
|
503
|
+
costUsd: number | null;
|
|
504
|
+
unpricedRounds: number;
|
|
505
|
+
}) => void;
|
|
438
506
|
maxRevisions?: number;
|
|
439
507
|
structureRetries?: number;
|
|
440
508
|
structuredMode?: "tool" | "text";
|
|
@@ -485,21 +553,66 @@ export const STRUCTURE_INSTRUCTION_TEXT: string;
|
|
|
485
553
|
* cannot emit anything else (D2/D3), so a genre it has never seen is a
|
|
486
554
|
* composition problem, not a catalogue problem.
|
|
487
555
|
*
|
|
488
|
-
* The GREEN set
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
556
|
+
* The GREEN set descends from questions 1–6 of the frozen TYPES set (prereg §5).
|
|
557
|
+
* TWO slots have been deleted since, both because the interview was ASKING FOR
|
|
558
|
+
* SOMETHING IT ALREADY HAD or already knew:
|
|
559
|
+
*
|
|
560
|
+
* - D13's genre CONFIRM ("this looks like a type-fixing job, correct?") —
|
|
561
|
+
* superseded in full: the interview never asks about a genre again, and the
|
|
562
|
+
* refusal it carried MOVED to the composer, on the same counted
|
|
563
|
+
* `request-red` path (see `authorCloseForJob`);
|
|
564
|
+
* - "Is there a code repo I can look at? Where?" — DROPPED on hamr's ruling
|
|
565
|
+
* after he drove the terminal interview himself (2026-08-15, verbatim: *"drop
|
|
566
|
+
* Q6"*). The repository is a MANDATORY, STRUCTURED input already —
|
|
567
|
+
* `runInterview`'s own `repoPath`, which the runner takes from `--patient` and
|
|
568
|
+
* refuses to start without. Asking a person to re-type in prose a path the
|
|
569
|
+
* machine is holding in a variable is the SWE tax this product refuses, and
|
|
570
|
+
* it invites a second, drifting answer for the same fact (hamr's own answer
|
|
571
|
+
* on the live run was *"Yes — the patient itself."*). Nothing downstream lost
|
|
572
|
+
* anything: the scout reads `repoPath`, never the answer.
|
|
573
|
+
*
|
|
574
|
+
* Question 2 was WIDENED in the same ruling: it now asks for the read-or-draw-from
|
|
575
|
+
* files as well as the ones that change. The two halves belong in one answer
|
|
576
|
+
* because they are one decision for the person answering — what the work touches,
|
|
577
|
+
* and what it touches only to look at.
|
|
578
|
+
*
|
|
579
|
+
* The five that remain are genre-neutral, which is why every generalisation so far
|
|
580
|
+
* has deleted a slot rather than reworded one. They are NUMBERED CONTIGUOUSLY from
|
|
581
|
+
* 1: the number a person sees is the key their answer is filed under, and a gap
|
|
582
|
+
* would be a hole in a form nobody can explain.
|
|
495
583
|
*/
|
|
496
584
|
export const GREEN_QUESTIONS: Readonly<{
|
|
497
585
|
1: "What do you want done?";
|
|
498
|
-
2: "Which files or folders should change?";
|
|
586
|
+
2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
|
|
587
|
+
3: "Is there anything that must not change?";
|
|
588
|
+
4: "How do you check today whether it's working?";
|
|
589
|
+
5: "What would make you say this came back worse than before?";
|
|
590
|
+
}>;
|
|
591
|
+
/**
|
|
592
|
+
* THE HITL SET (N4 slice 1) — the green questions BYTE FOR BYTE, plus one.
|
|
593
|
+
*
|
|
594
|
+
* The five are unchanged deliberately rather than economically: the
|
|
595
|
+
* mechanical-first composition law (2026-08-07) says every hitl close is
|
|
596
|
+
* *deterministic stages first, judge minimal, human last*, so a hitl job needs
|
|
597
|
+
* exactly the same mechanical facts a green one does — what changes, what must
|
|
598
|
+
* not, how it is checked today, what "worse" looks like. Rewording them would
|
|
599
|
+
* fork one interview into two that drift.
|
|
600
|
+
*
|
|
601
|
+
* The last one is the only thing the green set cannot supply, and it is not a
|
|
602
|
+
* flourish: `human-confirms` requires an `ask` — the question the signer answers
|
|
603
|
+
* at the end of the run — and nothing else in the interview names it. Without it
|
|
604
|
+
* the composer would have to invent what a person is deciding, which is the one
|
|
605
|
+
* thing a human stage must never have done for it. It is the SPREAD's next number
|
|
606
|
+
* rather than a fixed one, so a green-side deletion renumbers it instead of
|
|
607
|
+
* leaving a gap (it was 7 while the green set held six, and is 6 now it holds five).
|
|
608
|
+
*/
|
|
609
|
+
export const HITL_QUESTIONS: Readonly<{
|
|
610
|
+
6: "When you look at the finished result yourself, what are you deciding?";
|
|
611
|
+
1: "What do you want done?";
|
|
612
|
+
2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
|
|
499
613
|
3: "Is there anything that must not change?";
|
|
500
614
|
4: "How do you check today whether it's working?";
|
|
501
615
|
5: "What would make you say this came back worse than before?";
|
|
502
|
-
6: "Is there a code repo I can look at? Where?";
|
|
503
616
|
}>;
|
|
504
617
|
/**
|
|
505
618
|
* The three sets. `questions: null` on a locked class is ABSENT, never `{}`:
|
|
@@ -710,6 +823,11 @@ export const PARAM_SCHEMAS: Readonly<{
|
|
|
710
823
|
requireNonEmpty: {
|
|
711
824
|
const: boolean;
|
|
712
825
|
};
|
|
826
|
+
ask: {
|
|
827
|
+
type: string;
|
|
828
|
+
minLength: number;
|
|
829
|
+
description: string;
|
|
830
|
+
};
|
|
713
831
|
}>;
|
|
714
832
|
export type Red = {
|
|
715
833
|
code: string;
|