Skip to content

v0.33.0 — CLIPipe native MCP tool mode (BA-16)#24

Merged
hamr0 merged 4 commits into
mainfrom
feat/ba16-native-mcp-tools
Jul 21, 2026
Merged

v0.33.0 — CLIPipe native MCP tool mode (BA-16)#24
hamr0 merged 4 commits into
mainfrom
feat/ba16-native-mcp-tools

Conversation

@hamr0

@hamr0 hamr0 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

What

Native CLIPipe tool mode (toolProtocol:'claude-mcp') — drive an agentic Loop over a Claude CLI subscription at ~$0.006/turn instead of the v0.32.0 emulation's $0.25–0.55/round. The CLI runs its own multi-turn session and executes the caller's tools natively over a unix-socket MCP bridge that calls back into in-process closures.

Because the CLI owns the inner cycle, governance moves to the tools/call bridge:

  • the same policy chokepoint as Loop({policy}) → identical audit-row shape, zero gate changes
  • BA-11 deny-streak + BA-12 identical-error guards, same 3/3 triggers
  • turn bound via --max-turnsnamed stop error:'max_turns'
  • onTurn streams per-turn usage (4 cache tiers), one closing session event with the authoritative cost; the Loop skips its own forward when wired (billed once)

Honest accounting: GenerateResult.session + metrics.sessionTurns report the real turn/tool count; a dead bridge (which still ends subtype:'success') is caught parent-side by attempted-vs-served and error-tagged bridge-failed. assemble/trim/cacheMessages and a Loop-level policy throw at construction (no silently-dead knobs). Emulation retained for MCP-less CLIs.

Claim is cost (measured); output-quality parity is deliberately unminted (n=2).

Verification

  • Suite 966 pass / 0 fail / 2 skipped; typecheck + build:types clean
  • /security clean (argv spawn no-shell, 0600 unix socket / 0700 dir, no bridge logging, clamped diagnostics)
  • /code-review medium → 1 real bug (async-framing data loss) fixed + mutation-proved; 3 items validated
  • Live: acceptance script + fence check (criterion 3) + detector-live proof — all GREEN

🤖 Generated with Claude Code

https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92

hamr0 and others added 4 commits July 21, 2026 13:56
…subscription (BA-16)

The claude CLI has a real tool channel; v0.32.0's envelope emulation was built
as if it did not. `toolProtocol:'claude-mcp'` runs ONE CLI session per call and
exposes the caller's tools to it as an MCP server whose handlers call back into
the caller's in-process closures over a unix-socket bridge. The CLI owns the
inner cycle and caches its transcript session-side.

The claim is COST, measured: emulation re-sends the whole transcript every round
($0.25-0.55/round on a real ~40-round job transcript) vs a native session that
caches session-side (~$0.006/turn). Output-quality parity is deliberately
UNMINTED (n=2 suggestive evidence) — not sold as a capability win (BA-7 precedent).
Emulation is retained, not retired — still right for a CLI with no MCP support.

The CLI owning the turns means the Loop's per-round machinery can't run on them,
so governance moves to the `tools/call` bridge (every call crosses it):
- the SAME `policy(tool,args,ctx)` chokepoint (identical audit-row shape, zero
  gate changes); a deny is an advisory tool result, the handler never runs
- BA-11 deny-streak + BA-12 identical-tool-error guards, same 3/3 narrowest triggers
- the turn bound via `--max-turns`, NAMED stop `error:'max_turns'`, never a silent success
- `onTurn` STREAMS per-turn usage (four cache tiers) as it arrives then one closing
  session event with the authoritative cost; the Loop skips its own forward when wired
  (billed once, never starved)

Honest accounting (the BA-4/5/6/13 optimistic-rounding class, caught pre-build):
a dead bridge still ends `subtype:'success'`, so bridge health is tracked
parent-side (attempted vs served tool calls) and error-tags the run `bridge-failed`.
`Provider.ownsCycle` makes the Loop THROW at construction on any option it could
never honor — assemble/trim/cacheMessages and a Loop-level policy (a fence
silently not there) — no silently-dead knobs. `GenerateResult.session` +
`metrics.sessionTurns` report the real turn/tool count so a 14-turn session never
reads as one round.

The bridge is a unix socket (0600 in a 0700 dir), never a listening port. The
fence (`--tools '' --strict-mcp-config --setting-sources ''`) is set by the mode,
not the caller. Diagnostics from unknown-shape throws are allowlist-clamped
(F16/BA-1 audit-leak class).

+47 offline tests, every load-bearing guard mutation-proved in both directions
(remove it -> red; make it always-fire -> also red). Live verify-shipped through
a real Loop on a real session: poc/ba16-native-shipped.mjs (all green). Suite
963 tests / 961 pass / 0 fail / 2 skipped; typecheck clean.

Note: `--max-turns` is undocumented in the claude CLI's --help (2.1.216) though
accepted — the live check is the only tripwire if it is ever renamed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
…d the line buffer (code-review)

/code-review medium on the BA-16 diff caught a real latent data-loss bug in
`runSession`. The stdout handler was `async` and `await`ed `onTurn` INSIDE its
line-parse loop, which mutated a shared `buf`. While that await was suspended, the
next `'data'` event re-entered the handler and sliced `buf` under the suspended
parse — dropping, duplicating, or mangling lines (lost per-turn usage, a missed
`result` event, a miscounted attempted-vs-served bridge check).

Invisible with a synchronous `onTurn` (the await resolves on the microtask queue
before the next macrotask `'data'` event), which is why the live verify-shipped
passed — its onTurn was a plain push. REACHABLE the moment onTurn does real async
work: a wired gate's `gate.record`, the actual intended consumer.

Fix: extract `createSessionStream` — framing is now purely synchronous (never
awaits), and per-turn `onTurn` forwards run through a SERIAL async drainer in
arrival order; `done()` flushes the queue before resolving so a session that ends
with a forward pending still surfaces that turn's spend (F12/F18). Extracting it
also made the parser unit-testable without spawning the CLI.

+6 regression tests (async onTurn + chunks split across boundaries, in-order
delivery, split-line framing, HaltError-in-drain, malformed-line skip); the
serial-drain guard is mutation-proved load-bearing. Suite 968 / 966 pass / 0 fail;
typecheck clean; live verify-shipped re-run all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
…leanup)

Validated all three deferred findings from the medium review; fixed the ones that
passed as real, with no regression.

- Broken-bridge detector (was: true-positive only unit-tested): the risk was never
  a false positive — it was that if the REAL stream-json tool_use shape differed
  from what `createSessionStream` counts, `attempted` would be 0 on every run and
  the detector would be silent dead code. Closed by capturing real stream-json from
  a live native session (poc/ba16-detector-live.mjs): blocks arrive as
  `{type:'tool_use', name:'mcp__bareagent__lookup_code'}`, attempted===served on a
  healthy run — the counter is wired to reality (a dead bridge → attempted>served
  → bridge-failed). No code change; a real validation gap now has a durable proof.

- `state.calls`: validated as DEAD state — written twice, read nowhere (not by
  shipped code, not by tests). Removed (declaration + both pushes), not made
  "consistent" — every line must have a purpose.

- Parallel tool-call races on the guard counters: validated as NOT a defect — the
  affected counters are advisory bounds whose sequential-retry-spin precondition
  does not hold for simultaneous calls, and the worst case is benign + gate-bounded.
  No fix; documented on handleCall so it is not re-flagged.

Suite 968 / 966 pass / 0 fail; typecheck clean; detector-live re-run GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
Native `toolProtocol:'claude-mcp'` runs one CLI session per call and executes
the caller's tools over an MCP bridge back into in-process closures — the CLI
owns the cycle, governance (policy/guards/turn-bound) rides the provider,
per-turn usage streams, and honest accounting keeps a multi-turn session from
reading as one round. Emulation (v0.32.0) retained for MCP-less CLIs; the claim
is cost (measured), behaviour parity deliberately unminted.

Includes the code-review fix (serial session-stream framing — async onTurn no
longer corrupts the line buffer) and the dead-`state.calls` cleanup.

- version bumped in package.json, package-lock.json, bareagent.context.md
- CHANGELOG 0.33.0; PRD §22 decision entry; README two-mode note
- build:types JSDoc type-contract fix (Provider.policy signature)

Gates: tests 966 pass / 0 fail / 2 skipped; typecheck + build:types clean;
/security clean (argv spawn, 0600 unix socket, no bridge logging); /diff-review
via /code-review medium (1 bug fixed, 3 items validated). Live verify-shipped +
fence + detector-live all GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
@hamr0
hamr0 merged commit a93ff94 into main Jul 21, 2026
2 checks passed
@hamr0
hamr0 deleted the feat/ba16-native-mcp-tools branch July 21, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant