Files
AirCoding/AirPlan/docs/architecture/system-detailed-design.md
AirCoding 33a76a1ebc Move project from external drive to local NVMe
迁移路径: /run/media/airlongdian/EasyU/AirCoding -> /home/airlongdian/DataDevices/AirWorkSpace/AirCoding

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:51:49 +08:00

2448 lines
112 KiB
Markdown
Executable File

# AirCoding V1.0.0 Alpha System Detailed Design and UML Class Model
Date: 2026-05-29
Status: Detailed design derived strictly from frozen baselines and system overview design
Scope: Implementation-facing class/method design, UML, sequence, and state designs for V1.0.0 Alpha
## 0. Authority and Traceability
This document is bound by the following frozen sources. It elaborates them into class-level
design but does not introduce new public contracts, new event types, new DB columns, or new
runtime semantics. Where a detail is not fixed by a baseline, it is marked `IMPL` (implementation
freedom inside the contract boundary).
| Source | Role in this document |
|---|---|
| `interface-contracts-v1.md` | Public TypeScript contracts. All classes implement these as-is. |
| `c4/code-view.md` | Package/file layout and class inventory. |
| `c4/module.md` | Container dependency direction. |
| `db-schema-v1.md` | SQLite tables, enums, indexes. |
| `event-registry-v1.md` | Event names, payloads, persistence policy. |
| `scheduler-state-machine-v1.md` | Scheduler lifecycle and transitions. |
| `main-agent-state-machine.md` | Main Agent lifecycle. |
| `runtime-semantics-v1.md` | Ingestion boundary, FK-off, outbox, execution primitives. |
| `scope-escalation-v1.md` | Escalation/impact routing. |
| `security-model-v1.md` | Path/command risk, permission layering. |
| `error-taxonomy-v1.md` | `AirError`, `ErrorKind`, retryability, signature. |
| `prompt-layering-v1.md` | PromptLayer L0-L9. |
| `provider-capability-matrix-v1.md` | Provider adapter behavior. |
| `capability-trust-v1.md` | Capability lifecycle/trust. |
| `artifact-naming-v1.md` | Artifact URI/ID/filename. |
| `system-overview-design.md` | Container/component/flow overview. |
Naming rule (from contracts §1): exported contract fields are `snake_case`; class names are
`PascalCase`; private methods may use local camelCase. Method signatures below restate the frozen
contract types verbatim.
## 1. Document Structure
1. §2 System decomposition and module ownership
2. §3 Contracts package detailed design
3. §4 Storage and repositories
4. §5 Event subsystem (Ingestor/Store/Bus/SchemaRegistry)
5. §6 Project/session lifecycle
6. §7 Scheduler subsystem
7. §8 Worker/IPC subsystem
8. §9 Tool + Permission + Capability subsystem
9. §10 Context/Prompt/Compaction subsystem
10. §11 Artifact/Evidence/Knowledge subsystem
11. §12 Provider (LLM) subsystem
12. §13 Projection + TUI subsystem
13. §14 Agents (Main, Architecture Designer) subsystem
14. §15 Toolchain C++ subsystem
15. §16 Doctor/Logging/Migration/Recovery subsystem
16. §17 CLI subsystem
17. §18 Cross-cutting designs (error, transaction, FK-off, outbox)
18. §19 Sequence designs
19. §20 State machine designs
20. §21 Traceability matrix
## 2. System Decomposition and Module Ownership
Dependency direction (frozen by `c4/module.md`), expressed as allowed imports:
```text
contracts → (none)
llm → contracts
toolchain-cpp → contracts
tui → contracts
runtime → contracts, llm (facade only)
cli → contracts, runtime, tui, llm, toolchain-cpp
workers → contracts + WorkerRuntime IPC surface (no direct runtime import)
```
Forbidden edges (frozen by contracts §23) are enforced by lint boundaries and reviewed at the
architecture gate. The detailed class design below never crosses these edges.
Ownership summary (frozen by code-view §11 State Ownership):
| State | Owning class | Reader access |
|---|---|---|
| session DB | `SessionStore`, `EventStore` | runtime services only |
| live events | `EventBus` | runtime publish/subscribe |
| UI projection | `ProjectionStore` | TUI read-only via `ProjectionClient` |
| artifacts | `ArtifactStore` | tools/workers via runtime API |
| evidence | `EvidenceStore` | reports/reviews/debug |
| tasks/agents | `Scheduler` | repositories are storage-only |
| permission decisions | `PermissionEngine` | `ToolRegistry` requests |
| model config | `ProviderManager` | runtime/Doctor read via API |
| project rules/context | `ContextAssembler`/`ProjectStore` | workers get excerpts |
**Worker `AgentType` vs. runtime roles** (contracts §5 `AgentType`, overview §10):
The frozen `AgentType` union covers only worker child-process roles:
`executor | reviewer | debugger | compactor | experience_miner`.
Runtime-resident roles are *not* members of `AgentType` and never appear in
`WorkerResult.agent_type`, `agents.agent_type`, or `PromptLayerLoader.load_role(role)`:
| Runtime role | Implementing class | LLM use | Prompt source |
|---|---|---|---|
| `main` | `MainAgent` (§14.1) | Yes, via `ProviderManager` | Built-in resource (loaded directly by `MainAgent`, see §10.2) |
| `architecture_designer` | `ArchitectureDesigner` (§14.2) | Yes, via `ProviderManager` | Built-in resource (loaded directly by `ArchitectureDesigner`, see §10.2) |
| `scheduler` | `Scheduler` (§7.1) | No (pure orchestration) | N/A |
This split keeps the contract `AgentType` enum stable and avoids accidentally giving runtime
roles a worker `WorkerResult` shape. Where event payloads need to identify a runtime role
(e.g. `EventSource.kind` in §22.1), they use a separate `"main" | "architecture_designer" |
"scheduler"` literal set defined in `event.ts`, never `AgentType`.
## 3. Contracts Package Detailed Design
`packages/contracts` is type-only. It contains zero runtime logic, only `export interface`,
`export type`, and nominal aliases. File layout is frozen by code-view §3.
`IMPL` note: the contracts package may include tiny pure type-guards (e.g. `is_air_error`) only if
they have no dependencies; default is to keep it declaration-only to satisfy code-view §3 rule
"type-only package has no implementation deps" (todo T-002).
Design rules for the contracts package:
1. Every interface in `interface-contracts-v1.md` §2-§21 is exported from the file mapped in
code-view §3 "Contract Ownership".
2. No interface gains extra fields here. Field additions require an ADR (contracts §22 rule 6).
3. `JsonSchema<T>` stays nominal (`JsonObject`); no runtime schema engine lives in contracts.
4. Re-export surface is `index.ts` which barrel-exports every contract file.
Contract-to-file map (frozen by code-view §3):
```text
ids.ts → primitive ID aliases, Clock, IdGenerator
error.ts → ErrorKind, ErrorSeverity, Retryability, AirError
event.ts → EntityType, EntityRef, EventSource, RuntimeEvent, EventFilter
runtime.ts → AgentType, AgentRuntimeContext, ContextPack
ipc.ts → IpcDirection, IpcEnvelope, IpcKind, IpcMessage, ControlMessage, payloads
task.ts → Task*, VerificationPolicy, TaskConstraints, TaskSpec, TaskGraph, Scheduler*
worker-result.ts→ WorkerStatus, WorkerResult, ExecutorResult, ReviewerResult, DebuggerResult,
CompactorResult, ExperienceMinerResult, BlockerReport, Risk, FollowUpTask
tool.ts → ToolCategory, ToolDefinition, ToolExecutor, StreamingToolExecutor,
ToolExecutionContext, ToolResultEnvelope, ToolEvent, ToolRegistry
permission.ts → PathPolicy, PermissionRequestContext, PermissionAction, PermissionGrantScope,
PermissionDecision, PermissionRecordResult, PermissionEngine
artifact.ts → ArtifactRef, ArtifactCreateInput, ArtifactContext, ArtifactReadResult,
ArtifactStore
evidence.ts → EvidenceRef, EvidenceCreateInput, EvidenceStore
project.ts → ProjectContext, ProjectInitOptions, ProjectStore, SessionContext,
OpenSessionOptions, SessionManager
provider.ts → ProviderCapabilityMatrix, ModelRequirement, ProviderCompletionInput,
ProviderStreamEvent, ProviderAdapter, ProviderManager, ModelAssignment
ui.ts → all *Projection, ProjectionSnapshot, ProjectionStore, ProjectionClient
capability.ts → CapabilityManifestV1, ValidationResult, CapabilityRegistry
platform.ts → cross-platform tier enums referenced by Doctor (from cross-platform-matrix)
```
`IMPL`: `storage.ts`, `scheduler.ts`, `workers.ts`, `context.ts`, `projection.ts`, `doctor.ts`,
`knowledge.ts`, `diagnostics.ts` from overview §4 may either be separate files or be merged into the
above; the binding requirement is that the exported symbol set equals the contract set. This design
keeps the code-view §3 list as canonical and treats overview §4 as the superset note.
**Frozen file-set decision (P2-09)**: For V1.0.0 Alpha, the canonical contracts package has exactly
**16 mandatory files** (frozen by code-view §3); see the list above. Overview §4 symbol groups that
do not have a matching dedicated file are merged into one of these 16 files per the mapping below.
The default for V1.0.0 Alpha is: **all overview §4 symbol groups are inlined into the 16
mandatory files** (i.e. the "default home" column).
| Overview §4 symbol group | Default home (mandatory, no ADR needed) |
|---|---|
| `storage.ts` symbols (TransactionManager, Repository facades) | merged into `task.ts` (TransactionManager) and per-domain files |
| `scheduler.ts` symbols (SchedulerWavePlan, SchedulerRunResult) | `task.ts` |
| `workers.ts` symbols (WorkerRole, WorkerRuntime, IPC payloads) | `ipc.ts` + `worker-result.ts` |
| `context.ts` symbols (PromptLayer, PromptLayerLoader, ContextAssembler) | `runtime.ts` (`ContextPack` and prompt-layer types are co-located) |
| `projection.ts` symbols | `ui.ts` |
| `doctor.ts` symbols (DoctorService, DoctorRunInput/Output) | `platform.ts` (cross-platform tier + doctor types co-located) |
| `knowledge.ts` symbols (DebugKnowledgeStore, LearnedMemoryStore) | `artifact.ts` |
| `diagnostics.ts` symbols (Diagnostic, semantic_signature types) | `tool.ts` |
The barrel `index.ts` exports the full union. **No new `.ts` files** are added to the contracts
package for V1.0.0 Alpha. Any future split (for example extracting a dedicated `context.ts`,
`doctor.ts`, `knowledge.ts`, or `diagnostics.ts`) is **out of scope for V1.0.0 Alpha** and requires
an ADR under `docs/architecture/adr/` plus a synchronized update to code-view §3 before it may be
introduced in a later version.
## 4. Storage and Repositories
Module: `packages/runtime/src/storage/`. Classes: `DatabaseManager`, `MigrationRunner`,
repositories under `repositories/`.
### 4.1 DatabaseManager
Implements `TransactionManager` (contracts §6) over Bun's SQLite.
```text
class DatabaseManager implements TransactionManager
+open(path: string): DatabaseHandle
+transaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>
-applyPragmas(db): void // WAL, synchronous=NORMAL, foreign_keys=OFF (db-schema §1)
-handleFor(tx): RawDb
```
Rules (db-schema §1): on `open`, set `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=OFF`.
`transaction` wraps `BEGIN`/`COMMIT`/`ROLLBACK`. A durable event insert plus its domain update run
inside one `transaction` call (runtime-semantics §3). `TransactionHandle.id` is an opaque token
mapping to the active raw transaction; nested calls reuse the active handle (`IMPL`: single-writer
per session DB, so no real nesting needed).
### 4.2 MigrationRunner
```text
class MigrationRunner
+migrate(db: DatabaseHandle): Promise<void>
-currentVersion(db): number // reads schema_meta.schema_version
-targetVersion(): number // = 1 for V1.0.0 Alpha
-applyV1(db): void // creates all tables/indexes from db-schema §2-§18
```
V1 has a single target `schema_version = 1`. `migrate` is idempotent: if `schema_meta` is missing it
creates the full schema and seeds the initial keys (db-schema §2). It updates
`aircoding_version_last_opened` on every open. Destructive/non-trivial migrations follow overview §15
(plan, backup, confirm) but V1 only needs create-on-empty.
### 4.3 Repository layer
All repositories implement `Repository<TRecord, TInsert, TUpdate>` (contracts §6) or its narrow
extensions. They are thin persistence adapters: no scheduling, permission, or projection policy
(contracts §6, code-view §9). Records mirror db-schema columns exactly (`snake_case`).
Repository inventory (code-view §9) and their record types:
| Repository | Record (db-schema table) | Notable methods beyond CRUD |
|---|---|---|
| `SessionRepository` | `sessions` (§3) | `list_active()` |
| `MessageRepository` | `messages` (§4) | `list_by_session(session_id, since?)` |
| `MessageDraftRepository` | `message_drafts` (§5) | `upsert`, `delete_for_message` |
| `EventRepository` | `events` (§6) | `insert(rec, tx)`, `query(filter)` (contracts §6) |
| `TaskRepository` | `tasks` (§7) | `list_by_status`, `list_runnable_candidates` (contracts §6) |
| `TaskDependencyRepository` | `task_dependencies` (§8) | `list_for_task`, `list_dependents` |
| `TaskAttemptRepository` | `task_attempts` (§9) | `next_attempt_index(task_id)`, `list_by_task` |
| `AgentRepository` | `agents` (§10) | `list_active`, `update_heartbeat` |
| `ToolRunRepository` | `tool_runs` (§11) | `list_by_task`, `list_by_origin_message` |
| `CommandRunRepository` | `command_runs` (§12) | `list_by_task` (status derived, §4.4) |
| `ArtifactRepository` | `artifacts` (§13) | `list_by_entity`, `get_by_uri` |
| `DiagnosticRepository` | `diagnostics` (§14) | `list_by_signature`, `list_by_command_run` |
| `EvidenceRepository` | `evidence_refs` (§15) | `list_for_entity(type,id)` |
| `WorkspaceRepository` | `workspaces` (§16) | `list_by_status`, `list_gc_candidates` |
| `SummaryRepository` | `summaries` (§17) | `get`, `insert` |
| `UiStateRepository` | `ui_state` (§18) | `upsert(scope,key,value)`, `read(scope,key)` |
`SessionStore` aggregates all repositories (code-view §9):
```text
class SessionStore
+sessions: SessionRepository
+messages: MessageRepository
+message_drafts: MessageDraftRepository
+events: EventRepository
+tasks: TaskRepository
+task_dependencies: TaskDependencyRepository
+task_attempts: TaskAttemptRepository
+agents: AgentRepository
+tool_runs: ToolRunRepository
+command_runs: CommandRunRepository
+artifacts: ArtifactRepository
+diagnostics: DiagnosticRepository
+evidence: EvidenceRepository
+workspaces: WorkspaceRepository
+summaries: SummaryRepository
+ui_state: UiStateRepository
+referential_check(): Promise<OrphanReport> // FK-off invariants (§18.3)
```
### 4.4 Derived command status
`command_runs` has no physical `status` column (db-schema §12, runtime-semantics §5). The repository
exposes a pure derivation used by projection and reports:
```text
function derive_command_status(row): "running" | "ok" | "error" | "cancelled" | "unknown"
completed_at == null → "running"
cancellation metadata present → "cancelled"
exit_code === 0 → "ok"
exit_code != 0 (non-null) → "error"
otherwise → "unknown"
```
This matches `CommandRunProjection.status` (contracts §17) so projection never invents a value.
### 4.5 Enum validation
Every closed-enum TEXT column (db-schema §21, 18 rows) is validated on insert/update. `IMPL`: a
shared `assert_enum(table, column, value)` helper backed by the db-schema §21 table; on violation it
throws an `AirError` of kind `system_error` (programmer error, never user-facing).
## 5. Event Subsystem
*Applicable invariants (§18.6): INV-1 (state columns written only here, in `project()`), INV-2 (project never touches external stores), INV-5 (EventBus is transport only).*
Module: `packages/runtime/src/events/`. Classes: `EventSchemaRegistry`, `EventStore`, `EventBus`,
`EventIngestor`. All implement contracts §7 verbatim.
### 5.1 EventIngestor
The single runtime entry point for events from agents/tools/workers (runtime-semantics §2).
```text
class EventIngestor implements EventIngestor (contracts §7)
+ingest<T>(event: RuntimeEvent<T>): Promise<void>
+ingest_ephemeral<T>(event: RuntimeEvent<T>): Promise<void>
-policyFor(type): EventPersistence // durable | ephemeral, from registry
```
`ingest` flow (runtime-semantics §2):
```text
validate envelope + schema/version (EventSchemaRegistry)
→ look up persistence policy by event.type
→ durable: EventStore.append(event) // tx + projection + post-commit publish
→ ephemeral: EventBus.publish(event) // live only
```
If `ingest` receives a type whose policy is `ephemeral`, it delegates to `ingest_ephemeral`. The
ingestor never creates scheduler tasks, permission decisions, or memory promotions; those are
follow-up events emitted by owning services (runtime-semantics §2).
### 5.2 EventSchemaRegistry
```text
class EventSchemaRegistry implements EventSchemaRegistry (contracts §7)
+register(type, version, schema: JsonObject): void
+validate(type, version, payload): boolean
+list(): Array<{type, version}>
+get_schema(type, version): JsonObject | undefined
```
Seeded at startup from `event-registry-v1.md` §3 (durable) and §4 (ephemeral). Unknown
`type`+`version` fails validation → ingestion rejects with `AirError` kind `system_error`.
Payload schema change requires a new `version` (event-registry §2 rule 7).
### 5.3 EventStore
```text
class EventStore implements EventStore (contracts §7)
+append<T>(event, options?): Promise<void>
+append_many(events, options?): Promise<void>
+query(filter: EventFilter): Promise<RuntimeEvent[]>
-toRecord(event): PersistedEventRecord // route_text = route.join("/")
-project(event, tx): void // domain table update per registry map
```
`append` algorithm (runtime-semantics §3, event-registry §2):
```text
DatabaseManager.transaction(tx => {
schema validate (must already be durable policy)
EventRepository.insert(toRecord(event), tx)
project(event, tx) // domain projection from §5.4 map
})
EventBus.publish(event) // AFTER commit (contracts §7 rule 3)
```
**Error handling for `project()`**: If `project(event, tx)` throws an exception (e.g., due to FK-off
referential inconsistency, constraint violation, or programmer error), the entire transaction rolls
back. `EventBus.publish()` is never called. The exception propagates to the caller as an `AirError`
with `kind: "system_error"`. If the failure is due to FK-off inconsistency (e.g., referencing a
non-existent `tasks.id`), the error is logged to developer log and `SessionStore.referential_check()`
is triggered asynchronously to diagnose and repair orphaned references.
`route_text` is always derived `route.join("/")` (event-registry §2 rule 6); `route` itself is
append-only (rule 5) — `EventStore` never rewrites prior route entries.
### 5.4 Durable projection map
`project(event, tx)` switches on `event.type` and applies exactly the domain update fixed by
event-registry §3. To make the boundary between pure intra-transaction projection and
post-commit outbox/compensation work explicit, the map is split into two tables.
**Table A — Pure projection within `EventStore.append` transaction (no external writes):**
| Event type | Domain update (in `events_session.db` transaction) |
|---|---|
| `session.created` | insert `sessions` |
| `session.archived` / `session.deleted` | update `sessions.status` |
| `user.message.created` | insert `messages` |
| `assistant.message.started` | upsert `message_drafts` (status=streaming) |
| `assistant.message.created` | insert `messages` + delete matching `message_drafts` |
| `assistant.message.failed` | `message_drafts.status=error` or failure artifact ref |
| `agent.started` | insert `agents` row with `status = 'running'` when the event is emitted after the WorkerProcess handshake has completed (the normal case), or `status = 'starting'` when emitted before handshake acknowledgement; the event carries exactly one status and projects it once (event-registry §3: "insert row with status = running or starting"). There is **no** runtime `starting → running` UPDATE that bypasses the event log: if a row was inserted as `starting` and the agent later becomes fully ready, the next durable `agent.*` event (or a re-emitted `agent.started` for the running phase) carries the status; `agents.status` is only ever written by `agent.*` event projection (runtime-semantics §3), never by a direct `WorkerManager` write |
| `agent.completed/failed/lost/cancelled` | update `agents.status` |
| `task.created` | insert `tasks` (+ optional `task_dependencies`) |
| `task.started` | `tasks.status=running`, set started/agent/workspace; insert `task_attempts` |
| `task.completed` | `tasks.status=completed`, set worker_result_json, completed_at; update attempt |
| `task.blocked` | `tasks.status=blocked`; update attempt |
| `task.failed` | `tasks.status=failed`; update `task_attempts.failure_*` |
| `task.cancelled` | `tasks.status=cancelled` |
| `task.interrupted` | `tasks.status=interrupted` |
| `tool.started` | insert `tool_runs` (running) |
| `tool.completed` | `tool_runs.status=ok` + output/artifacts/evidence/duration |
| `tool.failed` | `tool_runs.status=error` |
| `tool.cancelled` | `tool_runs.status=cancelled` |
| `command.started` | insert `command_runs` |
| `command.completed` | update exit_code, artifact refs, diagnostics, duration |
| `command.failed` | update exit_code when available + failure artifacts |
| `artifact.created` | insert `artifacts` (after temp→rename completed before commit) |
| `diagnostic.created` | insert `diagnostics` |
| `evidence.created` | insert `evidence_refs` |
| `context.compaction.requested` | insert compaction task row if accepted |
| `context.compaction.started` | mark compaction task running |
| `context.compaction.completed` | mark compaction task complete (no summary row) |
| `context.compaction.failed` | mark compaction task failed/blocked |
| `summary.created` | insert `summaries` (only place that does) |
| `permission.decision.recorded` | append (optional future projection table) |
| `permission.prompt.requested/resolved` | append (optional UI projection) |
| `doctor.*` | append (+ optional report artifact / command rows) |
| `requirement.changed` | append (Scheduler applies impacted-task marking on consumption) |
| `architecture.plan.updated` | append + plan/artifact refs |
| `architecture.impact.completed` | append (Scheduler consumes on EventBus subscription) |
| `workspace.created` | insert `workspaces` |
| `workspace.merge.started` | append + mark merge in progress (metadata) |
| `workspace.merge.completed` | `workspaces.status=merged`, set merged_at |
| `workspace.merge.conflicted` | `workspaces.status=conflicted` |
| `workspace.cleaned` | `workspaces.status=cleaned` |
| `memory.candidate.created` | append |
| `memory.archived` | append (mark memory inactive in session-side mirror if any) |
**Table B — Projection paired with prior owner-side external write (cross-DB):**
For these events the **external write happens first** in the owning store; the durable
session event is then ingested by the owning service to record the completed cross-store
transition. `project(event, tx)` only writes session-DB rows; the cross-DB pair is
eventually consistent (§18.4, runtime-semantics §6.3-§6.4).
| Event type | Projection step (in session-DB transaction) | External write (already done by owner before event is ingested) | Owner |
|---|---|---|---|
| `memory.promoted` | append durable event (event log row); domain side has no dedicated session-DB table — `LearnedMemoryStore` is project-DB only | write `rules/`, `skills/`, or `learned-memory.db` row | `LearnedMemoryStore` / rules subsystem |
| `memory.archived` | append durable event | mark memory inactive in external store | `LearnedMemoryStore` |
| `debug.record.created` | append durable event | insert/update row in `debug-records.db` | `DebugKnowledgeStore` |
Rules:
- `project(event, tx)` never opens external DBs or files; it only writes
`events_session.db` rows (events, drafts, session domain projections).
- The owning service performs its external write **before** ingesting the durable event.
This makes the session-DB event the "we observed the external write succeeded" record.
- If the external write fails, the owning service does **not** ingest the success event.
See §18.4 for failure modes (`task.failed` carrying `AirError`, candidate re-queue).
- On restart, recovery cross-checks for upstream session rows (e.g. `memory.candidate.created`)
without a matching downstream event (`memory.promoted`) and re-queues work
(runtime-semantics §6.4).
### 5.5 EventBus
```text
class EventBus implements EventBus (contracts §7)
+publish<T>(event: RuntimeEvent<T>): void
+subscribe(filter: EventFilter, handler): Subscription
+drain?(): Promise<void>
-match(filter, event): boolean
```
Rules: live transport only, never a recovery source of truth (contracts §7 rule 4). If a handler
throws, the error is caught, logged to developer log, and does not propagate; subscription stays
active (contracts §7 rule 5). `drain` flushes pending async handlers for clean shutdown.
Ephemeral coalescing (event-registry §4): `agent.heartbeat`, `task.progress`,
`assistant.message.delta`, `tool.progress`, `command.stdout.delta`, `command.stderr.delta`,
`hud.frame.rendered` may be throttled before reaching subscribers.
## 6. Project and Session Lifecycle
Module: `packages/runtime/src/project/` and `sessions/`. Classes: `ProjectLocator`,
`ProjectInitializer`, `ProjectStore`, `SessionManager`, `SessionStore`.
### 6.1 ProjectStore
Implements contracts §8.
```text
class ProjectStore implements ProjectStore
+locate(start_path): Promise<ProjectContext | undefined>
+initialize(project_root, options?): Promise<ProjectContext>
+open(project_root): Promise<ProjectContext>
```
`locate` walks up from `start_path` looking for `.air/shared/project.json` (overview §8.1). `initialize`
creates the `.air/shared` and `.air/local` trees (overview §8.1), generates a stable `project_id`
UUID stored in `.air/shared/project.json` (not derived from path; overview §8.1), seeds default rules
when `options.default_rules`, and emits nothing durable yet (no session DB until a session opens).
`open` loads the existing `ProjectContext` (project_id, roots, schema_version).
Delegated helpers (`IMPL`, code-view §4):
- `ProjectLocator.locate(start)` — upward search.
- `ProjectInitializer.scaffold(root, options)` — directory + file creation.
### 6.2 SessionManager and SessionStore
```text
class SessionManager implements SessionManager (contracts §8)
+open_session(project, options?): Promise<SessionContext>
+close_session(session_id): Promise<void>
class SessionStore // §4.3 aggregate of repositories
```
`open_session` flow:
```text
resolve session_id (options or IdGenerator.session_id())
compute db_path = .air/local/sessions/<session-id>/session.db (db-schema header)
DatabaseManager.open(db_path); MigrationRunner.migrate(db)
SessionStore bound to this db
ingest session.created (durable → inserts sessions row)
return SessionContext { session_id, project_id, project_root, db_path, artifact_root }
```
`close_session` flushes `ui_state` (db-schema §1), publishes a terminal session event when archiving,
and releases the DB handle. Session provider/model selection (`model_provider_id`, `model_id`) is
captured at open and is immutable for the session (overview §14).
## 7. Scheduler Subsystem
*Applicable invariants (§18.6): INV-1 (Scheduler/WorkspaceManager/AgentMonitor mutate `tasks`/`agents`/`workspaces` status only by emitting events for projection — never direct UPDATE; heartbeat timestamps are the only exemption), INV-5 (rebuild queues from SQLite, not EventBus replay).*
Module: `packages/runtime/src/scheduler/`. Classes: `Scheduler`, `TaskGraph`, `WavePlanner`,
`RetryPlanner`, `WorkspaceManager`, `AgentMonitor` (code-view §4). The Scheduler is an orchestration
service, not a coding agent (scheduler-state-machine §intro). All durable state goes through
EventStore; in-memory queues rebuild from SQLite (scheduler-state-machine §1).
### 7.1 Scheduler
Implements contracts §9.
```text
class Scheduler implements Scheduler
+create_tasks(session_id, specs: TaskSpec[]): Promise<void>
+add_dependency(session_id, task_id, dependency: TaskDependencySpec): Promise<void>
+load_graph(session_id): Promise<TaskGraph>
+run_until_idle(session_id): Promise<SchedulerRunResult>
+cancel_task(task_id, reason): Promise<void>
-plan_wave(graph): SchedulerWavePlan
-dispatch(wave): Promise<void>
-collect_results(): Promise<void>
-state: SchedulerState // §20.2 lifecycle
```
`create_tasks` ingests `task.created` per spec (durable → inserts `tasks` + dependency rows).
`run_until_idle` drives the lifecycle in §20.2 until a terminal graph state, returning
`SchedulerRunResult` (contracts §9). The Scheduler asks the user only through Main Agent /
PermissionEngine (scheduler-state-machine §8); it never prompts directly.
### 7.2 TaskGraph
```text
class TaskGraph
+session_id: SessionID
+tasks: Map<TaskID, TaskNode>
+dependencies: TaskDependencyRecord[]
+get_runnable_tasks(): TaskNode[] // hard deps satisfied, not conflicting
+mark_terminal(task_id, status): void
+dependents_of(task_id): TaskNode[]
+validate_refs(): OrphanReport // FK-off (§18.3)
```
`get_runnable_tasks` honors dependency semantics (scheduler-state-machine §4 PLANNING_WAVE): hard
deps must be completed; soft deps affect priority only; conflict/serialization deps block concurrent
dispatch on overlapping write areas.
### 7.3 WavePlanner
```text
class WavePlanner
+plan(graph, resources: ResourceSnapshot): SchedulerWavePlan
-serialize_write_conflicts(candidates): {runnable, serialized}
-assign_workspace(task): WorkspacePlan
-assign_model(task): ModelAssignment
```
Produces `SchedulerWavePlan` (contracts §9). Rules from scheduler-state-machine §4:
different write areas → concurrent; same area uncertain conflict → serialize; reviewers read-only and
concurrent except against unstable unmerged outputs; debugger serializes on same failure surface;
machine resources cap concurrency (§7 resource-aware). Inferred `conflict`/`serialization` edges are
persisted via `task_dependencies` + durable events so restart need not rediscover them
(scheduler-state-machine §1).
### 7.4 RetryPlanner
```text
class RetryPlanner
+decide(task: TaskNode, attempts: TaskAttemptRecord[], error: AirError): RetryDecision
```
`RetryDecision` (scheduler-state-machine §5):
`action ∈ {retry, retry_serial, debug, skip, block, cancel}`. Rules: retry only when plausibly
recoverable; later retries change one dimension (model/context/command/serialization/debugger);
identical `failure_signature` escalates faster; environment impossibility → `block`, never infinite
retry; architecture/interface mismatch → route to Architecture Designer; fallback skip only when
`VerificationPolicy.fallback_allowed` and task non-critical. Budget = `TaskConstraints.retry_budget`.
### 7.5 WorkspaceManager
```text
class WorkspaceManager
+create_workspace(plan: WorkspacePlan): Promise<WorkspaceRef>
+merge_workspace(workspace_id): Promise<MergeResult>
+cleanup_workspace(workspace_id): Promise<void>
```
Strategies (db-schema §16, scheduler-state-machine §MERGING): `main` (no merge), `worktree`
(git merge/patch), `isolated_copy` (copy-back/patch). Emits `workspace.created`,
`workspace.merge.started`, and a terminal `workspace.merge.completed` or `workspace.merge.conflicted`.
GC retention follows overview §15 (active until merge/cancel; merged 7d; abandoned 3d; cleaned keeps
DB row).
**Responsibility split — Scheduler vs. WorkspaceManager** (overview §10.3, scheduler-state-machine §4/§MERGING):
| Responsibility | Owner |
|---|---|
| Decide *whether* a task needs an isolated workspace | Scheduler (via `WavePlanner.assign_workspace`) |
| Decide *which strategy* (`main`/`worktree`/`isolated_copy`) | Scheduler (via `WavePlanner.assign_workspace`) |
| Encode the decision into a `WorkspacePlan` | Scheduler (in `SchedulerWavePlan`) |
| Materialize the workspace on disk (clone/copy/setup) | WorkspaceManager (`create_workspace`) |
| Apply merge/patch back to project root | WorkspaceManager (`merge_workspace`) |
| Detect/report merge conflicts | WorkspaceManager (emits `workspace.merge.conflicted`) |
| Decide *how* to resolve a conflict | Scheduler (consumes conflicted event; may re-plan or escalate to Main Agent) |
| Cleanup/GC of abandoned/cleaned workspaces | WorkspaceManager (`cleanup_workspace`, driven by Scheduler heartbeat / Doctor) |
| Persist workspace lifecycle events | WorkspaceManager (durable events) → EventStore projection updates `workspaces` row |
WorkspaceManager is a pure executor of Scheduler decisions: it never plans concurrency, never decides
which task occupies which workspace, and never re-plans on conflict. The Scheduler is the sole policy
owner; WorkspaceManager is the sole mechanism owner.
### 7.6 AgentMonitor
```text
class AgentMonitor
+record_heartbeat(event): void // coalesced into agents.last_heartbeat_at + tasks.heartbeat_at
+detect_lost_agents(): Promise<AgentLost[]>
+enforce_timeouts(): Promise<void>
```
Heartbeat coalescing interval default 5s or meaningful status change (runtime-semantics §4).
Missing heartbeat past threshold → inspect process → alive-but-silent → status ping/soft cancel;
gone-without-result → emit `agent.lost` (scheduler-state-machine §MONITORING). Soft timeout asks for
checkpoint and may extend; hard timeout cancels/kills and marks task failed/interrupted
(scheduler-state-machine §MONITORING timeout table).
## 8. Worker and IPC Subsystem
*Applicable invariants (§18.6): INV-1 (WorkerManager never writes `agents.status` directly — `agent.started` projection sets it; `worker.ready` handshake is a live signal, not a status write), INV-3 (workers reach fs/shell/network/SQLite only through parent-mediated tool IPC).*
Module: `packages/runtime/src/workers/`. Classes: `WorkerManager`, `WorkerProcess`,
`WorkerProtocol`, and roles under `roles/`. IPC is NDJSON over stdio (contracts §10, ADR-0005).
### 8.1 WorkerManager and WorkerProcess
```text
class WorkerManager
+spawn(task_spec: TaskSpec, context_pack: ContextPack): Promise<WorkerProcess>
+cancel(agent_id, reason): Promise<void>
class WorkerProcess
+agent_id: AgentID
+pid?: number
+send(envelope: IpcEnvelope): void
+on_message(handler: (m: IpcEnvelope) => void): void
```
`spawn` starts a Bun child process, then performs the handshake (§8.2). `WorkerProcess` owns the
NDJSON pipe; stdout carries protocol only, stderr is fatal/logging (baselineV1 §8).
Worker exit codes (baselineV1 §8, authoritative):
| Code | Meaning |
|---:|---|
| 0 | protocol-level completion (including task failed/blocked via WorkerResult) |
| 1 | uncaught exception |
| 2 | startup/protocol error |
| 3 | permission error |
| 4 | parent cancelled |
| 5 | hard timeout killed |
**Design decision**: Task success/failure is communicated through `WorkerResult.status`, not exit
codes. Exit code 0 means the worker completed the IPC protocol correctly and returned a valid
`WorkerResult`; the actual task outcome (`completed`/`failed`/`blocked`/`cancelled`) is in the
result payload. Non-zero exit codes indicate process-level or protocol-level failures that prevent
normal result delivery.
### 8.2 WorkerProtocol and handshake
```text
class WorkerProtocol
+encode(msg: IpcMessage): string // NDJSON line
+decode(line: string): IpcMessage
+validate_direction(msg): void // parent_to_worker vs worker_to_parent
+check_protocol_version(v: number): boolean
```
Handshake (contracts §10): parent spawns → parent sends `agent.start` control (with `TaskSpec`,
`ContextPack`, `AgentRuntimeContext`) → worker replies `worker.ready { protocol_version, worker_version }`
→ parent validates `protocol_version`; mismatch terminates worker with `protocol.error`.
Direction typing (contracts §10): parent→worker = `control`, `tool.result`, `tool.stream`;
worker→parent = `event`, `log`, `tool.call`, `worker.result`, `worker.checkpoint`, `protocol.error`.
`validate_direction` rejects messages on the wrong channel.
### 8.3 Worker roles and WorkerRuntime
Roles implement `WorkerRole<TResult>` (contracts §10). The in-worker `WorkerRuntime` is the only
side-effect surface (contracts §10, §23: workers never write SQLite or touch fs/shell/network except
through parent-mediated tools).
```text
interface WorkerRole<TResult> { run(task_spec, context_pack, runtime): Promise<WorkerResult<TResult>> }
class WorkerRuntime
+emit(event: RuntimeEvent): Promise<void> // → IPC event → parent EventIngestor
+call_tool<I,O>(name, input): Promise<ToolResultEnvelope<O>> // → IPC tool.call
+checkpoint(data): Promise<void> // → IPC worker.checkpoint
```
Role inventory and constraints (code-view §10):
| Role class | `output_contract` | Write access | Required result |
|---|---|---|---|
| `ExecutorRole` | `ExecutorResult` | scoped project writes | ExecutorResult in WorkerResult |
| `ReviewerRole` | `ReviewerResult` | read-only | ReviewerResult |
| `DebuggerRole` | `DebuggerResult` | scoped writes only when assigned | DebuggerResult |
| `CompactorRole` | `CompactorResult` | summaries/artifacts only | CompactorResult |
| `ExperienceMinerRole` | `ExperienceMinerResult` | candidates/rules/skills only when assigned | ExperienceMinerResult |
Each role's `run` ends by returning a `WorkerResult<TResult>` with the matching `agent_type`
(contracts §11). `status ∈ {completed, failed, blocked, cancelled}`; a code-changing result cannot be
`completed` unless verification passed or was explicitly skipped with evidence/risk and
`fallback_allowed` (runtime-semantics §9.4, overview §10.9). Self-escalation returns `blocked` with a
`BlockerReport` rather than improvising (scope-escalation §9).
**TaskType → WorkerRole mapping** (contracts §9 `TaskType`, db-schema §`tasks.type` closed enum):
| TaskType | Owning WorkerRole | Worker `agent_type` | Output contract | Notes |
|---|---|---|---|---|
| `execute` | `ExecutorRole` | `executor` | `ExecutorResult` | Code-changing work; scoped project writes |
| `review` | `ReviewerRole` | `reviewer` | `ReviewerResult` | Read-only audit/review |
| `debug` | `DebuggerRole` | `debugger` | `DebuggerResult` | Scoped writes when assigned |
| `compact` | `CompactorRole` | `compactor` | `CompactorResult` | Summary/artifact writes only |
| `mine_experience`| `ExperienceMinerRole` | `experience_miner` | `ExperienceMinerResult` | Candidate/rule/skill writes when assigned |
| `docs` | `ExecutorRole` | `executor` | `ExecutorResult` | Documentation-only task; `TaskScope.write_area` restricted to docs paths (overview §10.9); same events as `execute`: `task.created/started/completed`, `tool.*` for fs writes, `artifact.created` for doc artifacts |
**Design decision — `docs` task type closure**: `docs` is a formal `TaskType` (contracts §9,
db-schema §`tasks.type`). It is *not* a separate WorkerRole; it reuses `ExecutorRole` with a
documentation-scoped `TaskScope.write_area` (typically `docs/`, `AirPlan/docs/`, README, ADR/C4
folders) and an empty or docs-only `VerificationPolicy.commands`. All persistent events for a
`docs` task are identical to an `execute` task: `task.created``task.started` (with
`agent_type='executor'`) → `tool.*` for file edits → `artifact.created` for documentation
artifacts → `task.completed` with `ExecutorResult`. The `tasks.type='docs'` field is the only
domain-level differentiator, used by Scheduler for routing/policy (e.g. lighter verification,
no test runs) and by Architecture Designer to detect doc-only changes that may still trigger
the architecture gate when they touch ADR/C4 files.
### 8.4 Execution discipline enforcement (Executor/Debugger)
Read-before-edit and exact-edit are enforced at the tool layer, not just by prompt (§9.4); the role
loop additionally:
1. records a read observation before `fs.edit`/`fs.patch` (runtime-semantics §9.1);
2. keeps changes within `TaskScope.write_area`/`allowed_paths` (overview §10.9 rule 3);
3. runs `VerificationPolicy.commands` before declaring `completed` (contracts §9, §11);
4. attaches evidence refs for diffs/builds/tests (overview §10.9 rule 6).
## 9. Tool, Permission, and Capability Subsystem
*Applicable invariants (§18.6): INV-3 (every side effect goes through `ToolRegistry.call` → `PermissionEngine.evaluate` first), INV-4 (capabilities install dependencies only through Doctor).*
Module: `packages/runtime/src/tools/`, `security/`, `capabilities/`.
### 9.1 ToolRegistry
Implements contracts §12.
```text
class ToolRegistry implements ToolRegistry
+register<I,O>(definition: ToolDefinition<I,O>, executor: ToolExecutor<I,O>): void
+register_streaming<I,O>(definition, executor: StreamingToolExecutor<I,O>): void
+call<I,O>(name, input, context: ToolExecutionContext): Promise<ToolResultEnvelope<O>>
+call_streaming<I,O>(name, input, context): AsyncIterable<ToolEvent | ToolResultEnvelope<O>>
+list(): ToolDefinition[]
-validate_input(def, input): void // JsonSchema check
```
`call` algorithm (contracts §12, §23 "tool → side effect without PermissionEngine" forbidden):
```text
look up definition+executor (else AirError tool_error)
validate input against input_schema
build PermissionRequestContext from context + definition.permissions + input paths/command
decision = PermissionEngine.evaluate(request)
branch on decision.action (§9.3)
emit tool.started (durable → tool_runs running)
run executor.execute(input, context) // streaming consumed internally for call()
emit tool.completed | tool.failed | tool.cancelled
return ToolResultEnvelope
```
`call_streaming` exposes `ToolEvent` progress and ends with exactly one final `ToolResultEnvelope`
(contracts §12 streaming rule). `BuiltInToolRegistrar` registers fs/shell/git/project/artifact/
context/permission/doctor tools (code-view §4); each tool declares `category`, `permissions`, and
`streaming` (contracts §12).
### 9.2 PermissionEngine
Implements contracts §13.
```text
class PermissionEngine implements PermissionEngine
+evaluate(context: PermissionRequestContext): Promise<PermissionDecision>
+record(decision, context): Promise<PermissionRecordResult>
class PathClassifier
+classify(path, project_root): PathRiskClassification // 8 categories, realpath-normalized
class CommandRiskAnalyzer
+analyze(command, cwd): CommandRiskAnalysis // 10 categories
class SecretRedactor
+redact(text): string // for logs/evidence
```
`evaluate` applies the frozen layered order (contracts §13, runtime-semantics §8, overview §12):
```text
1 tool capability declaration
2 permission profile (permission_template)
3 TaskSpec scope allowed/denied paths
4 path/command/network risk classification (PathClassifier + CommandRiskAnalyzer)
5 credential/system-sensitive override
6 user prompt workflow if required
```
Key invariants: project-level allow never overrides task scope; credential/system-sensitive overrides
broad allows (runtime-semantics §8, overview §12 rules 8-9); paths normalized via realpath before
prefix checks (overview §12 rule 1); `.git/` internals protected (rule 2). `record` writes a
`permission.decision.recorded` durable event and returns `PermissionRecordResult`; on write failure it
returns `{ok:false, error}` (contracts §13).
### 9.3 PermissionDecision branching
`ToolRegistry.call` branches on `PermissionDecision.action` (contracts §13, scope-escalation §7):
| action | ToolRegistry behavior |
|---|---|
| `allow` | execute; create backup first if `backup_required` |
| `announce_then_run` | emit visible notice, then execute unless interrupted; bounded by `grant_scope` |
| `ask_user` | suspend; emit `permission.prompt.requested`; resume on `permission.prompt.resolved` |
| `deny` | do not execute; return `ToolResultEnvelope{status:"error"}`; caller may pick safe path |
| `block` | return blocked outcome → task.blocked upstream |
| `refuse` | return `AirError{kind:"policy_error"}`; no execution |
### 9.4 Filesystem tool execution rules
`fs.edit`/`fs.patch` enforce read-before-edit (runtime-semantics §9.1-§9.3, overview §10.9):
- require an active task read observation for the target file or explicit `expected_existing_sha256`;
- `old_string` exact match; non-unique match fails unless `replace_all`; no indentation guessing;
- patch paths must be within scope; rejected hunks become artifacts; partial apply only if unchanged
rejected paths are provably untouched, else atomic fail;
- successful edit/patch emits a diff artifact (→ `artifact.created`).
### 9.5 CapabilityRegistry
Implements contracts §18; lifecycle/trust from capability-trust-v1.
```text
class CapabilityRegistry implements CapabilityRegistry
+discover(): Promise<CapabilityManifestV1[]>
+validate(manifest): Promise<ValidationResult>
+enable(capability_id): Promise<void>
+disable(capability_id): Promise<void>
+register_tools(tool_registry: ToolRegistry): Promise<void>
class CapabilityManifestValidator
+validate(manifest): ValidationResult // schema_version=1, tool schemas, permissions
```
Lifecycle (overview §6, capability-trust §7): discovered → validated → doctor_checked → enabled →
registered → active → disabled|failed|updated. Trust levels (contracts §18): built_in, project_local,
user_installed, verified_publisher, untrusted. Trust affects default enablement/prompt posture but
never bypasses ToolRegistry or PermissionEngine (overview §6). Dependency installs go only through
Doctor (contracts §23 "capability → dependency install outside Doctor" forbidden).
## 10. Context, Prompt, and Compaction Subsystem
Module: `packages/runtime/src/context/`. Classes: `ContextAssembler`, `PromptLayerLoader`,
`CompactionPolicy` (contracts §16).
### 10.1 ContextAssembler
```text
class ContextAssembler implements ContextAssembler
+assemble(input: ContextAssembleInput): Promise<AssembledContext>
-load_layers(purpose): Promise<PromptLayer[]>
-fit_budget(layers, budget): BudgetFitResult
```
`assemble` builds an Anthropic-canonical context (`AssembledContext.canonical_format="anthropic"`,
contracts §16). It loads ordered layers (§10.2), fits them to `token_budget` via `fit_budget`, and
reports `omissions`. When the context is too large for inline return it writes a messages artifact and
sets `messages_artifact_id`; `ContextPack.assembled_context_ref` points to it (contracts §16). If the
budget cannot fit required layers it sets `compaction_requested=true` (consumed by Scheduler, §10.3).
### 10.2 PromptLayerLoader and layer order
```text
class PromptLayerLoader implements PromptLayerLoader
+load_runtime_invariant(): PromptLayer
+load_role(role: AgentType): PromptLayer
+load_project_rules(project: ProjectContext): PromptLayer[]
+load_task_context(spec: TaskSpec, refs: TaskContextRefs): PromptLayer[]
```
`PromptLayer.level` is the frozen `PromptLayerLevel` union (contracts §16), ordered L0-L9 per
prompt-layering-v1 §2 and overview §13:
| Level enum (contracts) | L# (prompt-layering) | Loader method / source |
|---|---|---|
| `runtime_invariant` | L0 Runtime invariant | `load_runtime_invariant()` |
| `role` | L1 Role / agent mode | `load_role(role)` |
| `safety` | L2 Safety and permission policy | `ContextAssembler` internal (see below) |
| `project_rules` | L3 Project rules and user preferences | `load_project_rules(project)` |
| `architecture` | L4 Architecture baseline and current plan | `ContextAssembler` internal (see below) |
| `task_spec` | L5 Task specification and acceptance criteria | `load_task_context(spec, refs)` |
| `evidence` | L6 Relevant code / artifacts / evidence | `ContextAssembler` internal (see below) |
| `conversation` | L7 Recent conversation and decision context | `ContextAssembler` internal (see below) |
| `tool_output` | L8 Tool result history / diagnostics | `ContextAssembler` internal (see below) |
| `user_override` | L9 Immediate instruction | `ContextAssembler` internal (see below) |
| `system_debug` | (system debug directive, applied within L9 when present) | `ContextAssembler` internal |
**Design decision — Layer loading responsibility**:
`PromptLayerLoader` interface (contracts §16) provides 4 methods for layers that require external
configuration or resource loading (L0, L1, L3, L5). The remaining layers are assembled by
`ContextAssembler.load_layers()` internally:
| Layer | Source | Assembled by |
|---|---|---|
| L2 Safety | Active permission profile (`~/.air/permissions.yaml` + project permission config; same source the `PermissionEngine` implementation reads internally — no new contract method) | `ContextAssembler` |
| L4 Architecture | `TaskSpec.context_refs.arc_ref` → load from plan/ADR/C4 docs | `ContextAssembler` |
| L6 Evidence | `TaskSpec.context_refs.artifacts` + `EvidenceStore.list_for_entity("task", task_id)` | `ContextAssembler` |
| L7 Conversation | `SessionStore.messages.list_by_session()` (recent N messages) | `ContextAssembler` |
| L8 Tool output | `SessionStore.tool_runs` + `command_runs` for current task | `ContextAssembler` |
| L9 Immediate | `TaskSpec.description` + `acceptance_criteria` + immediate user instruction | `ContextAssembler` |
This design keeps `PromptLayerLoader` focused on resource-backed layers while `ContextAssembler`
owns the assembly logic for session/task-specific layers.
**Design decision — Runtime roles (main/architecture/scheduler)**:
`AgentType` (contracts §5) only covers worker roles (`executor`/`reviewer`/`debugger`/`compactor`/
`experience_miner`). Runtime roles (`main`/`architecture`/`scheduler`) are not child processes and
do not use `PromptLayerLoader.load_role()`. Instead:
- `MainAgent` loads its L1 role prompt from built-in resources directly
- `ArchitectureDesigner` loads its L1 role prompt from built-in resources directly
- `Scheduler` does not use LLM prompts (it is a pure orchestration service)
Higher-priority layers win on budget pressure; `immutable=true` layers (L0, L1, L2) are never dropped
(prompt-layering L0 `Mutable: no`).
### 10.3 CompactionPolicy
```text
class CompactionPolicy implements CompactionPolicy
+should_compact(messages, token_budget): boolean
+compact(messages, target_tokens): Promise<CompactionResult>
```
`ContextAssembler` may request compaction but does not compact itself; Scheduler creates a `compact`
task that runs `CompactorRole` (overview §13, runtime-semantics §7). Sequence:
`context.compaction.requested → context.compaction.started → summary.created → context.compaction.completed`.
Only `summary.created` inserts the `summaries` row (runtime-semantics §7); the completion event
references the `summary_id` and never duplicates the row. Original messages are preserved for
backtracking.
## 11. Artifact, Evidence, and Knowledge Subsystem
*Applicable invariants (§18.6): INV-2 (DebugKnowledgeStore / LearnedMemoryStore are single writers of their project DBs and follow the outbox model — external write first, then one completion event), INV-1 (session-side markers like `artifact.created` write session rows only via projection).*
Module: `packages/runtime/src/artifacts/` and `knowledge/`.
### 11.1 ArtifactStore
Implements contracts §14; naming from artifact-naming-v1.
```text
class ArtifactStore implements ArtifactStore
+create(input: ArtifactCreateInput, context: ArtifactContext): Promise<ArtifactRef>
+get(artifact_id): Promise<ArtifactRef | undefined>
+read(artifact_id): Promise<ArtifactReadResult>
-write_temp_then_rename(bytes): {path, sha256, size}
```
`create` algorithm (runtime-semantics §6.2, overview §8.4):
```text
write temp file → compute sha256 + size → atomic rename to artifact path
artifact_id = art_<ulid>
uri = artifact://project/<project-id>/session/<session-id>/<artifact-id>
filename = <timestamp>-<artifact-id>-<slug><extension>
ingest artifact.created (durable → inserts artifacts row)
```
If the DB insert fails after rename, startup recovery scans orphaned files and registers or
quarantines them (runtime-semantics §6.2). Artifact `type` is from the closed set (db-schema §21:
log, diff, screenshot, pcap, report, diagnostic, bundle, other).
### 11.2 EvidenceStore
```text
class EvidenceStore implements EvidenceStore
+create(input: EvidenceCreateInput): Promise<EvidenceRef>
+list_for_entity(entity_type, entity_id): Promise<EvidenceRef[]>
```
Emits `evidence.created` (durable → `evidence_refs`). Worker results embed full `EvidenceRef[]` when
evidence is part of the conclusion; lightweight payloads carry `evidence_ref_ids` (contracts §14).
`kind` is from the closed set (db-schema §21: build_output, test_output, log, screenshot, diff,
metric, other).
### 11.3 DebugKnowledgeStore and LearnedMemoryStore
Project-level DBs (db-schema §20), implement contracts §20.
```text
class DebugKnowledgeStore implements DebugKnowledgeStore // debug-records.db
+insert(record: DebugRecord): Promise<void>
+lookup_by_signature(failure_signature): Promise<DebugRecord[]>
+lookup_by_task(task_id): Promise<DebugRecord[]>
+update(debug_record_id, patch): Promise<void>
class LearnedMemoryStore implements LearnedMemoryStore // learned-memory.db
+insert(memory: LearnedMemory): Promise<void>
+lookup_by_type(memory_type): Promise<LearnedMemory[]>
+update_status(memory_id, status): Promise<void>
+scan_stale(): Promise<LearnedMemory[]>
```
Writes to these DBs are cross-store side effects: the session DB records intent/completion events and
the owning store performs the external write (outbox model, §18.4; runtime-semantics §6.3-§6.4).
`debug.record.created` and `memory.promoted` are the session-side durable markers.
## 12. Provider (LLM) Subsystem
Module: `packages/llm/src/`. Classes per code-view §5. `packages/llm` owns adapters, model config,
conversion, and `ProviderManager`; `runtime` calls it only through the facade (code-view §2 rule 5).
### 12.1 ProviderManager
Implements contracts §15.
```text
class ProviderManager implements ProviderManager
+load_config(): Promise<void>
+select_model(requirement: ModelRequirement): Promise<ModelAssignment>
+complete(input: ProviderCompletionInput): AsyncIterable<ProviderStreamEvent>
-adapter_for(provider_id): ProviderAdapter
```
`select_model` matches a `ModelRequirement` against the capability matrix and returns a
`ModelAssignment` (contracts §9/§15). `complete` routes to the right adapter and yields normalized
`ProviderStreamEvent`s. Runtime/session provider+model are fixed for the session; there is no runtime
switching API (overview §14).
### 12.2 ProviderAdapter implementations
```text
interface ProviderAdapter (contracts §15)
provider_id; list_models(); validate_model(model_id); complete(input); count_tokens?(input)
class AnthropicAdapter implements ProviderAdapter
class OpenAICompatibleAdapter implements ProviderAdapter
-converter: AnthropicCanonicalConverter
class AnthropicCanonicalConverter // canonical ↔ provider format
class ToolUseConverter
class StreamNormalizer // provider stream → ProviderStreamEvent
```
Adapters convert external formats to/from the Anthropic canonical internal format and must not
silently drop semantic prompt/tool information (contracts §23 "provider adapter → silent semantic
prompt loss" forbidden; overview §14). `ModelConfigLoader` loads global `~/.air/models.yaml` and
project config; `CapabilityMatrixRegistry` holds `ProviderCapabilityMatrix` rows
(provider-capability-matrix-v1). LLM output is never allowed to perform direct file/shell side effects
(contracts §23).
## 13. Projection and TUI Subsystem
Modules: `packages/runtime/src/projection/` (ProjectionStore) and `packages/tui/src/`.
### 13.1 ProjectionStore
Implements contracts §17.
```text
class ProjectionStore implements ProjectionStore
+hydrate(session_id): Promise<void>
+apply(event: RuntimeEvent): void
+snapshot(): ProjectionSnapshot
+subscribe(handler): Subscription
-projections: { session, tasks, agents, tool_runs, command_runs, artifacts, permission_prompts, blockers }
```
`hydrate` rebuilds from DB via repositories (code-view §4: `ProjectionStore → SessionStore`). `apply`
handles all durable events plus key ephemeral events (`agent.heartbeat`, `task.progress`,
`assistant.message.delta`, `tool.progress`, `command.stdout.delta`, `command.stderr.delta`); unknown
event types are ignored (contracts §17 comment). `command_runs` projection status uses the derivation
in §4.4. ProjectionStore is never a scheduling/recovery source of truth (overview §9.3).
### 13.2 TUI
Module `packages/tui/src/` (code-view §7). `TuiApp` consumes a `ProjectionClient` and renders
components; it imports only `packages/contracts` (contracts §17, §23; code-view §2 rule 3).
```text
class TuiApp
+start(): void
+stop(): void
class ProjectionClient implements ProjectionClient
+snapshot(): ProjectionSnapshot
+subscribe(handler): Subscription
components: SessionView, TaskListView, AgentStatusView, ToolRunView, DiffView, EvidenceView,
PermissionPrompt, BlockerReport, HudView
```
Rules (code-view §7): components render projections only; permission prompts emit user decisions
through the narrow `UiCommandChannel` (contracts §17), never private runtime services; UI never
mutates domain tables; diff/evidence views link back to artifact/evidence refs. V1 transport is
in-process (contracts §17): `ProjectionClient` is a direct interface reference, not IPC. HUD presets
Full/Essential/Minimal and permission `announce_then_run` visualization per overview §14.
## 14. Agents Subsystem
Module: `packages/runtime/src/agents/main/` and `architecture/`. These run inside the runtime process
(overview §7). Main Agent stays idle-ready; background work is dispatched to workers via Scheduler
(main-agent-state-machine §Idle Principle).
### 14.1 MainAgent
```text
class MainAgent
+handle_user_message(message): Promise<void>
+present_progress(): Promise<void>
+present_blocker(blocker: BlockerReport): Promise<void>
-classify_intent(message): "chat" | "task" | "direct"
-state: MainAgentState // §20.1
```
Lifecycle is the frozen state machine (main-agent-state-machine.md, §20.1 here):
IDLE → CLASSIFYING → ANSWERING | DELEGATING | DIRECT_MODE; DELEGATING → SCHEDULING |
ARCHITECTURE_DESIGNING → CONFIRMING → EXECUTING → (INTERRUPTING | ARCHITECTURE_REVISING) →
SUMMARIZING → IDLE. Direct mode uses `permission_template="main_direct"` and writes only to the main
workspace (contracts §10, runtime-semantics §16). Requirement changes emit `requirement.changed`
(main-agent-state-machine events table). Confirmation gating: implementation-only silent → EXECUTING;
architecture-level → Architecture Designer assessment → low-permission user confirm / high-permission
auto-proceed (main-agent-state-machine §Confirmation Gating).
### 14.2 ArchitectureDesigner
```text
class ArchitectureDesigner
+assess_impact(change): Promise<ArchitectureImpact>
+update_architecture_docs(update): Promise<DocumentUpdate>
```
Emits `architecture.impact.completed` and `architecture.plan.updated` (event-registry §3, overview
§10.7). It owns the architecture review gate: interface/schema/event/package-boundary/security/
runtime-semantics/ADR/C4/plan consistency; it does not replace Reviewer (overview §10.7). Gate trigger
conditions and result rules (`silent_continue` / `requires_user_confirmation` / `requires_replan` /
`reject_or_escalate`) follow scope-escalation §4 and overview §10.7. It calls the LLM only through the
`ProviderManager` facade (code-view §4) and routes escalations through Main Agent.
## 15. Toolchain C++ Subsystem
Module: `packages/toolchain-cpp/src/` (code-view §6). Exposes `cpp.*` tools through capability
registration, not direct runtime coupling (code-view §2 rule 4).
```text
class CppToolRegistrar { +register(tool_registry): void }
class CppProjectDetector { +detect(project_root): Promise<CppDetectOutput> }
class CMakeConfigurator { +configure(input): Promise<CppCmakeConfigureOutput> }
class CppBuilder { +build(input): Promise<CppBuildOutput> }
class CppTestRunner { +run_tests(input): Promise<CppTestOutput> }
class CppcheckRunner { +run(input): Promise<CppcheckOutput> }
class ClangdClient { +query(input): Promise<ClangdQueryOutput> }
class DiagnosticParser {
+parse_compiler_output(output): Diagnostic[]
+semantic_signature(diagnostic): string
}
```
Workflow (overview §10.8): detect → configure (CMake+Ninja preferred, Make fallback) → build → parse
diagnostics → test → cppcheck → clangd query when needed → Debugger on failure → scoped fix → Reviewer
→ architecture gate if contracts/schema/events/boundaries changed. `DiagnosticParser` performs
deterministic extraction and `semantic_signature` only; LLM interpretation lives in runtime
Debugger/Reviewer, never inside `toolchain-cpp` (runtime-semantics §12). `Diagnostic` matches
contracts §21; `compile_commands.json` is generated/located when clangd/static analysis needs it
(overview §10.8).
## 16. Doctor, Logging, Migration, and Recovery Subsystem
Module: `packages/runtime/src/doctor/`, `logging/`, plus `MigrationRunner` (§4.2) and recovery in
SessionManager/Scheduler.
### 16.1 DoctorService
Implements contracts §19.
```text
class DoctorService implements DoctorService
+run(input: DoctorRunInput): Promise<DoctorRunOutput>
-check_capability(capability): DoctorIssue[] // internal helper, not a public contract method
-self_bootstrap(): DoctorIssue[] // Bun, SQLite, shell, .air writability
```
Self-bootstrap before any capability check (runtime-semantics §18, overview §15): verify Bun runtime,
SQLite, basic shell, `.air/` writability; on failure report a blocking issue and skip remaining
checks. Modes (contracts §19): `read_only`, `fix` (under PermissionEngine), and `bundle` export (local
artifact, no auto-upload, overview §15). Emits `doctor.*` events (event-registry §3).
### 16.2 Logging
```text
class Logger implements Logger { debug/info/warn/error(message, data?) }
class DeveloperLogEncryptor implements DeveloperLogEncryptor { encrypt_log_chunk(chunk): Promise<Uint8Array> }
class SecretRedactor { redact(text): string } // shared with PermissionEngine §9.2
```
`air.log` is user-facing with redacted operational errors; `air.developer.log` is encrypted and more
detailed (overview §15, baselineV1 §23). All logs redact secrets/auth refs/provider keys via
`SecretRedactor`. Failures link log artifacts through evidence refs rather than copying sensitive
content into user summaries.
### 16.3 Recovery
On startup/resume (scheduler-state-machine §9, overview §15, runtime-semantics §6.2/§14):
1. load `tasks` with status `running`/`interrupted`, active `agents`/`workspaces`;
2. check process liveness by PID; reconnect IPC if alive, else emit `agent.lost`;
3. mark task failed/interrupted by resumability;
4. preserve workspaces until merge/cleanup decision;
5. orphan-artifact scan registers or quarantines files;
6. FK-off orphan scan (§18.3) logs and re-parents/archives dangling references;
7. workspace GC applies retention (overview §15);
8. rebuild scheduler queue from pending/failed-with-retry tasks.
## 17. CLI Subsystem
Module: `packages/cli/src/` (code-view §8). `CliEntrypoint.main(argv)` routes to command classes; a
`RuntimeFactory` builds the `RuntimeApp`.
```text
class CliEntrypoint { +main(argv): Promise<number> }
class RuntimeFactory { +create(options): Promise<RuntimeApp> }
```
Command class inventory (frozen mapping, code-view §8, overview §14):
| Command class | Subcommand(s) | Notes |
|---|---|---|
| `RunCommand` | `run [project]` | Default entry; spawns RuntimeApp + TUI |
| `InitCommand` | `init` | First-run wizard, project bootstrap |
| `DoctorCommand` | `doctor [--fix|--bundle]` | Wraps `DoctorService` (§16.1) |
| `ProviderCommand` | `provider list`, `provider current` | Read-only; no runtime model switch |
| `E2ECommand` | `e2e` | Local end-to-end harness, no CI hooks |
| `ReleaseCommand` | `release` | Build/package release artifacts |
| `ResumeCommand` | `resume [--session SID]` | Catalog: resume last/specific session |
| `CompactCommand` | `compact [--session SID]` | Catalog: trigger compaction task |
| `HistoryCommand` | `history [--session SID]` | Catalog: render past sessions/events |
| `SessionListCommand` | `session list` | Catalog: enumerate sessions |
| `RestoreCommand` | `restore <file|time|session> ...` | Catalog: git-backed backup restore (overview §15, runtime-semantics §19) |
`ProviderCommand` is read-only (`provider list`/`current`); there is no runtime provider/model switch
command (overview §14). `RestoreCommand` supports file/time/session granularities over the git-backed
backup repo (overview §15, runtime-semantics §19). All CLI command classes route side effects through
`RuntimeApp` services (Scheduler, SessionManager, DoctorService, ArtifactStore) and never bypass
`ToolRegistry` or `PermissionEngine`.
## 18. Cross-Cutting Designs
### 18.1 AirError construction
All failures use `AirError` (contracts §3, error-taxonomy-v1). Construction helper:
```text
function make_air_error(opts: {
kind: ErrorKind,
severity?: ErrorSeverity, // default "error"
message: string,
detail?: string,
retryability?: Retryability, // default "unknown"
semantic_signature?: string, // default derived from kind+message hash
cause_ref?: EntityRef,
cause_refs?: EntityRef[],
user_action?: string,
metadata?: JsonObject
}): AirError
```
`semantic_signature` is the stable grouping key for repeated failure detection, debug knowledge
lookup, and Scheduler retry/debug/escalation routing (error-taxonomy §3, overview §9.2). When not
provided, derive from `kind + normalized_message_hash`. Scheduler decisions use kind, retryability,
severity, task scope, permission result, architecture impact, verification evidence, and repetition
count (overview §9.2).
### 18.2 Transaction discipline
All durable event + domain update pairs run inside one `DatabaseManager.transaction` call
(runtime-semantics §3, db-schema §1). Pseudo-code:
```text
await db.transaction(async tx => {
await event_repo.insert(to_record(event), tx)
await domain_repo.insert_or_update(domain_row, tx)
})
event_bus.publish(event) // AFTER commit
```
Artifact file writes use temp → sha256/size → atomic rename → DB record (runtime-semantics §6.2).
If the DB insert fails after rename, recovery scans orphaned files (§16.3).
### 18.3 FK-off invariants
`foreign_keys = OFF` (db-schema §1) is compensated by application-level checks (runtime-semantics §5,
overview §8.2). The 8 invariants:
1. `tasks.session_id` → existing `sessions.id`
2. `task_attempts.task_id` → existing `tasks.id`
3. `agents.task_id` → existing `tasks.id` when not null
4. `tool_runs.task_id`, `tool_runs.agent_id` → existing rows when not null
5. `command_runs.task_id`, `command_runs.agent_id`, `command_runs.tool_run_id` → existing rows
6. `workspaces.task_id`, `workspaces.agent_id` → existing rows when not null
7. `diagnostics.command_run_id`, `diagnostics.artifact_id` → existing rows when not null
8. `evidence_refs` foreign columns → existing rows when not null
`SessionStore.referential_check()` (§4.3) runs at startup and periodically; violations are logged to
developer log and either re-parented or archived (runtime-semantics §5).
### 18.4 Outbox / compensation for cross-DB writes
Writes to project-level DBs (`debug-records.db`, `learned-memory.db`) or external files follow the
outbox model (runtime-semantics §6.3-§6.4, overview §8.3). The general pattern is:
```text
1. Owning service (LearnedMemoryStore, DebugKnowledgeStore, ...) performs the external
DB/file write through its own transaction. Each owning store is single-writer.
2. On success: the owning service ingests a single durable session event whose payload is
already the completion form fixed by event-registry §3 (e.g. memory.promoted with
target_ref populated, debug.record.created with debug_record_id populated). The
EventStore commit makes the cross-DB pair eventually consistent.
3. On external failure: the owning service does NOT emit the success event. It either
emits a task.failed carrying the AirError (so Scheduler/Doctor can decide), or queues
a fresh upstream candidate (e.g. memory.candidate.created) for retry.
4. On restart: recovery cross-checks session-side "upstream" rows (memory.candidate.created
without a matching memory.promoted, or pending debug-task tasks) against external store
state. Stale candidates are re-queued; orphan external rows are surfaced through Doctor.
```
No event payload is extended with intent/commit phase markers; the existing payload
schemas in `event-registry-v1.md §3` are honored as-is. Any future change to add an
intent-phase event would require an ADR plus a new event type or payload version bump
(event-registry §2 rule 7).
**`memory.promoted` outbox semantics** (event-registry §3 `MemoryPromotedPayload`, runtime-semantics §6.4):
Per `event-registry-v1.md §3`, `memory.promoted` payload is fixed to:
`{ candidate_id, target_ref, promoted_by, summary }` — there is no `phase` field
and no separate intent/completion variants. Per `runtime-semantics-v1.md §6.4`,
`memory.promoted` records the **completed** promotion (target_ref already populated).
The outbox sequence is therefore:
| Step | Event / action | DB |
|---|---|---|
| 1 | `memory.candidate.created` (durable, already in registry) | session DB |
| 2 | Owning service (`ExperienceMiner` / curator) approves the candidate and writes the external store (`learned-memory.db` row, `.air/shared/rules` file, or skill file) | external DB / file |
| 3 | On success, emit `memory.promoted` with `target_ref` pointing at the just-written external row/file | session DB |
| 4 | On write failure: do **not** emit `memory.promoted`. Either re-queue via a fresh `memory.candidate.created`, or emit a `task.failed` carrying the `AirError` so Scheduler / Doctor can surface it to the user |
| 5 | On restart: recovery uses session DB candidates without matching promotion events to detect work in flight (runtime-semantics §6.4) |
`memory.archived` likewise records a completed archival (no `phase` field; payload
is fixed by event-registry §3 `MemoryArchivedPayload`).
**`debug.record.created` outbox semantics** (event-registry §3 `DebugRecordCreatedPayload`, runtime-semantics §6.3):
The payload is a single fixed schema with `debug_record_id` already assigned by
the owning `DebugKnowledgeStore`. Per `runtime-semantics §6.3` the flow is:
1. Owning `DebugKnowledgeStore` performs the `debug-records.db` insert/update.
2. On success, emit `debug.record.created` referencing the new `debug_record_id`.
3. On `debug-records.db` write failure: emit a `task.failed` (or future
`debug.record.failed`, gated by ADR) carrying the `AirError`; do **not** emit
`debug.record.created`.
No `phase` payload field is introduced in either event. Any future intent/commit
split must go through an ADR + payload version bump (event-registry §2 rule 7).
### 18.5 Security invariants
From contracts §23, security-model-v1, overview §12:
- LLM output is untrusted until validated by runtime/tool schemas and PermissionEngine.
- Provider output cannot directly modify files or run commands.
- Credentials are referenced by `auth_ref`, never copied into events/artifacts.
- No automatic upload of source, logs, screenshots, bundles, pcaps, or artifacts.
- Destructive/system-sensitive actions require confirmation or policy block.
- Path policy uses realpath normalization; symlink escapes are not allowed by string-prefix checks.
- `.git/` internals are protected from arbitrary write tools.
- `sudo` risk is determined by command intent/target/system sensitivity, not string alone.
### 18.6 Domain write-ownership invariants
This section consolidates the cross-cutting rules that any implementer — including a
context-isolated subagent that only holds one subsystem slice — must not violate. Each
subsystem chapter (§4-§17) lists its applicable invariants by number; this section is the
single source of truth for them.
**INV-1 — Session-DB state columns are written only by event projection.**
Every `*.status` / lifecycle column in the session DB is mutated **only** inside
`EventStore.project(event, tx)` as part of the durable event transaction (§18.2,
runtime-semantics §3). No service (Scheduler, WorkerManager, ToolRegistry, MainAgent, …)
may issue a direct `UPDATE` to these columns outside event projection. The authoritative
table → writing-event map:
| Table.column | Written only by (durable event projection) |
|---|---|
| `sessions.status` | `session.created` (active) / `session.archived` / `session.deleted` |
| `message_drafts.status` | `assistant.message.started` (streaming) / `assistant.message.failed` (error); row deleted by `assistant.message.created` |
| `tasks.status` | `task.created` (pending) / `task.started` (running) / `task.completed` / `task.failed` / `task.blocked` / `task.cancelled` / `task.interrupted` |
| `task_attempts.status` / `failure_*` | `task.started` (insert) / `task.completed` / `task.failed` (update) |
| `agents.status` | `agent.started` (running or starting, projected once) / `agent.completed` / `agent.failed` / `agent.lost` / `agent.cancelled`**no event-less `starting → running` write** (§5.4, §20.4) |
| `tool_runs.status` | `tool.started` (running) / `tool.completed` (ok) / `tool.failed` (error) / `tool.cancelled` |
| `workspaces.status` | `workspace.created` (active) / `workspace.merge.completed` (merged) / `workspace.merge.conflicted` (conflicted) / `workspace.cleaned` (cleaned); `abandoned` set by GC compaction event path (§7.5, runtime-semantics §15) |
| `command_runs` | **no physical status column** — status is *derived* (§4.4), never written |
**INV-1 exemptions (explicitly NOT event-sourced; safe to write directly):**
| Column | Direct writer | Basis |
|---|---|---|
| `agents.last_heartbeat_at`, `tasks.heartbeat_at` | `AgentMonitor` coalesce (throttled) | runtime-semantics §4 — liveness timestamps, recovery does not replay heartbeat events |
| `ui_state.*` | `UiStateRepository.upsert` | db-schema §18 — UI scratch state, not event-driven (§21.3) |
**INV-2 — Cross-DB / external writes use the outbox model with a single writer.**
Writes to `debug-records.db`, `learned-memory.db`, `rules/`, `skills/`, or artifact files
go through the owning store, which is the **single writer** for that store, and follow the
outbox sequence in §18.4 (external write first → then ingest one completion event). No other
component writes those stores. `EventStore.project()` never opens an external DB or file.
**INV-3 — Side effects only through the tool + permission path.**
All filesystem / shell / network / git side effects go through `ToolRegistry.call`, which
always consults `PermissionEngine.evaluate` first (§9.1, §9.3). Workers never touch fs/shell/
network/SQLite except via parent-mediated tool IPC (contracts §10, §23). LLM/provider output
never performs a direct side effect (§18.5).
**INV-4 — Import/dependency direction is one-way.**
The allowed-import graph in §2 (frozen by `c4/module.md` + contracts §23) is never crossed:
`contracts` is imported by all and imports nothing; `workers` import only contracts + the
WorkerRuntime IPC surface; TUI imports only contracts; capabilities install dependencies only
through Doctor.
**INV-5 — EventBus is transport, never a source of truth.**
Recovery and scheduling rebuild from SQLite (events + domain rows), never from replaying
EventBus traffic (§5.5, contracts §7, overview §9.3). A dropped/duplicated EventBus delivery
must never change durable state.
> **For context-isolated execution (e.g. parallel isolated subagents):** inject §18.6 in full
> into every subagent's working context regardless of which subsystem slice it owns. INV-1 in
> particular guards the most common slice-local mistake — writing a status column directly from
> the subsystem that happens to know the new value — which a partial context cannot otherwise
> detect.
## 19. Sequence Designs
### 19.1 User request → task execution → completion
```text
User
│ user.message.created
MainAgent (CLASSIFYING)
│ classify_intent → "task"
MainAgent (DELEGATING)
│ architecture impact? → ArchitectureDesigner.assess_impact
│ architecture.impact.completed
MainAgent (SCHEDULING)
│ Scheduler.create_tasks(specs)
│ task.created (durable)
Scheduler (PLANNING_WAVE)
│ WavePlanner.plan → SchedulerWavePlan
Scheduler (DISPATCHING)
│ WorkspaceManager.create_workspace
│ workspace.created (durable)
│ ContextAssembler.assemble → ContextPack
│ WorkerManager.spawn
│ agent.started (durable)
│ task.started (durable)
WorkerProcess (ExecutorRole)
│ WorkerRuntime.call_tool("fs.read", ...)
│ → IPC tool.call → parent ToolRegistry.call
│ → PermissionEngine.evaluate → allow
│ → tool.started (durable)
│ → execute → tool.completed (durable)
│ → IPC tool.result
│ WorkerRuntime.call_tool("fs.edit", ...)
│ → (same flow, read-before-edit enforced)
│ WorkerRuntime.call_tool("shell.run", verification)
│ → command.started, command.completed (durable)
│ return WorkerResult { status: "completed", ... }
Scheduler (COLLECTING_RESULTS)
│ validate WorkerResult
│ task.completed (durable)
│ agent.completed (durable)
Scheduler (MERGING)
│ WorkspaceManager.merge_workspace
│ workspace.merge.completed (durable)
Scheduler (REVIEWING_WAVE) [if required]
│ create review task → ReviewerRole
│ ReviewerResult { verdict: "approved" }
Scheduler (COMPLETED)
│ SchedulerRunResult { status: "completed" }
MainAgent (SUMMARIZING)
│ present result to user
│ trigger ExperienceMiner task (optional)
MainAgent (IDLE)
```
### 19.2 Tool call with permission prompt
```text
WorkerRuntime.call_tool("shell.run", { command: "rm -rf build/" })
ToolRegistry.call
│ PermissionEngine.evaluate
│ → PathClassifier.classify → project_build_output
│ → CommandRiskAnalyzer.analyze → destructive
│ → decision: ask_user (risk: medium)
ToolRegistry suspends
│ permission.prompt.requested (durable)
ProjectionStore.apply → PermissionPromptProjection
TUI PermissionPrompt renders
│ user selects "Allow once"
UiCommandChannel emits permission response
│ permission.prompt.resolved (durable)
│ PermissionEngine.record
ToolRegistry resumes
│ tool.started (durable)
│ execute shell command
│ command.started, command.completed (durable)
│ tool.completed (durable)
IPC tool.result → WorkerRuntime
```
### 19.3 Compaction flow
```text
ContextAssembler.assemble
│ fit_budget fails → compaction_requested = true
Scheduler receives compaction request
│ create_tasks([{ type: "compact", ... }])
│ task.created (durable)
│ context.compaction.requested (durable)
Scheduler dispatches CompactorRole
│ task.started, agent.started (durable)
│ context.compaction.started (durable)
CompactorRole
│ snapshot immutable message range
│ generate summary via LLM
│ ArtifactStore.create(summary artifact)
│ artifact.created (durable)
│ return CompactorResult { summary_id }
Scheduler collects result
│ summary.created (durable) ← inserts summaries row
│ context.compaction.completed (durable)
│ task.completed (durable)
Original messages preserved for backtracking
```
### 19.4 Architecture Designer review gate
Triggered when a Worker result, plan change, or detected drift touches contracts, schema,
events, package boundaries, security, or runtime semantics (scope-escalation §4, overview §10.7).
```text
Trigger source (Worker/Scheduler/Main Agent)
│ detects: contract/schema/event change | boundary cross | ADR/C4 drift
Main Agent.dispatch_architecture_review(change)
│ build ImpactRequest { change, affected_refs, evidence_refs }
ArchitectureDesigner.assess_impact
│ load L4 architecture pack (plan/ADR/C4) via ContextAssembler
│ ProviderManager.complete (read-only analysis, no fs/shell writes)
│ classify result:
│ silent_continue → no event mutation, log advisory
│ requires_user_confirmation → architecture.impact.completed (durable)
│ + permission.prompt.requested via Main Agent
│ requires_replan → architecture.impact.completed (durable)
│ + Scheduler replan trigger
│ reject_or_escalate → architecture.impact.completed (durable)
│ + escalate to user / block tasks
ArchitectureDesigner.update_architecture_docs (only if confirmed)
│ DocumentUpdate applied via tool layer (ToolRegistry + PermissionEngine)
│ architecture.plan.updated (durable) + artifact refs to plan/ADR/C4
Scheduler consumes architecture.impact.completed
│ mark impacted tasks via requirement.changed propagation
│ wave replan or task cancellation per result class
Main Agent resumes; downstream tasks proceed with refreshed L4 context
```
Notes:
- The gate never replaces Reviewer (overview §10.7); it adds an architecture-level check.
- ArchitectureDesigner has no direct fs/shell access; all writes go through `ToolRegistry`
under `PermissionEngine` (contracts §13, §23 rule "tools → side effect only via PermissionEngine").
- Result class strings come from scope-escalation §4 (`silent_continue`, `requires_user_confirmation`,
`requires_replan`, `reject_or_escalate`).
- All gate decisions land as durable `architecture.impact.completed`; subsequent doc edits land
as `architecture.plan.updated` (event-registry §3).
### 19.5 Debug knowledge capture
```text
ExecutorRole fails with build error
│ WorkerResult { status: "failed", error: AirError }
Scheduler (REPAIRING_OR_CONTINUING)
│ RetryPlanner.decide → "debug"
│ create debug task
DebuggerRole
│ analyze failure evidence
│ lookup DebugKnowledgeStore.lookup_by_signature
│ (no match) → diagnose root cause
│ fix or return blocker
│ return DebuggerResult { diagnosis, root_cause, fixed: true }
Scheduler
│ task.completed (durable)
│ debug.record.created (durable) ← session event
│ DebugKnowledgeStore.insert (outbox step 3)
Future similar failure
│ DebugKnowledgeStore.lookup_by_signature → hit
│ DebuggerRole applies known fix faster
```
## 20. State Machine Designs
### 20.1 Main Agent State Machine
From main-agent-state-machine.md. States and transitions:
```text
┌─────────────────────────────────────────────────────────────────┐
│ IDLE │
│ Waiting for user input │
└───────────────────────────┬─────────────────────────────────────┘
│ user.message.created
┌─────────────────────────────────────────────────────────────────┐
│ CLASSIFYING │
│ LLM classifies intent │
└────────┬──────────────────┬──────────────────┬──────────────────┘
│ chat/Q&A │ task request │ /direct
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ANSWERING│ │ DELEGATING │ │ DIRECT_MODE │
│→ IDLE │ └──┬──────┬───┘ │ /done→IDLE │
└─────────┘ │ │ └─────────────┘
simple│ needs│plan
▼ ▼
┌──────────┐ ┌─────────────────────┐
│SCHEDULING│ │ARCHITECTURE_DESIGNING│
│→ EXECUTING│ │→ CONFIRMING │
└──────────┘ └─────────────────────┘
┌─────────────┐
│ CONFIRMING │
│ user confirm│
└──┬──────┬───┘
confirm │ │ reject → IDLE
┌─────────────┐
│ EXECUTING │
│ Scheduler │
└──┬──────┬───┘
requirement │ │ all done
change ▼ │
┌─────────────┐│
│INTERRUPTING ││
└──┬──────┬───┘│
exec-only│ design │
▼ ▼ │
resume ┌──────────┐ │
EXECUTING│ARCH_REV │ │
│→CONFIRM │ │
└─────────┘ │
┌─────────────┐
│ SUMMARIZING │
│ → IDLE │
└─────────────┘
```
State-to-permission_template (main-agent-state-machine §State-to-AgentRuntimeContext):
| State | permission_template |
|---|---|
| IDLE, CLASSIFYING, ANSWERING, CONFIRMING, SUMMARIZING | N/A |
| DIRECT_MODE | `main_direct` |
| EXECUTING | per-task (Scheduler assigns) |
| INTERRUPTING, ARCHITECTURE_REVISING | N/A (delegation) |
### 20.2 Scheduler State Machine
From scheduler-state-machine-v1.md §4. States:
```text
IDLE
│ task.created / execution request
LOADING_GRAPH
│ reconstruct TaskGraph from DB
│ detect orphaned agents → agent.lost
│ validate dependencies
├─ valid runnable → PLANNING_WAVE
├─ no runnable + blocked → BLOCKED
├─ all terminal success → COMPLETED
└─ invalid beyond repair → BLOCKED
PLANNING_WAVE
│ WavePlanner.plan
├─ runnable wave → DISPATCHING
└─ no runnable → BLOCKED
DISPATCHING
│ create workspaces
│ assemble context
│ spawn workers
│ emit task.started, agent.started
└─ → MONITORING
MONITORING
│ track heartbeats, progress, timeouts
│ handle requirement.changed
├─ all agents terminal → COLLECTING_RESULTS
├─ user/global cancel → CANCELLED
└─ architecture/user blocker → BLOCKED
COLLECTING_RESULTS
│ validate WorkerResult
│ persist artifacts/evidence
│ classify task terminal status
├─ workspaces need merge → MERGING
├─ review required → REVIEWING_WAVE
├─ more work → REPAIRING_OR_CONTINUING
└─ all done → COMPLETED
MERGING
│ WorkspaceManager.merge_workspace
├─ success + review → REVIEWING_WAVE
├─ success + more → REPAIRING_OR_CONTINUING
├─ conflict recoverable → REPAIRING_OR_CONTINUING
└─ unrecoverable → BLOCKED
REVIEWING_WAVE
│ schedule review tasks
│ collect ReviewerResult
└─ → REPAIRING_OR_CONTINUING
REPAIRING_OR_CONTINUING
│ retry failed within budget
│ create repair tasks
│ skip allowed failures
├─ more runnable → PLANNING_WAVE
├─ blocked → BLOCKED
├─ all done → COMPLETED
└─ cancelled → CANCELLED
Terminal: COMPLETED | BLOCKED | CANCELLED
```
### 20.3 Task Status Transitions
From db-schema §7, scheduler-state-machine §2:
```text
pending ──task.started──▶ running
running ──task.completed──▶ completed
running ──task.failed──▶ failed
running ──task.blocked──▶ blocked
running ──task.cancelled──▶ cancelled
running ──task.interrupted──▶ interrupted
failed ──retry──▶ pending (new attempt)
interrupted ──resume──▶ pending
blocked ──decision received──▶ pending
```
### 20.4 Agent Status Transitions
From db-schema §10. Each transition below is driven by a durable `agent.*` event and
applied to `agents.status` only through event projection (runtime-semantics §3); there is
no direct status write that bypasses the event log.
```text
(insert) ──agent.started──▶ starting | running
running ──agent.completed──▶ completed
running ──agent.failed──▶ failed
running ──agent.lost──▶ lost
running ──agent.cancelled──▶ cancelled
```
`agent.started` sets the initial status to `running` (handshake completed before emission,
the normal case) or `starting` (emitted before handshake). The `starting` value is the
initial projected status, not a separate event-less transition; baseline defines no
durable `starting → running` event, and `agents.status` is never updated outside
`agent.*` projection. Process readiness (`worker.ready` handshake, §8.2) is a live IPC
signal, not a status-column write.
### 20.5 Workspace Status Transitions
From db-schema §16:
```text
active ──merge success──▶ merged
active ──merge conflict──▶ conflicted
active ──abandon──▶ abandoned
conflicted ──decision──▶ abandoned | active (retry)
merged ──GC (7d)──▶ cleaned
abandoned ──GC (3d)──▶ cleaned
```
### 20.6 Capability Lifecycle
From capability-trust-v1 §7, overview §6:
```text
discovered ──validate──▶ validated
validated ──doctor_check──▶ doctor_checked
doctor_checked ──enable──▶ enabled
enabled ──register_tools──▶ registered
registered ──activate──▶ active
active ──disable──▶ disabled
active ──failure──▶ failed
active ──update──▶ updated (→ discovered)
```
## 21. Traceability Matrix
This matrix maps frozen baseline sections to detailed design sections, ensuring no baseline
requirement is unaddressed.
### 21.1 Contracts → Detailed Design
| Contracts section | Detailed design section |
|---|---|
| §2 Core Primitive Types | §3 (contracts package) |
| §3 Error Contracts | §3, §18.1 |
| §4 Entity References | §3 |
| §5 Runtime Event Contracts | §3, §5 |
| §6 Transaction and Storage | §4, §18.2 |
| §7 EventBus/Store/Ingestor | §5 |
| §8 Project and Session | §6 |
| §9 Task and Scheduler | §7 |
| §10 Worker and IPC | §8 |
| §11 WorkerResult | §8.3 |
| §12 Tool Contracts | §9.1 |
| §13 Permission Contracts | §9.2, §9.3 |
| §14 Artifact and Evidence | §11 |
| §15 Provider Contracts | §12 |
| §16 Context Contracts | §10 |
| §17 Projection/UI Contracts | §13 |
| §18 Capability Contracts | §9.5 |
| §19 Doctor and Logging | §16 |
| §20 Debug/Learned Memory | §11.3 |
| §21 Diagnostic Contracts | §15 (DiagnosticParser) |
| §22 Versioning Rules | §3 (contracts package rules) |
| §23 Boundary Rules | §2, §18.5 |
### 21.2 State Machines → Detailed Design
| State machine baseline | Detailed design section |
|---|---|
| main-agent-state-machine.md | §14.1, §20.1 |
| scheduler-state-machine-v1.md | §7, §20.2 |
| db-schema §7 task status | §20.3 |
| db-schema §10 agent status | §20.4 |
| db-schema §16 workspace status | §20.5 |
| capability-trust-v1 §7 | §9.5, §20.6 |
### 21.3 DB Schema → Detailed Design
| DB schema table | Repository (§4.3) | Event projection (§5.4) |
|---|---|---|
| schema_meta | MigrationRunner | — |
| sessions | SessionRepository | session.* |
| messages | MessageRepository | user/assistant.message.* |
| message_drafts | MessageDraftRepository | assistant.message.started/failed |
| events | EventRepository | all durable |
| tasks | TaskRepository | task.* |
| task_dependencies | TaskDependencyRepository | task.created |
| task_attempts | TaskAttemptRepository | task.started/completed/failed |
| agents | AgentRepository | agent.* |
| tool_runs | ToolRunRepository | tool.* |
| command_runs | CommandRunRepository | command.* |
| artifacts | ArtifactRepository | artifact.created |
| diagnostics | DiagnosticRepository | diagnostic.created |
| evidence_refs | EvidenceRepository | evidence.created |
| workspaces | WorkspaceRepository | workspace.* |
| summaries | SummaryRepository | summary.created |
| ui_state | UiStateRepository | — (not event-driven) |
| debug_records (project DB) | DebugKnowledgeStore | debug.record.created |
| learned_memories (project DB) | LearnedMemoryStore | memory.* |
### 21.4 Code View → Detailed Design
| Code view package | Detailed design sections |
|---|---|
| packages/contracts | §3 |
| packages/runtime | §4-§11, §13-§14, §16 |
| packages/llm | §12 |
| packages/toolchain-cpp | §15 |
| packages/tui | §13.2 |
| packages/cli | §17 |
### 21.5 Overview → Detailed Design
| Overview section | Detailed design section |
|---|---|
| §2 System Goal | §0 (authority), §2 |
| §4 Container Overview | §2, §3 |
| §5 Dependency Rules | §2 |
| §6 Runtime Component Overview | §4-§11 |
| §7 Runtime Agent Overview | §14 |
| §8 State and Data Overview | §4, §6, §11 |
| §9 Event, Error, Projection | §5, §18.1, §13.1 |
| §10 Execution Flow Overview | §7, §8, §9, §19 |
| §11 IPC and Worker Overview | §8 |
| §12 Permission and Security | §9.2, §9.3, §18.5 |
| §13 Context, Memory, Compaction | §10, §11.3, §19.3 |
| §14 UI/HUD and Provider | §12, §13 |
| §15 Doctor, Restore, Recovery | §16 |
| §16 Implementation Phase Mapping | (implementation phase, not design) |
| §17 Validation Overview | (test phase, not design) |
## 22. UML Class Diagrams (Mermaid)
### 22.1 Contracts Package
```mermaid
classDiagram
class RuntimeEvent~T~ {
+id: UUID
+type: string
+version: number
+timestamp: ISOTimeString
+session_id: SessionID
+project_id?: ProjectID
+source: EventSource
+route: string[]
+payload: T
}
class EventSource {
+kind: "main"|"architecture_designer"|"scheduler"|"agent"|"tool"|"system"
+id?: string
+agent_type?: AgentType
}
class TaskSpec {
+id: TaskID
+type: TaskType
+title: string
+description: string
+acceptance_criteria: string[]
+scope: TaskScope
+dependencies: TaskDependencySpec[]
+verification: VerificationPolicy
+constraints: TaskConstraints
+context_refs: TaskContextRefs
+output_contract: WorkerOutputContract
}
class WorkerResult~T~ {
+task_id: TaskID
+agent_id: AgentID
+agent_type: AgentType
+status: WorkerStatus
+summary: string
+changed_files: string[]
+artifacts: ArtifactRef[]
+verification: VerificationResult[]
+risks: Risk[]
+follow_up_tasks: FollowUpTask[]
+evidence_refs: EvidenceRef[]
+result: T
}
class AirError {
+error_id: UUID
+kind: ErrorKind
+severity: ErrorSeverity
+message: string
+detail?: string
+retryability: Retryability
+semantic_signature: string
+cause_ref?: EntityRef
+cause_refs?: EntityRef[]
+user_action?: string
+metadata?: JsonObject
}
class ToolDefinition~I,O~ {
+name: string
+version: number
+description: string
+input_schema: JsonSchema~I~
+output_schema: JsonSchema~O~
+category: ToolCategory
+permissions: ToolPermissionSpec
+streaming: boolean
}
class PermissionDecision {
+action: PermissionAction
+grant_scope: PermissionGrantScope
+risk_level: string
+reason: string
+required_confirmation?: boolean
+backup_required?: boolean
+evidence_ref_ids?: EvidenceRefID[]
}
class ArtifactRef {
+artifact_id: ArtifactID
+uri: string
+path: string
+type: string
+sha256?: string
+size_bytes?: number
}
class EvidenceRef {
+evidence_ref_id: EvidenceRefID
+kind: string
+ref: string
+claim: string
+location_json?: unknown
}
RuntimeEvent --> EventSource
WorkerResult --> ArtifactRef
WorkerResult --> EvidenceRef
AirError --> EntityRef
```
### 22.2 Runtime Core Services
```mermaid
classDiagram
class RuntimeApp {
+start(options): Promise~void~
+shutdown(): Promise~void~
}
class ServiceRegistry {
+get~T~(token): T
+register(token, service): void
}
class ProjectStore {
+locate(start_path): Promise~ProjectContext~
+initialize(root, options): Promise~ProjectContext~
+open(root): Promise~ProjectContext~
}
class SessionManager {
+open_session(project, options): Promise~SessionContext~
+close_session(session_id): Promise~void~
}
class DatabaseManager {
+open(path): DatabaseHandle
+transaction~T~(fn): Promise~T~
}
class EventIngestor {
+ingest~T~(event): Promise~void~
+ingest_ephemeral~T~(event): Promise~void~
}
class EventStore {
+append~T~(event, options): Promise~void~
+append_many(events, options): Promise~void~
+query(filter): Promise~RuntimeEvent[]~
}
class EventBus {
+publish~T~(event): void
+subscribe(filter, handler): Subscription
+drain(): Promise~void~
}
class ProjectionStore {
+hydrate(session_id): Promise~void~
+apply(event): void
+snapshot(): ProjectionSnapshot
+subscribe(handler): Subscription
}
RuntimeApp --> ServiceRegistry
RuntimeApp --> ProjectStore
RuntimeApp --> SessionManager
SessionManager --> DatabaseManager
EventIngestor --> EventStore
EventIngestor --> EventBus
EventStore --> DatabaseManager
EventStore --> EventBus : publishes after commit
ProjectionStore --> EventBus : subscribes
```
### 22.3 Scheduler Subsystem
```mermaid
classDiagram
class Scheduler {
+create_tasks(session_id, specs): Promise~void~
+add_dependency(session_id, task_id, dep): Promise~void~
+load_graph(session_id): Promise~TaskGraph~
+run_until_idle(session_id): Promise~SchedulerRunResult~
+cancel_task(task_id, reason): Promise~void~
-plan_wave(graph): SchedulerWavePlan
-dispatch(wave): Promise~void~
-collect_results(): Promise~void~
}
class TaskGraph {
+session_id: SessionID
+tasks: Map~TaskID, TaskNode~
+dependencies: TaskDependencyRecord[]
+get_runnable_tasks(): TaskNode[]
+mark_terminal(task_id, status): void
}
class WavePlanner {
+plan(graph, resources): SchedulerWavePlan
}
class RetryPlanner {
+decide(task, attempts, error): RetryDecision
}
class WorkspaceManager {
+create_workspace(plan): Promise~WorkspaceRef~
+merge_workspace(workspace_id): Promise~MergeResult~
+cleanup_workspace(workspace_id): Promise~void~
}
class AgentMonitor {
+record_heartbeat(event): void
+detect_lost_agents(): Promise~AgentLost[]~
+enforce_timeouts(): Promise~void~
}
class WorkerManager {
+spawn(task_spec, context_pack): Promise~WorkerProcess~
+cancel(agent_id, reason): Promise~void~
}
Scheduler --> TaskGraph
Scheduler --> WavePlanner
Scheduler --> RetryPlanner
Scheduler --> WorkspaceManager
Scheduler --> AgentMonitor
Scheduler --> WorkerManager
```
### 22.4 Tool and Permission Subsystem
```mermaid
classDiagram
class ToolRegistry {
+register~I,O~(definition, executor): void
+register_streaming~I,O~(definition, executor): void
+call~I,O~(name, input, context): Promise~ToolResultEnvelope~O~~
+call_streaming~I,O~(name, input, context): AsyncIterable
+list(): ToolDefinition[]
}
class PermissionEngine {
+evaluate(context): Promise~PermissionDecision~
+record(decision, context): Promise~PermissionRecordResult~
}
class PathClassifier {
+classify(path, project_root): PathRiskClassification
}
class CommandRiskAnalyzer {
+analyze(command, cwd): CommandRiskAnalysis
}
class CapabilityRegistry {
+discover(): Promise~CapabilityManifestV1[]~
+validate(manifest): Promise~ValidationResult~
+enable(capability_id): Promise~void~
+disable(capability_id): Promise~void~
+register_tools(tool_registry): Promise~void~
}
ToolRegistry --> PermissionEngine
ToolRegistry --> ToolDefinition : registers
ToolRegistry --> ToolExecutor : invokes
PermissionEngine --> PathClassifier
PermissionEngine --> CommandRiskAnalyzer
CapabilityRegistry --> ToolRegistry : registers tools
```
### 22.5 Worker and IPC
```mermaid
classDiagram
class WorkerProcess {
+agent_id: AgentID
+pid?: number
+send(envelope): void
+on_message(handler): void
}
class WorkerProtocol {
+encode(msg): string
+decode(line): IpcMessage
+validate_direction(msg): void
+check_protocol_version(v): boolean
}
class WorkerRuntime {
+emit(event): Promise~void~
+call_tool~I,O~(name, input): Promise~ToolResultEnvelope~O~~
+checkpoint(data): Promise~void~
}
class WorkerRole~T~ {
<<interface>>
+run(task_spec, context_pack, runtime): Promise~WorkerResult~T~~
}
class ExecutorRole {
+run(...): Promise~WorkerResult~ExecutorResult~~
}
class ReviewerRole {
+run(...): Promise~WorkerResult~ReviewerResult~~
}
class DebuggerRole {
+run(...): Promise~WorkerResult~DebuggerResult~~
}
class CompactorRole {
+run(...): Promise~WorkerResult~CompactorResult~~
}
class ExperienceMinerRole {
+run(...): Promise~WorkerResult~ExperienceMinerResult~~
}
WorkerRole <|.. ExecutorRole
WorkerRole <|.. ReviewerRole
WorkerRole <|.. DebuggerRole
WorkerRole <|.. CompactorRole
WorkerRole <|.. ExperienceMinerRole
WorkerRole --> WorkerRuntime
WorkerProcess --> WorkerProtocol
```
### 22.6 Provider (LLM) Subsystem
```mermaid
classDiagram
class ProviderManager {
+load_config(): Promise~void~
+select_model(requirement): Promise~ModelAssignment~
+complete(input): AsyncIterable~ProviderStreamEvent~
}
class ProviderAdapter {
<<interface>>
+provider_id: ProviderID
+list_models(): Promise~ProviderCapabilityMatrix[]~
+validate_model(model_id): Promise~ProviderCapabilityMatrix~
+complete(input): AsyncIterable~ProviderStreamEvent~
+count_tokens?(input): Promise~number~
}
class AnthropicAdapter {
+provider_id: "anthropic"
}
class OpenAICompatibleAdapter {
+provider_id: string
-converter: AnthropicCanonicalConverter
}
class AnthropicCanonicalConverter {
+to_provider(messages): unknown[]
+from_provider(response): unknown[]
}
class StreamNormalizer {
+normalize(stream): AsyncIterable~ProviderStreamEvent~
}
ProviderManager --> ProviderAdapter
ProviderAdapter <|.. AnthropicAdapter
ProviderAdapter <|.. OpenAICompatibleAdapter
OpenAICompatibleAdapter --> AnthropicCanonicalConverter
ProviderAdapter --> StreamNormalizer
```
### 22.7 Context and Compaction
```mermaid
classDiagram
class ContextAssembler {
+assemble(input): Promise~AssembledContext~
-load_layers(purpose): Promise~PromptLayer[]~
-fit_budget(layers, budget): BudgetFitResult
}
class PromptLayerLoader {
+load_runtime_invariant(): PromptLayer
+load_role(role): PromptLayer
+load_project_rules(project): PromptLayer[]
+load_task_context(spec, refs): PromptLayer[]
}
class CompactionPolicy {
+should_compact(messages, budget): boolean
+compact(messages, target): Promise~CompactionResult~
}
class PromptLayer {
+level: PromptLayerLevel
+priority: number
+content: unknown
+token_estimate?: number
+source_ref?: string
+immutable?: boolean
}
ContextAssembler --> PromptLayerLoader
ContextAssembler --> CompactionPolicy
ContextAssembler ..> EvidenceStore : L6 evidence
ContextAssembler ..> SessionStore : L7/L8
PromptLayerLoader --> PromptLayer
```
### 22.8 Agents
```mermaid
classDiagram
class MainAgent {
+handle_user_message(message): Promise~void~
+present_progress(): Promise~void~
+present_blocker(blocker): Promise~void~
-classify_intent(message): string
-state: MainAgentState
}
class ArchitectureDesigner {
+assess_impact(change): Promise~ArchitectureImpact~
+update_architecture_docs(update): Promise~DocumentUpdate~
}
MainAgent --> Scheduler
MainAgent --> ContextAssembler
MainAgent --> ProviderManager
ArchitectureDesigner --> ContextAssembler
ArchitectureDesigner --> ProviderManager
ArchitectureDesigner --> EventIngestor
```
## 23. Reference Implementation Map
AirCoding is **self-owned, not a wrapper** (baselineV1 §1): the architecture and class
design above are AirCoding's own. This section records, per baselineV1 §2 and
`decisions-round-1/2/3`, **where an implementer should consult mature reference code**
instead of designing a component from scratch, and the **reuse mode** for each. This is a
guidance map, not a dependency contract — it never overrides §2 import direction or §18.6
invariants.
**Reuse modes:**
- `npm-dep` — consume directly as a dependency; do not re-implement.
- `fork/adapt` — copy and adapt source into our package; keep our own boundaries.
- `pattern` — reference structure/patterns; implement independently.
- `behavioral` — match observable behavior/quality; no code lineage.
**Reference source location:** local reference checkouts live under the working-copy `reference/`
tree at the **git root** — i.e. `<repo-root>/reference/`, where `<repo-root>` is `AirCoding/`, the
parent of `AirPlan/` (which holds this design). Paths in the table below are relative to
`<repo-root>`, not to `AirPlan/`. This `reference/` tree is **git-ignored / not committed**
(working-copy only, ~0.5 GB). Implementers (including isolated subagents) must verify the referenced
checkout exists and is non-empty before relying on it; if absent, clone the upstream repo to that
path. Current pinned directory names (verified present):
```text
reference/opencode-1.15.5/ reference/openai-codex/
reference/hermes-agent-2026.5.16/ reference/anthropic-skills/
reference/asciinema-3.2.0/ reference/atuin-18.16.1/
reference/claude-hud-0.0.12/ reference/claude-code-cli/ (Claude Code 2.1.88, behavioral ref only)
```
| DD component | Reference project (baselineV1 §2) | Reuse mode | Notes / local path |
|---|---|---|---|
| §13.2 TUI (`TuiApp`, components) | OpenTUI (`@opentui/solid` `@opentui/core` `@opentui/keymap`) | `npm-dep` | D-002/D-048: direct dependency; do not build a renderer. Theme/dialog/toast/keymap/layout/spinner/border/markdown/code/diff rendering adapted from OpenCode UI patterns. |
| §13.2 TUI interaction layout | OpenCode TUI (`reference/opencode-1.15.5/`) | `pattern` | D-048: reference visual/interaction patterns; **do not** reuse OpenCode SDK/sync/session business state. |
| §12 Provider (`ProviderManager`, `ProviderAdapter`, converters) | `@opencode-ai/llm` (`reference/opencode-1.15.5/`) | `fork/adapt` | D-002 list: provider/model abstraction may be forked/adapted; output stays Anthropic-canonical (D-046). |
| §8.4 + §9.4 Execution discipline (read-before-edit, exact edit, verify-before-complete) | Claude Code CLI (`reference/claude-code-cli/`) | `behavioral` | baselineV1 §2: quality benchmark only — edit safety, patch granularity, conflict handling, verification discipline. No code lineage. |
| §9.4 `fs.edit`/`fs.patch` + §15 patch/test execution loop | OpenAI Codex (`reference/openai-codex/`) | `pattern` | baselineV1 §2: shell/patch/test direct-execution loop, tool orchestration, wider tool surface. |
| §11.3 + §14 Knowledge / ExperienceMiner / Curator | Hermes Agent (`reference/hermes-agent-2026.5.16/`) | `pattern` | D-024: Nudge Engine interval trigger (~10 turns/tool-calls), Curator dedup daemon, skill self-patch. |
| §11.3 + runtime-semantics §11 Skills (`SKILL.md`, `scripts/`/`references/`/`assets/`) | Anthropic Claude Skills (`reference/anthropic-skills/`) | `pattern` | D-058: SKILL.md frontmatter + directory layout + trigger/retrieval descriptions; SkillGenerator output format. |
| §16.2 Logging / HUD + future PTY capture | asciinema / Atuin / claude-hud (`reference/asciinema-3.2.0/`, `reference/atuin-18.16.1/`, `reference/claude-hud-0.0.12/`) | `pattern` | baselineV1 §2: PTY capture, command metadata/history indexing, HUD/statusline layout. |
| §3 Message format (Anthropic canonical content blocks) | Claude Code message model (`reference/claude-code-cli/`) | `behavioral` | D-016: Text/Thinking/ToolUse/ToolResult blocks; canonical internal format. |
**Rules for reference reuse:**
1. `npm-dep` items (`@opentui/*`) are real dependencies — implementers must not re-implement
them; see `packages/tui` (code-view §7).
2. `fork/adapt` and `pattern` items: adapt into our own package layout (§2 import direction),
preserve our contracts (§3) and invariants (§18.6) verbatim. A reference's internal model
never leaks across our package boundaries — e.g. OpenCode session/sync state is **not**
adopted (D-048).
3. `behavioral` items contribute **no code**; they set a quality bar verified by tests/evidence.
4. License compliance for any copied/forked source is checked at the architecture gate before
merge.
> **For context-isolated execution:** include the row(s) relevant to a subagent's slice in its
> working context. The intent is to stop a partial-context implementer from re-deriving a diff
> engine, TUI renderer, or skill format that a named reference already provides.
## 24. Design Freeze Declaration
**System Detailed Design is frozen as of 2026-06-01 (commit `2673e49` → this commit).**
Multi-model review complete: DeepSeek (R1, R7) / MIMO 2.5 Pro (R2) / GPT-5.5 Pro (R3) /
Opus 4.8 (R4, R6) / Opus 4.7 (R5 regression). All P0/P1/P2 findings closed
across 7 review rounds. Coverage: contracts 100%, events 100%, DB schema 100%,
state machines 100%, forbidden edges 100%.
**Design-level verification (complete — verified in review):**
- [x] All contracts §2-§21 interfaces mapped to DD classes (§21.1)
- [x] All db-schema §3-§18 tables have corresponding repositories (§4.3, §21.3)
- [x] All event-registry §3 durable events have projection handlers (§5.4)
- [x] All state machines specified (Main Agent / Scheduler / Task / Agent / Workspace / Capability, §20)
- [x] FK-off invariants defined (§18.3 ≡ runtime-semantics §14)
- [x] Outbox model defined (§18.4)
- [x] Security invariants defined (§18.5)
**Implementation-entry gates (to be verified as code is written):**
- [ ] All contracts §2-§21 interfaces are exported from `packages/contracts`
- [ ] All forbidden paths (contracts §23) are enforced by lint/import boundaries
- [ ] All closed enums (db-schema §21) are validated on insert/update
- [ ] FK-off invariants (§18.3) are checked at startup and periodically
- [ ] Outbox model (§18.4) is used for cross-DB writes
- [ ] Security invariants (§18.5) are enforced
---
End of System Detailed Design (frozen).