docs: sync architecture and code structure docs

This commit is contained in:
Vaibhaav 2026-06-26 03:10:42 +05:30
parent c87eb075bc
commit bc7fc85f79
3 changed files with 2804 additions and 602 deletions

View File

@ -1,26 +1,161 @@
# Agent Orchestrator backend architecture
# Architecture
The backend is a long-running Go daemon that supervises coding-agent sessions.
The current model is intentionally small: session rows persist only durable facts,
and display status is derived at read time.
Agent Orchestrator is a long-running Go daemon that orchestrates parallel AI coding agents, each in an isolated `git worktree`, with automatic feedback routing from CI failures, review comments, and merge conflicts.
## Mental model
## Core Mental Model
```
OBSERVE external facts → UPDATE durable facts → DERIVE display status / ACT
```
The system stores only immutable facts (`activity_state`, `is_terminated`, PR facts) in SQLite. Display status is computed at read time — never stored.
The durable session facts are:
- `activity_state` — what the agent last reported or what the runtime observer
can safely conclude (`active`, `idle`, `waiting_input`, `exited`).
- `is_terminated` — whether the session should be treated as over.
- PR facts in the `pr`, `pr_checks`, and `pr_comment` tables.
- **`activity_state`** — what the agent last reported or what the runtime observer can safely conclude (`active`, `idle`, `waiting_input`, `exited`)
- **`is_terminated`** — whether the session should be treated as over
- **PR facts** — in the `pr`, `pr_checks`, and `pr_comment` tables
The UI status is not stored. `service.Session` computes it from the session
record plus PR facts while assembling controller-facing read models.
The UI status is not stored. `service.Session` computes it from the session record plus PR facts while assembling controller-facing read models.
## Package layout
## Architecture Overview
```mermaid
graph TB
subgraph Frontend["Frontend Layer"]
EMain["Electron Main Process"]
React["React 19 UI (renderer)"]
TanStack["TanStack Router/Query + shadcn/ui"]
EMain --> HTTP["HTTP (REST + SSE + WebSocket)"]
React --> HTTP
TanStack --> HTTP
end
subgraph Backend["Backend Go Daemon"]
HTTP2["HTTP Layer (Controllers)"]
CLI["CLI Layer (Cobra cmd)"]
Service["Service Layer<br/>(Project/Session/PR)"]
SessionMgr["Session Manager<br/>(spawn/kill/restore/cleanup)"]
RuntimeSelect["Runtime Selector<br/>(Platform-based)"]
Tmux["tmux<br/>(Darwin/Linux)"]
Conpty["conpty<br/>(Windows)"]
Lifecycle["Lifecycle Manager<br/>(Reduces observations → facts)"]
SCM["SCM Observer<br/>(GitHub)"]
Reaper["Reaper<br/>(Runtime liveness)"]
Storage["Storage + CDC<br/>(SQLite with triggers)"]
CLI --> Service
HTTP2 --> Service
Service --> SessionMgr
SessionMgr --> RuntimeSelect
RuntimeSelect -->|Darwin/Linux| Tmux
RuntimeSelect -->|Windows| Conpty
SessionMgr --> Lifecycle
Tmux --> Lifecycle
Conpty --> Lifecycle
Lifecycle --> Storage
SCM --> Lifecycle
Reaper --> Lifecycle
end
HTTP -->|"Loopback Only<br/>(127.0.0.1)"| HTTP2
```
## Runtime Architecture
### Platform-Specific Runtime Selection
The system uses a dual-runtime architecture optimized for each platform:
```mermaid
graph LR
AO["Agent Orchestrator"] --> RuntimeCheck{"Platform Check"}
RuntimeCheck -->|Darwin/Linux| TmuxRuntime["tmux Runtime"]
RuntimeCheck -->|Windows| ConptyRuntime["conpty Runtime"]
TmuxRuntime --> TmuxFeatures["• tmux sessions<br/>• Direct CLI interaction<br/>• Unix PTY integration"]
ConptyRuntime --> ConptyFeatures["• Pty-host server<br/>• B1 binary protocol<br/>• Loopback TCP communication"]
TmuxFeatures --> Common["Common Interface:<br/>ports.Runtime + ports.Attacher"]
ConptyFeatures --> Common
Common --> Session["Session Management<br/>& Terminal Streaming"]
```
### Runtime Interface
Both tmux and conpty implement the same core interface:
```go
type Runtime interface {
ports.Runtime // Create, Destroy, IsAlive
ports.Attacher // Attach for terminal streaming
SendMessage() // Send input to session
GetOutput() // Get scrollback output
}
```
### tmux Runtime (Darwin/Linux)
```mermaid
sequenceDiagram
participant AO
participant Tmux
participant Shell
participant Agent
AO->>Tmux: tmux new-session -d -s <session-id>
Tmux->>Shell: Launch shell with agent command
Shell->>Agent: Execute agent
Agent->>Shell: Agent output
Shell->>Tmux: Terminal data
AO->>Tmux: tmux attach-session -t <session-id>
Tmux-->>AO: Terminal stream
```
**Key Features:**
- Creates detached tmux sessions with hidden status bar
- Direct tmux CLI interaction for session management
- `tmux send-keys` for input delivery
- `tmux capture-pane` for scrollback retrieval
- Sessions survive daemon restart (tmux persistence)
### conpty Runtime (Windows)
```mermaid
sequenceDiagram
participant AO
participant Host
participant Conpty
participant Agent
AO->>Host: Spawn detached pty-host process
Host->>Host: Create ConPTY
Host->>Host: Listen on loopback TCP
Host-->>AO: READY signal
AO->>Host: Store session (addr + PID)
Host->>Agent: Execute agent in ConPTY
Agent->>Host: Terminal output
Host->>Host: Store in ring buffer
Note over AO,Host: Attach Phase
AO->>Host: Dial loopback TCP
Host-->>AO: Scrollback replay (MsgTerminalData)
Host->>AO: Live terminal stream
```
**Key Features:**
- Detached pty-host process with ConPTY
- Custom B1 binary protocol over loopback TCP
- Ring buffer for scrollback storage
- File-based registry for crash recovery
- Graceful shutdown with cleanup
## Package Layout
```
backend/internal/domain shared vocabulary and API status value types
@ -40,64 +175,269 @@ backend/internal/daemon production wiring and shutdown
backend/internal/config daemon env/default config
```
## Status derivation
## Adapter Layer
`service.Session` selects the display PR from all PR snapshots for a session, then
applies this rough precedence:
Swappable implementations for each port:
1. `is_terminated``terminated`, except merged PRs display `merged`.
2. `activity_state=waiting_input``needs_input`.
3. Open PR facts drive PR pipeline statuses: `ci_failed`, `draft`,
`changes_requested`, `mergeable`, `approved`, `review_pending`, `pr_open`.
4. `activity_state=active``working`.
5. A signal-capable harness that has never sent a hook callback past the
~90s spawn grace → `no_signal` (a broken hook pipeline is visible rather
than reported as a confident `idle`). Hook-less harnesses stay `idle`.
6. Everything else → `idle`.
| Port | Darwin/Linux | Windows | Purpose |
|------|--------------|---------|---------|
| **Runtime** | tmux | conpty | Terminal multiplexing and session isolation |
| **Workspace** | git worktree | git worktree | Isolated working directories |
| **Agent** | claude-code, codex, etc. | claude-code, codex, etc. | AI coding agent execution |
| **SCM** | GitHub | GitHub | Pull request observation |
| **Tracker** | GitHub | GitHub | Issue tracking |
| **Reviewer** | claude-code | claude-code | Code review execution |
| **Notifier** | desktop, slack, discord, webhook | desktop, slack, discord, webhook | Notification delivery |
## Lifecycle manager
## Session Lifecycle
`lifecycle.Manager` is the write path for session lifecycle facts and lifecycle-owned agent nudges:
```mermaid
stateDiagram-v2
[*] --> Creating: POST /api/v1/sessions
Creating --> WorkspaceCreated: Create git worktree
WorkspaceCreated --> RuntimeCreated: Create tmux/conpty session
RuntimeCreated --> Launching: Execute agent command
Launching --> Spawned: Agent running
Spawned --> Active: Agent reports activity
Spawned --> Idle: Agent waiting
Spawned --> WaitingInput: Agent needs input
Spawned --> Terminated: Agent exits
Active --> Idle: Agent finishes task
Active --> WaitingInput: Agent needs input
Active --> Terminated: Agent exits
Idle --> Active: Agent starts new task
Idle --> WaitingInput: Agent needs input
Idle --> Terminated: Agent exits
WaitingInput --> Active: User provides input
WaitingInput --> Terminated: Session killed
Terminated --> [*]: Session cleaned up
```
- runtime observations can mark a session terminated only when runtime and
process are both clearly dead and recent activity does not contradict that;
failed/unknown probes do not persist a special state.
- activity signals update `activity_state`; `exited` also marks the session
terminated.
- PR observations do not write PR rows here, but after the PR service persists
them lifecycle sends actionable agent nudges for CI failures, review feedback,
and merge conflicts.
## Data Flow: Spawn Session
## PR manager
```mermaid
sequenceDiagram
participant User
participant CLI
participant HTTP
participant Service
participant SessionMgr
participant Workspace
participant Runtime
participant Agent
participant Lifecycle
participant SQLite
participant CDC
participant Frontend
`pr.Manager` records SCM observations into the PR/check/comment tables, then
forwards the observation to lifecycle for agent nudges. A merged PR marks the
owning session terminated through the lifecycle manager; other PR facts are
consumed at read time for display status.
User->>CLI: ao spawn --project my-repo
CLI->>HTTP: POST /api/v1/sessions
HTTP->>Service: service.Session.Spawn()
Service->>SessionMgr: session_manager.Spawn()
SessionMgr->>SQLite: Create session row (seed state)
SessionMgr->>Workspace: Workspace.Create()
Workspace-->>SessionMgr: git worktree created
SessionMgr->>Runtime: Runtime.Create()
alt Darwin/Linux
Runtime->>Runtime: tmux new-session -d -s <id>
else Windows
Runtime->>Runtime: Spawn pty-host + ConPTY
end
Runtime-->>SessionMgr: Session handle
SessionMgr->>Agent: Get launch command
Agent-->>SessionMgr: Command
Runtime->>Agent: Execute in tmux/ConPTY
Agent->>Lifecycle: Report spawn complete
Lifecycle->>SQLite: Update session (spawned state)
SQLite->>CDC: Trigger change_log
CDC->>Frontend: SSE session_created event
Frontend->>User: UI update
```
## Session manager
## Data Flow: Terminal Streaming
```mermaid
sequenceDiagram
participant Frontend
participant HTTP
participant TerminalMgr
participant Runtime
Frontend->>HTTP: WebSocket upgrade /api/v1/sessions/{id}/terminal
HTTP->>TerminalMgr: Create terminal session
TerminalMgr->>Runtime: Runtime.Attach(handle, rows, cols)
alt tmux (Darwin/Linux)
Runtime->>Runtime: tmux attach-session -t <id>
Runtime-->>TerminalMgr: PTY stream
else conpty (Windows)
Runtime->>Runtime: Dial loopback TCP
Runtime->>Runtime: B1 protocol handshake
Runtime->>Runtime: MsgTerminalData (scrollback)
Runtime-->>TerminalMgr: TCP stream
end
TerminalMgr-->>Frontend: WebSocket terminal stream
Note over Frontend,Runtime: Bidirectional terminal I/O
```
## Status Derivation
`service.Session` selects the display PR from all PR snapshots for a session, then applies this precedence:
```mermaid
graph TD
Start["Session Status Check"] --> Check1{"is_terminated?"}
Check1 -->|Yes| Merged{"Merged PR?"}
Check1 -->|No| Check2{"activity_state = waiting_input?"}
Merged -->|Yes| Status1["Status: merged"]
Merged -->|No| Status2["Status: terminated"]
Check2 -->|Yes| Status3["Status: needs_input"]
Check2 -->|No| Check3{"Open PR Facts?"}
Check3 -->|Yes| PRStatus["PR Pipeline Status:<br/>ci_failed, draft, changes_requested,<br/>mergeable, approved, review_pending, pr_open"]
Check3 -->|No| Check4{"activity_state = active?"}
Check4 -->|Yes| Status4["Status: working"]
Check4 -->|No| Check5{"Signal capable + <90s grace?"}
Check5 -->|Yes| Status5["Status: no_signal"]
Check5 -->|No| Status6["Status: idle"]
PRStatus --> Final["Display Status"]
Status1 --> Final
Status2 --> Final
Status3 --> Final
Status4 --> Final
Status5 --> Final
Status6 --> Final
```
## Core Components
### Lifecycle Manager
`lifecycle.Manager` is the write path for session lifecycle facts:
- Runtime observations can mark a session terminated only when runtime and process are both clearly dead
- Activity signals update `activity_state`; `exited` also marks the session terminated
- PR observations trigger agent nudges for CI failures, review feedback, and merge conflicts
### Session Manager
`session_manager.Manager` performs internal session mutations:
- `Spawn` creates a row, creates workspace/runtime resources, and reports the
handles to the lifecycle manager.
- `Kill` marks the row terminated, then tears down runtime/workspace resources.
- `Restore` relaunches a terminated session and clears `is_terminated` via the
spawn-completed path.
| Operation | Description |
|-----------|-------------|
| **Spawn** | Create row → create workspace → create runtime → execute agent → report spawned |
| **Kill** | Mark terminated → destroy runtime → destroy workspace |
| **Restore** | Check terminated → restore workspace → create runtime → execute agent → report spawned |
| **Cleanup** | Reclaim terminated session workspaces |
`service.Session` is the controller-facing boundary. It delegates commands to
`session_manager.Manager` and attaches derived display status on read paths.
### PR Manager
`pr.Manager` records SCM observations into the PR/check/comment tables:
- Persists PR state, CI results, review comments
- Forwards observations to lifecycle for agent nudges
- Merged PR marks owning session terminated
### Reaper
`observe/reaper` polls runtime liveness:
- Checks if tmux sessions still exist
- Checks if pty-host processes are alive
- Marks dead sessions terminated
- Cleans up leaked resources
## Persistence and CDC
SQLite is the durable store. User-visible table changes are captured by database
triggers into `change_log`; the Go store does not manually emit CDC events. A
poller tails `change_log` and publishes live events to in-process subscribers.
SQLite is the durable store with CDC triggers:
## Load-bearing rules
```mermaid
graph LR
SQLite["SQLite Database"]
Trigger["DB Triggers"]
ChangeLog["change_log table"]
Poller["CDC Poller"]
SSE["SSE Broadcaster"]
Frontend["Frontend Clients"]
- Do not store display status.
- Keep session status facts small: `activity_state`, `is_terminated`, and PR
facts are the durable inputs.
- Do not treat failed probes as death.
- Do not force-delete registered dirty worktrees.
SQLite -->|"INSERT/UPDATE"| Trigger
Trigger -->|"append row"| ChangeLog
ChangeLog -->|"tail (100ms)"| Poller
Poller -->|"publish events"| SSE
SSE -->|"SSE stream"| Frontend
```
**Tables:**
- `projects` — Registered repos with soft-delete
- `sessions` — Session facts (activity_state, is_terminated, runtime metadata)
- `pr` — PR facts (state, ci_state, review_decision, mergeability)
- `pr_checks` — CI run history
- `pr_comment` — Review comments
- `change_log` — CDC event log
- `notifications` — Dashboard notifications
- `review_runs` — Code review execution records
- `telemetry_events` — Telemetry storage
## Supported Agents (23+)
claude-code, codex, aider, cursor, opencode, cline, copilot, grok, droid, amp, agy, crush, qwen, goose, auggie, continue, devin, kimi, kiro, kilocode, vibe, pi, autohand
## Configuration
All configuration via environment variables:
| Variable | Default | Purpose |
|----------|---------|---------|
| `AO_PORT` | `3001` | HTTP bind port |
| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap |
| `AO_RUN_FILE` | `~/.ao/running.json` | PID/port handshake |
| `AO_DATA_DIR` | `~/.ao/data` | SQLite data directory |
| `AO_AGENT` | `claude-code` | Compatibility agent |
| `GITHUB_TOKEN` | — | GitHub auth token |
**Runtime selection is automatic** based on platform — no configuration needed.
## Data Directory Structure
```
~/.ao/
├── running.json # PID + port handshake
├── data/ # SQLite state
│ ├── ao.db # Main database
│ ├── ao.db-wal # Write-ahead log
│ └── ao.db-shm # Shared memory
└── electron/ # Electron userData (for desktop app)
```
## Load-bearing Rules
- Do not store display status
- Keep session status facts small: `activity_state`, `is_terminated`, and PR facts are the durable inputs
- Do not treat failed probes as death
- Do not force-delete registered dirty worktrees
- Runtime selection is platform-based, not configurable
## Related Documentation
- [Backend Code Structure](backend-code-structure.md) — Package-by-package ownership
- [AGENTS.md](../AGENTS.md) — Contributor and worker-agent contract
- [CLI Reference](cli/README.md) — Complete CLI command documentation
- [Telemetry](telemetry.md) — Telemetry policy and configuration

View File

@ -1,392 +1,536 @@
# Backend Code Structure
This document describes package ownership for the Go backend. It is about where
code belongs. See [architecture.md](architecture.md) for lifecycle behavior,
status derivation, persistence, CDC, and invariants.
This document describes package ownership for the Go backend. It is about where code belongs. See [architecture.md](architecture.md) for lifecycle behavior, status derivation, persistence, CDC, and invariants.
## Goal
The backend is a local daemon that supervises coding-agent sessions. The code
needs clear homes for product workflows, protocol surfaces, persistence, and
replaceable external systems without turning any single package into a catch-all.
The backend is a local daemon that supervises coding-agent sessions. The code needs clear homes for product workflows, protocol surfaces, persistence, and replaceable external systems without turning any single package into a catch-all.
The current structure is a layered hybrid:
- `domain` holds shared product vocabulary and durable fact records.
- `service/*` owns controller-facing product use cases and read models.
- `session_manager` owns internal session mutations and resource orchestration.
- `lifecycle` owns the durable session fact reducer.
- `ports` defines narrow capability interfaces consumed by core code.
- `adapters/*` implements those capabilities with real external systems.
- `storage/sqlite` and `cdc` own persistence and change delivery.
- `httpd` and `cli` own protocol concerns.
- `daemon` wires the production graph together.
```
domain → shared product vocabulary and durable fact records
service/* → controller-facing product use cases and read models
session_manager → internal session mutations and resource orchestration
lifecycle → durable session fact reducer
ports → narrow capability interfaces consumed by core code
adapters/* → implementations of those capabilities
storage/sqlite → persistence and change delivery
httpd → HTTP protocol concerns
cli → CLI protocol concerns
daemon → production composition root
```
## Package Architecture Overview
```mermaid
graph TB
subgraph EntryPoints["Entry Points"]
CLI["cmd/ao<br/>CLI entrypoint"]
Main["main.go<br/>Daemon entrypoint"]
end
subgraph CoreLayer["Core Layer"]
Domain["domain<br/>Shared vocabulary"]
Ports["ports<br/>Capability interfaces"]
Services["service/*<br/>Product use cases"]
SessionMgr["session_manager<br/>Internal commands"]
Lifecycle["lifecycle<br/>Fact reducer"]
end
subgraph AdapterLayer["Adapter Layer"]
AgentAdapters["adapters/agent/*<br/>23+ agents"]
RuntimeAdapters["adapters/runtime/*<br/>tmux + conpty"]
WorkspaceAdapters["adapters/workspace/*<br/>git worktree"]
SCMAdapters["adapters/scm/*<br/>GitHub"]
TrackerAdapters["adapters/tracker/*<br/>GitHub"]
end
subgraph Infrastructure["Infrastructure"]
Storage["storage/sqlite<br/>Persistence"]
CDC["cdc<br/>Change delivery"]
Terminal["terminal<br/>PTY protocol"]
HTTPD["httpd<br/>HTTP API"]
CLI2["cli<br/>CLI commands"]
Daemon["daemon<br/>Composition root"]
Config["config<br/>Configuration"]
end
CLI --> CLI2
Main --> Daemon
Daemon --> Services
Daemon --> AdapterLayer
Daemon --> Infrastructure
Services --> Domain
Services --> Ports
Services --> SessionMgr
SessionMgr --> Ports
SessionMgr --> Lifecycle
AdapterLayer --> Ports
AdapterLayer --> Domain
HTTPD --> Services
CLI2 --> HTTPD
Services --> Storage
Lifecycle --> Storage
Storage --> CDC
Terminal --> RuntimeAdapters
HTTPD --> Terminal
```
## Package Relationships
```mermaid
graph LR
Controllers["httpd/controllers"] -->|calls| Services["service/*"]
Controllers2["cli commands"] -->|calls| HTTP["httpd"]
Services -->|orchestrates| SessionMgr["session_manager"]
Services -->|queries| Storage["storage/sqlite"]
SessionMgr -->|uses| Runtime["ports.Runtime"]
SessionMgr -->|uses| Workspace["ports.Workspace"]
SessionMgr -->|uses| Agent["ports.Agent"]
SessionMgr -->|reports to| Lifecycle["lifecycle"]
Runtime -->|implemented by| Tmux["adapters/runtime/tmux"]
Runtime -->|implemented by| Conpty["adapters/runtime/conpty"]
Lifecycle -->|persists to| Storage
Storage -->|triggers| CDC["cdc"]
CDC -->|broadcasts to| Frontend["Frontend subscribers"]
```
## Package Roles
### `internal/domain`
`domain` is AO's shared product language. Keep it stable and free of
infrastructure imports.
`domain` is AO's shared product language. Keep it stable and free of infrastructure imports.
Belongs here:
**Belongs here:**
- Shared IDs: `ProjectID`, `SessionID`, `IssueID`
- Shared enums and status vocabulary
- Durable fact records that multiple packages must agree on
- PR, tracker, project, and session vocabulary (not transport-specific)
- shared IDs such as `ProjectID`, `SessionID`, and `IssueID`;
- shared enums and status vocabulary;
- durable fact records that multiple packages must agree on;
- PR, tracker, project, and session vocabulary that is not transport-specific.
**Does not belong here:**
- HTTP request/response DTOs
- CLI output shapes
- OpenAPI wrapper/envelope types
- sqlc generated rows
- GitHub, tmux, Claude, Codex, or OpenCode payloads
- One-resource controller helper types
Does not belong here:
- HTTP request/response DTOs;
- CLI output shapes;
- OpenAPI wrapper/envelope types;
- sqlc generated rows;
- GitHub, tmux, Claude, Codex, or OpenCode payloads;
- one-resource controller helper types.
Rule of thumb: if AO would still use the concept after replacing HTTP, the CLI,
SQLite, GitHub, the tmux/conpty runtime, and every agent adapter, and more than one package needs
the exact vocabulary, it may belong in `domain`.
**Rule of thumb:** If AO would still use the concept after replacing HTTP, the CLI, SQLite, GitHub, the tmux/conpty runtime, and every agent adapter, and more than one package needs the exact vocabulary, it may belong in `domain`.
### `internal/service/*`
`service` packages are the controller-facing application boundary.
Current examples:
**Belongs here:**
- Resource use cases called by HTTP controllers and CLI-backed API flows
- Resource read models and command/result types
- Display-model assembly (session status derived from session and PR facts)
- Resource-specific validation and user-facing errors
- Small store interfaces consumed by the service
```txt
internal/service/project
internal/service/session
internal/service/pr
internal/service/review
```
Belongs here:
- resource use cases called by HTTP controllers and CLI-backed API flows;
- resource read models and command/result types;
- display-model assembly, such as session status derived from session and PR
facts;
- resource-specific validation and user-facing errors;
- small store interfaces consumed by the service.
Does not belong here:
- low-level runtime/workspace/agent process control;
- raw sqlc generated rows as public service results;
- HTTP routing, path parsing, status-code decisions, or OpenAPI generation;
- concrete external adapter details.
For example, project API concepts live in `internal/service/project`, not in
`domain` and not in a top-level `internal/project` package.
**Does not belong here:**
- Low-level runtime/workspace/agent process control
- Raw sqlc generated rows as public service results
- HTTP routing, path parsing, status-code decisions, or OpenAPI generation
- Concrete external adapter details
### `internal/session_manager`
`session_manager` owns internal session commands: spawn, restore, kill, cleanup,
and send-related orchestration over runtime, workspace, agent, storage,
messenger, and lifecycle dependencies.
`session_manager` owns internal session commands: spawn, restore, kill, cleanup, and send-related orchestration over runtime, workspace, agent, storage, messenger, and lifecycle dependencies.
Belongs here:
**Belongs here:**
- Multi-step session mutations
- Rollback/cleanup sequencing when spawn partially succeeds
- Resource teardown safety
- Internal errors (not found, terminated, not restorable)
- multi-step session mutations;
- rollback/cleanup sequencing when spawn partially succeeds;
- resource teardown safety;
- internal errors such as not found, terminated, or not restorable.
**Does not belong here:**
- HTTP request decoding
- CLI formatting
- Controller-facing list/get read-model assembly
- Terminal WebSocket framing
Does not belong here:
- HTTP request decoding;
- CLI formatting;
- controller-facing list/get read-model assembly;
- terminal WebSocket framing.
The split is intentional: `service/session` is the product/API boundary;
`session_manager` is the internal command engine.
The split is intentional: `service/session` is the product/API boundary; `session_manager` is the internal command engine.
### `internal/lifecycle`
`lifecycle` is the canonical write path for durable session lifecycle facts. It
reduces runtime observations, activity signals, spawn completion, termination,
and PR observations into small persisted facts.
`lifecycle` is the canonical write path for durable session lifecycle facts. It reduces runtime observations, activity signals, spawn completion, termination, and PR observations into small persisted facts.
Belongs here:
**Belongs here:**
- Updates to lifecycle-owned session facts
- Guardrails around runtime/activity observations
- Lifecycle-triggered agent nudges for actionable PR facts
- updates to lifecycle-owned session facts;
- guardrails around runtime/activity observations;
- lifecycle-triggered agent nudges for actionable PR facts.
Does not belong here:
- display status persistence;
- HTTP/CLI DTOs;
- direct adapter implementation details;
- PR row persistence.
The UI status is derived at read time by service code. Do not store display
status in lifecycle or SQLite.
**Does not belong here:**
- Display status persistence
- HTTP/CLI DTOs
- Direct adapter implementation details
- PR row persistence
### `internal/ports`
`ports` contains narrow capability interfaces and shared adapter-facing structs.
It connects core code to replaceable systems.
`ports` contains narrow capability interfaces and shared adapter-facing structs. It connects core code to replaceable systems.
Current capability examples:
**Capability interfaces:**
- `Runtime` — Create/Destroy/IsAlive for tmux/conpty sessions
- `Workspace` — Git worktree creation/destruction
- `Agent` — Agent launch, restore, hooks, session info
- `Attacher` — Terminal streaming attachment
- `PRWriter` — PR fact persistence
- `AgentResolver` — Agent binary resolution
- `AgentMessenger` — Message delivery to agents
- `Runtime`
- `Workspace`
- `Agent`
- `AgentResolver`
- `AgentMessenger`
- `PRWriter`
**Belongs here:**
- Interfaces consumed by core packages and implemented by adapters
- Capability structs: `RuntimeConfig`, `WorkspaceConfig`, `SpawnConfig`
- Vocabulary needed at the boundary between core orchestration and adapters
Belongs here:
- interfaces consumed by core packages and implemented by adapters;
- capability structs such as `RuntimeConfig`, `WorkspaceConfig`, and
`SpawnConfig`;
- vocabulary needed at the boundary between core orchestration and adapters.
Does not belong here:
- resource read models like project/session API responses;
- HTTP request/response DTOs;
- sqlc rows;
- concrete adapter options;
- one-off interfaces that only a single package needs internally.
Keep `ports` capability-oriented. It should not become the dumping ground for
every manager, DTO, and resource contract.
**Does not belong here:**
- Resource read models (project/session API responses)
- HTTP request/response DTOs
- sqlc rows
- Concrete adapter options
- One-off interfaces that only a single package needs internally
### `internal/adapters/*`
Adapters are concrete implementations of external systems.
Adapters are concrete implementations of external systems. They should be leaves in the import graph.
Current examples:
**Runtime Adapters:**
```mermaid
graph LR
RunSelect["runtimeselect"] -->|Darwin/Linux| Tmux["tmux<br/>Unix PTY integration"]
RunSelect -->|Windows| Conpty["conpty<br/>ConPTY + B1 protocol"]
Tmux -->|implements| RuntimePort["ports.Runtime"]
Conpty -->|implements| RuntimePort
Tmux -->|uses| PtyExec["ptyexec<br/>Unix PTY spawning"]
Conpty -->|uses| PtyHost["Pty-host server<br/>Ring buffer + Registry"]
```
**Current adapters:**
```txt
internal/adapters/agent/claudecode
internal/adapters/agent/codex
internal/adapters/agent/opencode
internal/adapters/runtime/tmux
internal/adapters/runtime/conpty
internal/adapters/runtime/runtimeselect
internal/adapters/runtime/ptyexec
internal/adapters/runtime/tmux # Darwin/Linux
internal/adapters/runtime/conpty # Windows
internal/adapters/runtime/runtimeselect # Platform selector
internal/adapters/runtime/ptyexec # PTY spawning
internal/adapters/workspace/gitworktree
internal/adapters/scm/github
internal/adapters/tracker/github
internal/adapters/reviewer/claudecode
```
Adapters should be leaves in the import graph. They translate external behavior
into AO ports and domain concepts; they should not own product workflows.
Good:
```txt
session_manager -> ports.Runtime
adapters/runtime/tmux -> ports + domain
adapters/workspace/gitworktree -> ports + domain
daemon -> adapters + services + storage
**Good dependencies:**
```
session_manager → ports.Runtime
adapters/runtime/tmux → ports + domain
adapters/workspace/gitworktree → ports + domain
daemon → adapters + services + storage
```
Avoid:
```txt
domain -> adapters
service/session -> adapters/runtime/tmux
httpd/controllers -> storage/sqlite/store
adapters/* -> httpd
**Avoid:**
```
domain → adapters
service/session → adapters/runtime/tmux
httpd/controllers → storage/sqlite/store
adapters/* → httpd
```
### `internal/storage/sqlite`
`storage/sqlite` owns SQLite setup, migrations, sqlc generated code, and store
implementations.
`storage/sqlite` owns SQLite setup, migrations, sqlc generated code, and store implementations.
Belongs here:
**Belongs here:**
- Connection setup and PRAGMAs
- Goose migrations
- sqlc queries and generated code
- Table-specific store methods
- Transactions and CDC-triggered persistence behavior
- connection setup and PRAGMAs;
- goose migrations;
- sqlc queries and generated code;
- table-specific store methods;
- transactions and CDC-triggered persistence behavior.
**Does not belong here:**
- HTTP response types
- CLI output formatting
- Product display status rules
- External adapter logic
Does not belong here:
- HTTP response types;
- CLI output formatting;
- product display status rules;
- external adapter logic.
Generated sqlc types should stay behind store methods. Services and lifecycle
code should work with domain records or service read models, not generated rows.
Generated sqlc types should stay behind store methods. Services and lifecycle code should work with domain records or service read models, not generated rows.
### `internal/cdc`
`cdc` owns `change_log` polling and event broadcasting. SQLite triggers append
durable events to `change_log`; the poller tails that table and fans events out
to subscribers.
`cdc` owns `change_log` polling and event broadcasting. SQLite triggers append durable events to `change_log`; the poller tails that table and fans events out to subscribers.
Belongs here:
**Belongs here:**
- Event type definitions for the CDC stream
- Poller and broadcaster logic
- Subscriber fan-out behavior
- event type definitions for the CDC stream;
- poller and broadcaster logic;
- subscriber fan-out behavior.
Does not belong here:
- terminal byte streams;
- product workflow decisions;
- database schema ownership.
**Does not belong here:**
- Terminal byte streams
- Product workflow decisions
- Database schema ownership
### `internal/terminal`
`terminal` owns the terminal session protocol and PTY attach management used by
the HTTP mux. The runtime is selected by `runtimeselect`: tmux on Darwin/Linux,
conpty on Windows. Every client that opens a pane gets its own attach Stream. On
unix, tmux spawns `tmux attach` on a local PTY via ptyexec; on Windows, conpty
dials the session's loopback pty-host directly. Either way the runtime owns
screen state and scrollback and replays its init handshake plus a full repaint
per attach, so there is no shared per-pane buffer.
`terminal` owns the terminal session protocol and PTY attach management used by the HTTP mux. The runtime is selected by `runtimeselect`: tmux on Darwin/Linux, conpty on Windows.
Belongs here:
```mermaid
sequenceDiagram
participant WS as WebSocket Client
participant Mux as Terminal Mux
participant Term as terminal package
participant Runtime as Runtime Adapter
- per-client attachment lifecycle (liveness gating, re-attach backoff);
- input/output framing independent of HTTP;
- PTY-backed attach handling and terminal protocol tests.
WS->>Mux: WebSocket upgrade
Mux->>Term: Create attach session
Term->>Runtime: Runtime.Attach(handle, rows, cols)
alt tmux (Darwin/Linux)
Runtime->>Runtime: tmux attach-session -t <id>
Runtime->>Runtime: PTY stream via ptyexec
else conpty (Windows)
Runtime->>Runtime: Dial loopback TCP
Runtime->>Runtime: B1 protocol handshake
Runtime->>Runtime: MsgTerminalData (scrollback)
end
Runtime-->>Term: Stream
Term-->>Mux: Framed protocol
Mux-->>WS: WebSocket messages
```
`httpd` adapts WebSocket connections to terminal interfaces; `terminal` should
not import `httpd`.
**Belongs here:**
- Per-client attachment lifecycle (liveness gating, re-attach backoff)
- Input/output framing independent of HTTP
- PTY-backed attach handling and terminal protocol tests
### `internal/httpd`
`httpd` is the HTTP protocol adapter.
Belongs here:
**Belongs here:**
- Routing and middleware
- HTTP request decoding and response encoding
- Path/query parameter handling
- Status-code mapping
- API error envelopes
- OpenAPI generation and serving
- WebSocket upgrade handling for terminal mux
- routing and middleware;
- HTTP request decoding and response encoding;
- path/query parameter handling;
- status-code mapping;
- API error envelopes;
- OpenAPI generation and serving;
- WebSocket upgrade handling for terminal mux.
Controllers call service managers and translate service results/errors into HTTP
responses. Controllers should not reach directly into concrete adapters or the
SQLite store.
HTTP-only request/response wrappers belong in `httpd` or
`httpd/controllers`. Application read models shared by controller and CLI flows
belong in the owning `service/*` package.
Controllers call service managers and translate service results/errors into HTTP responses. Controllers should not reach directly into concrete adapters or the SQLite store.
### `internal/cli`
`cli` owns the user-facing `ao` command. It should stay thin:
- discover the local daemon;
- call the daemon's loopback HTTP API;
- format command output;
- start/stop/status/doctor process control.
- Discover the local daemon
- Call the daemon's loopback HTTP API
- Format command output
- Start/stop/status/doctor process control
The CLI should not duplicate daemon business logic. If a command needs product
behavior, put the behavior in the daemon service/API path and have the CLI call
that path.
The CLI should not duplicate daemon business logic. If a command needs product behavior, put the behavior in the daemon service/API path and have the CLI call that path.
### `internal/daemon`
`daemon` is the production composition root. It wires config, logging, SQLite,
CDC, lifecycle, reaper, runtime, terminal manager, services, HTTP, and shutdown.
`daemon` is the production composition root. It wires config, logging, SQLite, CDC, lifecycle, reaper, runtime, terminal manager, services, HTTP, and shutdown.
Belongs here:
```mermaid
graph TD
Daemon["daemon package"] --> Config["config"]
Daemon --> Logging["slog logger"]
Daemon --> Storage["storage/sqlite"]
Daemon --> CDC["cdc"]
Daemon --> Lifecycle["lifecycle"]
Daemon --> Reaper["observe/reaper"]
Daemon --> Runtime["runtimeselect"]
Daemon --> Terminal["terminal"]
Daemon --> Services["service/*"]
Daemon --> HTTPD["httpd"]
Config -->|"env vars"| Daemon
Storage -->|"triggers"| CDC
CDC -->|"events"| HTTPD
```
- production dependency construction;
- adapter registration;
- startup/shutdown sequencing;
- cross-component wiring.
**Belongs here:**
- Production dependency construction
- Adapter registration
- Startup/shutdown sequencing
- Cross-component wiring
Does not belong here:
**Does not belong here:**
- Business logic that should be testable in service, lifecycle, or manager packages
- Adapter implementation details
- business logic that should be testable in service, lifecycle, or manager
packages;
- adapter implementation details.
## Current Tree
```mermaid
graph TB
Root["backend/"]
Root --> Cmd["cmd/ao/<br/># CLI entrypoint"]
Root --> Main["main.go<br/># Daemon entrypoint"]
Root --> Sqlc["sqlc.yaml"]
Root --> Domain["internal/domain/<br/># Shared vocabulary"]
Root --> Ports["internal/ports/<br/># Capability interfaces"]
Root --> Service["internal/service/"]
Service --> Proj["project/<br/># Project API"]
Service --> Sess["session/<br/># Session API"]
Service --> PR["pr/<br/># PR service"]
Service --> Review["review/<br/># Code review"]
Root --> SessMgr["internal/session_manager/<br/># Internal commands"]
Root --> Life["internal/lifecycle/<br/># Fact reducer"]
Root --> Observe["internal/observe/"]
Observe --> SCM["scm/<br/># GitHub observer"]
Observe --> Reap["reaper/<br/># Liveness observer"]
Root --> Store["internal/storage/sqlite/<br/># DB + stores"]
Root --> CDC2["internal/cdc/<br/># Change delivery"]
Root --> Term["internal/terminal/<br/># PTY protocol"]
Root --> HTTPD2["internal/httpd/<br/># HTTP API"]
Root --> CLI2["internal/cli/<br/># CLI commands"]
Root --> Daemon2["internal/daemon/<br/># Composition"]
Root --> Cfg["internal/config/<br/># Config"]
Root --> Adapters["internal/adapters/"]
Adapters --> Agent["agent/<br/># 23+ agents"]
Adapters --> Runtime2["runtime/"]
Runtime2 --> Tmux2["tmux<br/># Darwin/Linux"]
Runtime2 --> Conpty2["conpty<br/># Windows"]
Runtime2 --> RS["runtimeselect<br/># Selector"]
Runtime2 --> PE["ptyexec<br/># PTY spawning"]
Adapters --> WS["workspace/gitworktree"]
Adapters --> SCM2["scm/github"]
Adapters --> Track["tracker/github"]
Adapters --> Rev["reviewer/claudecode"]
```
## Interface Placement
Prefer interfaces near their consumers, except for shared capabilities.
- If only one package consumes an abstraction, define the smallest interface in
that package.
- If multiple core packages consume a replaceable capability, define it in
`ports`.
- If HTTP controllers need a resource service, use the owning `service/*`
manager interface.
- Return concrete types from constructors unless callers genuinely need an
interface.
## Current Tree
The current main-line shape is:
```txt
backend/
cmd/ao/ # CLI entrypoint
main.go # daemon entrypoint compatibility
sqlc.yaml
internal/domain/ # shared product vocabulary and durable facts
internal/ports/ # capability interfaces
internal/service/
project/ # project API/use-case boundary
session/ # session API/use-case boundary
pr/ # PR observation/action service
review/ # code-review API/use-case boundary
internal/session_manager/ # internal session command engine
internal/lifecycle/ # durable lifecycle fact reducer
internal/observe/scm/ # SCM (GitHub) observer loop
internal/observe/reaper/ # runtime liveness observation loop
internal/storage/sqlite/ # DB, migrations, queries, generated sqlc, stores
internal/cdc/ # change_log poller and broadcaster
internal/terminal/ # terminal session protocol and PTY handling
internal/httpd/ # HTTP API, controllers, OpenAPI, terminal mux
internal/cli/ # user-facing ao command
internal/daemon/ # production wiring and shutdown
internal/config/ # daemon env/default config
internal/adapters/ # concrete agent/runtime/workspace/SCM/tracker adapters
```
- **Single consumer:** Define the smallest interface in that package
- **Multiple core consumers:** Define it in `ports`
- **Resource service:** Use the owning `service/*` manager interface
- **Return types:** Return concrete types from constructors unless callers genuinely need an interface
## Adding New Code
Use these defaults:
- New HTTP route: add controller/API code in `httpd`, call a `service/*`
package, and update OpenAPI generation/spec tests.
- New product resource: put shared IDs/vocabulary in `domain`, use cases and
read models in `service/<resource>`, storage in `storage/sqlite`, and external
system seams in `ports`.
- New adapter: implement a `ports` interface under `adapters/<capability>/<impl>`
and wire it in `daemon`.
- New persisted fact: add a migration, sqlc query, store method, domain record or
event vocabulary, and CDC behavior when the UI/API must observe it.
- New CLI command: keep command parsing/formatting in `cli`; call the daemon API
rather than reimplementing daemon behavior.
```mermaid
graph LR
NewCode["New Code"] --> Choice{"What type?"}
Choice -->|HTTP route| HTTPRoute["Add to httpd/<br/>Call service/*<br/>Update OpenAPI"]
Choice -->|Product resource| Product["domain + service/<br/>storage + ports"]
Choice -->|Adapter| AdapterPath["adapters/<capability>/<impl><br/>Implement ports<br/>Wire in daemon"]
Choice -->|Persisted fact| Fact["migration + sqlc<br/>store + domain<br/>CDC trigger"]
Choice -->|CLI command| CLIPath["cli parsing/formatting<br/>Call daemon API"]
```
## Project Routes Example
Project-owned concepts live in `internal/service/project`:
- project read models;
- project add/remove command types;
- project validation and user-facing errors;
- the `Manager` contract consumed by HTTP controllers.
- Project read models
- Project add/remove command types
- Project validation and user-facing errors
- The `Manager` contract consumed by HTTP controllers
`internal/httpd/controllers` remains responsible for:
- Route registration
- JSON decoding/encoding
- HTTP status codes and error envelopes
- Mapping service errors to responses
- route registration;
- JSON decoding/encoding;
- HTTP status codes and error envelopes;
- mapping service errors to responses.
## Dependency Rules
When a type is ambiguous, ask whether it is a product use-case/read model or an
HTTP wire wrapper. Product use-case/read models belong in `service/project`;
HTTP wire wrappers belong in `httpd`.
```mermaid
graph LR
subgraph Allowed["Allowed Dependencies"]
Controllers["httpd/controllers"] -->|✓| Services["service/*"]
Services -->|✓| SessionMgr["session_manager"]
Services -->|✓| Storage["storage/sqlite"]
SessionMgr -->|✓| Ports["ports"]
Adapters["adapters/*"] -->|✓| Ports
Adapters -->|✓| Domain["domain"]
end
subgraph Avoid["Avoid Dependencies"]
Services2["service/*"] -.->|✗| Adapters2["adapters/*"]
Domain2["domain"] -.->|✗| Adapters2
Httpd["httpd"] -.->|✗| Storage2["storage/sqlite"]
Adapters2 -.->|✗| Httpd
end
```
**Key rules:**
- Controllers call services, not storage directly
- Services call session manager, not adapters directly
- Adapters implement ports, don't depend on HTTP/storage
- Domain stays pure, no infrastructure dependencies
- Terminal and httpd are separate (terminal should not import httpd)
## Runtime Architecture Details
### Platform Selection
The `runtimeselect` package automatically chooses the appropriate runtime based on the platform:
```go
func New(_ *slog.Logger) Runtime {
if runtime.GOOS != "windows" {
return tmux.New(tmux.Options{}) // Darwin/Linux
}
return conpty.New(conpty.Options{}) // Windows
}
```
No configuration needed — the system handles this automatically.
### tmux Implementation (Darwin/Linux)
**File:** `internal/adapters/runtime/tmux/tmux.go`
- Creates detached tmux sessions
- Uses `tmux send-keys` for input delivery
- Uses `tmux capture-pane` for scrollback
- Spawns `tmux attach-session` for terminal streaming
- Sessions survive daemon restart (tmux persistence)
### conpty Implementation (Windows)
**Files:** `internal/adapters/runtime/conpty/*.go`
- Spawns detached pty-host process
- Uses custom B1 binary protocol over loopback TCP
- Implements ring buffer for scrollback
- File-based registry for crash recovery
- Direct TCP connection for terminal streaming (no CLI attach)
## Related Documentation
- [Architecture](architecture.md) — System architecture and data flows
- [AGENTS.md](../AGENTS.md) — Contributor and worker-agent contract
- [CLI Reference](cli/README.md) — Complete CLI command documentation

2248
package-lock.json generated

File diff suppressed because it is too large Load Diff