# agent-dashboard

## Go conventions

### External commands must go through interfaces

Never call `exec.Command` or `exec.CommandContext` directly in business logic. All external command execution must go through a mockable interface with a package-level variable that tests can swap.

Pattern:
```go
// 1. Define the interface in a runner.go file
type Runner interface {
    Output(ctx context.Context, name string, args ...string) ([]byte, error)
}

// 2. Production implementation
type execRunner struct{}
func (r *execRunner) Output(ctx context.Context, name string, args ...string) ([]byte, error) {
    return exec.CommandContext(ctx, name, args...).Output()
}

// 3. Package-level variable
var runner Runner = &execRunner{}
```

The only files allowed to import `"os/exec"` are runner/interface implementation files (e.g. `runner.go`, `tmux.go`). If you need a new command execution pattern, extend the existing interface rather than calling `exec.Command` directly.

### Tests must use mocks, never real subprocesses

Tests must never spawn real subprocesses (`git`, `gh`, `tmux`, `open`, etc.). Always use mockery-generated mocks from `internal/mocks/`. Swap the package-level runner in tests:

```go
m := mocks.NewMockGitRunner(t)
orig := gitRunner
gitRunner = m
t.Cleanup(func() { gitRunner = orig })
```

Mocks are generated by mockery — config is in `.mockery.yaml`. After changing an interface, regenerate with `mockery`.

### Testing

- Run tests with `CGO_ENABLED=0 go test -race ./...` (CGO disabled to avoid AMFI kills on macOS)
- The `-race` flag is mandatory — it catches data races that cause tmux server crashes

## Per-harness packages

The dashboard supports two coding-agent harnesses today: `claude` and `codex`. Per-harness logic lives in `internal/harness/<name>/` for spawn-command construction and `internal/<harness>/` for reading the harness-specific state files. Adding another harness means:

1. Implement `domain.Harness` under `internal/harness/<name>/` with `*runner.go` for any `os/exec` calls (same conventions as above).
2. Register the name in `internal/harness/registry.go` `Resolve()`.
3. Add a `<Name>HarnessSettings` struct to `internal/domain/types.go` and wire it into `HarnessSettings`.
4. Route per-harness flags in `internal/harness/spawnopts.go` so unused fields stay zero-valued.
5. If the harness has its own conversation/state schema, add a reader under `internal/<harness>/` and route through `internal/conversation/router.go` `Read()` (dispatches on `agent.Harness`).
6. Update hook scripts (`adapters/claude-code/scripts/hooks/agent-state-fast.js` `detectHarness`) so the SessionStart hook stamps the harness name in the per-agent state file — without that, the router can't dispatch.

For codex specifically: codex CLI 0.130.0's hook payload schema is **1:1 with Claude's** (`codex-rs/hooks/schema/generated/*.json`), and codex sets `CLAUDE_PLUGIN_ROOT` for OOTB plugin compatibility (`codex-rs/hooks/src/engine/discovery.rs`). Most JS hooks work unchanged; the discriminator is `process.env.PLUGIN_ROOT` (codex-only) or `input.model` starting with `gpt-`.

When adding a new dashboard skill that requires Claude-only orchestration primitives, append its name to `internal/skills/guard.go`'s `codexBlockedSkills` map so both TUI and web creation reject that skill against a codex session. The guard is a Codex blocklist: supported built-in skills and custom maintained skills are allowed by default.
