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>
Route A unblock + the first Route A code increment.
- U9 verdict recorded (plan OQ1 + docs/acp-contract.md): in an authenticated
interactive session the pinned claude-code-cli-acp 0.1.1 bridge forwards
session/new mcpServers to Claude, Claude invokes the forwarded Fusion tool,
and the call traverses the ACP permission gate (session/request_permission).
Both security-critical answers resolve positively — overturns the headless
NOT-GO chain (FN-6466/6467/6473/6476), whose only blocker was running
detached from the login keychain session.
- R17 (daemon auth) recorded and closed for the supported setup: creds are
macOS Keychain-only; the user's login-session fn daemon has keychain access
(the existing claude -p provider authenticates there), so the bridge does too.
- U10: thread an optional mcpServers list through the ACP runtime contract.
newAcpSession now forwards it (was hardcoded []); AgentRuntimeOptions (engine
+ plugin-local copy) gains the field; defaults to [] to preserve Route B's
read-only ask posture. Tool calls still route through the U5 permission floor.
Plugin typechecks clean; provider-session tests 12/12 (incl. 2 new forwarding
tests). U11-U13 (provider transport swap, picker/auth, workflow verify) remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Index the upstream ACP MCP forwarding sponsorship doc so the docs README and CLI index test keep it discoverable.
- Add the upstream ACP MCP passthrough and permission forwarding sponsorship doc link under Audit Reports.
- Add the upstream doc to the required docs README index coverage.
- Guard that CLI Printing Press docs remain indexed in Audit Reports only, not duplicated under Plugins.
Files changed:
docs/README.md | 5 +++++
.../cli/src/__tests__/docs-readme-index.test.ts | 21 +++++++++++++++++++++
2 files changed, 26 insertions(+)
Fusion-Task-Id: FN-6479
Fusion-Task-Lineage: f23afeba-a989-4552-8857-fe2984df6081
Ensure AI-merge clean-room directories are cleaned up even when setup fails before worktree registration.
- Move clean-room directory creation inside the cleanup guard so terminal setup failures can remove it.
- Skip cleanup only when no merge root was ever created, while still unregistering any registered paths.
- Add regression coverage for active-session registration failure after mkdtemp and before git worktree add.
Files changed:
.../ai-merge-worktree-cleanup.test.ts | 31 ++++++++++++++++++++++
packages/engine/src/merger-ai.ts | 21 ++++++++++-----
2 files changed, 45 insertions(+), 7 deletions(-)
Fusion-Task-Id: FN-6453
Fusion-Task-Lineage: 364b329b-c3e7-4902-8ca8-673360b0d9ac
Add a durable proof-point runbook for validating external plugins against released Fusion CLI builds.
- Document release selection, npm integrity capture, scaffold/build/test/install/enable validation, and pass/fail criteria for external plugin proof points.
- Link the runbook from the docs index so it remains discoverable with other plugin authoring docs.
- Extend the docs README index test to require the new proof-point runbook entry.
Files changed:
docs/README.md | 1 +
docs/plugins/external-proof-point-runbook.md | 211 +++++++++++++++++++++
.../cli/src/__tests__/docs-readme-index.test.ts | 1 +
3 files changed, 213 insertions(+)
Fusion-Task-Id: FN-6438
Fusion-Task-Lineage: 5c768d7e-4465-42ac-9535-f16dc42ab6d0
Enable mobile users to swipe across the agent detail tab strip.
- Restores horizontal touch panning on the agent detail tabs while preserving vertical pan behavior.
- Documents the mobile touch-action requirement next to the tab-strip CSS.
- Adds a mobile regression test covering pan-x touch-action and horizontal overflow.
Files changed:
packages/dashboard/app/components/AgentDetailView.css | 6 ++++++
.../__tests__/AgentDetailView.mobile-scroll.test.tsx | 15 +++++++++++++++
2 files changed, 21 insertions(+)
Fusion-Task-Id: FN-6450
Fusion-Task-Lineage: 7006af83-4490-4fe0-a357-7d19b7b522c0
Rescue the session reconnect SSE test by shutting down route-owned background work before temp cleanup.
- Isolate the focused API harness from TaskStore EventEmitter startup workers.
- Dispose API routes and scheduled AI session cleanup before deleting the test temp root.
- Remove session-reconnect from the dashboard quarantine ledger and vitest skip list.
Files changed:
.../src/__tests__/session-reconnect.test.ts | 22 ++++++++++++++++++----
packages/dashboard/vitest.config.ts | 6 ++++--
scripts/lib/test-quarantine.json | 5 -----
3 files changed, 22 insertions(+), 11 deletions(-)
Fusion-Task-Id: FN-6447
Fusion-Task-Lineage: a3fef28c-b92b-4f47-9181-a63bb31e4f48
Ensure the Secrets page root expands within the project content flex row on narrow screens.
- Let the Secrets view root flex to available width with a zero min-width.
- Preserve existing height and spacing behavior while avoiding intrinsic card-width collapse.
- Cover the mobile layout contract with a root-container CSS assertion.
Files changed:
packages/dashboard/app/components/SecretsView.css | 7 +++++++
.../dashboard/app/components/__tests__/SecretsView.mobile.test.tsx | 7 +++++++
2 files changed, 14 insertions(+)
Fusion-Task-Id: FN-6446
Fusion-Task-Lineage: 34f55d8e-ee57-432c-b1ce-165cc43333ee
Remove the CLI extension integration suite's hidden Vitest timeout override while keeping its explicit build-only allowance.
- Replace the file-wide 30s Vitest test and hook timeout override with a scoped comment documenting default caps.
- Derive the extension bundle path from the CLI root in the test file.
- Use a SQLite collision trigger instead of a TaskStore prototype spy and fix the agent delete tool payload.
- Clear the timeout appeasement allowlist now that the CLI test exemption is gone.
Files changed:
.../src/__tests__/extension-integration.test.ts | 25 ++++++++++++++++------
.../lib/test-timeout-appeasement-allowlist.json | 8 +------
2 files changed, 20 insertions(+), 13 deletions(-)
Fusion-Task-Id: FN-6436
Fusion-Task-Lineage: 670f41ea-f98f-400c-aa16-6a1cbb1e9bea
Handle logEntry failures after in-place parking without falling back to executor requeue and tighten the reliability assertion for STUCK_LOOP_EXHAUSTED.
Preserve incomplete task progress after stuck-kill budget exhaustion while marking the task failed and paused for manual intervention instead of making it scheduler-runnable again.
A real touch tap dispatches both pointerdown and touchstart, and the quick chat
send (and stop) buttons ran their action on each event. Because React had not
flushed the composer clear between the two synchronous handlers, both saw the
same input and fired handleSendMessage — and the hook's second send closed the
first's freshly-opened stream and re-POSTed, which could drop the agent's
response (notably for the first message after a response completed).
Add a tap-scoped guard (cleared after the current input task) so only the first
of the paired pointerdown/touchstart events performs the action. This is kept
separate from the 700ms onClick latch — which is shared between the send and
stop buttons — so a stop tap right after a send is never swallowed.
The earlier component test masked this because fireEvent flushes React state
between calls; the regression test now dispatches the full tap sequence in one
act() to mirror the device.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The quick chat stop button reused ChatView's `.chat-input-stop` class, which
sizes itself with `--chat-input-control-size` — a variable defined on
`.chat-input-row` in the ChatView composer and undefined in the quick chat DOM.
The invalid var collapsed the button's width (and mobile min-width) toward its
icon, leaving it noticeably narrower than the send button it replaces.
Pin the stop button to the send button's square dimensions with a compound
selector using globally-scoped spacing tokens, so it outranks the single-class
base and mobile rules from both stylesheets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the quick chat send fix. The send-time stale-stream recovery
only fires when a message is queued while the streaming flag is already
stuck, and the pre-session flush effect fires once on session activation and
bails permanently if a stream ref is lingering. Either gap leaves a queued
message stranded idle in the composer — shown locally but never sent to the
agent or persisted (so also absent from regular chat).
Add a delivery watchdog: whenever a message stays pending under an active
session, re-confirm after a short delay and force-deliver it if the server
reports no generation in flight and no live stream is connected. This is a
catch-all backstop independent of which targeted flush trigger bailed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the current CLI quarantine decision so the deletion ratchet stays explicit.
- Record that all 24 quarantined CLI Vitest suites were re-triaged and kept in-window.\n- Capture the direct-run evidence and package-load rescue rationale in the CLI Vitest config.\n- Preserve the existing quarantine list without adding or removing suites.\n\nFiles changed:\n packages/cli/vitest.config.ts | 4 ++++\n 1 file changed, 4 insertions(+)
Fusion-Task-Id: FN-6427
Fusion-Task-Lineage: 30e4ccc7-97d3-4aa1-838c-dcb35f2734f1
Two mobile send failures with a shared symptom of "nothing happens":
- Regular chat: the send button was dead to touch. The action lived only
in onClick, but iOS suppresses the trailing synthetic click after
preventDefault() in the touch sequence, so taps never sent. Fire the send
from pointerdown/touchstart with a self-clearing dedupe latch (mirroring the
QuickChat send button), keeping a single send per tap.
- Quick chat: a queued message could strand in the composer — shown locally
but never reaching the agent or the persisted session (so it also never
appeared in regular chat). A stream that dropped without onDone/onError
(e.g. mobile tab suspension) left the streaming flag stuck true, so every
later send took the "queue while streaming" branch and was never flushed.
On a queued send, detect the stale flag via the stream's connection state
and the server's generation status, then tear down the dead stream and flush.
Both paths covered by new tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refresh stored Claude OAuth credentials before reporting dashboard auth status or resolving model auth so users do not need to repeatedly re-login after access-token expiry.
Coalesce concurrent refresh attempts, prevent stale refreshes from overwriting newer logins, and route CLI dashboard/serve/daemon/onboard auth wiring through the shared refresh-capable storage.
Correct scheduler dispatch diagnostics so capacity decisions use consistent non-negative slot counts.
- Clamp excess semaphore releases at zero and warn once when a slot is returned without an active holder.
- Recompute dispatch capacity at each queue decision, including tasks started earlier in the same scheduler tick.
- Update scheduler and semaphore tests for true binding gates, non-negative diagnostics, and workflow-step env stability.
- Add a patch changeset for the scheduler capacity fix.
Files changed:
.changeset/fn-6423-scheduler-capacity.md | 5 +
packages/engine/src/__tests__/concurrency.test.ts | 32 ++++++
.../src/__tests__/executor-step-session.test.ts | 10 +-
packages/engine/src/__tests__/scheduler.test.ts | 122 ++++++++++++++++++++-
packages/engine/src/concurrency.ts | 29 ++++-
packages/engine/src/scheduler.ts | 117 ++++++++++----------
6 files changed, 248 insertions(+), 67 deletions(-)
Fusion-Task-Id: FN-6423
Fusion-Task-Lineage: a6b2e668-a822-46e9-9cd8-ac267fbde804
Keep the task chat expand action visible as an icon-only overlay inside the chat view.
- Move the expand/collapse button out of the scrolling transcript and onto the task chat container.
- Style the control as a centered icon-only button with stable overlay positioning.
- Extend TaskChatTab coverage for empty, loading, populated, expanded, and scrolled transcript states.
Files changed:
packages/dashboard/app/components/TaskChatTab.css | 10 +++-
packages/dashboard/app/components/TaskChatTab.tsx | 26 +++++-----
.../app/components/__tests__/TaskChatTab.test.tsx | 58 +++++++++++++++++-----
3 files changed, 68 insertions(+), 26 deletions(-)
Fusion-Task-Id: FN-6425
Fusion-Task-Lineage: 6526eb4a-4be9-472b-9595-c7fcea6971b1
Prevent the terminal symbols font from corrupting ASCII cell measurement on mobile.\n\n- Add unicode-range scoping to the Fusion terminal Nerd Font symbols face.\n- Cover the CSS contract so symbols ranges include Nerd Font blocks and exclude printable ASCII.\n- Document the xterm font-loading regression and required verification path.\n\nFiles changed:\n .../xterm-symbols-nerd-font-unicode-range.md | 63 ++++++++++++++++++++++\n .../dashboard/app/__tests__/terminal-input.test.ts | 49 +++++++++++++++++\n .../dashboard/app/components/TerminalModal.css | 5 ++\n 3 files changed, 117 insertions(+)
Fusion-Task-Id: FN-6424
Fusion-Task-Lineage: 7350c926-1d51-474b-bfc3-a17f922b9322
Tightens the task detail chat layout so the composer sits closer to the transcript without affecting other detail tabs.
- Reduce the chat tab stack gap and composer vertical padding on desktop and mobile.
- Remove the chat section's inherited top margin while keeping shared tab padding intact.
- Document the spacing requirement with FNXC comments near the affected chat styles.
Files changed:
packages/dashboard/app/components/TaskChatTab.css | 10 ++++++----
packages/dashboard/app/components/TaskDetailModal.css | 7 ++++++-
2 files changed, 12 insertions(+), 5 deletions(-)
Fusion-Task-Id: FN-6418
Fusion-Task-Lineage: 4b484de5-9d6f-403b-902d-5df9f0591811