bare-agent 0.22.0 → 0.23.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/README.md CHANGED
@@ -94,7 +94,7 @@ Every piece works alone — take what you need, ignore the rest. Two axes: **Act
94
94
 
95
95
  ### Recurse — break a hard task into a tree *(the RLM primitive)*
96
96
 
97
- `recurse(task, ctx, opts)` does **decompose → fan-out → verify → synthesize** in one call — Recursive Language Models as a single import, composed *around* the Loop (never a new engine). The default is **model-driven**: the worker is handed a `spawn_child` tool and decides whether to split, bounded by depth + bareguard (no second guard layer). Forced fan-out (`count` / `mode:'fanout'`) and data-driven width (`mode:'partition'`, measured from a corpus) are opt-in. Give workers a stance with `opts.persona` (prepended to every worker, carries down the tree, deliberately kept out of the isolated verifier). The headline guarantee: **aggregation is code, never a model-stated number**, and a dead worker or exhausted guard returns an honest `{ incomplete, missingSlices }` — never a faked pass.
97
+ `recurse(task, ctx, opts)` does **decompose → fan-out → verify → synthesize** in one call — Recursive Language Models as a single import, composed *around* the Loop (never a new engine). The default is **model-driven**: the worker is handed a `spawn_child` tool and decides whether to split, bounded by depth + bareguard (no second guard layer). Forced fan-out (`count` / `mode:'fanout'`) and data-driven width (`mode:'partition'`, measured from a corpus) are opt-in. Give workers a stance with `opts.persona` (prepended to every worker, carries down the tree, deliberately kept out of the isolated verifier), and tell them *where they are* with `opts.context` (a read-only paths/cwd blob threaded to every worker so a sliced child can locate its artifact — facts, not a stance). For a leaf that should self-correct, pass `opts.refineLeaf` (opt-in): a definite leaf becomes a bounded generate→sense→regenerate loop driven by *your* deterministic sensor (test/compile/lint), feeding the gap back with escalating temperature. The headline guarantee: **aggregation is code, never a model-stated number**, and a dead worker or exhausted guard returns an honest `{ incomplete, missingSlices }` — never a faked pass.
98
98
 
99
99
  Over a corpus, context reaches a worker as a **handle routed by question shape** (`opts.retrieval`):
100
100
 
@@ -1,7 +1,7 @@
1
1
  # bareagent — Integration Guide
2
2
 
3
3
  > For AI assistants and developers wiring bareagent into a project.
4
- > v0.22.0 | Node.js >= 18 | zero required deps (`bareguard ^0.9.0` optional peer for governance) | Apache 2.0
4
+ > v0.23.0 | Node.js >= 18 | zero required deps (`bareguard ^0.9.0` optional peer for governance) | Apache 2.0
5
5
  >
6
6
  > Full human guide with composition examples, design philosophy, and recipes: [Usage Guide](docs/02-features/usage-guide.md)
7
7
 
@@ -37,6 +37,8 @@ Eight entry points:
37
37
  | Decompose a hard task into a verified tree (RLM) | recurse — decompose → fan-out → verify → synthesize in one call (**wire a gate**, cost is open by design) |
38
38
  | Count / answer "how many / all" over a corpus, honestly | recurse(task, ctx, `{ corpus, retrieval: 'scan' }`) — scans every slice, CODE-counts |
39
39
  | Give recurse workers a persona/role (senior-dev stance) | recurse(task, ctx, `{ persona }`) — prepended to every worker, carries down the tree; not applied to the verifier |
40
+ | Tell recurse workers WHERE they are (paths/cwd) so a slice can find its file | recurse(task, ctx, `{ context }`) — read-only blob on every worker's task message + the Planner + verifier; carries down (facts, not a stance) |
41
+ | Let a recurse LEAF retry its own failure with a deterministic check | recurse(task, ctx, `{ refineLeaf: { sensor } }`) — leaf becomes a bounded generate→sense→regenerate loop; your sensor (test/compile/lint), gap fed back, escalating temperature |
40
42
  | Track task state (pending/running/done/failed) | StateMachine |
41
43
  | Run agent turns on a schedule (cron, timers) | Scheduler |
42
44
  | Require human approval before dangerous actions | Checkpoint |
@@ -669,7 +671,18 @@ const out = await recurse('Audit auth.js, billing.js, gateway.js for authz bugs'
669
671
  });
670
672
  ```
671
673
 
672
- **What a delegated child inherits (important the setpoint is the TOP node's job):** when a worker delegates with `spawn_child`, the child runs a **fresh `recurse`** that inherits `tools`, `synthesize`, `maxDepth`, and `persona` but the parent's **`contract`/`evaluate` are stripped** (and the forced `count`/`mode` + the corpus `retrieval` knobs). A slice is not graded against the *whole*-task definition-of-done (that verdict is the top node's, and a slice satisfying the whole DoD is the wrong question); only the top `recurse` verifies the synthesized result. The non-overridable `critical → force-verify` safety floor still fires per node (it keys on the task text, not the contract). So: set `contract`/`evaluate` once at the top; they do not and should not — re-run per intermediate node.
674
+ **Worker context (`opts.context`, v0.23.0):** a read-only working-context string (paths/cwd) PREPENDED to every worker's TASK message as a `Working context:` block so a sliced child can **locate its artifact** (the Planner paraphrases the goal into subtasks and drops absolute paths; without this, workers guess `.`/`~`/`/tmp` and get denied). Forwarded to the Planner as `info` (path-aware slices) and shown to the verifier too (neutral FACTS, not a stance distinct from `persona`, which is a privileged SYSTEM-prompt stance). Carries down the tree. **Security:** it still becomes part of the prompt, so pass caller-trusted run-state only, never untrusted/end-user text (lower-privilege than `persona` user message, not system but still an injection surface). Absent the task message is unchanged.
675
+
676
+ **Leaf self-correction (`opts.refineLeaf`, v0.23.0, opt-in):** turn a **definite leaf** (a node offered no `spawn_child` — `simple` tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass: `{ sensor, maxIterations?, temperatures? }`. `sensor(result, { task, context, contract }) → Verdict` is YOUR **deterministic** close (test/compile/lint — not a model judge); on a non-pass its `critique` (the gap, not the transcript) is fed FRESH into the next attempt and the **retry temperature ESCALATES** (default `[0.2, 0.7, 1.0]` — load-bearing: a weak model at a flat temperature regenerates identical wrong code and ignores even crisp feedback). Each attempt is gate-checked + metered; a HaltError mid-loop → clean `{ incomplete }`; honest non-recovery → `receipts.refineLeaf.passed === false` (never a faked pass); `receipts.tokens` sums all attempts. The error-keyed `recall` stays YOUR tool (`opts.tools`), keyed off the fed-back critique — bareagent stays litectx-agnostic. Carries down (engages at the leaves). Absent ⇒ a leaf is a single pass.
677
+
678
+ ```javascript
679
+ const out = await recurse('Fix the failing function in calc.js', ctx, {
680
+ context: `project root: ${process.cwd()}\nresolve relative paths against it`,
681
+ refineLeaf: { sensor: (code) => runTestsAndGrade(code) }, // your deterministic test/compile close
682
+ });
683
+ ```
684
+
685
+ **What a delegated child inherits (important — the setpoint is the TOP node's job):** when a worker delegates with `spawn_child`, the child runs a **fresh `recurse`** that inherits `tools`, `synthesize`, `maxDepth`, `persona`, `context`, and `refineLeaf` — but the parent's **`contract`/`evaluate` are stripped** (and the forced `count`/`mode` + the corpus `retrieval` knobs). A slice is not graded against the *whole*-task definition-of-done (that verdict is the top node's, and a slice satisfying the whole DoD is the wrong question); only the top `recurse` verifies the synthesized result. The non-overridable `critical → force-verify` safety floor still fires per node (it keys on the task text, not the contract). So: set `contract`/`evaluate` once at the top; they do not — and should not — re-run per intermediate node.
673
686
 
674
687
  ## Wiring with Evaluator + refine (output-side verification)
675
688
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
package/src/recurse.d.ts CHANGED
@@ -64,11 +64,51 @@ export type RecurseOptions = {
64
64
  * and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only.
65
65
  */
66
66
  persona?: string | undefined;
67
+ /**
68
+ * - (BA-9 / relayfact F19) An optional caller-supplied READ-ONLY working-context
69
+ * blob (e.g. "project root: /abs/path\nfiles are relative to it") prepended to EVERY worker's task message as
70
+ * a `Working context:` block, so a sliced child can LOCATE its artifact — the concrete context (absolute
71
+ * paths / cwd) the Planner strips when it paraphrases the parent goal into child subtasks. CARRIES DOWN the
72
+ * tree (preserved by `forChild`, like `persona`) and, when forced fan-out plans, is forwarded as the Planner's
73
+ * `info` so the slices themselves are path-aware. Also shown to the verifier (neutral FACTS, not a stance, so
74
+ * no anti-sycophancy concern — and an agentic critic needs the path to exercise the artifact). Distinct from
75
+ * `persona`: persona is a privileged SYSTEM-prompt stance; context is run-state facts on the USER message.
76
+ * Absent/blank ⇒ byte-identical to pre-BA-9 (backward-compatible). Validated live (`poc/ba9-context-thread.mjs`:
77
+ * a weak model went 0/3 → 3/3 at locating an unguessable file once the root was threaded).
78
+ * **SECURITY:** this becomes part of every worker's prompt (and the verifier's) — lower-privilege than
79
+ * `persona` (the USER message, not the SYSTEM prompt) but still a prompt-injection surface. Intended for
80
+ * TRUSTED run-state (paths/cwd); do NOT pass untrusted / end-user-controlled text here.
81
+ */
82
+ context?: string | undefined;
67
83
  /**
68
84
  * - Handle tools offered to EVERY worker (RC-5 pull-default: litectx
69
85
  * `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus.
70
86
  */
71
87
  tools?: import("../types").ToolDef[] | undefined;
88
+ /**
89
+ * (Opt-in, BA-8 / relayfact F17) Turn a DEFINITE LEAF (a node that is offered no `spawn_child` — `simple`
90
+ * tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed
91
+ * slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that
92
+ * returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next
93
+ * attempt (D6/A1 anti-anchoring) and the **retry temperature ESCALATES** (`temperatures`, default
94
+ * `[0.2,0.7,1.0]`) — the live-validated requirement that lets a weak model escape a repeat-the-same-mistake rut
95
+ * (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). `maxIterations` defaults to
96
+ * `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the
97
+ * tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL
98
+ * (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node
99
+ * that delegates (its children + the tree verify own quality), nor to the scan/fanout/partition dispatch paths.
100
+ * Absent ⇒ a leaf is a single pass (byte-identical to pre-BA-8). An error-keyed `recall` is the CALLER's tool
101
+ * (`opts.tools`) keyed off the fed-back critique — bareagent stays litectx-agnostic.
102
+ */
103
+ refineLeaf?: {
104
+ sensor: (result: any, ctx: {
105
+ task: string;
106
+ context: string | undefined;
107
+ contract: string | null;
108
+ }) => (Verdict | Promise<Verdict>);
109
+ maxIterations?: number;
110
+ temperatures?: number[];
111
+ } | undefined;
72
112
  /**
73
113
  * - Definition of done (A3). When present, the verifier grades against THIS,
74
114
  * not the loose task, and verification always runs.
@@ -177,6 +217,16 @@ export type RecurseNode = {
177
217
  * - The worker Loop's `metrics.tokens`.
178
218
  */
179
219
  tokens: object | null;
220
+ /**
221
+ * - (BA-8) when this leaf
222
+ * ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally passed
223
+ * (false = honest non-recovery, not a faked success).
224
+ */
225
+ refineLeaf?: {
226
+ iterations: number;
227
+ passed: boolean;
228
+ temperatures: number[];
229
+ } | undefined;
180
230
  model: string | null;
181
231
  /**
182
232
  * - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`),
@@ -283,8 +333,35 @@ export type Slice = {
283
333
  * **SECURITY:** this is a PRIVILEGED system-prompt seam — treat `persona` like a system prompt. Do NOT pass
284
334
  * untrusted / end-user-controlled text here; a hostile persona is prepended ahead of the decomposition policy
285
335
  * and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only.
336
+ * @property {string} [context] - (BA-9 / relayfact F19) An optional caller-supplied READ-ONLY working-context
337
+ * blob (e.g. "project root: /abs/path\nfiles are relative to it") prepended to EVERY worker's task message as
338
+ * a `Working context:` block, so a sliced child can LOCATE its artifact — the concrete context (absolute
339
+ * paths / cwd) the Planner strips when it paraphrases the parent goal into child subtasks. CARRIES DOWN the
340
+ * tree (preserved by `forChild`, like `persona`) and, when forced fan-out plans, is forwarded as the Planner's
341
+ * `info` so the slices themselves are path-aware. Also shown to the verifier (neutral FACTS, not a stance, so
342
+ * no anti-sycophancy concern — and an agentic critic needs the path to exercise the artifact). Distinct from
343
+ * `persona`: persona is a privileged SYSTEM-prompt stance; context is run-state facts on the USER message.
344
+ * Absent/blank ⇒ byte-identical to pre-BA-9 (backward-compatible). Validated live (`poc/ba9-context-thread.mjs`:
345
+ * a weak model went 0/3 → 3/3 at locating an unguessable file once the root was threaded).
346
+ * **SECURITY:** this becomes part of every worker's prompt (and the verifier's) — lower-privilege than
347
+ * `persona` (the USER message, not the SYSTEM prompt) but still a prompt-injection surface. Intended for
348
+ * TRUSTED run-state (paths/cwd); do NOT pass untrusted / end-user-controlled text here.
286
349
  * @property {ToolDef[]} [tools] - Handle tools offered to EVERY worker (RC-5 pull-default: litectx
287
350
  * `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus.
351
+ * @property {{sensor: (result: any, ctx: {task: string, context: string|undefined, contract: string|null}) => (Verdict|Promise<Verdict>), maxIterations?: number, temperatures?: number[]}} [refineLeaf]
352
+ * (Opt-in, BA-8 / relayfact F17) Turn a DEFINITE LEAF (a node that is offered no `spawn_child` — `simple`
353
+ * tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed
354
+ * slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that
355
+ * returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next
356
+ * attempt (D6/A1 anti-anchoring) and the **retry temperature ESCALATES** (`temperatures`, default
357
+ * `[0.2,0.7,1.0]`) — the live-validated requirement that lets a weak model escape a repeat-the-same-mistake rut
358
+ * (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). `maxIterations` defaults to
359
+ * `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the
360
+ * tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL
361
+ * (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node
362
+ * that delegates (its children + the tree verify own quality), nor to the scan/fanout/partition dispatch paths.
363
+ * Absent ⇒ a leaf is a single pass (byte-identical to pre-BA-8). An error-keyed `recall` is the CALLER's tool
364
+ * (`opts.tools`) keyed off the fed-back critique — bareagent stays litectx-agnostic.
288
365
  * @property {string} [contract] - Definition of done (A3). When present, the verifier grades against THIS,
289
366
  * not the loose task, and verification always runs.
290
367
  * @property {(result: any, ctx: {contract: string|null, task: string}) => (Verdict|Promise<Verdict>)} [evaluate]
@@ -343,6 +420,9 @@ export type Slice = {
343
420
  * @property {boolean} incomplete
344
421
  * @property {boolean} halted
345
422
  * @property {object|null} tokens - The worker Loop's `metrics.tokens`.
423
+ * @property {{iterations: number, passed: boolean, temperatures: number[]}} [refineLeaf] - (BA-8) when this leaf
424
+ * ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally passed
425
+ * (false = honest non-recovery, not a faked success).
346
426
  * @property {string|null} model
347
427
  * @property {string|null} [retrieval] - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`),
348
428
  * or null/absent for a plain reasoning node.
package/src/recurse.js CHANGED
@@ -28,6 +28,7 @@ const { Evaluator } = require('./evaluator');
28
28
  const { Planner } = require('./planner');
29
29
  const { runPlan } = require('./run-plan');
30
30
  const { assessComplexity, isCritical } = require('./complexity');
31
+ const { refine } = require('./refine');
31
32
  const { HaltError } = require('./errors');
32
33
  const { DECOMPOSITION_POLICY, capabilityScrub } = require('./recurse-prompts');
33
34
  const { synthesize } = require('./recurse-synthesize');
@@ -51,6 +52,11 @@ const DEFAULT_FANOUT_CONCURRENCY = 4;
51
52
  // many parallel scan-workers a measured corpus needs. A calibratable knob (the §9.1 algorithm; corpus-specific),
52
53
  // not a discovered constant — overridable via `opts.workerBudget`. 100 items ≈ a worker doing ~25 scan windows.
53
54
  const DEFAULT_WORKER_BUDGET = 100;
55
+ // BA-8 leaf-refine: temperature ESCALATES per retry. The live POC (poc/ba8-leaf-refine.mjs) found that at a flat
56
+ // low temperature a weak model regenerates byte-identical wrong code and IGNORES even crisp deterministic
57
+ // feedback (0/5 recovery); recovery only appears once retries are given room to vary (0/5 → 2-3/5). So escalation
58
+ // is a DESIGN REQUIREMENT of the seam, not a tuning nicety. Overridable via `opts.refineLeaf.temperatures`.
59
+ const DEFAULT_REFINE_TEMPS = [0.2, 0.7, 1.0];
54
60
 
55
61
  /**
56
62
  * Split an array into EXACTLY `n` contiguous, near-equal chunks (the data-partition for the §11 width path).
@@ -82,6 +88,23 @@ function workerPersonaPrefix(persona) {
82
88
  return p ? p + '\n\n' : '';
83
89
  }
84
90
 
91
+ /**
92
+ * BA-9 (relayfact F19): prepend the caller's read-only working-context blob to a worker's task message, so a
93
+ * sliced child can LOCATE its artifact (absolute paths / cwd) — the concrete context the Planner otherwise
94
+ * strips when it paraphrases the parent goal into child subtasks. Distinct from `persona`: persona is a STANCE
95
+ * on the SYSTEM prompt (a privileged seam); context is neutral run-state FACTS on the USER message (the form
96
+ * the live POC `poc/ba9-context-thread.mjs` validated: no-context 0/3 → context 3/3 on a weak model). Absent/
97
+ * blank ⇒ the task message is byte-identical to pre-BA-9 (backward-compatible). Carries down the tree via
98
+ * `forChild` (a child of a worker rooted at `/proj` is still rooted at `/proj`), like `persona`.
99
+ * @param {string} task
100
+ * @param {unknown} context
101
+ * @returns {string}
102
+ */
103
+ function withContext(task, context) {
104
+ const c = typeof context === 'string' ? context.trim() : '';
105
+ return c ? `Working context (read-only):\n${c}\n\n${task}` : task;
106
+ }
107
+
85
108
  /**
86
109
  * The opts a delegated child inherits. Strips the parent's TOP-LEVEL SETPOINT — `contract`/`evaluate` grade
87
110
  * the WHOLE task's final answer; a child grading its own slice against the whole definition-of-done is wasted
@@ -91,9 +114,10 @@ function workerPersonaPrefix(persona) {
91
114
  * answered over the parent's corpus; a child has its own subtask and must not re-scan the parent's full corpus
92
115
  * (that would fan a whole-corpus count out under every child). The `critical → force-verify` SAFETY FLOOR is
93
116
  * unaffected — it keys on the task text via `isCritical`, not the contract, so a critical child still
94
- * self-verifies. Handle tools (`opts.tools`), `synthesize`, `maxDepth`, and **`persona`** carry down — the
95
- * persona is a DURABLE worker stance (a child of a "senior security engineer" is still one), unlike the
96
- * top-only `contract`/`evaluate` setpoint. It rides through the `...opts` spread (not in the strip list).
117
+ * self-verifies. Handle tools (`opts.tools`), `synthesize`, `maxDepth`, **`persona`**, and **`context`** (BA-9)
118
+ * carry down — the persona is a DURABLE worker stance (a child of a "senior security engineer" is still one)
119
+ * and the context is durable run-state (a child rooted at `/proj` is still rooted at `/proj`), unlike the
120
+ * top-only `contract`/`evaluate` setpoint. They ride through the `...opts` spread (not in the strip list).
97
121
  * @param {RecurseOptions} opts
98
122
  * @returns {RecurseOptions}
99
123
  */
@@ -173,8 +197,35 @@ function auditSafeCtx(ctx, overrides = {}) {
173
197
  * **SECURITY:** this is a PRIVILEGED system-prompt seam — treat `persona` like a system prompt. Do NOT pass
174
198
  * untrusted / end-user-controlled text here; a hostile persona is prepended ahead of the decomposition policy
175
199
  * and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only.
200
+ * @property {string} [context] - (BA-9 / relayfact F19) An optional caller-supplied READ-ONLY working-context
201
+ * blob (e.g. "project root: /abs/path\nfiles are relative to it") prepended to EVERY worker's task message as
202
+ * a `Working context:` block, so a sliced child can LOCATE its artifact — the concrete context (absolute
203
+ * paths / cwd) the Planner strips when it paraphrases the parent goal into child subtasks. CARRIES DOWN the
204
+ * tree (preserved by `forChild`, like `persona`) and, when forced fan-out plans, is forwarded as the Planner's
205
+ * `info` so the slices themselves are path-aware. Also shown to the verifier (neutral FACTS, not a stance, so
206
+ * no anti-sycophancy concern — and an agentic critic needs the path to exercise the artifact). Distinct from
207
+ * `persona`: persona is a privileged SYSTEM-prompt stance; context is run-state facts on the USER message.
208
+ * Absent/blank ⇒ byte-identical to pre-BA-9 (backward-compatible). Validated live (`poc/ba9-context-thread.mjs`:
209
+ * a weak model went 0/3 → 3/3 at locating an unguessable file once the root was threaded).
210
+ * **SECURITY:** this becomes part of every worker's prompt (and the verifier's) — lower-privilege than
211
+ * `persona` (the USER message, not the SYSTEM prompt) but still a prompt-injection surface. Intended for
212
+ * TRUSTED run-state (paths/cwd); do NOT pass untrusted / end-user-controlled text here.
176
213
  * @property {ToolDef[]} [tools] - Handle tools offered to EVERY worker (RC-5 pull-default: litectx
177
214
  * `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus.
215
+ * @property {{sensor: (result: any, ctx: {task: string, context: string|undefined, contract: string|null}) => (Verdict|Promise<Verdict>), maxIterations?: number, temperatures?: number[]}} [refineLeaf]
216
+ * (Opt-in, BA-8 / relayfact F17) Turn a DEFINITE LEAF (a node that is offered no `spawn_child` — `simple`
217
+ * tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed
218
+ * slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that
219
+ * returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next
220
+ * attempt (D6/A1 anti-anchoring) and the **retry temperature ESCALATES** (`temperatures`, default
221
+ * `[0.2,0.7,1.0]`) — the live-validated requirement that lets a weak model escape a repeat-the-same-mistake rut
222
+ * (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). `maxIterations` defaults to
223
+ * `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the
224
+ * tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL
225
+ * (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node
226
+ * that delegates (its children + the tree verify own quality), nor to the scan/fanout/partition dispatch paths.
227
+ * Absent ⇒ a leaf is a single pass (byte-identical to pre-BA-8). An error-keyed `recall` is the CALLER's tool
228
+ * (`opts.tools`) keyed off the fed-back critique — bareagent stays litectx-agnostic.
178
229
  * @property {string} [contract] - Definition of done (A3). When present, the verifier grades against THIS,
179
230
  * not the loose task, and verification always runs.
180
231
  * @property {(result: any, ctx: {contract: string|null, task: string}) => (Verdict|Promise<Verdict>)} [evaluate]
@@ -234,6 +285,9 @@ function auditSafeCtx(ctx, overrides = {}) {
234
285
  * @property {boolean} incomplete
235
286
  * @property {boolean} halted
236
287
  * @property {object|null} tokens - The worker Loop's `metrics.tokens`.
288
+ * @property {{iterations: number, passed: boolean, temperatures: number[]}} [refineLeaf] - (BA-8) when this leaf
289
+ * ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally passed
290
+ * (false = honest non-recovery, not a faked success).
237
291
  * @property {string|null} model
238
292
  * @property {string|null} [retrieval] - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`),
239
293
  * or null/absent for a plain reasoning node.
@@ -401,6 +455,15 @@ async function recurse(task, ctx = {}, opts = {}) {
401
455
  if (Array.isArray(opts.corpus)) retrievalTools.push(buildExactTool(normalizeCorpus(opts.corpus)));
402
456
  }
403
457
  const handleTools = [...(Array.isArray(opts.tools) ? opts.tools : []), ...retrievalTools];
458
+
459
+ // BA-8 (opt-in): a DEFINITE leaf (no spawn offered — `simple` tier or at `maxDepth`) with a caller sensor runs
460
+ // as a bounded refine-with-escalation loop instead of a single pass, so a failed slice self-corrects. Gating on
461
+ // `!canSpawn` keeps it predictable (a node that may delegate is an orchestrator, not a leaf) and means the seam
462
+ // engages exactly at the leaves of a Family-A tree (it carries down via forChild). A no-op when unset.
463
+ if (!canSpawn && opts.refineLeaf && typeof opts.refineLeaf.sensor === 'function') {
464
+ return recurseRefineLeaf(task, ctx, opts, { provider, system, handleTools, depth, critical, node, sensor: opts.refineLeaf.sensor });
465
+ }
466
+
404
467
  // NB-3: collect each child's declared RESULT value (copy-on-return: the value, never its transcript) so the
405
468
  // reducer can aggregate them. Step-3's seam handed the receipts only, so a code-reduce could not see what to
406
469
  // combine — this closes that gap and is what Family B (step 5) will reduce over `runPlan` results[].
@@ -421,7 +484,8 @@ async function recurse(task, ctx = {}, opts = {}) {
421
484
  // Fresh message array = a true fresh window (RC-2 copy-on-return, IN side): the worker sees ONLY its task,
422
485
  // never a parent transcript. `ctx.depth` is threaded so bareguard's policy can enforce the depth cap (§6).
423
486
  const out = await loop.run(
424
- [{ role: 'user', content: task }],
487
+ // BA-9: prepend the caller's read-only working-context (paths/cwd) so this worker can locate its artifact.
488
+ [{ role: 'user', content: withContext(task, opts.context) }],
425
489
  tools,
426
490
  // auditSafeCtx: the run ctx reaches the gate as `_ctx`; strip the key-bearing provider (F16/BA-1). The
427
491
  // worker Loop already has `provider` as a constructor option, so stripping it from the run ctx is invisible
@@ -504,6 +568,96 @@ async function recurse(task, ctx = {}, opts = {}) {
504
568
  }
505
569
  }
506
570
 
571
+ /**
572
+ * BA-8 leaf-refine — run a DEFINITE leaf as a bounded generate→sense→regenerate loop (relayfact F17). Reuses the
573
+ * existing `refine.js` primitive (the Outcomes iterate→grade→revise port): each attempt is a FRESH leaf Loop
574
+ * (fresh window = fresh-feedback, D6/A1) seeded with the working-context'd task + (on a retry) the prior GAP, run
575
+ * at an ESCALATING temperature — the live-validated requirement that lets a weak model escape a repeat-the-same-
576
+ * mistake rut (a flat temperature recovered 0/5 in `poc/ba8-leaf-refine.mjs`). The `sensor` is the caller's
577
+ * DETERMINISTIC close (test/compile/lint, not a model judge). Governance is bareguard's: every attempt is gate-
578
+ * checked (`ctx.policy`) and metered (`onLlmResult`); a HaltError mid-loop is a clean `{incomplete}`. Honest
579
+ * non-recovery is reported (`receipts.refineLeaf.passed=false`), never a faked pass. An optional rubric `verify`
580
+ * still runs on top when a `contract`/`evaluate`/critical applies (the sensor gates retries; the rubric grades).
581
+ * @param {string} task
582
+ * @param {RecurseCtx} ctx
583
+ * @param {RecurseOptions} opts
584
+ * @param {{provider: Provider, system: string, handleTools: ToolDef[], depth: number, critical: boolean, node: RecurseNode, sensor: Function}} state
585
+ * @returns {Promise<RecurseResult>}
586
+ */
587
+ async function recurseRefineLeaf(task, ctx, opts, state) {
588
+ const { provider, system, handleTools, depth, critical, node, sensor } = state;
589
+ node.model = provider.model || null;
590
+ const cfg = /** @type {{maxIterations?: number, temperatures?: number[]}} */ (opts.refineLeaf || {});
591
+ const temps = Array.isArray(cfg.temperatures) && cfg.temperatures.length ? cfg.temperatures : DEFAULT_REFINE_TEMPS;
592
+ const maxIterations = Number.isInteger(cfg.maxIterations) && /** @type {number} */ (cfg.maxIterations) > 0
593
+ ? /** @type {number} */ (cfg.maxIterations) : temps.length;
594
+
595
+ // A refine leaf runs N Loops, so its receipts.tokens SUMS every attempt's spend (not just the last) — the
596
+ // honest cost of the node. The 4-tier tokens object (`{input,output,cacheCreation,cacheRead}`, loop.js) is flat
597
+ // numeric, so we accrue field-wise (robust to extra/renamed numeric fields). The gate already sees each attempt
598
+ // via onLlmResult independently; this is the receipts mirror. Stays null until an attempt produces metrics.
599
+ /** @type {Record<string, number>|null} */
600
+ let tokensSum = null;
601
+ const accrueTokens = (/** @type {any} */ t) => {
602
+ if (!t || typeof t !== 'object') return;
603
+ tokensSum = tokensSum || {};
604
+ for (const [k, v] of Object.entries(t)) if (typeof v === 'number') tokensSum[k] = (tokensSum[k] || 0) + v;
605
+ };
606
+ // One attempt = a fresh leaf Loop (no spawn tool: a retry is a direct correction, not a re-decomposition) at the
607
+ // iteration's temperature, with the GAP fed forward as fresh feedback. A governance halt → throw so refine stops.
608
+ const attempt = async ({ iteration, critique }) => {
609
+ const temperature = temps[Math.min(iteration, temps.length - 1)];
610
+ const loop = new Loop({
611
+ provider, system,
612
+ policy: ctx.policy || undefined,
613
+ onLlmResult: ctx.onLlmResult || undefined,
614
+ stream: ctx.stream || undefined,
615
+ throwOnError: false,
616
+ });
617
+ const base = withContext(task, opts.context);
618
+ const userText = critique
619
+ ? `${base}\n\nYour previous attempt FAILED these checks:\n${critique}\n\nReturn a corrected result that passes ALL of them.`
620
+ : base;
621
+ const out = await loop.run([{ role: 'user', content: userText }], handleTools, { ctx: auditSafeCtx(ctx, { depth }), temperature });
622
+ accrueTokens(out.metrics ? out.metrics.tokens : null);
623
+ if (typeof out.error === 'string' && out.error.startsWith('halt:')) throw new HaltError('refine-leaf attempt halted', { rule: out.error.slice('halt:'.length) });
624
+ if (out.error) throw new Error(out.error); // a non-halt worker fault → honest incomplete
625
+ return out.text;
626
+ };
627
+
628
+ try {
629
+ const outcome = await refine({
630
+ attempt,
631
+ evaluate: (result, c) => sensor(result, { task, context: opts.context, contract: c.contract }),
632
+ contract: typeof opts.contract === 'string' ? opts.contract : undefined,
633
+ maxIterations,
634
+ });
635
+ node.tokens = tokensSum;
636
+ node.refineLeaf = { iterations: outcome.iterations, passed: !!(outcome.verdict && outcome.verdict.pass), temperatures: temps.slice(0, outcome.iterations) };
637
+ const result = outcome.result;
638
+
639
+ // Optional rubric layer on top of the deterministic sensor (RC-7): forced for critical, or a contract/override.
640
+ const wantVerify = critical || typeof opts.contract === 'string' || typeof opts.evaluate === 'function';
641
+ if (wantVerify) {
642
+ const verdict = await verify(task, result, ctx, opts);
643
+ node.verdict = verdict;
644
+ return { result, verdict, receipts: node };
645
+ }
646
+ // No rubric layer ⇒ the sensor's final verdict IS the node verdict (a non-pass is surfaced, not hidden).
647
+ node.verdict = outcome.verdict || null;
648
+ return { result, verdict: outcome.verdict || null, receipts: node };
649
+ } catch (err) {
650
+ node.tokens = tokensSum; // record whatever attempts DID spend, on both the halt and fault paths
651
+ if (err instanceof HaltError) {
652
+ node.halted = true;
653
+ node.incomplete = true;
654
+ return { incomplete: true, best: null, receipts: node };
655
+ }
656
+ node.incomplete = true;
657
+ return { incomplete: true, best: null, receipts: node };
658
+ }
659
+ }
660
+
507
661
  /**
508
662
  * SCAN (§10 step 7 / §9.2.1) — the default retrieval mode for a "how many / all" task over a corpus. A
509
663
  * deterministic ORCHESTRATION, not a worker model call: every slice is processed, an isolated Loop LLM-judges
@@ -747,7 +901,10 @@ async function recurseFanout(task, ctx, opts, state) {
747
901
  const planner = new Planner({ provider, onLlmResult: /** @type {any} */ (ctx.onLlmResult) || undefined });
748
902
  let steps;
749
903
  try {
750
- steps = await planner.plan(task, { count });
904
+ // BA-9: forward the working-context as the Planner's `info` so the slices it writes are path-aware (a
905
+ // child still also receives `opts.context` directly via `forChild` — this just improves the split).
906
+ const planContext = typeof opts.context === 'string' && opts.context.trim() ? { count, info: opts.context } : { count };
907
+ steps = await planner.plan(task, planContext);
751
908
  } catch (err) {
752
909
  if (err instanceof HaltError) throw err;
753
910
  node.incomplete = true;
@@ -928,6 +1085,9 @@ function buildSpawnTool(ctx, opts, depth, maxDepth, node, childResults) {
928
1085
  */
929
1086
  function verify(task, result, ctx, opts) {
930
1087
  const contract = typeof opts.contract === 'string' ? opts.contract : null;
1088
+ // BA-9: the verifier sees the working-context too — neutral facts (not a stance, so no anti-sycophancy risk),
1089
+ // and an agentic critic needs the path to exercise the artifact. A caller `evaluate` gets the RAW task (it owns
1090
+ // its own context); only the default isolated grader is contextualized.
931
1091
  if (typeof opts.evaluate === 'function') {
932
1092
  return Promise.resolve(opts.evaluate(result, { contract, task }));
933
1093
  }
@@ -937,7 +1097,7 @@ function verify(task, result, ctx, opts) {
937
1097
  ? 'Judge whether the result satisfies the definition of done. Be strict and adversarial; cite the specific gap on any shortfall.'
938
1098
  : 'Judge whether the result fully and correctly answers the goal. Be strict and adversarial; cite the specific gap on any shortfall.';
939
1099
  return evaluator.evaluate(
940
- task,
1100
+ withContext(task, opts.context),
941
1101
  result,
942
1102
  { rubric, contract: contract || undefined },
943
1103
  { onLlmResult: /** @type {any} */ (ctx.onLlmResult), policy: ctx.policy },