Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions sdk/go/harness/coverage_branches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ")
Expand Down
7 changes: 5 additions & 2 deletions sdk/go/harness/opencode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions sdk/python/agentfield/harness/providers/opencode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions sdk/python/tests/test_harness_provider_opencode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down
24 changes: 15 additions & 9 deletions sdk/typescript/src/harness/providers/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,36 @@ export class OpenCodeProvider implements HarnessProvider {
}

async execute(prompt: string, options: Record<string, unknown>): Promise<RawResult> {
const cmd = [this.bin];
// opencode v1.4+ uses the `run` subcommand. Prior `-c <dir> -p <prompt>`
// 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<string, string> = { ...(options.env as Record<string, string>) };

// 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 {
Expand Down
13 changes: 6 additions & 7 deletions sdk/typescript/tests/harness_provider_opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
Expand Down
Loading