bare-agent 0.16.1 → 0.18.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,7 +17,7 @@
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
- **Agent orchestration in ~2.7K lines of core. One required dep ([bareguard](https://npmjs.com/package/bareguard) ^0.2.0).**
20
+ **Lightweight agent orchestration. One required dep ([bareguard](https://npmjs.com/package/bareguard) ^0.9.0).**
21
21
 
22
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. Single-gate governance via bareguard: every tool call traverses one policy hook, one audit log, one budget cap.
23
23
 
@@ -62,38 +62,44 @@ and show me the wiring code.
62
62
 
63
63
  ## What's inside
64
64
 
65
- Every piece works alone — take what you need, ignore the rest.
65
+ 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.
66
+
67
+ ### Act — get work done
66
68
 
67
69
  | Component | What it does |
68
70
  |---|---|
69
- | **Loop** | Think → act → observe repeat. Calls any LLM, runs your tools, loops until done, returns estimated USD cost per run. Three opt-in seams hook external libraries in without touching your code: **`policy`** (governance — wire bareguard for one gated chokepoint over every tool call), **`assemble`** (context engineering — recall/compress/trim the window per round; the seam [litectx](https://npmjs.com/package/litectx) plugs into, transcript untouched), and **`trim`** (destructively bound the transcript for unbounded runs, harvesting turns before eviction). Each is a single chokepoint, fail-open, off by default. `onError` + `loop:error` surface every silent failure |
70
- | **Planner** | Break a goal into a step DAG via LLM. Built-in caching (`cacheTTL`) |
71
- | **assessComplexity** | Pure-code pre-planner (no LLM): rates a goal `simple`/`medium`/`complex`/`critical` from its text via keyword scoring + a critical safety override. `needsPlanning` gates whether to spend a Planner pass; `critical` flags security/production/compliance work for extra scrutiny. Free, instant, debuggable via `signals` |
72
- | **runPlan** | Execute steps in parallel waves. Dependency-aware, failure propagation, per-step retry |
73
- | **Retry** | Exponential/linear backoff with jitter. Respects `err.retryable` |
74
- | **CircuitBreaker** | Fail fast after N errors. Auto-recovers after cooldown. Per-key isolation |
75
- | **Fallback** | Try providers in order — if one is down, next one picks up. Transparent to Loop |
76
- | **Memory** | Persist and search context across turns/sessions through a swappable `Store`. Zero-dep JSON file by default, or mount [litectx](https://npmjs.com/package/litectx) for ranked, graph-aware recall in one line the host code never changes ([example](examples/litectx-as-store.mjs)). A minimal `SQLite` FTS5 store also ships, though litectx supersedes it for SQLite-backed memory |
77
- | **StateMachine** | Task lifecycle tracking with event hooks. `pending running done / failed / waiting / cancelled` |
78
- | **Checkpoint** | Human approval gate. You provide the transport terminal, Telegram, Slack, whatever |
79
- | **Scheduler** | Cron (`0 9 * * 1-5`) or relative (`2h`, `30m`). Persisted jobs survive restarts |
80
- | **Stream** | Structured event emitter. Pipe as JSONL, subscribe in-process, or custom transport |
81
- | **Errors** | Typed hierarchy `ProviderError`, `ToolError`, `TimeoutError`, `CircuitOpenError`, `ValidationError`. Halt decisions (turn cap, budget cap, content rules) come from bareguard, not Loop |
82
- | **bareguard adapter** | `wireGate(gate)` → `{ policy, onLlmResult, onToolResult, filterTools, formatDeny }`: one-line wiring to bareguard's `Gate`. Routes every LLM + tool result through the gate so budget caps cover token-heavy workloads, drops denied tools before the LLM ever sees them, and turns halts into a clean exit. `require('bare-agent/bareguard')` |
83
- | **Browsing** | Web navigation, clicking, typing, reading via `barebrowse` (17 tools). Two modes: library tools (inline snapshots, pass to Loop) or CLI session (disk-based snapshots, token-efficient for multi-step flows). Optional `assess` tool (privacy scan) when `wearehere` is installed |
84
- | **Mobile** | Android + iOS device control via `baremobile`. Same two modes: library tools (`createMobileTools` action tools auto-return snapshots) or CLI session (`baremobile` CLI — disk-based snapshots) |
85
- | **Shell** | Cross-platform `shell_read`, `shell_grep`, `shell_run` (argv, no shell), `shell_exec` (raw shell). Pure Node — no `grep`/`rg`/`findstr` dependency. Injection-proof `shell_run` for policy-gated use |
86
- | **MCP Bridge** | Auto-discover MCP servers from your $HOME/IDE configs (Claude Code, Cursor, …) and expose them as bareagent tools — bulk (`tools`) or token-thrifty meta-tools (`mcp_discover` + `mcp_invoke`) for large catalogs. Same `Loop({ policy })` hook governs MCP and native tools alike. The project-cwd `.mcp.json` is **opt-in** (untrusted-repo safety); vet every server spawn with `confirmServer`; every RPC is time-bounded. Zero deps |
87
- | **Spawn** | Fork a child bareagent as a specialist agent — LLM-callable (blocks until exit) or a library handle (`wait`, `onLine`, `kill`). The whole family stitches into one audit log + budget; `bareguard ^0.2.0` adds per-family rate + depth caps. `timeoutMs` caps wall-clock, opt-in `idleTimeoutMs` kills a child gone silent (slow-but-working children survive) |
88
- | **Defer** | Queue a `{action, when}` record for a separate waker (cron / `examples/wake.sh`) to fire later. Governed twice once when emitted, again when it fires. `bareguard ^0.2.0` adds a family-wide rate cap |
89
-
90
- **Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Ollama, CLIPipe (any CLI tool via stdin/stdout with real-time streaming), Fallback, or bring your own (one method: `generate`). All return the same shape swap freely. The OpenAI provider warns if it would send your key over plaintext `http://` to a non-loopback host (use `https`, or drop `apiKey` for keyless local endpoints).
91
-
92
- **Tools:** Any function is a tool. REST APIs, MCP servers, CLI commands, shell scripts if it's a function, it works. Built-in: `barebrowse` for web browsing, `baremobile` for Android + iOS device control (both optional) library tools for inline results or CLI session mode for token-efficient disk-based snapshots.
93
-
94
- **Cross-language:** Runs as a subprocess. Communicate via JSONL on stdin/stdout from Python, Go, Rust, Ruby, Java, or anything that can spawn a process. Ready-made wrappers in [`contrib/`](contrib/README.md).
95
-
96
- **Deps:** 1 required (`bareguard ^0.2.0` for governance single-gate policy + audit + budget + per-family rate caps). Optional: `cron-parser` (cron expressions), `better-sqlite3` (SQLite store), `barebrowse` (web browsing), `baremobile` (Android + iOS device control), `wearehere` (privacy assessment via barebrowse).
71
+ | **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) |
72
+ | **Planner** | Break a goal into a step DAG. Cached |
73
+ | **assessComplexity** | Rate a goal `simple`→`critical` from its text no LLM. Gates whether to plan |
74
+ | **runPlan** | Run plan steps in parallel waves. Dependency-aware, per-step retry |
75
+ | **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 |
76
+ | **StateMachine** | Task lifecycle: `pending running done / failed / waiting / cancelled` |
77
+ | **Scheduler** | Cron or relative triggers. Jobs survive restarts |
78
+ | **Checkpoint** | Human approval gatebring your own transport |
79
+ | **Spawn** | Fork a child agent. One shared audit log + budget |
80
+ | **Defer** | Queue an action for a waker to fire later. Governed when emitted *and* when it fires |
81
+ | **Retry · CircuitBreaker · Fallback** | Resilience: backoff with jitter, fail-fast, provider failover |
82
+ | **Stream · Errors** | Structured JSONL events; typed error hierarchy |
83
+ | **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 |
84
+ | **MCP Bridge** | Auto-discover MCP servers from your IDE configs; expose as tools (bulk or meta-tools) |
85
+
86
+ ### Verifycheck the work, keep context clean *(the eval-assist suite)*
87
+
88
+ | Component | What it does |
89
+ |---|---|
90
+ | **Evaluator + refine** | Judge output by `predicate` (no tokens), `rubric` (an isolated adversarial grader), or `agentic` (a critic that exercises the live artifact). `refine` is the bounded generate→evaluate→regenerate loop |
91
+ | **SkillRegistry** | Surface skills on demand: one meta-tool catalog; activating a skill injects its instructions and unlocks its tools |
92
+ | **stash** | Compact finished work out of the live window (restorable), or auto-fold the middle under token pressure |
93
+
94
+ **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. `require('bare-agent/bareguard')`
95
+
96
+ **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.
97
+
98
+ **Tools:** Any function is a toolREST, MCP, CLI, shell. Built-in web + mobile (optional).
99
+
100
+ **Cross-language:** Run as a subprocess; talk JSONL over stdin/stdout from Python, Go, Rust, Ruby, or Java. Wrappers in [`contrib/`](contrib/README.md).
101
+
102
+ **Deps:** 1 required (`bareguard ^0.9.0`). Optional: `cron-parser`, `better-sqlite3`, `barebrowse`, `baremobile`, `wearehere`.
97
103
 
98
104
  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/02-features/usage-guide.md).
99
105
 
@@ -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.16.1 | Node.js >= 18 | one required dep (`bareguard ^0.4.2`) | Apache 2.0
4
+ > v0.18.0 | Node.js >= 18 | one required dep (`bareguard ^0.9.0`) | 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
 
@@ -46,6 +46,10 @@ Eight entry points:
46
46
  | Retry individual plan steps | runPlan({ stepRetry }) |
47
47
  | Use a CLI tool as an LLM provider | CLIPipe |
48
48
  | Health-check provider, store, and tools | Loop.validate() |
49
+ | Verify an agent's output (judge / grade / critic) | Evaluator + refine — `predicate` / `rubric` / `agentic` criteria |
50
+ | Offer skills on demand without bloating context | SkillRegistry — `skill_use` meta-tool + `skills.activeTools` thunk |
51
+ | Keep the context window lean (compact finished sub-tasks) | createStashSkill — register the skill + wire its `trim` into `Loop({ trim })` |
52
+ | Consolidate finished work into durable facts (across runs) | remember — distill harvested spans → write through any `Store` socket |
49
53
  | Track cost per run | Automatic — `result.cost` and `loop:done` event |
50
54
  | Catch typed errors programmatically | ProviderError, ToolError, TimeoutError, CircuitOpenError |
51
55
  | Cache identical planner calls | Planner({ cacheTTL: 60000 }) |
@@ -142,6 +146,61 @@ const loop = new Loop({
142
146
  });
143
147
  ```
144
148
 
149
+ ## Wiring with remember (consolidate harvested spans → durable facts)
150
+
151
+ `remember` is the consolidation pass: it distills the spans `stash` folded out of the live transcript into durable facts and writes them through the **same `Store` socket** Memory uses (`store/search/get/delete`) — so it works with any backend (JsonFile, SQLite, litectx, or your own), with no backend coupling. One cheap LLM pass per span keeps only durable signal (decisions, config, identifiers, stable prefs) and drops chatter, superseded values, and unanswered questions. It composes *around* the Loop — never inside it.
152
+
153
+ ```javascript
154
+ const { remember, Memory } = require('bare-agent');
155
+ const { Anthropic } = require('bare-agent/providers');
156
+ const { JsonFile } = require('bare-agent/stores');
157
+
158
+ const memory = new Memory({ store: new JsonFile({ path: './facts.json' }) });
159
+
160
+ // `spans` are the harvested chunks stash evicted (lossless parks), or any finished-work transcript text.
161
+ const { facts } = await remember(spans, {
162
+ provider: new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-haiku-4-5' }),
163
+ store: memory, // writes each fact as { kind: 'fact', ... } via memory.store()
164
+ contract: 'only architecture + config decisions count as durable', // optional: steer "durable"
165
+ ctx, // optional: counts each write into result.metrics.memory.facts (disjoint from `stored`)
166
+ onLlmResult, // optional: forward distill-pass usage to a wired bareguard gate
167
+ });
168
+ ```
169
+
170
+ Budget visibility carries through `onLlmResult` (mirror of Evaluator); a governance `HaltError` from the provider propagates clean. Cheap by design — a small model and one pass per span.
171
+
172
+ **Security:** facts are model output over *untrusted* transcript content written to durable memory. The distiller refuses a direct "record this fact" injection (validated live), but treat recalled facts as untrusted **context**, not authority — gate them like any model output before a privileged action.
173
+
174
+ ## Wiring with Skills + Stash (progressive disclosure + compaction)
175
+
176
+ Skills are operator-registered `{ name, description, instructions, tools }` bundles surfaced on demand: only a one-liner per skill sits in context until the agent calls `skill_use({ name })`, which injects the skill's instructions and unlocks its (namespaced) tools for the next round. Pass `skills.activeTools` (a bound thunk) as the Loop's `tools` — it is re-evaluated each round, so freshly-unlocked tools appear automatically. The gate still governs every unlocked tool; skills change discovery, not authorization.
177
+
178
+ The shipped reference skill is **stash** — compaction-first context hygiene. Its `trim` wires into `Loop({ trim })` and folds finished sub-tasks out of the live transcript (restorable), keeping long runs under budget. Pass a litectx instance as `ctx` for lossless verbatim parking + the `ctx.summarize` lossy path; set `compaction.ceilingTokens` to enable automatic token-pressure folding.
179
+
180
+ ```javascript
181
+ const { Loop, SkillRegistry, createStashSkill } = require('bare-agent');
182
+ const { Anthropic } = require('bare-agent/providers');
183
+
184
+ const { skill, trim } = createStashSkill({
185
+ // optional auto-compaction: fold the middle when measured input tokens cross 70% of the ceiling
186
+ compaction: { ceilingTokens: 150000, triggerAt: 0.7, strategy: 'summarize' },
187
+ });
188
+
189
+ const skills = new SkillRegistry();
190
+ skills.register(skill); // catalog now lists "stash" in skill_use's description
191
+
192
+ const loop = new Loop({
193
+ provider: new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }),
194
+ trim, // stash's fold runs here, at each round boundary
195
+ });
196
+
197
+ // Pass the bound thunk as tools; ctx carries litectx (lossless parking + summarizer).
198
+ await loop.run(messages, skills.activeTools, { ctx });
199
+ // The model: skill_use({name:'stash'}) → stash_checkpoint({label}) → …work… → stash_compact({label, reason})
200
+ ```
201
+
202
+ Folds are provider-safe by construction (whole rounds, alternation-preserving note pairs) and confirmed on the real Anthropic wire. Governance, budget visibility (`ctx.summarize` tokens forward to the gate), and `result.metrics.context.compactions` all flow through unchanged.
203
+
145
204
  ## Multi-agent: spawn + defer + wake (v0.9)
146
205
 
147
206
  Three primitives, no framework. The "always-on" feeling of multi-agent
@@ -257,7 +316,9 @@ $HOME/IDE configs — NOT the project-cwd `./.mcp.json`** (v0.16.1): a checked-i
257
316
  config in an untrusted repo would otherwise auto-spawn arbitrary commands. To
258
317
  include the project config, pass `createMCPBridge({ includeProjectConfig: true })`,
259
318
  or a `confirmServer` hook (which implies it, since the hook vets every command).
260
- Explicitly-passed `configPaths` are honored verbatim. Pass `confirmServer(name,
319
+ Explicitly-passed `configPaths` are honored verbatim. So you're not left guessing,
320
+ discovery logs a one-line hint (v0.16.2) when a `./.mcp.json` is present but skipped —
321
+ naming the opt-in — rather than silently ignoring it. Pass `confirmServer(name,
261
322
  def) => boolean` to approve each server **before its command is spawned** (return
262
323
  `false` to skip it; a throw fails closed). When no `confirmServer` is set, the
263
324
  bridge still trusts all *discovered* servers and prints a one-time warning naming
package/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { Loop } from "./src/loop";
2
2
  import { Planner } from "./src/planner";
3
+ import { Evaluator } from "./src/evaluator";
4
+ import { refine } from "./src/refine";
5
+ import { remember } from "./src/remember";
3
6
  import { assessComplexity } from "./src/complexity";
7
+ import { isCritical } from "./src/complexity";
8
+ import { SkillRegistry } from "./src/skills";
9
+ import { createStashSkill } from "./src/stash";
4
10
  import { StateMachine } from "./src/state";
5
11
  import { Scheduler } from "./src/scheduler";
6
12
  import { Checkpoint } from "./src/checkpoint";
@@ -23,4 +29,4 @@ import { TimeoutError } from "./src/errors";
23
29
  import { ValidationError } from "./src/errors";
24
30
  import { CircuitOpenError } from "./src/errors";
25
31
  import { HaltError } from "./src/errors";
26
- export { Loop, Planner, assessComplexity, StateMachine, Scheduler, Checkpoint, Memory, Stream, Retry, runPlan, CircuitBreaker, wireGate, defaultActionTranslator, toUnits, fromUnits, unitAssembler, unitTrimmer, harvestKey, BareAgentError, ProviderError, ToolError, TimeoutError, ValidationError, CircuitOpenError, HaltError };
32
+ export { Loop, Planner, Evaluator, refine, remember, assessComplexity, isCritical, SkillRegistry, createStashSkill, StateMachine, Scheduler, Checkpoint, Memory, Stream, Retry, runPlan, CircuitBreaker, wireGate, defaultActionTranslator, toUnits, fromUnits, unitAssembler, unitTrimmer, harvestKey, BareAgentError, ProviderError, ToolError, TimeoutError, ValidationError, CircuitOpenError, HaltError };
package/index.js CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  const { Loop } = require('./src/loop');
4
4
  const { Planner } = require('./src/planner');
5
- const { assessComplexity } = require('./src/complexity');
5
+ const { Evaluator } = require('./src/evaluator');
6
+ const { refine } = require('./src/refine');
7
+ const { remember } = require('./src/remember');
8
+ const { assessComplexity, isCritical } = require('./src/complexity');
9
+ const { SkillRegistry } = require('./src/skills');
10
+ const { createStashSkill } = require('./src/stash');
6
11
  const { StateMachine } = require('./src/state');
7
12
  const { Scheduler } = require('./src/scheduler');
8
13
  const { Checkpoint } = require('./src/checkpoint');
@@ -26,7 +31,13 @@ const {
26
31
  module.exports = {
27
32
  Loop,
28
33
  Planner,
34
+ Evaluator,
35
+ refine,
36
+ remember,
29
37
  assessComplexity,
38
+ isCritical,
39
+ SkillRegistry,
40
+ createStashSkill,
30
41
  StateMachine,
31
42
  Scheduler,
32
43
  Checkpoint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "LICENSE",
14
14
  "NOTICE"
15
15
  ],
16
- "description": "Lightweight, composable agent orchestration for autonomous agents. Multi-agent primitives (spawn, defer, MCP meta-tools), single-gate governance via bareguard, cross-platform shell tools, MCP bridge. ~2.7K lines core, one required dep.",
16
+ "description": "Lightweight, composable agent orchestration for autonomous agents. Multi-agent primitives (spawn, defer, MCP meta-tools), single-gate governance via bareguard, cross-platform shell tools, MCP bridge. Lightweight core, one required dep.",
17
17
  "license": "Apache-2.0",
18
18
  "author": "hamr0",
19
19
  "repository": {
@@ -75,7 +75,7 @@
75
75
  "governance"
76
76
  ],
77
77
  "dependencies": {
78
- "bareguard": "^0.4.2"
78
+ "bareguard": "^0.9.0"
79
79
  },
80
80
  "optionalDependencies": {
81
81
  "barebrowse": "^0.5.0",
@@ -91,7 +91,8 @@
91
91
  }
92
92
  },
93
93
  "scripts": {
94
- "test": "node --test test/**/*.test.js",
94
+ "test": "node --test --test-force-exit test/**/*.test.js",
95
+ "test:unit": "node --test --test-force-exit \"test/!(integration*|*mcp*|spawn*).test.js\"",
95
96
  "typecheck": "tsc --noEmit",
96
97
  "prebuild:types": "node scripts/clean-types.js",
97
98
  "build:types": "tsc",
@@ -139,18 +139,36 @@ function wireGate(gate, options = {}) {
139
139
  * @param {string|null} [arg.model]
140
140
  * @param {string|null} [arg.provider]
141
141
  * @param {Usage} [arg.usage]
142
- * @param {number} [arg.costUsd]
142
+ * @param {number|null} [arg.costUsd]
143
+ * @param {('priced'|'unpriced')} [arg.pricing] - The meter's price verdict, forwarded VERBATIM
144
+ * (never synthesized here). Absent (older meter) ⇒ forwarded as undefined; bareguard's
145
+ * back-compat treats an absent flag as priced, keeping the contract back-compatible.
143
146
  * @param {number|null} [arg.durationMs]
144
147
  * @param {Ctx} [arg.ctx]
145
148
  */
146
- const onLlmResult = async ({ model, provider, usage, costUsd, durationMs, ctx }) => {
149
+ const onLlmResult = async ({ model, provider, usage, costUsd, pricing, durationMs, ctx }) => {
147
150
  // LLM rounds bypass actionTranslator — they always use the canonical
148
151
  // {type:'llm'} action so budget rules can match without translator collusion.
149
152
  await gate.record(
150
153
  { type: 'llm', args: { model: model || null, provider: provider || null }, _ctx: ctx ?? null },
151
154
  {
152
- costUsd: typeof costUsd === 'number' ? costUsd : 0,
153
- tokens: (usage?.inputTokens || 0) + (usage?.outputTokens || 0),
155
+ // Forward costUsd AS-IS a null (unpriced) cost must NOT coerce to 0 here. Coercing it
156
+ // tells the gate the round was "free" instead of "couldn't price", so an active
157
+ // budget.maxCostUsd cap silently accrues zero and never halts — the #3 silent-zero class
158
+ // (§3.7), reproduced on the adapter.
159
+ costUsd: costUsd ?? null,
160
+ // Forward the meter's price verdict VERBATIM — never synthesize it. bareguard treats an
161
+ // explicit pricing:'unpriced' as the SOLE trigger for the unpriced contract; a null cost
162
+ // WITHOUT pricing deliberately stays on bareguard's back-compat (?? 0 ⇒ priced) path. loop.js
163
+ // always sets pricing (loop.js:566), so faithful forwarding arms the contract on every real
164
+ // round. Manufacturing 'unpriced' from a bare null here would arm cases the gate intentionally
165
+ // leaves unarmed — the exact mirror of bareguard's budget-pricing round-trip. (§3.8 contract.)
166
+ pricing,
167
+ // ALL FOUR token tiers, not just input+output — cache read/creation are real consumed
168
+ // tokens; omitting them undercounts the gate's token axis on a cached run (L7). The token
169
+ // axis stays enforceable even when pricing is 'unpriced' — only the cost axis goes unknown.
170
+ tokens: (usage?.inputTokens || 0) + (usage?.outputTokens || 0)
171
+ + (usage?.cacheReadTokens || 0) + (usage?.cacheCreationTokens || 0),
154
172
  durationMs: durationMs ?? null,
155
173
  },
156
174
  );
@@ -29,3 +29,15 @@ export type ComplexityResult = {
29
29
  * @returns {ComplexityResult}
30
30
  */
31
31
  export function assessComplexity(prompt: string): ComplexityResult;
32
+ /**
33
+ * The durable critical-safety floor, standalone. Returns `true` when a goal touches high-stakes
34
+ * work (security/production/compliance/financial incidents, or a securing action on a sensitive
35
+ * domain) — the same deterministic override `assessComplexity` applies, exported on its own so a
36
+ * consumer can gate extra scrutiny (e.g. the Evaluator's adversarial verification, Feature 1)
37
+ * WITHOUT pulling in the frozen 3-tier scorer. Deterministic, auditable, testable — not a
38
+ * capability estimate. Normalizes input (trims, length-caps, lowercases) then applies the override;
39
+ * non-string / blank input is `false`.
40
+ * @param {string} prompt - The goal to test.
41
+ * @returns {boolean}
42
+ */
43
+ export function isCritical(prompt: string): boolean;
package/src/complexity.js CHANGED
@@ -16,6 +16,15 @@
16
16
  * and two calibrated thresholds. It is a heuristic — transparent and debuggable via `signals`, not
17
17
  * a model. On the upstream validation corpus it lands ~89% (the fuller LLM-free original ~95%);
18
18
  * the gap is long-tail ambiguity ("add a button" is genuinely context-dependent).
19
+ *
20
+ * FROZEN (eval-assist Feature 4, do not re-litigate). The 3-tier scorer is deletable scaffolding —
21
+ * the exact input-side routing a stronger model does better — so it is kept optional but NOT grown:
22
+ * **do not extend the keyword lists or add tiers.** The durable half is the deterministic critical
23
+ * override (`isCritical`), a safety FLOOR for high-stakes work (security/production/compliance/
24
+ * financial), exported standalone so a consumer can gate adversarial verification on it (the
25
+ * Evaluator, Feature 1) without the scorer. Adding critical keywords reopens the same treadmill the
26
+ * scorer froze, so the override list is frozen too. Input-side LLM subgoal-decomposition is a
27
+ * deliberate NO-BUILD (mechanical anchoring + cascading errors); the Planner is the durable form.
19
28
  */
20
29
 
21
30
  const has = (/** @type {Set<string>} */ words, /** @type {Set<string>} */ set) =>
@@ -35,8 +44,12 @@ const CRIT_ACTIONS = ['fix', 'patch', 'investigate', 'secure', 'protect', 'mitig
35
44
  const FINANCIAL = ['payment', 'transaction', 'billing', 'financial'];
36
45
  const SECURE_ACTS = ['encrypt', 'secure', 'protect', 'audit'];
37
46
 
38
- /** @param {string} s lowercased prompt */
39
- function isCritical(s) {
47
+ /**
48
+ * Internal: the critical override over an already-lowercased string. Public callers use
49
+ * {@link isCritical}, which normalizes first; `assessComplexity` calls this on its lowered text.
50
+ * @param {string} s lowercased prompt
51
+ */
52
+ function isCriticalLower(s) {
40
53
  if (hasAny(s, CRIT_INCIDENT) || hasAny(s, CRIT_COMPLIANCE)) return true;
41
54
  if (hasAny(s, SEC_CONTEXT) && hasAny(s, CRIT_ACTIONS)) return true; // e.g. "fix security ..."
42
55
  if (hasAny(s, FINANCIAL) && hasAny(s, SECURE_ACTS)) return true; // e.g. "encrypt payment ..."
@@ -83,7 +96,7 @@ function assessComplexity(prompt) {
83
96
  }
84
97
  const text = prompt.trim().slice(0, MAX_ASSESS_LEN);
85
98
  const lower = text.toLowerCase();
86
- if (isCritical(lower)) {
99
+ if (isCriticalLower(lower)) {
87
100
  return { level: 'critical', score: 100, needsPlanning: true, signals: ['critical_override'] };
88
101
  }
89
102
 
@@ -146,4 +159,20 @@ function assessComplexity(prompt) {
146
159
  return { level, score, needsPlanning: level !== 'simple', signals };
147
160
  }
148
161
 
149
- module.exports = { assessComplexity };
162
+ /**
163
+ * The durable critical-safety floor, standalone. Returns `true` when a goal touches high-stakes
164
+ * work (security/production/compliance/financial incidents, or a securing action on a sensitive
165
+ * domain) — the same deterministic override `assessComplexity` applies, exported on its own so a
166
+ * consumer can gate extra scrutiny (e.g. the Evaluator's adversarial verification, Feature 1)
167
+ * WITHOUT pulling in the frozen 3-tier scorer. Deterministic, auditable, testable — not a
168
+ * capability estimate. Normalizes input (trims, length-caps, lowercases) then applies the override;
169
+ * non-string / blank input is `false`.
170
+ * @param {string} prompt - The goal to test.
171
+ * @returns {boolean}
172
+ */
173
+ function isCritical(prompt) {
174
+ if (typeof prompt !== 'string' || !prompt.trim()) return false;
175
+ return isCriticalLower(prompt.trim().slice(0, MAX_ASSESS_LEN).toLowerCase());
176
+ }
177
+
178
+ module.exports = { assessComplexity, isCritical };
@@ -0,0 +1,147 @@
1
+ export type Provider = import("../types").Provider;
2
+ export type ToolDef = import("../types").ToolDef;
3
+ /**
4
+ * The uniform outcome of an evaluation, across every criteria type.
5
+ *
6
+ * Tri-state `status` mirrors Anthropic Managed Agents "Outcomes" (`satisfied` /
7
+ * `needs_revision` / `failed`) — the distinction matters to `refine`: `needs_revision`
8
+ * is retryable, `failed` is terminal (stop spending). `pass` is derived (`status ===
9
+ * 'satisfied'`) so a boolean consumer never has to special-case the enum.
10
+ */
11
+ export type Verdict = {
12
+ /**
13
+ * - Tri-state outcome.
14
+ */
15
+ status: "satisfied" | "needs_revision" | "failed";
16
+ /**
17
+ * - Derived: `status === 'satisfied'`.
18
+ */
19
+ pass: boolean;
20
+ /**
21
+ * - 0–10 for the rubric path; null for predicate (pass/fail only).
22
+ */
23
+ score: number | null;
24
+ /**
25
+ * - Why it failed / what to improve. '' when satisfied with no notes.
26
+ */
27
+ critique: string;
28
+ /**
29
+ * - Concrete fixes (rubric may populate; [] otherwise).
30
+ */
31
+ suggestions: string[];
32
+ };
33
+ export type EvaluatorOptions = {
34
+ /**
35
+ * - LLM provider — REQUIRED for the rubric and agentic paths; predicate needs none.
36
+ */
37
+ provider?: import("../types").Provider | undefined;
38
+ /**
39
+ * - Override the adversarial grader system prompt (rubric path).
40
+ */
41
+ prompt?: string | undefined;
42
+ /**
43
+ * - Override the adversarial tool-running critic system prompt (agentic path).
44
+ */
45
+ agenticPrompt?: string | undefined;
46
+ /**
47
+ * - The critic's SCOPED functional tools (`barebrowse`/`baremobile`) for the
48
+ * agentic path — what lets it exercise the live artifact rather than read text. Overridable per call via
49
+ * `EvaluateOptions.tools`. Ignored by predicate/rubric.
50
+ */
51
+ tools?: import("../types").ToolDef[] | undefined;
52
+ };
53
+ export type Criteria = {
54
+ /**
55
+ * - Deterministic check, no tokens.
56
+ */
57
+ predicate?: ((result: any) => boolean | Promise<boolean>) | undefined;
58
+ /**
59
+ * - Natural-language grading criteria an LLM scores. Exactly one of predicate|rubric|agentic.
60
+ */
61
+ rubric?: string | undefined;
62
+ /**
63
+ * - Instructions for a tool-running critic (D9): how to EXERCISE the live artifact
64
+ * (open it, click, read console/network) and what would make it fail. Runs an ISOLATED Loop with the scoped
65
+ * `tools`. The strongest verification — catches what only running the thing reveals. Exactly one of the three.
66
+ */
67
+ agentic?: string | undefined;
68
+ /**
69
+ * - The shared, authoritative "definition of done" the grader judges against
70
+ * (A3 / D10). When present it is what success means — not the loose goal. Folded into the rubric/agentic prompt.
71
+ */
72
+ contract?: string | undefined;
73
+ };
74
+ export type EvaluateOptions = {
75
+ /**
76
+ * - Budget hook.
77
+ * Judge-call tokens are real spend; forward them to the gate (BA1 lineage) so they count against budget and
78
+ * are never invisible. For the agentic path EVERY critic round forwards here (re-tagged `kind:'evaluate'`).
79
+ * A `HaltError` thrown here propagates as a clean governance exit. Wire `wireGate`'s.
80
+ */
81
+ onLlmResult?: ((payload: {
82
+ usage: any;
83
+ model: string | null;
84
+ kind: "evaluate";
85
+ }) => any) | undefined;
86
+ /**
87
+ * - Per-call override of the agentic critic's scoped tools (else `EvaluatorOptions.tools`).
88
+ */
89
+ tools?: import("../types").ToolDef[] | undefined;
90
+ /**
91
+ * - bareguard `policy` forwarded to the agentic critic's Loop — a tool-running
92
+ * critic MUST be bounded (turn/budget caps come from the gate; the Loop's HARD_ROUND_LIMIT is only a net).
93
+ */
94
+ policy?: Function | undefined;
95
+ };
96
+ /**
97
+ * Output-side judge — the mirror of `Planner` (input-side). Judges whether a result meets a goal, by a
98
+ * deterministic `predicate`, an LLM `rubric`, or a tool-running `agentic` critic, returning one uniform
99
+ * `Verdict`. The rubric and agentic paths run an ISOLATED adversarial critic (separate context + independent
100
+ * system prompt) — that isolation, not a feedback knob, is what defeats the self-evaluation trap. The agentic
101
+ * path additionally EXERCISES the artifact with scoped tools (it does not read the diff). Composes AROUND a
102
+ * Loop (never inside `loop.js`).
103
+ *
104
+ * Built flagged-and-deletable per D11 — opt-in by import; calibrate the rubric/prompt from execution traces.
105
+ */
106
+ export class Evaluator {
107
+ /** @param {EvaluatorOptions} [options] */
108
+ constructor(options?: EvaluatorOptions);
109
+ provider: import("../types").Provider | null;
110
+ prompt: string;
111
+ agenticPrompt: string;
112
+ tools: import("../types").ToolDef[];
113
+ /**
114
+ * Judge `result` against `goal` by exactly one criteria type.
115
+ * @param {string} goal - The objective the result is judged against.
116
+ * @param {any} result - The output under judgment.
117
+ * @param {Criteria} criteria - Exactly one of `predicate` | `rubric` | `agentic` (none/more-than-one throws).
118
+ * @param {EvaluateOptions} [opts]
119
+ * @returns {Promise<Verdict>}
120
+ * @throws {ValidationError} not-exactly-one criteria supplied, or rubric/agentic requested with no provider.
121
+ */
122
+ evaluate(goal: string, result: any, criteria: Criteria, opts?: EvaluateOptions): Promise<Verdict>;
123
+ /**
124
+ * Agentic path — run an ISOLATED tool-running critic Loop that exercises the artifact, then parse its
125
+ * final text into a `Verdict`. Isolation is by construction: a brand-new Loop with its own message array
126
+ * and the harsh `agenticPrompt` system prompt — a separate context window, never the generator's
127
+ * transcript (A1/D8). Budget visibility: every critic round forwards to `onLlmResult` (re-tagged
128
+ * `kind:'evaluate'`). A budget HALT during investigation re-throws as a clean `HaltError` (governance
129
+ * exit) so `refine` stops spending rather than misreading it as a verdict.
130
+ * @param {string} goal
131
+ * @param {any} result
132
+ * @param {string} instructions - The `agentic` criteria string: how to exercise the artifact.
133
+ * @param {string|null} contract
134
+ * @param {EvaluateOptions} opts
135
+ * @returns {Promise<Verdict>}
136
+ * @throws {ValidationError} no provider, or the critic loop errored / produced no parseable verdict.
137
+ * @throws {HaltError} a governance cap halted the critic mid-run.
138
+ */
139
+ _evaluateAgentic(goal: string, result: any, instructions: string, contract: string | null, opts: EvaluateOptions): Promise<Verdict>;
140
+ /**
141
+ * Defensive JSON parse of a grader response into a `Verdict` (mirrors `Planner._parse`).
142
+ * @param {string} text
143
+ * @returns {Verdict}
144
+ * @throws {ValidationError} when no JSON object can be recovered (a `refine` loop can catch to abort).
145
+ */
146
+ _parse(text: string): Verdict;
147
+ }