docs: sync architecture and code structure docs
This commit is contained in:
parent
c87eb075bc
commit
bc7fc85f79
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue