bareloop 0.2.0 → 0.4.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 +426 -0
- package/README.md +15 -6
- package/bareloop.context.md +295 -14
- package/package.json +3 -3
- package/src/extract.js +14 -6
- package/src/index.js +13 -3
- package/src/interpret.js +652 -29
- package/src/job.js +119 -3
- package/src/ledger.js +266 -0
- package/src/ralph.js +245 -15
- package/src/root.js +305 -0
- package/src/run.js +507 -0
- package/src/text.js +65 -8
- package/src/validate.js +42 -1
- package/types/extract.d.ts +4 -2
- package/types/index.d.ts +5 -3
- package/types/interpret.d.ts +47 -6
- package/types/job.d.ts +32 -1
- package/types/ledger.d.ts +76 -0
- package/types/ralph.d.ts +111 -17
- package/types/root.d.ts +96 -0
- package/types/run.d.ts +71 -0
- package/types/text.d.ts +35 -1
- package/types/validate.d.ts +13 -0
package/src/validate.js
CHANGED
|
@@ -15,6 +15,20 @@ export const LOOP_SHAPES = Object.freeze(['refine', 'plan']);
|
|
|
15
15
|
export const SLOTS = Object.freeze(['before-attempt', 'after-red', 'on-green']);
|
|
16
16
|
export const VERBS = Object.freeze(['recall', 'compress', 'stash', 'remember']);
|
|
17
17
|
const TOP_FIELDS = ['schema', 'loop', 'memory', 'hooks', 'gate', 'escalation'];
|
|
18
|
+
// Unknown-field guard, PER SECTION — not just at the top (F17). The arbiter
|
|
19
|
+
// split is guarded "both directions by inexpressibility": the drafted config
|
|
20
|
+
// must be UNABLE to say certain things. A guard that stops at the top level
|
|
21
|
+
// does not deliver that — `gate: { judged: {...} }` (the close's own honesty
|
|
22
|
+
// check) validated green, as would `gate.maxCostUsd`. Nothing consumed those
|
|
23
|
+
// keys, so nothing broke; the guarantee is that nothing ever can. Hook ops are
|
|
24
|
+
// guarded separately by VERB_PARAMS.
|
|
25
|
+
const SECTION_FIELDS = Object.freeze({
|
|
26
|
+
loop: ['shape', 'maxIterations'],
|
|
27
|
+
memory: ['store', 'recall', 'compressLevel'],
|
|
28
|
+
'memory.recall': ['k', 'kinds'],
|
|
29
|
+
gate: ['budgetUsd', 'writeScope'],
|
|
30
|
+
escalation: ['mode'],
|
|
31
|
+
});
|
|
18
32
|
const MAX_OPS_PER_SLOT = 2;
|
|
19
33
|
|
|
20
34
|
// per-verb parameter contracts: name → check(value) (op field itself excluded).
|
|
@@ -22,7 +36,7 @@ const MAX_OPS_PER_SLOT = 2;
|
|
|
22
36
|
// (the set remember() itself validates against — adaptlearn F5's drift lesson:
|
|
23
37
|
// the export you bind can be wider than the function you call), minus doc:
|
|
24
38
|
// the doc/upload axis stays gated out deliberately.
|
|
25
|
-
const REMEMBER_KINDS = WRITE_KINDS.filter((k) => k !== 'doc');
|
|
39
|
+
export const REMEMBER_KINDS = WRITE_KINDS.filter((k) => k !== 'doc'); // exported: the drafting prompt advertises THIS menu (one source, no drift)
|
|
26
40
|
const VERB_PARAMS = {
|
|
27
41
|
recall: { k: (v) => Number.isInteger(v) && v >= 1 && v <= 20, kinds: isKinds },
|
|
28
42
|
compress: { level: (v) => COMPRESS_LEVELS.includes(v) },
|
|
@@ -134,6 +148,24 @@ export const SECRET_PATTERNS = [
|
|
|
134
148
|
];
|
|
135
149
|
const SECRET_RE = new RegExp(SECRET_PATTERNS.map((r) => r.source).join('|'));
|
|
136
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Scan a RAW text stream (a spine file, a close's output, a transcript) for
|
|
153
|
+
* known secret shapes and return the literal matches. The ONE spelling of the
|
|
154
|
+
* text-side scan, for the same reason SECRET_PATTERNS is the ONE inventory: a
|
|
155
|
+
* hand-rolled copy that misses a shape is a leak on the very output it guards
|
|
156
|
+
* (seven copies of this expression lived in scripts/ before it landed here).
|
|
157
|
+
* `sweepSecretLiterals` is the config-tree twin — same shapes, tree walk.
|
|
158
|
+
* Never throws; returns [] for a missing stream.
|
|
159
|
+
* @param {unknown} raw
|
|
160
|
+
* @returns {string[]} every match, in stream order per pattern
|
|
161
|
+
*/
|
|
162
|
+
export function scanSecrets(raw) {
|
|
163
|
+
const text = String(raw ?? '');
|
|
164
|
+
// a fresh global clone per call: a shared /g regex carries lastIndex between
|
|
165
|
+
// calls and would skip matches on the next stream
|
|
166
|
+
return SECRET_PATTERNS.flatMap((re) => text.match(new RegExp(re.source, `${re.flags.replace('g', '')}g`)) ?? []);
|
|
167
|
+
}
|
|
168
|
+
|
|
137
169
|
/**
|
|
138
170
|
* Red every string in a config tree that carries a known secret-token shape.
|
|
139
171
|
* @param {any} root
|
|
@@ -287,6 +319,15 @@ export function validateConfig(input, { shellCapUsd = 2, jobWriteScope } = {}) {
|
|
|
287
319
|
}
|
|
288
320
|
}
|
|
289
321
|
|
|
322
|
+
// 4b. inexpressibility, at every depth the arbiter can be reached from
|
|
323
|
+
for (const [section, allowed] of Object.entries(SECTION_FIELDS)) {
|
|
324
|
+
const obj = section === 'memory.recall' ? memory.recall : c[section];
|
|
325
|
+
if (!isObj(obj)) continue;
|
|
326
|
+
for (const key of Object.keys(obj)) {
|
|
327
|
+
if (!allowed.includes(key)) red('unknown-field', `${section}.${key}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
290
331
|
// 5. secrets sweep — same guard as the job spec (the agent-authored document
|
|
291
332
|
// is the riskier one; a green config is a ledger fact and inherits forever)
|
|
292
333
|
sweepSecretLiterals(c, red);
|
package/types/extract.d.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* @param {object} opts.provider a bareagent provider — SHELL-owned, sealed
|
|
6
6
|
* @param {string[]|null} opts.priorRules the lineage's current rules, if any
|
|
7
7
|
* @param {string[]} [opts.revisionDiff] changed config paths, when the run recovered mid-run
|
|
8
|
-
* @returns {Promise<{rules: string[]|null, valid: boolean, reds: Array<object>, costUsd: number, raw: string}>}
|
|
8
|
+
* @returns {Promise<{rules: string[]|null, valid: boolean, reds: Array<object>, costUsd: number|null, raw: string}>}
|
|
9
|
+
* costUsd is null when the spend could not be priced — the explicit-unknown
|
|
10
|
+
* signal (F6: unpriced is never free); callers must not coerce it to 0
|
|
9
11
|
*/
|
|
10
12
|
export function extractRules({ config, provider, priorRules, revisionDiff }: {
|
|
11
13
|
config: object;
|
|
@@ -16,7 +18,7 @@ export function extractRules({ config, provider, priorRules, revisionDiff }: {
|
|
|
16
18
|
rules: string[] | null;
|
|
17
19
|
valid: boolean;
|
|
18
20
|
reds: Array<object>;
|
|
19
|
-
costUsd: number;
|
|
21
|
+
costUsd: number | null;
|
|
20
22
|
raw: string;
|
|
21
23
|
}>;
|
|
22
24
|
export const MAX_RULES: 5;
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { makeSpine } from "./spine.js";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
2
|
+
export { runJob } from "./run.js";
|
|
3
|
+
export { ralph, runClose, CLOSE_FAULTS } from "./ralph.js";
|
|
4
|
+
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";
|
|
5
6
|
export { interpret, STALL_REDS } from "./interpret.js";
|
|
6
7
|
export { extractRules, MAX_RULES, MAX_RULE_CHARS } from "./extract.js";
|
|
8
|
+
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from "./ledger.js";
|
package/types/interpret.d.ts
CHANGED
|
@@ -4,13 +4,26 @@
|
|
|
4
4
|
* @param {object|string} configRaw schema v1 config (object or raw JSON text)
|
|
5
5
|
* @param {object} opts
|
|
6
6
|
* @param {string} opts.task implement instruction shown to the worker
|
|
7
|
-
* @param {string} opts.target absolute path the artifact is written to
|
|
7
|
+
* @param {string} [opts.target] absolute path the artifact is written to —
|
|
8
|
+
* required in text mode; unused in tool mode (the worker writes through
|
|
9
|
+
* the gated tools, wherever the fence allows)
|
|
8
10
|
* @param {string[]} opts.close argv whose exit code is truth (shell-owned)
|
|
11
|
+
* @param {number} [opts.closeExpect] the exit code the SIGNED spec calls success
|
|
12
|
+
* @param {{pattern: string, min: number}} [opts.closeJudged] the signed spec's
|
|
13
|
+
* judgment-rendered signal — proof the close actually judged something before
|
|
14
|
+
* its exit code is believed in EITHER direction (PRD v1.11; the drafted config
|
|
15
|
+
* cannot express it, and must not: it is the arbiter's own honesty check)
|
|
16
|
+
* @param {string} [opts.closeGapKeep] the signed spec's kept-failures pattern (F28):
|
|
17
|
+
* close-output lines matching it survive the gap bound, so a large suite's
|
|
18
|
+
* `not ok` names reach the worker instead of being elided into "N fail" — also
|
|
19
|
+
* arbiter territory, inexpressible in the drafted config
|
|
9
20
|
* @param {string} opts.workdir run directory (litectx root, gate audit, scope base)
|
|
10
21
|
* @param {number} opts.capRuns shell iteration budget; the config may tighten via loop.maxIterations, never exceed
|
|
11
22
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
12
23
|
* @param {object} opts.provider a bareagent provider — SHELL-owned binding (adaptlearn F8: an unsealed binding is a gate bypass)
|
|
13
|
-
* @param {number} [opts.shellCapUsd=2] the shell's USD cap; config budgetUsd
|
|
24
|
+
* @param {number} [opts.shellCapUsd=2] the shell's USD cap; a config budgetUsd above it REDS at validation (bounds — no silent clamping)
|
|
25
|
+
* @param {number} [opts.closeTimeoutMs] close wall-clock cap, threaded to the shell
|
|
26
|
+
* (shell/operator territory — the workflow config cannot express it)
|
|
14
27
|
* @param {string[]} [opts.jobWriteScope] the job spec's outer write fence (operator law,
|
|
15
28
|
* job-v1) — enforced HERE, the one choke point where a config becomes a Gate:
|
|
16
29
|
* every workflow scope must fit inside it (scope-escape config-red otherwise),
|
|
@@ -22,17 +35,40 @@
|
|
|
22
35
|
* (arbiter-touch / cap-touch revision-reds otherwise; the run continues on the old
|
|
23
36
|
* config). Revisor spend rides the run's own gate handlers — same budget axis as
|
|
24
37
|
* the worker.
|
|
38
|
+
* @param {string} [opts.closeState] the close's CURRENT output on the tree as it stands
|
|
39
|
+
* (the shell's pre-token close check, F13) — shown to the first attempt only, and
|
|
40
|
+
* never framed as an attempt: the worker cannot run the close itself (`run` is a
|
|
41
|
+
* locked verb), so without this it is asked to fix a failure it cannot see
|
|
42
|
+
* @param {'text'|'tools'} [opts.mode] middle mode (2b): 'text' (default) writes the ONE
|
|
43
|
+
* target from the response artifact; 'tools' gives the worker Gate-governed file
|
|
44
|
+
* tools — SPEC-side territory (the step declares it; the config cannot express it)
|
|
45
|
+
* @param {boolean} [opts.layerRoot=false] Layer R (the within-run ratchet, src/root.js):
|
|
46
|
+
* shell-assembled fixation detection over the run's own books, injecting an
|
|
47
|
+
* escalating summary→verbatim note when consecutive attempts rewrite the same
|
|
48
|
+
* files without moving the reds. Inert when the worker is not stuck (RSI §3.3).
|
|
49
|
+
* Defaults OFF (decided 2026-07-21): fixation is extinct on every current job
|
|
50
|
+
* (F41), so ON is unproven — the field read defers to Layer 2. `true` is the
|
|
51
|
+
* ON/experimental arm; never a worker-visible knob
|
|
52
|
+
* @param {string[]} [opts.tools] the spec's tool grant (subset of read|grep|write,
|
|
53
|
+
* job-v1 validated); defaults to the full menu in tool mode
|
|
25
54
|
* @returns {Promise<'green'|'escalated'|'config-red'>}
|
|
26
55
|
*/
|
|
27
|
-
export function interpret(configRaw: object | string, { task, target, close, workdir, capRuns, emit, provider, shellCapUsd, jobWriteScope, revisor }: {
|
|
56
|
+
export function interpret(configRaw: object | string, { task, target, close, workdir, capRuns, emit, provider, shellCapUsd, jobWriteScope, revisor, closeTimeoutMs, mode, tools, closeState, closeExpect, closeJudged, closeGapKeep, layerRoot }: {
|
|
28
57
|
task: string;
|
|
29
|
-
target
|
|
58
|
+
target?: string | undefined;
|
|
30
59
|
close: string[];
|
|
60
|
+
closeExpect?: number | undefined;
|
|
61
|
+
closeJudged?: {
|
|
62
|
+
pattern: string;
|
|
63
|
+
min: number;
|
|
64
|
+
} | undefined;
|
|
65
|
+
closeGapKeep?: string | undefined;
|
|
31
66
|
workdir: string;
|
|
32
67
|
capRuns: number;
|
|
33
68
|
emit: (type: string, data?: object) => object;
|
|
34
69
|
provider: object;
|
|
35
70
|
shellCapUsd?: number | undefined;
|
|
71
|
+
closeTimeoutMs?: number | undefined;
|
|
36
72
|
jobWriteScope?: string[] | undefined;
|
|
37
73
|
revisor?: ((o: {
|
|
38
74
|
config: object;
|
|
@@ -44,14 +80,19 @@ export function interpret(configRaw: object | string, { task, target, close, wor
|
|
|
44
80
|
parseError?: string | null;
|
|
45
81
|
costUsd?: number;
|
|
46
82
|
}>) | undefined;
|
|
83
|
+
closeState?: string | undefined;
|
|
84
|
+
mode?: "text" | "tools" | undefined;
|
|
85
|
+
layerRoot?: boolean | undefined;
|
|
86
|
+
tools?: string[] | undefined;
|
|
47
87
|
}): Promise<"green" | "escalated" | "config-red">;
|
|
48
|
-
/** @typedef {Error & {category?: string}} CategorizedError the failure map's carrier: ralph relays by `category` */
|
|
88
|
+
/** @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) */
|
|
49
89
|
export const STALL_REDS: 2;
|
|
50
90
|
/**
|
|
51
|
-
* the failure map's carrier: ralph relays by `category`
|
|
91
|
+
* 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)
|
|
52
92
|
*/
|
|
53
93
|
export type CategorizedError = Error & {
|
|
54
94
|
category?: string;
|
|
95
|
+
lib?: string;
|
|
55
96
|
};
|
|
56
97
|
/**
|
|
57
98
|
* litectx recall hit — body present only with `{body: true}`
|
package/types/job.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @typedef {{code: string, path: string, detail?: string}} Red */
|
|
1
|
+
/** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red — `verb` rides request-reds as structured data (the ledger keys on it, never on prose) */
|
|
2
2
|
/**
|
|
3
3
|
* Validate an operator-owned job spec (`schema: "job-v1"`). Never throws on
|
|
4
4
|
* JSON text or plain parsed data — the ingest contract; a live object with a
|
|
@@ -44,14 +44,45 @@ export const CLASS_BY_CLOSE: Readonly<{
|
|
|
44
44
|
hitl: readonly string[];
|
|
45
45
|
}>;
|
|
46
46
|
export const GOLD_COMPARE: readonly string[];
|
|
47
|
+
/** step middle modes (2b): text = single-target artifact; tools = Gate-governed file tools */
|
|
48
|
+
export const STEP_MODES: readonly string[];
|
|
49
|
+
/** the tool grant menu (2b interview #1): read/grep/write, plus the two litectx
|
|
50
|
+
* RETRIEVAL verbs (F19) — `run` stays locked-but-listed; a spec requesting it
|
|
51
|
+
* reds, and that red IS the request-red evidence admission waits on (curation
|
|
52
|
+
* doctrine, PRD F2).
|
|
53
|
+
*
|
|
54
|
+
* `recall`/`get` are READ-ONLY BY CONSTRUCTION, not by promise: `recall` returns
|
|
55
|
+
* pointers (path/symbol/line-range, no bodies) and `get` trades ONE pointer for
|
|
56
|
+
* ONE chunk — it refuses any range that is not a chunk boundary, so it cannot be
|
|
57
|
+
* widened into a whole-file read, and it is content-hash gated (a drifted file
|
|
58
|
+
* throws rather than serving a stale body). Admitting them does NOT weaken the
|
|
59
|
+
* `run` lock: the worker still cannot execute anything, so it still cannot run
|
|
60
|
+
* its own close (the arbiter). F19: `shell_read` starts at byte ZERO and cannot
|
|
61
|
+
* seek, so a pointer was INERT — the worker paged 1.37 MB of source through
|
|
62
|
+
* context to reach 8 lines. The ranged read is the pager.
|
|
63
|
+
*
|
|
64
|
+
* `edit` (BA-13, bare-agent 0.29.0) is the anchored exact-once replace — a
|
|
65
|
+
* WRITE-class verb judged by the SAME writeScope fence as `write` (bareguard
|
|
66
|
+
* action type 'edit'). Admitted because whole-file rewrite is measurably
|
|
67
|
+
* unreliable at size (F31: 4 of 5 big-file whole-writes broke the tree) and
|
|
68
|
+
* taxes output tokens ∝ file size. It admits no execution either. */
|
|
69
|
+
export const TOOL_MENU: readonly string[];
|
|
70
|
+
/** locked-but-listed tools: real capabilities deliberately outside the grant
|
|
71
|
+
* menu. Requesting one is a DISTINCT `request-red` (module 4) — the ledger
|
|
72
|
+
* counts admission demand, and a generic invalid-value would bury it as a typo. */
|
|
73
|
+
export const LOCKED_TOOLS: readonly string[];
|
|
47
74
|
export const CADENCE_UNITS: readonly string[];
|
|
48
75
|
export const PROVIDERS: readonly string[];
|
|
49
76
|
/** V3 environment label: declared keys only — every field is a lineage-key
|
|
50
77
|
* candidate at N3. `provider` is part of the key by definition (top-level,
|
|
51
78
|
* not duplicated here). */
|
|
52
79
|
export const CONDITION_KEYS: readonly string[];
|
|
80
|
+
/**
|
|
81
|
+
* — `verb` rides request-reds as structured data (the ledger keys on it, never on prose)
|
|
82
|
+
*/
|
|
53
83
|
export type Red = {
|
|
54
84
|
code: string;
|
|
55
85
|
path: string;
|
|
56
86
|
detail?: string;
|
|
87
|
+
verb?: string;
|
|
57
88
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify one spine's events into incident occurrences (the design table in
|
|
3
|
+
* bareloop vocabulary). Deliberate exclusions — bare cap-halt (a budget story),
|
|
4
|
+
* close-verdict reds and artifact-red (worker stories, the §5b line), gate-red
|
|
5
|
+
* (governance working as intended), pr-red (operator environment, not a suite
|
|
6
|
+
* lib), smoke-red escalations (already counted via primitive-smoke), hitl-close
|
|
7
|
+
* and close-unsupported (by design) — classify to nothing.
|
|
8
|
+
* @param {any[]} events one spine's parsed events, in seq order
|
|
9
|
+
* @param {{spine?: string}} [opts] spine id stamped into samples
|
|
10
|
+
* @returns {Occurrence[]}
|
|
11
|
+
*/
|
|
12
|
+
export function classifyIncidents(events: any[], { spine }?: {
|
|
13
|
+
spine?: string;
|
|
14
|
+
}): Occurrence[];
|
|
15
|
+
/**
|
|
16
|
+
* Fold ledger rows to current state — latest cumulative count per key, latest
|
|
17
|
+
* human-appended status riding on top (default 'open'). A status row for a key
|
|
18
|
+
* with no incident row still folds (occurrences 0): a human note must never
|
|
19
|
+
* silently vanish.
|
|
20
|
+
* @param {any[]} rows parsed ledger rows, in seq order
|
|
21
|
+
* @returns {Record<string, FoldEntry>}
|
|
22
|
+
*/
|
|
23
|
+
export function foldLedger(rows: any[]): Record<string, FoldEntry>;
|
|
24
|
+
/**
|
|
25
|
+
* Compute the append-only delta rows: group occurrences by key; a row is due
|
|
26
|
+
* only when the key is new or its corpus total grew past the folded count.
|
|
27
|
+
* detail is first-seen (the fold's wins); samples are the corpus's first few.
|
|
28
|
+
* @param {Record<string, FoldEntry>} fold current ledger state
|
|
29
|
+
* @param {Occurrence[]} occurrences the FULL corpus's occurrences
|
|
30
|
+
* @returns {object[]} lib-incident rows, unstamped (no seq/ts)
|
|
31
|
+
*/
|
|
32
|
+
export function ledgerDeltas(fold: Record<string, FoldEntry>, occurrences: Occurrence[]): object[];
|
|
33
|
+
/**
|
|
34
|
+
* The collector: read the ledger, classify the FULL spine corpus, append the
|
|
35
|
+
* deltas (spine conventions: type first, ts stamped last, seq continuing
|
|
36
|
+
* monotonically across appends). Idempotent over the same corpus; the CLI
|
|
37
|
+
* wrapping this lands at N5, the panel reads the same file at N6.
|
|
38
|
+
* @param {{ledgerFile: string, spineFiles: string[]}} o spineFiles: the corpus
|
|
39
|
+
* (pass ALL of it each time — counts are totals, not increments)
|
|
40
|
+
* @returns {{appended: object[], fold: Record<string, FoldEntry>}}
|
|
41
|
+
*/
|
|
42
|
+
export function updateLedger({ ledgerFile, spineFiles }: {
|
|
43
|
+
ledgerFile: string;
|
|
44
|
+
spineFiles: string[];
|
|
45
|
+
}): {
|
|
46
|
+
appended: object[];
|
|
47
|
+
fold: Record<string, FoldEntry>;
|
|
48
|
+
};
|
|
49
|
+
/** severity order, worst-first (the fold's display order; frequency ranks within) */
|
|
50
|
+
export const LEDGER_CLASSES: readonly string[];
|
|
51
|
+
export type Sample = {
|
|
52
|
+
spine: string;
|
|
53
|
+
seq: number;
|
|
54
|
+
};
|
|
55
|
+
export type Occurrence = {
|
|
56
|
+
key: string;
|
|
57
|
+
class: string;
|
|
58
|
+
lib: string;
|
|
59
|
+
verb: string;
|
|
60
|
+
sig: string;
|
|
61
|
+
detail: string;
|
|
62
|
+
sample: Sample;
|
|
63
|
+
};
|
|
64
|
+
export type FoldEntry = {
|
|
65
|
+
key: string;
|
|
66
|
+
lib: string;
|
|
67
|
+
verb: string;
|
|
68
|
+
class: string;
|
|
69
|
+
sig: string;
|
|
70
|
+
detail: string;
|
|
71
|
+
occurrences: number;
|
|
72
|
+
samples: Sample[];
|
|
73
|
+
suggestedAsk: string;
|
|
74
|
+
status: string;
|
|
75
|
+
ref?: string;
|
|
76
|
+
};
|
package/types/ralph.d.ts
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Run a close command; its exit code is the verdict (hard green, PRD §4).
|
|
3
|
-
*
|
|
3
|
+
* `expect` → satisfied; any other exit → needs_revision (gap fed back).
|
|
4
|
+
*
|
|
5
|
+
* THE FORBIDDEN ZONE (PRD v1.11, F17; adaptlearn F25/V10). The two clean bands
|
|
6
|
+
* are green (exit == expect, judgment rendered) and red (exit != expect,
|
|
7
|
+
* judgment rendered). Every other outcome rendered NO JUDGMENT and is therefore
|
|
8
|
+
* NOT A VERDICT — it gets its own name and escalates; coercing one into a
|
|
9
|
+
* verdict IS the instrument fault (law #8 generalized):
|
|
10
|
+
* - `failed` — the close cannot RUN (spawn error). → broken-close
|
|
11
|
+
* - `timed-out` — it ran and never finished judging. → close-timeout
|
|
12
|
+
* - `killed` — it died by signal (status null, no spawn error). → close-killed
|
|
13
|
+
* - `crashed` — it ran, exited, and judged nothing. → close-crashed
|
|
14
|
+
* `timed-out` is split OUT of `failed` deliberately: "raise the timeout" and
|
|
15
|
+
* "fix the argv" are different human decisions, so pooling them erases the
|
|
16
|
+
* decision information the escalation exists to carry.
|
|
17
|
+
*
|
|
18
|
+
* `judged` is the judgment-rendered signal, and it is the hard one. Exit code
|
|
19
|
+
* ALONE cannot separate a crash-at-load from an honest red — they are
|
|
20
|
+
* byte-identical at this seam, and against `node --test` so are the test counts
|
|
21
|
+
* (node synthesizes ONE failing test for the file that crashed, so a crash
|
|
22
|
+
* reports `tests 1 / fail 1`, exactly like a one-assertion failure). So the
|
|
23
|
+
* signal cannot be "zero judged"; it must be a FLOOR against a declared
|
|
24
|
+
* baseline: litectx runs ~390 tests, and a run claiming it judged 1 did not
|
|
25
|
+
* judge. `judged.pattern` extracts one integer from the close's own (redacted)
|
|
26
|
+
* output; below `judged.min` the close crashed, WHATEVER its exit code says —
|
|
27
|
+
* and that check runs on the GREEN side too: a close that exits 0 having judged
|
|
28
|
+
* nothing is a confident fake green, the only real failure there is (law #8).
|
|
29
|
+
* `judged` is OPTIONAL — a linter or a hitl close may have no count to give —
|
|
30
|
+
* and its absence stamps `unaudited: true` so the blind spot is NAMED on the
|
|
31
|
+
* record rather than passed off as a trustworthy exit code.
|
|
4
32
|
*
|
|
5
33
|
* `redact` scrubs the close's own output at the SOURCE — before the gap becomes
|
|
6
34
|
* a spine event or a worker prompt — so a secret a checked command echoes
|
|
@@ -10,24 +38,49 @@
|
|
|
10
38
|
* primitive (bareguard's, wired by the interpreter), never an emergent
|
|
11
39
|
* component — so V4 holds. The shell's canonical emission IS the redacted text;
|
|
12
40
|
* a benign gap is returned byte-identical (default: identity).
|
|
41
|
+
* `cwd` is where the close RUNS, and it is load-bearing (F8): a close is a
|
|
42
|
+
* command in a repository — `npm test`, `make check` — and every one of them is
|
|
43
|
+
* cwd-relative. Run it anywhere but the workdir and the arbiter judges the wrong
|
|
44
|
+
* tree: exit-code-is-truth silently becomes exit-code-of-some-other-repo-is-truth.
|
|
45
|
+
* The default (the runner's own cwd) exists only for callers with an absolute
|
|
46
|
+
* close; the runner and the interpreter always pass the workdir.
|
|
47
|
+
*
|
|
48
|
+
* `gapKeep` (F28) is a regex SOURCE whose matching lines are PRESERVED through
|
|
49
|
+
* the gap bound in addition to the head/tail sample: a large TAP suite prints
|
|
50
|
+
* its `not ok` lines in the middle of the stream, exactly where `boundGap`
|
|
51
|
+
* elides — so the failing-test NAMES (the causal input the worker navigates on)
|
|
52
|
+
* were deleted in transit and the worker was told "5 fail" and never WHICH. It
|
|
53
|
+
* is arbiter territory like the rest here: the drafted config cannot express it.
|
|
54
|
+
*
|
|
13
55
|
* @param {string[]} close argv, e.g. ['node', '--test', 'close/']
|
|
14
56
|
* @param {(s: string) => string} [redact] source scrubber; identity by default
|
|
57
|
+
* @param {{ timeoutMs?: number, cwd?: string, expect?: number, judged?: {pattern: string, min: number}, gapKeep?: string }} [opts]
|
|
58
|
+
* close wall-clock cap (operator-set via the runner, never the config's — the
|
|
59
|
+
* agent must not author its arbiter's clock), the directory the close runs in
|
|
60
|
+
* (the workdir — the tree it is judging), the exit code the SIGNED spec calls
|
|
61
|
+
* success, the judgment-rendered signal, and the kept-failures pattern. All
|
|
62
|
+
* five are arbiter territory: the drafted workflow config cannot express any.
|
|
63
|
+
* @returns {{verdict: 'satisfied'|'needs_revision'|'failed'|'timed-out'|'killed'|'crashed',
|
|
64
|
+
* gap?: string, exitCode?: number, signal?: string, detail?: string,
|
|
65
|
+
* judgedCount?: number|null, unaudited?: boolean}}
|
|
15
66
|
*/
|
|
16
|
-
export function runClose(close: string[], redact?: (s: string) => string
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
detail?:
|
|
67
|
+
export function runClose(close: string[], redact?: (s: string) => string, { timeoutMs, cwd, expect, judged, gapKeep }?: {
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
cwd?: string;
|
|
70
|
+
expect?: number;
|
|
71
|
+
judged?: {
|
|
72
|
+
pattern: string;
|
|
73
|
+
min: number;
|
|
74
|
+
};
|
|
75
|
+
gapKeep?: string;
|
|
76
|
+
}): {
|
|
77
|
+
verdict: "satisfied" | "needs_revision" | "failed" | "timed-out" | "killed" | "crashed";
|
|
78
|
+
gap?: string;
|
|
79
|
+
exitCode?: number;
|
|
80
|
+
signal?: string;
|
|
81
|
+
detail?: string;
|
|
82
|
+
judgedCount?: number | null;
|
|
83
|
+
unaudited?: boolean;
|
|
31
84
|
};
|
|
32
85
|
/**
|
|
33
86
|
* The loop: `while close-red and under-cap: run the middle`. Stops at first
|
|
@@ -50,12 +103,53 @@ export function runClose(close: string[], redact?: (s: string) => string): {
|
|
|
50
103
|
* @param {(type: string, data?: object) => object} opts.emit a spine emitter
|
|
51
104
|
* @param {(s: string) => string} [opts.redact] source scrubber for close output
|
|
52
105
|
* (secrets never enter the spine); injected so the shell stays stdlib-only
|
|
106
|
+
* @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell/operator
|
|
107
|
+
* territory — the workflow config cannot express it)
|
|
108
|
+
* @param {string} [opts.cwd] the directory every close runs in — the tree it is
|
|
109
|
+
* judging (F8: a cwd-relative close run elsewhere judges the wrong repository)
|
|
110
|
+
* @param {number} [opts.expect] the exit code the SIGNED spec calls success
|
|
111
|
+
* @param {{pattern: string, min: number}} [opts.judged] the judgment-rendered signal
|
|
112
|
+
* @param {string} [opts.gapKeep] kept-failures pattern (F28): matching close-output
|
|
113
|
+
* lines survive the gap bound so a large suite's `not ok` names reach the worker
|
|
114
|
+
* @param {() => string[]} [opts.workerWrites] the crash-attribution instrument (F32):
|
|
115
|
+
* answers "which files has the WORKER written so far this run?" — the runner wires
|
|
116
|
+
* it to the gate audit's allow-decision writes. Injected like `redact` so the shell
|
|
117
|
+
* stays stdlib-only and dumb; consulted ONLY at a crashed verdict. With no seam, or
|
|
118
|
+
* with zero writes, a crash stays what it always was: an instrument stop.
|
|
53
119
|
* @returns {Promise<'green'|'escalated'>}
|
|
54
120
|
*/
|
|
55
|
-
export function ralph({ middle, close, capRuns, emit, redact }: {
|
|
121
|
+
export function ralph({ middle, close, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
|
|
56
122
|
middle: (iteration: number, gap?: string) => void | Promise<void>;
|
|
57
123
|
close: string[];
|
|
58
124
|
capRuns: number;
|
|
59
125
|
emit: (type: string, data?: object) => object;
|
|
60
126
|
redact?: ((s: string) => string) | undefined;
|
|
127
|
+
closeTimeoutMs?: number | undefined;
|
|
128
|
+
cwd?: string | undefined;
|
|
129
|
+
expect?: number | undefined;
|
|
130
|
+
judged?: {
|
|
131
|
+
pattern: string;
|
|
132
|
+
min: number;
|
|
133
|
+
} | undefined;
|
|
134
|
+
gapKeep?: string | undefined;
|
|
135
|
+
workerWrites?: (() => string[]) | undefined;
|
|
61
136
|
}): Promise<"green" | "escalated">;
|
|
137
|
+
export const GAP_KEEP_TRIM_MARKER: "more elided by the";
|
|
138
|
+
/**
|
|
139
|
+
* The forbidden zone, named (PRD v1.11). Each row is an outcome in which the
|
|
140
|
+
* close rendered NO JUDGMENT — so it is not a verdict, it never feeds a gap
|
|
141
|
+
* back, and it is never retried. They are kept SEPARATE on purpose: pooling
|
|
142
|
+
* them would erase the decision information the escalation exists to carry —
|
|
143
|
+
* "raise the timeout", "fix the argv", and "re-run, it was OOM-killed" are
|
|
144
|
+
* three different human answers.
|
|
145
|
+
* Exported because the RUNNER's close-first precheck runs the same arbiter
|
|
146
|
+
* before any tokens: two maps would be two instruments, and two instruments
|
|
147
|
+
* disagreeing about the same outcome is the fault this whole addendum exists
|
|
148
|
+
* to close.
|
|
149
|
+
* @type {Record<string, {category: string, decision: string, options: string[]}>}
|
|
150
|
+
*/
|
|
151
|
+
export const CLOSE_FAULTS: Record<string, {
|
|
152
|
+
category: string;
|
|
153
|
+
decision: string;
|
|
154
|
+
options: string[];
|
|
155
|
+
}>;
|
package/types/root.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root collector. One per run; state dies with the run.
|
|
3
|
+
*
|
|
4
|
+
* Call order (wired by the interpreter's middle):
|
|
5
|
+
* observe({iteration: 1, writes}) → null (nothing to compare)
|
|
6
|
+
* stageWrite(path, content) → stage, before the tool runs
|
|
7
|
+
* settleWrite(landed) | discardWrite() → the outcome, after it ran
|
|
8
|
+
* noteWrite(path, content) → stage+settle, text mode (which
|
|
9
|
+
* writes before it reports)
|
|
10
|
+
* observe({iteration: N, gap, writes}) → null | {stage, note, event}
|
|
11
|
+
* `writes` is the CUMULATIVE allow-decision write/edit path list from the gate
|
|
12
|
+
* audit (the F32 instrument); the per-attempt set is the delta between calls —
|
|
13
|
+
* so a gate-DENIED write is never counted (the tool never ran at all).
|
|
14
|
+
*
|
|
15
|
+
* Finding 7 — the two axes, deliberately NOT merged. An allow-decision records
|
|
16
|
+
* what the worker REACHED FOR, which is the right instrument for "is it
|
|
17
|
+
* repeating itself"; whether those bytes reached the file is a different
|
|
18
|
+
* question, and only the note (a claim about file contents) needs it. Collapsing
|
|
19
|
+
* them either way is a bug: settling the note on the allow makes it lie, and
|
|
20
|
+
* running the detector off the file makes it blind to a worker re-firing an
|
|
21
|
+
* identical edit whose anchor never matches — measured null on every attempt.
|
|
22
|
+
*
|
|
23
|
+
* @param {{gapKeep?: string, redact?: (s: string) => string, writesInformative?: boolean}} [opts]
|
|
24
|
+
* the signed spec's kept-failures pattern (the red-set source; without it the
|
|
25
|
+
* detector degrades to write-overlap alone and the event says so), the shell's
|
|
26
|
+
* secret scrubber (verbatim content is scrubbed before it rides a prompt), and
|
|
27
|
+
* `writesInformative` (Finding 3, default true): whether the per-attempt
|
|
28
|
+
* write-set carries information. In TOOL mode the worker CHOOSES which files to
|
|
29
|
+
* write, so re-writing the same file is a real repetition signal; in TEXT mode
|
|
30
|
+
* the one target is rewritten every attempt by construction, so write-overlap
|
|
31
|
+
* is constant-true and carries none — with no red-set to compare, the detector
|
|
32
|
+
* cannot tell repetition from progress and must stay inert (it would otherwise
|
|
33
|
+
* steer a progressing worker off course, violating "inert when not stuck").
|
|
34
|
+
*/
|
|
35
|
+
export function createRoot({ gapKeep, redact, writesInformative }?: {
|
|
36
|
+
gapKeep?: string;
|
|
37
|
+
redact?: (s: string) => string;
|
|
38
|
+
writesInformative?: boolean;
|
|
39
|
+
}): {
|
|
40
|
+
stageWrite: (path: string, content: string) => void;
|
|
41
|
+
/**
|
|
42
|
+
* The staged write RAN — record what it actually did (Finding 7). Retained
|
|
43
|
+
* either way: an attempted-but-unapplied edit is still the worker reaching
|
|
44
|
+
* for the same file, which is what the detector measures, and dropping it
|
|
45
|
+
* here would blind the ratchet to the purest fixation there is (a worker
|
|
46
|
+
* re-firing an identical edit whose anchor never matches — measured null on
|
|
47
|
+
* every attempt under a tree-diff detector, 2026-07-20). Only the NOTE's
|
|
48
|
+
* wording turns on `landed`. Last write per path wins (the tree's final
|
|
49
|
+
* state for that attempt is what failed the close).
|
|
50
|
+
* @param {boolean} landed whether the content is now in the file
|
|
51
|
+
*/
|
|
52
|
+
settleWrite(landed: boolean): void;
|
|
53
|
+
/**
|
|
54
|
+
* The staged write was DENIED or HALTED: it is not in the file, so it must
|
|
55
|
+
* never be surfaced as "your own change that landed". Drops the stage and
|
|
56
|
+
* leaves whatever DID land for that path untouched.
|
|
57
|
+
*/
|
|
58
|
+
discardWrite(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Stage and commit in one call — the post-allow write site (text mode
|
|
61
|
+
* gate-checks and writes before it calls this, so the verdict is already in).
|
|
62
|
+
* @param {string} path @param {string} content
|
|
63
|
+
*/
|
|
64
|
+
noteWrite(path: string, content: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Called at the START of attempt `iteration`, with the PREVIOUS attempt's
|
|
67
|
+
* gap. Finalizes that attempt from the books, runs the detector, and
|
|
68
|
+
* returns the injection for the attempt about to run — or null (inert).
|
|
69
|
+
* @param {{iteration: number, gap?: string, writes: string[]}} o
|
|
70
|
+
* @returns {null | {stage: 'summary'|'verbatim', note: string, event: object}}
|
|
71
|
+
*/
|
|
72
|
+
observe({ iteration, gap, writes }: {
|
|
73
|
+
iteration: number;
|
|
74
|
+
gap?: string;
|
|
75
|
+
writes: string[];
|
|
76
|
+
}): null | {
|
|
77
|
+
stage: "summary" | "verbatim";
|
|
78
|
+
note: string;
|
|
79
|
+
event: object;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
export function normalizeRedLine(line: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* A teed write: the content the worker emitted, plus whether it actually reached
|
|
85
|
+
* the file. `landed` is the OUTCOME axis (Finding 7) — the gate's allow is an
|
|
86
|
+
* intent record written before the tool runs, and a shell_edit whose anchor
|
|
87
|
+
* misses returns a refusal RESULT with the file untouched (bare-agent
|
|
88
|
+
* tools/shell.js:190). A note that says "these are your changes" is a claim
|
|
89
|
+
* about the file's contents and must be true, so it reads this flag, never the
|
|
90
|
+
* audit. The DETECTOR deliberately does not: see `observe`.
|
|
91
|
+
*/
|
|
92
|
+
export type TeeEntry = {
|
|
93
|
+
content: string;
|
|
94
|
+
trimmed: boolean;
|
|
95
|
+
landed: boolean;
|
|
96
|
+
};
|
package/types/run.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run an approved job end to end: approval gate → primitive smoke → sequential
|
|
3
|
+
* per-step interpret loops under the one ledger → the hitl step opens the draft
|
|
4
|
+
* PR (deterministic git, never model tools) and becomes the decision-ready
|
|
5
|
+
* escalation. Every predicate step runs its close FIRST (close-first skip,
|
|
6
|
+
* resume model): already-green skips the step for zero tokens as a distinct
|
|
7
|
+
* spine record, so a stopped run reruns from where it died — the workdir plus
|
|
8
|
+
* the closes are the checkpoint. The sealed config draft (one shot + one
|
|
9
|
+
* redraft) is deferred to the first step that actually needs a worker; a clean
|
|
10
|
+
* rerun pays zero provider calls.
|
|
11
|
+
*
|
|
12
|
+
* N2 bounds (honest, documented): text-mode steps write a single `target`
|
|
13
|
+
* artifact; tool-mode steps (2b) work multi-file through the gated shell tools,
|
|
14
|
+
* granted per step by the SPEC (mode/tools — the drafted config cannot express
|
|
15
|
+
* either); `gold`/`rubric` closes refuse `close-unsupported` (execution lands
|
|
16
|
+
* with the verdict classes, N4); the pricing-red halt lands at the step
|
|
17
|
+
* boundary — the Gate still caps within a step, but an unpriced result cannot
|
|
18
|
+
* be summed, so the run stops rather than counting it $0 (F6).
|
|
19
|
+
*
|
|
20
|
+
* EVERY terminal `job-end` states the money: `spentUsd` (the sum of PRICED
|
|
21
|
+
* rounds only — never an estimate; a real 0 on the pre-token reds) plus
|
|
22
|
+
* `spendComplete`, false when any round came back unpriced and the figure is
|
|
23
|
+
* therefore a FLOOR, not the total. Both are always present, so a consumer
|
|
24
|
+
* never branches on field presence nor launders a blank into $0.
|
|
25
|
+
*
|
|
26
|
+
* @param {object|string} rawSpec the job spec (job-v1), text or parsed
|
|
27
|
+
* @param {object} opts
|
|
28
|
+
* @param {unknown} opts.approvals `{ specHash, signer, ts }` records (from OUTSIDE the spec)
|
|
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
|
+
* @param {any} opts.provider shell-owned LLM binding (never the config's)
|
|
32
|
+
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
33
|
+
* @param {number} [opts.capRuns] shell-owned per-step iteration cap
|
|
34
|
+
* @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
|
|
35
|
+
* @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
|
|
36
|
+
* @param {ExecCmd} [opts.execCmd] process runner for the hitl PR mechanics
|
|
37
|
+
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
38
|
+
* territory, threaded to every predicate step's interpreter. Defaults OFF
|
|
39
|
+
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
40
|
+
* ON has never won its own A/B — the acceptance read defers to Layer 2 (or
|
|
41
|
+
* a manufactured-fixation probe). `true` is the ON/experimental arm; the
|
|
42
|
+
* default is the OFF arm. (Shell-owned seam, same doctrine as the provider
|
|
43
|
+
* binding.)
|
|
44
|
+
* @returns {Promise<string>} outcome: 'green' | 'escalated' | 'unapproved-spec' |
|
|
45
|
+
* 'job-red' | 'smoke-red' | 'config-red' | 'pricing-red' | 'provider-red' |
|
|
46
|
+
* 'cap-halt' | 'close-unsupported' | `step-red:<id>`
|
|
47
|
+
*/
|
|
48
|
+
export function runJob(rawSpec: object | string, { approvals, workdir, target, provider, emit, capRuns, shellCapUsd, closeTimeoutMs, execCmd, layerRoot }: {
|
|
49
|
+
approvals: unknown;
|
|
50
|
+
workdir: string;
|
|
51
|
+
target?: string | undefined;
|
|
52
|
+
provider: any;
|
|
53
|
+
emit: (type: string, data?: object) => object;
|
|
54
|
+
capRuns?: number | undefined;
|
|
55
|
+
shellCapUsd?: number | undefined;
|
|
56
|
+
closeTimeoutMs?: number | undefined;
|
|
57
|
+
execCmd?: ExecCmd | undefined;
|
|
58
|
+
layerRoot?: boolean | undefined;
|
|
59
|
+
}): Promise<string>;
|
|
60
|
+
export type Red = {
|
|
61
|
+
code: string;
|
|
62
|
+
path: string;
|
|
63
|
+
detail?: string;
|
|
64
|
+
};
|
|
65
|
+
export type ExecCmd = (argv: string[], opts?: {
|
|
66
|
+
cwd?: string;
|
|
67
|
+
}) => {
|
|
68
|
+
status: number | null;
|
|
69
|
+
stdout: string;
|
|
70
|
+
stderr: string;
|
|
71
|
+
};
|