bareloop 0.3.0 → 0.5.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.
@@ -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";
@@ -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
  *
@@ -42,11 +109,18 @@
42
109
  * @param {'text'|'tools'} [opts.mode] middle mode (2b): 'text' (default) writes the ONE
43
110
  * target from the response artifact; 'tools' gives the worker Gate-governed file
44
111
  * tools — SPEC-side territory (the step declares it; the config cannot express it)
112
+ * @param {boolean} [opts.layerRoot=false] Layer R (the within-run ratchet, src/root.js):
113
+ * shell-assembled fixation detection over the run's own books, injecting an
114
+ * escalating summary→verbatim note when consecutive attempts rewrite the same
115
+ * files without moving the reds. Inert when the worker is not stuck (RSI §3.3).
116
+ * Defaults OFF (decided 2026-07-21): fixation is extinct on every current job
117
+ * (F41), so ON is unproven — the field read defers to Layer 2. `true` is the
118
+ * ON/experimental arm; never a worker-visible knob
45
119
  * @param {string[]} [opts.tools] the spec's tool grant (subset of read|grep|write,
46
120
  * job-v1 validated); defaults to the full menu in tool mode
47
121
  * @returns {Promise<'green'|'escalated'|'config-red'>}
48
122
  */
49
- export function interpret(configRaw: object | string, { task, target, close, workdir, capRuns, emit, provider, shellCapUsd, jobWriteScope, revisor, closeTimeoutMs, mode, tools, closeState, closeExpect, closeJudged, closeGapKeep }: {
123
+ export function interpret(configRaw: object | string, { task, target, close, workdir, capRuns, emit, provider, shellCapUsd, jobWriteScope, revisor, closeTimeoutMs, mode, tools, closeState, closeExpect, closeJudged, closeGapKeep, layerRoot }: {
50
124
  task: string;
51
125
  target?: string | undefined;
52
126
  close: string[];
@@ -75,10 +149,42 @@ export function interpret(configRaw: object | string, { task, target, close, wor
75
149
  }>) | undefined;
76
150
  closeState?: string | undefined;
77
151
  mode?: "text" | "tools" | undefined;
152
+ layerRoot?: boolean | undefined;
78
153
  tools?: string[] | undefined;
79
154
  }): Promise<"green" | "escalated" | "config-red">;
80
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) */
81
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
+ };
82
188
  /**
83
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)
84
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,
@@ -0,0 +1,41 @@
1
+ /** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red */
2
+ /**
3
+ * Validate an agent-authored plan (`schema: "plan-v1"`) against the SIGNED
4
+ * job spec. Never throws on JSON text or plain parsed data; every failure is
5
+ * a named red. Returns the parsed plan on ok (single parse), null on any red.
6
+ * @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
7
+ * @param {{ job?: any, maxStepRounds?: number }} [opts] `job`: the
8
+ * validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
9
+ * menu all come from it — a missing or non-plan-shape job fails CLOSED);
10
+ * `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
11
+ * tool-mode TURNS_PER_ATTEMPT) — a step may tighten it, never exceed it.
12
+ * @returns {{ ok: boolean, reds: Red[], plan: object|null }}
13
+ */
14
+ export function validatePlan(input: object | string, { job, maxStepRounds }?: {
15
+ job?: any;
16
+ maxStepRounds?: number;
17
+ }): {
18
+ ok: boolean;
19
+ reds: Red[];
20
+ plan: object | null;
21
+ };
22
+ /** the closed exit menu (PRD v1.12 §3 + decision 1's `check-passes`): the
23
+ * shell evaluates every form with its own fixed code, never a command — the
24
+ * same both-directions inexpressibility guard as the two-validator split */
25
+ export const EXIT_TYPES: readonly string[];
26
+ /** decision 8: AND-only conjunction, bounded — no OR (a weak arm would
27
+ * launder the exit), no NOT */
28
+ export const MAX_EXITS_PER_STEP: 2;
29
+ /** plan size bound — per-step rounds bound the spend, but an unbounded step
30
+ * COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
31
+ export const MAX_PLAN_STEPS: 8;
32
+ /** the write-class verbs (BA-13: `edit` is judged by the same fence as
33
+ * `write`) — a step granting one is a WRITE step for the F17 pairing rule
34
+ * and the v1.18 target requirement */
35
+ export const WRITE_VERBS: readonly string[];
36
+ export type Red = {
37
+ code: string;
38
+ path: string;
39
+ detail?: string;
40
+ verb?: string;
41
+ };
@@ -0,0 +1,49 @@
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
+ * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
27
+ * 'check-red' | 'close-red' | 'close-unsupported' | 'pricing-red' | 'cap-halt' |
28
+ * 'provider-red' | 'interpreter-red' | `step-red:<id>`
29
+ */
30
+ export function runPlan(job: any, { workdir, provider, nativeProvider, emit, remainingUsd, isUnpriced, capRuns, closeTimeoutMs, maxStepRounds }: {
31
+ workdir: string;
32
+ provider: any;
33
+ nativeProvider?: ((o: {
34
+ policy: Function;
35
+ onTurn?: Function;
36
+ maxTurns: number;
37
+ hasTools: boolean;
38
+ }) => any) | undefined;
39
+ emit: (type: string, data?: object) => object;
40
+ remainingUsd: () => number;
41
+ isUnpriced?: (() => boolean) | undefined;
42
+ capRuns?: number | undefined;
43
+ closeTimeoutMs?: number | undefined;
44
+ maxStepRounds?: number | undefined;
45
+ }): Promise<string>;
46
+ export type CategorizedError = Error & {
47
+ category?: string;
48
+ lib?: string;
49
+ };
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: string[];
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;
@@ -134,6 +151,7 @@ export function ralph({ middle, close, capRuns, emit, redact, closeTimeoutMs, cw
134
151
  gapKeep?: string | undefined;
135
152
  workerWrites?: (() => string[]) | undefined;
136
153
  }): Promise<"green" | "escalated">;
154
+ export const GAP_KEEP_TRIM_MARKER: "more elided by the";
137
155
  /**
138
156
  * The forbidden zone, named (PRD v1.11). Each row is an outcome in which the
139
157
  * close rendered NO JUDGMENT — so it is not a verdict, it never feeds a gap
@@ -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 CHANGED
@@ -17,32 +17,55 @@
17
17
  * boundary — the Gate still caps within a step, but an unpriced result cannot
18
18
  * be summed, so the run stops rather than counting it $0 (F6).
19
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
+ *
20
26
  * @param {object|string} rawSpec the job spec (job-v1), text or parsed
21
27
  * @param {object} opts
22
28
  * @param {unknown} opts.approvals `{ specHash, signer, ts }` records (from OUTSIDE the spec)
23
29
  * @param {string} opts.workdir the run directory (the fence's root)
24
30
  * @param {string} [opts.target] the step artifact path (text-mode steps; unused by tool-mode steps)
25
- * @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
26
36
  * @param {(type: string, data?: object) => object} opts.emit spine emitter
27
37
  * @param {number} [opts.capRuns] shell-owned per-step iteration cap
28
38
  * @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
29
39
  * @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
30
40
  * @param {ExecCmd} [opts.execCmd] process runner for the hitl PR mechanics
31
- * (shell-owned seam, same doctrine as the provider binding)
41
+ * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
42
+ * territory, threaded to every predicate step's interpreter. Defaults OFF
43
+ * (decided 2026-07-21): fixation is extinct on every current job (F41), so
44
+ * ON has never won its own A/B — the acceptance read defers to Layer 2 (or
45
+ * a manufactured-fixation probe). `true` is the ON/experimental arm; the
46
+ * default is the OFF arm. (Shell-owned seam, same doctrine as the provider
47
+ * binding.)
32
48
  * @returns {Promise<string>} outcome: 'green' | 'escalated' | 'unapproved-spec' |
33
49
  * 'job-red' | 'smoke-red' | 'config-red' | 'pricing-red' | 'provider-red' |
34
50
  * 'cap-halt' | 'close-unsupported' | `step-red:<id>`
35
51
  */
36
- export function runJob(rawSpec: object | string, { approvals, workdir, target, provider, emit, capRuns, shellCapUsd, closeTimeoutMs, execCmd }: {
52
+ export function runJob(rawSpec: object | string, { approvals, workdir, target, provider, nativeProvider, emit, capRuns, shellCapUsd, closeTimeoutMs, execCmd, layerRoot }: {
37
53
  approvals: unknown;
38
54
  workdir: string;
39
55
  target?: string | undefined;
40
56
  provider: any;
57
+ nativeProvider?: ((o: {
58
+ policy: Function;
59
+ onTurn?: Function;
60
+ maxTurns: number;
61
+ hasTools: boolean;
62
+ }) => any) | undefined;
41
63
  emit: (type: string, data?: object) => object;
42
64
  capRuns?: number | undefined;
43
65
  shellCapUsd?: number | undefined;
44
66
  closeTimeoutMs?: number | undefined;
45
67
  execCmd?: ExecCmd | undefined;
68
+ layerRoot?: boolean | undefined;
46
69
  }): Promise<string>;
47
70
  export type Red = {
48
71
  code: string;