# 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` 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(fn: (tx: TransactionHandle) => Promise): Promise -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 -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` (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 // 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(event: RuntimeEvent): Promise +ingest_ephemeral(event: RuntimeEvent): Promise -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(event, options?): Promise +append_many(events, options?): Promise +query(filter: EventFilter): Promise -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(event: RuntimeEvent): void +subscribe(filter: EventFilter, handler): Subscription +drain?(): Promise -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 +initialize(project_root, options?): Promise +open(project_root): Promise ``` `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 +close_session(session_id): Promise 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.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 +add_dependency(session_id, task_id, dependency: TaskDependencySpec): Promise +load_graph(session_id): Promise +run_until_idle(session_id): Promise +cancel_task(task_id, reason): Promise -plan_wave(graph): SchedulerWavePlan -dispatch(wave): Promise -collect_results(): Promise -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 +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 +merge_workspace(workspace_id): Promise +cleanup_workspace(workspace_id): Promise ``` 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 +enforce_timeouts(): Promise ``` 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 +cancel(agent_id, reason): Promise 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` (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 { run(task_spec, context_pack, runtime): Promise> } class WorkerRuntime +emit(event: RuntimeEvent): Promise // → IPC event → parent EventIngestor +call_tool(name, input): Promise> // → IPC tool.call +checkpoint(data): Promise // → 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` 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(definition: ToolDefinition, executor: ToolExecutor): void +register_streaming(definition, executor: StreamingToolExecutor): void +call(name, input, context: ToolExecutionContext): Promise> +call_streaming(name, input, context): AsyncIterable> +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 +record(decision, context): Promise 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 +validate(manifest): Promise +enable(capability_id): Promise +disable(capability_id): Promise +register_tools(tool_registry: ToolRegistry): Promise 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 -load_layers(purpose): Promise -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 ``` `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 +get(artifact_id): Promise +read(artifact_id): Promise -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_ uri = artifact://project//session// filename = -- 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 +list_for_entity(entity_type, entity_id): Promise ``` 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 +lookup_by_signature(failure_signature): Promise +lookup_by_task(task_id): Promise +update(debug_record_id, patch): Promise class LearnedMemoryStore implements LearnedMemoryStore // learned-memory.db +insert(memory: LearnedMemory): Promise +lookup_by_type(memory_type): Promise +update_status(memory_id, status): Promise +scan_stale(): Promise ``` 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 +select_model(requirement: ModelRequirement): Promise +complete(input: ProviderCompletionInput): AsyncIterable -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 +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 +present_progress(): Promise +present_blocker(blocker: BlockerReport): Promise -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 +update_architecture_docs(update): Promise ``` 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 } class CMakeConfigurator { +configure(input): Promise } class CppBuilder { +build(input): Promise } class CppTestRunner { +run_tests(input): Promise } class CppcheckRunner { +run(input): Promise } class ClangdClient { +query(input): Promise } 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 -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 } 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 } class RuntimeFactory { +create(options): Promise } ``` 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 ...` | 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~ { <> +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 { <> +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. 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).