From 45184aef7e3dda293ea6c46ccbcb139a22cb1cd8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 15 Jun 2026 12:14:20 -0700 Subject: [PATCH] feat(acp): enable Route A via experimental flag (claudeCliAcp), default ON Replace the manual FUSION_CLAUDE_ACP env enable with an experimental feature switch. `experimentalFeatures.claudeCliAcp` is ON by default (off only when explicitly set false); the engine translates it into the FUSION_CLAUDE_ACP dispatch the pi-claude-cli provider reads, at registerExtensionProviders time. - Still fail-closed: with no bridge path published (acp-runtime plugin absent), the provider falls back to `claude -p`. - Explicit FUSION_CLAUDE_ACP env always wins (operator / test override). - New testable helper claude-acp-enable.ts (6/6 tests); flag documented in the core experimentalFeatures doc. So with the acp-runtime plugin installed, Claude CLI now routes through the ACP bridge by default; set experimentalFeatures.claudeCliAcp=false to force `-p`. Co-Authored-By: Claude Opus 4.8 --- packages/core/src/types.ts | 11 ++++-- .../src/__tests__/claude-acp-enable.test.ts | 38 +++++++++++++++++++ packages/engine/src/claude-acp-enable.ts | 36 ++++++++++++++++++ packages/engine/src/pi.ts | 11 +++++- 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 packages/engine/src/__tests__/claude-acp-enable.test.ts create mode 100644 packages/engine/src/claude-acp-enable.ts diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c8a0b1f83e..517e96c4e3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -3170,9 +3170,14 @@ export interface GlobalSettings { * "another-experiment": false * } * - * Default: workflow columns, graph executor, dual-observe, and authoritative - * interpreter flags enabled; operators may explicitly set individual flags - * false while rollout controls remain available. */ + * Default: workflow columns, graph executor, dual-observe, authoritative + * interpreter, and `claudeCliAcp` flags enabled; operators may explicitly set + * individual flags false while rollout controls remain available. + * + * `claudeCliAcp` (default ON): routes the Claude CLI provider through the + * `claude-code-cli-acp` ACP bridge instead of `claude -p`. Effective only when + * the acp-runtime plugin is installed (it publishes the bundled bridge path); + * otherwise the provider fails closed to `-p`. Set false to force `-p`. */ experimentalFeatures?: Record; /** Per-adapter CLI-agent launch configuration (CLI Agent Executor, U15). * Keyed by adapter id (e.g. `"claude-code"`, `"codex"`, `"generic"`). Each diff --git a/packages/engine/src/__tests__/claude-acp-enable.test.ts b/packages/engine/src/__tests__/claude-acp-enable.test.ts new file mode 100644 index 0000000000..def192efb3 --- /dev/null +++ b/packages/engine/src/__tests__/claude-acp-enable.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { claudeAcpExperimentalEnabled, applyClaudeAcpEnable } from "../claude-acp-enable.js"; + +describe("claudeAcpExperimentalEnabled — default ON", () => { + it("is ON when no settings / no experimentalFeatures", () => { + expect(claudeAcpExperimentalEnabled(undefined)).toBe(true); + expect(claudeAcpExperimentalEnabled({})).toBe(true); + expect(claudeAcpExperimentalEnabled({ experimentalFeatures: {} })).toBe(true); + }); + it("is ON when explicitly true", () => { + expect(claudeAcpExperimentalEnabled({ experimentalFeatures: { claudeCliAcp: true } })).toBe(true); + }); + it("is OFF only when explicitly false", () => { + expect(claudeAcpExperimentalEnabled({ experimentalFeatures: { claudeCliAcp: false } })).toBe(false); + }); +}); + +describe("applyClaudeAcpEnable — translates the flag to FUSION_CLAUDE_ACP", () => { + it("sets FUSION_CLAUDE_ACP=1 when enabled (default ON) and env unset", () => { + const env: NodeJS.ProcessEnv = {}; + expect(applyClaudeAcpEnable({}, env)).toBe(true); + expect(env.FUSION_CLAUDE_ACP).toBe("1"); + }); + it("does NOT set the env when the flag is explicitly false", () => { + const env: NodeJS.ProcessEnv = {}; + expect(applyClaudeAcpEnable({ experimentalFeatures: { claudeCliAcp: false } }, env)).toBe(false); + expect(env.FUSION_CLAUDE_ACP).toBeUndefined(); + }); + it("honors an explicit env override (operator/test wins over the flag)", () => { + const off: NodeJS.ProcessEnv = { FUSION_CLAUDE_ACP: "0" }; + expect(applyClaudeAcpEnable({}, off)).toBe(false); // flag default-on, but env says off + expect(off.FUSION_CLAUDE_ACP).toBe("0"); + + const on: NodeJS.ProcessEnv = { FUSION_CLAUDE_ACP: "1" }; + expect(applyClaudeAcpEnable({ experimentalFeatures: { claudeCliAcp: false } }, on)).toBe(true); + expect(on.FUSION_CLAUDE_ACP).toBe("1"); + }); +}); diff --git a/packages/engine/src/claude-acp-enable.ts b/packages/engine/src/claude-acp-enable.ts new file mode 100644 index 0000000000..500702faca --- /dev/null +++ b/packages/engine/src/claude-acp-enable.ts @@ -0,0 +1,36 @@ +/** + * Route A enable resolution (experimental, DEFAULT ON). + * + * The `pi-claude-cli` provider drives Claude through the `claude-code-cli-acp` + * ACP bridge instead of `claude -p` when BOTH hold at dispatch time: + * 1. `FUSION_CLAUDE_ACP=1` (this module sets it from the experimental flag), and + * 2. a bridge path is resolvable (the acp-runtime plugin publishes + * `FUSION_CLAUDE_ACP_BRIDGE` on load — KTD10; absent → fail-closed to `-p`). + * + * The user-facing switch is `experimentalFeatures.claudeCliAcp`: ON unless the + * user explicitly sets it to `false`. An explicit `FUSION_CLAUDE_ACP` env value + * always wins (operator / test override) — see {@link applyClaudeAcpEnable}. + */ + +/** True unless `experimentalFeatures.claudeCliAcp === false` (default ON). */ +export function claudeAcpExperimentalEnabled( + globalSettings: Record | undefined, +): boolean { + const exp = ((globalSettings ?? {}).experimentalFeatures ?? {}) as Record; + return exp.claudeCliAcp !== false; +} + +/** + * Translate the experimental flag into the `FUSION_CLAUDE_ACP` dispatch the + * provider reads. No-op when the env var is already set (explicit override wins), + * so operators/tests keep full control. Returns the resolved enabled state. + */ +export function applyClaudeAcpEnable( + globalSettings: Record | undefined, + env: NodeJS.ProcessEnv = process.env, +): boolean { + if (typeof env.FUSION_CLAUDE_ACP === "string") return env.FUSION_CLAUDE_ACP === "1"; + const enabled = claudeAcpExperimentalEnabled(globalSettings); + if (enabled) env.FUSION_CLAUDE_ACP = "1"; + return enabled; +} diff --git a/packages/engine/src/pi.ts b/packages/engine/src/pi.ts index cb9bd2ebfa..84ee5cc048 100644 --- a/packages/engine/src/pi.ts +++ b/packages/engine/src/pi.ts @@ -57,6 +57,7 @@ import { type SkillSelectionContext, } from "./skill-resolver.js"; import { isContextLimitError } from "./context-limit-detector.js"; +import { applyClaudeAcpEnable } from "./claude-acp-enable.js"; import { createFusionAuthStorage, getModelRegistryModelsPath } from "./auth-storage.js"; import { piLog, extensionsLog } from "./logger.js"; import { readCustomProviders } from "./custom-providers.js"; @@ -1368,10 +1369,18 @@ async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegis try { const agentDir = getPackageManagerAgentDir(); + const settingsView = createReadOnlyPiSettingsView(cwd, agentDir); + + // Route A enable (experimental, DEFAULT ON): translate + // experimentalFeatures.claudeCliAcp into the FUSION_CLAUDE_ACP dispatch the + // pi-claude-cli provider reads. Still fail-closed — with no bridge path + // published (acp-runtime plugin absent), the provider falls back to `-p`. + applyClaudeAcpEnable(settingsView.getGlobalSettings() as Record); + const packageManager = new DefaultPackageManager({ cwd, agentDir, - settingsManager: createReadOnlyPiSettingsView(cwd, agentDir) as any, + settingsManager: settingsView as any, }); const resolvedPaths = await packageManager.resolve(); const packageExtensionPaths = resolvedPaths.extensions