diff --git a/packages/paperclip-runner/src/backends/codex-native-backend.ts b/packages/paperclip-runner/src/backends/codex-native-backend.ts index 1653c75a6f..47a83bb5fe 100644 --- a/packages/paperclip-runner/src/backends/codex-native-backend.ts +++ b/packages/paperclip-runner/src/backends/codex-native-backend.ts @@ -92,6 +92,10 @@ function createTransportBackedNativeSessionBackend( ): NativeSessionBackend { const driverIdentity = transportDriverIdentity(input); const isCodex = input.provider.kind === "codex"; + const supportsCollaborativePlanning = + isCodex || + input.provider.kind === "opencode" || + input.provider.kind === "acpx"; if ( input.provider.kind === "codex" && input.provider.approvalPolicy !== undefined @@ -113,14 +117,18 @@ function createTransportBackedNativeSessionBackend( baseInstructions: nativeSystemInstructions(input), includeSkillInstructions: isCodex && "runtimeContext" in input, requestedCollaborationMode: - isCodex && "executionMode" in input ? input.executionMode : "default", + supportsCollaborativePlanning && "executionMode" in input + ? input.executionMode + : "default", taskEnvelope: createCodexTaskEnvelope({ objective: input.completionContract.contract.objective, contractRevision: input.completionContract.contract.revision, criteria: input.completionContract.contract.criteria, constraints: [ "Work only inside the supplied working directory.", - ...(isCodex && "executionMode" in input && input.executionMode === "plan" + ...(supportsCollaborativePlanning && + "executionMode" in input && + input.executionMode === "plan" ? [ "Use native plan collaboration mode and do not modify workspace files.", "Treat the supplied Paperclip planning context as the canonical pinned base revision.", @@ -143,7 +151,9 @@ function createTransportBackedNativeSessionBackend( capabilities: isCodex ? {} : { steering: false, goals: false, threadLineage: false }, - collaborationModes: isCodex ? ["default", "plan"] : ["default"], + collaborationModes: supportsCollaborativePlanning + ? ["default", "plan"] + : ["default"], requireProviderSessionIdentity: options.transportFactory !== undefined, })); } diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index f31d0c3b4c..260751e5c0 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import type { NativeExecutionInput } from "../contracts/native-execution.js"; +import { FakeCodexTransport } from "../drivers/codex/codex-app-server-driver.test-support.js"; import { createNativeSessionBackend } from "../index.js"; import { createCodexNativeSessionBackend } from "./codex-native-backend.js"; @@ -126,6 +127,23 @@ function opencodeExecution(): NativeExecutionInput { }; } +function planningExecution(input: NativeExecutionInput): NativeExecutionInput { + return { + ...input, + schema: "paperclip.native-execution-input.v2", + task: { ...input.task, workMode: "planning" }, + executionMode: "plan", + planningContext: { + documentId: null, + baseRevisionId: null, + baseRevisionNumber: 0, + markdown: "", + sha256: "empty-plan", + reviewContext: {}, + }, + }; +} + function managedExecution( kind: "claude_managed" | "aws_agentcore", ): NativeExecutionInput { @@ -251,10 +269,53 @@ describe("native backend factory", () => { resume: true, interruption: true, dynamicTools: true, + collaborationModes: ["default", "plan"], }, }); }); + it.each([ + ["OpenCode", opencodeExecution()], + ["ACPX Codex", acpxExecution("codex")], + ["ACPX Claude", acpxExecution("claude")], + ])( + "opens %s planning runs through the runner-managed plan contract", + async (_label, input) => { + const transport = new FakeCodexTransport(); + const backend = createNativeSessionBackend(planningExecution(input), { + codexTransportFactory: () => transport, + }); + + await expect(backend.descriptor()).resolves.toMatchObject({ + capabilities: { collaborationModes: ["default", "plan"] }, + }); + const session = await backend.openSession({ + identity: { + runId: "run", + sessionId: "session", + companyId: "company", + issueId: "issue", + agentId: "agent", + }, + workingDirectory: process.cwd(), + }); + + await expect( + session.startTurn({ + message: { role: "user", text: "Author a plan." }, + requestedCollaborationMode: "plan", + }), + ).resolves.toMatchObject({ effectiveCollaborationMode: "plan" }); + expect( + transport.calls.find((call) => call.method === "thread/start")?.params, + ).toMatchObject({ permissions: "paperclip-runner-workspace-read-only" }); + expect( + transport.calls.find((call) => call.method === "turn/start")?.params, + ).toMatchObject({ collaborationMode: { mode: "plan" } }); + await session.close({ reason: "test complete" }); + }, + ); + it.each([ [ "claude_managed" as const, @@ -337,6 +398,7 @@ describe("native backend factory", () => { resume: true, interruption: true, dynamicTools: true, + collaborationModes: ["default", "plan"], }, }); },