bareloop 0.5.1 → 0.7.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/types/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  export { makeSpine } from "./spine.js";
2
2
  export { runPlan } from "./planrun.js";
3
3
  export { runJob } from "./run.js";
4
- export { ralph, runClose, CLOSE_FAULTS } from "./ralph.js";
5
- export { validateConfig, diffPaths, globToPrefix, scanSecrets, LOOP_SHAPES, SLOTS, VERBS } from "./validate.js";
6
- export { validateJob, jobSpecHash, checkApproval, CLOSE_TYPES, CLASSES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, STEP_MODES, TOOL_MENU, LOCKED_TOOLS, VERDICT_TYPES, LOCKED_VERDICTS } from "./job.js";
7
- export { validatePlan, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from "./plan.js";
4
+ export { ralph, runClose, runStages, CLOSE_FAULTS } from "./ralph.js";
5
+ export { globToPrefix, scanSecrets } from "./validate.js";
6
+ export { validateJob, jobSpecHash, checkApproval, checkMenu, CLOSE_TYPES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, TOOL_MENU, LOCKED_TOOLS, STORE_VERBS, VERDICT_TYPES, LOCKED_VERDICTS } from "./job.js";
7
+ export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from "./plan.js";
8
8
  export { snapshotScope, evalExits } from "./exits.js";
9
- export { interpret, STALL_REDS } from "./interpret.js";
10
- export { extractRules, MAX_RULES, MAX_RULE_CHARS } from "./extract.js";
9
+ export { BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate, mintBridge, appendGreen, appendRed, loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists } from "./bridges.js";
10
+ export { renderListing, selectionPrompt } from "./selection.js";
11
+ export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate } from "./reuse.js";
11
12
  export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from "./ledger.js";
package/types/job.d.ts CHANGED
@@ -17,9 +17,52 @@ export function validateJob(input: object | string, { shellCapUsd }?: {
17
17
  job: object | null;
18
18
  };
19
19
  /**
20
- * Content hash of a job spec — what an approval record binds to. Any edit to
21
- * the spec changes the hash, so an edited spec is unapproved by construction
22
- * (interview decision #1: human signs, always, once per spec VERSION).
20
+ * The staged close (PRD v1.28) — the close is an ORDERED LIST of named stages,
21
+ * and the check menu DERIVES from it. The principle: *the user authors the
22
+ * destination, never the road.* A user who cannot describe a workflow can still
23
+ * say what done looks like, and everything downstream of that is derived — which
24
+ * is why `checks[]` no longer exists as a field (hamr: *"there shouldn't be user
25
+ * authoring anywhere, that defies the point of bareloop"*).
26
+ *
27
+ * Two stage classes cannot be handed to the agent as a mid-build ruler, and F58
28
+ * measured both against a real close:
29
+ * - a PRECONDITION ("the seed commit exists") is not a ruler for the work; run
30
+ * as a check it passes instantly and teaches nothing. It sets `offer: false`.
31
+ * - a stage that reads what an EARLIER stage built ("the public API matches the
32
+ * emitted declarations") reds on its own for a reason that has nothing to do
33
+ * with the worker's edit. It names `needs`, and picking it runs that chain first.
34
+ * Everything else is offerable: derivation gave job #5 SEVEN rulers where the
35
+ * operator had hand-carved three (F58) — removing the operator does not cost the
36
+ * agent rulers, it hands it more.
37
+ *
38
+ * The `needs` expansion is TRANSITIVE (fixed point, not one level): a
39
+ * prerequisite may itself name one, and running an incomplete chain reproduces
40
+ * the exact false-red `needs` exists to remove, one level deeper. The expanded
41
+ * chain is ordered by the CLOSE, never by the order names were typed —
42
+ * validateStagedClose forces `needs` to name EARLIER stages, so close order IS
43
+ * a topological order. A `needs` cycle is inexpressible in a validated close for
44
+ * that same reason; the walk is still cycle-safe by construction (each stage is
45
+ * collected at most once), because a pure exported helper must terminate on
46
+ * whatever it is handed rather than hang the runner before any token burns.
47
+ *
48
+ * @param {any[]} close the validated stage list
49
+ * @returns {{name: string, run: any[]}[]} offerable stages, each with the ordered
50
+ * chain that must run for it (prerequisites first, the stage itself last)
51
+ */
52
+ export function checkMenu(close: any[]): {
53
+ name: string;
54
+ run: any[];
55
+ }[];
56
+ /**
57
+ * Content hash of a job spec — what an approval record binds to. Any SEMANTIC
58
+ * edit changes the hash, so a spec whose meaning moved is unapproved by
59
+ * construction (interview decision #1: human signs, always, once per spec
60
+ * VERSION). Not every BYTE edit: the hash is taken over the RESOLVED spec
61
+ * (resolveSpec), so writing today's full TOOL_MENU into a spec that omitted
62
+ * `tools` is hash-neutral — the two spell the same ceiling, and that identity
63
+ * is the point of MED-1, since it is what pins WHICH menu the signature covers.
64
+ * The direction that matters still bites: the menu itself changing (or the
65
+ * explicit list changing) moves the resolved form, and the hash with it.
23
66
  * @param {object} job
24
67
  * @returns {string} sha256 hex
25
68
  */
@@ -35,8 +78,11 @@ export function jobSpecHash(job: object): string;
35
78
  */
36
79
  export function checkApproval(job: object, approvals: unknown): boolean;
37
80
  export const CLOSE_TYPES: readonly string[];
38
- export const CLASSES: readonly string[];
39
- /** the hierarchy: which verdict classes a close type may claim (PRD §7) */
81
+ /** the hierarchy: which verdict classes a close type may claim (PRD §7). Read by
82
+ * the verdictType check — a declared verdict may not demand a class its close
83
+ * type cannot legally claim (green-on-rubric is laundering). The `CLASSES`
84
+ * vocabulary export went with steps[] (PRD v1.32): a class was a STEP field, and
85
+ * nothing declares one now. */
40
86
  export const CLASS_BY_CLOSE: Readonly<{
41
87
  predicate: readonly string[];
42
88
  gold: readonly string[];
@@ -44,8 +90,6 @@ export const CLASS_BY_CLOSE: Readonly<{
44
90
  hitl: readonly string[];
45
91
  }>;
46
92
  export const GOLD_COMPARE: readonly string[];
47
- /** step middle modes (2b): text = single-target artifact; tools = Gate-governed file tools */
48
- export const STEP_MODES: readonly string[];
49
93
  /** the tool grant menu (2b interview #1): read/grep/write, plus the two litectx
50
94
  * RETRIEVAL verbs (F19) — `run` stays locked-but-listed; a spec requesting it
51
95
  * reds, and that red IS the request-red evidence admission waits on (curation
@@ -65,8 +109,39 @@ export const STEP_MODES: readonly string[];
65
109
  * WRITE-class verb judged by the SAME writeScope fence as `write` (bareguard
66
110
  * action type 'edit'). Admitted because whole-file rewrite is measurably
67
111
  * unreliable at size (F31: 4 of 5 big-file whole-writes broke the tree) and
68
- * taxes output tokens ∝ file size. It admits no execution either. */
112
+ * taxes output tokens ∝ file size. It admits no execution either.
113
+ *
114
+ * P (design record 2026-07-28, hamr: full catalog) widened the menu to the four
115
+ * components — write · select · compress · isolate — every verb an EXISTING
116
+ * litectx implementation (menu-is-inventory). impact/related/recent/compress/
117
+ * peek are read-only like the retrieval pair; stash/remember/forget are
118
+ * STORE-writes (`.litectx`, never the tree — own gate action types, and the U
119
+ * runner deletes the store at reset so cold stays cold). None admits execution:
120
+ * the `run` lock is untouched. */
69
121
  export const TOOL_MENU: readonly string[];
122
+ /** the write-class split the validator and the scout filter read: tree writes
123
+ * (writeScope fence) vs store writes (isolate) — NEITHER is read-capable. */
124
+ export const WRITE_VERBS: readonly string[];
125
+ export const STORE_VERBS: readonly string[];
126
+ /**
127
+ * The floor for `maxWallMs`: one default close timeout (`runClose`'s 120_000).
128
+ * Addendum 1 MEASURED that a wall deadline can only be read BETWEEN rounds —
129
+ * `loop.stop()` does not cut an in-flight call (fired at 500ms, returned at
130
+ * 4,018ms) — so enforcement is `maxWallMs + closeStages × closeTimeoutMs` (W5: a
131
+ * staged close runs every stage under the FULL timeout, ralph.js `runStages`).
132
+ * Under this floor the overshoot exceeds the budget itself and the advertised
133
+ * number would be more wrong than right, which is the advertised-must-equal-
134
+ * enforced line.
135
+ *
136
+ * The VALUE is a one-stage, default-timeout floor and is deliberately left alone —
137
+ * a threshold is operator territory, not something this module re-derives from a
138
+ * spec's own stage count. It therefore bounds only the degenerate case; a job with
139
+ * many stages or a raised `closeTimeoutMs` can still sit far above it while its
140
+ * overshoot dwarfs its cap. The clock reports that honestly (`closeStages` and
141
+ * `enforcedMs` are both on the `wall-clock` record) rather than the floor pretending
142
+ * to prevent it.
143
+ */
144
+ export const MIN_WALL_MS: 120000;
70
145
  /** locked-but-listed tools: real capabilities deliberately outside the grant
71
146
  * menu. Requesting one is a DISTINCT `request-red` (module 4) — the ledger
72
147
  * counts admission demand, and a generic invalid-value would bury it as a typo. */
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Normalize a gap for comparison. Deliberately NARROW: only bytes a re-run changes
3
+ * on its own are removed. Error COUNTS, file names and line numbers all survive —
4
+ * a trajectory like "30 errors" → "22 errors" must read as two different gaps, and
5
+ * a normalizer that scrubbed numbers would turn the converging case this module
6
+ * exists to protect into an instant strike.
7
+ * @param {string} text @returns {string}
8
+ */
9
+ export function normalizeGap(text: string): string;
10
+ /**
11
+ * The comparable key for a gap: the normalized text, hashed. Hashed rather than
12
+ * kept whole because the seen-set lives for the whole step and a close's output
13
+ * can be tens of KB — and because the set holds no content, nothing here can leak
14
+ * model or file bytes into a record.
15
+ * @param {string} text @returns {string}
16
+ */
17
+ export function gapKey(text: string): string;
18
+ /**
19
+ * Build one ladder. ONE PER STEP: the seen-set and the strike count are the step's
20
+ * own history, and a ladder shared across steps would strike a fresh step for a gap
21
+ * an earlier one had already seen.
22
+ *
23
+ * @param {object} opts
24
+ * @param {number} [opts.limit] strikes that end the step (default `STRIKE_LIMIT`)
25
+ * @param {() => number} opts.writeCount the CUMULATIVE count of the worker's
26
+ * allow-decision write/edit records so far this step (planrun reads it off the
27
+ * gate audit). Required, and a throw when it is missing rather than a silent
28
+ * degradation to repeats-only: a strike rule running on one of its two signals
29
+ * with nothing on the record saying so is the blind-instrument class, and this
30
+ * is the BA-4 param-guard class where a throw is the right answer.
31
+ */
32
+ export function createLadder({ limit, writeCount }: {
33
+ limit?: number | undefined;
34
+ writeCount: () => number;
35
+ }): {
36
+ /**
37
+ * Read one RED iteration. Called by ralph after the judge has rendered a
38
+ * non-green, non-fault verdict — a green never reaches here, so every reading
39
+ * is a failing iteration by construction.
40
+ * @param {{iteration: number, gap?: string}} o
41
+ * @returns {{governor: string, iteration: number, strike: boolean, strikes: number,
42
+ * limit: number, wrote: boolean, repeatOf: number|null, distinctGaps: number}}
43
+ */
44
+ record({ iteration, gap }: {
45
+ iteration: number;
46
+ gap?: string;
47
+ }): {
48
+ governor: string;
49
+ iteration: number;
50
+ strike: boolean;
51
+ strikes: number;
52
+ limit: number;
53
+ wrote: boolean;
54
+ repeatOf: number | null;
55
+ distinctGaps: number;
56
+ };
57
+ /** has the step run out of strikes? */
58
+ struckOut: () => boolean;
59
+ /** the ladder's own books — counts and iteration numbers, never gap content */
60
+ report: () => {
61
+ iterations: number;
62
+ strikes: number;
63
+ limit: number;
64
+ distinctGaps: number;
65
+ repeats: {
66
+ at: number;
67
+ firstAt: number;
68
+ }[];
69
+ idle: number[];
70
+ };
71
+ /**
72
+ * The mechanism, in one sentence, for the replan brief. It is the only channel
73
+ * the redrafting planner adapts to, so it states WHICH shape ended the step and
74
+ * the evidence for it — never a label on its own, and never a culprit file
75
+ * beyond what the close itself reported (F28): the inputs here are iteration
76
+ * numbers and counts, so naming one is inexpressible.
77
+ * @returns {string}
78
+ */
79
+ brief(): string;
80
+ };
81
+ /**
82
+ * The strike ceiling. Shell-owned, exactly where the fixed count was: the runner
83
+ * sets it, the agent cannot express it, and it is arbiter territory — a step's own
84
+ * plan may not tighten it and may not raise it. Two is hamr's number.
85
+ */
86
+ export const STRIKE_LIMIT: 2;
package/types/ledger.d.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  * bareloop vocabulary). Deliberate exclusions — bare cap-halt (a budget story),
4
4
  * close-verdict reds and artifact-red (worker stories, the §5b line), gate-red
5
5
  * (governance working as intended), pr-red (operator environment, not a suite
6
- * lib), smoke-red escalations (already counted via primitive-smoke), hitl-close
6
+ * lib), smoke-red escalations (already counted via primitive-smoke), step-stalled
7
+ * (our own fuse firing on an ABSENCE of beats — no observed lib failure to file),
8
+ * hitl-close
7
9
  * and close-unsupported (by design) — classify to nothing.
8
10
  * @param {any[]} events one spine's parsed events, in seq order
9
11
  * @param {{spine?: string}} [opts] spine id stamped into samples
package/types/plan.d.ts CHANGED
@@ -1,3 +1,29 @@
1
+ /**
2
+ * The MENU of legal `tree-changed` scopes — choose-don't-describe (design record
3
+ * §4, hamr's correction: *"did agent choose from a list or not?"*).
4
+ *
5
+ * F60 measured 13 of 18 validator reds as ONE class: the agent writes
6
+ * `scope: "src/*.js"`, the grammar accepts only a trailing `/**` or `/*`, and
7
+ * each red costs a redraft call. Teaching the rule in the prompt or widening the
8
+ * grammar both leave the agent authoring a free-text string and guessing its
9
+ * shape. Enumerating instead makes an illegal scope INEXPRESSIBLE.
10
+ *
11
+ * This is the only producer of scope menus, and it filters by the signed fence —
12
+ * so a menu entry that escapes containment cannot be constructed, rather than
13
+ * being caught by a downstream check. `globToPrefix` is untouched: this changes
14
+ * what the agent is OFFERED, never what the fence ENFORCES.
15
+ *
16
+ * @param {string[]} writeScope the signed fence entries (always offerable — they
17
+ * cover directories a step has not created yet, which `snapshotScope` treats
18
+ * as an empty snapshot)
19
+ * @param {string[]} [dirs] workdir-relative directories discovered under the
20
+ * fence; each becomes `<dir>/**`
21
+ * @param {number} [cap] the menu bound; `Infinity` asks for the UNCAPPED set, so
22
+ * a caller can report how many entries the cap dropped (no silent caps — a menu
23
+ * that reads as complete when it is not is the blind-instrument class)
24
+ * @returns {string[]} the offerable scopes, deduped, shallowest-first, capped
25
+ */
26
+ export function legalScopes(writeScope: string[], dirs?: string[], cap?: number): string[];
1
27
  /**
2
28
  * F49 — a heuristic reject for the catastrophic-backtracking footgun: an
3
29
  * unbounded quantifier applied to a group whose body itself repeats unboundedly
@@ -34,22 +60,66 @@
34
60
  * @returns {boolean} true iff a nested unbounded quantifier is present
35
61
  */
36
62
  export function hasNestedQuantifier(src: string): boolean;
63
+ /**
64
+ * The close, as the ordered STAGE LIST the plan flow actually executes (PRD
65
+ * v1.28). A staged close is already that list; the legacy object form is the
66
+ * ONE-stage list it stands for, named `close`. Anything else — a gold, rubric or
67
+ * hitl close, which names no command to run — is null, and the runner escalates
68
+ * `close-unsupported` before any tokens.
69
+ *
70
+ * W4 — this is the ONE staging, and it exists because there used to be two. The
71
+ * runner synthesized the single stage and derived its check menu from THAT, while
72
+ * `planPrompt` and `validatePlan` called `checkMenu` on the RAW spec — where an
73
+ * object close is not an array, so the menu came back empty. The result was a
74
+ * runner announcing and preflighting a stage the drafter was never offered and the
75
+ * validator would have redded `check-unknown`. Same class as the two-transforms
76
+ * defects before it: the fix is one derivation every consumer calls, not three
77
+ * copies kept in step by comment.
78
+ *
79
+ * Lives here rather than beside `checkMenu` in job.js only because job.js owns the
80
+ * SIGNED vocabulary and this is the consumer-side reading of it; `plan.js` is the
81
+ * lowest module every check-menu consumer already imports (planrun imports plan,
82
+ * never the reverse), so it is the one place all three can share without a cycle.
83
+ * @param {any} close the spec's `close` field, any shape
84
+ * @returns {any[]|null} the stage list, or null when the close names no command
85
+ */
86
+ export function stageClose(close: any): any[] | null;
37
87
  /** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red */
38
88
  /**
39
89
  * Validate an agent-authored plan (`schema: "plan-v1"`) against the SIGNED
40
90
  * job spec. Never throws on JSON text or plain parsed data; every failure is
41
91
  * a named red. Returns the parsed plan on ok (single parse), null on any red.
42
92
  * @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
43
- * @param {{ job?: any, maxStepRounds?: number }} [opts] `job`: the
93
+ * @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[] }} [opts] `job`: the
44
94
  * validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
45
95
  * menu all come from it — a missing or non-plan-shape job fails CLOSED);
46
96
  * `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
47
- * tool-mode TURNS_PER_ATTEMPT) — a step may tighten it, never exceed it.
97
+ * tool-mode TURNS_PER_ATTEMPT) — a step may tighten it, never exceed it;
98
+ * `scopes`: the offered `tree-changed` menu from `legalScopes` (the SAME array
99
+ * the drafting prompt enumerated). Omitted, it derives from the signed
100
+ * writeScope — never a free-text fallback (F50).
101
+ *
102
+ * The two shape-lottery gate rules (2026-08-04) — both keyed on RECORDED
103
+ * facts, never an LLM judgment of "is this job small", and both inactive
104
+ * when their fact is omitted (resume/reuse callers stay byte-identical):
105
+ * `seedRed`: check names whose PREFLIGHT verdict was needs_revision. Rule
106
+ * A-v2 — a seed-red check is the goal itself, not a milestone; it may only
107
+ * sit on the plan's FINAL write step (`check-placement`). The $0 archive
108
+ * sweep: seed-red-check-on-an-early-step has 0 honest greens ever; the one
109
+ * wide closing step greens 7/7. Green-at-seed checks stay free mid-plan
110
+ * (the 20 TESTGEN mid-plan greens — a guard, not a goal).
111
+ * `priorChecks`: the check names the PREDECESSOR plan carried (replan gate
112
+ * only). Rule B — a redraft may not drop one (`check-shed`): exits without
113
+ * the check verify FORM alone, so a shed lets the run "green" unearned
114
+ * (u-msdsmkid mechanism b; the 3 archived sheds were all on step-red runs).
48
115
  * @returns {{ ok: boolean, reds: Red[], plan: object|null }}
49
116
  */
50
- export function validatePlan(input: object | string, { job, maxStepRounds }?: {
117
+ export function validatePlan(input: object | string, { job, maxStepRounds, scopes, seedRed, priorChecks }?: {
51
118
  job?: any;
52
119
  maxStepRounds?: number;
120
+ scopes?: string[];
121
+ seedRed?: string[];
122
+ priorChecks?: string[];
53
123
  }): {
54
124
  ok: boolean;
55
125
  reds: Red[];
@@ -65,13 +135,22 @@ export const MAX_EXITS_PER_STEP: 2;
65
135
  /** plan size bound — per-step rounds bound the spend, but an unbounded step
66
136
  * COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
67
137
  export const MAX_PLAN_STEPS: 8;
68
- /** the write-class verbs (BA-13: `edit` is judged by the same fence as
69
- * `write`) — a step granting one is a WRITE step for the F17 pairing rule
70
- * and the v1.18 target requirement */
71
- export const WRITE_VERBS: readonly string[];
138
+ export { WRITE_VERBS };
139
+ /** scope-menu bound: the menu is a PROMPT ingredient on every draft and
140
+ * redraft, so an unbounded repo would price the drafting call by its directory
141
+ * count. Shallow entries survive the cap (see `legalScopes`) because they are
142
+ * the useful ones — a deep leaf is reachable via its parent. */
143
+ export const MAX_SCOPE_MENU: 24;
144
+ /** P (design record 2026-07-28): the signed per-step model-tier menu. Closed by
145
+ * construction — the agent picks a tier, never names an arbitrary model string
146
+ * (the tier→model mapping and any effort params are the RUNNER's, per provider).
147
+ * opus is deliberately absent: reserved for fresh builds hamr explicitly assigns,
148
+ * never plan-selectable. */
149
+ export const STEP_MODELS: readonly string[];
72
150
  export type Red = {
73
151
  code: string;
74
152
  path: string;
75
153
  detail?: string;
76
154
  verb?: string;
77
155
  };
156
+ import { WRITE_VERBS } from './job.js';
@@ -1,5 +1,58 @@
1
1
  /**
2
- * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close/checks[]).
2
+ * Files named in an exit gap that the step's worker never wrote — the mechanical
3
+ * fact behind the prose-prohibition trap (u-msdpuaej, 2026-08-03): a drafter wrote
4
+ * "do not modify any file outside X" into a step whose exit check judges the whole
5
+ * goal, the worker obeyed, and the replanner — handed the gap AND the stall note —
6
+ * rebuilt the same shape, because inferring "the exit reds on files this step never
7
+ * touched" is exactly the semantic leap that fails (F38: mechanical gaps convert,
8
+ * semantic ones stall; 3/3 across the mailbox class). This computes that fact from
9
+ * the two books that already exist: path-like tokens in the gap text (extension
10
+ * required — a bare `step/two` is not a file claim) suffix-matched against the F32
11
+ * write audit's absolute paths. Advisory-only and fail-safe: extraction that finds
12
+ * nothing yields no line, never a guess.
13
+ *
14
+ * @param {string | undefined} gap the step's last exit gap text
15
+ * @param {string[] | undefined} writes the step's cumulative write-audit paths
16
+ * @returns {string[]} gap-named files with no matching write, in gap order
17
+ */
18
+ export function gapFilesNeverWritten(gap: string | undefined, writes: string[] | undefined): string[];
19
+ /**
20
+ * The plan-drafting prompt: a schema DESCRIPTION built from the live validator
21
+ * menus — never a copyable example (the drafter must author, not echo; the
22
+ * run.js draftPrompt precedent). Check NAMES only: a check's command is
23
+ * arbiter territory the planner never sees.
24
+ * @param {any} job @param {string} scoutBlob @param {any[]|null} reds
25
+ * @param {number} maxStepRounds @param {string|null} failure replan context
26
+ * @param {string[]|undefined} scopes the offered `tree-changed` menu (`legalScopes`).
27
+ * Choose-don't-describe (§4): the agent picks a value, never authors a glob and
28
+ * guesses its shape. Omitted, it derives from the signed fence — the SAME
29
+ * fail-closed derivation `validatePlan` uses, so prompt and validator can never
30
+ * disagree about what was offered.
31
+ * @param {{ balanceUsd?: number|null, remainingMs?: number|null, progress?: string }} [materials]
32
+ * what the run has LEFT (T/A). Omitted → no materials block at all, which is the
33
+ * pre-T behaviour and keeps every existing caller byte-identical.
34
+ * @param {object|null} [startingDraft] Layer 3 (D4) — a loaded bridge's plan, handed over
35
+ * as the draft to TWEAK. Omitted/null is the COLD path and renders byte-identically to
36
+ * the pre-Layer-3 prompt: the block is additive, never surgery on the prompt's interior,
37
+ * so the two paths cannot drift (the F47 works-both-ways rule). Only the runner passes
38
+ * it, and only for the FIRST draft phase — a replan drafts from the run's own state.
39
+ * @param {{ seedRed?: string[], priorChecks?: string[] }} [checkFacts] the shape-lottery
40
+ * gate rules, STATED as well as enforced (the mailbox precedent: a rule only the
41
+ * validator knows costs a redraft per draft). `seedRed`: preflight-red check names —
42
+ * Rule A-v2's law (final write step only, framed wide). `priorChecks`: the predecessor
43
+ * plan's carried checks, replan phase only — Rule B's law (keep every one). Empty/omitted
44
+ * renders byte-identically to the pre-rules prompt.
45
+ */
46
+ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxStepRounds: number, failure: string | null, scopes: string[] | undefined, materials?: {
47
+ balanceUsd?: number | null;
48
+ remainingMs?: number | null;
49
+ progress?: string;
50
+ }, startingDraft?: object | null, checkFacts?: {
51
+ seedRed?: string[];
52
+ priorChecks?: string[];
53
+ }): string;
54
+ /**
55
+ * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close).
3
56
  * Called by runJob after the approval gate, validation, and the smoke — this
4
57
  * function owns the plan flow only; the caller owns the job-end record and the
5
58
  * one ledger (every provider round is emitted here as `worker-round`, which
@@ -20,9 +73,25 @@
20
73
  * @param {() => number} opts.remainingUsd the one wallet: what is left of the signed budget right now
21
74
  * @param {() => boolean} [opts.isUnpriced] has any round come back with a null cost? (F6) — the
22
75
  * plan flow bails IN-FLIGHT on the first unpriced round instead of burning the whole plan
23
- * @param {number} [opts.capRuns] shell-owned per-step attempt cap
76
+ * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier factory
77
+ * (tier ∈ STEP_MODELS → a provider). A plan naming a tier with no factory supplied is an
78
+ * interpreter-red STOP — never silently run the default tier as if the choice was honoured.
79
+ * @param {number} [opts.capRuns] shell-owned iteration cap, RETIRED as a governor
80
+ * (v1.46 §4). It stopped bounding a plan step when the strike ladder landed, and it
81
+ * stopped bounding the close-fix loop when the close trend rule did. It survives as
82
+ * that loop's fallback for the ONE case the trend cannot read — a close whose output
83
+ * carries no number at all — because a governor that cannot see the variable must
84
+ * not be the governor, and the honest fallback is the cruder bound it replaced
85
+ * rather than "unbounded". It lifts the moment a stage reports a comparable number.
86
+ * @param {number} [opts.strikeLimit] shell-owned STRIKE ceiling for a plan step's
87
+ * ladder (src/ladder.js). Arbiter territory exactly as the count it replaced was —
88
+ * the runner sets it, the plan cannot express it, and no step may tighten or raise
89
+ * it.
24
90
  * @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
25
91
  * @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
92
+ * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
93
+ * reserved — a scout that spends every round on tools gets one toolless round to write its
94
+ * survey, because the bound halts it mid-tool-use and text is its only deliverable)
26
95
  * @param {boolean} [opts.layerRoot=false] Layer R — the within-run ratchet (src/root.js),
27
96
  * scoped PER STEP's ralph loop (each micro-wheel is the Layer-1 atom). Shell-assembled from
28
97
  * the step's own books: per-attempt write-sets from the F32 workerWrites audit (teed for
@@ -32,11 +101,67 @@
32
101
  * first plan-flow job to emit `root-injected` runs the pre-registered ON-vs-OFF acceptance
33
102
  * read (the Layer R default-flip, LAYERS.md ⚠). Excluded on native (clipipe): the native
34
103
  * worker has no onToolResult seam, so the tee cannot settle and same-path rewrites are blind.
104
+ * @param {unknown} [opts.bridge] Layer 3 (D3/D4) — a bridge-v1 registry entry to REUSE as the
105
+ * drafter's starting point. Absent is the COLD path and is byte-identical to the pre-Layer-3
106
+ * flow. Present, the D2-split LOAD GATE runs at the door, before the close precheck and
107
+ * before any token: on a pass the newest version's plan (the run-as-executed artifact)
108
+ * rides into the FIRST draft prompt only; on a fail the run returns the distinct terminal
109
+ * `recipe-stale` having spent nothing. Selecting the entry — the listing, the pin, the
110
+ * LLM's pick — is the CALLER's, and so is falling back to cold: a silent automatic
111
+ * fall-back would spend a run's budget on a decision nobody made.
112
+ * @param {number} [opts.priorWallMs=0] RESUME (module C) — wall time a PREVIOUS, killed
113
+ * attempt of this same try already consumed, folded into the run clock (see
114
+ * `createClock`'s `priorElapsedMs`). The SIGNED `maxWallMs` is never rewritten: it is in
115
+ * the spec hash, and a resume that edited it would need a signature nobody typed. The
116
+ * restarted attempt simply starts already partly spent, so a kill can never buy a fresh
117
+ * allotment of the operator's wall.
118
+ * @param {any} [opts.resumeSeed] RESUME (module C v2) — the FINEST honest checkpoint of a
119
+ * KILLED run of this same try, read off its own spine by `readResume` (src/reuse.js) and
120
+ * by nothing else: `{ phase, plan, completedSteps: [{id, seq, by}] }`. hamr's ruling is
121
+ * the whole contract — *"it should allow resume and start last step instead from the
122
+ * beginning, why would i want to waste more money on something i already started"* — so
123
+ * with a seed this flow re-pays for NO paid unit the dead run already bought:
124
+ *
125
+ * - the SCOUT does not run (`scout-skipped`, never silence). Its survey is not on the
126
+ * spine (only its byte count is), so the ONE consequence is stated rather than
127
+ * discovered: a replan after a resume drafts from an empty survey plus the failure
128
+ * brief. Re-scouting to refill it would re-pay the exact call this exists to save.
129
+ * - the PLAN is not re-drafted. It is reloaded from the seed and RE-VALIDATED against
130
+ * the current signed spec; a plan that no longer validates is `plan-red` by name
131
+ * (`resume-plan-red`), never run. It is then emitted as this leg's own
132
+ * `plan-accepted` (`phase: 'resume'`) — R1 mints a bridge version from the plan AS
133
+ * EXECUTED, read back off the spine, so a leg that greens without one would mint
134
+ * nothing.
135
+ * - a step whose exit was already SATISFIED is skipped, in prefix order, each with its
136
+ * own `step-skipped` record naming the event that proves it. Never a fake
137
+ * step-start/step-end pair, and never silence: a reader must be able to tell skipped
138
+ * from run (the resume-to-cap invariant).
139
+ *
140
+ * The worker's conversation is deliberately NOT replayed — a transcript is not a
141
+ * checkpoint, and every attempt is fresh by the loop's own design. Absent is the
142
+ * ordinary path and is byte-identical to a run that was never killed.
143
+ * @param {{stage?: string|null, value?: number|null}[]} [opts.resumeGrades] RESUME — the
144
+ * close GRADES the dead leg recorded, oldest first (`readResume`'s `restart.grades`).
145
+ * A resumed leg is the same run continuing, so its halt readouts must judge the whole
146
+ * chain: without this the trend restarts at this leg's first close and can report
147
+ * `flat` — "revise the goal" — on a run that was converging when its allowance ran
148
+ * out. Baselines only (src/trend.js's THE SEED): it spends none of this leg's own
149
+ * bounds and mints none of its strikes. Empty/omitted is the cold path.
150
+ * @param {() => boolean} [opts.spendComplete] is the run's spend EXACT, or a floor? The
151
+ * ledger one level up owns the answer (unpriced rounds, a self-healed stall, a mid-call
152
+ * cut, an inherited resume floor); the money-halt readout states it beside the remaining
153
+ * it quotes, because `budget − floor` is a CEILING and printing it bare reads as exact
154
+ * (F6). Defaults to exact, which is what every pre-record caller was implicitly saying.
155
+ * @param {() => number} [opts.now] the wall clock's time source, injected. The real clock is the
156
+ * default; a caller supplies this to drive time deterministically (the same seam `createClock`
157
+ * already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
158
+ * whose floor is one close timeout).
35
159
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
36
- * 'check-red' | 'close-red' | 'close-unsupported' | 'pricing-red' | 'cap-halt' |
37
- * 'provider-red' | 'interpreter-red' | `step-red:<id>`
160
+ * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
161
+ * 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' |
162
+ * `step-red:<id>`
38
163
  */
39
- export function runPlan(job: any, { workdir, provider, nativeProvider, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds, layerRoot }: {
164
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades }: {
40
165
  workdir: string;
41
166
  provider: any;
42
167
  nativeProvider?: ((o: {
@@ -48,10 +173,22 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, emit, rem
48
173
  emit: (type: string, data?: object) => object;
49
174
  remainingUsd: () => number;
50
175
  isUnpriced?: (() => boolean) | undefined;
176
+ providerFor?: ((tier: string) => any) | undefined;
51
177
  capRuns?: number | undefined;
178
+ strikeLimit?: number | undefined;
52
179
  closeTimeoutMs?: number | undefined;
53
180
  maxStepRounds?: number | undefined;
181
+ scoutRounds?: number | undefined;
54
182
  layerRoot?: boolean | undefined;
183
+ bridge?: unknown;
184
+ priorWallMs?: number | undefined;
185
+ resumeSeed?: any;
186
+ resumeGrades?: {
187
+ stage?: string | null;
188
+ value?: number | null;
189
+ }[] | undefined;
190
+ spendComplete?: (() => boolean) | undefined;
191
+ now?: (() => number) | undefined;
55
192
  }): Promise<string>;
56
193
  export type CategorizedError = Error & {
57
194
  category?: string;