Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3d35697
corral: work
Aug 9, 2026
a86f6bb
corral: work
Aug 9, 2026
1466a1b
corral: merge corral/run_3aa2e957cb73/w1/1
Aug 9, 2026
be0937d
merge: resolve main.go conflict between reviewer wiring and run safeg…
thiago-ss Aug 9, 2026
16252cc
corral: work
Aug 10, 2026
902e92f
corral: work
Aug 10, 2026
e032a16
corral: merge corral/run_5e443e470f20/w1/1
Aug 10, 2026
db3acd6
corral: merge corral/run_5e443e470f20/w2/1
thiago-ss Aug 11, 2026
c773d3f
corral: work
Aug 11, 2026
06ef37b
corral: work
Aug 11, 2026
cce0a9e
corral: merge corral/run_3aa2e957cb73/w3/2
Aug 11, 2026
b21fb8b
corral: work
Aug 11, 2026
8e4cfea
corral: merge corral/run_3aa2e957cb73/w4/1
Aug 11, 2026
769dbad
corral: merge corral/run_3aa2e957cb73/w5/1
thiago-ss Aug 11, 2026
2842fa0
corral: align tests with CreateOptions design
thiago-ss Aug 11, 2026
93a0c56
fix: scope attempt IDs by run
thiago-ss Aug 12, 2026
e1c9079
chore: ignore generated OpenCode config
thiago-ss Aug 12, 2026
9e1fa7e
fix: preserve dirty worktrees during prune
thiago-ss Aug 12, 2026
ac9b9cb
fix: enforce read-only reviewer tools
thiago-ss Aug 12, 2026
33d135e
fix: keep pre-authorized gates explicit
thiago-ss Aug 12, 2026
4fca660
feat: stream durable run events over SSE
thiago-ss Aug 12, 2026
c1b66dd
merge: add durable run event streaming
thiago-ss Aug 12, 2026
4083b60
fix: retain event subscribers after overload
thiago-ss Aug 12, 2026
ccef5ab
feat: add live TUI telemetry and attention
thiago-ss Aug 12, 2026
7da1205
fix: harden live TUI event handling
thiago-ss Aug 12, 2026
e8455cd
fix: stop TUI event stream on terminal runs
thiago-ss Aug 12, 2026
c6ac506
fix: keep waiting run telemetry connected
thiago-ss Aug 12, 2026
c1cf9de
fix: preserve monotonic live TUI state
thiago-ss Aug 12, 2026
6f5cae1
docs: align provider and streaming contracts
thiago-ss Aug 12, 2026
5b8f2b5
merge: add live TUI telemetry
thiago-ss Aug 12, 2026
0700acd
fix: pause attempt budgets for permissions
thiago-ss Aug 12, 2026
78c9aea
feat: add Claude Code adapter
thiago-ss Aug 12, 2026
da0e964
fix: preserve Claude streams and usage
thiago-ss Aug 12, 2026
2bf87a1
merge: add Claude Code adapter
thiago-ss Aug 12, 2026
9c1fefb
fix: harden concurrent orchestration
thiago-ss Aug 12, 2026
a1d8baf
fix: close agent authorization boundaries
thiago-ss Aug 13, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ bin/
.DS_Store
.corral/
/corral
/opencode.json
35 changes: 29 additions & 6 deletions .opencode/tools/corral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ async function loadKey(): Promise<string> {
}

// Map the current OpenCode agent to a corral role for server-side
// enforcement. Anything unknown falls back to operator (human).
// enforcement. Unknown agents stay unprivileged; only non-model clients such
// as the CLI/TUI may claim the operator role directly.
const ROLE_MAP: Record<string, string> = {
"corral-orchestrator": "orchestrator",
"corral-planner": "planner",
Expand All @@ -29,7 +30,7 @@ const ROLE_MAP: Record<string, string> = {

function roleFor(agent?: string): string {
if (agent && agent in ROLE_MAP) return ROLE_MAP[agent]
return "operator"
return "unknown"
}

async function call(path: string, body?: unknown, role?: string) {
Expand All @@ -40,7 +41,7 @@ async function call(path: string, body?: unknown, role?: string) {
method: body === undefined ? "GET" : "POST",
headers: {
"Content-Type": "application/json",
"X-Corral-Role": role ?? "operator",
"X-Corral-Role": role ?? "unknown",
...(key ? { Authorization: `Bearer ${key}` } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
Expand All @@ -67,14 +68,20 @@ export const plan = tool({

export const start = tool({
description: "Start a corral run from an approved graph.",
args: { graph: tool.schema.string().describe("Graph JSON (as returned by corral_plan)") },
args: {
graph: tool.schema.string().describe("Graph JSON (as returned by corral_plan)"),
},
async execute(args, context) {
let graph: unknown
let parsed: unknown
try {
graph = JSON.parse(args.graph)
parsed = JSON.parse(args.graph)
} catch {
return "error: graph is not valid JSON"
}
const graph =
typeof parsed === "object" && parsed !== null && "graph" in parsed
? (parsed as { graph: unknown }).graph
: parsed
return call("/api/runs", { graph }, roleFor(context.agent))
},
})
Expand All @@ -89,6 +96,22 @@ export const status = tool({
},
})

export const watch = tool({
description:
"Watch a corral run and block until its state changes (new events, a human gate awaiting approval, or completion) or the timeout elapses. Drive the run loop by calling this repeatedly and passing the previous response's `since` cursor back. `gatesAwaitingApproval` lists human gates parked in running waiting for a decision: if the response's `autoApproveGates` is true the run is pre-authorized and you should approve each gate via corral_approve; otherwise never approve them yourself — report them to the user and keep watching until they resolve.",
args: {
runID: tool.schema.string(),
since: tool.schema.number().optional().describe("Event cursor; only return events after this"),
timeout: tool.schema.number().optional().describe("Block for up to this many seconds (default 60, max 120)"),
},
async execute(args, context) {
const q = new URLSearchParams()
if (args.since !== undefined) q.set("since", String(args.since))
if (args.timeout !== undefined) q.set("timeout", String(args.timeout))
return call(`/api/runs/${args.runID}/watch?${q}`, undefined, roleFor(context.agent))
},
})

export const approve = tool({
description: "Approve a human gate (or the run's merge) by node id.",
args: {
Expand Down
77 changes: 58 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,10 @@ Inside OpenCode:
1. Switch to `corral-planner` and ask: `plan a graph to <your goal>`.
2. Review the returned graph.
3. Switch to `corral-orchestrator` and ask it to start that graph.
4. Follow progress with `corral_status`; approve, reject, retry, cancel, or
steer nodes when needed.
4. Follow progress with `corral_status` / `corral_watch`; approve, reject,
retry, cancel, or steer nodes when needed. Trusted operator API clients may
set `autoApproveGates` when creating a run; model agents cannot grant that
authority to themselves.

Or follow the same run from the terminal:

Expand All @@ -99,22 +101,28 @@ corral tui
<a href="docs/assets/tui.svg"><picture><source media="(max-width: 900px)" srcset="docs/assets/tui-mobile.svg"><img src="docs/assets/tui.svg" alt="Corral TUI inspecting a completed attempt, its worktree, command gate, and exit evidence" width="960"></picture></a>
</div>

The TUI exposes the graph, node states, attempts, sessions, worktrees, evidence,
and operator actions. Worker edits stay in attempt worktrees; initialization
itself may add the OpenCode tool and agent config to your checkout.
The TUI follows durable server-sent run events, falls back to polling after a
stream failure, and exposes graph state, live transcript tails, budget usage,
attempts, sessions, worktrees, evidence, permissions, and operator actions. It
can raise desktop attention when a gate needs approval or a node fails. Worker
edits stay in attempt worktrees; initialization itself may add the OpenCode tool
and agent config to your checkout.

## What counts as evidence

Corral currently wires three completion paths:
Corral currently wires four completion paths:

- **Command:** run an argv-style command in the attempt worktree and require
exit code `0`.
- **JSON Schema:** validate a declared JSON artifact against a schema.
- **Default diff:** when no gate is declared, require at least one file diff
reported by the driver. Prose alone fails.

The graph schema also contains a reviewer-gate seam, but the production daemon
does not wire a reviewer implementation yet.
- **Reviewer:** a read-only OpenCode session reviews the attempt's evidence —
objective, prior feedback, transcript, the recorded diff artifact, and check
results — and must return exactly `APPROVED` or `CHANGES_REQUESTED`, followed
by a required `Note:` line. A change request returns its note as focused
retry feedback. Set `CORRAL_REVIEWER_MODEL` to a `provider/model` value to
use a specific model for review sessions.

## Proof, not promises

Expand Down Expand Up @@ -156,8 +164,10 @@ evidence remain stored when an attempt retries.
- **Landing:** merge nodes commit accepted worktree changes, merge branches with
`--no-ff`, run their post-merge command, and prune consumed worktrees.

OpenCode is the implemented driver. The generic `adapter.Driver` interface is
the seam for future executors.
OpenCode is the production-wired driver. A self-contained Claude Code adapter
implements the same contract, including scoped permission mediation, but is not
yet selected by `corral daemon`. The generic `adapter.Driver` interface remains
the seam for additional executors.

## Operations

Expand All @@ -168,15 +178,40 @@ the seam for future executors.
| `corral doctor` | Check OpenCode, Git, daemon, plugin, and config |
| `corral update` | Install a newer GitHub release after a sanity check |
| `corral export <runID>` | Print the full audit export |
| `corral worktrees` | List attempt worktrees; `--prune` removes clean merged/removed and stale ones |

`status`, `tui`, `doctor`, and `export` read the repository key automatically.

### Run-level safeguards

The daemon ships with run-level safeguards enabled by default. Each is
overridable via an environment variable; a value of `0` disables it. These are
ceilings for runaway runs — normal runs should never hit them.

`status`, `tui`, and `doctor` read the repository key automatically. Until the
export command does the same, use:
| Variable | Default | Behavior |
|---|---|---|
| `CORRAL_BREAKER_MAX_FAILURES` | `5` | Circuit breaker: once `N` node failures occur within the window, the run stops starting new work; pending nodes are blocked (`reason: circuit breaker`) and an operator retry resets the breaker. |
| `CORRAL_BREAKER_WINDOW` | `900` (seconds, 15 min) | Failures are counted within this rolling window. |
| `CORRAL_RUN_MAX_TOKENS` | `1_000_000` | Run-level token budget, accumulated across all finished attempts; once exceeded, pending nodes are blocked (`reason: run budget exceeded`). |
| `CORRAL_RUN_MAX_COST` | `100` (USD) | Run-level cost budget, accumulated across all finished attempts; once exceeded, pending nodes are blocked. |

Example:

```sh
CORRAL_DAEMON_KEY="$(cat .corral/api.key)" \
corral export <runID> > audit.json
CORRAL_BREAKER_MAX_FAILURES=3 \
CORRAL_BREAKER_WINDOW=600 \
CORRAL_RUN_MAX_TOKENS=250000 \
CORRAL_RUN_MAX_COST=50 \
corral up
```

`corral worktrees` works directly on git (no daemon, no key). It lists the
worktrees kept after failed attempts — path, branch, HEAD, last-activity time,
and dirty/locked markers — and with `--prune` removes clean ones that are safe
to drop: branches already merged into the main checkout, and (with `--stale
<duration>`, e.g. `24h`) worktrees idle longer than that. It never touches the
main checkout; dirty, locked, and detached worktrees are left alone.

## Development

```sh
Expand All @@ -194,9 +229,11 @@ The core packages are deliberately small:
| `internal/graph` | graph schema, validation, states, ready computation |
| `internal/sched` | leases, priority, retries, gates, merge orchestration |
| `internal/store` | SQLite event log, materialized nodes, attempts, artifacts |
| `internal/verify` | command, JSON Schema, and diff evidence |
| `internal/verify` | command, JSON Schema, diff, and reviewer evidence |
| `internal/worktree` | branch/worktree lifecycle and diff artifacts |
| `internal/ocxadapter` | OpenCode sessions and completion reconciliation |
| `internal/claudeadapter` | standalone Claude Code sessions, usage, and permission mediation |
| `internal/ocxreviewer` | OpenCode reviewer sessions for the reviewer gate |
| `internal/daemon` | control API, planning, role routing, audit export |
| `internal/tui` | terminal dashboard and operator controls |

Expand All @@ -205,9 +242,11 @@ Visual language, color roles, and asset rules live in the

## Scope

Corral is currently local, single-machine, single-repository software with one
implemented executor: OpenCode. Distributed workers, Codex/Claude drivers,
interactive graph editing, and a web dashboard remain roadmap work.
Corral is currently local, single-machine, single-repository software. OpenCode
is the production-wired executor; the Claude Code adapter is available as a
self-contained package but has no daemon selection/configuration path yet.
Distributed workers, a Codex driver, interactive graph editing, and a web
dashboard remain roadmap work.

## License

Expand Down
Loading
Loading