bareloop 0.5.0 → 0.6.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/plan.d.ts CHANGED
@@ -1,19 +1,110 @@
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[];
27
+ /**
28
+ * F49 — a heuristic reject for the catastrophic-backtracking footgun: an
29
+ * unbounded quantifier applied to a group whose body itself repeats unboundedly
30
+ * (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
31
+ * group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
32
+ * and is caught by propagating the inner repeat up through the wrapper. Such a
33
+ * pattern can hang `RegExp.test` for
34
+ * seconds on a short crafted body, and `evalExits` runs the agent-authored
35
+ * `artifact-written` pattern with NO timeout. This is self-DoS only — the agent
36
+ * authors both the pattern and (via the worker) the artifact, so a hang burns
37
+ * only its own run's wall-clock; there is NO arbiter compromise (it cannot
38
+ * escape the fence, forge a green, or leak a secret). So the reject targets the
39
+ * dominant exponential class and fails it as a mechanical gap at the validation
40
+ * gate, before any tokens burn. A full ReDoS analyzer needs a real regex engine
41
+ * (an external native dep we do not take for a LOW issue); exotic
42
+ * overlapping-alternation blowup is out of scope by decision.
43
+ *
44
+ * SCOPE IS ASYMMETRIC, both directions named on purpose:
45
+ * - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
46
+ * detected — out of scope (self-DoS only, no arbiter compromise).
47
+ * - false POSITIVE: a group whose repetitions are disambiguated by a literal
48
+ * anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
49
+ * though a real engine runs it linearly — the scan sees the nested-quantifier
50
+ * SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
51
+ * never admits an unsafe pattern), and the cost is bounded: the plan drafter
52
+ * gets a mechanical gap and rewrites (drop the outer `+`, or match once).
53
+ * Detecting "safe because anchored" needs the same real engine we declined,
54
+ * and guessing it wrong would ADMIT an exponential pattern — so the shape
55
+ * reject stands, and the over-rejection is a named, accepted limitation.
56
+ *
57
+ * The input is guaranteed to compile as a RegExp (checked first by the caller),
58
+ * so this scan assumes valid, balanced JS regex syntax.
59
+ * @param {string} src a compiled regex source string
60
+ * @returns {boolean} true iff a nested unbounded quantifier is present
61
+ */
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;
1
87
  /** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red */
2
88
  /**
3
89
  * Validate an agent-authored plan (`schema: "plan-v1"`) against the SIGNED
4
90
  * job spec. Never throws on JSON text or plain parsed data; every failure is
5
91
  * a named red. Returns the parsed plan on ok (single parse), null on any red.
6
92
  * @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
7
- * @param {{ job?: any, maxStepRounds?: number }} [opts] `job`: the
93
+ * @param {{ job?: any, maxStepRounds?: number, scopes?: string[], capRuns?: number }} [opts] `job`: the
8
94
  * validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
9
95
  * menu all come from it — a missing or non-plan-shape job fails CLOSED);
10
96
  * `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
11
- * 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).
12
101
  * @returns {{ ok: boolean, reds: Red[], plan: object|null }}
13
102
  */
14
- export function validatePlan(input: object | string, { job, maxStepRounds }?: {
103
+ export function validatePlan(input: object | string, { job, maxStepRounds, scopes, capRuns }?: {
15
104
  job?: any;
16
105
  maxStepRounds?: number;
106
+ scopes?: string[];
107
+ capRuns?: number;
17
108
  }): {
18
109
  ok: boolean;
19
110
  reds: Red[];
@@ -29,13 +120,22 @@ export const MAX_EXITS_PER_STEP: 2;
29
120
  /** plan size bound — per-step rounds bound the spend, but an unbounded step
30
121
  * COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
31
122
  export const MAX_PLAN_STEPS: 8;
32
- /** the write-class verbs (BA-13: `edit` is judged by the same fence as
33
- * `write`) — a step granting one is a WRITE step for the F17 pairing rule
34
- * and the v1.18 target requirement */
35
- export const WRITE_VERBS: readonly string[];
123
+ export { WRITE_VERBS };
124
+ /** scope-menu bound: the menu is a PROMPT ingredient on every draft and
125
+ * redraft, so an unbounded repo would price the drafting call by its directory
126
+ * count. Shallow entries survive the cap (see `legalScopes`) because they are
127
+ * the useful ones — a deep leaf is reachable via its parent. */
128
+ export const MAX_SCOPE_MENU: 24;
129
+ /** P (design record 2026-07-28): the signed per-step model-tier menu. Closed by
130
+ * construction — the agent picks a tier, never names an arbitrary model string
131
+ * (the tier→model mapping and any effort params are the RUNNER's, per provider).
132
+ * opus is deliberately absent: reserved for fresh builds hamr explicitly assigns,
133
+ * never plan-selectable. */
134
+ export const STEP_MODELS: readonly string[];
36
135
  export type Red = {
37
136
  code: string;
38
137
  path: string;
39
138
  detail?: string;
40
139
  verb?: string;
41
140
  };
141
+ import { WRITE_VERBS } from './job.js';
@@ -1,5 +1,30 @@
1
1
  /**
2
- * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close/checks[]).
2
+ * The plan-drafting prompt: a schema DESCRIPTION built from the live validator
3
+ * menus — never a copyable example (the drafter must author, not echo; the
4
+ * run.js draftPrompt precedent). Check NAMES only: a check's command is
5
+ * arbiter territory the planner never sees.
6
+ * @param {any} job @param {string} scoutBlob @param {any[]|null} reds
7
+ * @param {number} maxStepRounds @param {string|null} failure replan context
8
+ * @param {string[]|undefined} scopes the offered `tree-changed` menu (`legalScopes`).
9
+ * Choose-don't-describe (§4): the agent picks a value, never authors a glob and
10
+ * guesses its shape. Omitted, it derives from the signed fence — the SAME
11
+ * fail-closed derivation `validatePlan` uses, so prompt and validator can never
12
+ * disagree about what was offered.
13
+ * @param {{ balanceUsd?: number|null, remainingMs?: number|null, progress?: string }} [materials]
14
+ * what the run has LEFT (T/A). Omitted → no materials block at all, which is the
15
+ * pre-T behaviour and keeps every existing caller byte-identical.
16
+ * @param {number} [capRuns] the shell's attempt cap — the ceiling `attempts` may only
17
+ * TIGHTEN. Interpolated the way every sibling bound is: `validatePlan` reds outside
18
+ * `1..capRuns`, and a prompt that says only "integer" spends a round teaching it.
19
+ * Defaults to validatePlan's own default so existing callers are unchanged.
20
+ */
21
+ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxStepRounds: number, failure: string | null, scopes: string[] | undefined, materials?: {
22
+ balanceUsd?: number | null;
23
+ remainingMs?: number | null;
24
+ progress?: string;
25
+ }, capRuns?: number): string;
26
+ /**
27
+ * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close).
3
28
  * Called by runJob after the approval gate, validation, and the smoke — this
4
29
  * function owns the plan flow only; the caller owns the job-end record and the
5
30
  * one ledger (every provider round is emitted here as `worker-round`, which
@@ -20,14 +45,33 @@
20
45
  * @param {() => number} opts.remainingUsd the one wallet: what is left of the signed budget right now
21
46
  * @param {() => boolean} [opts.isUnpriced] has any round come back with a null cost? (F6) — the
22
47
  * plan flow bails IN-FLIGHT on the first unpriced round instead of burning the whole plan
48
+ * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier factory
49
+ * (tier ∈ STEP_MODELS → a provider). A plan naming a tier with no factory supplied is an
50
+ * interpreter-red STOP — never silently run the default tier as if the choice was honoured.
23
51
  * @param {number} [opts.capRuns] shell-owned per-step attempt cap
24
52
  * @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
25
53
  * @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
54
+ * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
55
+ * reserved — a scout that spends every round on tools gets one toolless round to write its
56
+ * survey, because the bound halts it mid-tool-use and text is its only deliverable)
57
+ * @param {boolean} [opts.layerRoot=false] Layer R — the within-run ratchet (src/root.js),
58
+ * scoped PER STEP's ralph loop (each micro-wheel is the Layer-1 atom). Shell-assembled from
59
+ * the step's own books: per-attempt write-sets from the F32 workerWrites audit (teed for
60
+ * same-path rewrites, which the cumulative audit alone cannot see) and the red-set from the
61
+ * exit evaluator's own gap. Defaults OFF (decided 2026-07-21, F41): fixation is extinct on
62
+ * every current job, so ON has never won its A/B; `true` is the ON/experimental arm, and the
63
+ * first plan-flow job to emit `root-injected` runs the pre-registered ON-vs-OFF acceptance
64
+ * read (the Layer R default-flip, LAYERS.md ⚠). Excluded on native (clipipe): the native
65
+ * worker has no onToolResult seam, so the tee cannot settle and same-path rewrites are blind.
66
+ * @param {() => number} [opts.now] the wall clock's time source, injected. The real clock is the
67
+ * default; a caller supplies this to drive time deterministically (the same seam `createClock`
68
+ * already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
69
+ * whose floor is one close timeout).
26
70
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
27
71
  * 'check-red' | 'close-red' | 'close-unsupported' | 'pricing-red' | 'cap-halt' |
28
- * 'provider-red' | 'interpreter-red' | `step-red:<id>`
72
+ * 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' | `step-red:<id>`
29
73
  */
30
- export function runPlan(job: any, { workdir, provider, nativeProvider, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds }: {
74
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, now }: {
31
75
  workdir: string;
32
76
  provider: any;
33
77
  nativeProvider?: ((o: {
@@ -39,9 +83,13 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, emit, rem
39
83
  emit: (type: string, data?: object) => object;
40
84
  remainingUsd: () => number;
41
85
  isUnpriced?: (() => boolean) | undefined;
86
+ providerFor?: ((tier: string) => any) | undefined;
42
87
  capRuns?: number | undefined;
43
88
  closeTimeoutMs?: number | undefined;
44
89
  maxStepRounds?: number | undefined;
90
+ scoutRounds?: number | undefined;
91
+ layerRoot?: boolean | undefined;
92
+ now?: (() => number) | undefined;
45
93
  }): Promise<string>;
46
94
  export type CategorizedError = Error & {
47
95
  category?: string;
package/types/ralph.d.ts CHANGED
@@ -60,9 +60,9 @@
60
60
  * (the workdir — the tree it is judging), the exit code the SIGNED spec calls
61
61
  * success, the judgment-rendered signal, and the kept-failures pattern. All
62
62
  * five are arbiter territory: the drafted workflow config cannot express any.
63
- * @returns {{verdict: 'satisfied'|'needs_revision'|'failed'|'timed-out'|'killed'|'crashed',
63
+ * @returns {Promise<{verdict: 'satisfied'|'needs_revision'|'failed'|'timed-out'|'killed'|'crashed',
64
64
  * gap?: string, exitCode?: number, signal?: string, detail?: string,
65
- * judgedCount?: number|null, unaudited?: boolean}}
65
+ * judgedCount?: number|null, unaudited?: boolean}>}
66
66
  */
67
67
  export function runClose(close: string[], redact?: (s: string) => string, { timeoutMs, cwd, expect, judged, gapKeep }?: {
68
68
  timeoutMs?: number;
@@ -73,7 +73,7 @@ export function runClose(close: string[], redact?: (s: string) => string, { time
73
73
  min: number;
74
74
  };
75
75
  gapKeep?: string;
76
- }): {
76
+ }): Promise<{
77
77
  verdict: "satisfied" | "needs_revision" | "failed" | "timed-out" | "killed" | "crashed";
78
78
  gap?: string;
79
79
  exitCode?: number;
@@ -81,7 +81,36 @@ export function runClose(close: string[], redact?: (s: string) => string, { time
81
81
  detail?: string;
82
82
  judgedCount?: number | null;
83
83
  unaudited?: boolean;
84
- };
84
+ }>;
85
+ /**
86
+ * Run a STAGED close (PRD v1.28) — an ordered list of named command stages, run
87
+ * in order until one renders a verdict that is not `satisfied`. The return is
88
+ * runClose's own shape, so every consumer (the precheck, ralph's judge, the
89
+ * escalation's CLOSE_FAULTS routing) is unchanged: a staged close is still ONE
90
+ * verdict, not a scoreboard.
91
+ *
92
+ * Two additions, both mechanical information the worker can act on:
93
+ * - `stage` names which stage rendered the verdict. The close's output must
94
+ * never name the CULPRIT FILE (v1.12/F28 — the worker finds that itself), and
95
+ * naming the wall it hit is the opposite of that: it is the mechanical genre
96
+ * that converts (F38/F46), not the answer.
97
+ * - `stages` is the ordered record of what actually ran. A stage after the
98
+ * first red never runs, so a later green can never mask an earlier failure.
99
+ *
100
+ * The same function runs a DERIVED CHECK: a check is its stage's chain
101
+ * (`checkMenu` puts the prerequisites first), so there is one execution path for
102
+ * the close and for the rulers derived from it — never two, which is the F9
103
+ * two-transforms class applied to the arbiter.
104
+ *
105
+ * @param {any[]} stages the stage list (already validated by validateJob)
106
+ * @param {(s: string) => string} [redact] scrub, applied at capture on EVERY stage
107
+ * @param {{timeoutMs?: number, cwd?: string}} [opts]
108
+ * @returns {Promise<any>} runClose's verdict shape, plus `stage` and `stages`
109
+ */
110
+ export function runStages(stages: any[], redact?: (s: string) => string, { timeoutMs, cwd }?: {
111
+ timeoutMs?: number;
112
+ cwd?: string;
113
+ }): Promise<any>;
85
114
  /**
86
115
  * The loop: `while close-red and under-cap: run the middle`. Stops at first
87
116
  * green (within-run tuning past a visible close is the fit-to-pass surface,
@@ -151,6 +180,35 @@ export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeou
151
180
  gapKeep?: string | undefined;
152
181
  workerWrites?: (() => string[]) | undefined;
153
182
  }): Promise<"green" | "escalated">;
183
+ /**
184
+ * What the close child must NOT be able to read (operator ruling, 2026-07-31:
185
+ * "strip it when unnecessary, minimize unneeded exposure").
186
+ *
187
+ * The close child executes WORKER-AUTHORED CODE. `pytest`, `npm test`, `tsc`
188
+ * run whatever the worker just wrote into the tree, in a child that — before
189
+ * this — inherited the whole operator environment, credentials included. None
190
+ * of git/npm/tsc/pytest/mypy needs an API key to judge a tree, so the exposure
191
+ * bought nothing and is removed. This is EXPOSURE REDUCTION, not a sandbox:
192
+ * see the threat-model note in the PRD (v1.41) and README for what remains.
193
+ *
194
+ * Two rules, deliberately kept BOTH ways round:
195
+ * - `names` / `prefixes` — the knowns. A literal denylist is a chase (every
196
+ * new provider mints a new variable), so it is not load-bearing on its own;
197
+ * it exists for names the shape rule cannot see (`AWS_ACCESS_KEY_ID`).
198
+ * - `shape` — the NAME-SHAPE rule, which is what bounds the chase: anything
199
+ * whose name ends in the credential suffixes goes, whoever minted it and
200
+ * whether or not anyone here has heard of it. Case-insensitive; no `g` flag,
201
+ * so `.test` stays stateless and safe to reuse across every key.
202
+ *
203
+ * It is arbiter territory like the rest of this file: no drafted plan or config
204
+ * can express, widen, or opt out of it.
205
+ * @type {Readonly<{names: readonly string[], prefixes: readonly string[], shape: RegExp}>}
206
+ */
207
+ export const CLOSE_ENV_DENY: Readonly<{
208
+ names: readonly string[];
209
+ prefixes: readonly string[];
210
+ shape: RegExp;
211
+ }>;
154
212
  export const GAP_KEEP_TRIM_MARKER: "more elided by the";
155
213
  /**
156
214
  * The forbidden zone, named (PRD v1.11). Each row is an outcome in which the
package/types/root.d.ts CHANGED
@@ -66,13 +66,21 @@ export function createRoot({ gapKeep, redact, writesInformative }?: {
66
66
  * Called at the START of attempt `iteration`, with the PREVIOUS attempt's
67
67
  * gap. Finalizes that attempt from the books, runs the detector, and
68
68
  * returns the injection for the attempt about to run — or null (inert).
69
- * @param {{iteration: number, gap?: string, writes: string[]}} o
69
+ *
70
+ * `redStage`/`redKeep` are the STAGED-CLOSE seam (PRD v1.28): a close is an
71
+ * ordered list of named stages, the first red renders the verdict, so the
72
+ * pattern that designates failures belongs to whichever stage rendered it
73
+ * THIS attempt. Callers with a single red-set source (the per-step exit
74
+ * evaluator) pass neither and keep the constructor's run-wide pattern.
75
+ * @param {{iteration: number, gap?: string, writes: string[], redStage?: string, redKeep?: string}} o
70
76
  * @returns {null | {stage: 'summary'|'verbatim', note: string, event: object}}
71
77
  */
72
- observe({ iteration, gap, writes }: {
78
+ observe({ iteration, gap, writes, redStage, redKeep }: {
73
79
  iteration: number;
74
80
  gap?: string;
75
81
  writes: string[];
82
+ redStage?: string;
83
+ redKeep?: string;
76
84
  }): null | {
77
85
  stage: "summary" | "verbatim";
78
86
  note: string;
package/types/run.d.ts CHANGED
@@ -27,32 +27,31 @@
27
27
  * @param {object} opts
28
28
  * @param {unknown} opts.approvals `{ specHash, signer, ts }` records (from OUTSIDE the spec)
29
29
  * @param {string} opts.workdir the run directory (the fence's root)
30
- * @param {string} [opts.target] the step artifact path (text-mode steps; unused by tool-mode steps)
31
30
  * @param {any} opts.provider shell-owned LLM binding (never the config's; the Loop path)
32
31
  * @param {(o: {policy: Function, onTurn?: Function, maxTurns: number, hasTools: boolean}) => any} [opts.nativeProvider]
33
32
  * native clipipe provider factory (BA-16), required for a `clipipe-subscription` plan-shape job —
34
33
  * threaded to runPlan, which builds one per worker (native tool mode when `hasTools`, else a
35
34
  * metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
36
35
  * @param {(type: string, data?: object) => object} opts.emit spine emitter
37
- * @param {number} [opts.capRuns] shell-owned per-step iteration cap
36
+ * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
37
+ * @param {number} [opts.capRuns] shell-owned per-step attempt cap
38
38
  * @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
39
39
  * @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
40
- * @param {ExecCmd} [opts.execCmd] process runner for the hitl PR mechanics
41
40
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
42
- * territory, threaded to every predicate step's interpreter. Defaults OFF
41
+ * territory, threaded to the plan flow. Defaults OFF
43
42
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
44
43
  * ON has never won its own A/B — the acceptance read defers to Layer 2 (or
45
44
  * a manufactured-fixation probe). `true` is the ON/experimental arm; the
46
45
  * default is the OFF arm. (Shell-owned seam, same doctrine as the provider
47
46
  * binding.)
48
- * @returns {Promise<string>} outcome: 'green' | 'escalated' | 'unapproved-spec' |
49
- * 'job-red' | 'smoke-red' | 'config-red' | 'pricing-red' | 'provider-red' |
50
- * 'cap-halt' | 'close-unsupported' | `step-red:<id>`
47
+ * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
48
+ * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
49
+ * 'close-red' | 'close-unsupported' | 'pricing-red' | 'provider-red' |
50
+ * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
51
51
  */
52
- export function runJob(rawSpec: object | string, { approvals, workdir, target, provider, nativeProvider, emit, capRuns, shellCapUsd, closeTimeoutMs, execCmd, layerRoot }: {
52
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, shellCapUsd, closeTimeoutMs, layerRoot }: {
53
53
  approvals: unknown;
54
54
  workdir: string;
55
- target?: string | undefined;
56
55
  provider: any;
57
56
  nativeProvider?: ((o: {
58
57
  policy: Function;
@@ -61,10 +60,10 @@ export function runJob(rawSpec: object | string, { approvals, workdir, target, p
61
60
  hasTools: boolean;
62
61
  }) => any) | undefined;
63
62
  emit: (type: string, data?: object) => object;
63
+ providerFor?: ((tier: string) => any) | undefined;
64
64
  capRuns?: number | undefined;
65
65
  shellCapUsd?: number | undefined;
66
66
  closeTimeoutMs?: number | undefined;
67
- execCmd?: ExecCmd | undefined;
68
67
  layerRoot?: boolean | undefined;
69
68
  }): Promise<string>;
70
69
  export type Red = {
@@ -72,10 +71,3 @@ export type Red = {
72
71
  path: string;
73
72
  detail?: string;
74
73
  };
75
- export type ExecCmd = (argv: string[], opts?: {
76
- cwd?: string;
77
- }) => {
78
- status: number | null;
79
- stdout: string;
80
- stderr: string;
81
- };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @typedef {Object} StallWatch
3
+ * @property {<T>(call: (gen: number) => Promise<T>) => Promise<T>} watch run `call`,
4
+ * reissuing it whenever it goes `stallMs` without a beat; rejects `StallError` past
5
+ * `maxStalls`. Each issue is handed its GENERATION — the token every callback that
6
+ * call installs must carry back, so a corpse's callbacks cannot touch the live call.
7
+ * @property {(gen: number) => void} beat a round completed on generation `gen` — reset
8
+ * the window. From any other generation it is inert.
9
+ * @property {(gen: number) => boolean} isCurrent is `gen` still the call this watch is
10
+ * waiting on? The caller reads it to withhold everything else a dead call would do.
11
+ * @property {() => number} stalls how many stalls this watch has absorbed
12
+ */
13
+ /**
14
+ * Start a stall watchdog.
15
+ * @param {{ stallMs?: number, maxStalls?: number, onStall?: (n: number) => void,
16
+ * expired?: () => boolean,
17
+ * setTimer?: (fn: () => void, ms: number) => any,
18
+ * clearTimer?: (id: any) => void }} [opts]
19
+ * `onStall` is announced for EVERY stall including the last — a trim or a
20
+ * recovery that happens silently is indistinguishable from one that never
21
+ * happened (F28's rule, applied to time).
22
+ * `expired` is the run's WALL, read at the one moment this module decides to
23
+ * spend again (L4). Omitted, the watch behaves exactly as it did before the wall
24
+ * existed — a caller with no clock reissues on the same rules.
25
+ * @returns {StallWatch}
26
+ */
27
+ export function createStallWatch({ stallMs, maxStalls, onStall, expired, setTimer, clearTimer, }?: {
28
+ stallMs?: number;
29
+ maxStalls?: number;
30
+ onStall?: (n: number) => void;
31
+ expired?: () => boolean;
32
+ setTimer?: (fn: () => void, ms: number) => any;
33
+ clearTimer?: (id: any) => void;
34
+ }): StallWatch;
35
+ /** hamr's number: no completed round for five minutes is a stall. */
36
+ export const STALL_MS: 300000;
37
+ /** hamr's number: three stalls, then stop reissuing and replan. The count is
38
+ * CUMULATIVE over the watch's whole life and `watch()` never resets it — planrun
39
+ * builds one watch per WORKER, so the ceiling is spent across the STEP, not per
40
+ * call: two stalls on one call and one on the next give up just the same. That
41
+ * is deliberate (a step that keeps hanging is a step to replan, not a call to
42
+ * retry forever), and it is why nothing here says "on one call". */
43
+ export const MAX_STALLS: 3;
44
+ /**
45
+ * A stall gave up. Carries the category the step loop's replan trigger reads and
46
+ * the `lib` attribution stamped AT THE THROW SITE (never sniffed from prose).
47
+ */
48
+ export class StallError extends Error {
49
+ /** @param {string} message @param {number} stalls */
50
+ constructor(message: string, stalls: number);
51
+ /** @type {string} */
52
+ category: string;
53
+ /** @type {string} */
54
+ lib: string;
55
+ /** @type {number} */
56
+ stalls: number;
57
+ }
58
+ export type StallWatch = {
59
+ /**
60
+ * run `call`,
61
+ * reissuing it whenever it goes `stallMs` without a beat; rejects `StallError` past
62
+ * `maxStalls`. Each issue is handed its GENERATION — the token every callback that
63
+ * call installs must carry back, so a corpse's callbacks cannot touch the live call.
64
+ */
65
+ watch: <T>(call: (gen: number) => Promise<T>) => Promise<T>;
66
+ /**
67
+ * a round completed on generation `gen` — reset
68
+ * the window. From any other generation it is inert.
69
+ */
70
+ beat: (gen: number) => void;
71
+ /**
72
+ * is `gen` still the call this watch is
73
+ * waiting on? The caller reads it to withhold everything else a dead call would do.
74
+ */
75
+ isCurrent: (gen: number) => boolean;
76
+ /**
77
+ * how many stalls this watch has absorbed
78
+ */
79
+ stalls: () => number;
80
+ };