Skip to content

Commit 54b248b

Browse files
committed
Merge branch 'main' of https://github.com/easel/dun
2 parents eb6f604 + 0eb3c69 commit 54b248b

21 files changed

Lines changed: 680 additions & 50 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
- `dun check --prompt` - Get work list as a prompt (pick ONE task, complete it, exit)
88
- `dun loop --harness claude` - Run autonomous loop with Claude
99
- `dun loop --harness gemini` - Run autonomous loop with Gemini
10+
- `dun loop --harness opencode` - Run autonomous loop with OpenCode
11+
- `dun loop --harness pi` - Run autonomous loop with Pi
12+
- `dun loop --harness cursor` - Run autonomous loop with Cursor
1013
- `dun help` - Full documentation
1114

1215
Autonomous iteration pattern:

cmd/dun/harnesses.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
var defaultQuorumHarnesses = []string{"codex", "claude", "gemini"}
11-
var harnessPreferenceOrder = []string{"codex", "claude", "gemini", "opencode"}
11+
var harnessPreferenceOrder = []string{"codex", "claude", "gemini", "opencode", "pi", "cursor"}
1212

1313
func resolveHarnessesForQuorum(explicit string) []string {
1414
if strings.TrimSpace(explicit) != "" {

cmd/dun/loop_cache_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func TestRunLoopQuorumUsesCachedHarnesses(t *testing.T) {
1414

1515
cache := dun.HarnessCache{
1616
LastCheck: time.Now(),
17-
Harnesses: []dun.HarnessStatus{{Name: "codex", Command: "codex", Available: true}},
17+
Harnesses: []dun.HarnessStatus{{Name: "codex", Command: "codex", Available: true, Live: true}},
1818
}
1919
if err := cache.Save(); err != nil {
2020
t.Fatalf("save cache: %v", err)

cmd/dun/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ LOOP MODE:
143143
144144
Options:
145145
--config Config file path (default .dun/config.yaml; also loads user config)
146-
--harness Agent to use: codex, claude, gemini, opencode (default: from config)
146+
--harness Agent to use: codex, claude, gemini, opencode, pi, cursor (default: from config)
147147
--model Model override for selected harness(es)
148148
--models Per-harness model overrides (e.g., codex:o3,claude:sonnet)
149149
--automation Mode: manual, plan, auto, yolo (default: auto)
@@ -155,7 +155,7 @@ LOOP MODE:
155155
156156
Quorum Options (multi-agent consensus):
157157
--quorum Strategy: any, majority, unanimous, or number (e.g., 2)
158-
--harnesses Comma-separated list of harnesses (default: cached or codex,claude,gemini)
158+
--harnesses Comma-separated list of harnesses (default: cached or codex,claude,gemini; supports codex,claude,gemini,opencode,pi,cursor)
159159
--cost-optimized Run harnesses sequentially to minimize cost
160160
--escalate Pause for human review on conflict
161161
--prefer Preferred harness on conflict (e.g., codex)
@@ -167,7 +167,7 @@ LOOP MODE:
167167
dun loop --automation yolo # Allow autonomous edits
168168
dun loop --dry-run # Preview prompt
169169
dun loop --verbose # Show prompt and responses
170-
dun loop --quorum majority --harnesses codex,claude,gemini,opencode
170+
dun loop --quorum majority --harnesses codex,claude,gemini,opencode,pi,cursor
171171
dun loop --quorum 2 --harnesses codex,claude --prefer codex
172172
173173
VERSION:
@@ -530,7 +530,7 @@ func runLoop(args []string, stdout io.Writer, stderr io.Writer) int {
530530
fs := flag.NewFlagSet("loop", flag.ContinueOnError)
531531
fs.SetOutput(stderr)
532532
configPath := fs.String("config", explicitConfig, "path to config file (default .dun/config.yaml if present; also loads user config)")
533-
harness := fs.String("harness", "", "agent harness (codex|claude|gemini|opencode); default from config")
533+
harness := fs.String("harness", "", "agent harness (codex|claude|gemini|opencode|pi|cursor); default from config")
534534
model := fs.String("model", opts.AgentModel, "model override for selected harness(es)")
535535
models := fs.String("models", "", "per-harness model overrides (e.g., codex:o3,claude:sonnet)")
536536
automation := fs.String("automation", opts.AutomationMode, "automation mode (manual|plan|auto|yolo)")

cmd/dun/main_cli_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,27 @@ func TestCallHarnessCodexYolo(t *testing.T) {
8484
t.Fatalf("codex should be a known harness")
8585
}
8686
}
87+
88+
func TestCallHarnessPi(t *testing.T) {
89+
// This test verifies the command construction for pi harness
90+
_, err := callHarness("pi", "test prompt", "auto")
91+
// We expect an error since pi CLI is likely not installed
92+
if err == nil {
93+
return
94+
}
95+
if strings.Contains(err.Error(), "unknown harness") {
96+
t.Fatalf("pi should be a known harness")
97+
}
98+
}
99+
100+
func TestCallHarnessCursor(t *testing.T) {
101+
// This test verifies the command construction for cursor harness
102+
_, err := callHarness("cursor", "test prompt", "auto")
103+
// We expect an error since cursor CLI is likely not installed
104+
if err == nil {
105+
return
106+
}
107+
if strings.Contains(err.Error(), "unknown harness") {
108+
t.Fatalf("cursor should be a known harness")
109+
}
110+
}

cmd/dun/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,7 @@ func TestRunHelpIncludesLoop(t *testing.T) {
14021402
if !strings.Contains(output, "--max-iterations") {
14031403
t.Fatalf("help should document max-iterations option")
14041404
}
1405-
if !strings.Contains(output, "codex, claude, gemini, opencode") {
1405+
if !strings.Contains(output, "codex, claude, gemini, opencode, pi, cursor") {
14061406
t.Fatalf("help should list available harnesses")
14071407
}
14081408
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,15 @@ proceeding (quorum is applied to the iteration prompt, not per-check prompts).
209209

210210
**Options**:
211211
- `--config` : Path to config file (default `.dun/config.yaml` if present)
212-
- `--harness` : Agent harness (`codex`, `claude`, `gemini`, `opencode`, default from config)
212+
- `--harness` : Agent harness (`codex`, `claude`, `gemini`, `opencode`, `pi`, `cursor`, default from config)
213213
- `--model` : Model override for selected harness(es)
214214
- `--models` : Per-harness model overrides (e.g., `codex:o3,claude:sonnet`)
215215
- `--automation` : Automation mode (`manual`, `plan`, `auto`, `yolo`, default `auto`)
216216
- `--max-iterations` : Maximum iterations before stopping (default `100`)
217217
- `--dry-run` : Print prompt without calling harness
218218
- `--verbose` : Print prompts sent to harnesses and responses received
219219
- `--quorum` : Quorum strategy (`any`, `majority`, `unanimous`, or number)
220-
- `--harnesses` : Comma-separated harness list for quorum (supports `name@persona`, default cached or `codex,claude,gemini`)
220+
- `--harnesses` : Comma-separated harness list for quorum (supports `name@persona`, default cached or `codex,claude,gemini`; supports `codex,claude,gemini,opencode,pi,cursor`)
221221
- `--cost-optimized` : Run harnesses sequentially to minimize cost
222222
- `--escalate` : Pause for human review on conflict
223223
- `--prefer` : Preferred harness on conflict
@@ -265,6 +265,7 @@ $ dun loop --verbose
265265
**Output**:
266266
- Format: Human-readable status report
267267
- Side effects: writes harness cache to `~/.dun/harnesses.json`
268+
- Behavior: performs harness liveness pings to confirm the CLIs respond
268269

269270
**Exit Codes**:
270271
- `0`: Success
@@ -279,7 +280,7 @@ $ dun loop --verbose
279280
**Options**:
280281
- `--task` : Task prompt (string)
281282
- `--quorum` : Quorum strategy (`any`, `majority`, `unanimous`, or number)
282-
- `--harnesses` : Comma-separated harness list (supports `name@persona`, default cached or `codex,claude,gemini`)
283+
- `--harnesses` : Comma-separated harness list (supports `name@persona`, default cached or `codex,claude,gemini`; supports `codex,claude,gemini,opencode,pi,cursor`)
283284
- `--cost-optimized` : Run harnesses sequentially to minimize cost
284285
- `--escalate` : Pause for human review on conflict
285286
- `--prefer` : Preferred harness on conflict
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
dun:
3+
id: F-022
4+
depends_on:
5+
- helix.prd
6+
---
7+
# Feature Spec: F-022 Doctor Command
8+
9+
## Summary
10+
11+
Provide a `dun doctor` command that checks harness/tool availability, performs
12+
basic harness liveness pings, and reports actionable fixes when dependencies are
13+
missing. Persist a cache of available harnesses for quorum defaults.
14+
15+
## Requirements
16+
17+
- Provide a `dun doctor` CLI command with no flags.
18+
- Detect installed harness CLIs (codex, claude, gemini, opencode).
19+
- Perform a short liveness ping per available harness.
20+
- Report model info when the harness provides it.
21+
- Detect project helpers based on repo signals:
22+
- Go: `go`, `go tool cover`, `staticcheck`, `govulncheck`, `gosec`.
23+
- Git: `git`, and configured hook tools (lefthook or pre-commit).
24+
- Beads: `bd` when `.beads` exists.
25+
- Provide actionable next steps when tools are missing.
26+
- Write `~/.dun/harnesses.json` with liveness results for quorum defaults.
27+
- Do not fail when tools are missing; only fail on runtime errors (for example,
28+
unable to write the cache file).
29+
30+
## Non-Goals
31+
32+
- Installing or configuring tools automatically.
33+
- Deep validation of tool configuration beyond presence and liveness.
34+
35+
## Acceptance Criteria
36+
37+
- `dun doctor` prints harness availability and liveness status.
38+
- `dun doctor` prints helper/tool availability with actionable hints for missing tools.
39+
- `dun doctor` writes/updates `~/.dun/harnesses.json`.
40+
- Missing tools produce warnings but do not return a non-zero exit code.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
dun:
3+
id: US-015
4+
depends_on:
5+
- F-022
6+
---
7+
# US-015: Diagnose Tooling Readiness
8+
9+
As a user, I want to know if all supporting tools I need to use Dun are
10+
available, with help fixing them when they are missing.
11+
12+
## Acceptance Criteria
13+
14+
- `dun doctor` runs without flags and produces a human-readable report.
15+
- The report lists harness availability and liveness results.
16+
- The report lists project helpers (Go, Git hooks, Beads) when applicable.
17+
- Missing tools include a clear next step (install hint or command).
18+
- `~/.dun/harnesses.json` is updated after each run.
19+
- Missing tools do not cause `dun doctor` to fail.

internal/dun/doctor.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dun
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67
"os/exec"
@@ -22,10 +23,13 @@ type DoctorReport struct {
2223

2324
// HarnessStatus reports availability of a harness CLI.
2425
type HarnessStatus struct {
25-
Name string `json:"name"`
26-
Command string `json:"command"`
27-
Available bool `json:"available"`
28-
Detail string `json:"detail"`
26+
Name string `json:"name"`
27+
Command string `json:"command"`
28+
Available bool `json:"available"`
29+
Detail string `json:"detail"`
30+
Live bool `json:"live"`
31+
Model string `json:"model,omitempty"`
32+
LiveDetail string `json:"live_detail,omitempty"`
2933
}
3034

3135
// HelperStatus reports availability of project helper tools.
@@ -61,6 +65,8 @@ func RunDoctor(root string) (DoctorReport, error) {
6165
return report, nil
6266
}
6367

68+
var harnessLivenessFn = checkHarnessLiveness
69+
6470
func checkHarnesses() []HarnessStatus {
6571
names := DefaultRegistry.List()
6672
sort.Strings(names)
@@ -75,10 +81,15 @@ func checkHarnesses() []HarnessStatus {
7581
if err != nil {
7682
status.Available = false
7783
status.Detail = "command not found"
78-
} else {
79-
status.Available = true
80-
status.Detail = "found " + path
84+
statuses = append(statuses, status)
85+
continue
8186
}
87+
status.Available = true
88+
status.Detail = "found " + path
89+
live, model, detail := harnessLivenessFn(name)
90+
status.Live = live
91+
status.Model = model
92+
status.LiveDetail = detail
8293
statuses = append(statuses, status)
8394
}
8495
return statuses
@@ -93,6 +104,17 @@ func defaultHarnessCommand(name string) string {
93104
}
94105
}
95106

107+
func checkHarnessLiveness(name string) (bool, string, string) {
108+
result, err := PingHarness(context.Background(), name, HarnessConfig{
109+
Name: name,
110+
AutomationMode: AutomationAuto,
111+
})
112+
if err != nil && result.Detail == "" {
113+
result.Detail = err.Error()
114+
}
115+
return result.Live, result.Model, result.Detail
116+
}
117+
96118
func checkProjectHelpers(root string) []HelperStatus {
97119
var helpers []HelperStatus
98120
if fileExists(filepath.Join(root, "go.mod")) {
@@ -245,6 +267,23 @@ func FormatDoctorReport(report DoctorReport) string {
245267
b.WriteString(harness.Detail)
246268
b.WriteString(")")
247269
}
270+
if harness.Available {
271+
liveStatus := "fail"
272+
if harness.Live {
273+
liveStatus = "ok"
274+
}
275+
b.WriteString("; live: ")
276+
b.WriteString(liveStatus)
277+
if harness.Model != "" {
278+
b.WriteString(" (model: ")
279+
b.WriteString(harness.Model)
280+
b.WriteString(")")
281+
} else if harness.LiveDetail != "" {
282+
b.WriteString(" (")
283+
b.WriteString(harness.LiveDetail)
284+
b.WriteString(")")
285+
}
286+
}
248287
b.WriteString("\n")
249288
}
250289
}

0 commit comments

Comments
 (0)