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/CHANGELOG.md +1129 -1
- package/README.md +40 -13
- package/bareloop.context.md +799 -245
- package/package.json +3 -3
- package/src/bridges.js +571 -0
- package/src/clock.js +233 -0
- package/src/exits.js +8 -2
- package/src/index.js +52 -17
- package/src/job.js +231 -108
- package/src/ladder.js +196 -0
- package/src/ledger.js +55 -3
- package/src/plan.js +334 -20
- package/src/planrun.js +1519 -134
- package/src/ralph.js +373 -34
- package/src/reuse.js +1602 -0
- package/src/root.js +32 -12
- package/src/run.js +129 -391
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/stall.js +252 -0
- package/src/tools.js +611 -0
- package/src/trend.js +405 -0
- package/src/validate.js +35 -250
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +131 -0
- package/types/index.d.ts +7 -6
- package/types/job.d.ts +83 -8
- package/types/ladder.d.ts +86 -0
- package/types/ledger.d.ts +3 -1
- package/types/plan.d.ts +86 -7
- package/types/planrun.d.ts +142 -5
- package/types/ralph.d.ts +143 -7
- package/types/reuse.d.ts +388 -0
- package/types/root.d.ts +10 -2
- package/types/run.d.ts +56 -17
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/stall.d.ts +80 -0
- package/types/tools.d.ts +262 -0
- package/types/trend.d.ts +108 -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/src/run.js
CHANGED
|
@@ -1,83 +1,23 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The runner — the shell's top layer. It owns everything the emergent parts
|
|
2
2
|
// must never touch: the approval gate (human-signs-always), the primitive
|
|
3
3
|
// smoke, the ONE budget ledger (cap-not-estimate; unpriced is never free, F6),
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// and the decision-ready stops. It composes the plan flow below it and
|
|
5
|
+
// interprets nothing itself.
|
|
6
|
+
// Design record: docs/plans/2026-07-12-n2-headless-loop-design.md.
|
|
7
|
+
//
|
|
8
|
+
// The legacy operator-authored `steps[]` path was DELETED 2026-07-26 (PRD
|
|
9
|
+
// v1.32): the drafting call, config-v1, the per-step interpret loops and the
|
|
10
|
+
// hitl draft-PR step went with it. runJob is now approval → smoke → ledger →
|
|
11
|
+
// runPlan, and `hitl`/`soft-green` return as a Layer 3 decision, rebuilt in the
|
|
12
|
+
// plan shape rather than ported.
|
|
7
13
|
|
|
8
|
-
import { createRequire } from 'node:module';
|
|
9
14
|
import { join } from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
import { LiteCtx, COMPRESS_LEVELS, KINDS } from 'litectx';
|
|
12
|
-
import { redact } from 'bareguard';
|
|
15
|
+
import { LiteCtx } from 'litectx';
|
|
13
16
|
import { validateJob, jobSpecHash, checkApproval } from './job.js';
|
|
14
|
-
import { validateConfig, LOOP_SHAPES, SLOTS, VERBS, globToPrefix, SECRET_PATTERNS, REMEMBER_KINDS } from './validate.js';
|
|
15
|
-
import { interpret } from './interpret.js';
|
|
16
17
|
import { runPlan } from './planrun.js';
|
|
17
|
-
import { runClose, CLOSE_FAULTS } from './ralph.js';
|
|
18
|
-
import { extractArtifact, priceOf } from './text.js';
|
|
19
|
-
|
|
20
|
-
const require = createRequire(import.meta.url);
|
|
21
|
-
const { Loop } = require('bare-agent');
|
|
22
18
|
|
|
23
19
|
/** @typedef {{code: string, path: string, detail?: string}} Red */
|
|
24
20
|
|
|
25
|
-
/**
|
|
26
|
-
* The shell's own drafting allowance, carved out of the job budget (F9).
|
|
27
|
-
* Drafting is a paid shell call that happens BEFORE the config it produces is
|
|
28
|
-
* validated — so a ceiling advertised as the whole job budget is invalidated by
|
|
29
|
-
* the very call that answers it, and a rational drafter (one that claims the
|
|
30
|
-
* ceiling it is given) deadlocks every run. The shell therefore reserves this
|
|
31
|
-
* slice for itself and advertises `budget − reserve` as the ceiling the config
|
|
32
|
-
* may claim. It is a CAP, not an estimate: overspend it and the run stops on
|
|
33
|
-
* the budget story it is, never on a config-red blaming the drafter.
|
|
34
|
-
*/
|
|
35
|
-
const DRAFT_RESERVE_FRAC = 0.05;
|
|
36
|
-
/** truncate (never round up) — an advertised ceiling must be one the shell can honour */
|
|
37
|
-
const floor4 = (/** @type {number} */ n) => Math.floor(n * 10_000) / 10_000;
|
|
38
|
-
|
|
39
|
-
// The drafting prompt: a schema DESCRIPTION built from the live validator
|
|
40
|
-
// menus — never a copyable example config (the drafter must author, not echo;
|
|
41
|
-
// the N2 drafting probe proved a frontier model greens on this, F6). The
|
|
42
|
-
// DRAFT-CONFIG sentinel is fixed shell vocabulary, not content.
|
|
43
|
-
// REMEMBER_KINDS comes from the validator: the prompt advertises the SAME menu
|
|
44
|
-
// the validator enforces — one source, no drift.
|
|
45
|
-
/** @param {any} job @param {Red[]|null} reds @param {number} ceiling the budget the config may actually claim (F9) */
|
|
46
|
-
function draftPrompt(job, reds, ceiling) {
|
|
47
|
-
const doc = `DRAFT-CONFIG
|
|
48
|
-
You are drafting a workflow config (schema "v1") for the job spec below. The config is
|
|
49
|
-
pure declarative JSON validated by a strict schema; ANY unknown field, wrong enum value,
|
|
50
|
-
or out-of-bounds number is rejected. Output ONLY the JSON object, no fences, no commentary.
|
|
51
|
-
|
|
52
|
-
Required top-level fields (no others exist):
|
|
53
|
-
- "schema": must be exactly "v1"
|
|
54
|
-
- "loop": { "shape": one of ${JSON.stringify([...LOOP_SHAPES])}, "maxIterations": optional integer 1..8 }
|
|
55
|
-
- "memory": { "store": must be "litectx",
|
|
56
|
-
"recall": optional { "k": integer 1..20, "kinds": optional non-empty subset of ${JSON.stringify([...KINDS])} },
|
|
57
|
-
"compressLevel": optional, one of ${JSON.stringify([...COMPRESS_LEVELS])} }
|
|
58
|
-
- "gate": { "budgetUsd": number, 0 < n <= the ceiling stated below, "writeScope": array of path-prefix globs like "src/**" }
|
|
59
|
-
- "escalation": { "mode": must be "decision-ready" }
|
|
60
|
-
- "hooks": optional; keys from ${JSON.stringify([...SLOTS])}, each an array of ops.
|
|
61
|
-
Each op is { "op": one of ${JSON.stringify([...VERBS])}, ...params }. STRICT slot legality:
|
|
62
|
-
"recall" (optional "k", "kinds") and "compress" (optional "level") ONLY in "before-attempt";
|
|
63
|
-
"stash" (no params) ONLY in "after-red"; "remember" (optional "kind", one of
|
|
64
|
-
${JSON.stringify(REMEMBER_KINDS)}) ONLY in "on-green".
|
|
65
|
-
|
|
66
|
-
Hard constraints from the job spec (violations are rejected):
|
|
67
|
-
- Every writeScope entry must fit INSIDE the job's writeScope (${JSON.stringify(job.writeScope)}).
|
|
68
|
-
No "..", no absolute paths, no mid-path wildcards.
|
|
69
|
-
- gate.budgetUsd must be <= ${ceiling}. (This is ONE STEP's share of the job's
|
|
70
|
-
$${job.budgetUsd} budget, minus the shell's own drafting allowance: this config is drafted
|
|
71
|
-
once and reused for every step, so the number must still fit after earlier steps have
|
|
72
|
-
spent. Claiming exactly ${ceiling} is legal and validates.)
|
|
73
|
-
- The config CANNOT contain close commands, provider choice, retry caps, or minting claims.
|
|
74
|
-
|
|
75
|
-
Job spec:
|
|
76
|
-
${JSON.stringify(job, null, 2)}`;
|
|
77
|
-
return reds
|
|
78
|
-
? `${doc}\n\nYour previous draft was REJECTED with these reds (code:path):\n${JSON.stringify(reds)}\nFix every red. Output ONLY the corrected JSON object.`
|
|
79
|
-
: doc;
|
|
80
|
-
}
|
|
81
21
|
|
|
82
22
|
/**
|
|
83
23
|
* Known-answer smoke on the primitive the run is about to trust (PRD v1.5 §4,
|
|
@@ -99,71 +39,6 @@ async function primitiveSmoke(workdir) {
|
|
|
99
39
|
}
|
|
100
40
|
}
|
|
101
41
|
|
|
102
|
-
/** @typedef {(argv: string[], opts?: {cwd?: string}) => {status: number|null, stdout: string, stderr: string}} ExecCmd */
|
|
103
|
-
|
|
104
|
-
/** default process runner for the hitl PR mechanics — real spawnSync, no shell
|
|
105
|
-
* @type {ExecCmd} */
|
|
106
|
-
const defaultExec = (argv, { cwd } = {}) => {
|
|
107
|
-
const r = spawnSync(argv[0], argv.slice(1), { cwd, encoding: 'utf8', timeout: 60_000 });
|
|
108
|
-
if (r.error) return { status: null, stdout: '', stderr: String(r.error) };
|
|
109
|
-
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The hitl middle's work (2b): branch → stage the job fence ONLY → commit →
|
|
114
|
-
* push → draft PR. Deterministic runner code, never model tools (the model
|
|
115
|
-
* never sees a git or gh surface — design addendum 2026-07-12b). Staging is
|
|
116
|
-
* fence-scoped so spines, audit logs, and anything else living in the workdir
|
|
117
|
-
* never enter the PR. Any failure is an honest red carrying the failed argv;
|
|
118
|
-
* the caller still escalates — a broken PR path must never swallow the
|
|
119
|
-
* escalation (the pain channel, law #7).
|
|
120
|
-
* The checkout is handed back exactly as it was found: the starting branch is
|
|
121
|
-
* read BEFORE anything moves and restored on EVERY path, success or failure
|
|
122
|
-
* (found by the real job #1 run, 2026-07-13 — the workdir was left sitting on
|
|
123
|
-
* the bareloop branch, so the next cadenced run would branch off yesterday's
|
|
124
|
-
* unmerged branch and judge its close against that state). A restore that
|
|
125
|
-
* itself fails is loud (`strandedOn`), never silent.
|
|
126
|
-
*
|
|
127
|
-
* @param {{workdir: string, branch: string, title: string, body: string, addPaths: string[], exec: ExecCmd}} o
|
|
128
|
-
* @returns {Promise<{url: string|null, red: {argv: string, detail: string}|null, strandedOn: string|null}>}
|
|
129
|
-
*/
|
|
130
|
-
async function openDraftPr({ workdir, branch, title, body, addPaths, exec }) {
|
|
131
|
-
// Subprocess output is scrubbed AT CAPTURE with the ONE shape inventory —
|
|
132
|
-
// the same doctrine as the close path (interpret wires SECRET_PATTERNS into
|
|
133
|
-
// ralph): git/gh error text can echo a credentialed remote URL, and the
|
|
134
|
-
// detail lands on the append-only spine (pr-red + the escalation's pr.error).
|
|
135
|
-
const scrub = (/** @type {string} */ s) => redact(s, { patterns: SECRET_PATTERNS });
|
|
136
|
-
const head = exec(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: workdir });
|
|
137
|
-
const startBranch = head.status === 0 ? head.stdout.trim() : '';
|
|
138
|
-
/** put the checkout back where it started; report the branch it is stranded on if that fails */
|
|
139
|
-
const restore = () => {
|
|
140
|
-
if (!startBranch) return null; // never knew where we started — nothing honest to restore to
|
|
141
|
-
const r = exec(['git', 'checkout', startBranch], { cwd: workdir });
|
|
142
|
-
return r.status === 0 ? null : branch;
|
|
143
|
-
};
|
|
144
|
-
/** @param {{url?: string|null, red?: {argv: string, detail: string}|null}} o */
|
|
145
|
-
const done = ({ url = null, red = null }) => ({ url, red, strandedOn: restore() });
|
|
146
|
-
|
|
147
|
-
const steps = [
|
|
148
|
-
['git', 'checkout', '-b', branch],
|
|
149
|
-
['git', 'add', '--', ...addPaths],
|
|
150
|
-
['git', 'commit', '-m', title],
|
|
151
|
-
['git', 'push', '-u', 'origin', branch],
|
|
152
|
-
['gh', 'pr', 'create', '--draft', '--title', title, '--body', body],
|
|
153
|
-
];
|
|
154
|
-
for (const argv of steps) {
|
|
155
|
-
const r = exec(argv, { cwd: workdir });
|
|
156
|
-
if (r.status !== 0) {
|
|
157
|
-
return done({ red: { argv: argv.join(' '), detail: `${argv.slice(0, 2).join(' ')} failed: ${scrub((r.stderr || r.stdout || `exit ${r.status}`)).trim().slice(0, 500)}` } });
|
|
158
|
-
}
|
|
159
|
-
if (argv[0] === 'gh') {
|
|
160
|
-
const url = (r.stdout.match(/https?:\/\/\S+/) ?? [null])[0];
|
|
161
|
-
// a "successful" gh with no link is not a PR the human can act on
|
|
162
|
-
return url ? done({ url }) : done({ red: { argv: argv.join(' '), detail: 'no PR URL in gh output' } });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return done({ red: { argv: 'gh pr create', detail: 'PR step never ran' } }); // unreachable while gh is last; belt for edits
|
|
166
|
-
}
|
|
167
42
|
|
|
168
43
|
/**
|
|
169
44
|
* Run an approved job end to end: approval gate → primitive smoke → sequential
|
|
@@ -194,35 +69,105 @@ async function openDraftPr({ workdir, branch, title, body, addPaths, exec }) {
|
|
|
194
69
|
* @param {object} opts
|
|
195
70
|
* @param {unknown} opts.approvals `{ specHash, signer, ts }` records (from OUTSIDE the spec)
|
|
196
71
|
* @param {string} opts.workdir the run directory (the fence's root)
|
|
197
|
-
* @param {string} [opts.target] the step artifact path (text-mode steps; unused by tool-mode steps)
|
|
198
72
|
* @param {any} opts.provider shell-owned LLM binding (never the config's; the Loop path)
|
|
199
73
|
* @param {(o: {policy: Function, onTurn?: Function, maxTurns: number, hasTools: boolean}) => any} [opts.nativeProvider]
|
|
200
74
|
* native clipipe provider factory (BA-16), required for a `clipipe-subscription` plan-shape job —
|
|
201
75
|
* threaded to runPlan, which builds one per worker (native tool mode when `hasTools`, else a
|
|
202
76
|
* metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
|
|
203
77
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
204
|
-
* @param {
|
|
78
|
+
* @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
|
|
79
|
+
* @param {number} [opts.capRuns] shell-owned iteration cap for the plan flow's
|
|
80
|
+
* CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
|
|
81
|
+
* @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
|
|
205
82
|
* @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
|
|
206
83
|
* @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
|
|
207
|
-
* @param {
|
|
84
|
+
* @param {unknown} [opts.bridge] Layer 3 — a bridge-v1 entry to reuse as the drafter's
|
|
85
|
+
* STARTING DRAFT (design record 2026-08-01, D4), forwarded to the plan flow. Omitted is
|
|
86
|
+
* the cold path and is byte-identical to a pre-Layer-3 run. WHICH entry (the listing, an
|
|
87
|
+
* operator pin, the LLM's pick) is the caller's decision, and so is what to do when the
|
|
88
|
+
* load gate refuses one: a refusal returns the distinct `recipe-stale` outcome with zero
|
|
89
|
+
* spend rather than silently drafting cold, because starting a paid run on a decision
|
|
90
|
+
* nobody made is the same class of error as widening a cap to manufacture a green.
|
|
91
|
+
* @param {number} [opts.priorSpentUsd=0] RESUME (module C) — money a PREVIOUS, killed
|
|
92
|
+
* attempt of this same try already spent. It seeds the ledger, so the restarted
|
|
93
|
+
* attempt runs under the REMAINDER of the signed `budgetUsd` and the terminal
|
|
94
|
+
* `job-end` states the try's WHOLE spend across both attempts. hamr's ruling: "a
|
|
95
|
+
* budget ceiling folds in prior spend so re-invoking cannot silently widen it" —
|
|
96
|
+
* the fold is the seam precisely because the CAP is in the spec hash, so
|
|
97
|
+
* tightening the cap instead would make a new spec version needing a signature
|
|
98
|
+
* nobody typed. The caller reconstructs the figure from the dead run's own spine.
|
|
99
|
+
* @param {boolean} [opts.priorSpendComplete=true] RESUME — was that fold EXACT?
|
|
100
|
+
* `readResume` reports false when any round inside the dead attempt came back
|
|
101
|
+
* unpriced, which makes the fold a FLOOR. The unknown does not heal by being carried
|
|
102
|
+
* forward, so it rides one-way onto every `job-end` this run emits: a total built on
|
|
103
|
+
* a floor is a floor, and reporting it as exact is F6 in a resume's coat.
|
|
104
|
+
* @param {number} [opts.priorWallMs=0] RESUME — the same ruling in a time coat: wall
|
|
105
|
+
* time the killed attempt already consumed, folded into the run clock so the
|
|
106
|
+
* restart gets the remainder of the SIGNED wall, never a fresh allotment.
|
|
107
|
+
* @param {any} [opts.resumeSeed] RESUME (module C v2) — WHERE inside the killed attempt
|
|
108
|
+
* the kill landed, read off its own spine by `readResume`: the plan it accepted and the
|
|
109
|
+
* steps that reached their exits. Forwarded verbatim to the plan flow, which reloads
|
|
110
|
+
* the plan instead of re-scouting and re-drafting, and skips the finished steps.
|
|
111
|
+
* hamr's ruling: *"start last step instead from the beginning, why would i want to
|
|
112
|
+
* waste more money on something i already started"*. Absent is the ordinary path.
|
|
113
|
+
* @param {{stage?: string|null, value?: number|null}[]} [opts.resumeGrades] RESUME — the
|
|
114
|
+
* close grades the killed leg recorded (`readResume`'s `restart.grades`), forwarded to
|
|
115
|
+
* the plan flow as the trend's BASELINES. A resumed run's halt readout judges the whole
|
|
116
|
+
* chain, not just the leg: without them a leg that re-grades an unchanged tree reports
|
|
117
|
+
* `flat` — "revise the goal" — on a run that was converging when the money ran out.
|
|
118
|
+
* Counts and stage names only; no close bytes cross this seam. Absent is the cold path.
|
|
208
119
|
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
209
|
-
* territory, threaded to
|
|
120
|
+
* territory, threaded to the plan flow. Defaults OFF
|
|
210
121
|
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
211
122
|
* ON has never won its own A/B — the acceptance read defers to Layer 2 (or
|
|
212
123
|
* a manufactured-fixation probe). `true` is the ON/experimental arm; the
|
|
213
124
|
* default is the OFF arm. (Shell-owned seam, same doctrine as the provider
|
|
214
125
|
* binding.)
|
|
215
|
-
* @returns {Promise<string>} outcome: 'green' | '
|
|
216
|
-
* '
|
|
217
|
-
* '
|
|
126
|
+
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
127
|
+
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
128
|
+
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
|
|
129
|
+
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
|
|
218
130
|
*/
|
|
219
|
-
export async function runJob(rawSpec, { approvals, workdir,
|
|
131
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [] }) {
|
|
220
132
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
221
133
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
222
134
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
223
135
|
// into $0 (F12's class, at the terminal record instead of mid-attempt).
|
|
224
|
-
|
|
136
|
+
//
|
|
137
|
+
// It starts at the RESUME fold rather than at 0 when a killed attempt of this
|
|
138
|
+
// same try already spent money (see `priorSpentUsd`). Belted like every other
|
|
139
|
+
// number that enters the arbiter's arithmetic: a garbage fold is 0, never a NaN
|
|
140
|
+
// that would poison every later comparison into "no cap".
|
|
141
|
+
let spentUsd = typeof priorSpentUsd === 'number' && Number.isFinite(priorSpentUsd) && priorSpentUsd > 0 ? priorSpentUsd : 0;
|
|
225
142
|
let unpriced = false;
|
|
143
|
+
// RESUME (v1.46 §3): was the FOLD itself exact? `readResume` marks it false when any
|
|
144
|
+
// round inside the dead attempt came back unpriced, and that unknown does not heal by
|
|
145
|
+
// being carried forward — every round of THIS attempt being priced repairs nothing
|
|
146
|
+
// about the one before it. One-way, like `stalled` and `cutMidCall`, and read by
|
|
147
|
+
// `spend()`, so the terminal states a floor instead of an exact-looking total (F6).
|
|
148
|
+
// Read off the FLAG alone, never conjoined with the money: a leg killed with every
|
|
149
|
+
// round unpriced folds $0 and still says `false`, and that is a floor OF zero, not an
|
|
150
|
+
// exact zero. Gating on `spentUsd > 0` dropped exactly that predecessor's unknown and
|
|
151
|
+
// handed the resumed terminal an exact-looking total. The param defaults `true`, so a
|
|
152
|
+
// run nobody resumed is unchanged.
|
|
153
|
+
const priorFloor = priorSpendComplete === false;
|
|
154
|
+
// W1: did this run ABSORB a stall? The terminal `step-stalled` is the rare case;
|
|
155
|
+
// the common one self-heals (src/stall.js abandons the hung call and reissues),
|
|
156
|
+
// and an abandoned call may already have been billed — a reissue can pay twice
|
|
157
|
+
// and the provider never told us. So a run that stalls and then ends green (or
|
|
158
|
+
// escalated, or at the cap) also holds a FLOOR, not a total.
|
|
159
|
+
let stalled = false;
|
|
160
|
+
// F64: did the wall cut this run from INSIDE a provider call? A `wall-halt` is two
|
|
161
|
+
// stops wearing one name. Read between rounds (or between attempts, W-2) it is the
|
|
162
|
+
// cleanest terminal there is — nothing was in flight, every round the sum counted
|
|
163
|
+
// came back priced, and the figure is exact. But the deadline can also land inside a
|
|
164
|
+
// call and come back as the provider's own timeout, and THAT call returned no usage
|
|
165
|
+
// at all: it may already have been billed and will never say so. Same unknown as the
|
|
166
|
+
// transport floor and the self-healed stall, so the same honest answer.
|
|
167
|
+
// The discriminator is the wall-halt record's own `cutMidCall` field, never the
|
|
168
|
+
// outcome — keying on the outcome would floor the exact stop too, which is the same
|
|
169
|
+
// dishonesty pointing the other way.
|
|
170
|
+
let cutMidCall = false;
|
|
226
171
|
// The money on the terminal record, and whether the money is EXACT. `spentUsd`
|
|
227
172
|
// is the accumulated sum of PRICED rounds ONLY — never an estimate derived
|
|
228
173
|
// from tokens or averages (cap-not-estimate). When any round came back
|
|
@@ -230,7 +175,10 @@ export async function runJob(rawSpec, { approvals, workdir, target, provider, na
|
|
|
230
175
|
// says so machine-readably instead of dressing a floor up as exact. Emitted on
|
|
231
176
|
// EVERY job-end (true when everything was priced) so no consumer ever has to
|
|
232
177
|
// branch on field presence.
|
|
233
|
-
|
|
178
|
+
// ONE spelling of the four causes, so the terminal and the in-flight money readout can
|
|
179
|
+
// never disagree about whether the same run's figure is exact.
|
|
180
|
+
const spendComplete = () => !unpriced && !stalled && !cutMidCall && !priorFloor;
|
|
181
|
+
const spend = () => ({ spentUsd, spendComplete: spendComplete() });
|
|
234
182
|
// 1. human-signs-always — before ANY provider call (N1 decision #1)
|
|
235
183
|
if (!checkApproval(rawSpec, approvals)) {
|
|
236
184
|
emit('job-end', { outcome: 'unapproved-spec', detail: 'no approval record matches this exact spec version', ...spend() });
|
|
@@ -243,22 +191,19 @@ export async function runJob(rawSpec, { approvals, workdir, target, provider, na
|
|
|
243
191
|
return 'job-red';
|
|
244
192
|
}
|
|
245
193
|
const job = /** @type {any} */ (jv.job);
|
|
246
|
-
// Layer 2: the two exclusive shapes (decision 6). A plan-shape spec (goal/
|
|
247
|
-
// verdictType/close/checks[]) has no steps[] and its steps are tool-mode by
|
|
248
|
-
// construction — the text-mode target rule below is a legacy-steps rule only.
|
|
249
|
-
const planShape = job.goal !== undefined;
|
|
250
|
-
// Reds-before-tokens: a text-mode predicate step writes ONE artifact — with
|
|
251
|
-
// no target it would burn a draft + a worker call and then crash inside the
|
|
252
|
-
// middle as a misfiled interpreter-red. The spec is fine; the CALL is not.
|
|
253
|
-
if (!planShape && (typeof target !== 'string' || !target)
|
|
254
|
-
&& job.steps.some((/** @type {any} */ s) => s.close.type === 'predicate' && (s.mode ?? 'text') === 'text')) {
|
|
255
|
-
emit('job-red', { code: 'missing-required', path: 'opts.target', detail: 'text-mode steps write ONE artifact — pass opts.target (reds-before-tokens)' });
|
|
256
|
-
emit('job-end', { outcome: 'job-red', ...spend() });
|
|
257
|
-
return 'job-red';
|
|
258
|
-
}
|
|
259
194
|
emit('job-start', {
|
|
260
195
|
job: job.job, specHash: jobSpecHash(job), budgetUsd: job.budgetUsd,
|
|
261
|
-
|
|
196
|
+
shape: 'plan', goal: job.goal,
|
|
197
|
+
// RESUME (v1.46 §3) — the DECLARED fold, the `try-start` precedent one level
|
|
198
|
+
// down. A reader reconstructing a chain of resumes must add only each attempt's
|
|
199
|
+
// OWN new rounds; without this record the second resume of a halted run would
|
|
200
|
+
// re-derive from its own file alone and silently widen the ceiling by everything
|
|
201
|
+
// spent before it. Emitted only when there IS a fold: an absent fold is absent,
|
|
202
|
+
// and a decorative `0` would be indistinguishable from a run nobody folded.
|
|
203
|
+
// A fold of $0 that is NOT exact is still a fold — the unknown is the whole of what
|
|
204
|
+
// it inherited, and it is the only field that can carry it forward.
|
|
205
|
+
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
206
|
+
...(typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? { priorWallMs } : {}),
|
|
262
207
|
});
|
|
263
208
|
|
|
264
209
|
// 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
|
|
@@ -288,6 +233,15 @@ export async function runJob(rawSpec, { approvals, workdir, target, provider, na
|
|
|
288
233
|
// unknown, never $0 — F6): per-round metering means a partially-unpriced run
|
|
289
234
|
// is caught natively, round by round, with no separate unpricedRounds tally.
|
|
290
235
|
if (type === 'worker-round') account(/** @type {any} */ (data)?.costUsd);
|
|
236
|
+
// W1: a `stall` is the plan flow announcing it ABANDONED a call and reissued
|
|
237
|
+
// it. The abandoned call's spend is unknowable from here (billed or not,
|
|
238
|
+
// indistinguishable), so from this point the priced sum is a floor no matter
|
|
239
|
+
// how the run ends — the flag is one-way and read by `spend()`.
|
|
240
|
+
if (type === 'stall') stalled = true;
|
|
241
|
+
// F64: the plan flow's own TIME record says which of the two wall stops this was.
|
|
242
|
+
// One-way, and read by `spend()` — the run ends on this record either way, but the
|
|
243
|
+
// between-rounds stop must keep its exact figure rather than inheriting a floor.
|
|
244
|
+
if (type === 'wall-halt' && /** @type {any} */ (data)?.cutMidCall === true) cutMidCall = true;
|
|
291
245
|
return emit(type, data);
|
|
292
246
|
};
|
|
293
247
|
const pricingRed = () => {
|
|
@@ -296,249 +250,33 @@ export async function runJob(rawSpec, { approvals, workdir, target, provider, na
|
|
|
296
250
|
return 'pricing-red';
|
|
297
251
|
};
|
|
298
252
|
|
|
299
|
-
// ──
|
|
300
|
-
// the
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
if (planShape) {
|
|
253
|
+
// ── the plan flow (SCOUT → PLAN → EXECUTE → close) under the approval gate,
|
|
254
|
+
// the smoke and the ledger above — runJob stays the one entry (N2 lock).
|
|
255
|
+
// runPlan emits every provider round as worker-round, so the metered emit
|
|
256
|
+
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
257
|
+
{
|
|
305
258
|
const outcome = await runPlan(job, {
|
|
306
|
-
workdir, provider, nativeProvider, emit: meter, capRuns, closeTimeoutMs, layerRoot,
|
|
259
|
+
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades,
|
|
307
260
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
308
261
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
262
|
+
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
|
309
263
|
});
|
|
310
264
|
if (unpriced) return pricingRed();
|
|
311
265
|
if (outcome.startsWith('step-red:')) {
|
|
312
266
|
emit('job-end', { outcome: 'step-red', step: outcome.slice('step-red:'.length), ...spend() });
|
|
313
|
-
} else if (outcome === 'provider-red') {
|
|
267
|
+
} else if (outcome === 'provider-red' || outcome === 'step-stalled') {
|
|
314
268
|
// F44: a transport-throw provider-red never returned a usage figure for the
|
|
315
|
-
// failed call, so the priced sum is a FLOOR, not the total —
|
|
316
|
-
//
|
|
269
|
+
// failed call, so the priced sum is a FLOOR, not the total — spendComplete
|
|
270
|
+
// false, never an exact-looking total.
|
|
271
|
+
// F66: `step-stalled` carries the same unknown. Every abandoned call MAY
|
|
272
|
+
// already have been billed by the provider before it went quiet — that is
|
|
273
|
+
// the known cost of self-heal — and an unbilled reissue is indistinguishable
|
|
274
|
+
// from a billed one from here. Reporting the priced sum as exact would be F6
|
|
275
|
+
// in a self-heal coat.
|
|
317
276
|
emit('job-end', { outcome, ...spend(), spendComplete: false });
|
|
318
277
|
} else {
|
|
319
278
|
emit('job-end', { outcome, ...spend() });
|
|
320
279
|
}
|
|
321
280
|
return outcome;
|
|
322
281
|
}
|
|
323
|
-
|
|
324
|
-
// 4. sealed drafting: one shot + one redraft, reds fed back, PRICED path
|
|
325
|
-
// (through Loop, the same accounting the worker uses — never around it, F6)
|
|
326
|
-
const drafter = new Loop({ provider, system: 'You draft workflow configs as pure JSON. Output only the JSON object.' });
|
|
327
|
-
/** @type {string|null} a transport throw from the drafting call — spend UNKNOWN, terminal */
|
|
328
|
-
let transportRed = null;
|
|
329
|
-
/** @type {string|null} a drafting round the API TRUNCATED (BA-6, bare-agent 0.27.0) — provider-red like
|
|
330
|
-
* a throw, but the round WAS metered so spend is known; must never launder into a config-red (F25). */
|
|
331
|
-
let draftTruncated = null;
|
|
332
|
-
/** @param {Red[]|null} reds */
|
|
333
|
-
const draft = async (reds) => {
|
|
334
|
-
let r;
|
|
335
|
-
try {
|
|
336
|
-
r = await drafter.run([{ role: 'user', content: draftPrompt(job, reds, ceilingNow()) }]);
|
|
337
|
-
} catch (e) {
|
|
338
|
-
// Loop defaults throwOnError: true — a misconfigured binding or a bare
|
|
339
|
-
// transient 500 REJECTS here. The spend is UNKNOWN (F6), and the spine
|
|
340
|
-
// must still terminate: never an unhandled rejection out of the runner.
|
|
341
|
-
transportRed = String(/** @type {Error} */ (e)?.message ?? e);
|
|
342
|
-
emit('draft-result', { costUsd: null, redraft: reds !== null, error: transportRed });
|
|
343
|
-
return null;
|
|
344
|
-
}
|
|
345
|
-
const { costUsd, unpricedRounds } = priceOf(r); // the ONE honest-null cost read (F6)
|
|
346
|
-
account(costUsd);
|
|
347
|
-
if (unpricedRounds > 0) unpriced = true;
|
|
348
|
-
emit('draft-result', { costUsd, redraft: reds !== null, error: r.error ?? null });
|
|
349
|
-
// BA-6: the API cut the drafting round off mid-JSON. That is provider-red (the API's
|
|
350
|
-
// fault, not the drafter's), so it must NOT be laundered into a config-red blaming the
|
|
351
|
-
// drafter (F25's class) and must NOT consume the redraft — retrying a truncation blindly
|
|
352
|
-
// just burns budget; the human decides. Metered above, so unlike transportRed spend is known.
|
|
353
|
-
if (typeof r.error === 'string' && r.error.startsWith('truncated:')) { draftTruncated = r.error; return null; }
|
|
354
|
-
return r.error ? null : (extractArtifact(r.text).code ?? '');
|
|
355
|
-
};
|
|
356
|
-
const providerRed = () => {
|
|
357
|
-
emit('escalation', { category: 'provider-red', decisionReady: true, decision: 'The provider path threw before a result existed — spend for the failed call is unknown (F6), and no drafting verdict exists.', options: ['fix the provider binding', 'retry the run', 'abandon the run'], detail: transportRed, spentUsd });
|
|
358
|
-
// A transport THROW never returned a usage figure, so the floor is NOT the total
|
|
359
|
-
// (F6): spendComplete must be false — spend()'s `!unpriced` only knows about priced
|
|
360
|
-
// rounds that came back unpriced, not a call that never returned at all. Leaving it
|
|
361
|
-
// true would have the job-end contradict the escalation's own "spend … is unknown".
|
|
362
|
-
emit('job-end', { outcome: 'provider-red', ...spend(), spendComplete: false });
|
|
363
|
-
return 'provider-red';
|
|
364
|
-
};
|
|
365
|
-
const capNow = () => Math.min(shellCapUsd, job.budgetUsd - spentUsd);
|
|
366
|
-
// The ceiling the CONFIG may claim (F9): the job budget minus the shell's own
|
|
367
|
-
// drafting allowance, and never above what is actually left. Advertised in the
|
|
368
|
-
// prompt and enforced by the validator — ONE number, so a drafter that claims
|
|
369
|
-
// the ceiling it was given always validates. (Before: the prompt advertised the
|
|
370
|
-
// whole job budget while the validator enforced budget − drafting-spend, a bound
|
|
371
|
-
// the drafter was never told and could not satisfy.)
|
|
372
|
-
// ...and a PER-STEP share of it, not the whole pot. The config is drafted ONCE
|
|
373
|
-
// and re-validated at every step against what is LEFT, so a ceiling sized to the
|
|
374
|
-
// whole budget is stale the moment step 1 spends: step 2 then reds `bounds` on an
|
|
375
|
-
// unchanged config with plenty of money still in the pot (review 2026-07-18).
|
|
376
|
-
// Dividing by the steps that will actually run a worker makes the number the
|
|
377
|
-
// drafter is handed one that still fits after its predecessors have spent —
|
|
378
|
-
// sum(shares) = budget − reserve, so every step can claim its share. This is a
|
|
379
|
-
// DRAFTING change, not an enforcement one: cap-not-estimate is untouched, and a
|
|
380
|
-
// step that needs more than its share still cap-halts cleanly with a resume
|
|
381
|
-
// point. Single-step jobs (every shipped job today) are arithmetically unchanged.
|
|
382
|
-
const loopSteps = Math.max(1, job.steps.filter((s) => s.close.type === 'predicate').length);
|
|
383
|
-
const workerCeiling = floor4(job.budgetUsd * (1 - DRAFT_RESERVE_FRAC) / loopSteps);
|
|
384
|
-
const ceilingNow = () => Math.min(workerCeiling, capNow());
|
|
385
|
-
const cOpts = () => ({ shellCapUsd: ceilingNow(), jobWriteScope: job.writeScope });
|
|
386
|
-
// Drafting is DEFERRED to the first step that needs a worker (resume model,
|
|
387
|
-
// design addendum 2026-07-13): a step whose close already greens skips for
|
|
388
|
-
// zero tokens, so a clean rerun may need no config at all — drafting one no
|
|
389
|
-
// interpret will ever read is spend for nothing (F6's spirit). Decision #3
|
|
390
|
-
// holds: when a config IS needed, it is drafted fresh THIS run, never inherited.
|
|
391
|
-
/** @type {any} the validated config, drafted on first need */
|
|
392
|
-
let config = null;
|
|
393
|
-
/** @returns {Promise<string|null>} a terminal job outcome, or null with `config` set */
|
|
394
|
-
const ensureConfig = async () => {
|
|
395
|
-
if (config) return null;
|
|
396
|
-
let text = await draft(null);
|
|
397
|
-
let cv = validateConfig(text ?? '', cOpts());
|
|
398
|
-
emit('config-validate', { ok: cv.ok, reds: cv.reds, phase: 'draft-1' });
|
|
399
|
-
// the redraft only fires when it can still help: not over a transport red,
|
|
400
|
-
// not over unpriced spend, and never over an already-blown budget (a paid
|
|
401
|
-
// call against a negative cap can only red again — budget story, not drafter)
|
|
402
|
-
if (!cv.ok && !unpriced && !transportRed && !draftTruncated && capNow() > 0) {
|
|
403
|
-
text = await draft(cv.reds);
|
|
404
|
-
cv = validateConfig(text ?? '', cOpts());
|
|
405
|
-
emit('config-validate', { ok: cv.ok, reds: cv.reds, phase: 'draft-2' });
|
|
406
|
-
}
|
|
407
|
-
if (transportRed) return providerRed();
|
|
408
|
-
if (draftTruncated) {
|
|
409
|
-
// A truncated draft is provider-red, NOT config-red — the API cut it off; the drafter is
|
|
410
|
-
// not on trial. Mirrors the worker path (interpret.js) and the human's options: the round
|
|
411
|
-
// was metered, so this is a fault with a known cost, not a spend-unknown transport throw.
|
|
412
|
-
emit('escalation', { category: 'provider-red', decisionReady: true, decision: 'The drafting round was truncated at the output cap (BA-6) — the API cut the config off mid-generation, so no valid config exists. Not the drafter\'s competence (never a config-red), and not silently retried.', options: ['raise the drafting cap and rerun', 'retry the run', 'abandon the run'], detail: draftTruncated, spentUsd });
|
|
413
|
-
emit('job-end', { outcome: 'provider-red', ...spend() });
|
|
414
|
-
return 'provider-red';
|
|
415
|
-
}
|
|
416
|
-
if (unpriced) return pricingRed();
|
|
417
|
-
if (!cv.ok) {
|
|
418
|
-
if (capNow() <= 0) {
|
|
419
|
-
// drafting consumed the whole job budget before a valid config existed —
|
|
420
|
-
// the honest stop is a cap story, never a config-red blaming the drafter
|
|
421
|
-
emit('cap-halt', { category: 'cap-halt', meaning: 'not under cap — not "can\'t"', spentUsd, budgetUsd: job.budgetUsd });
|
|
422
|
-
emit('escalation', { category: 'cap-halt', decisionReady: true, decision: `Drafting spend ($${spentUsd.toFixed(4)}) consumed the job budget ($${job.budgetUsd}) before a valid config existed.`, options: ['raise the job budget and rerun', 'abandon the run'], spentUsd });
|
|
423
|
-
emit('job-end', { outcome: 'cap-halt', ...spend() });
|
|
424
|
-
return 'cap-halt';
|
|
425
|
-
}
|
|
426
|
-
for (const r of cv.reds) emit('config-red', r);
|
|
427
|
-
emit('job-end', { outcome: 'config-red', ...spend() });
|
|
428
|
-
return 'config-red';
|
|
429
|
-
}
|
|
430
|
-
config = cv.config;
|
|
431
|
-
return null;
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
// 5. sequential per-step loops under the one ledger; a step-red stops the
|
|
435
|
-
// job and the stop is a result (ladder discipline applied to steps)
|
|
436
|
-
for (const step of job.steps) {
|
|
437
|
-
if (step.close.type === 'hitl') {
|
|
438
|
-
// Nothing changed in the fence → nothing for a human to review (found by
|
|
439
|
-
// the real job #1 run, 2026-07-13): a cadenced run whose steps all skip
|
|
440
|
-
// already-green would otherwise branch/add/commit, and `git commit`
|
|
441
|
-
// correctly fails ("nothing added to commit") — a broken-PR escalation
|
|
442
|
-
// every single day. A hitl close is a human decision point; with no
|
|
443
|
-
// changes there IS no decision. Only an AFFIRMATIVE clean answer skips:
|
|
444
|
-
// a failed check (not a repo, broken git) falls through to the PR path
|
|
445
|
-
// and reds honestly there — an unknown fence state is never a green.
|
|
446
|
-
const fence = job.writeScope.map((/** @type {string} */ g) => globToPrefix(g));
|
|
447
|
-
const st = execCmd(['git', 'status', '--porcelain', '--', ...fence], { cwd: workdir });
|
|
448
|
-
if (st.status === 0 && st.stdout.trim() === '') {
|
|
449
|
-
emit('pr-skipped', { step: step.id, reason: 'no changes in the job fence — nothing for a human to review' });
|
|
450
|
-
emit('step-end', { step: step.id, outcome: 'already-green', spentUsd });
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
// the hitl middle's work (2b): deterministic branch/commit/draft-PR —
|
|
454
|
-
// model tools never touch git; a PR failure is red + escalation, never a
|
|
455
|
-
// swallowed escalation
|
|
456
|
-
const branch = `bareloop/${job.job}-${Date.now().toString(36)}`;
|
|
457
|
-
const title = `bareloop draft: ${job.job}`;
|
|
458
|
-
const pr = await openDraftPr({
|
|
459
|
-
workdir, branch, title,
|
|
460
|
-
body: `${job.description}\n\nstep: ${step.id}\nprompt: ${step.close.prompt}\nspent: $${spentUsd.toFixed(4)} of $${job.budgetUsd}\n\nDraft by bareloop — merge stays human, forever.`,
|
|
461
|
-
addPaths: job.writeScope.map((/** @type {string} */ g) => globToPrefix(g)),
|
|
462
|
-
exec: execCmd,
|
|
463
|
-
});
|
|
464
|
-
if (pr.red) emit('pr-red', { step: step.id, ...pr.red });
|
|
465
|
-
else emit('pr-opened', { step: step.id, branch, url: pr.url });
|
|
466
|
-
// a checkout left on a bareloop branch poisons the NEXT cadenced run's
|
|
467
|
-
// baseline — loud, and never allowed to un-open a real PR
|
|
468
|
-
if (pr.strandedOn) emit('workdir-red', { step: step.id, branch: pr.strandedOn, detail: 'the run could not return the checkout to its starting branch — the next run would build on this one' });
|
|
469
|
-
emit('escalation', { category: 'hitl-close', decisionReady: true, step: step.id, prompt: step.close.prompt, spentUsd, decision: step.close.prompt, options: ['approve', 'reject'], pr: { url: pr.url, branch, error: pr.red?.detail ?? null } });
|
|
470
|
-
emit('job-end', { outcome: 'escalated', step: step.id, ...spend() });
|
|
471
|
-
return 'escalated'; // by design: the human acts outside the run, forever
|
|
472
|
-
}
|
|
473
|
-
if (step.close.type !== 'predicate') {
|
|
474
|
-
// honest refusal: gold/rubric EXECUTION lands with the verdict classes
|
|
475
|
-
// (N4); a fake verdict here would poison every contrast downstream
|
|
476
|
-
emit('escalation', { category: 'close-unsupported', decisionReady: true, step: step.id, decision: `Step "${step.id}" has a ${step.close.type} close — N2 executes predicate and hitl closes only.`, options: ['restate the close as a predicate', 'wait for the verdict-classes rung'] });
|
|
477
|
-
emit('job-end', { outcome: 'close-unsupported', step: step.id, ...spend() });
|
|
478
|
-
return 'close-unsupported';
|
|
479
|
-
}
|
|
480
|
-
emit('step-start', { step: step.id, remainingUsd: job.budgetUsd - spentUsd, spentUsd, mode: step.mode ?? 'text' });
|
|
481
|
-
// Close-first skip (resume model): the workdir + the close ARE the
|
|
482
|
-
// checkpoint. Run the step's own arbiter BEFORE any tokens — already-green
|
|
483
|
-
// means the step's work is done, and the step skips as a DISTINCT record
|
|
484
|
-
// (already-green, never plain green: nothing was done, so it mints no
|
|
485
|
-
// learning credit and runs no on-green retention). Same instrument as
|
|
486
|
-
// ralph's verdict (runClose), same at-capture scrub as every close output.
|
|
487
|
-
// trim before splitting: validateJob reds whitespace-padded cmds, but an
|
|
488
|
-
// empty argv[0] would THROW at spawn — belt for any path around the validator
|
|
489
|
-
const closeArgv = step.close.cmd.trim().split(/\s+/);
|
|
490
|
-
const closeOpts = { timeoutMs: closeTimeoutMs, cwd: workdir, expect: step.close.expect, judged: step.close.judged, gapKeep: step.close.gapKeep };
|
|
491
|
-
const pre = runClose(closeArgv, (/** @type {string} */ s) => redact(s, { patterns: SECRET_PATTERNS }), closeOpts);
|
|
492
|
-
emit('close-precheck', { step: step.id, ...pre });
|
|
493
|
-
if (pre.verdict === 'satisfied') {
|
|
494
|
-
emit('step-end', { step: step.id, outcome: 'already-green', spentUsd });
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
// reds-before-tokens: the forbidden zone is checked BEFORE the draft and the
|
|
498
|
-
// worker call. A close that cannot run, never finished, died by signal, or
|
|
499
|
-
// judged nothing rendered NO VERDICT — escalate by its own name (the SAME
|
|
500
|
-
// map ralph uses; two maps would be two instruments), never spend on it.
|
|
501
|
-
const fault = Object.hasOwn(CLOSE_FAULTS, pre.verdict) ? CLOSE_FAULTS[pre.verdict] : undefined;
|
|
502
|
-
if (fault) {
|
|
503
|
-
emit('escalation', { category: fault.category, decisionReady: true, step: step.id, decision: fault.decision, options: fault.options, detail: pre.detail, spentUsd });
|
|
504
|
-
emit('job-end', { outcome: 'step-red', step: step.id, cause: fault.category, ...spend() });
|
|
505
|
-
return `step-red:${step.id}`;
|
|
506
|
-
}
|
|
507
|
-
// The precheck's gap goes to the worker as what it IS: the close's output on
|
|
508
|
-
// the tree as it stands (F13). It is never framed as an attempt — no attempt
|
|
509
|
-
// has happened — so the contrast evidence stays clean. Withholding it (the
|
|
510
|
-
// original call) left the worker unable to see the failure it was hired to
|
|
511
|
-
// fix: `run` is a locked verb, so it cannot execute the close itself.
|
|
512
|
-
const halted = await ensureConfig();
|
|
513
|
-
if (halted) return halted;
|
|
514
|
-
let outcome;
|
|
515
|
-
try {
|
|
516
|
-
outcome = await interpret(config, {
|
|
517
|
-
task: `${job.description} — step: ${step.id}`,
|
|
518
|
-
target, close: closeArgv, workdir, capRuns,
|
|
519
|
-
emit: meter, provider, ...cOpts(), closeTimeoutMs, closeState: pre.gap,
|
|
520
|
-
// the SPEC's grant, threaded verbatim (2b decision #2) — the drafted
|
|
521
|
-
// config cannot express mode or tools, so there is nothing to merge.
|
|
522
|
-
// expect/judged/gapKeep ride the same rail for the same reason: they are
|
|
523
|
-
// arbiter territory (PRD v1.11; F28 for gapKeep).
|
|
524
|
-
mode: step.mode ?? 'text', tools: step.tools,
|
|
525
|
-
closeExpect: step.close.expect, closeJudged: step.close.judged, closeGapKeep: step.close.gapKeep,
|
|
526
|
-
layerRoot,
|
|
527
|
-
});
|
|
528
|
-
} catch (e) {
|
|
529
|
-
// ralph belts throws INSIDE the loop; this belts the interpreter's own
|
|
530
|
-
// setup (gate.init, store ctor) — the spine must terminate, never dangle
|
|
531
|
-
emit('escalation', { category: 'interpreter-red', decisionReady: true, step: step.id, decision: 'The interpreter broke outside the loop — no harness verdict is trustworthy until it is fixed.', options: ['fix the interpreter/run directory', 'abandon the run'], detail: String(/** @type {Error} */ (e)?.message ?? e), spentUsd });
|
|
532
|
-
emit('job-end', { outcome: 'step-red', step: step.id, cause: 'interpreter-red', ...spend() });
|
|
533
|
-
return `step-red:${step.id}`;
|
|
534
|
-
}
|
|
535
|
-
emit('step-end', { step: step.id, outcome, spentUsd });
|
|
536
|
-
if (unpriced) return pricingRed();
|
|
537
|
-
if (outcome !== 'green') {
|
|
538
|
-
emit('job-end', { outcome: 'step-red', step: step.id, cause: outcome, ...spend() });
|
|
539
|
-
return `step-red:${step.id}`;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
emit('job-end', { outcome: 'green', ...spend() });
|
|
543
|
-
return 'green';
|
|
544
282
|
}
|