bareloop 0.4.0 → 0.5.1
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 +135 -0
- package/README.md +10 -10
- package/bareloop.context.md +147 -24
- package/package.json +2 -2
- package/src/exits.js +163 -0
- package/src/index.js +7 -1
- package/src/interpret.js +25 -22
- package/src/job.js +231 -88
- package/src/plan.js +327 -0
- package/src/planrun.js +788 -0
- package/src/ralph.js +18 -6
- package/src/run.js +41 -4
- package/types/exits.d.ts +51 -0
- package/types/index.d.ts +4 -1
- package/types/interpret.d.ts +98 -0
- package/types/job.d.ts +12 -0
- package/types/plan.d.ts +77 -0
- package/types/planrun.d.ts +59 -0
- package/types/ralph.d.ts +20 -3
- package/types/run.d.ts +12 -2
package/src/ralph.js
CHANGED
|
@@ -235,7 +235,15 @@ export const CLOSE_FAULTS = Object.freeze({
|
|
|
235
235
|
*
|
|
236
236
|
* @param {object} opts
|
|
237
237
|
* @param {(iteration: number, gap?: string) => void|Promise<void>} opts.middle the emergent middle; never sees close/cap
|
|
238
|
-
* @param {string[]} opts.close argv whose exit code is truth
|
|
238
|
+
* @param {string[]} [opts.close] argv whose exit code is truth (required unless `judge` is injected)
|
|
239
|
+
* @param {() => Promise<{verdict: string, gap?: string, detail?: string}>|{verdict: string, gap?: string, detail?: string}} [opts.judge]
|
|
240
|
+
* Layer 2 (PRD v1.12 §4): the judge generalized to a SHELL-owned seam. When
|
|
241
|
+
* present it replaces runClose for this loop — a plan STEP's micro-loop is
|
|
242
|
+
* judged by the exit evaluator (declarative form checks), not a command. It
|
|
243
|
+
* returns the same verdict vocabulary runClose renders, so the forbidden
|
|
244
|
+
* zone, worker-crash routing (F32), and the cap taxonomy apply unchanged.
|
|
245
|
+
* Injected by run.js only; inexpressible in any config or plan — the agent
|
|
246
|
+
* never authors its judge any more than its close.
|
|
239
247
|
* @param {number} opts.capRuns budget: max middle runs
|
|
240
248
|
* @param {(type: string, data?: object) => object} opts.emit a spine emitter
|
|
241
249
|
* @param {(s: string) => string} [opts.redact] source scrubber for close output
|
|
@@ -255,13 +263,15 @@ export const CLOSE_FAULTS = Object.freeze({
|
|
|
255
263
|
* with zero writes, a crash stays what it always was: an instrument stop.
|
|
256
264
|
* @returns {Promise<'green'|'escalated'>}
|
|
257
265
|
*/
|
|
258
|
-
export async function ralph({ middle, close, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }) {
|
|
259
|
-
emit('run-start', { capRuns, close: close.join(' ') });
|
|
266
|
+
export async function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }) {
|
|
267
|
+
emit('run-start', { capRuns, close: judge ? 'judge:injected' : /** @type {string[]} */ (close).join(' ') });
|
|
260
268
|
// The blind spot is NAMED, never hidden: with no judgment-rendered signal this
|
|
261
269
|
// close cannot tell a crash from an honest red (they are byte-identical at the
|
|
262
270
|
// exit-code seam), so the record says so out loud rather than passing the exit
|
|
263
|
-
// code off as trustworthy (PRD v1.11).
|
|
264
|
-
|
|
271
|
+
// code off as trustworthy (PRD v1.11). The judged-floor story belongs to
|
|
272
|
+
// CLOSES: an injected judge (Layer 2 exit evaluation) owns its own honesty
|
|
273
|
+
// reads, so the note would be noise there.
|
|
274
|
+
if (!judged && !judge) emit('close-unaudited', { close: /** @type {string[]} */ (close).join(' '), meaning: 'no judgment-rendered signal declared — a crash-at-load is indistinguishable from an honest red for this close' });
|
|
265
275
|
const verdicts = [];
|
|
266
276
|
let gap;
|
|
267
277
|
for (let iteration = 1; iteration <= capRuns; iteration++) {
|
|
@@ -299,7 +309,9 @@ export async function ralph({ middle, close, capRuns, emit, redact, closeTimeout
|
|
|
299
309
|
return 'escalated';
|
|
300
310
|
}
|
|
301
311
|
emit('middle-done', { iteration });
|
|
302
|
-
const v =
|
|
312
|
+
const v = judge
|
|
313
|
+
? await judge()
|
|
314
|
+
: runClose(/** @type {string[]} */ (close), redact, { timeoutMs: closeTimeoutMs ?? 120_000, cwd, expect, judged, gapKeep });
|
|
303
315
|
// Worker-crash attribution (F32, measured in F31: 4 of 7 battery rows). A crash is
|
|
304
316
|
// still not a verdict (F17) — but a crash that FOLLOWS worker writes, on a run whose
|
|
305
317
|
// precheck proved the close judged at baseline (run.js escalates a crash-at-precheck
|
package/src/run.js
CHANGED
|
@@ -13,6 +13,7 @@ import { redact } from 'bareguard';
|
|
|
13
13
|
import { validateJob, jobSpecHash, checkApproval } from './job.js';
|
|
14
14
|
import { validateConfig, LOOP_SHAPES, SLOTS, VERBS, globToPrefix, SECRET_PATTERNS, REMEMBER_KINDS } from './validate.js';
|
|
15
15
|
import { interpret } from './interpret.js';
|
|
16
|
+
import { runPlan } from './planrun.js';
|
|
16
17
|
import { runClose, CLOSE_FAULTS } from './ralph.js';
|
|
17
18
|
import { extractArtifact, priceOf } from './text.js';
|
|
18
19
|
|
|
@@ -194,7 +195,11 @@ async function openDraftPr({ workdir, branch, title, body, addPaths, exec }) {
|
|
|
194
195
|
* @param {unknown} opts.approvals `{ specHash, signer, ts }` records (from OUTSIDE the spec)
|
|
195
196
|
* @param {string} opts.workdir the run directory (the fence's root)
|
|
196
197
|
* @param {string} [opts.target] the step artifact path (text-mode steps; unused by tool-mode steps)
|
|
197
|
-
* @param {any} opts.provider shell-owned LLM binding (never the config's)
|
|
198
|
+
* @param {any} opts.provider shell-owned LLM binding (never the config's; the Loop path)
|
|
199
|
+
* @param {(o: {policy: Function, onTurn?: Function, maxTurns: number, hasTools: boolean}) => any} [opts.nativeProvider]
|
|
200
|
+
* native clipipe provider factory (BA-16), required for a `clipipe-subscription` plan-shape job —
|
|
201
|
+
* threaded to runPlan, which builds one per worker (native tool mode when `hasTools`, else a
|
|
202
|
+
* metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
|
|
198
203
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
199
204
|
* @param {number} [opts.capRuns] shell-owned per-step iteration cap
|
|
200
205
|
* @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
|
|
@@ -211,7 +216,7 @@ async function openDraftPr({ workdir, branch, title, body, addPaths, exec }) {
|
|
|
211
216
|
* 'job-red' | 'smoke-red' | 'config-red' | 'pricing-red' | 'provider-red' |
|
|
212
217
|
* 'cap-halt' | 'close-unsupported' | `step-red:<id>`
|
|
213
218
|
*/
|
|
214
|
-
export async function runJob(rawSpec, { approvals, workdir, target, provider, emit, capRuns = 3, shellCapUsd = 2, closeTimeoutMs, execCmd = defaultExec, layerRoot = false }) {
|
|
219
|
+
export async function runJob(rawSpec, { approvals, workdir, target, provider, nativeProvider, emit, capRuns = 3, shellCapUsd = 2, closeTimeoutMs, execCmd = defaultExec, layerRoot = false }) {
|
|
215
220
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
216
221
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
217
222
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -238,16 +243,23 @@ export async function runJob(rawSpec, { approvals, workdir, target, provider, em
|
|
|
238
243
|
return 'job-red';
|
|
239
244
|
}
|
|
240
245
|
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;
|
|
241
250
|
// Reds-before-tokens: a text-mode predicate step writes ONE artifact — with
|
|
242
251
|
// no target it would burn a draft + a worker call and then crash inside the
|
|
243
252
|
// middle as a misfiled interpreter-red. The spec is fine; the CALL is not.
|
|
244
|
-
if ((typeof target !== 'string' || !target)
|
|
253
|
+
if (!planShape && (typeof target !== 'string' || !target)
|
|
245
254
|
&& job.steps.some((/** @type {any} */ s) => s.close.type === 'predicate' && (s.mode ?? 'text') === 'text')) {
|
|
246
255
|
emit('job-red', { code: 'missing-required', path: 'opts.target', detail: 'text-mode steps write ONE artifact — pass opts.target (reds-before-tokens)' });
|
|
247
256
|
emit('job-end', { outcome: 'job-red', ...spend() });
|
|
248
257
|
return 'job-red';
|
|
249
258
|
}
|
|
250
|
-
emit('job-start', {
|
|
259
|
+
emit('job-start', {
|
|
260
|
+
job: job.job, specHash: jobSpecHash(job), budgetUsd: job.budgetUsd,
|
|
261
|
+
...(planShape ? { shape: 'plan', goal: job.goal } : { steps: job.steps.map((/** @type {any} */ s) => s.id) }),
|
|
262
|
+
});
|
|
251
263
|
|
|
252
264
|
// 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
|
|
253
265
|
const smoke = await primitiveSmoke(workdir);
|
|
@@ -284,6 +296,31 @@ export async function runJob(rawSpec, { approvals, workdir, target, provider, em
|
|
|
284
296
|
return 'pricing-red';
|
|
285
297
|
};
|
|
286
298
|
|
|
299
|
+
// ── Layer 2 dispatch: the plan flow (SCOUT → PLAN → EXECUTE → close) under
|
|
300
|
+
// the SAME approval gate, smoke, and ledger — runJob stays the one entry
|
|
301
|
+
// (N2 lock). runPlan emits every provider round as worker-round, so the
|
|
302
|
+
// metered emit above accounts the plan flow natively (F12); the job-end
|
|
303
|
+
// money contract is identical to the legacy path.
|
|
304
|
+
if (planShape) {
|
|
305
|
+
const outcome = await runPlan(job, {
|
|
306
|
+
workdir, provider, nativeProvider, emit: meter, capRuns, closeTimeoutMs, layerRoot,
|
|
307
|
+
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
308
|
+
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
309
|
+
});
|
|
310
|
+
if (unpriced) return pricingRed();
|
|
311
|
+
if (outcome.startsWith('step-red:')) {
|
|
312
|
+
emit('job-end', { outcome: 'step-red', step: outcome.slice('step-red:'.length), ...spend() });
|
|
313
|
+
} else if (outcome === 'provider-red') {
|
|
314
|
+
// 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 — mirror the
|
|
316
|
+
// legacy providerRed(): spendComplete false, never overclaim an exact total.
|
|
317
|
+
emit('job-end', { outcome, ...spend(), spendComplete: false });
|
|
318
|
+
} else {
|
|
319
|
+
emit('job-end', { outcome, ...spend() });
|
|
320
|
+
}
|
|
321
|
+
return outcome;
|
|
322
|
+
}
|
|
323
|
+
|
|
287
324
|
// 4. sealed drafting: one shot + one redraft, reds fed back, PRICED path
|
|
288
325
|
// (through Loop, the same accounting the worker uses — never around it, F6)
|
|
289
326
|
const drafter = new Loop({ provider, system: 'You draft workflow configs as pure JSON. Output only the JSON object.' });
|
package/types/exits.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** @typedef {{ type: string, pass: boolean, detail?: string, fault?: string }} ExitResult
|
|
2
|
+
* `fault` present means the INSTRUMENT could not judge (unwired snapshot/seam,
|
|
3
|
+
* a check in runClose's forbidden zone) — it carries the runClose verdict name
|
|
4
|
+
* ('failed'|'timed-out'|'killed'|'crashed') so the micro-loop escalates through
|
|
5
|
+
* the same CLOSE_FAULTS taxonomy instead of feeding a fake gap to the worker.
|
|
6
|
+
* A plain pass:false with no fault is honest worker feedback. */
|
|
7
|
+
/**
|
|
8
|
+
* Snapshot the file state under a scope prefix — the "before" side of
|
|
9
|
+
* `tree-changed`. Content hashes, not mtimes: only real bytes count as
|
|
10
|
+
* outcome. A scope directory that does not exist yet snapshots empty (the
|
|
11
|
+
* step creating it IS the change). Never throws on a missing dir.
|
|
12
|
+
* @param {string} dir absolute workdir (the run directory)
|
|
13
|
+
* @param {string} scope a validated scope glob (e.g. `tests/**`)
|
|
14
|
+
* @returns {Promise<Map<string, string>>} relative path → sha256
|
|
15
|
+
*/
|
|
16
|
+
export function snapshotScope(dir: string, scope: string): Promise<Map<string, string>>;
|
|
17
|
+
/**
|
|
18
|
+
* Evaluate a step's exits — AND-only (decision 8): the step passes iff EVERY
|
|
19
|
+
* listed exit passes. All exits are evaluated (never short-circuit): the gap
|
|
20
|
+
* must name every wall, not the first one hit.
|
|
21
|
+
* @param {Array<Record<string, any>>} exits validated exit items
|
|
22
|
+
* @param {{ dir: string, snapshot?: Map<string, string>, runCheck?: (name: string) => Promise<{pass: boolean, gap?: string}> }} ctx
|
|
23
|
+
* `dir`: the run directory; `snapshot`: the pre-step `snapshotScope` result
|
|
24
|
+
* (required by tree-changed — absent fails closed); `runCheck`: the runner's
|
|
25
|
+
* runClose-backed seam (required by check-passes — absent fails closed).
|
|
26
|
+
* @returns {Promise<{ pass: boolean, results: ExitResult[] }>}
|
|
27
|
+
*/
|
|
28
|
+
export function evalExits(exits: Array<Record<string, any>>, { dir, snapshot, runCheck }: {
|
|
29
|
+
dir: string;
|
|
30
|
+
snapshot?: Map<string, string>;
|
|
31
|
+
runCheck?: (name: string) => Promise<{
|
|
32
|
+
pass: boolean;
|
|
33
|
+
gap?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
pass: boolean;
|
|
37
|
+
results: ExitResult[];
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* `fault` present means the INSTRUMENT could not judge (unwired snapshot/seam,
|
|
41
|
+
* a check in runClose's forbidden zone) — it carries the runClose verdict name
|
|
42
|
+
* ('failed'|'timed-out'|'killed'|'crashed') so the micro-loop escalates through
|
|
43
|
+
* the same CLOSE_FAULTS taxonomy instead of feeding a fake gap to the worker.
|
|
44
|
+
* A plain pass:false with no fault is honest worker feedback.
|
|
45
|
+
*/
|
|
46
|
+
export type ExitResult = {
|
|
47
|
+
type: string;
|
|
48
|
+
pass: boolean;
|
|
49
|
+
detail?: string;
|
|
50
|
+
fault?: string;
|
|
51
|
+
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export { makeSpine } from "./spine.js";
|
|
2
|
+
export { runPlan } from "./planrun.js";
|
|
2
3
|
export { runJob } from "./run.js";
|
|
3
4
|
export { ralph, runClose, CLOSE_FAULTS } from "./ralph.js";
|
|
4
5
|
export { validateConfig, diffPaths, globToPrefix, scanSecrets, LOOP_SHAPES, SLOTS, VERBS } from "./validate.js";
|
|
5
|
-
export { validateJob, jobSpecHash, checkApproval, CLOSE_TYPES, CLASSES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, STEP_MODES, TOOL_MENU, LOCKED_TOOLS } from "./job.js";
|
|
6
|
+
export { validateJob, jobSpecHash, checkApproval, CLOSE_TYPES, CLASSES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, STEP_MODES, TOOL_MENU, LOCKED_TOOLS, VERDICT_TYPES, LOCKED_VERDICTS } from "./job.js";
|
|
7
|
+
export { validatePlan, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from "./plan.js";
|
|
8
|
+
export { snapshotScope, evalExits } from "./exits.js";
|
|
6
9
|
export { interpret, STALL_REDS } from "./interpret.js";
|
|
7
10
|
export { extractRules, MAX_RULES, MAX_RULE_CHARS } from "./extract.js";
|
|
8
11
|
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from "./ledger.js";
|
package/types/interpret.d.ts
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retrieval pair (F19). `shell_read` cannot seek — it starts at byte zero — so a
|
|
3
|
+
* pointer at a symbol was inert and the worker paged whole files to reach one function.
|
|
4
|
+
* `ctx_recall` hands back a POINTER (no body: a search index returns pointers, and bodies
|
|
5
|
+
* on every hit rebuild the bloat — measured, a 5-hit recall dumps ~15k tokens unbidden);
|
|
6
|
+
* `ctx_get` trades that pointer for ONE chunk. A chunk starts at its doc-comment, so the
|
|
7
|
+
* body arrives WITH the docstring that says what it was supposed to do.
|
|
8
|
+
* @param {any} lc the run's LiteCtx (rooted at workdir)
|
|
9
|
+
* @param {string} workdir
|
|
10
|
+
* @param {(type: string, data: object) => void} emit the spine — a retrieval verb whose
|
|
11
|
+
* RESULT is invisible cannot be judged: a `ctx_get` that silently reds (stale pointer,
|
|
12
|
+
* bad range) looks exactly like one that worked, and the worker's fallback to a
|
|
13
|
+
* whole-file read looks like a free choice instead of a forced one.
|
|
14
|
+
*/
|
|
15
|
+
export function createCtxTools(lc: any, workdir: string, emit: (type: string, data: object) => void): ({
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: {
|
|
19
|
+
type: string;
|
|
20
|
+
properties: {
|
|
21
|
+
query: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
n: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
path?: undefined;
|
|
30
|
+
startLine?: undefined;
|
|
31
|
+
endLine?: undefined;
|
|
32
|
+
};
|
|
33
|
+
required: string[];
|
|
34
|
+
};
|
|
35
|
+
execute: ({ query, n }: {
|
|
36
|
+
query: string;
|
|
37
|
+
n?: number;
|
|
38
|
+
}) => Promise<any>;
|
|
39
|
+
} | {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
parameters: {
|
|
43
|
+
type: string;
|
|
44
|
+
properties: {
|
|
45
|
+
path: {
|
|
46
|
+
type: string;
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
49
|
+
startLine: {
|
|
50
|
+
type: string;
|
|
51
|
+
description: string;
|
|
52
|
+
};
|
|
53
|
+
endLine: {
|
|
54
|
+
type: string;
|
|
55
|
+
description: string;
|
|
56
|
+
};
|
|
57
|
+
query?: undefined;
|
|
58
|
+
n?: undefined;
|
|
59
|
+
};
|
|
60
|
+
required: string[];
|
|
61
|
+
};
|
|
62
|
+
execute: ({ path: p, startLine, endLine }: {
|
|
63
|
+
path: string;
|
|
64
|
+
startLine: number;
|
|
65
|
+
endLine: number;
|
|
66
|
+
}) => Promise<any>;
|
|
67
|
+
})[];
|
|
1
68
|
/**
|
|
2
69
|
* Execute a workflow config against one task under the dumb shell.
|
|
3
70
|
*
|
|
@@ -87,6 +154,37 @@ export function interpret(configRaw: object | string, { task, target, close, wor
|
|
|
87
154
|
}): Promise<"green" | "escalated" | "config-red">;
|
|
88
155
|
/** @typedef {Error & {category?: string, lib?: string}} CategorizedError the failure map's carrier: ralph relays by `category`, and by `lib` when the thrower knows which library owns the failure (the ledger prefers it over prose) */
|
|
89
156
|
export const STALL_REDS: 2;
|
|
157
|
+
export const PERSONA_TOOLS: string;
|
|
158
|
+
export const RETRIEVAL_STRATEGY: string;
|
|
159
|
+
export const EDIT_STRATEGY: string;
|
|
160
|
+
export const TOOL_BY_VERB: Readonly<{
|
|
161
|
+
read: "shell_read";
|
|
162
|
+
grep: "shell_grep";
|
|
163
|
+
write: "shell_write";
|
|
164
|
+
edit: "shell_edit";
|
|
165
|
+
recall: "ctx_recall";
|
|
166
|
+
get: "ctx_get";
|
|
167
|
+
}>;
|
|
168
|
+
export const CTX_TOOLS: readonly string[];
|
|
169
|
+
export function toolAction(name: string, args: any, workdir?: string): {
|
|
170
|
+
type: string;
|
|
171
|
+
path: string;
|
|
172
|
+
args: {
|
|
173
|
+
bytes: number;
|
|
174
|
+
tool?: undefined;
|
|
175
|
+
};
|
|
176
|
+
} | {
|
|
177
|
+
type: string;
|
|
178
|
+
path: string;
|
|
179
|
+
args: {
|
|
180
|
+
tool: string;
|
|
181
|
+
bytes?: undefined;
|
|
182
|
+
};
|
|
183
|
+
} | {
|
|
184
|
+
type: string;
|
|
185
|
+
args: any;
|
|
186
|
+
path?: undefined;
|
|
187
|
+
};
|
|
90
188
|
/**
|
|
91
189
|
* the failure map's carrier: ralph relays by `category`, and by `lib` when the thrower knows which library owns the failure (the ledger prefers it over prose)
|
|
92
190
|
*/
|
package/types/job.d.ts
CHANGED
|
@@ -71,7 +71,19 @@ export const TOOL_MENU: readonly string[];
|
|
|
71
71
|
* menu. Requesting one is a DISTINCT `request-red` (module 4) — the ledger
|
|
72
72
|
* counts admission demand, and a generic invalid-value would bury it as a typo. */
|
|
73
73
|
export const LOCKED_TOOLS: readonly string[];
|
|
74
|
+
/** Layer 2 (design record 2026-07-21, decision 5): the verdict-type radio of
|
|
75
|
+
* the four-field plan shape. v1 ADMITS only `green`; soft-green/hitl are
|
|
76
|
+
* declared-but-locked — declaring one is a `request-red` (the tool-menu
|
|
77
|
+
* pattern: disclosure ≠ admission), so the ledger counts the demand and the
|
|
78
|
+
* non-code rung later fills a declared slot instead of reshaping the schema. */
|
|
79
|
+
export const VERDICT_TYPES: readonly string[];
|
|
80
|
+
export const LOCKED_VERDICTS: readonly string[];
|
|
74
81
|
export const CADENCE_UNITS: readonly string[];
|
|
82
|
+
/** SP-2: API-first; local deferred (PRD §5/§8). `clipipe-subscription` (F42,
|
|
83
|
+
* bare-agent 0.32.0 tool mode): the CLI as a bare turn-provider on a Claude
|
|
84
|
+
* subscription — its costUsd axis is NOTIONAL (API-equivalent value, not
|
|
85
|
+
* billed), so it is a DISTINCT condition: rows never pool with anthropic-api
|
|
86
|
+
* rows on the cost axis, and budgets sized for one do not transfer. */
|
|
75
87
|
export const PROVIDERS: readonly string[];
|
|
76
88
|
/** V3 environment label: declared keys only — every field is a lineage-key
|
|
77
89
|
* candidate at N3. `provider` is part of the key by definition (top-level,
|
package/types/plan.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F49 — a heuristic reject for the catastrophic-backtracking footgun: an
|
|
3
|
+
* unbounded quantifier applied to a group whose body itself repeats unboundedly
|
|
4
|
+
* (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
|
|
5
|
+
* group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
|
|
6
|
+
* and is caught by propagating the inner repeat up through the wrapper. Such a
|
|
7
|
+
* pattern can hang `RegExp.test` for
|
|
8
|
+
* seconds on a short crafted body, and `evalExits` runs the agent-authored
|
|
9
|
+
* `artifact-written` pattern with NO timeout. This is self-DoS only — the agent
|
|
10
|
+
* authors both the pattern and (via the worker) the artifact, so a hang burns
|
|
11
|
+
* only its own run's wall-clock; there is NO arbiter compromise (it cannot
|
|
12
|
+
* escape the fence, forge a green, or leak a secret). So the reject targets the
|
|
13
|
+
* dominant exponential class and fails it as a mechanical gap at the validation
|
|
14
|
+
* gate, before any tokens burn. A full ReDoS analyzer needs a real regex engine
|
|
15
|
+
* (an external native dep we do not take for a LOW issue); exotic
|
|
16
|
+
* overlapping-alternation blowup is out of scope by decision.
|
|
17
|
+
*
|
|
18
|
+
* SCOPE IS ASYMMETRIC, both directions named on purpose:
|
|
19
|
+
* - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
|
|
20
|
+
* detected — out of scope (self-DoS only, no arbiter compromise).
|
|
21
|
+
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
22
|
+
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
23
|
+
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
24
|
+
* SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
|
|
25
|
+
* never admits an unsafe pattern), and the cost is bounded: the plan drafter
|
|
26
|
+
* gets a mechanical gap and rewrites (drop the outer `+`, or match once).
|
|
27
|
+
* Detecting "safe because anchored" needs the same real engine we declined,
|
|
28
|
+
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
29
|
+
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
30
|
+
*
|
|
31
|
+
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
32
|
+
* so this scan assumes valid, balanced JS regex syntax.
|
|
33
|
+
* @param {string} src a compiled regex source string
|
|
34
|
+
* @returns {boolean} true iff a nested unbounded quantifier is present
|
|
35
|
+
*/
|
|
36
|
+
export function hasNestedQuantifier(src: string): boolean;
|
|
37
|
+
/** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red */
|
|
38
|
+
/**
|
|
39
|
+
* Validate an agent-authored plan (`schema: "plan-v1"`) against the SIGNED
|
|
40
|
+
* job spec. Never throws on JSON text or plain parsed data; every failure is
|
|
41
|
+
* a named red. Returns the parsed plan on ok (single parse), null on any red.
|
|
42
|
+
* @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
|
|
43
|
+
* @param {{ job?: any, maxStepRounds?: number }} [opts] `job`: the
|
|
44
|
+
* validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
|
|
45
|
+
* menu all come from it — a missing or non-plan-shape job fails CLOSED);
|
|
46
|
+
* `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
|
|
47
|
+
* tool-mode TURNS_PER_ATTEMPT) — a step may tighten it, never exceed it.
|
|
48
|
+
* @returns {{ ok: boolean, reds: Red[], plan: object|null }}
|
|
49
|
+
*/
|
|
50
|
+
export function validatePlan(input: object | string, { job, maxStepRounds }?: {
|
|
51
|
+
job?: any;
|
|
52
|
+
maxStepRounds?: number;
|
|
53
|
+
}): {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
reds: Red[];
|
|
56
|
+
plan: object | null;
|
|
57
|
+
};
|
|
58
|
+
/** the closed exit menu (PRD v1.12 §3 + decision 1's `check-passes`): the
|
|
59
|
+
* shell evaluates every form with its own fixed code, never a command — the
|
|
60
|
+
* same both-directions inexpressibility guard as the two-validator split */
|
|
61
|
+
export const EXIT_TYPES: readonly string[];
|
|
62
|
+
/** decision 8: AND-only conjunction, bounded — no OR (a weak arm would
|
|
63
|
+
* launder the exit), no NOT */
|
|
64
|
+
export const MAX_EXITS_PER_STEP: 2;
|
|
65
|
+
/** plan size bound — per-step rounds bound the spend, but an unbounded step
|
|
66
|
+
* COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
|
|
67
|
+
export const MAX_PLAN_STEPS: 8;
|
|
68
|
+
/** the write-class verbs (BA-13: `edit` is judged by the same fence as
|
|
69
|
+
* `write`) — a step granting one is a WRITE step for the F17 pairing rule
|
|
70
|
+
* and the v1.18 target requirement */
|
|
71
|
+
export const WRITE_VERBS: readonly string[];
|
|
72
|
+
export type Red = {
|
|
73
|
+
code: string;
|
|
74
|
+
path: string;
|
|
75
|
+
detail?: string;
|
|
76
|
+
verb?: string;
|
|
77
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute a validateJob-GREEN plan-shape job (goal/verdictType/close/checks[]).
|
|
3
|
+
* Called by runJob after the approval gate, validation, and the smoke — this
|
|
4
|
+
* function owns the plan flow only; the caller owns the job-end record and the
|
|
5
|
+
* one ledger (every provider round is emitted here as `worker-round`, which
|
|
6
|
+
* the caller's metered emit accounts — F12).
|
|
7
|
+
*
|
|
8
|
+
* @param {any} job the validated plan-shape spec
|
|
9
|
+
* @param {object} opts
|
|
10
|
+
* @param {string} opts.workdir the run directory (the fence's root)
|
|
11
|
+
* @param {any} opts.provider shell-owned LLM binding (the Loop path — `anthropic-api`)
|
|
12
|
+
* @param {(o: {policy: Function, onTurn?: Function, maxTurns: number, hasTools: boolean}) => any} [opts.nativeProvider]
|
|
13
|
+
* NATIVE clipipe factory (BA-16): required when `job.provider === 'clipipe-subscription'`.
|
|
14
|
+
* The runner builds a FRESH provider per worker and picks the mode by `hasTools`:
|
|
15
|
+
* `true` → native tool mode (`toolProtocol:'claude-mcp'`, wire `policy`+`onTurn`+`maxTurns`);
|
|
16
|
+
* `false` → the drafter has no tools, so a native session would report NO cost — return a
|
|
17
|
+
* metered claude-json TEXT provider (`--output-format json`, `parse:'claude-json'`) instead,
|
|
18
|
+
* so its spend is never invisible. The Loop path (`anthropic-api`) never touches this.
|
|
19
|
+
* @param {(type: string, data?: object) => object} opts.emit spine emitter (the caller's METERED emit)
|
|
20
|
+
* @param {() => number} opts.remainingUsd the one wallet: what is left of the signed budget right now
|
|
21
|
+
* @param {() => boolean} [opts.isUnpriced] has any round come back with a null cost? (F6) — the
|
|
22
|
+
* plan flow bails IN-FLIGHT on the first unpriced round instead of burning the whole plan
|
|
23
|
+
* @param {number} [opts.capRuns] shell-owned per-step attempt cap
|
|
24
|
+
* @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
|
|
25
|
+
* @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
|
|
26
|
+
* @param {boolean} [opts.layerRoot=false] Layer R — the within-run ratchet (src/root.js),
|
|
27
|
+
* scoped PER STEP's ralph loop (each micro-wheel is the Layer-1 atom). Shell-assembled from
|
|
28
|
+
* the step's own books: per-attempt write-sets from the F32 workerWrites audit (teed for
|
|
29
|
+
* same-path rewrites, which the cumulative audit alone cannot see) and the red-set from the
|
|
30
|
+
* exit evaluator's own gap. Defaults OFF (decided 2026-07-21, F41): fixation is extinct on
|
|
31
|
+
* every current job, so ON has never won its A/B; `true` is the ON/experimental arm, and the
|
|
32
|
+
* first plan-flow job to emit `root-injected` runs the pre-registered ON-vs-OFF acceptance
|
|
33
|
+
* read (the Layer R default-flip, LAYERS.md ⚠). Excluded on native (clipipe): the native
|
|
34
|
+
* worker has no onToolResult seam, so the tee cannot settle and same-path rewrites are blind.
|
|
35
|
+
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
36
|
+
* 'check-red' | 'close-red' | 'close-unsupported' | 'pricing-red' | 'cap-halt' |
|
|
37
|
+
* 'provider-red' | 'interpreter-red' | `step-red:<id>`
|
|
38
|
+
*/
|
|
39
|
+
export function runPlan(job: any, { workdir, provider, nativeProvider, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds, layerRoot }: {
|
|
40
|
+
workdir: string;
|
|
41
|
+
provider: any;
|
|
42
|
+
nativeProvider?: ((o: {
|
|
43
|
+
policy: Function;
|
|
44
|
+
onTurn?: Function;
|
|
45
|
+
maxTurns: number;
|
|
46
|
+
hasTools: boolean;
|
|
47
|
+
}) => any) | undefined;
|
|
48
|
+
emit: (type: string, data?: object) => object;
|
|
49
|
+
remainingUsd: () => number;
|
|
50
|
+
isUnpriced?: (() => boolean) | undefined;
|
|
51
|
+
capRuns?: number | undefined;
|
|
52
|
+
closeTimeoutMs?: number | undefined;
|
|
53
|
+
maxStepRounds?: number | undefined;
|
|
54
|
+
layerRoot?: boolean | undefined;
|
|
55
|
+
}): Promise<string>;
|
|
56
|
+
export type CategorizedError = Error & {
|
|
57
|
+
category?: string;
|
|
58
|
+
lib?: string;
|
|
59
|
+
};
|
package/types/ralph.d.ts
CHANGED
|
@@ -98,7 +98,15 @@ export function runClose(close: string[], redact?: (s: string) => string, { time
|
|
|
98
98
|
*
|
|
99
99
|
* @param {object} opts
|
|
100
100
|
* @param {(iteration: number, gap?: string) => void|Promise<void>} opts.middle the emergent middle; never sees close/cap
|
|
101
|
-
* @param {string[]} opts.close argv whose exit code is truth
|
|
101
|
+
* @param {string[]} [opts.close] argv whose exit code is truth (required unless `judge` is injected)
|
|
102
|
+
* @param {() => Promise<{verdict: string, gap?: string, detail?: string}>|{verdict: string, gap?: string, detail?: string}} [opts.judge]
|
|
103
|
+
* Layer 2 (PRD v1.12 §4): the judge generalized to a SHELL-owned seam. When
|
|
104
|
+
* present it replaces runClose for this loop — a plan STEP's micro-loop is
|
|
105
|
+
* judged by the exit evaluator (declarative form checks), not a command. It
|
|
106
|
+
* returns the same verdict vocabulary runClose renders, so the forbidden
|
|
107
|
+
* zone, worker-crash routing (F32), and the cap taxonomy apply unchanged.
|
|
108
|
+
* Injected by run.js only; inexpressible in any config or plan — the agent
|
|
109
|
+
* never authors its judge any more than its close.
|
|
102
110
|
* @param {number} opts.capRuns budget: max middle runs
|
|
103
111
|
* @param {(type: string, data?: object) => object} opts.emit a spine emitter
|
|
104
112
|
* @param {(s: string) => string} [opts.redact] source scrubber for close output
|
|
@@ -118,9 +126,18 @@ export function runClose(close: string[], redact?: (s: string) => string, { time
|
|
|
118
126
|
* with zero writes, a crash stays what it always was: an instrument stop.
|
|
119
127
|
* @returns {Promise<'green'|'escalated'>}
|
|
120
128
|
*/
|
|
121
|
-
export function ralph({ middle, close, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
|
|
129
|
+
export function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
|
|
122
130
|
middle: (iteration: number, gap?: string) => void | Promise<void>;
|
|
123
|
-
close
|
|
131
|
+
close?: string[] | undefined;
|
|
132
|
+
judge?: (() => Promise<{
|
|
133
|
+
verdict: string;
|
|
134
|
+
gap?: string;
|
|
135
|
+
detail?: string;
|
|
136
|
+
}> | {
|
|
137
|
+
verdict: string;
|
|
138
|
+
gap?: string;
|
|
139
|
+
detail?: string;
|
|
140
|
+
}) | undefined;
|
|
124
141
|
capRuns: number;
|
|
125
142
|
emit: (type: string, data?: object) => object;
|
|
126
143
|
redact?: ((s: string) => string) | undefined;
|
package/types/run.d.ts
CHANGED
|
@@ -28,7 +28,11 @@
|
|
|
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
30
|
* @param {string} [opts.target] the step artifact path (text-mode steps; unused by tool-mode steps)
|
|
31
|
-
* @param {any} opts.provider shell-owned LLM binding (never the config's)
|
|
31
|
+
* @param {any} opts.provider shell-owned LLM binding (never the config's; the Loop path)
|
|
32
|
+
* @param {(o: {policy: Function, onTurn?: Function, maxTurns: number, hasTools: boolean}) => any} [opts.nativeProvider]
|
|
33
|
+
* native clipipe provider factory (BA-16), required for a `clipipe-subscription` plan-shape job —
|
|
34
|
+
* threaded to runPlan, which builds one per worker (native tool mode when `hasTools`, else a
|
|
35
|
+
* metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
|
|
32
36
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
33
37
|
* @param {number} [opts.capRuns] shell-owned per-step iteration cap
|
|
34
38
|
* @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
|
|
@@ -45,11 +49,17 @@
|
|
|
45
49
|
* 'job-red' | 'smoke-red' | 'config-red' | 'pricing-red' | 'provider-red' |
|
|
46
50
|
* 'cap-halt' | 'close-unsupported' | `step-red:<id>`
|
|
47
51
|
*/
|
|
48
|
-
export function runJob(rawSpec: object | string, { approvals, workdir, target, provider, emit, capRuns, shellCapUsd, closeTimeoutMs, execCmd, layerRoot }: {
|
|
52
|
+
export function runJob(rawSpec: object | string, { approvals, workdir, target, provider, nativeProvider, emit, capRuns, shellCapUsd, closeTimeoutMs, execCmd, layerRoot }: {
|
|
49
53
|
approvals: unknown;
|
|
50
54
|
workdir: string;
|
|
51
55
|
target?: string | undefined;
|
|
52
56
|
provider: any;
|
|
57
|
+
nativeProvider?: ((o: {
|
|
58
|
+
policy: Function;
|
|
59
|
+
onTurn?: Function;
|
|
60
|
+
maxTurns: number;
|
|
61
|
+
hasTools: boolean;
|
|
62
|
+
}) => any) | undefined;
|
|
53
63
|
emit: (type: string, data?: object) => object;
|
|
54
64
|
capRuns?: number | undefined;
|
|
55
65
|
shellCapUsd?: number | undefined;
|