- P1 (Greptile): a tool-use break-early turn released the warm connection
(inUse=false) while conn.prompt() was still pending, letting the next turn
launch a concurrent prompt on the same ACP session (protocol corruption).
keepWarm now requires !sawToolCall, so a tool-use turn tears the connection
down like the non-reuse path; only a clean stop turn (prompt fully resolved
before finish) keeps it warm. + test.
- buildBridgeEnv: treat a whitespace-only auth var as absent (v.trim()), so a
blank higher-preference token can't shadow a real lower-preference one and we
never forward a useless blank token. + test.
- Auth-forwarding tests: clear ambient auth vars in beforeEach so a runner-env
token can't shadow the case under test (CodeRabbit).
- Doc: clarify the allow-list never carries API keys by default; the single
FUSION_CLAUDE_ACP_FORWARD_AUTH opt-in (default OFF) is the only exception.
348/348 pass, tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keep a warm bridge connection + ACP session across turns of one
conversation (gated by FUSION_CLAUDE_ACP_REUSE=1, default OFF), so
multi-turn lanes skip the cold bridge/claude spawn and session/new
round-trip and send only the latest-turn delta (buildResumePrompt).
A stable router indirection serves each turn's handlers.
Addresses the adversarial review of the reuse path:
- P0: a warm-child death routes failure to the CURRENT owner turn via
router.fail, so a reuse turn fails fast instead of hanging until the
30-min inactivity timeout.
- P1: eviction is cache-identity-aware (evictCachedAcpConn only deletes
the map key when it still points at the entry), so a concurrent cold
turn / stale close handler / idle timer can't evict or kill a newer
live entry's child.
- P1: an empty resume delta cold-starts instead of issuing an empty
prompt that could hang.
- P2: a per-turn token drops cross-turn stray updates on the shared
warm connection.
- The idle reaper is unref'd so it never pins the process.
Default OFF → the cold path is functionally unchanged (reviewer-verified).
Adds multi-turn tests: reuse skips spawn+session/new, flag-off spawns
fresh, fail-fast on warm-child death, empty-resume cold fallback.
346/346 pass, tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- P2: event-bridge handleMessageDelta now consumes cache_read/cache_creation
tokens (parity with handleMessageStart) — the OQ3 usage path carried them but
they were silently dropped, understating cost for cached turns.
- P2: validate the untrusted bridge usage payload — coerce each field to a
finite, non-negative number before forwarding, so a malformed value
(string/NaN/negative) can't corrupt totalTokens/cost.
- Tests: usage now asserts cache tokens + totalTokens; new cases for malformed
usage, tool-use turns reporting zero usage, the ANTHROPIC_AUTH_TOKEN middle
precedence, and that the auth token is read from process.env (never a
caller-supplied value — no token substitution).
- Doc: state the auth-forwarding exposure trade-off in the code comment.
acp-driver 13/13; event-bridge tests green; typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Item 2 (OQ3): capture PromptResponse.usage from conn.prompt and feed it into
the bridge before finish(), so ACP-path turns report token usage/cost instead
of always zero. Zero-when-absent is safe; tool-use (break-early) turns
inherently report zero (the prompt result never resolves).
- Item 3 (R17): opt-in headless credential delivery. When
FUSION_CLAUDE_ACP_FORWARD_AUTH=1, buildBridgeEnv forwards a SINGLE Claude auth
token (CLAUDE_CODE_OAUTH_TOKEN > ANTHROPIC_AUTH_TOKEN > ANTHROPIC_API_KEY) from
the operator's launch env so a detached daemon (no login Keychain) can
authenticate. Default OFF — the secure no-secrets posture is unchanged.
acp-driver tests 9/9 (usage + the three auth-opt-in cases); typecheck clean.
Remaining: item 1 (connection reuse / resume latency).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CodeRabbit: spinner class `spin` -> `animate-spin` (matches the card's other
Loader2 usages).
- CodeRabbit (major): tighten auth-failure detection so it only fires when the
WHOLE turn is the short "Not logged in" message (<=80 chars), not when a long
legitimate answer merely mentions the phrase — avoids false positives.
- CodeRabbit (major): expand the auth-signal test to assert the full invariant —
set on a not-logged-in turn, clear (unlink) on a real response, and NOT flag a
long answer that mentions the phrase.
pi-claude-cli acp-driver 5/5; typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Greptile P2: `acp.active` now reflects the ACTUAL dispatch determinant
(FUSION_CLAUDE_ACP, which includes the operator force-override), not the
experimental flag alone — so the status isn't misleading when forced on/off.
- CodeRabbit/Greptile P2: add FNXC:ClaudeAcp comments to the new code blocks
per the AGENTS.md greppable-comment convention.
Already fixed in the prior commit (daa37d08c): the P1 "sticky env" / latch
(applyClaudeAcpEnable now recomputes each call + FUSION_CLAUDE_ACP_FORCE
override) and the enable->disable-on-same-env regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the bridged `claude` can't authenticate (detached daemon / no keychain),
the turn returns "Not logged in" instead of a real answer. Rather than silently
relay that, detect it and let the user choose.
- Driver: detect a "Not logged in"-only turn and write a cross-process signal
(fusion-acp-bridge-auth.json); a real response clears it (acp-driver test).
- Dashboard status: GET /providers/claude-cli/status reports
acp.authFailed + authReason from the signal.
- UI: the Claude CLI provider card shows an auth-failure banner with
"Use claude -p" (sets experimentalFeatures.claudeCliAcp=false) and
"I fixed auth — re-test", plus a fix hint (run `claude` to log in).
- Enable resolution now recomputes each call with an operator force-override
(FUSION_CLAUDE_ACP_FORCE), so the "Use -p" fallback takes effect on the next
turn — no restart. claude-acp-enable tests updated.
pi-claude-cli + engine tests green; dashboard typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
streamSimple routes to streamViaAcp ONLY when FUSION_CLAUDE_ACP=1 AND a bridge
path is provided; otherwise stays on the -p streamViaCli path. Covers the three
cases (off / flag-without-path / flag+path) and asserts the bridge path + env
are forwarded. 333/333 pi-claude-cli tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three-reviewer pass (correctness/security/reliability) on the highest-risk file.
P0:
- Break-early now gates on isPiKnownClaudeTool: Claude's internal ToolSearch
(used to load deferred MCP tools) no longer aborts the turn before the real
fn_* call. Surface+break works from both tool_call updates and
request_permission. New test replays the U9 [ToolSearch, fn_task_list] sequence.
- Downgrade a tool_use turn that surfaced zero pi tool calls -> stop (mirrors
provider.ts), so pi never dispatches non-existent tools.
- register the bridge child in the process registry (no orphan on teardown).
- inactivity timeout (30 min, re-armed per chunk) + per-RPC timeouts on
newSession (a hung bridge now ends the stream and dies).
P1:
- capture bridge stderr + child 'close' handler -> surface exit code/stderr
(no more silent, undebuggable failures).
- sanitize untrusted agent output: strip ANSI/control chars, per-chunk +
per-turn caps, bound tool ids/names (no terminal-escape injection / DoS).
- validate bridge path (absolute + exists) before spawn.
- preserve image content blocks in the prompt (flatten-to-text dropped vision).
P2:
- enforce the bridge env allow-list INSIDE the driver (HOME/PATH/terminal only),
not trusting the caller-supplied object.
Documented residual (kill-switch stays OFF until verified): the bridge's
tool-execution ordering and native-tool (Bash/Read/Write) execution-prevention
need a live behavioral test before any lane enables this path.
pi-claude-cli: 330/330 tests green; typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds streamViaAcp: a drop-in alternative to streamViaCli that drives Claude
through the claude-code-cli-acp bridge over ACP instead of `claude -p`. Returns
the same AssistantMessageEventStream, so streamSimple dispatches to either
transport behind a kill-switch (FUSION_CLAUDE_ACP=1 + an injected bridge path),
OFF by default — the live `-p` path is byte-for-byte untouched until soak.
- Full-history prompt every turn (buildPrompt) — the ACP path has no --resume (R13).
- Forwards schema-only MCP servers so Claude emits correct tool calls; breaks
early on the first tool_call (cancel turn, surface to pi) so the bridge never
executes Fusion's tools — mirrors the `-p` break-early pattern.
- Translation reuses the tested createEventBridge by synthesizing Claude stream
events from ACP session/updates, sharing pi sequencing + tool-name mapping.
- Bridge env forwards only HOME/PATH so `claude` authenticates from the login
session (R17); never inherited process.env or API keys.
Verified: 3/3 translation unit tests; real-bridge session/update shapes confirmed
(agent_message_chunk text + tool_call); 326/326 existing pi-claude-cli tests green;
typecheck clean.
Remaining for Route A: engine injection of the bridge path (KTD10), U12 picker/
auth/status, U13 workflow verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a changeset for FN-3815. The core change reclassifies clean-exit stderr output in the pi-claude-cli provider to reduce noisy logging, with corresponding test coverage distinguishing between clean and non-zero exit stderr handling.
Fusion-Task-Id: FN-3815
Merged FN-3207, FN-3212, and FN-3242: removed runtime memory-backend side-loading in core, added comprehensive regression tests for QuickChat, chat routes, and SSE streams, and documented compact mobile chat dialogs in the dashboard guide. The refactor in `project-memory.ts` reduces complexity while
Fusion-Task-Id: FN-3212
This merge lands v0.12.0 with two major features: a droid-cli provider integration adding auth routes, status endpoints, and a settings toggle hook for controlling CLI-based authentication, plus a new experimental agent onboarding modal with a create-agent form. The release also stabilizes engine st
Fusion-Task-Id: FN-2981
Adds vitest coverage for validateCliPresenceAsync and validateCliAuthAsync,
verifying success paths, spawn-error paths, non-zero exit codes, and that
auth failures emit the expected warning. Pairs with the async/memoized
validation refactor in 442238196 that unblocks the dashboard event loop
on every chat send.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fn_task_create and fn_task_update accepted any string as `agentId` and
wrote it verbatim onto `task.assignedAgentId`, letting hallucinated IDs
(e.g. `agent-executor-001`) appear as agent badges in the dashboard.
Mirror the validation already used by fn_delegate: look the agent up via
AgentStore and reject unknown or ephemeral/runtime-managed agents. Null
still clears the field on update.
Also clean up two stale failures in bundle-output.test that predated this
change:
- pi-claude-cli no longer imports cross-spawn, so drop the dependency and
its orphan type-decl file.
- Loosen the spawn-import regex to match `spawn` anywhere in the
destructured import (the source has additional named imports).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add missing Claude model entries and extend provider metadata handling for model extras
- Improve subprocess diagnostics in pi-claude-cli process management for clearer failure visibility
- Add targeted tests for provider model extras and process-manager diagnostic behavior
- Update Settings modal copy for project default model guidance and record changes in pi-claude-cli changelog
Fusion-Task-Id: FN-2898
- Strip private @fusion/* workspace devDependencies from the published CLI manifest via prepare-publish-manifest and package metadata updates
- Replace cross-spawn usage and add staged bundle layout assertions to verify resolver output in dist packaging
- Add per-task/project model override resolution across core, dashboard settings/task modals, and route coverage with new regression tests
- Strengthen engine merge/recovery handling for paused/interrupted/squash paths and surface merger timeline activity with additional self-healing and merger tests
- Add changesets for npm bundle dependency fixes, project model override stabilization, and FTS5 corruption recovery
Fusion-Task-Id: FN-2897
- Remove ToolSearch prerequisite so custom MCP tools can be used during triage and plan generation flows
- Align built-in tool sets between provider wiring and prompt builder handling, including custom ls behavior
- Expand pi-claude-cli tests for event bridge, MCP config, prompt builder, and tool mapping regressions
- Add FN-2703 changeset and delivery documentation for the published @runfusion/fusion package
- test(FN-2618): harden process-manager fs/tmpdir mocks
- fix(cli): mark react-devtools-core external in bun compile
- fix(pty): switch to @homebridge/node-pty-prebuilt-multiarch fork
- Update TaskCard done-duration calculations to use createdAt as the start baseline for completed tasks
- Refactor TaskCard timer tests to assert against TaskStore-backed timestamps and avoid brittle elapsed-time assumptions
- Cover done-state timer behavior with expanded test scenarios for start/end timestamp combinations
- Remove unused provider code in pi-claude-cli to satisfy lint and keep the full test/build gates green
Triage with claude-sonnet-4-6 via pi-claude-cli kept looping on
fn_review_spec calls that were rejected by pi's validator with
"root: must be object". Parameterless MCP tools (schema
{type:"object", properties:{}}) emit zero input_json_delta events,
so partialJson stayed "" and the catch fell through to
finalArgs = "" — a string, which TypeBox's Type.Object({}) rightly
refuses. Default empty partialJson to {} so the call lands.
Also:
- Add a 2-step reminder loop in triage before swapping to the
fallback planning model — primary models that wrote PROMPT.md
but forgot fn_review_spec recover from a nudge, no need to pay
the cold-start tax of a new triage on a different model.
- Inject @runfusion/fusion's own pi extension into dashboard/
daemon/serve sessions and propagate the path to createFnAgent
via setHostExtensionPaths so fn_* tools register globally
without requiring `pi install npm:@runfusion/fusion`.
- Drop the "historical" qualifier from replayed tool labels —
Claude was reading "TOOL RESULT (historical Read):" as
"previous session, ignore" and looping on verification.
- Remove subprocess-lifecycle stderr debug logs that landed for
hang diagnosis — root cause is fixed, the noise can go.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triage runs through pi-claude-cli still hang silently for 20+ minutes after
only 2 thinking-deltas before the engine's StuckTaskDetector kills them. We
can't tell from agentLogEntries whether the subprocess crashed, the MCP
server failed to attach, or Claude is in extended thinking that doesn't
stream.
Adds stderr lines tagged [pi-claude-cli] for:
- spawn (pid, model, mode, prompt sizes, mcp on/off)
- init (session id, model, permissionMode, mcp_servers status list)
- first-stdout-line (latency from spawn)
- tool_use (per top-level tool, with piKnown flag)
- break-early (when we kill at message_stop)
- close (exit code, signal, elapsed, broken-flag)
No behavioural change. Restart the dashboard, trigger triage, and these
lines tell us which phase silence sets in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The engine already runs a StuckTaskDetector with a configurable per-task
heartbeat (default 1 hour) and aborts triage/executor sessions through
AbortSignal when it judges them quiet. pi-claude-cli forwards that signal to
the subprocess. The 300s subprocess-level inactivity timeout was a redundant,
much-tighter shadow that kept killing Sonnet 4.6 mid-thinking on the 40k-char
triage prompt — exactly what the engine-level detector is designed not to do.
Move the local timeout to 30 minutes purely as a last-resort guard for embeds
that don't pass an abort signal. Stuck-detection responsibility now lives in
the caller, where it can be configured per task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sonnet 4.6 with extended thinking on the triage prompt (~40k chars + AGENTS.md
+ skills) can take >3 minutes between thinking deltas. The 180s inactivity
timeout was killing those subprocesses before they could finish PROMPT.md +
fn_review_spec, leaving triage looping into the fallback model. Bump to 5
minutes.
Test mock timings updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of "fn_review_spec was never called" tracked through the diagnostic
chain: the prompt actually sent to triage agents has zero `fn_*` tokens —
because resolveAgentPrompt("triage", ...) returns the BUILTIN_AGENT_PROMPTS
default-triage template (TRIAGE_PROMPT_TEXT in core/agent-prompts.ts), and that
template was forked from an older version that never had the "MUST call
fn_review_spec()" workflow nor any fn_-prefixed tool names. The fallback
`|| TRIAGE_SYSTEM_PROMPT` in engine/triage.ts never fires because the core
template is non-empty.
So the model writes PROMPT.md, doesn't see any instruction to review it, and
ends. zai/glm-5.1 happened to call fn_review_spec from training-pattern
inertia; Sonnet via pi-claude-cli stopped at write — same prompt, same bug.
Replace TRIAGE_PROMPT_TEXT with the engine's up-to-date TRIAGE_SYSTEM_PROMPT
verbatim (fn_-prefixed tools, fn_review_spec workflow, subtask breakdown,
project-commands handling, frontend UX criteria injection). Also remove the
diagnostic-only console.error lines added during this debugging session — the
core fix is now elsewhere and the noise isn't worth keeping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reports "no custom tool refs to rewrite (tools=18)" — meaning Context.tools
is populated but the prompt body doesn't contain any of the names. Need the
first 200 chars of the prompt and a sample of custom-tool names to figure out
whether the engine's customPrompt is reaching us at all or whether names
differ from what we expect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds stderr lines so we can verify the rewrite is actually firing on a given
session ("rewrote N custom tool ref(s) [fn_review_spec×3, ...]") vs. silently
no-opping (no tools, no matches). Helps distinguish "fix not deployed yet"
from "fix not effective" without redeploying instrumentation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triage system prompts read "MUST call fn_review_spec()" but Sonnet 4.6 routed
through pi-claude-cli writes PROMPT.md and silently skips the call — even with
the addendum explaining the deferred-tool protocol. Confirmed by the FN-2564
agent log: model called other MCP tools (fn_memory_search, fn_task_list) fine
but consistently never reached fn_review_spec, leaving triage looping on
"fn_review_spec was never called" and falling back to zai/glm-5.1 every time.
Rewrite bare `fn_*` (and any non-built-in custom tool name) references in the
system prompt to their `mcp__custom-tools__fn_*` form before sending. The
prompt now literally says "call mcp__custom-tools__fn_review_spec()" so the
model has no inference step, and the deferred-tool reminder Claude Code injects
matches verbatim. Word-boundary safe (won't touch fn_review_specifier) and
idempotent (won't double-prefix already-MCP-named occurrences).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude Code 2.x defers MCP tool schemas behind ToolSearch — tools are listed
but calling them by short name (e.g. fn_review_spec) returns "tool not found"
until ToolSearch loads the schema. Triage system prompts that say "call
fn_review_spec()" leave the model guessing whether to use the bare name or
the mcp__custom-tools__ form.
Append a system-prompt section listing every custom tool with its short and
MCP-prefixed names plus the ToolSearch + call protocol. Drives off the
Context.tools we now already thread through, so plain chats with no custom
tools get no addendum.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix regenerated the MCP config per call but still pulled the tool
set from pi.getAllTools(), which depends on the runtime binding being scoped to
the right AgentSession. Engine-spawned triage/executor sessions register custom
tools (fn_review_spec, fn_review_step) and pi-ai already passes those to the
provider via Context.tools — that's the authoritative per-session list and
bypasses any binding ambiguity. Falls back to pi.getAllTools() when the caller
doesn't populate Context.tools.
Also logs the tool names on refresh so missing tools are diagnosable from stderr.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MCP config was generated lazily once and locked, so engine session-scoped
tools (fn_review_spec, fn_review_step) never reached the Claude CLI subprocess
and triage/executor sessions failed with "unknown tool" errors. Now the config
is hashed per call and rewritten when the tool set changes.
Also adds scripts/sync-fusion-skill-tools.mjs to regenerate the SKILL.md
tool-categories block from extension.ts at build time, with a --check mode
wired into skill-sync tests so drift fails CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TaskCard: four catch((err: any) => err.message) promise handlers in
archive/unarchive/delete/move → catch((err) => getErrorMessage(err)).
- InlineCreateCard + QuickEntryBox: .catch((err: any)) model-load handlers
→ getErrorMessage(err) with existing @fusion/core import.
- TerminalModal: drop (navigator as any).maxTouchPoints — modern lib.dom
types already expose the property.
- serve.ts: remove unused any annotation on OpenRouter model mapper; the
array element type is already inferred from json.data.
- pi.js, runtime-resolution.ts, dashboard.ts, serve.ts, dev-server-port-
detect.ts, devserver-manager.ts: drop now-stale eslint-disable comments
that the cleanup made redundant.
Fix a prompt-builder regression surfaced by agent's `any` cleanup: toolCall
with a raw string `arguments` field must be preserved verbatim (JSON-quoted)
rather than coerced to `{}`; restores a previously-passing test.
Then promote @typescript-eslint/no-explicit-any from warn → error. Future
new anys must either come with a one-line disable + justification or use a
real type. Workspace is now lint-clean (0 problems).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parallel subagent pass: four typescript-pro agents on non-overlapping scopes.
Patterns applied:
- catch (err: any) { ... err.message ... } → catch (err) { ... getErrorMessage(err) ... }
using the new @fusion/core helper. Bare catch {} where the error was unused.
- SQLite row types: defined typed XxxRow interfaces per table and cast
.all()/.get() results via `as unknown as XxxRow[]` (the double cast is
required because better-sqlite3 returns Record<string, SQLOutputValue>).
- rowToX(row: any) converters: typed argument with the matching row interface.
- Dynamic settings key writes: (settings as Record<string, unknown>)[key].
- React event handlers and setState callbacks: inferred types or concrete
React.{Mouse,Change,Form}Event<...> where needed.
- pi-claude-cli: local PiMessage / PiContext duck types to avoid re-typing
pi-ai concrete shapes; typed Claude stream event message fields.
72 files changed, ~400 anys eliminated. Typecheck passes across the workspace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the stray useClaudeCli settings checkbox + onboarding question
with a proper provider-card UX. The card lives next to OAuth + API-key
cards in onboarding and settings, with Enable/Disable + Test actions.
Backend:
- Vendors rchern/pi-claude-cli@0.3.1 as packages/pi-claude-cli
(MIT, attribution in UPSTREAM.md). Lets us bump peer-dep on
pi-coding-agent in lockstep with Fusion (upstream pinned ^0.52.0
vs ours ^0.62.0) and fix bugs without waiting on upstream.
- Adds @fusion/pi-claude-cli as a workspace dep of @runfusion/fusion
so users don't have to `npm install -g pi-claude-cli` manually.
- serve/daemon/dashboard conditionally load the extension via
discoverAndLoadExtensions() when GlobalSettings.useClaudeCli is on;
no side-effects on user ~/.fusion/agent/settings.json.
- New GET /api/providers/claude-cli/status: claude --version probe
+ toggle state + cached extension resolution.
- New POST /api/auth/claude-cli: flips useClaudeCli, refuses if the
claude binary is missing, fires the existing skill-backfill hook.
- /api/auth/status now injects a synthetic {id:"claude-cli", type:"cli"}
provider entry so onboarding + settings see a consistent list.
Frontend:
- New ClaudeCliProviderCard component shared between ModelOnboardingModal
and SettingsModal's Authentication section.
- New AuthProvider.type = "cli" variant.
- Removed the old "Route AI calls through the Claude CLI" checkbox from
Global Models settings and the opt-in step from the onboarding wizard.
- ProviderIcon gets a composite Anthropic-mark-plus-terminal glyph for
the claude-cli provider id.
Tests:
- 8 unit tests for extension resolution (@fusion/pi-claude-cli is
workspace-linked so these run in-tree).
- 2 unit tests for the binary probe.
- Existing /auth/status tests filter out the new synthetic entry so
they keep asserting structural OAuth/API-key behavior in isolation.
- The vendored package's own 296 tests still pass unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>