Files
AirCoding/AirPlan/docs/architecture/system-detailed-design.md
AirCoding abc244f0da Detailed design: fix two semantic findings from Opus deep re-audit
An Opus-perspective semantic re-audit (reading state-machine semantics,
event-sourcing invariants, and baseline together rather than grep-style
structural checks) surfaced two issues the prior seven rounds missed.

O2 [P1] §5.4 + §20.4: removed the event-less `starting → running` status
  transition. The prior R1-02 fix eliminated the same-event atomicity error
  but left a second status-machine arc that (a) has no basis in baseline
  (event-registry §3 says agent.started inserts status = running OR starting,
  with no second transition) and (b) had WorkerManager UPDATE agents.status
  directly, bypassing the event log and violating runtime-semantics §3
  (all same-session domain updates go through BEGIN→insert events row→
  project→COMMIT). agents.status is now written only by agent.* projection;
  worker.ready handshake is clarified as a live IPC signal, not a status write.

O1 [P2] §5.4: MemoryRepository (an orphan name appearing exactly once,
  never defined in §4.3/§11.3/contracts/code-view) corrected to
  LearnedMemoryStore, matching §11.3 and the §21.3 traceability matrix.

Detailed design re-frozen with these corrections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:09:46 +08:00

102 KiB

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:

contracts      → (none)
llm            → contracts
toolchain-cpp  → contracts
tui            → contracts
runtime        → contracts, llm (facade only)
cli            → contracts, runtime, tui, llm, toolchain-cpp
workers        → contracts + WorkerRuntime IPC surface (no direct runtime import)

Forbidden edges (frozen by contracts §23) are enforced by lint boundaries and reviewed at the architecture gate. The detailed class design below never crosses these edges.

Ownership summary (frozen by code-view §11 State Ownership):

State Owning class Reader access
session DB SessionStore, EventStore runtime services only
live events EventBus runtime publish/subscribe
UI projection ProjectionStore TUI read-only via ProjectionClient
artifacts ArtifactStore tools/workers via runtime API
evidence EvidenceStore reports/reviews/debug
tasks/agents Scheduler repositories are storage-only
permission decisions PermissionEngine ToolRegistry requests
model config ProviderManager runtime/Doctor read via API
project rules/context ContextAssembler/ProjectStore workers get excerpts

Worker AgentType vs. runtime roles (contracts §5 AgentType, overview §10):

The frozen AgentType union covers only worker child-process roles: executor | reviewer | debugger | compactor | experience_miner.

Runtime-resident roles are not members of AgentType and never appear in WorkerResult.agent_type, agents.agent_type, or PromptLayerLoader.load_role(role):

Runtime role Implementing class LLM use Prompt source
main MainAgent (§14.1) Yes, via ProviderManager Built-in resource (loaded directly by MainAgent, see §10.2)
architecture_designer ArchitectureDesigner (§14.2) Yes, via ProviderManager Built-in resource (loaded directly by ArchitectureDesigner, see §10.2)
scheduler Scheduler (§7.1) No (pure orchestration) N/A

This split keeps the contract AgentType enum stable and avoids accidentally giving runtime roles a worker WorkerResult shape. Where event payloads need to identify a runtime role (e.g. EventSource.kind in §22.1), they use a separate "main" | "architecture_designer" | "scheduler" literal set defined in event.ts, never AgentType.

3. Contracts Package Detailed Design

packages/contracts is type-only. It contains zero runtime logic, only export interface, export type, and nominal aliases. File layout is frozen by code-view §3.

IMPL note: the contracts package may include tiny pure type-guards (e.g. is_air_error) only if they have no dependencies; default is to keep it declaration-only to satisfy code-view §3 rule "type-only package has no implementation deps" (todo T-002).

Design rules for the contracts package:

  1. Every interface in interface-contracts-v1.md §2-§21 is exported from the file mapped in code-view §3 "Contract Ownership".
  2. No interface gains extra fields here. Field additions require an ADR (contracts §22 rule 6).
  3. JsonSchema<T> stays nominal (JsonObject); no runtime schema engine lives in contracts.
  4. Re-export surface is index.ts which barrel-exports every contract file.

Contract-to-file map (frozen by code-view §3):

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.

class DatabaseManager implements TransactionManager
  +open(path: string): DatabaseHandle
  +transaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>
  -applyPragmas(db): void     // WAL, synchronous=NORMAL, foreign_keys=OFF (db-schema §1)
  -handleFor(tx): RawDb

Rules (db-schema §1): on open, set journal_mode=WAL, synchronous=NORMAL, foreign_keys=OFF. transaction wraps BEGIN/COMMIT/ROLLBACK. A durable event insert plus its domain update run inside one transaction call (runtime-semantics §3). TransactionHandle.id is an opaque token mapping to the active raw transaction; nested calls reuse the active handle (IMPL: single-writer per session DB, so no real nesting needed).

4.2 MigrationRunner

class MigrationRunner
  +migrate(db: DatabaseHandle): Promise<void>
  -currentVersion(db): number          // reads schema_meta.schema_version
  -targetVersion(): number             // = 1 for V1.0.0 Alpha
  -applyV1(db): void                   // creates all tables/indexes from db-schema §2-§18

V1 has a single target schema_version = 1. migrate is idempotent: if schema_meta is missing it creates the full schema and seeds the initial keys (db-schema §2). It updates aircoding_version_last_opened on every open. Destructive/non-trivial migrations follow overview §15 (plan, backup, confirm) but V1 only needs create-on-empty.

4.3 Repository layer

All repositories implement Repository<TRecord, TInsert, TUpdate> (contracts §6) or its narrow extensions. They are thin persistence adapters: no scheduling, permission, or projection policy (contracts §6, code-view §9). Records mirror db-schema columns exactly (snake_case).

Repository inventory (code-view §9) and their record types:

Repository Record (db-schema table) Notable methods beyond CRUD
SessionRepository sessions (§3) list_active()
MessageRepository messages (§4) list_by_session(session_id, since?)
MessageDraftRepository message_drafts (§5) upsert, delete_for_message
EventRepository events (§6) insert(rec, tx), query(filter) (contracts §6)
TaskRepository tasks (§7) list_by_status, list_runnable_candidates (contracts §6)
TaskDependencyRepository task_dependencies (§8) list_for_task, list_dependents
TaskAttemptRepository task_attempts (§9) next_attempt_index(task_id), list_by_task
AgentRepository agents (§10) list_active, update_heartbeat
ToolRunRepository tool_runs (§11) list_by_task, list_by_origin_message
CommandRunRepository command_runs (§12) list_by_task (status derived, §4.4)
ArtifactRepository artifacts (§13) list_by_entity, get_by_uri
DiagnosticRepository diagnostics (§14) list_by_signature, list_by_command_run
EvidenceRepository evidence_refs (§15) list_for_entity(type,id)
WorkspaceRepository workspaces (§16) list_by_status, list_gc_candidates
SummaryRepository summaries (§17) get, insert
UiStateRepository ui_state (§18) upsert(scope,key,value), read(scope,key)

SessionStore aggregates all repositories (code-view §9):

class SessionStore
  +sessions: SessionRepository
  +messages: MessageRepository
  +message_drafts: MessageDraftRepository
  +events: EventRepository
  +tasks: TaskRepository
  +task_dependencies: TaskDependencyRepository
  +task_attempts: TaskAttemptRepository
  +agents: AgentRepository
  +tool_runs: ToolRunRepository
  +command_runs: CommandRunRepository
  +artifacts: ArtifactRepository
  +diagnostics: DiagnosticRepository
  +evidence: EvidenceRepository
  +workspaces: WorkspaceRepository
  +summaries: SummaryRepository
  +ui_state: UiStateRepository
  +referential_check(): Promise<OrphanReport>   // FK-off invariants (§18.3)

4.4 Derived command status

command_runs has no physical status column (db-schema §12, runtime-semantics §5). The repository exposes a pure derivation used by projection and reports:

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

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).

class EventIngestor implements EventIngestor (contracts §7)
  +ingest<T>(event: RuntimeEvent<T>): Promise<void>
  +ingest_ephemeral<T>(event: RuntimeEvent<T>): Promise<void>
  -policyFor(type): EventPersistence    // durable | ephemeral, from registry

ingest flow (runtime-semantics §2):

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

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

class EventStore implements EventStore (contracts §7)
  +append<T>(event, options?): Promise<void>
  +append_many(events, options?): Promise<void>
  +query(filter: EventFilter): Promise<RuntimeEvent[]>
  -toRecord(event): PersistedEventRecord       // route_text = route.join("/")
  -project(event, tx): void                    // domain table update per registry map

append algorithm (runtime-semantics §3, event-registry §2):

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

class EventBus implements EventBus (contracts §7)
  +publish<T>(event: RuntimeEvent<T>): void
  +subscribe(filter: EventFilter, handler): Subscription
  +drain?(): Promise<void>
  -match(filter, event): boolean

Rules: live transport only, never a recovery source of truth (contracts §7 rule 4). If a handler throws, the error is caught, logged to developer log, and does not propagate; subscription stays active (contracts §7 rule 5). drain flushes pending async handlers for clean shutdown.

Ephemeral coalescing (event-registry §4): agent.heartbeat, task.progress, assistant.message.delta, tool.progress, command.stdout.delta, command.stderr.delta, hud.frame.rendered may be throttled before reaching subscribers.

6. Project and Session Lifecycle

Module: packages/runtime/src/project/ and sessions/. Classes: ProjectLocator, ProjectInitializer, ProjectStore, SessionManager, SessionStore.

6.1 ProjectStore

Implements contracts §8.

class ProjectStore implements ProjectStore
  +locate(start_path): Promise<ProjectContext | undefined>
  +initialize(project_root, options?): Promise<ProjectContext>
  +open(project_root): Promise<ProjectContext>

locate walks up from start_path looking for .air/shared/project.json (overview §8.1). initialize creates the .air/shared and .air/local trees (overview §8.1), generates a stable project_id UUID stored in .air/shared/project.json (not derived from path; overview §8.1), seeds default rules when options.default_rules, and emits nothing durable yet (no session DB until a session opens). open loads the existing ProjectContext (project_id, roots, schema_version).

Delegated helpers (IMPL, code-view §4):

  • ProjectLocator.locate(start) — upward search.
  • ProjectInitializer.scaffold(root, options) — directory + file creation.

6.2 SessionManager and SessionStore

class SessionManager implements SessionManager (contracts §8)
  +open_session(project, options?): Promise<SessionContext>
  +close_session(session_id): Promise<void>

class SessionStore                 // §4.3 aggregate of repositories

open_session flow:

resolve session_id (options or IdGenerator.session_id())
compute db_path = .air/local/sessions/<session-id>/session.db   (db-schema header)
DatabaseManager.open(db_path); MigrationRunner.migrate(db)
SessionStore bound to this db
ingest session.created (durable → inserts sessions row)
return SessionContext { session_id, project_id, project_root, db_path, artifact_root }

close_session flushes ui_state (db-schema §1), publishes a terminal session event when archiving, and releases the DB handle. Session provider/model selection (model_provider_id, model_id) is captured at open and is immutable for the session (overview §14).

7. Scheduler Subsystem

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.

class Scheduler implements Scheduler
  +create_tasks(session_id, specs: TaskSpec[]): Promise<void>
  +add_dependency(session_id, task_id, dependency: TaskDependencySpec): Promise<void>
  +load_graph(session_id): Promise<TaskGraph>
  +run_until_idle(session_id): Promise<SchedulerRunResult>
  +cancel_task(task_id, reason): Promise<void>
  -plan_wave(graph): SchedulerWavePlan
  -dispatch(wave): Promise<void>
  -collect_results(): Promise<void>
  -state: SchedulerState     // §20.2 lifecycle

create_tasks ingests task.created per spec (durable → inserts tasks + dependency rows). run_until_idle drives the lifecycle in §20.2 until a terminal graph state, returning SchedulerRunResult (contracts §9). The Scheduler asks the user only through Main Agent / PermissionEngine (scheduler-state-machine §8); it never prompts directly.

7.2 TaskGraph

class TaskGraph
  +session_id: SessionID
  +tasks: Map<TaskID, TaskNode>
  +dependencies: TaskDependencyRecord[]
  +get_runnable_tasks(): TaskNode[]      // hard deps satisfied, not conflicting
  +mark_terminal(task_id, status): void
  +dependents_of(task_id): TaskNode[]
  +validate_refs(): OrphanReport          // FK-off (§18.3)

get_runnable_tasks honors dependency semantics (scheduler-state-machine §4 PLANNING_WAVE): hard deps must be completed; soft deps affect priority only; conflict/serialization deps block concurrent dispatch on overlapping write areas.

7.3 WavePlanner

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

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

class WorkspaceManager
  +create_workspace(plan: WorkspacePlan): Promise<WorkspaceRef>
  +merge_workspace(workspace_id): Promise<MergeResult>
  +cleanup_workspace(workspace_id): Promise<void>

Strategies (db-schema §16, scheduler-state-machine §MERGING): main (no merge), worktree (git merge/patch), isolated_copy (copy-back/patch). Emits workspace.created, workspace.merge.started, and a terminal workspace.merge.completed or workspace.merge.conflicted. GC retention follows overview §15 (active until merge/cancel; merged 7d; abandoned 3d; cleaned keeps DB row).

Responsibility split — Scheduler vs. WorkspaceManager (overview §10.3, scheduler-state-machine §4/§MERGING):

Responsibility Owner
Decide whether a task needs an isolated workspace Scheduler (via WavePlanner.assign_workspace)
Decide which strategy (main/worktree/isolated_copy) Scheduler (via WavePlanner.assign_workspace)
Encode the decision into a WorkspacePlan Scheduler (in SchedulerWavePlan)
Materialize the workspace on disk (clone/copy/setup) WorkspaceManager (create_workspace)
Apply merge/patch back to project root WorkspaceManager (merge_workspace)
Detect/report merge conflicts WorkspaceManager (emits workspace.merge.conflicted)
Decide how to resolve a conflict Scheduler (consumes conflicted event; may re-plan or escalate to Main Agent)
Cleanup/GC of abandoned/cleaned workspaces WorkspaceManager (cleanup_workspace, driven by Scheduler heartbeat / Doctor)
Persist workspace lifecycle events WorkspaceManager (durable events) → EventStore projection updates workspaces row

WorkspaceManager is a pure executor of Scheduler decisions: it never plans concurrency, never decides which task occupies which workspace, and never re-plans on conflict. The Scheduler is the sole policy owner; WorkspaceManager is the sole mechanism owner.

7.6 AgentMonitor

class AgentMonitor
  +record_heartbeat(event): void          // coalesced into agents.last_heartbeat_at + tasks.heartbeat_at
  +detect_lost_agents(): Promise<AgentLost[]>
  +enforce_timeouts(): Promise<void>

Heartbeat coalescing interval default 5s or meaningful status change (runtime-semantics §4). Missing heartbeat past threshold → inspect process → alive-but-silent → status ping/soft cancel; gone-without-result → emit agent.lost (scheduler-state-machine §MONITORING). Soft timeout asks for checkpoint and may extend; hard timeout cancels/kills and marks task failed/interrupted (scheduler-state-machine §MONITORING timeout table).

8. Worker and IPC Subsystem

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

class WorkerManager
  +spawn(task_spec: TaskSpec, context_pack: ContextPack): Promise<WorkerProcess>
  +cancel(agent_id, reason): Promise<void>

class WorkerProcess
  +agent_id: AgentID
  +pid?: number
  +send(envelope: IpcEnvelope): void
  +on_message(handler: (m: IpcEnvelope) => void): void

spawn starts a Bun child process, then performs the handshake (§8.2). WorkerProcess owns the NDJSON pipe; stdout carries protocol only, stderr is fatal/logging (baselineV1 §8).

Worker exit codes (baselineV1 §8, authoritative):

Code Meaning
0 protocol-level completion (including task failed/blocked via WorkerResult)
1 uncaught exception
2 startup/protocol error
3 permission error
4 parent cancelled
5 hard timeout killed

Design decision: Task success/failure is communicated through WorkerResult.status, not exit codes. Exit code 0 means the worker completed the IPC protocol correctly and returned a valid WorkerResult; the actual task outcome (completed/failed/blocked/cancelled) is in the result payload. Non-zero exit codes indicate process-level or protocol-level failures that prevent normal result delivery.

8.2 WorkerProtocol and handshake

class WorkerProtocol
  +encode(msg: IpcMessage): string         // NDJSON line
  +decode(line: string): IpcMessage
  +validate_direction(msg): void           // parent_to_worker vs worker_to_parent
  +check_protocol_version(v: number): boolean

Handshake (contracts §10): parent spawns → parent sends agent.start control (with TaskSpec, ContextPack, AgentRuntimeContext) → worker replies worker.ready { protocol_version, worker_version } → parent validates protocol_version; mismatch terminates worker with protocol.error.

Direction typing (contracts §10): parent→worker = control, tool.result, tool.stream; worker→parent = event, log, tool.call, worker.result, worker.checkpoint, protocol.error. validate_direction rejects messages on the wrong channel.

8.3 Worker roles and WorkerRuntime

Roles implement WorkerRole<TResult> (contracts §10). The in-worker WorkerRuntime is the only side-effect surface (contracts §10, §23: workers never write SQLite or touch fs/shell/network except through parent-mediated tools).

interface WorkerRole<TResult> { run(task_spec, context_pack, runtime): Promise<WorkerResult<TResult>> }

class WorkerRuntime
  +emit(event: RuntimeEvent): Promise<void>            // → IPC event → parent EventIngestor
  +call_tool<I,O>(name, input): Promise<ToolResultEnvelope<O>>   // → IPC tool.call
  +checkpoint(data): Promise<void>                     // → IPC worker.checkpoint

Role inventory and constraints (code-view §10):

Role class output_contract Write access Required result
ExecutorRole ExecutorResult scoped project writes ExecutorResult in WorkerResult
ReviewerRole ReviewerResult read-only ReviewerResult
DebuggerRole DebuggerResult scoped writes only when assigned DebuggerResult
CompactorRole CompactorResult summaries/artifacts only CompactorResult
ExperienceMinerRole ExperienceMinerResult candidates/rules/skills only when assigned ExperienceMinerResult

Each role's run ends by returning a WorkerResult<TResult> with the matching agent_type (contracts §11). status ∈ {completed, failed, blocked, cancelled}; a code-changing result cannot be completed unless verification passed or was explicitly skipped with evidence/risk and fallback_allowed (runtime-semantics §9.4, overview §10.9). Self-escalation returns blocked with a BlockerReport rather than improvising (scope-escalation §9).

TaskType → WorkerRole mapping (contracts §9 TaskType, db-schema §tasks.type closed enum):

TaskType Owning WorkerRole Worker agent_type Output contract Notes
execute ExecutorRole executor ExecutorResult Code-changing work; scoped project writes
review ReviewerRole reviewer ReviewerResult Read-only audit/review
debug DebuggerRole debugger DebuggerResult Scoped writes when assigned
compact CompactorRole compactor CompactorResult Summary/artifact writes only
mine_experience ExperienceMinerRole experience_miner ExperienceMinerResult Candidate/rule/skill writes when assigned
docs ExecutorRole executor ExecutorResult Documentation-only task; TaskScope.write_area restricted to docs paths (overview §10.9); same events as execute: task.created/started/completed, tool.* for fs writes, artifact.created for doc artifacts

Design decision — docs task type closure: docs is a formal TaskType (contracts §9, db-schema §tasks.type). It is not a separate WorkerRole; it reuses ExecutorRole with a documentation-scoped TaskScope.write_area (typically docs/, AirPlan/docs/, README, ADR/C4 folders) and an empty or docs-only VerificationPolicy.commands. All persistent events for a docs task are identical to an execute task: task.createdtask.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

Module: packages/runtime/src/tools/, security/, capabilities/.

9.1 ToolRegistry

Implements contracts §12.

class ToolRegistry implements ToolRegistry
  +register<I,O>(definition: ToolDefinition<I,O>, executor: ToolExecutor<I,O>): void
  +register_streaming<I,O>(definition, executor: StreamingToolExecutor<I,O>): void
  +call<I,O>(name, input, context: ToolExecutionContext): Promise<ToolResultEnvelope<O>>
  +call_streaming<I,O>(name, input, context): AsyncIterable<ToolEvent | ToolResultEnvelope<O>>
  +list(): ToolDefinition[]
  -validate_input(def, input): void        // JsonSchema check

call algorithm (contracts §12, §23 "tool → side effect without PermissionEngine" forbidden):

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.

class PermissionEngine implements PermissionEngine
  +evaluate(context: PermissionRequestContext): Promise<PermissionDecision>
  +record(decision, context): Promise<PermissionRecordResult>

class PathClassifier
  +classify(path, project_root): PathRiskClassification   // 8 categories, realpath-normalized

class CommandRiskAnalyzer
  +analyze(command, cwd): CommandRiskAnalysis              // 10 categories

class SecretRedactor
  +redact(text): string                                   // for logs/evidence

evaluate applies the frozen layered order (contracts §13, runtime-semantics §8, overview §12):

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.

class CapabilityRegistry implements CapabilityRegistry
  +discover(): Promise<CapabilityManifestV1[]>
  +validate(manifest): Promise<ValidationResult>
  +enable(capability_id): Promise<void>
  +disable(capability_id): Promise<void>
  +register_tools(tool_registry: ToolRegistry): Promise<void>

class CapabilityManifestValidator
  +validate(manifest): ValidationResult        // schema_version=1, tool schemas, permissions

Lifecycle (overview §6, capability-trust §7): discovered → validated → doctor_checked → enabled → registered → active → disabled|failed|updated. Trust levels (contracts §18): built_in, project_local, user_installed, verified_publisher, untrusted. Trust affects default enablement/prompt posture but never bypasses ToolRegistry or PermissionEngine (overview §6). Dependency installs go only through Doctor (contracts §23 "capability → dependency install outside Doctor" forbidden).

10. Context, Prompt, and Compaction Subsystem

Module: packages/runtime/src/context/. Classes: ContextAssembler, PromptLayerLoader, CompactionPolicy (contracts §16).

10.1 ContextAssembler

class ContextAssembler implements ContextAssembler
  +assemble(input: ContextAssembleInput): Promise<AssembledContext>
  -load_layers(purpose): Promise<PromptLayer[]>
  -fit_budget(layers, budget): BudgetFitResult

assemble builds an Anthropic-canonical context (AssembledContext.canonical_format="anthropic", contracts §16). It loads ordered layers (§10.2), fits them to token_budget via fit_budget, and reports omissions. When the context is too large for inline return it writes a messages artifact and sets messages_artifact_id; ContextPack.assembled_context_ref points to it (contracts §16). If the budget cannot fit required layers it sets compaction_requested=true (consumed by Scheduler, §10.3).

10.2 PromptLayerLoader and layer order

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

class CompactionPolicy implements CompactionPolicy
  +should_compact(messages, token_budget): boolean
  +compact(messages, target_tokens): Promise<CompactionResult>

ContextAssembler may request compaction but does not compact itself; Scheduler creates a compact task that runs CompactorRole (overview §13, runtime-semantics §7). Sequence: context.compaction.requested → context.compaction.started → summary.created → context.compaction.completed. Only summary.created inserts the summaries row (runtime-semantics §7); the completion event references the summary_id and never duplicates the row. Original messages are preserved for backtracking.

11. Artifact, Evidence, and Knowledge Subsystem

Module: packages/runtime/src/artifacts/ and knowledge/.

11.1 ArtifactStore

Implements contracts §14; naming from artifact-naming-v1.

class ArtifactStore implements ArtifactStore
  +create(input: ArtifactCreateInput, context: ArtifactContext): Promise<ArtifactRef>
  +get(artifact_id): Promise<ArtifactRef | undefined>
  +read(artifact_id): Promise<ArtifactReadResult>
  -write_temp_then_rename(bytes): {path, sha256, size}

create algorithm (runtime-semantics §6.2, overview §8.4):

write temp file → compute sha256 + size → atomic rename to artifact path
artifact_id = art_<ulid>
uri = artifact://project/<project-id>/session/<session-id>/<artifact-id>
filename = <timestamp>-<artifact-id>-<slug><extension>
ingest artifact.created (durable → inserts artifacts row)

If the DB insert fails after rename, startup recovery scans orphaned files and registers or quarantines them (runtime-semantics §6.2). Artifact type is from the closed set (db-schema §21: log, diff, screenshot, pcap, report, diagnostic, bundle, other).

11.2 EvidenceStore

class EvidenceStore implements EvidenceStore
  +create(input: EvidenceCreateInput): Promise<EvidenceRef>
  +list_for_entity(entity_type, entity_id): Promise<EvidenceRef[]>

Emits evidence.created (durable → evidence_refs). Worker results embed full EvidenceRef[] when evidence is part of the conclusion; lightweight payloads carry evidence_ref_ids (contracts §14). kind is from the closed set (db-schema §21: build_output, test_output, log, screenshot, diff, metric, other).

11.3 DebugKnowledgeStore and LearnedMemoryStore

Project-level DBs (db-schema §20), implement contracts §20.

class DebugKnowledgeStore implements DebugKnowledgeStore   // debug-records.db
  +insert(record: DebugRecord): Promise<void>
  +lookup_by_signature(failure_signature): Promise<DebugRecord[]>
  +lookup_by_task(task_id): Promise<DebugRecord[]>
  +update(debug_record_id, patch): Promise<void>

class LearnedMemoryStore implements LearnedMemoryStore     // learned-memory.db
  +insert(memory: LearnedMemory): Promise<void>
  +lookup_by_type(memory_type): Promise<LearnedMemory[]>
  +update_status(memory_id, status): Promise<void>
  +scan_stale(): Promise<LearnedMemory[]>

Writes to these DBs are cross-store side effects: the session DB records intent/completion events and the owning store performs the external write (outbox model, §18.4; runtime-semantics §6.3-§6.4). debug.record.created and memory.promoted are the session-side durable markers.

12. Provider (LLM) Subsystem

Module: packages/llm/src/. Classes per code-view §5. packages/llm owns adapters, model config, conversion, and ProviderManager; runtime calls it only through the facade (code-view §2 rule 5).

12.1 ProviderManager

Implements contracts §15.

class ProviderManager implements ProviderManager
  +load_config(): Promise<void>
  +select_model(requirement: ModelRequirement): Promise<ModelAssignment>
  +complete(input: ProviderCompletionInput): AsyncIterable<ProviderStreamEvent>
  -adapter_for(provider_id): ProviderAdapter

select_model matches a ModelRequirement against the capability matrix and returns a ModelAssignment (contracts §9/§15). complete routes to the right adapter and yields normalized ProviderStreamEvents. Runtime/session provider+model are fixed for the session; there is no runtime switching API (overview §14).

12.2 ProviderAdapter implementations

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.

class ProjectionStore implements ProjectionStore
  +hydrate(session_id): Promise<void>
  +apply(event: RuntimeEvent): void
  +snapshot(): ProjectionSnapshot
  +subscribe(handler): Subscription
  -projections: { session, tasks, agents, tool_runs, command_runs, artifacts, permission_prompts, blockers }

hydrate rebuilds from DB via repositories (code-view §4: ProjectionStore → SessionStore). apply handles all durable events plus key ephemeral events (agent.heartbeat, task.progress, assistant.message.delta, tool.progress, command.stdout.delta, command.stderr.delta); unknown event types are ignored (contracts §17 comment). command_runs projection status uses the derivation in §4.4. ProjectionStore is never a scheduling/recovery source of truth (overview §9.3).

13.2 TUI

Module packages/tui/src/ (code-view §7). TuiApp consumes a ProjectionClient and renders components; it imports only packages/contracts (contracts §17, §23; code-view §2 rule 3).

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

class MainAgent
  +handle_user_message(message): Promise<void>
  +present_progress(): Promise<void>
  +present_blocker(blocker: BlockerReport): Promise<void>
  -classify_intent(message): "chat" | "task" | "direct"
  -state: MainAgentState     // §20.1

Lifecycle is the frozen state machine (main-agent-state-machine.md, §20.1 here): IDLE → CLASSIFYING → ANSWERING | DELEGATING | DIRECT_MODE; DELEGATING → SCHEDULING | ARCHITECTURE_DESIGNING → CONFIRMING → EXECUTING → (INTERRUPTING | ARCHITECTURE_REVISING) → SUMMARIZING → IDLE. Direct mode uses permission_template="main_direct" and writes only to the main workspace (contracts §10, runtime-semantics §16). Requirement changes emit requirement.changed (main-agent-state-machine events table). Confirmation gating: implementation-only silent → EXECUTING; architecture-level → Architecture Designer assessment → low-permission user confirm / high-permission auto-proceed (main-agent-state-machine §Confirmation Gating).

14.2 ArchitectureDesigner

class ArchitectureDesigner
  +assess_impact(change): Promise<ArchitectureImpact>
  +update_architecture_docs(update): Promise<DocumentUpdate>

Emits architecture.impact.completed and architecture.plan.updated (event-registry §3, overview §10.7). It owns the architecture review gate: interface/schema/event/package-boundary/security/ runtime-semantics/ADR/C4/plan consistency; it does not replace Reviewer (overview §10.7). Gate trigger conditions and result rules (silent_continue / requires_user_confirmation / requires_replan / reject_or_escalate) follow scope-escalation §4 and overview §10.7. It calls the LLM only through the ProviderManager facade (code-view §4) and routes escalations through Main Agent.

15. Toolchain C++ Subsystem

Module: packages/toolchain-cpp/src/ (code-view §6). Exposes cpp.* tools through capability registration, not direct runtime coupling (code-view §2 rule 4).

class CppToolRegistrar { +register(tool_registry): void }
class CppProjectDetector { +detect(project_root): Promise<CppDetectOutput> }
class CMakeConfigurator { +configure(input): Promise<CppCmakeConfigureOutput> }
class CppBuilder { +build(input): Promise<CppBuildOutput> }
class CppTestRunner { +run_tests(input): Promise<CppTestOutput> }
class CppcheckRunner { +run(input): Promise<CppcheckOutput> }
class ClangdClient { +query(input): Promise<ClangdQueryOutput> }
class DiagnosticParser {
  +parse_compiler_output(output): Diagnostic[]
  +semantic_signature(diagnostic): string
}

Workflow (overview §10.8): detect → configure (CMake+Ninja preferred, Make fallback) → build → parse diagnostics → test → cppcheck → clangd query when needed → Debugger on failure → scoped fix → Reviewer → architecture gate if contracts/schema/events/boundaries changed. DiagnosticParser performs deterministic extraction and semantic_signature only; LLM interpretation lives in runtime Debugger/Reviewer, never inside toolchain-cpp (runtime-semantics §12). Diagnostic matches contracts §21; compile_commands.json is generated/located when clangd/static analysis needs it (overview §10.8).

16. Doctor, Logging, Migration, and Recovery Subsystem

Module: packages/runtime/src/doctor/, logging/, plus MigrationRunner (§4.2) and recovery in SessionManager/Scheduler.

16.1 DoctorService

Implements contracts §19.

class DoctorService implements DoctorService
  +run(input: DoctorRunInput): Promise<DoctorRunOutput>
  -check_capability(capability): DoctorIssue[]    // internal helper, not a public contract method
  -self_bootstrap(): DoctorIssue[]    // Bun, SQLite, shell, .air writability

Self-bootstrap before any capability check (runtime-semantics §18, overview §15): verify Bun runtime, SQLite, basic shell, .air/ writability; on failure report a blocking issue and skip remaining checks. Modes (contracts §19): read_only, fix (under PermissionEngine), and bundle export (local artifact, no auto-upload, overview §15). Emits doctor.* events (event-registry §3).

16.2 Logging

class Logger implements Logger { debug/info/warn/error(message, data?) }
class DeveloperLogEncryptor implements DeveloperLogEncryptor { encrypt_log_chunk(chunk): Promise<Uint8Array> }
class SecretRedactor { redact(text): string }   // shared with PermissionEngine §9.2

air.log is user-facing with redacted operational errors; air.developer.log is encrypted and more detailed (overview §15, baselineV1 §23). All logs redact secrets/auth refs/provider keys via SecretRedactor. Failures link log artifacts through evidence refs rather than copying sensitive content into user summaries.

16.3 Recovery

On startup/resume (scheduler-state-machine §9, overview §15, runtime-semantics §6.2/§14):

  1. load tasks with status running/interrupted, active agents/workspaces;
  2. check process liveness by PID; reconnect IPC if alive, else emit agent.lost;
  3. mark task failed/interrupted by resumability;
  4. preserve workspaces until merge/cleanup decision;
  5. orphan-artifact scan registers or quarantines files;
  6. FK-off orphan scan (§18.3) logs and re-parents/archives dangling references;
  7. workspace GC applies retention (overview §15);
  8. rebuild scheduler queue from pending/failed-with-retry tasks.

17. CLI Subsystem

Module: packages/cli/src/ (code-view §8). CliEntrypoint.main(argv) routes to command classes; a RuntimeFactory builds the RuntimeApp.

class CliEntrypoint { +main(argv): Promise<number> }
class RuntimeFactory { +create(options): Promise<RuntimeApp> }

Command class inventory (frozen mapping, code-view §8, overview §14):

Command class Subcommand(s) Notes
RunCommand run [project] Default entry; spawns RuntimeApp + TUI
InitCommand init First-run wizard, project bootstrap
DoctorCommand `doctor [--fix --bundle]`
ProviderCommand provider list, provider current Read-only; no runtime model switch
E2ECommand e2e Local end-to-end harness, no CI hooks
ReleaseCommand release Build/package release artifacts
ResumeCommand resume [--session SID] Catalog: resume last/specific session
CompactCommand compact [--session SID] Catalog: trigger compaction task
HistoryCommand history [--session SID] Catalog: render past sessions/events
SessionListCommand session list Catalog: enumerate sessions
RestoreCommand `restore <file time

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:

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:

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:

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.

19. Sequence Designs

19.1 User request → task execution → completion

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

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

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).

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

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:

┌─────────────────────────────────────────────────────────────────┐
│                           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:

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:

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.

(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:

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:

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

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

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

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

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

classDiagram
    class WorkerProcess {
        +agent_id: AgentID
        +pid?: number
        +send(envelope): void
        +on_message(handler): void
    }
    class WorkerProtocol {
        +encode(msg): string
        +decode(line): IpcMessage
        +validate_direction(msg): void
        +check_protocol_version(v): boolean
    }
    class WorkerRuntime {
        +emit(event): Promise~void~
        +call_tool~I,O~(name, input): Promise~ToolResultEnvelope~O~~
        +checkpoint(data): Promise~void~
    }
    class WorkerRole~T~ {
        <<interface>>
        +run(task_spec, context_pack, runtime): Promise~WorkerResult~T~~
    }
    class ExecutorRole {
        +run(...): Promise~WorkerResult~ExecutorResult~~
    }
    class ReviewerRole {
        +run(...): Promise~WorkerResult~ReviewerResult~~
    }
    class DebuggerRole {
        +run(...): Promise~WorkerResult~DebuggerResult~~
    }
    class CompactorRole {
        +run(...): Promise~WorkerResult~CompactorResult~~
    }
    class ExperienceMinerRole {
        +run(...): Promise~WorkerResult~ExperienceMinerResult~~
    }
    WorkerRole <|.. ExecutorRole
    WorkerRole <|.. ReviewerRole
    WorkerRole <|.. DebuggerRole
    WorkerRole <|.. CompactorRole
    WorkerRole <|.. ExperienceMinerRole
    WorkerRole --> WorkerRuntime
    WorkerProcess --> WorkerProtocol

22.6 Provider (LLM) Subsystem

classDiagram
    class ProviderManager {
        +load_config(): Promise~void~
        +select_model(requirement): Promise~ModelAssignment~
        +complete(input): AsyncIterable~ProviderStreamEvent~
    }
    class ProviderAdapter {
        <<interface>>
        +provider_id: ProviderID
        +list_models(): Promise~ProviderCapabilityMatrix[]~
        +validate_model(model_id): Promise~ProviderCapabilityMatrix~
        +complete(input): AsyncIterable~ProviderStreamEvent~
        +count_tokens?(input): Promise~number~
    }
    class AnthropicAdapter {
        +provider_id: "anthropic"
    }
    class OpenAICompatibleAdapter {
        +provider_id: string
        -converter: AnthropicCanonicalConverter
    }
    class AnthropicCanonicalConverter {
        +to_provider(messages): unknown[]
        +from_provider(response): unknown[]
    }
    class StreamNormalizer {
        +normalize(stream): AsyncIterable~ProviderStreamEvent~
    }
    ProviderManager --> ProviderAdapter
    ProviderAdapter <|.. AnthropicAdapter
    ProviderAdapter <|.. OpenAICompatibleAdapter
    OpenAICompatibleAdapter --> AnthropicCanonicalConverter
    ProviderAdapter --> StreamNormalizer

22.7 Context and Compaction

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

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):

  • All contracts §2-§21 interfaces mapped to DD classes (§21.1)
  • All db-schema §3-§18 tables have corresponding repositories (§4.3, §21.3)
  • All event-registry §3 durable events have projection handlers (§5.4)
  • All state machines specified (Main Agent / Scheduler / Task / Agent / Workspace / Capability, §20)
  • FK-off invariants defined (§18.3 ≡ runtime-semantics §14)
  • Outbox model defined (§18.4)
  • 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).