bareloop 0.5.1 → 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/CHANGELOG.md +722 -1
- package/README.md +13 -3
- package/bareloop.context.md +311 -239
- package/package.json +3 -3
- package/src/clock.js +181 -0
- package/src/exits.js +8 -2
- package/src/index.js +23 -17
- package/src/job.js +231 -108
- package/src/ledger.js +28 -3
- package/src/plan.js +266 -20
- package/src/planrun.js +870 -101
- package/src/ralph.js +226 -7
- package/src/root.js +32 -12
- package/src/run.js +61 -390
- package/src/stall.js +252 -0
- package/src/tools.js +611 -0
- package/src/validate.js +35 -250
- package/types/clock.d.ts +109 -0
- package/types/index.d.ts +4 -6
- package/types/job.d.ts +83 -8
- package/types/ledger.d.ts +3 -1
- package/types/plan.d.ts +71 -7
- package/types/planrun.d.ts +41 -3
- package/types/ralph.d.ts +62 -4
- package/types/root.d.ts +10 -2
- package/types/run.d.ts +9 -17
- package/types/stall.d.ts +80 -0
- package/types/tools.d.ts +262 -0
- package/types/validate.d.ts +21 -38
- package/src/extract.js +0 -103
- package/src/interpret.js +0 -887
- package/types/extract.d.ts +0 -25
- package/types/interpret.d.ts +0 -201
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,51 @@
|
|
|
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[], capRuns?: number }} [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).
|
|
48
101
|
* @returns {{ ok: boolean, reds: Red[], plan: object|null }}
|
|
49
102
|
*/
|
|
50
|
-
export function validatePlan(input: object | string, { job, maxStepRounds }?: {
|
|
103
|
+
export function validatePlan(input: object | string, { job, maxStepRounds, scopes, capRuns }?: {
|
|
51
104
|
job?: any;
|
|
52
105
|
maxStepRounds?: number;
|
|
106
|
+
scopes?: string[];
|
|
107
|
+
capRuns?: number;
|
|
53
108
|
}): {
|
|
54
109
|
ok: boolean;
|
|
55
110
|
reds: Red[];
|
|
@@ -65,13 +120,22 @@ export const MAX_EXITS_PER_STEP: 2;
|
|
|
65
120
|
/** plan size bound — per-step rounds bound the spend, but an unbounded step
|
|
66
121
|
* COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
|
|
67
122
|
export const MAX_PLAN_STEPS: 8;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*
|
|
71
|
-
|
|
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[];
|
|
72
135
|
export type Red = {
|
|
73
136
|
code: string;
|
|
74
137
|
path: string;
|
|
75
138
|
detail?: string;
|
|
76
139
|
verb?: string;
|
|
77
140
|
};
|
|
141
|
+
import { WRITE_VERBS } from './job.js';
|
package/types/planrun.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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,9 +45,15 @@
|
|
|
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)
|
|
26
57
|
* @param {boolean} [opts.layerRoot=false] Layer R — the within-run ratchet (src/root.js),
|
|
27
58
|
* scoped PER STEP's ralph loop (each micro-wheel is the Layer-1 atom). Shell-assembled from
|
|
28
59
|
* the step's own books: per-attempt write-sets from the F32 workerWrites audit (teed for
|
|
@@ -32,11 +63,15 @@
|
|
|
32
63
|
* first plan-flow job to emit `root-injected` runs the pre-registered ON-vs-OFF acceptance
|
|
33
64
|
* read (the Layer R default-flip, LAYERS.md ⚠). Excluded on native (clipipe): the native
|
|
34
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).
|
|
35
70
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
36
71
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'pricing-red' | 'cap-halt' |
|
|
37
|
-
* 'provider-red' | 'interpreter-red' | `step-red:<id>`
|
|
72
|
+
* 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' | `step-red:<id>`
|
|
38
73
|
*/
|
|
39
|
-
export function runPlan(job: any, { workdir, provider, nativeProvider, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds, layerRoot }: {
|
|
74
|
+
export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, now }: {
|
|
40
75
|
workdir: string;
|
|
41
76
|
provider: any;
|
|
42
77
|
nativeProvider?: ((o: {
|
|
@@ -48,10 +83,13 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, emit, rem
|
|
|
48
83
|
emit: (type: string, data?: object) => object;
|
|
49
84
|
remainingUsd: () => number;
|
|
50
85
|
isUnpriced?: (() => boolean) | undefined;
|
|
86
|
+
providerFor?: ((tier: string) => any) | undefined;
|
|
51
87
|
capRuns?: number | undefined;
|
|
52
88
|
closeTimeoutMs?: number | undefined;
|
|
53
89
|
maxStepRounds?: number | undefined;
|
|
90
|
+
scoutRounds?: number | undefined;
|
|
54
91
|
layerRoot?: boolean | undefined;
|
|
92
|
+
now?: (() => number) | undefined;
|
|
55
93
|
}): Promise<string>;
|
|
56
94
|
export type CategorizedError = Error & {
|
|
57
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
|
-
*
|
|
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 {
|
|
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
|
|
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' | '
|
|
49
|
-
* '
|
|
50
|
-
* '
|
|
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,
|
|
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
|
-
};
|
package/types/stall.d.ts
ADDED
|
@@ -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
|
+
};
|