From 9794291319a7570e2e3b595dd26dc7d96af6dd0e Mon Sep 17 00:00:00 2001 From: Harsh Batheja <40922251+harsh-batheja@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:02:15 +0530 Subject: [PATCH] fix: make opencode bootstrap exit before attach (#427) * fix: make opencode bootstrap exit before attach Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus * test: align opencode integration assertions with bootstrap resume flow Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --------- Co-authored-by: Sisyphus --- .../src/agent-opencode.integration.test.ts | 53 ++++++------ .../plugins/agent-opencode/src/index.test.ts | 84 +++++++++++-------- packages/plugins/agent-opencode/src/index.ts | 9 +- 3 files changed, 81 insertions(+), 65 deletions(-) diff --git a/packages/integration-tests/src/agent-opencode.integration.test.ts b/packages/integration-tests/src/agent-opencode.integration.test.ts index dc1326e43..a5e1e87e3 100644 --- a/packages/integration-tests/src/agent-opencode.integration.test.ts +++ b/packages/integration-tests/src/agent-opencode.integration.test.ts @@ -202,10 +202,11 @@ describe("getLaunchCommand (integration)", () => { prompt: "fix the bug", }); expect(cmd).toContain("--agent 'sisyphus'"); - expect(cmd).toContain("opencode run --format json --title 'AO:test-1'"); + expect(cmd).toContain( + "opencode run --format json --title 'AO:test-1' --agent 'sisyphus' --command true", + ); expect(cmd).toContain("'fix the bug'"); - expect(cmd).not.toContain("--prompt"); - expect(cmd).toContain("&& exec opencode --session"); + expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'fix the bug'"); expect(cmd).toContain("--agent 'sisyphus'"); }); @@ -215,10 +216,12 @@ describe("getLaunchCommand (integration)", () => { systemPrompt: "You are an orchestrator", prompt: "do the task", }); - expect(cmd).toContain("opencode run --format json --title 'AO:test-1'"); - expect(cmd).not.toContain("--prompt"); - expect(cmd).toContain("You are an orchestrator"); - expect(cmd).toContain("do the task"); + expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true"); + expect(cmd).toContain( + `exec opencode --session "$SES_ID" --prompt 'You are an orchestrator + +do the task'`, + ); }); it("generates correct command with systemPromptFile", () => { @@ -227,10 +230,9 @@ describe("getLaunchCommand (integration)", () => { systemPromptFile: "/tmp/orchestrator-prompt.md", prompt: "do the task", }); - expect(cmd).toContain("opencode run --format json --title 'AO:test-1'"); - expect(cmd).not.toContain("--prompt"); + expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true"); expect(cmd).toContain( - "$(cat '/tmp/orchestrator-prompt.md'; printf '\\n\\n'; printf %s 'do the task')", + "exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/orchestrator-prompt.md'; printf '\\n\\n'; printf %s 'do the task')\"", ); }); @@ -252,10 +254,12 @@ describe("getLaunchCommand (integration)", () => { prompt: "review this code", }); expect(cmd).toContain("--agent 'oracle'"); - expect(cmd).toContain("opencode run --format json --title 'AO:test-1'"); - expect(cmd).not.toContain("--prompt"); - expect(cmd).toContain("You are an expert"); - expect(cmd).toContain("review this code"); + expect(cmd).toContain( + "opencode run --format json --title 'AO:test-1' --agent 'oracle' --model 'gpt-5.2' --command true", + ); + expect(cmd).toContain( + "exec opencode --session \"$SES_ID\" --prompt 'You are an expert\n\nreview this code' --agent 'oracle' --model 'gpt-5.2'", + ); expect(cmd).toContain("--model 'gpt-5.2'"); }); @@ -277,9 +281,11 @@ describe("getLaunchCommand (integration)", () => { systemPromptFile: "/tmp/orchestrator-prompt.md", }); expect(cmd).toContain( - "opencode run --format json --title 'AO:test-orchestrator' \"$(cat '/tmp/orchestrator-prompt.md')\"", + "opencode run --format json --title 'AO:test-orchestrator' --command true", + ); + expect(cmd).toContain( + 'exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/orchestrator-prompt.md\')"', ); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); }); it("escapes single quotes in systemPrompt", () => { @@ -303,9 +309,9 @@ describe("getLaunchCommand (integration)", () => { ...baseConfig, prompt: "fix and `backtick` and 'quote'", }); - expect(cmd).toContain("opencode run --format json --title 'AO:test-1'"); + expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true"); expect(cmd).toContain("fix and `backtick`"); - expect(cmd).not.toContain("--prompt"); + expect(cmd).toContain('exec opencode --session "$SES_ID" --prompt'); }); it("handles empty prompt", () => { @@ -314,7 +320,7 @@ describe("getLaunchCommand (integration)", () => { prompt: "", }); expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true"); - expect(cmd).toContain("&& exec opencode --session"); + expect(cmd).toContain('exec opencode --session "$SES_ID"'); expect(cmd).toContain("opencode session list --format json"); expect(cmd).toContain("AO:test-1"); }); @@ -324,9 +330,8 @@ describe("getLaunchCommand (integration)", () => { ...baseConfig, prompt: "line1\nline2", }); - expect(cmd).toContain("opencode run --format json --title 'AO:test-1'"); - expect(cmd).toContain("'line1"); - expect(cmd).not.toContain("--prompt"); + expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true"); + expect(cmd).toContain('exec opencode --session "$SES_ID" --prompt \'line1'); }); it("uses run bootstrap launch for fresh sessions", () => { @@ -335,8 +340,8 @@ describe("getLaunchCommand (integration)", () => { prompt: "start work", }); expect(cmd).toContain("--title 'AO:test-1'"); - expect(cmd).not.toContain("--prompt 'start work'"); - expect(cmd).toContain("opencode run --format json --title 'AO:test-1' 'start work'"); + expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true"); + expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'start work'"); expect(cmd).toContain('exec opencode --session "$SES_ID"'); }); diff --git a/packages/plugins/agent-opencode/src/index.test.ts b/packages/plugins/agent-opencode/src/index.test.ts index 07a3b23b7..946e2f800 100644 --- a/packages/plugins/agent-opencode/src/index.test.ts +++ b/packages/plugins/agent-opencode/src/index.test.ts @@ -109,8 +109,8 @@ describe("getLaunchCommand", () => { it("uses --prompt with shell-escaped prompt", () => { const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "Fix it" })); - expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' 'Fix it'"); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); + expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'Fix it'"); }); it("includes --model with shell-escaped value", () => { @@ -123,16 +123,18 @@ describe("getLaunchCommand", () => { makeLaunchConfig({ prompt: "Go", model: "claude-sonnet-4-5-20250929" }), ); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' 'Go'", + "opencode run --format json --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' --command true", + ); + expect(cmd).toContain( + "exec opencode --session \"$SES_ID\" --prompt 'Go' --model 'claude-sonnet-4-5-20250929'", ); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929'"); }); it("escapes single quotes in prompt (POSIX shell escaping)", () => { const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "it's broken" })); - expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' 'it'\\''s broken'"); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); + expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'it'\\''s broken'"); }); it("omits optional flags when not provided", () => { @@ -151,9 +153,11 @@ describe("getLaunchCommand", () => { makeLaunchConfig({ subagent: "sisyphus", prompt: "fix bug" }), ); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' 'fix bug'", + "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' --command true", + ); + expect(cmd).toContain( + "exec opencode --session \"$SES_ID\" --prompt 'fix bug' --agent 'sisyphus'", ); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); expect(cmd).toContain("--agent 'sisyphus'"); }); @@ -166,9 +170,11 @@ describe("getLaunchCommand", () => { }), ); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' --model 'claude-sonnet-4-5-20250929' 'fix the bug'", + "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' --model 'claude-sonnet-4-5-20250929' --command true", + ); + expect(cmd).toContain( + "exec opencode --session \"$SES_ID\" --prompt 'fix the bug' --agent 'sisyphus' --model 'claude-sonnet-4-5-20250929'", ); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); expect(cmd).toContain("--agent 'sisyphus"); expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929"); }); @@ -207,8 +213,8 @@ describe("getLaunchCommand", () => { it("backward compatible: no agent flag when subagent not provided", () => { const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "fix it" })); expect(cmd).not.toContain("--agent"); - expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' 'fix it'"); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); + expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'fix it'"); }); it("combines model and prompt without agent (backward compatible)", () => { @@ -217,9 +223,11 @@ describe("getLaunchCommand", () => { ); expect(cmd).not.toContain("--agent"); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' 'Go'", + "opencode run --format json --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' --command true", + ); + expect(cmd).toContain( + "exec opencode --session \"$SES_ID\" --prompt 'Go' --model 'claude-sonnet-4-5-20250929'", ); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929"); }); @@ -227,47 +235,48 @@ describe("getLaunchCommand", () => { const cmd = agent.getLaunchCommand( makeLaunchConfig({ systemPrompt: "You are an orchestrator" }), ); - expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'"); - expect(cmd).toContain("'You are an orchestrator"); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); + expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'You are an orchestrator'"); }); it("generates command with systemPrompt and task prompt", () => { const cmd = agent.getLaunchCommand( makeLaunchConfig({ systemPrompt: "You are an orchestrator", prompt: "do the task" }), ); - expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'"); - expect(cmd).not.toContain("--prompt 'You are an orchestrator"); - expect(cmd).toContain("do the task"); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); + expect(cmd).toContain( + `exec opencode --session "$SES_ID" --prompt 'You are an orchestrator + +do the task'`, + ); }); it("escapes single quotes in systemPrompt", () => { const cmd = agent.getLaunchCommand(makeLaunchConfig({ systemPrompt: "it's important" })); - expect(cmd).toContain("'it'\\''s important'"); + expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'it'\\''s important'"); }); it("handles very long systemPrompt", () => { const longPrompt = "A".repeat(500); const cmd = agent.getLaunchCommand(makeLaunchConfig({ systemPrompt: longPrompt })); - expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'"); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); expect(cmd.length).toBeGreaterThan(500); }); it("generates command with systemPromptFile via shell substitution", () => { const cmd = agent.getLaunchCommand(makeLaunchConfig({ systemPromptFile: "/tmp/prompt.md" })); - expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' \"$(cat '/tmp/prompt.md')\"", - ); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); + expect(cmd).toContain('exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/prompt.md\')"'); }); it("escapes path in systemPromptFile", () => { const cmd = agent.getLaunchCommand( makeLaunchConfig({ systemPromptFile: "/tmp/it's-prompt.md" }), ); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' \"$(cat '/tmp/it'\\''s-prompt.md')\"", + "exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/it'\\''s-prompt.md')\"", ); - expect(cmd).toContain('exec opencode --session "$SES_ID"'); }); it("systemPromptFile takes precedence over systemPrompt", () => { @@ -277,10 +286,10 @@ describe("getLaunchCommand", () => { systemPromptFile: "/tmp/file-prompt.md", }), ); + expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true"); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' \"$(cat '/tmp/file-prompt.md')\"", + 'exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/file-prompt.md\')"', ); - expect(cmd).toContain("exec opencode --session"); expect(cmd).not.toContain("direct prompt"); }); @@ -293,11 +302,12 @@ describe("getLaunchCommand", () => { }), ); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' \"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')\"", + "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' --command true", + ); + expect(cmd).toContain( + "exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')\" --agent 'sisyphus'", ); - expect(cmd).toContain("exec opencode --session"); expect(cmd).toContain("--agent 'sisyphus"); - expect(cmd).not.toContain("--prompt"); expect(cmd).toContain( "$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')", ); @@ -311,10 +321,10 @@ describe("getLaunchCommand", () => { systemPromptFile: "/tmp/orchestrator.md", }), ); + expect(cmd).toContain("opencode run --format json --title 'AO:my-orchestrator' --command true"); expect(cmd).toContain( - "opencode run --format json --title 'AO:my-orchestrator' \"$(cat '/tmp/orchestrator.md')\"", + 'exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/orchestrator.md\')"', ); - expect(cmd).toContain("exec opencode --session"); }); it("combines systemPromptFile with subagent and prompt - shell escape", () => { @@ -326,9 +336,11 @@ describe("getLaunchCommand", () => { }), ); expect(cmd).toContain( - "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' \"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')\"", + "opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' --command true", + ); + expect(cmd).toContain( + "exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')\" --agent 'sisyphus'", ); - expect(cmd).not.toContain("--prompt"); expect(cmd).toContain( "$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')", ); diff --git a/packages/plugins/agent-opencode/src/index.ts b/packages/plugins/agent-opencode/src/index.ts index a842c2461..49f54de98 100644 --- a/packages/plugins/agent-opencode/src/index.ts +++ b/packages/plugins/agent-opencode/src/index.ts @@ -178,17 +178,16 @@ function createOpenCodeAgent(): Agent { ]; const captureScript = buildSessionIdCaptureScript(); const fallbackScript = buildSessionLookupScript(); - const runCommand = promptValue - ? ["opencode", "run", ...runOptions, promptValue].join(" ") - : ["opencode", "run", ...runOptions, "--command", "true"].join(" "); - const sharedOptionsSuffix = sharedOptions.length > 0 ? ` ${sharedOptions.join(" ")}` : ""; + const runCommand = ["opencode", "run", ...runOptions, "--command", "true"].join(" "); + const resumeOptions = [...(promptValue ? ["--prompt", promptValue] : []), ...sharedOptions]; + const resumeOptionsSuffix = resumeOptions.length > 0 ? ` ${resumeOptions.join(" ")}` : ""; const missingSessionError = shellEscape( `failed to discover OpenCode session ID for AO:${config.sessionId}`, ); return [ `SES_ID=$(${runCommand} | node -e ${shellEscape(captureScript)})`, `if [ -z "$SES_ID" ]; then SES_ID=$(opencode session list --format json | node -e ${shellEscape(fallbackScript)} ${shellEscape(`AO:${config.sessionId}`)}); fi`, - `[ -n "$SES_ID" ] && exec opencode --session "$SES_ID"${sharedOptionsSuffix}; echo ${missingSessionError} >&2; exit 1`, + `[ -n "$SES_ID" ] && exec opencode --session "$SES_ID"${resumeOptionsSuffix}; echo ${missingSessionError} >&2; exit 1`, ].join("; "); }