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 <clio-agent@sisyphuslabs.ai>

* test: align opencode integration assertions with bootstrap resume flow

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Harsh Batheja 2026-03-11 20:02:15 +05:30 committed by GitHub
parent 2064595633
commit 9794291319
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 65 deletions

View File

@ -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"');
});

View File

@ -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')",
);

View File

@ -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("; ");
}