Skip to content

Commit abd6b0a

Browse files
committed
feat: add opencode harness coverage and streaming tests
1 parent 3413873 commit abd6b0a

9 files changed

Lines changed: 233 additions & 35 deletions

File tree

cmd/dun/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ LOOP MODE:
120120
and repeats until all checks pass or max iterations is reached.
121121
122122
Options:
123-
--harness Agent to use: codex, claude, gemini (default: from config)
123+
--harness Agent to use: codex, claude, gemini, opencode (default: from config)
124124
--automation Mode: manual, plan, auto, yolo (default: auto)
125125
--max-iterations Safety limit (default: 100)
126126
--dry-run Show prompt without calling agent
@@ -130,7 +130,7 @@ LOOP MODE:
130130
131131
Quorum Options (multi-agent consensus):
132132
--quorum Strategy: any, majority, unanimous, or number (e.g., 2)
133-
--harnesses Comma-separated list of harnesses (e.g., codex,claude,gemini)
133+
--harnesses Comma-separated list of harnesses (e.g., codex,claude,gemini,opencode)
134134
--cost-mode Run harnesses sequentially to minimize cost
135135
--escalate Pause for human review on conflict
136136
--prefer Preferred harness on conflict (e.g., codex)
@@ -142,7 +142,7 @@ LOOP MODE:
142142
dun loop --automation yolo # Allow autonomous edits
143143
dun loop --dry-run # Preview prompt
144144
dun loop --verbose # Show prompt and responses
145-
dun loop --quorum majority --harnesses codex,claude,gemini
145+
dun loop --quorum majority --harnesses codex,claude,gemini,opencode
146146
dun loop --quorum 2 --harnesses codex,claude --prefer codex
147147
148148
VERSION:
@@ -500,7 +500,7 @@ func runLoop(args []string, stdout io.Writer, stderr io.Writer) int {
500500
fs := flag.NewFlagSet("loop", flag.ContinueOnError)
501501
fs.SetOutput(stderr)
502502
configPath := fs.String("config", explicitConfig, "path to config file")
503-
harness := fs.String("harness", "", "agent harness (codex|claude|gemini); default from config")
503+
harness := fs.String("harness", "", "agent harness (codex|claude|gemini|opencode); default from config")
504504
automation := fs.String("automation", opts.AutomationMode, "automation mode (manual|plan|auto|yolo)")
505505
maxIterations := fs.Int("max-iterations", 100, "maximum iterations before stopping")
506506
dryRun := fs.Bool("dry-run", false, "print prompt without calling harness")

cmd/dun/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,7 @@ func TestRunHelpIncludesLoop(t *testing.T) {
13201320
if !strings.Contains(output, "--max-iterations") {
13211321
t.Fatalf("help should document max-iterations option")
13221322
}
1323-
if !strings.Contains(output, "codex, claude, gemini") {
1323+
if !strings.Contains(output, "codex, claude, gemini, opencode") {
13241324
t.Fatalf("help should list available harnesses")
13251325
}
13261326
}

docs/design/contracts/API-001-dun-cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ $ dun check --prompt
207207

208208
**Options**:
209209
- `--config` : Path to config file (default `.dun/config.yaml` if present)
210-
- `--harness` : Agent harness (`codex`, `claude`, `gemini`, default from config)
210+
- `--harness` : Agent harness (`codex`, `claude`, `gemini`, `opencode`, default from config)
211211
- `--automation` : Automation mode (`manual`, `plan`, `auto`, `yolo`, default `auto`)
212212
- `--max-iterations` : Maximum iterations before stopping (default `100`)
213213
- `--dry-run` : Print prompt without calling harness

internal/dun/harness.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func NewHarnessRegistry() *HarnessRegistry {
107107
r.Register("claude", NewClaudeHarness)
108108
r.Register("gemini", NewGeminiHarness)
109109
r.Register("codex", NewCodexHarness)
110+
r.Register("opencode", NewOpenCodeHarness)
110111
r.Register("mock", NewMockHarness)
111112
return r
112113
}
@@ -300,6 +301,44 @@ func (h *CodexHarness) runCommand(ctx context.Context, name string, stdin string
300301
return runHarnessCommand(ctx, h.config, name, stdin, args)
301302
}
302303

304+
// OpenCodeHarness wraps the OpenCode CLI for agent execution.
305+
type OpenCodeHarness struct {
306+
config HarnessConfig
307+
}
308+
309+
// NewOpenCodeHarness creates a new OpenCode harness.
310+
func NewOpenCodeHarness(config HarnessConfig) Harness {
311+
if config.Command == "" {
312+
config.Command = "opencode"
313+
}
314+
if config.AutomationMode == "" {
315+
config.AutomationMode = AutomationAuto
316+
}
317+
return &OpenCodeHarness{config: config}
318+
}
319+
320+
// Name returns "opencode".
321+
func (h *OpenCodeHarness) Name() string {
322+
return "opencode"
323+
}
324+
325+
// Execute runs the OpenCode CLI with the given prompt.
326+
// OpenCode expects the prompt as a positional message for `opencode run`.
327+
func (h *OpenCodeHarness) Execute(ctx context.Context, prompt string) (string, error) {
328+
args := []string{"run", prompt}
329+
330+
return h.runCommand(ctx, h.config.Command, prompt, args...)
331+
}
332+
333+
// SupportsAutomation returns true for all automation modes.
334+
func (h *OpenCodeHarness) SupportsAutomation(mode AutomationMode) bool {
335+
return true
336+
}
337+
338+
func (h *OpenCodeHarness) runCommand(ctx context.Context, name string, stdin string, args ...string) (string, error) {
339+
return runHarnessCommand(ctx, h.config, name, stdin, args)
340+
}
341+
303342
// MockHarness is a harness for testing that returns configurable responses.
304343
type MockHarness struct {
305344
config HarnessConfig

internal/dun/harness_integration_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ func TestCodexHarnessIntegration(t *testing.T) {
2525
runHarnessIntegration(t, "codex", NewCodexHarness)
2626
}
2727

28+
func TestOpenCodeHarnessIntegration(t *testing.T) {
29+
runHarnessIntegration(t, "opencode", NewOpenCodeHarness)
30+
}
31+
2832
func runHarnessIntegration(t *testing.T, command string, factory HarnessFactory) {
2933
t.Helper()
3034
if os.Getenv(harnessIntegrationEnv) == "" {

0 commit comments

Comments
 (0)