Architecture
Cue is a deterministic Bun + TypeScript runner. Headless coding agents sit inside pipeline stages. Routing, gating, retries, and label transitions are plain code — never ask the model something a script can decide (for example, whether tests passed).
GitHub is the state store: agent:* labels are the state machine, issue comments carry the plan, draft PRs are the output.
Layout
src/
├── cli.ts # entrypoint + label definitions; builds the real StageContext
├── action.ts # nextAction (label → stage) — shared by pipeline and prompts
├── pipeline.ts # runIssue (failure → agent:failed), process loop
├── cleanup.ts # merged/closed PRs → agent:done / agent:failed + worktree removal
├── stages/
│ ├── context.ts # StageContext — DI bundle every stage receives
│ ├── triage.ts # read-only plan generation; PLAN_MARKER
│ ├── replan.ts # plan revision from human comments (has WebSearch)
│ ├── dev.ts # worktree implementation + gate + draft PR
│ ├── revise.ts # human PR feedback → worktree + gate; review re-runs after
│ └── review.ts # JSON verdict + bounded fix loop
├── adapters/
│ ├── types.ts # AgentAdapter / AgentRunOptions (access, webSearch, bashAllowlist) / AgentResult
│ ├── base.ts # JsonlAdapter: shared env scrub + exec + JSONL parse + progress loop
│ ├── registry.ts # ADAPTERS: name → { make, defaultModels }
│ ├── summarize.ts # shared tool-input summarizer (adapters + dashboard transcript)
│ ├── usage.ts # shared token extractor: per-adapter usage → one disjoint TokenUsage
│ ├── antigravity.ts # agy -p --output-format stream-json --dangerously-skip-permissions
│ ├── claude.ts # claude -p --output-format stream-json --verbose; maps access → --allowedTools
│ └── codex.ts # codex exec --json; sandbox read-only / workspace-write; --search
├── server.ts # cue ui: Bun.serve, SSE, process/run triggers
├── github.ts # typed wrapper over the gh CLI
├── worktree.ts # git worktree per issue; bootstraps empty repos
├── gates.ts # deterministic test/lint runner (sh -c in the worktree)
├── specs.ts # opt-in knowledge layer: specs dir + .cue/learnings.md, presence-detected
├── exec.ts # THE ONLY place Bun.spawn is called
├── platform.ts # POSIX/Windows personality: gate shell, agent env allowlist
├── config.ts # valibot schema + resolveConfig
├── scaffold.ts # creates/tops up the target's .cue/ (config, prompts, gitignore)
├── configure.ts # cue init wizard: @clack/prompts behind an injectable Ask seam
├── spinner.ts # single-slot ora frame; disabled when stdout is not a TTY
├── upgrade.ts # cue upgrade: checksum-verified in-place binary update
├── embedded.ts # prompt embedding for compiled binaries (with { type: "file" })
└── log.ts # transcripts + cost under <target>/.cue/runs/<issue>/
prompts/ # packaged default role prompts
ui/ # dashboard SPA, built to ui/build/client
tests/ # one file per module + integration.test.tsInvariants
These are load-bearing. Tests encode most of them.
- All subprocess execution goes through
Execinsrc/exec.ts. Never callBun.spawnanywhere else — that is what makes every module testable. - Lean dependencies. The CLI's only runtime deps are valibot, consola, ora, and @clack/prompts. The dashboard is a separate package (
ui/package.json); its deps never enter the CLI. Do not add packages to either without discussion. - Label names are exact:
agent:ready,agent:planned,agent:approved,agent:replan,agent:in-dev,agent:in-review,agent:revise,agent:done,agent:failed,agent:stop. - Plan-comment marker is exactly
<!-- cue:plan -->(PLAN_MARKERinstages/triage.ts). Dev and replan find plans by the newest comment containing it. - Branch naming:
agent/issue-<number>. - The GitHub token must never reach agent subprocesses. The shared adapter base (
JsonlAdapter.run) scrubs the env down to an OS allowlist plus the adapter's own API keys — never another provider's credentials. Every adapter test assertsGH_TOKENis absent. - Agents never run git/gh side effects. The runner owns commit, push, PR creation, and labels. Cue never merges and never touches the base branch.
- Humans gate two moments: plan approval and PR merge. Do not automate either.
- Issue bodies and comments are untrusted input. Every prompt states this.
How processing works
cleanupreconciles PRs that have been merged or closed since the last run.nextActionmaps the issue's labels to a stage (triage/replan/dev/revise).runIssueinvokes that stage. On throw, it comments the error and appliesagent:failed.- Stages emit through
ctx.onEvent. The CLI prints events;cue uialso broadcasts them over SSE.
runIssue is the single place that turns a stage error into an issue comment plus agent:failed.
Adapters
Codex is the default adapter and runs via codex exec --json; read-only stages use Codex's read-only sandbox and implementation stages use workspace-write. Antigravity runs via agy -p --output-format stream-json --dangerously-skip-permissions. Claude Code remains available via claude -p --output-format stream-json --verbose. All adapters scrub their environments, retaining only their own authentication variables and never GH_TOKEN. If a CLI update breaks an adapter, check that CLI's --help output first.
Releases
Push a v* tag. .github/workflows/release.yml runs scripts/build-binaries.sh, which:
- Builds the dashboard
- Regenerates the UI embed manifest from
ui/build/client - Compiles per-target binaries into
dist/ - Restores the committed empty manifest stub
Release assets are dist/cue-* plus checksums.txt. install.sh and install.ps1 at the repo root are the checksum-verified installers.
Prompts embed via with { type: "file" } imports in src/embedded.ts. New disk assets (prompts, UI output) must join this embedding path or compiled installs break. Never commit a generated UI manifest.
More contributor workflow: Contributing.