bareloop 0.10.0 → 0.12.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.
@@ -32,12 +32,21 @@ export function schemaCoverage(catalogue?: Record<string, any>): {
32
32
  * and they arrive as validation reds through the revise loop — a schema that
33
33
  * tried to restate them would be a second validator.
34
34
  *
35
+ * `verdictType` narrows the branches to the kinds the picked class may express
36
+ * (softgreen module 3, `classMenu`): a green job cannot SPELL `judged-floor`,
37
+ * rather than spelling it and being told afterwards. COVERAGE is still computed
38
+ * over the WHOLE catalogue — the class filter must never make a parameter look
39
+ * orphaned and turn a menu narrowing into a catalogue-drift alarm.
40
+ *
35
41
  * THROWS on a catalogue it cannot cover: a malformed catalogue is a bareloop bug,
36
42
  * and failing loudly beats emitting a schema that quietly cannot say something
37
43
  * the validator will then demand.
38
44
  * @param {Record<string, any>} [catalogue]
45
+ * @param {{verdictType?: string|null}} [o]
39
46
  */
40
- export function declarationSchema(catalogue?: Record<string, any>): {
47
+ export function declarationSchema(catalogue?: Record<string, any>, { verdictType }?: {
48
+ verdictType?: string | null;
49
+ }): {
41
50
  type: string;
42
51
  properties: {
43
52
  stages: {
@@ -86,12 +95,13 @@ export function declarationSchema(catalogue?: Record<string, any>): {
86
95
  * The output channel. It records and acknowledges; it takes no action, which is
87
96
  * why handing it to a toolless authoring call does not reopen D3.
88
97
  * @param {{calls: any[]}} box
89
- * @param {{catalogue?: Record<string, any>}} [o]
98
+ * @param {{catalogue?: Record<string, any>, verdictType?: string|null}} [o]
90
99
  */
91
100
  export function declarationTool(box: {
92
101
  calls: any[];
93
- }, { catalogue }?: {
102
+ }, { catalogue, verdictType }?: {
94
103
  catalogue?: Record<string, any>;
104
+ verdictType?: string | null;
95
105
  }): {
96
106
  name: string;
97
107
  description: string;
@@ -146,9 +156,19 @@ export function declarationTool(box: {
146
156
  * The catalogue, as the authoring call sees it — rendered FROM the catalogue, so
147
157
  * the prompt and the validator judge one set of facts. `shape` and `asserts` are
148
158
  * M2's own strings.
159
+ *
160
+ * `verdictType` narrows it to the picked class's menu, exactly as it narrows the
161
+ * schema — the prompt and the tool must offer ONE set of kinds, or the composer
162
+ * reads about a kind it cannot call. A kind above the ceiling is omitted rather
163
+ * than rendered as unavailable: a LOCKED row says "this does not run in v1",
164
+ * which is a fact about the build; "not for your class" is a fact about a choice
165
+ * the person already made, and printing it invites the composer to argue with it.
149
166
  * @param {Record<string, any>} [catalogue]
167
+ * @param {{verdictType?: string|null}} [o]
150
168
  */
151
- export function catalogueBlock(catalogue?: Record<string, any>): string;
169
+ export function catalogueBlock(catalogue?: Record<string, any>, { verdictType }?: {
170
+ verdictType?: string | null;
171
+ }): string;
152
172
  /**
153
173
  * HOW THE GENRE'S OWN TOOLS PRINT — rendered FROM `genreInstruments`, so the
154
174
  * pattern the author is handed and the pattern the tree already grades with are
@@ -346,9 +366,25 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }: {
346
366
  * run could not be read afterwards. A cost entry with no raw beside it is that
347
367
  * run again.
348
368
  */
349
- /** @param {{ceilingUsd?: number|null}} [o] the OPERATOR's ceiling; `null`/absent is UNBOUNDED */
350
- export function makeCostBook({ ceilingUsd }?: {
369
+ /**
370
+ * @param {{ceilingUsd?: number|null,
371
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void}} [o]
372
+ * `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
373
+ * `onCall` is a REPORTING seam fired as each metered call lands — the shell's
374
+ * only chance to know what has been spent while the run is still alive. It is
375
+ * never consulted and never governs: `capStop` below is still the one
376
+ * predicate, still asked between calls, and still reads the same `entries`.
377
+ * `absorb` deliberately does NOT fire it: those calls were metered (and
378
+ * reported) by whoever made them, and reporting them twice would double the
379
+ * scout's spend in every reader downstream.
380
+ */
381
+ export function makeCostBook({ ceilingUsd, onCall }?: {
351
382
  ceilingUsd?: number | null;
383
+ onCall?: (call: {
384
+ label: string;
385
+ costUsd: number | null;
386
+ unpricedRounds: number;
387
+ }) => void;
352
388
  }): {
353
389
  /** @param {string} label @param {any} r a bare-agent run result @param {number} [attempt] */
354
390
  add(label: string, r: any, attempt?: number): {
@@ -417,6 +453,48 @@ export function makeLoopGenerate(provider: any, { system, maxTokens }?: {
417
453
  system?: string;
418
454
  maxTokens?: number;
419
455
  }): (messages: any[], tools: any[], opts?: any) => Promise<any>;
456
+ /**
457
+ * ONE structured ask, with the malformed-emission retry bounded on its OWN axis.
458
+ * A reply that carries no artifact is an ARTIFACT-RED — name the axis, retry
459
+ * under a cap, write nothing — and it never consumes a revision, because a
460
+ * transport fault is not a defect in a close.
461
+ *
462
+ * THE CHANNEL IS A PARAMETER, and that is softgreen module 4's only change here:
463
+ * the rubric card and the calibration set are a SECOND schema-forced emission
464
+ * with the same failure modes, and a second ladder is a second place for the cap,
465
+ * the reject block and the re-ask to drift. The declaration channel below is the
466
+ * one this loop has always used, spelled out rather than inlined — nothing about
467
+ * the authoring path's behaviour moves.
468
+ * @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
469
+ * label: string, book: ReturnType<typeof makeCostBook>,
470
+ * channel: {name: string, instruction: string, textPath?: string, tool: (box: {calls: any[]}) => any}}} o
471
+ * @returns {Promise<{artifact: any, attempts: number, convo: any[], raw: string,
472
+ * providerError: string|null, red: Red|null, budget: 'cap-halt'|'pricing-red'|null}>}
473
+ */
474
+ export function askStructured({ messages, generate, mode, retries, label, book, channel }: {
475
+ messages: any[];
476
+ generate: Function;
477
+ mode: "tool" | "text";
478
+ retries: number;
479
+ label: string;
480
+ book: ReturnType<typeof makeCostBook>;
481
+ channel: {
482
+ name: string;
483
+ instruction: string;
484
+ textPath?: string;
485
+ tool: (box: {
486
+ calls: any[];
487
+ }) => any;
488
+ };
489
+ }): Promise<{
490
+ artifact: any;
491
+ attempts: number;
492
+ convo: any[];
493
+ raw: string;
494
+ providerError: string | null;
495
+ red: Red | null;
496
+ budget: "cap-halt" | "pricing-red" | null;
497
+ }>;
420
498
  /**
421
499
  * THE AUTHORING FLOW.
422
500
  *
@@ -450,10 +528,12 @@ export function makeLoopGenerate(provider: any, { system, maxTokens }?: {
450
528
  * listing?: {stop: string|null, files: string[]|null, block: string|null, meta: any}|null,
451
529
  * generate: Function, seedReadFn?: Function, closeCtx?: any,
452
530
  * ceilingUsd?: number|null,
531
+ * onPhase?: (phase: string, data?: any) => void,
532
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
453
533
  * maxRevisions?: number, structureRetries?: number,
454
534
  * structuredMode?: 'tool'|'text', catalogue?: Record<string, any>}} o
455
535
  */
456
- export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, scout, listing, generate, seedReadFn, closeCtx, ceilingUsd, maxRevisions, structureRetries, structuredMode, catalogue, }: {
536
+ export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, scout, listing, generate, seedReadFn, closeCtx, ceilingUsd, onPhase, onCall, maxRevisions, structureRetries, structuredMode, catalogue, }: {
457
537
  workdir: string;
458
538
  seedRef: string;
459
539
  lang: string;
@@ -479,6 +559,12 @@ export function authorClose({ workdir, seedRef, lang, verdictType, answers, ques
479
559
  seedReadFn?: Function;
480
560
  closeCtx?: any;
481
561
  ceilingUsd?: number | null;
562
+ onPhase?: (phase: string, data?: any) => void;
563
+ onCall?: (call: {
564
+ label: string;
565
+ costUsd: number | null;
566
+ unpricedRounds: number;
567
+ }) => void;
482
568
  maxRevisions?: number;
483
569
  structureRetries?: number;
484
570
  structuredMode?: "tool" | "text";
@@ -529,28 +615,112 @@ export const STRUCTURE_INSTRUCTION_TEXT: string;
529
615
  * cannot emit anything else (D2/D3), so a genre it has never seen is a
530
616
  * composition problem, not a catalogue problem.
531
617
  *
532
- * The GREEN set is questions 1–6 of the frozen TYPES set (prereg §5) BYTE FOR
533
- * BYTE. What is gone is the seventh slot — D13's genre CONFIRM ("this looks like
534
- * a type-fixing job, correct?") — superseded in full: the interview never asks
535
- * about a genre again. The refusal it carried did not disappear; it MOVED to the
536
- * composer, on the same counted `request-red` path (see `authorCloseForJob`).
537
- * The remaining six are already genre-neutral, which is why generalising them
538
- * required deleting a slot rather than rewording one.
618
+ * The GREEN set descends from questions 1–6 of the frozen TYPES set (prereg §5).
619
+ * TWO slots have been deleted since, both because the interview was ASKING FOR
620
+ * SOMETHING IT ALREADY HAD or already knew:
621
+ *
622
+ * - D13's genre CONFIRM ("this looks like a type-fixing job, correct?") —
623
+ * superseded in full: the interview never asks about a genre again, and the
624
+ * refusal it carried MOVED to the composer, on the same counted
625
+ * `request-red` path (see `authorCloseForJob`);
626
+ * - "Is there a code repo I can look at? Where?" — DROPPED on hamr's ruling
627
+ * after he drove the terminal interview himself (2026-08-15, verbatim: *"drop
628
+ * Q6"*). The repository is a MANDATORY, STRUCTURED input already —
629
+ * `runInterview`'s own `repoPath`, which the runner takes from `--patient` and
630
+ * refuses to start without. Asking a person to re-type in prose a path the
631
+ * machine is holding in a variable is the SWE tax this product refuses, and
632
+ * it invites a second, drifting answer for the same fact (hamr's own answer
633
+ * on the live run was *"Yes — the patient itself."*). Nothing downstream lost
634
+ * anything: the scout reads `repoPath`, never the answer.
635
+ *
636
+ * Question 2 was WIDENED in the same ruling: it now asks for the read-or-draw-from
637
+ * files as well as the ones that change. The two halves belong in one answer
638
+ * because they are one decision for the person answering — what the work touches,
639
+ * and what it touches only to look at.
640
+ *
641
+ * The five that remain are genre-neutral, which is why every generalisation so far
642
+ * has deleted a slot rather than reworded one. They are NUMBERED CONTIGUOUSLY from
643
+ * 1: the number a person sees is the key their answer is filed under, and a gap
644
+ * would be a hole in a form nobody can explain.
539
645
  */
540
646
  export const GREEN_QUESTIONS: Readonly<{
541
647
  1: "What do you want done?";
542
- 2: "Which files or folders should change?";
648
+ 2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
649
+ 3: "Is there anything that must not change?";
650
+ 4: "How do you check today whether it's working?";
651
+ 5: "What would make you say this came back worse than before?";
652
+ }>;
653
+ /**
654
+ * THE HITL SET (N4 slice 1) — the green questions BYTE FOR BYTE, plus one.
655
+ *
656
+ * The five are unchanged deliberately rather than economically: the
657
+ * mechanical-first composition law (2026-08-07) says every hitl close is
658
+ * *deterministic stages first, judge minimal, human last*, so a hitl job needs
659
+ * exactly the same mechanical facts a green one does — what changes, what must
660
+ * not, how it is checked today, what "worse" looks like. Rewording them would
661
+ * fork one interview into two that drift.
662
+ *
663
+ * The last one is the only thing the green set cannot supply, and it is not a
664
+ * flourish: `human-confirms` requires an `ask` — the question the signer answers
665
+ * at the end of the run — and nothing else in the interview names it. Without it
666
+ * the composer would have to invent what a person is deciding, which is the one
667
+ * thing a human stage must never have done for it. It is the SPREAD's next number
668
+ * rather than a fixed one, so a green-side deletion renumbers it instead of
669
+ * leaving a gap (it was 7 while the green set held six, and is 6 now it holds five).
670
+ */
671
+ export const HITL_QUESTIONS: Readonly<{
672
+ 6: "When you look at the finished result yourself, what are you deciding?";
673
+ 1: "What do you want done?";
674
+ 2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
675
+ 3: "Is there anything that must not change?";
676
+ 4: "How do you check today whether it's working?";
677
+ 5: "What would make you say this came back worse than before?";
678
+ }>;
679
+ /**
680
+ * THE SOFTGREEN SET (module 3, design record §4.6) — the green questions BYTE
681
+ * FOR BYTE, plus TWO.
682
+ *
683
+ * Same discipline as the hitl set, and for the same reason: a softgreen close is
684
+ * *mechanical stages first, the judged stage after* (§4.5), so a softgreen job
685
+ * needs every mechanical fact a green one does. No renumbering of the green set,
686
+ * no genre question, nothing reworded.
687
+ *
688
+ * The two additions are the only things the green set cannot supply, and neither
689
+ * is a flourish — they are the judged floor's two REQUIRED inputs:
690
+ * - Q6 becomes the RUBRIC CARD (§4.3): the person's own pass/fail lines,
691
+ * compiled into enumerated items, signed, and enumerated in the spec hash.
692
+ * The documented ceiling rides with it — the judge catches violations of
693
+ * STATED card items only, and the fix for a miss is a new card line and a
694
+ * re-sign, never a smarter judge.
695
+ * - Q7 becomes the frozen CALIBRATION SET (§4.4): the whole pipe must grade it
696
+ * correctly before the close is signable.
697
+ * Both are asked in the person's own terms — an example they would pass and one
698
+ * they would fail, not a rubric they are asked to write — because compiling a lay
699
+ * answer into extractable items is the LLM's job (D5, proposed-and-signed) and
700
+ * asking the person to do it is the SWE tax this product refuses.
701
+ *
702
+ * What the two answers COMPILE INTO is module 4. This module carries them
703
+ * exactly as the hitl set carries its own last answer: they ride in `answers`,
704
+ * they reach the composer verbatim through the prompt's interview block, and
705
+ * nothing here derives anything from them.
706
+ */
707
+ export const SOFTGREEN_QUESTIONS: Readonly<{
708
+ 6: "When you judge the result yourself, what separates a pass from a fail? Name the few things you actually look for.";
709
+ 7: "Give one example you'd pass and one you'd fail, and say why.";
710
+ 1: "What do you want done?";
711
+ 2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
543
712
  3: "Is there anything that must not change?";
544
713
  4: "How do you check today whether it's working?";
545
714
  5: "What would make you say this came back worse than before?";
546
- 6: "Is there a code repo I can look at? Where?";
547
715
  }>;
548
716
  /**
549
- * The three sets. `questions: null` on a locked class is ABSENT, never `{}`:
550
- * an empty set would read as "this class needs no questions", which is a
551
- * different and false statement (F59's distinction, at the interview layer).
552
- * Nothing ever reads them — a locked class refuses at ADMISSION, before its
553
- * questions run — and the two readers below throw rather than hand back nothing.
717
+ * The three sets, all LIVE as of softgreen module 3. The locked shape stays
718
+ * spelled out in the readers below rather than deleted with the last locked
719
+ * class: `questions: null` is ABSENT, never `{}` — an empty set would read as
720
+ * "this class needs no questions", a different and false statement (F59's
721
+ * distinction, at the interview layer) — and a locked class refuses at ADMISSION,
722
+ * before its questions run, so the two readers throw rather than hand back
723
+ * nothing. A fourth class arrives locked, and the machinery is here for it.
554
724
  * @type {Record<string, {locked: boolean, questions: Record<string|number, string>|null, required: readonly number[]|null}>}
555
725
  */
556
726
  export const QUESTION_SETS: Record<string, {
@@ -754,6 +924,48 @@ export const PARAM_SCHEMAS: Readonly<{
754
924
  requireNonEmpty: {
755
925
  const: boolean;
756
926
  };
927
+ card: {
928
+ type: string;
929
+ description: string;
930
+ properties: {
931
+ items: {
932
+ type: string;
933
+ minItems: number;
934
+ items: {
935
+ type: string;
936
+ properties: {
937
+ rule: {
938
+ enum: string[];
939
+ description: string;
940
+ };
941
+ text: {
942
+ type: string;
943
+ minLength: number;
944
+ description: string;
945
+ };
946
+ };
947
+ required: string[];
948
+ additionalProperties: boolean;
949
+ };
950
+ };
951
+ };
952
+ required: string[];
953
+ additionalProperties: boolean;
954
+ };
955
+ paths: {
956
+ type: string;
957
+ minItems: number;
958
+ maxItems: number;
959
+ items: {
960
+ type: string;
961
+ };
962
+ description: string;
963
+ };
964
+ ask: {
965
+ type: string;
966
+ minLength: number;
967
+ description: string;
968
+ };
757
969
  }>;
758
970
  export type Red = {
759
971
  code: string;
@@ -9,10 +9,11 @@
9
9
  * `CLASS_BY_VERDICT` exist to stop, defeated from ABOVE by never letting the
10
10
  * illegal pair be formed.
11
11
  *
12
- * INERT IN v1 BY CONSTRUCTION, which is why it is cheap now: every LIVE kind is
13
- * mechanical, so no declaration a v1 catalogue can express reaches above green.
14
- * It is written before the second class exists so the first soft-green kind is
15
- * not the thing that discovers the question.
12
+ * NO LONGER INERT: `human-confirms` (N4 slice 1) and `judged-floor` (softgreen
13
+ * module 2) are both live and both above green, so a green declaration naming
14
+ * one is a real red rather than a hypothetical. `classMenu` below is the other
15
+ * half of the same rule, applied one step earlier — the composer is never handed
16
+ * a kind this would refuse.
16
17
  *
17
18
  * A kind the catalogue does not know contributes NOTHING — it already carries an
18
19
  * `unknown-kind` red, and guessing a class for it would be deriving the promise
@@ -28,6 +29,31 @@ export function closeCeiling(declaration: any, catalogue?: Record<string, KindSp
28
29
  stage: string | null;
29
30
  locked: boolean;
30
31
  } | null;
32
+ /**
33
+ * THE MENU A JOB OF THIS CLASS MAY EXPRESS — the ceiling rule, moved one step
34
+ * EARLIER, to the moment the composer is handed its vocabulary.
35
+ *
36
+ * `closeCeiling` refuses an above-ceiling kind AFTER a paid call has composed a
37
+ * declaration around it. That is a late red for an illegal pick the composer was
38
+ * shown in its own catalogue and its own tool schema — and the standing rule is
39
+ * the opposite: wherever only a closed set can satisfy a field, hand over the
40
+ * enumerated set, so the illegal value is INEXPRESSIBLE rather than rejected
41
+ * after the fact (the `check-passes(name)` model). A green job is therefore never
42
+ * shown `judged-floor` or `human-confirms` at all.
43
+ *
44
+ * ONLY THE MENU NARROWS. The VALIDATOR keeps the whole catalogue, deliberately:
45
+ * a model that names a kind it was never offered must be told which RULE it
46
+ * broke (`class-ceiling`), not that the kind does not exist (`unknown-kind`) —
47
+ * one is the truth and the other invites it to invent a spelling.
48
+ *
49
+ * `null` means UNFILTERED and exists for callers inspecting the catalogue itself
50
+ * (the schema/coverage suite). Every composing call site passes the user's own
51
+ * picked class; `authorPrompt` already throws without one.
52
+ * @param {string|null} verdictType
53
+ * @param {Record<string, KindSpec>} [catalogue]
54
+ * @returns {string[]} kind names at or below the picked class, in catalogue order
55
+ */
56
+ export function classMenu(verdictType: string | null, catalogue?: Record<string, KindSpec>): string[];
31
57
  /**
32
58
  * Which kinds can carry an environment at all, read off the CATALOGUE rather
33
59
  * than a hand-kept list. ONE spelling, because two consumers must agree
@@ -290,9 +316,17 @@ export function normalizeDeclaration(declaration: any): any;
290
316
  /** @typedef {{required: readonly string[], optional: readonly string[], pathParams: readonly string[], locked?: true, verdictClass: string, shape: string, asserts: string}} KindSpec */
291
317
  /** the whole menu, in ascending order of what it takes to render the verdict */
292
318
  export const VERDICT_CLASSES: readonly string[];
293
- /** declared-but-locked: selecting one is COUNTED demand, never an admission */
319
+ /** declared-but-locked: selecting one is COUNTED demand, never an admission.
320
+ * `hitl` LEFT this list at N4 slice 1 and `soft-green` at softgreen module 3
321
+ * (see `src/job.js`'s twin, `LOCKED_VERDICTS`, which moved in the same commit) —
322
+ * the judged floor its lock was waiting on is built, wired and calibrated.
323
+ *
324
+ * EMPTY is a true statement about a closed menu, not F59's absent-as-empty: the
325
+ * mechanism is read at call time from this constant, so re-locking a class is
326
+ * one edit and every counted refusal comes back with it. */
327
+ /** @type {readonly string[]} */
294
328
  export const LOCKED_CLASSES: readonly string[];
295
- /** the classes v1 actually builds — exactly one */
329
+ /** the classes v1 actually builds */
296
330
  export const LIVE_CLASSES: readonly string[];
297
331
  /**
298
332
  * The whole vocabulary, as DATA. `shape` and `asserts` are the strings the
@@ -508,40 +542,66 @@ export const TYPES_GENRE: Readonly<{
508
542
  }>;
509
543
  }>;
510
544
  }>;
511
- /**
512
- * THE GUARD BATTERY, KEYED BY VERDICT CLASS (hamr's ruling, PRD v1.57 §2).
513
- *
514
- * D5's battery was genre-keyed data. Under class-keyed interviews it has no
515
- * genre to hang from, and the ruling gives it the same home the interview took:
516
- * **the battery attaches to the verdict class the user picked** — not to a
517
- * problem-genre label, and not per catalogue KIND.
518
- *
519
- * WHY THE CLASS. What a battery is FOR is the class of dishonesty the class of
520
- * VERDICT admits. A green job is a job whose *done* is claimed to be
521
- * machine-checkable, so it carries the green battery whatever problem it came
522
- * from — that is the anti-suppression obligation the F87 genre earned, and it
523
- * must not travel with a label the interview no longer asks for.
524
- *
525
- * ATTACHMENT POINT vs CONTENTS, and the split is the whole implementation:
526
- * - the battery's STRUCTURE and its un-removability key off the CLASS. That
527
- * is this table: which guards exist, what they assert, which slot is the
528
- * model's (`fill`), and which params are ours.
529
- * - the battery's CONTENTS fill in at COMPOSITION (`compose`), because a
530
- * suppression pattern, a file extension and a checker's escape hatch belong
531
- * to the TOOL the declaration names, and nothing before composition knows
532
- * that tool. `classGuards` resolves them from the language data below.
533
- *
534
- * A LOCKED class carries `guards: null`, not `[]`. Absent is never empty (F59):
535
- * `[]` would read as "this class needs no guards", which is exactly the sentence
536
- * D5 exists to make unsayable. Nothing ever reaches it — admission refuses a
537
- * locked class before its battery is asked for — and `classGuards` throws rather
538
- * than hand back nothing if anything ever does.
539
- * @type {Record<string, {locked: boolean, guards: readonly any[]|null}>}
540
- */
541
- export const CLASS_BATTERIES: Record<string, {
542
- locked: boolean;
543
- guards: readonly any[] | null;
545
+ export const CLASS_BATTERIES: Readonly<{
546
+ green: Readonly<{
547
+ locked: false;
548
+ guards: readonly (Readonly<{
549
+ name: "changed-from-seed";
550
+ kind: "files-changed";
551
+ params: Readonly<{
552
+ requireNonEmpty: true;
553
+ }>;
554
+ compose: readonly never[];
555
+ fill: readonly string[];
556
+ }> | Readonly<{
557
+ name: "no-suppressions";
558
+ kind: "pattern-absent-in-diff";
559
+ params: Readonly<{}>;
560
+ compose: readonly string[];
561
+ fill: readonly never[];
562
+ }>)[];
563
+ }>;
564
+ 'soft-green': Readonly<{
565
+ locked: false;
566
+ guards: readonly (Readonly<{
567
+ name: "changed-from-seed";
568
+ kind: "files-changed";
569
+ params: Readonly<{
570
+ requireNonEmpty: true;
571
+ }>;
572
+ compose: readonly never[];
573
+ fill: readonly string[];
574
+ }> | Readonly<{
575
+ name: "no-suppressions";
576
+ kind: "pattern-absent-in-diff";
577
+ params: Readonly<{}>;
578
+ compose: readonly string[];
579
+ fill: readonly never[];
580
+ }>)[];
581
+ }>;
582
+ hitl: Readonly<{
583
+ locked: false;
584
+ guards: readonly (Readonly<{
585
+ name: "changed-from-seed";
586
+ kind: "files-changed";
587
+ params: Readonly<{
588
+ requireNonEmpty: true;
589
+ }>;
590
+ compose: readonly never[];
591
+ fill: readonly string[];
592
+ }> | Readonly<{
593
+ name: "no-suppressions";
594
+ kind: "pattern-absent-in-diff";
595
+ params: Readonly<{}>;
596
+ compose: readonly string[];
597
+ fill: readonly never[];
598
+ }>)[];
599
+ }>;
544
600
  }>;
601
+ /** the kinds NO ONE runs as an in-run ruler — ruling 5's law, as data. A
602
+ * declaration that offers one is a red rather than a normalization: the signer
603
+ * must not read one thing and have the runner store another. */
604
+ export const NEVER_OFFERED_KINDS: readonly string[];
545
605
  export type Red = {
546
606
  code: string;
547
607
  path: string;
@@ -82,13 +82,16 @@ export function refuseLockedKind(kind: string): Refusal;
82
82
  * verdictType?: string|null,
83
83
  * questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
84
84
  * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
85
+ * onPhase?: (phase: string, data?: any) => void,
86
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
85
87
  * seedFn?: Function, scoutFn?: Function, listingFn?: Function,
86
- * authorFn?: Function, authorOpts?: object}} o
88
+ * authorFn?: Function, authorOpts?: object,
89
+ * signerFix?: Function|any, proposeFn?: Function, compileOpts?: object}} o
87
90
  * @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
88
- * closeDecl: any, seedRef: string|null, authoring: any, reds: Red[], stop: string|null,
89
- * cost: any, interview: any}>}
91
+ * closeDecl: any, seedRef: string|null, authoring: any, judged: any, reds: Red[],
92
+ * stop: string|null, cost: any, interview: any}>}
90
93
  */
91
- export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, seedFn, scoutFn, listingFn, authorFn, authorOpts, }: {
94
+ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, onPhase, onCall, seedFn, scoutFn, listingFn, authorFn, authorOpts, signerFix, proposeFn, compileOpts, }: {
92
95
  answers: Record<string | number, any>;
93
96
  repoPath?: string | null;
94
97
  lang: string;
@@ -100,11 +103,20 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
100
103
  scout?: any;
101
104
  listing?: any;
102
105
  ceilingUsd?: number | null;
106
+ onPhase?: (phase: string, data?: any) => void;
107
+ onCall?: (call: {
108
+ label: string;
109
+ costUsd: number | null;
110
+ unpricedRounds: number;
111
+ }) => void;
103
112
  seedFn?: Function;
104
113
  scoutFn?: Function;
105
114
  listingFn?: Function;
106
115
  authorFn?: Function;
107
116
  authorOpts?: object;
117
+ signerFix?: Function | any;
118
+ proposeFn?: Function;
119
+ compileOpts?: object;
108
120
  }): Promise<{
109
121
  ok: boolean;
110
122
  refusal: Refusal | null;
@@ -112,6 +124,7 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
112
124
  closeDecl: any;
113
125
  seedRef: string | null;
114
126
  authoring: any;
127
+ judged: any;
115
128
  reds: Red[];
116
129
  stop: string | null;
117
130
  cost: any;
@@ -163,6 +176,24 @@ export function assembleSpec(specDraft: any, { closeDecl, verdictType }: {
163
176
  * A close with NO work stage red at seed has nothing to do and is refused
164
177
  * decision-ready — gate-2 round 2's arm B died exactly there, on a mypy that
165
178
  * scanned nothing and therefore read clean.
179
+ * 4. THE CALIBRATION GATE (softgreen module 5) — for a close carrying a
180
+ * `judged-floor` stage ONLY. The whole judged pipe runs over the signed ten
181
+ * and the five arbiter-owned injection artifacts, and anything short of
182
+ * 10/10 with itemized reds and 5/5 resisted is an unsignable close. It is
183
+ * the ONE gate here that spends money, so it runs LAST — every free refusal
184
+ * has already had its chance, and nobody pays to discover a broken
185
+ * declaration.
186
+ *
187
+ * D9.3 AND THE JUDGED-ONLY CLOSE (2026-08-18 second addendum, ruling 3; hamr:
188
+ * *"fix it now, we are delivering softgreen, isn't that the whole point?"*). Gate
189
+ * 3 protects against a close that cannot fail — one that scans nothing and reads
190
+ * green at seed against a true red. A judged stage SKIPS the seed read by ruling
191
+ * 8, so a close whose only work stage is judged has no seed red to show and used
192
+ * to be unsignable: the one job shape softgreen exists for. For that shape, and
193
+ * ONLY that shape, a PASSED calibration gate plays the seed-red role — it is that
194
+ * close's own proof of failability, held to the same all-or-nothing bar. A close
195
+ * with a mechanical work stage is UNCHANGED: it still needs a seed red, whatever
196
+ * its calibration says.
166
197
  *
167
198
  * Gates 2 and 3 share ONE execution, and that is stated rather than hidden: at
168
199
  * job creation the tree IS the seed, so "does every stage run against the real
@@ -175,11 +206,13 @@ export function assembleSpec(specDraft: any, { closeDecl, verdictType }: {
175
206
  * comes back is the resolved spec, its hash, and the evidence to sign against.
176
207
  *
177
208
  * @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
178
- * timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function}} o
209
+ * timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function,
210
+ * judgeLoop?: Function|null, onJudgeCost?: ((c: any) => void)|null, calibrateFn?: Function,
211
+ * ceilingUsd?: number|null, priorCalls?: any[]}} o
179
212
  * @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
180
213
  * gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
181
214
  */
182
- export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs, seedFn, listingFn, seedReadFn, }: {
215
+ export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs, seedFn, listingFn, seedReadFn, judgeLoop, onJudgeCost, calibrateFn, ceilingUsd, priorCalls, }: {
183
216
  spec: any;
184
217
  workdir: string;
185
218
  seedRef?: string | null;
@@ -188,6 +221,11 @@ export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs,
188
221
  seedFn?: Function;
189
222
  listingFn?: Function;
190
223
  seedReadFn?: Function;
224
+ judgeLoop?: Function | null;
225
+ onJudgeCost?: ((c: any) => void) | null;
226
+ calibrateFn?: Function;
227
+ ceilingUsd?: number | null;
228
+ priorCalls?: any[];
191
229
  }): Promise<{
192
230
  ok: boolean;
193
231
  specHash: string | null;
@@ -92,13 +92,14 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
92
92
  * @param {{workdir: string, provider?: any,
93
93
  * rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
94
94
  * attempts?: number, ceilingUsd?: number|null,
95
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
95
96
  * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
96
97
  * createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
97
98
  * @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
98
99
  * calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
99
100
  * budgetStop: 'cap-halt'|'pricing-red'|null}>}
100
101
  */
101
- export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, createLoop, createSurveyor, }: {
102
+ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, onCall, createLoop, createSurveyor, }: {
102
103
  workdir: string;
103
104
  provider?: any;
104
105
  rounds?: number;
@@ -108,6 +109,11 @@ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, m
108
109
  ctx?: boolean;
109
110
  attempts?: number;
110
111
  ceilingUsd?: number | null;
112
+ onCall?: (call: {
113
+ label: string;
114
+ costUsd: number | null;
115
+ unpricedRounds: number;
116
+ }) => void;
111
117
  createLoop?: (o: {
112
118
  system: string;
113
119
  policy: any;