diff --git a/sdk/go/harness/coverage_branches_test.go b/sdk/go/harness/coverage_branches_test.go index efc637b77..7770c1a5f 100644 --- a/sdk/go/harness/coverage_branches_test.go +++ b/sdk/go/harness/coverage_branches_test.go @@ -205,13 +205,15 @@ fi assert.False(t, raw.IsError) // $PWD reflects the subprocess working directory (Options.Cwd). assert.Contains(t, raw.Result, dir) - // opencode 1.14+ surface: `run` subcommand, --dir for project, -m for - // model, --dangerously-skip-permissions for headless, prompt is - // positional. -c, -q, -p are deprecated/rebound (see issue #517). + // opencode 1.14+ surface: `run` subcommand, --dir for project, -m + // for model, prompt is positional. -c, -q, -p are + // deprecated/rebound (see issue #517). --dangerously-skip-permissions + // is rejected by `run` on v1.14 — opencode prints help and exits 0, + // see agentfield#582 — so it must NOT be on the command line. assert.Contains(t, raw.Result, "run") assert.Contains(t, raw.Result, "--dir /ignored/project") assert.Contains(t, raw.Result, "-m stub-model") - assert.Contains(t, raw.Result, "--dangerously-skip-permissions") + assert.NotContains(t, raw.Result, "--dangerously-skip-permissions") // Prompt is the last positional argument (no -p flag in front). assert.Regexp(t, `\sprompt$`, strings.TrimSpace(raw.Result)) assert.NotContains(t, raw.Result, "-q ") diff --git a/sdk/go/harness/opencode.go b/sdk/go/harness/opencode.go index dab1b7076..378010900 100644 --- a/sdk/go/harness/opencode.go +++ b/sdk/go/harness/opencode.go @@ -75,8 +75,11 @@ func (p *OpenCodeProvider) Execute(ctx context.Context, prompt string, options O cmd = append(cmd, "-m", options.Model) } - // Skip the interactive permission prompt for headless execution. - cmd = append(cmd, "--dangerously-skip-permissions") + // opencode v1.14 does not accept --dangerously-skip-permissions on the + // `run` subcommand — passing it makes yargs print the run-help screen + // to stdout and exit 0, which the SDK then captures as the LLM + // response. opencode in non-TTY mode proceeds without permission + // prompting, so no flag is needed. See agentfield#582. // Prepend system prompt if provided. OpenCode has no native // --system-prompt flag, so inline it ahead of the user prompt. diff --git a/sdk/python/agentfield/harness/providers/opencode.py b/sdk/python/agentfield/harness/providers/opencode.py index 7bc161965..15968597b 100644 --- a/sdk/python/agentfield/harness/providers/opencode.py +++ b/sdk/python/agentfield/harness/providers/opencode.py @@ -163,8 +163,11 @@ async def _execute_impl(self, prompt: str, options: dict[str, object]) -> RawRes if options.get("model"): cmd.extend(["-m", str(options["model"])]) - # Skip interactive permission prompts for headless execution - cmd.append("--dangerously-skip-permissions") + # opencode v1.14 does not accept --dangerously-skip-permissions on the + # `run` subcommand — passing it makes yargs print the run-help screen + # to stdout and exit 0, which the SDK then captures as the LLM + # response. opencode in non-TTY mode proceeds without permission + # prompting, so no flag is needed. See agentfield#582. # Handle system prompt - prepend to user prompt since OpenCode # has no native --system-prompt flag diff --git a/sdk/python/tests/test_harness_provider_opencode.py b/sdk/python/tests/test_harness_provider_opencode.py index cd793c73c..687177ca1 100644 --- a/sdk/python/tests/test_harness_provider_opencode.py +++ b/sdk/python/tests/test_harness_provider_opencode.py @@ -45,7 +45,6 @@ async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): "json", "--dir", "/tmp/work", - "--dangerously-skip-permissions", "hello", ] assert captured["env"]["A"] == "1" @@ -129,7 +128,6 @@ async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): "json", "-m", "openai/gpt-5", - "--dangerously-skip-permissions", "hello", ] # Model is now passed via -m flag, not environment variable @@ -400,8 +398,9 @@ async def capture_cmd(cmd, *, env=None, cwd=None, timeout=None): assert "--dir" in captured_cmd, "Must use --dir for project directory (v1.4+)" # Must use -m for model assert "-m" in captured_cmd, "Must use -m flag for model (v1.4+)" - # Must skip permissions for headless execution - assert "--dangerously-skip-permissions" in cmd_str + # Must NOT use --dangerously-skip-permissions: opencode v1.14 rejects it + # on `run` and prints help to stdout, see agentfield#582. + assert "--dangerously-skip-permissions" not in cmd_str # Prompt must be positional (last arg) assert captured_cmd[-1] == "build the feature" diff --git a/sdk/typescript/src/harness/providers/opencode.ts b/sdk/typescript/src/harness/providers/opencode.ts index 9f1d7cb31..f3262fce1 100644 --- a/sdk/typescript/src/harness/providers/opencode.ts +++ b/sdk/typescript/src/harness/providers/opencode.ts @@ -11,30 +11,36 @@ export class OpenCodeProvider implements HarnessProvider { } async execute(prompt: string, options: Record): Promise { - const cmd = [this.bin]; + // opencode v1.4+ uses the `run` subcommand. Prior `-c -p ` + // syntax is broken on v1.14: `-c` now means `--continue` (a boolean) and + // there is no top-level `-p` flag, so opencode prints help to stdout and + // exits 0 — the SDK then captures the help screen as the LLM response. + // See agentfield#582. + const cmd = [this.bin, 'run']; - // Use -c for cwd (project directory) + // Use --dir for project directory. if (options.cwd && typeof options.cwd === 'string') { - cmd.push('-c', options.cwd); + cmd.push('--dir', options.cwd); } else if (options.project_dir && typeof options.project_dir === 'string') { - cmd.push('-c', options.project_dir); + cmd.push('--dir', options.project_dir); } - // Model is set via environment variable, not CLI flag const env: Record = { ...(options.env as Record) }; + + // Pass model via -m flag on the run subcommand (not env var). if (options.model) { - env['MODEL'] = String(options.model); + cmd.push('-m', String(options.model)); } // Handle system prompt - prepend to user prompt since OpenCode - // has no native --system-prompt flag + // has no native --system-prompt flag. let effectivePrompt = prompt; if (options.system_prompt && typeof options.system_prompt === 'string' && options.system_prompt.trim()) { effectivePrompt = `SYSTEM INSTRUCTIONS:\n${options.system_prompt.trim()}\n\n---\n\nUSER REQUEST:\n${prompt}`; } - // Use -p for single prompt mode (non-interactive) - cmd.push('-p', effectivePrompt); + // Prompt is the positional `message` arg to `opencode run`. + cmd.push(effectivePrompt); const startApi = Date.now(); try { diff --git a/sdk/typescript/tests/harness_provider_opencode.test.ts b/sdk/typescript/tests/harness_provider_opencode.test.ts index df94ffa89..9ba0f6c59 100644 --- a/sdk/typescript/tests/harness_provider_opencode.test.ts +++ b/sdk/typescript/tests/harness_provider_opencode.test.ts @@ -22,9 +22,10 @@ describe('opencode provider', () => { env: { A: '1' }, }); - expect(cli.runCli).toHaveBeenCalledWith(['/usr/local/bin/opencode', '-c', '/tmp/work', '-p', 'hello'], { - env: { A: '1' }, - }); + expect(cli.runCli).toHaveBeenCalledWith( + ['/usr/local/bin/opencode', 'run', '--dir', '/tmp/work', 'hello'], + { env: { A: '1' } }, + ); expect(result.isError).toBe(false); expect(result.result).toBe('final text'); expect(result.metrics.numTurns).toBe(1); @@ -68,10 +69,8 @@ describe('opencode provider', () => { const result = await provider.execute('hello', { model: 'openai/gpt-5' }); expect(cli.runCli).toHaveBeenCalledWith( - ['opencode', '-p', 'hello'], - { - env: { MODEL: 'openai/gpt-5' }, - } + ['opencode', 'run', '-m', 'openai/gpt-5', 'hello'], + { env: {} }, ); expect(result.isError).toBe(false); });