merge: main (CLI agent interface #1446) — renumber workflow_settings migration to 112 behind main's cli_sessions(110)/adapter(111), full-workspace literal sweep, i18n union

This commit is contained in:
gsxdsm
2026-06-05 16:18:39 -07:00
170 changed files with 24959 additions and 601 deletions

View File

@@ -27,6 +27,7 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "0.24.0",
"@fusion/core": "workspace:*",
"@fusion/plugin-sdk": "workspace:*"
},
"peerDependencies": {

View File

@@ -13,6 +13,7 @@
// to the agent.
import { spawn, type ChildProcess } from "node:child_process";
import { redactSecrets } from "@fusion/core";
function debugLog(message: string): void {
if (process.env.PI_ACP_DEBUG !== "1") return;
@@ -113,31 +114,9 @@ export function spawnAgent(options: SpawnAgentOptions): ChildProcess {
/** Maximum stderr bytes retained; older output is dropped to bound memory. */
const STDERR_BUFFER_CEILING = 64 * 1024;
/**
* Redact token-like / auth patterns from text so auth errors don't leak
* verbatim into the stderr buffer or logs (Risk S8). Best-effort: covers
* bearer tokens, `Authorization:` header values, `key=`/`token=`/`secret=`
* assignments, and long base64/hex secrets.
*/
export function redactSecrets(text: string): string {
return (
text
// Authorization: Bearer <token> / Authorization: <token>
.replace(/(authorization\s*[:=]\s*)(bearer\s+)?[^\s,;"']+/gi, "$1$2[REDACTED]")
// Bearer <token>
.replace(/\b(bearer)\s+[A-Za-z0-9._\-+/=]+/gi, "$1 [REDACTED]")
// key=... token=... secret=... password=... apikey=... (quoted or bare)
.replace(
/\b((?:api[_-]?key|key|token|secret|password|passwd|pwd|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*)("?)[^\s,;"']+\2/gi,
"$1$2[REDACTED]$2",
)
// sk-/ghp_/github_pat_/xoxb-/AKIA-style long opaque tokens
.replace(/\b(sk-|ghp_|gho_|github_pat_|xox[abpr]-|AKIA)[A-Za-z0-9_\-]{8,}/g, "[REDACTED]")
// standalone long base64/hex secrets (>=32 chars)
.replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[REDACTED]")
.replace(/\b[0-9a-fA-F]{32,}\b/g, "[REDACTED]")
);
}
// Secret redaction (Risk S8) lives in @fusion/core so PTY/process owners share
// one implementation; re-exported here to preserve this module's public surface.
export { redactSecrets };
/**
* Accumulate stderr into a bounded, secret-redacted buffer.

View File

@@ -0,0 +1,40 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CeOrchestrator, type CeSessionExecutor } from "../session/orchestrator.js";
import { makeHarness, type TestHarness } from "./_harness.js";
/**
* U9 CE executor seam contract.
*
* Proves the `executor` option threads end-to-end on a REAL orchestrator (not a
* scripted fake): the option set on deps is what `resolveExecutor()` returns,
* and it defaults to the model backend. The cli-agent one-shot wiring itself is
* engine-side (`@fusion/engine` runOneShotSession); this asserts the plugin
* carries the choice through, per the plugin-skills option-threading learning.
*/
let h: TestHarness;
beforeEach(() => {
h = makeHarness();
});
afterEach(() => {
h.close();
});
function makeOrch(executor?: CeSessionExecutor) {
return new CeOrchestrator({
ctx: h.ctx,
createInteractiveAiSession: vi.fn(async () => ({ session: {} as never })),
projectRoot: h.projectRoot,
executor,
});
}
describe("CE executor seam (U9)", () => {
it("defaults to the model backend when no executor option is supplied", () => {
expect(makeOrch().resolveExecutor()).toEqual({ kind: "model" });
});
it("threads a cli-agent executor selection through deps → resolver", () => {
const orch = makeOrch({ kind: "cli-agent", adapterId: "claude-code" });
expect(orch.resolveExecutor()).toEqual({ kind: "cli-agent", adapterId: "claude-code" });
});
});

View File

@@ -85,6 +85,20 @@ export class CeTurnTimeoutError extends Error {
}
}
/**
* Which session backend a CE stage runs on (U9 seam).
*
* - `model` (default): the model-backed interactive AI session (existing path).
* - `cli-agent`: a CLI agent adapter, run as a read-only one-shot per stage.
*
* The CE plugin threads this choice end-to-end (deps → resolver) per the
* plugin-skills option-threading learning; the cli-agent one-shot wiring itself
* lives engine-side in `@fusion/engine` (`runOneShotSession`).
*/
export type CeSessionExecutor =
| { kind: "model" }
| { kind: "cli-agent"; adapterId: string };
export interface OrchestratorDeps {
ctx: PluginContext;
/**
@@ -96,6 +110,19 @@ export interface OrchestratorDeps {
projectRoot?: string;
/** Override the per-turn timeout (ms). */
turnTimeoutMs?: number;
/**
* Session backend selector (U9). Defaults to `{ kind: "model" }`. When set to
* `{ kind: "cli-agent", adapterId }`, CE sessions select the CLI-agent
* one-shot executor. Threaded through to `resolveExecutor()` so callers can
* route a CE stage onto a CLI adapter.
*
* DEVIATION (see U9 report): the cli-agent branch of the *live* CE stage loop
* (replacing the interactive factory with one-shot turns inside
* `startStage`/`continueStage`) is not yet wired — only the option seam and
* its resolver contract land here. The engine-side one-shot runner is ready;
* the remaining work is invoking it from the stage loop.
*/
executor?: CeSessionExecutor;
}
/**
@@ -176,6 +203,7 @@ export class CeOrchestrator {
private readonly factory: CreateInteractiveAiSessionFactory | undefined;
private readonly projectRoot: string;
private readonly turnTimeoutMs: number;
private readonly executor: CeSessionExecutor;
/** Live in-memory session handles keyed by ce_session id. */
private readonly live = new Map<string, InteractiveAiSession>();
/** Mid-turn working output per session (transient; flushed to history on settle). */
@@ -194,6 +222,17 @@ export class CeOrchestrator {
this.factory = deps.createInteractiveAiSession ?? deps.ctx.createInteractiveAiSession;
this.projectRoot = deps.projectRoot ?? deps.ctx.taskStore.getRootDir();
this.turnTimeoutMs = deps.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS;
this.executor = deps.executor ?? { kind: "model" };
}
/**
* Resolve the session backend for a CE stage (U9 seam). Threaded from
* `OrchestratorDeps.executor`; defaults to the model-backed interactive
* session. Exposed so the seam contract is directly assertable in tests and so
* future stage-loop wiring has a single resolution point.
*/
resolveExecutor(): CeSessionExecutor {
return this.executor;
}
/**

View File

@@ -743,10 +743,10 @@ describe("RoadmapStore", () => {
});
describe("schema version", () => {
it("schema version is 110 after init", () => {
it("schema version is 112 after init", () => {
// Tracks @fusion/core's SCHEMA_VERSION (the roadmap store layers on core's
// Database). Bump this in lockstep when core adds a migration.
expect(db.getSchemaVersion()).toBe(110);
expect(db.getSchemaVersion()).toBe(112);
});
});