bare-agent 0.46.5 → 0.47.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
@@ -17,292 +17,62 @@
17
17
  <img src="https://img.shields.io/badge/license-Apache%202.0-2a4f8c" alt="license: Apache 2.0">
18
18
  </p>
19
19
 
20
- **Lightweight agent orchestration. Zero required deps — optional [bareguard](https://npmjs.com/package/bareguard) peer for single-gate governance.**
20
+ **Lightweight agent primitives. Zero required deps — optional [bareguard](https://npmjs.com/package/bareguard) peer for one-gate governance.**
21
21
 
22
- Lightweight enough to understand completely. Complete enough to not reinvent wheels. Not a framework, not 50,000 lines of opinions — just composable building blocks for agents. The core imports nothing; when you want governance, wire bareguard and every tool call traverses one policy hook, one audit log, one budget cap.
22
+ bare-agent started as primitives for building agents by hand: your code is the deterministic body, an LLM call sits in wherever you need judgment — the LangChain / CrewAI / AutoGen job, provider-agnostic, without a framework.
23
23
 
24
- ## Quick start
24
+ LLMs got good at wiring things themselves, so the same primitives are now offered as tool calls too: the model wires them up, bare-agent runs the loop, retries, budgets, and governance underneath. It's what [bareloop](https://github.com/hamr0/bareloop) and [fwdloop](https://github.com/hamr0/fwdloop) run on.
25
25
 
26
- ```bash
27
- npm install bare-agent
28
- ```
29
-
30
- **1. Give your AI assistant the integration guide**
26
+ Some things an LLM can't judge about its own work. Building a harness or an external arbiter? Every primitive is listed in `primitives.json` — when to use it, import, signature, failure modes, a runnable example — plus a decisive return-time `judge` and the Jev gut-check tier.
31
27
 
32
- ```
33
- Read bareagent.context.md from node_modules/bare-agent/bareagent.context.md
34
- ```
28
+ ## Start here
35
29
 
36
- This single file contains component selection, wiring recipes, API signatures, and gotchas — everything an agent needs to use the library correctly.
30
+ - **Building by hand** → `npm install bare-agent`, then hand your assistant `bareagent.context.md` (ships in the package — the complete contract).
31
+ - **AI agent / tool-calling** → read `primitives.json` first (`unpkg.com/bare-agent/primitives.json` before you install, or `require('bare-agent/primitives.json')`); generated from source, never drifts.
32
+ - **Harness / judge builder** → `primitives.json` + `judge` + Jev + wire bareguard.
37
33
 
38
- **2. Describe what you want**
34
+ ## Fast gut check — Jev
39
35
 
40
- ```
41
- I need an agent that:
42
- - Takes a user goal and breaks it into steps
43
- - Runs steps in parallel where possible
44
- - Retries failed steps twice
45
- - Streams progress as JSONL events
36
+ An LLM call is slow, deliberate thinking (System 2). Jev, from TypeSafe, is the fast gut check (System 1): a classifier that returns a typed decision with a calibrated probability — roughly 200× faster and 400× cheaper than an LLM, per TypeSafe. bare-agent's `JevProvider` covers all three shapes — **yes/no**, **pick-one**, **score** — so your automation calls it like any other step: act when it's confident, escalate when it isn't. Injection-hardened by default; also usable as the `jev` check in the Evaluator.
46
37
 
47
- Use bare-agent. The integration guide is in bareagent.context.md.
48
- ```
49
-
50
- That's it. The context doc is structured for LLM consumption — your agent reads it once and knows how to wire every component.
51
-
52
- **Not sure what you need?** Paste this into any AI assistant:
53
-
54
- ```
55
- I want to build an agent using bare-agent. Read the integration guide at
56
- node_modules/bare-agent/bareagent.context.md, then ask me up to 5 questions
57
- about what I need. Based on my answers, tell me which components to use
58
- and show me the wiring code.
59
- ```
38
+ ## Break big tasks down — RLM
60
39
 
61
- ---
62
-
63
- ## For AI agents — the menu
64
-
65
- Building tool-calling automation on top of bare-agent? Read **`primitives.json`** first. It's a compact, machine-readable menu of every building block — the fast path to *using* bare-agent without reading the docs: load it, pick a primitive, wire it. Each entry carries `when` to reach for it, its `import`, `signature`, `fails`, and a runnable `example`:
66
-
67
- ```jsonc
68
- {
69
- "name": "AnthropicProvider",
70
- "category": "providers",
71
- "when": "you want Claude models as the Loop's provider — native Messages API with opt-in prompt caching and thinking-block passthrough",
72
- "import": "import { AnthropicProvider } from 'bare-agent/providers'",
73
- "signature": "new AnthropicProvider(options?: AnthropicOptions)",
74
- "fails": "…",
75
- "example": "…"
76
- }
77
- // 51 entries across: providers · routing · retrieval · mcp · loop · memory · tools · skills · evaluation · hitl · resilience · governance · orchestration · …
78
- ```
79
-
80
- Browse it on unpkg (`unpkg.com/bare-agent/primitives.json`) before you install, `require` it (`const menu = require('bare-agent/primitives.json')`), or point a tool at it. Generated from the source, so it never drifts.
81
-
82
- **Then go deeper:** `bareagent.context.md` (the complete contract — every option and the full API) and the rest of this README for the recipes.
40
+ `recurse` brings Recursive Language Models (RLM) to your code in one call: split a hard task into smaller ones, run each in a fresh context window, check the result, and stitch it back together. The model decides when to split (or you force a fixed count), and over a large set of documents it picks the path by question shape — scan everything for "how many", search for a needle. Totals are counted by code, and a dead worker comes back `incomplete`, never a faked pass. Cost is open by design: run it under a budget cap (bareguard) or set `maxDepth: 1`.
83
41
 
84
42
  ## What's inside
85
43
 
86
- Every piece works alone — take what you need, ignore the rest. Two axes: **Act** (get work done) and **Verify** (check it, keep context clean), with **one gate** over both — plus **`recurse`**, an Act-side primitive big enough to earn its own spotlight below.
87
-
88
- ### Act — get work done
89
-
90
- | Component | What it does |
91
- |---|---|
92
- | **Loop** | Think → act → observe until done. Any LLM, your tools, per-run cost. Opt-in seams: `policy` (govern), `assemble` (context engineering), `trim` (bound the transcript) |
93
- | **Planner** | Break a goal into a step DAG. Cached |
94
- | **assessComplexity** | Rate a goal `simple`→`critical` from its text — no LLM. Gates whether to plan |
95
- | **runPlan** | Run plan steps in parallel waves. Dependency-aware, per-step retry |
96
- | **recurse** | RLM decompose→fan-out→verify→synthesize in one call. Model-driven (`spawn_child`) or forced fan-out (`count`); `retrieval:'scan'` answers "how many / all" over a corpus by scanning every slice and code-counting — never a faked pass (honest `{incomplete, missingSlices}`) |
97
- | **Memory** | Persist + recall across sessions via a swappable `Store` — zero-dep JSON, SQLite, or [litectx](https://npmjs.com/package/litectx) in a one-line swap |
98
- | **StateMachine** | Task lifecycle: `pending → running → done / failed / waiting / cancelled` |
99
- | **Scheduler** | Cron or relative triggers. Jobs survive restarts |
100
- | **Checkpoint** | Human approval gate — bring your own transport |
101
- | **Spawn** | Fork a child agent. One shared audit log + budget |
102
- | **Defer** | Queue an action for a waker to fire later. Governed when emitted *and* when it fires |
103
- | **Retry · CircuitBreaker · Fallback** | Resilience: backoff with jitter, fail-fast, provider failover |
104
- | **Stream · Errors** | Structured JSONL events; typed error hierarchy |
105
- | **Browsing · Mobile · Shell** | Hands: web ([barebrowse](https://npmjs.com/package/barebrowse)), Android + iOS ([baremobile](https://npmjs.com/package/baremobile)), cross-platform shell — library tools or token-thrifty CLI sessions |
106
- | **MCP Bridge** | Auto-discover MCP servers from your IDE configs; expose as tools (bulk or meta-tools) |
107
-
108
- ### Verify — check the work, keep context clean *(the eval-assist suite)*
109
-
110
- | Component | What it does |
111
- |---|---|
112
- | **Evaluator + refine** | Judge output by `predicate` (no tokens), `rubric` (an isolated adversarial grader), `agentic` (a critic that exercises the live artifact), or `jev` (a cheap calibrated classifier tier). `refine` is the bounded generate→evaluate→regenerate loop |
113
- | **SkillRegistry** | Surface skills on demand: one meta-tool catalog; activating a skill injects its instructions and unlocks its tools |
114
- | **stash** | Compact finished work out of the live window (restorable), or auto-fold the middle under token pressure |
44
+ Every piece works alone — take what you need, ignore the rest. No required deps — the core imports nothing.
115
45
 
116
- ### Recurse — break a hard task into a tree *(the RLM primitive)*
117
-
118
- `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 — one that judges the *returned* result, never a worker side-effect it could game), feeding the gap back (with escalating temperature on models that accept it; on a temperature-fixed model like `claude-sonnet-5` the gap critique carries recovery, and the receipt records the effective temps). On a temperature-fixed model, `refineLeaf.rejectedBuffer` adds a second lever — it feeds the model's own prior *failed attempts* back verbatim ("write something structurally different"), the directed-diversity complement to temperature's random diversity (adaptive by default; the two are antagonistic, so it holds temperature flat when it engages). A *broken* arbiter — a sensor or a caller `evaluate` that throws or returns a malformed verdict — is named, never blamed on the model: the node stops at the first broken close and returns `{ incomplete, blocker: 'broken-sensor' | 'broken-verifier' }` with the model's last output preserved. 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.
119
-
120
- Over a corpus, context reaches a worker as a **handle routed by question shape** (`opts.retrieval`):
121
-
122
- | `retrieval` | Use it for | How |
46
+ | Area | Component | What it does |
123
47
  |---|---|---|
124
- | `'scan'` *(default over a corpus)* | "how many / all / count" | scans every slice, LLM-judges each, **code-counts** the union — the only path that can't silently undercount |
125
- | `'search'` | find a needle (few matches) | litectx `recall` handle tool, embeddings on — **cannot count** |
126
- | `'exact'` | rule / exact-term match | code-side AND-filter, embeddings off |
127
- | `'tools'` | mixed task (needle *and* count) | offers all three; the worker picks per sub-query by tool description |
128
-
129
- ```js
130
- const { recurse } = require('bare-agent');
131
-
132
- // Honest count over a corpus: scans every slice, LLM-judges each, CODE-counts the union.
133
- const { result } = await recurse(
134
- 'How many of these support tickets are billing disputes?',
135
- { provider },
136
- { corpus: tickets /* {id,text}[] */, retrieval: 'scan' },
137
- );
138
- console.log(result.count, result.matchedIds); // a code-derived count + the ids that back it
139
- ```
140
-
141
- > **⚠️ Cost is open by design — wire a cap.** `recurse()` adds no intrinsic total-work limit. On the model-driven default a node can spawn up to ~100 children per level, each recursing to `maxDepth` (default 3), so **token / $ spend compounds and is bounded only by your gate** — not by recurse. Run it **with bareguard** (`ctx.policy`, which enforces depth/budget/call caps) **or with some token/USD cap** for any non-trivial or untrusted task; ungoverned, a weak model that over-decomposes *will* burn tokens. For a hard local brake without a gate, set `maxDepth: 1` (flat, no nesting). The forced modes (`mode:'fanout'` / `'partition'`) are bounded by a deterministic count + concurrency cap; the open path is the model-driven default.
142
-
143
- **Govern — one gate over both axes.** `wireGate(gate)` routes every LLM + tool call through one bareguard policy + audit + budget. Denied tools never reach the model; halts (turn / budget / content caps) exit cleanly. A plain deny stays advisory (the model can pivot to an allowed tool), but the Loop short-circuits a *spin* — `maxConsecutiveDenials` consecutive denials of the same action (default 3) stop the run with `error:'denied:<tool>'` instead of burning the budget to the cap; under `recurse` that surfaces as `{ incomplete, blocker:'governance-deny' }`. The same bound covers a tool that keeps *failing*: `maxIdenticalToolErrors` (default 3) stops a model re-sending a byte-identical call that cannot succeed, with `error:'stuck:<tool>'`. `require('bare-agent/bareguard')`
144
-
145
- **Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Gemini (native), Ollama, CLIPipe, Fallback — or bring your own (one `generate` method). All return the same shape; swap freely. Usage including prompt-cache tiers is normalized, so `result.metrics` reports honest cumulative tokens + cost — and `null`, never a silent `0`, for a model it couldn't price. A model that rejects a non-default `temperature` (e.g. `claude-sonnet-5`, OpenAI o1/gpt-5-class return a `400`) is handled gracefully — the provider drops the param and retries once rather than failing the call, surfacing `temperatureDropped` so a caller can report the effective value. **CLIPipe can drive a full agentic Loop — tools and turns — over a CLI *subscription* instead of the metered API.** Two modes, chosen by cost: `toolProtocol:'claude-mcp'` (v0.33.0, NATIVE — the CLI runs its own session and calls your tools over an MCP bridge; the gate, spin guards, and turn bound ride the provider, and it caches session-side — ~$0.006/turn) is the default for the Claude CLI, while `toolProtocol:'claude'` (v0.32.0, schema-validated emulation with the Loop keeping the cycle) stays right for a CLI with no MCP channel. A capable model is enforced upfront (sonnet-class+ for tools; Claude CLI for now).
146
-
147
- **The run tells you the truth about what happened on the wire.** Every provider reports **why** generation ended (`stopReason`, normalized across all of them and surfaced on **every** `Loop.run()` return), so a round the API **cut off at the token cap** can no longer masquerade as a finished answer: it returns `error: 'truncated:max_tokens'` with the partial text preserved, instead of a silent `error: null` (BA-6). Its tool calls are **refused, never executed** — a *complete* tool call always arrives tagged `tool_use`, so one riding a truncated round was cut off mid-generation with arguments missing, which is exactly how a truncated `shell_write` can zero a file. The same honesty now covers **every** non-clean terminal signal (BA-13): a safety `refusal` returns `error: 'refusal'` and a blown context window returns `error: 'context_exceeded'` (both were previously laundered into an empty success), while a resumable `pause_turn` makes the loop resume rather than terminate. `error` is the sole success signal; a bound firing preserves the model's work rather than discarding it (BA-5).
148
-
149
- **Thinking blocks survive the round-trip.** `claude-sonnet-5` and Opus 4.7+ run adaptive thinking **by default** — they return `thinking` blocks whether or not you ask — and Anthropic's contract is that those blocks come back **unchanged, signature included**, when a tool-use conversation continues. bare-agent used to drop every one, silently (the API returns 200 either way). They now ride the transcript on `Message.providerBlocks` and are replayed verbatim, with no flag to set. `new Anthropic({ thinking })` is a separate opt-in for pinning the mode or reaching `display`/`effort` — it does *not* enable thinking, which is already on. **This is a protocol/data-loss fix, not a capability one: a head-to-head with thinking fully preserved produced no better outcomes, and we say so rather than sell it (BA-7).**
150
-
151
- **Anthropic tool loops: turn on transcript caching.** Anthropic does **not** auto-cache, so by default a tool loop re-buys its entire growing transcript at full input price *every round*. `new Anthropic({ cacheMessages: true })` rolls a cache breakpoint forward each round — measured **$0.0753 → $0.0110 per round (6.8× cheaper)** in steady state on `claude-sonnet-5`; the 1.25× cache write is paid once. Caching pays for **re-sending, not growing** (fresh content still costs full price to write), and a destructive `trim` fold that rewrites the transcript *prefix* invalidates it — fold the middle, keep the head.
152
-
153
- **Tools:** Any function is a tool — REST, MCP, CLI, shell. Built-in web + mobile (optional).
154
-
155
- **Cross-language:** Run as a subprocess; talk JSONL over stdin/stdout from Python, Go, Rust, Ruby, or Java. Wrappers in [`contrib/`](contrib/README.md).
156
-
157
- **Deps:** none required — the core imports nothing. Optional peers: `bareguard >=0.9.0 <1.0.0` (governance), `better-sqlite3` (SQLite store); optional: `cron-parser`, `barebrowse`, `baremobile`, `wearehere`.
158
-
159
- This table is the map, not the manual — per-component wiring and API detail live in the [Integration Guide](bareagent.context.md) and [Usage Guide](docs/archive/usage-guide.md).
160
-
161
- ---
162
-
163
- ## Recipes
164
-
165
- ### Wire bareguard into Loop
166
-
167
- ```js
168
- const { Gate } = require('bareguard');
169
- const { Loop, wireGate, defaultActionTranslator } = require('bare-agent');
170
-
171
- const gate = new Gate({
172
- budget: { maxCostUsd: 0.50 },
173
- limits: { maxToolRounds: 20 }, // bareguard 0.4.2+ — N tool rounds, LLM rounds bypass
174
- audit: { path: './audit.jsonl' },
175
- });
176
- await gate.init();
177
-
178
- const { policy, onLlmResult, onToolResult, filterTools } = wireGate(gate, {
179
- // Optional: translate tool names → bareguard primitive types for bash/fs/net rules.
180
- // bareguard 0.4.1+ reads args.command / args.path verbatim, so args passes through.
181
- actionTranslator: (toolName, args, ctx) => {
182
- if (toolName === 'shell_exec') return { type: 'bash', args, _ctx: ctx };
183
- if (toolName === 'shell_read') return { type: 'read', args, _ctx: ctx };
184
- if (toolName === 'shell_write') return { type: 'write', args, _ctx: ctx }; // gate by fs.writeScope
185
- if (toolName === 'shell_edit') return { type: 'edit', args, _ctx: ctx }; // same fs.writeScope as write
186
- return defaultActionTranslator(toolName, args, ctx);
187
- },
188
- });
189
- const tools = await filterTools(myTools); // drop tools denied by static policy
190
-
191
- const loop = new Loop({ provider, policy, onLlmResult, onToolResult });
192
- await loop.run([{ role: 'user', content: 'go' }], tools, { ctx: { userId: 42 } });
193
- ```
194
-
195
- `onLlmResult` + `onToolResult` are what make `budget.maxCostUsd` actually cover token-heavy workloads — without them, budget only sees tool cost. `ctx` flows through to `gate.record` as `_ctx` for per-principal accounting.
196
-
197
- ### Per-principal bypass (owner / admin role)
198
-
199
- Wrap the gate policy when a principal is trusted unconditionally:
200
-
201
- ```js
202
- const { policy: gatePolicy } = wireGate(gate);
203
-
204
- const policy = async (toolName, args, ctx) => {
205
- if (ctx?.role === 'owner') return true; // bypass gate entirely
206
- return gatePolicy(toolName, args, ctx);
207
- };
208
-
209
- new Loop({ provider, policy, onLlmResult, onToolResult });
210
- ```
211
-
212
- Bypassing the gate also bypasses audit and budget — only do this for principals you trust unconditionally. For partial trust, use ctx-aware rules inside bareguard instead.
213
-
214
- ### Custom deny strings (localize / strip prefix)
215
-
216
- ```js
217
- const { policy } = wireGate(gate, {
218
- formatDeny: (decision) => `Sorry — ${decision.reason || 'not allowed'}`,
219
- });
220
- ```
221
-
222
- Halt-severity decisions bypass `formatDeny` (they throw `HaltError` and exit the loop without ever reaching the LLM).
223
-
224
- ### Catch halts in your app
225
-
226
- ```js
227
- const result = await loop.run(msgs, tools);
228
- if (result.error?.startsWith('halt:')) {
229
- // budget cap, turn cap, or gate terminated. Inspect rule:
230
- const rule = result.error.slice('halt:'.length);
231
- // tell the user, schedule retry, escalate, etc.
232
- }
233
- ```
234
-
235
- Halts also fire `loop:error` on the stream (`source: 'halt'`) and the `onError` callback (with a `HaltError` instance).
236
-
237
- ---
238
-
239
- ## Examples
240
-
241
- Runnable scripts in [`examples/`](examples/) — each is self-contained and the file's top docstring documents flags and required env vars.
242
-
243
- | File | What it shows |
244
- |---|---|
245
- | [`with-bareguard.mjs`](examples/with-bareguard.mjs) | End-to-end Loop + bareguard wiring: budget cap, fs scope, bash allowlist, audit log, humanChannel. The canonical governed-loop reference. |
246
- | [`mcp-bridge-poc.js`](examples/mcp-bridge-poc.js) | Auto-discover MCP servers from your IDE configs and expose them as bareagent tools. First run writes `.mcp-bridge.json` (edit to deny tools). |
247
- | [`mcp-bridge-concurrent.js`](examples/mcp-bridge-concurrent.js) | Soak test: fan out concurrent `barebrowse_browse` calls against real domains (Amazon, Wikipedia, GitHub, a dead host) and verify resilience. |
248
- | [`orchestrator/`](examples/orchestrator/) | Multi-agent dispatch via `spawn`. Three configs, one system prompt — no orchestrator class, no role types. Roles are JSON files. |
249
- | [`wake.sh`](examples/wake.sh) + [`wake.md`](examples/wake.md) | Reference cron + jq script for firing deferred actions. The runtime half of `createDeferTool` — bareagent emits, `wake.sh` fires. |
250
- | [`replay-job.js`](examples/replay-job.js) | Supervised replay POC: record a browser task once with the LLM driving, then replay against fresh snapshots with the LLM as locator-only. Falls back to full reasoning when the locator misses, and patches the trace. |
251
- | [`litectx-as-store.mjs`](examples/litectx-as-store.mjs) | Mount [litectx](https://npmjs.com/package/litectx) as the `Memory` `Store` — one-line swap from `JsonFileStore` to ranked, graph-aware recall; the host code never changes (RT-3). |
252
- | [`litectx-mcp-child.mjs`](examples/litectx-mcp-child.mjs) | Give a spawned child agent litectx's reasoning verbs as MCP tools, read-only on its own db, via `liteCtxMcpBridgeConfig` + `cfg.mcp` (RT-4). |
253
-
254
- ---
255
-
256
- ## Cross-language usage
257
-
258
- Not using Node.js? Spawn bare-agent as a subprocess from any language. Ready-made wrappers in [`contrib/`](contrib/README.md) for Python, Go, Rust, Ruby, and Java — copy one file, no package registry needed.
259
-
260
- ```python
261
- # Python — 3 lines to run an agent
262
- from bareagent import BareAgent
263
-
264
- agent = BareAgent(provider="openai", model="gpt-4o-mini")
265
- result = agent.run("What is the capital of France?")
266
- print(result["text"]) # → "The capital of France is Paris."
267
- agent.close()
268
- ```
269
-
270
- ```go
271
- // Go — same pattern
272
- agent, _ := bareagent.New("anthropic", "claude-haiku-4-5-20251001", "")
273
- result, _ := agent.Run("What is the capital of France?")
274
- fmt.Println(result.Text)
275
- agent.Close()
276
- ```
277
-
278
- ```ruby
279
- # Ruby — same pattern
280
- agent = BareAgent.new(provider: "ollama", model: "llama3.2")
281
- result = agent.run("What is the capital of France?")
282
- puts result["text"]
283
- agent.close
284
- ```
285
-
286
- All wrappers support optional event streaming for intermediate results. See [`contrib/README.md`](contrib/README.md) for Rust, Java, and full protocol reference.
287
-
288
- ---
289
-
290
- ## Production-validated
291
-
292
- | Component | Aurora (SOAR2) | Multis (assistant) |
293
- |---|:---:|:---:|
294
- | Loop | ✓ | ✓ |
295
- | Planner | ✓ | ✓ |
296
- | runPlan | ✓ | — |
297
- | Retry | ✓ | ✓ |
298
- | CircuitBreaker | — | ✓ |
299
- | Scheduler | — | ✓ |
300
- | Checkpoint | — | ✓ |
301
- | CLIPipe | ✓ | — |
302
-
303
- Aurora replaced ~400 lines of hand-rolled orchestration with ~60 lines of bare-agent wiring — zero workarounds, zero framework plumbing, 100% domain logic.
304
-
305
- For wiring recipes and API details, see the **[Integration Guide](bareagent.context.md)** (LLM-optimized). For the full human guide — usage patterns, composition examples, and what bare-agent deliberately doesn't build in (with recipes to do it yourself), see the **[Usage Guide](docs/archive/usage-guide.md)**. For error reference, see **[Error Guide](docs/product/errors.md)**. For release history, see **[CHANGELOG](CHANGELOG.md)**.
48
+ | Act | Loop | think → act → observe until done, any provider, opt-in policy/assemble/trim seams |
49
+ | Act | Planner + runPlan | break a goal into a step DAG, run steps in parallel waves |
50
+ | Act | assessComplexity | rate a goal from its text alone, no LLM — gates whether to plan |
51
+ | Act | recurse | RLM in one call — decompose → fan-out → verify → synthesize (see above) |
52
+ | Act | Memory | persist and recall across sessions — JSON, SQLite, or litectx in a one-line swap |
53
+ | Act | StateMachine | task lifecycle: pending → running → done / failed / waiting / cancelled |
54
+ | Act | Scheduler | cron or relative triggers, survives restarts |
55
+ | Act | Checkpoint | human approval gate, bring your own transport |
56
+ | Act | Spawn | fork a child agent, sharing one audit log and budget |
57
+ | Act | Defer | queue an action for a waker to fire later, governed on emit and on fire |
58
+ | Act | Retry · CircuitBreaker · Fallback | backoff with jitter, fail-fast, provider failover |
59
+ | Act | Stream · Errors | structured JSONL events, typed error hierarchy |
60
+ | Verify | Evaluator + refine | judge output by predicate, rubric, an agentic critic, or the Jev tier; refine loops generate → evaluate → regenerate |
61
+ | Verify | judge | a decisive return-time check of the result against the original request |
62
+ | Verify | remember | distill durable facts out of a finished run |
63
+ | Verify | SkillRegistry | surface extra tools on demand instead of loading them all upfront |
64
+ | Verify | stash | compact finished work out of the live context window, restorable |
65
+ | Verify | JevProvider | fast, cheap, calibrated yes/no · pick-one · score decisions — the System-1 gate (see above) |
66
+ | Govern | wireGate → bareguard | one policy, one audit log, one budget cap over every call; stops the spin on repeated denials or a stuck call |
67
+ | Hands | Browsing · Mobile · Shell · MCP Bridge | barebrowse, baremobile, cross-platform shell, and auto-discovered MCP servers — all as tools |
68
+ | Providers | OpenAI-compatible, Anthropic, Gemini, Ollama, CLIPipe, Fallback | swap freely, or bring your own with one `generate` method; CLIPipe runs the loop over a CLI subscription instead of the metered API |
69
+
70
+ ## It tells you the truth
71
+
72
+ - A cut-off, refused, or over-context answer is an error, not a success — and its half-written tool calls never run.
73
+ - A cost it can't price is `null`, never a silent `$0`.
74
+ - A dead worker or broken check returns `incomplete`, never a faked pass — and counts are done by code, not by the model.
75
+ - Stuck loops stop: repeated denials or the same failing call end the run cleanly instead of burning the budget.
306
76
 
307
77
  ## The bare ecosystem
308
78
 
@@ -331,6 +101,13 @@ mix and match, each module works standalone.
331
101
 
332
102
  **Why this exists:** Most automation stacks ship 200MB of opinions before you write a line of code. These don't. Install, import, go.
333
103
 
104
+ ## Docs
105
+
106
+ - **Integration Guide** (`bareagent.context.md`) — the complete contract: every option, the full API, wiring recipes. Ships in the package.
107
+ - **Primitives manifest** (`primitives.json`) — every primitive as machine-readable JSON. Ships in the package, generated from source.
108
+ - **[Error Guide](docs/product/errors.md)** — what each error means and how to react to it.
109
+ - **[CHANGELOG](CHANGELOG.md)** — release history. Not on Node? See [`contrib/`](contrib/README.md) for wrappers in Python, Go, Rust, Ruby, and Java.
110
+
334
111
  ## License
335
112
 
336
113
  Apache License, Version 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -19,7 +19,7 @@ Eight entry points:
19
19
  - `require('bare-agent/providers')` — OpenAI, Anthropic, Ollama, CLIPipe, Fallback (the canonical short names; `*Provider` aliases — `OpenAIProvider`, `AnthropicProvider`, etc. — are also exported and match the class names, so either destructure works, v0.12.1+)
20
20
  - `require('bare-agent/stores')` — SQLite (FTS5), JsonFile
21
21
  - `require('bare-agent/transports')` — JsonlTransport
22
- - `require('bare-agent/tools')` — createBrowsingTools, createMobileTools, createShellTools, createSpawnTool, createDeferTool, spawnChild, readDeferQueue, liteCtxMcpBridgeConfig
22
+ - `require('bare-agent/tools')` — createBrowsingTools, createMobileTools, createShellTools, resolveToolPath, createSpawnTool, createDeferTool, spawnChild, readDeferQueue, liteCtxMcpBridgeConfig
23
23
  - `require('bare-agent/mcp')` — createMCPBridge (returns `tools` + `metaTools`), discoverServers, buildMetaTools
24
24
  - `require('bare-agent/bareguard')` — wireGate (one-line bareguard Gate integration), defaultActionTranslator
25
25
 
@@ -526,6 +526,8 @@ const { policy, onToolResult } = wireGate(gate, {
526
526
 
527
527
  `onLlmResult` always uses `{type:'llm'}` regardless of the translator (so budget rules match without translator collusion). `defaultActionTranslator` is exported for composition. **A tool is NOT auto-gated by the fs/bash primitives without this translator** — e.g. `shell_write` runs the write but `fs.writeScope` only enforces once `shell_write` → `{type:'write', path}`; the default `{type:'shell_write'}` matches `tools.allow/denylist` only. Verified live: with the translator, `gate.check` ALLOWs `shell_run ["ls","/tmp"]` and DENYs `shell_read /etc/passwd` (`[deny: fs.readScope]`), and an out-of-scope `shell_write` is denied **before** `execute` (nothing touches disk).
528
528
 
529
+ > **Canonicalize the path before the gate sees it.** bareguard's `fs` primitive checks the path you hand it WITHOUT canonicalizing it — a `~`-prefixed or relative `args.path` reads `fs.readScope`/`fs.writeScope` differently than the absolute path the tool actually opens. Call `resolveToolPath` (exported from `bare-agent/tools` alongside `createShellTools`) on `args.path` in the translator — either put the result in an explicit `path` field (as the snippet above's sibling example, `examples/with-bareguard.mjs`, does) or, if relying on bareguard reading `args.path` directly, replace `args.path` with the canonicalized value before returning. Same string in, same string opened.
530
+
529
531
  **Bounding tool rounds — use `limits.maxToolRounds` (bareguard 0.4.2+), not doubled `maxTurns`.** `limits.maxTurns` ticks on every `gate.record` (LLM + tool), so an "N LLM-tool round" cap is `maxTurns: N*2`. `limits.maxToolRounds: N` ticks only on non-`llm` records and gives the natural semantic — pairs cleanly with our split `onLlmResult` / `onToolResult` (the LLM side writes `{type:'llm'}` records which the counter skips). Halt severity, same shape as `maxTurns`, rebuilt from audit on cold-start.
530
532
 
531
533
  **`HaltError` reachable from the public API (v0.10.1+).** `require('bare-agent').HaltError`, `require('bare-agent/errors').HaltError`. Adopters whose policy shim throws `HaltError` get identity-equal class across module boundaries — Loop's `instanceof HaltError` catches it cleanly.
@@ -572,7 +574,34 @@ await loop.run(messages, tools, {
572
574
  });
573
575
  ```
574
576
 
575
- For routing rules that don't fit bareguard's primitives (e.g. "owner can do anything; user can only read"), you can layer a custom closure on top of `wireGate(gate).policy` — but the cleaner pattern is one source of truth: encode the rules as bareguard primitives and let the gate evaluate them.
577
+ For routing rules that don't fit bareguard's primitives (e.g. "owner can do anything; user can only read"), you can layer a custom closure on top of `wireGate(gate).policy` — but the cleaner pattern is one source of truth: encode the rules as bareguard primitives and let the gate evaluate them. The recipe below is exactly that kind of closure, for the specific "owner bypasses everything" case.
578
+
579
+ ### Per-principal bypass (owner / admin role)
580
+
581
+ Wrap the gate policy when a principal is trusted unconditionally:
582
+
583
+ ```js
584
+ const { policy: gatePolicy } = wireGate(gate);
585
+
586
+ const policy = async (toolName, args, ctx) => {
587
+ if (ctx?.role === 'owner') return true; // bypass gate entirely
588
+ return gatePolicy(toolName, args, ctx);
589
+ };
590
+
591
+ new Loop({ provider, policy, onLlmResult, onToolResult });
592
+ ```
593
+
594
+ Bypassing the gate also bypasses audit and budget — only do this for principals you trust unconditionally. For partial trust, use ctx-aware rules inside bareguard instead.
595
+
596
+ ### Custom deny strings (localize / strip prefix)
597
+
598
+ ```js
599
+ const { policy } = wireGate(gate, {
600
+ formatDeny: (decision) => `Sorry — ${decision.reason || 'not allowed'}`,
601
+ });
602
+ ```
603
+
604
+ Halt-severity decisions bypass `formatDeny` (they throw `HaltError` and exit the loop without ever reaching the LLM).
576
605
 
577
606
  ### Catalog pre-filter (omit denied tools from the LLM's view)
578
607
 
@@ -1387,6 +1416,12 @@ Mobile tools follow the observe-act pattern: action tools auto-return a fresh sn
1387
1416
 
1388
1417
  **Zero baked-in allowlist.** The library ships the primitives; gating is bareguard's job via the standard `wireGate(gate)` wiring.
1389
1418
 
1419
+ > **Path canonicalization + symlink refusal, together.** bareguard's own resolved-path (symlink) containment check is planned for an upcoming release, not yet published; until then the gate matches paths lexically, so a symlinked parent dir is NOT contained even when the link sits inside an allowed scope. bare-agent canonicalizes the path (`~` + resolve, via `resolveToolPath`) before the gate check and opens that same path — this closes a *different* gap (the gate judging a `~`/relative path in a different form than the one the tool opens), not the lexical-match gap above. `noFollowSymlinks` refuses a symlinked final path *component* at open time. Together they narrow but do not close the check-then-open window, and neither covers hardlinks.
1420
+ >
1421
+ > **Opt-in symlink refusal:** `createShellTools({ noFollowSymlinks: true })` makes `shell_read`, `shell_write`, `shell_edit`, and `shell_grep` refuse to open a path whose **final** path component is a symlink — a file, a dir, or dangling — instead of silently following it. Refusal is a **thrown** error with `err.code === 'ELOOP'` (feeds the Loop's `maxIdenticalToolErrors` guard like any other tool error), and for `shell_write` the link's target is never created or touched. Scope is the final component **only** — a symlinked *parent* directory is not covered (per the canonicalization note above), and neither is the `cwd` option of `shell_run`/`shell_exec` — it only gets `~` expansion, never this flag or `resolveToolPath`'s full canonicalization, so a symlinked `cwd` is always followed. `shell_grep` refuses only a symlinked **root** path loudly, via the same atomic open every other tool uses (open-then-fstat, not a look-then-open `lstat`); a file that turns into a symlink partway through the walk is skipped silently, the same as any other read error. The check is atomic (`O_NOFOLLOW`) on linux/macOS; on Windows, which has no `O_NOFOLLOW`-equivalent open flag, it falls back to a non-atomic `lstat`-then-open check — a symlink planted in that gap slips through. A directory listing (`shell_read` on a dir, `shell_grep`'s root when it's a dir) additionally rechecks the path's `dev`+`ino` against the open handle right after listing — Node has no `fdopendir`, so the listing itself still goes by path, and this narrows (does not close) the window where a directory swapped in between the open and the listing would report a different directory's names; a swap-and-swap-back inside that narrow window still passes undetected. Default `false` — omitting the option is byte-identical to the pre-existing behavior.
1422
+ >
1423
+ > **`resolveToolPath(p)`** — exported alongside `createShellTools` from `bare-agent/tools`. THE canonicalizer every shell file tool applies internally before opening: expand `~`/`~/…` via `os.homedir()` (throws rather than silently degrading `~/x` to `/x` when no home directory can be determined), then `path.resolve` against `process.cwd()`. Idempotent; rejects a non-string/empty path (thrown, naming only the type/category of the bad value, never the value itself) — every file tool routes through it, so `shell_read`/`shell_grep` with `""` now error instead of silently reading/searching `process.cwd()`. Call it in your `actionTranslator` on `args.path` before returning the gated action, so the string the gate judges is the exact string the tool opens.
1424
+
1390
1425
  > **⚠️ `shell_write` requires `content` — and a gate cannot cover for it (v0.27+).** `content` used to default to `''`, so a tool call that OMITTED it silently overwrote the target with **zero bytes** and returned `"wrote 0 bytes to <path>"` as success. That is the ordinary shape of a model hitting its **output-token cap** mid-generation on a long file — observed live emptying a 1789-line source file. **No policy can catch it:** a 0-byte write is a *legal* write, and bareguard's `fs` primitive judges `{type:'write', path}` without inspecting the body (the gate correctly `allow`s it). `shell_write` now **rejects** an absent, `null`, or non-string `content` and leaves the file byte-identical; the error tells the model to retry with the full content. An explicit `content: ""` still empties the file — that one is deliberate.
1391
1426
 
1392
1427
  > **`shell_edit` — the surgical write (BA-13).** Changing one line of an 800-line file with `shell_write` forces the model to re-emit **all 800 lines** as tool-call JSON: an output-token tax ∝ file size (output is the expensive token class), paid on every revision, and the maximal broken-tree surface (a truncated rewrite mangles the lines it never meant to touch — the BA-4/BA-6 class). `shell_edit({path, oldText, newText})` emits only the anchor + replacement. Semantics worth knowing: `oldText` must match **exactly once** (a 0/2+ match is a refusal *returned as the tool result*, so the loop continues and the model widens the anchor — not a throw, so a repeated-identical miss is bounded by maxTurns/budget, not the spin guard); missing/empty `oldText` or missing/non-string `newText` **throw** (BA-4 guards; `newText:""` is a legal deletion); the write is **atomic** (sibling temp + rename, mode preserved) so an fs failure never leaves a partial file; and `newText` is a **literal splice** (a `$&`/`$1` in it lands verbatim, unlike `String.replace`). Gate it exactly like `shell_write` but as `{type:'edit'}` — **bareguard gates `edit` by the same `fs.writeScope` as `write` with zero config** (its FS primitive's `FS_TYPES` already includes `edit`).
@@ -21,7 +21,7 @@ import { createRequire } from 'node:module';
21
21
  const require = createRequire(import.meta.url);
22
22
  const { Loop, wireGate } = require('bare-agent');
23
23
  const { OpenAI } = require('bare-agent/providers');
24
- const { createShellTools } = require('bare-agent/tools');
24
+ const { createShellTools, resolveToolPath } = require('bare-agent/tools');
25
25
 
26
26
  // 1. Build the gate. Every primitive is optional with sensible defaults.
27
27
  const gate = new Gate({
@@ -50,18 +50,25 @@ await gate.init();
50
50
  // primitives: those fire only on `action.type ∈ {bash, read, write, edit}` and read `action.cmd`/`action.path`.
51
51
  // So to make the `bash.allow` + `fs.readScope` config above actually enforce, we MUST translate the shell
52
52
  // tools into those primitive shapes — otherwise the caps are silently dead (relayfact F7/BA-3).
53
+ //
54
+ // `path` below is `resolveToolPath(args.path)`, not the raw arg: bareguard's fs primitive checks the
55
+ // path you hand it WITHOUT canonicalizing it, so a `~`-prefixed or relative path would be judged
56
+ // against `fs.readScope`/`fs.writeScope` in a different form than the one the tool actually opens —
57
+ // gate string and opened string must be the SAME string. `resolveToolPath` is the one canonicalizer
58
+ // (expand `~` via os.homedir(), then path.resolve) every shell file tool already applies internally
59
+ // before opening; calling it here too means the gate sees exactly what gets opened.
53
60
  const actionTranslator = (toolName, args, ctx) => {
54
61
  switch (toolName) {
55
62
  // shell_run is argv (no shell); bareguard's bash.allow matches `cmd.startsWith(prefix)`, so join argv[0..].
56
63
  case 'shell_run': return { type: 'bash', cmd: (args?.argv || []).join(' '), args, _ctx: ctx ?? null };
57
64
  case 'shell_exec': return { type: 'bash', cmd: args?.command, args, _ctx: ctx ?? null };
58
- // shell_read / shell_grep are reads — gate them through fs.readScope.
65
+ // shell_read / shell_grep are reads — gate them through fs.readScope, on the canonicalized path.
59
66
  case 'shell_read':
60
- case 'shell_grep': return { type: 'read', path: args?.path, args, _ctx: ctx ?? null };
67
+ case 'shell_grep': return { type: 'read', path: resolveToolPath(args?.path), args, _ctx: ctx ?? null };
61
68
  // shell_write is a write — gate it through fs.writeScope (add writeScope to the Gate config to enforce).
62
- case 'shell_write': return { type: 'write', path: args?.path, args, _ctx: ctx ?? null };
69
+ case 'shell_write': return { type: 'write', path: resolveToolPath(args?.path), args, _ctx: ctx ?? null };
63
70
  // shell_edit is an anchored edit — bareguard gates {type:'edit'} by the SAME fs.writeScope as write.
64
- case 'shell_edit': return { type: 'edit', path: args?.path, args, _ctx: ctx ?? null };
71
+ case 'shell_edit': return { type: 'edit', path: resolveToolPath(args?.path), args, _ctx: ctx ?? null };
65
72
  default: return { type: toolName, args, _ctx: ctx ?? null };
66
73
  }
67
74
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.46.5",
3
+ "version": "0.47.0",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
@@ -95,7 +95,7 @@
95
95
  }
96
96
  },
97
97
  "scripts": {
98
- "test": "node --test test/**/*.test.js",
98
+ "test": "node --test test/**/*.test.js test/**/*.test.mjs",
99
99
  "test:unit": "node --test \"test/!(integration*|*mcp*|spawn*).test.js\"",
100
100
  "typecheck": "tsc --noEmit",
101
101
  "prebuild:types": "node scripts/clean-types.js",
package/primitives.json CHANGED
@@ -141,9 +141,9 @@
141
141
  "category": "tools",
142
142
  "when": "you want to give an agent shell/file tools (read, grep, write, edit, run, exec) — cross-platform, pure Node, zero deps",
143
143
  "import": "import { createShellTools } from 'bare-agent/tools'",
144
- "signature": "createShellTools() => {tools: ToolDef[]}",
145
- "fails": "never throws at creation; gating is the caller's via Loop({ policy }) and fs.writeScope, and shell_edit refuses a non-unique anchor as a tool result (file untouched).",
146
- "example": "const { tools } = createShellTools();\nconst loop = new Loop({ provider, tools, policy });"
144
+ "signature": "createShellTools(options?: {noFollowSymlinks?: boolean}) => {tools: ToolDef[]}",
145
+ "fails": "never throws at creation; gating is the caller's via Loop({ policy }) and fs.writeScope, shell_edit refuses a non-unique anchor as a tool result (file untouched), and with noFollowSymlinks:true the four file tools throw ELOOP on a symlinked final path component.",
146
+ "example": "const { tools } = createShellTools();\nconst loop = new Loop({ provider, tools, policy });\nconst { tools: safeTools } = createShellTools({ noFollowSymlinks: true });"
147
147
  },
148
148
  {
149
149
  "name": "createSpawnTool",
@@ -361,10 +361,19 @@
361
361
  "fails": "skips empty spans and never fabricates; a provider HaltError propagates clean. Each pass forwards usage via onLlmResult; a fact counts once via ctx.recordMemoryOp.",
362
362
  "example": "const { facts } = await remember(spans, { provider, store });\nconsole.log(`consolidated ${facts.length} durable facts`);"
363
363
  },
364
+ {
365
+ "name": "resolveToolPath",
366
+ "category": "tools",
367
+ "when": "you gate shell file tools — canonicalize the tool's path once with this, put it in the action you pass to `gate.check`, so the gate judges exactly the path the tool opens",
368
+ "import": "import { resolveToolPath } from 'bare-agent/tools'",
369
+ "signature": "resolveToolPath(p: string) => string",
370
+ "fails": "throws on a non-string or empty path, and on a `~` path when no home directory can be determined — never turns `~/x` into `/x` or an empty path into cwd",
371
+ "example": "const resolved = resolveToolPath('~/notes.txt');"
372
+ },
364
373
  {
365
374
  "name": "Retry",
366
375
  "category": "resilience",
367
- "when": "you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam Loop and runPlan wrap providers with",
376
+ "when": "you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam that Loop and runPlan wrap around providers",
368
377
  "import": "import { Retry } from 'bare-agent'",
369
378
  "signature": "new Retry(options?: RetryOptions)",
370
379
  "fails": "rethrows the last error once attempts are exhausted; by default only transient errors (429/5xx/ECONNRESET/ETIMEDOUT) are retried.",
package/src/retry.d.ts CHANGED
@@ -23,7 +23,7 @@ export type RetryOptions = {
23
23
  export class Retry {
24
24
  /**
25
25
  * @param {RetryOptions} [options={}]
26
- * @when you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam Loop and runPlan wrap providers with
26
+ * @when you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam that Loop and runPlan wrap around providers
27
27
  * @fails rethrows the last error once attempts are exhausted; by default only transient errors (429/5xx/ECONNRESET/ETIMEDOUT) are retried.
28
28
  * @example
29
29
  * const retry = new Retry({ maxAttempts: 3, jitter: true });
package/src/retry.js CHANGED
@@ -25,7 +25,7 @@ const DEFAULT_RETRY_ON = (err) => {
25
25
  class Retry {
26
26
  /**
27
27
  * @param {RetryOptions} [options={}]
28
- * @when you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam Loop and runPlan wrap providers with
28
+ * @when you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam that Loop and runPlan wrap around providers
29
29
  * @fails rethrows the last error once attempts are exhausted; by default only transient errors (429/5xx/ECONNRESET/ETIMEDOUT) are retried.
30
30
  * @example
31
31
  * const retry = new Retry({ maxAttempts: 3, jitter: true });
package/src/tools.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createBrowsingTools } from "../tools/browse";
2
2
  import { createMobileTools } from "../tools/mobile";
3
3
  import { createShellTools } from "../tools/shell";
4
+ import { resolveToolPath } from "../tools/shell";
4
5
  import { createSpawnTool } from "../tools/spawn";
5
6
  import { spawnChild } from "../tools/spawn";
6
7
  import { createDeferTool } from "../tools/defer";
7
8
  import { readQueue as readDeferQueue } from "../tools/defer";
8
9
  import { liteCtxMcpBridgeConfig } from "../tools/litectx-mcp";
9
- export { createBrowsingTools, createMobileTools, createShellTools, createSpawnTool, spawnChild, createDeferTool, readDeferQueue, liteCtxMcpBridgeConfig };
10
+ export { createBrowsingTools, createMobileTools, createShellTools, resolveToolPath, createSpawnTool, spawnChild, createDeferTool, readDeferQueue, liteCtxMcpBridgeConfig };
package/src/tools.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { createBrowsingTools } = require('../tools/browse');
4
4
  const { createMobileTools } = require('../tools/mobile');
5
- const { createShellTools } = require('../tools/shell');
5
+ const { createShellTools, resolveToolPath } = require('../tools/shell');
6
6
  const { createSpawnTool, spawnChild } = require('../tools/spawn');
7
7
  const { createDeferTool, readQueue: readDeferQueue } = require('../tools/defer');
8
8
  const { liteCtxMcpBridgeConfig } = require('../tools/litectx-mcp');
@@ -11,6 +11,7 @@ module.exports = {
11
11
  createBrowsingTools,
12
12
  createMobileTools,
13
13
  createShellTools,
14
+ resolveToolPath,
14
15
  createSpawnTool,
15
16
  spawnChild,
16
17
  createDeferTool,