docs: document backend code structure (#41)
This commit is contained in:
parent
86f3f0880c
commit
d623ab28b7
|
|
@ -13,6 +13,7 @@ Start with [architecture.md](architecture.md) for the current backend model and
|
|||
| Doc | What it covers |
|
||||
|-----|----------------|
|
||||
| [architecture.md](architecture.md) | Current backend model, package layout, status derivation, persistence/CDC, and load-bearing rules. |
|
||||
| [backend-code-structure.md](backend-code-structure.md) | Package ownership rules for the Go backend: domain, services, ports, adapters, storage, HTTP, CLI, and daemon wiring. |
|
||||
| [cli/README.md](cli/README.md) | CLI commands and daemon control surface. |
|
||||
| [status.md](status.md) | Current implementation shape, build/test command, and next integration work. |
|
||||
| [stack.md](stack.md) | Accepted library/runtime choices, pending stack decisions, and dependencies explicitly avoided for V1. |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,382 @@
|
|||
# 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.
|
||||
|
||||
## 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 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.
|
||||
|
||||
## Package Roles
|
||||
|
||||
### `internal/domain`
|
||||
|
||||
`domain` is AO's shared product language. Keep it stable and free of
|
||||
infrastructure imports.
|
||||
|
||||
Belongs here:
|
||||
|
||||
- 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, 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`.
|
||||
|
||||
### `internal/service/*`
|
||||
|
||||
`service` packages are the controller-facing application boundary.
|
||||
|
||||
Current examples:
|
||||
|
||||
```txt
|
||||
internal/service/project
|
||||
internal/service/session
|
||||
internal/service/pr
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### `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.
|
||||
|
||||
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.
|
||||
|
||||
### `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.
|
||||
|
||||
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.
|
||||
|
||||
### `internal/ports`
|
||||
|
||||
`ports` contains narrow capability interfaces and shared adapter-facing structs.
|
||||
It connects core code to replaceable systems.
|
||||
|
||||
Current capability examples:
|
||||
|
||||
- `Runtime`
|
||||
- `Workspace`
|
||||
- `Agent`
|
||||
- `AgentResolver`
|
||||
- `AgentMessenger`
|
||||
- `PRWriter`
|
||||
|
||||
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.
|
||||
|
||||
### `internal/adapters/*`
|
||||
|
||||
Adapters are concrete implementations of external systems.
|
||||
|
||||
Current examples:
|
||||
|
||||
```txt
|
||||
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/github
|
||||
```
|
||||
|
||||
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/zellij -> ports + domain
|
||||
adapters/workspace/gitworktree -> ports + domain
|
||||
daemon -> adapters + services + storage
|
||||
```
|
||||
|
||||
Avoid:
|
||||
|
||||
```txt
|
||||
domain -> adapters
|
||||
service/session -> adapters/runtime/zellij
|
||||
httpd/controllers -> storage/sqlite/store
|
||||
adapters/* -> httpd
|
||||
```
|
||||
|
||||
### `internal/storage/sqlite`
|
||||
|
||||
`storage/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.
|
||||
|
||||
### `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.
|
||||
|
||||
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.
|
||||
|
||||
### `internal/terminal`
|
||||
|
||||
`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`.
|
||||
|
||||
### `internal/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.
|
||||
|
||||
### `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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
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 adapters
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
`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`.
|
||||
Loading…
Reference in New Issue