This document describes package ownership for the Go backend. It is about where code belongs. See architecture.md for lifecycle behavior, status derivation, persistence, CDC, and invariants.
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:
domainholds shared product vocabulary and durable fact records.service/*owns controller-facing product use cases and read models.session_managerowns internal session mutations and resource orchestration.lifecycleowns the durable session fact reducer.portsdefines narrow capability interfaces consumed by core code.adapters/*implements those capabilities with real external systems.storage/sqliteandcdcown persistence and change delivery.httpdandcliown protocol concerns.daemonwires the production graph together.
domain is AO's shared product language. Keep it stable and free of
infrastructure imports.
Belongs here:
- shared IDs such as
ProjectID,SessionID, andIssueID; - 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, Zellij, 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, Zellij, and every agent adapter, and more than one package needs
the exact vocabulary, it may belong in domain.
service packages are the controller-facing application boundary.
Current examples:
internal/service/project
internal/service/session
internal/service/prBelongs 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.
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:
- 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.
The split is intentional: service/session is the product/API boundary;
session_manager is the internal command engine.
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:
- 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.
ports contains narrow capability interfaces and shared adapter-facing structs.
It connects core code to replaceable systems.
Current capability examples:
RuntimeWorkspaceAgentAgentResolverAgentMessengerPRWriter
Belongs here:
- interfaces consumed by core packages and implemented by adapters;
- capability structs such as
RuntimeConfig,WorkspaceConfig, andSpawnConfig; - 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.
Adapters are concrete implementations of external systems.
Current examples:
internal/adapters/agent/claudecode
internal/adapters/agent/codex
internal/adapters/agent/opencode
internal/adapters/runtime/zellij
internal/adapters/workspace/gitworktree
internal/adapters/scm/github
internal/adapters/tracker/githubAdapters should be leaves in the import graph. They translate external behavior into AO ports and domain concepts; they should not own product workflows.
Good:
session_manager -> ports.Runtime
adapters/runtime/zellij -> ports + domain
adapters/workspace/gitworktree -> ports + domain
daemon -> adapters + services + storageAvoid:
domain -> adapters
service/session -> adapters/runtime/zellij
httpd/controllers -> storage/sqlite/store
adapters/* -> httpdstorage/sqlite owns SQLite setup, migrations, sqlc generated code, and store
implementations.
Belongs here:
- 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.
Generated sqlc types should stay behind store methods. Services and lifecycle code should work with domain records or service read models, not generated rows.
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:
- 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.
terminal owns the terminal session protocol and PTY/session management used by
the HTTP mux.
Belongs here:
- terminal session lifecycle;
- input/output framing independent of HTTP;
- PTY-backed session handling;
- ring buffers and terminal protocol tests.
httpd adapts WebSocket connections to terminal interfaces; terminal should
not import httpd.
httpd is the HTTP protocol adapter.
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.
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.
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.
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.
daemon is the production composition root. It wires config, logging, SQLite,
CDC, lifecycle, reaper, runtime, terminal manager, services, HTTP, and shutdown.
Belongs here:
- production dependency construction;
- adapter registration;
- startup/shutdown sequencing;
- cross-component wiring.
Does not belong here:
- business logic that should be testable in service, lifecycle, or manager packages;
- adapter implementation details.
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.
The current main-line shape is:
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
internal/session_manager/ # internal session command engine
internal/lifecycle/ # durable lifecycle fact reducer
internal/observe/reaper/ # runtime 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 adaptersUse these defaults:
- New HTTP route: add controller/API code in
httpd, call aservice/*package, and update OpenAPI generation/spec tests. - New product resource: put shared IDs/vocabulary in
domain, use cases and read models inservice/<resource>, storage instorage/sqlite, and external system seams inports. - New adapter: implement a
portsinterface underadapters/<capability>/<impl>and wire it indaemon. - 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.
Project-owned concepts live in internal/service/project:
- project read models;
- project add/remove command types;
- project validation and user-facing errors;
- the
Managercontract 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.
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.