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/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); exotic overlapping-alternation blowup is out of scope by
205
- * decision (PARKED to the close-authoring rung — PRD v1.55).
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 is present
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
- /** @type {{ quant: boolean }[]} */
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
+ }
@@ -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
- export function makeCostBook(): {
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 is questions 1–6 of the frozen TYPES set (prereg §5) BYTE FOR
489
- * BYTE. What is gone is the seventh slot — D13's genre CONFIRM ("this looks like
490
- * a type-fixing job, correct?") — superseded in full: the interview never asks
491
- * about a genre again. The refusal it carried did not disappear; it MOVED to the
492
- * composer, on the same counted `request-red` path (see `authorCloseForJob`).
493
- * The remaining six are already genre-neutral, which is why generalising them
494
- * required deleting a slot rather than rewording one.
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;