diff --git a/docs/architecture/index.html b/docs/architecture/index.html new file mode 100644 index 000000000..cf94ac27f --- /dev/null +++ b/docs/architecture/index.html @@ -0,0 +1,762 @@ + + + + + +Agent Orchestrator — Architecture + + + +
+ + + +
+ + +
+

Agent Orchestrator
Architecture Design Doc

+

A platform for spawning and supervising many parallel AI coding agents — each isolated in its own git worktree with its own PR — behind a single dashboard. Agents autonomously fix CI failures, address review comments, and drive their PRs to merge.

+
+ ~30 pnpm packages + 8 plugin slots + 0 databases — flat files + TypeScript · Node 20+ + Next.js 15 · React 19 + macOS · Linux · Windows +
+

Every major claim below cites real source as file:line. Verified against the tree on the main branch.

+
+ + +
+

1System overview

+

AO runs multiple coding agents (Claude Code, Codex, Aider, OpenCode, Cursor, Grok, Kimi) simultaneously. One call — ao spawn <project> <issue> — creates an isolated workspace, launches an agent inside a managed runtime, and wires up the feedback loops so that PR reviews and CI failures route back to the right agent automatically.

+ +

The defining design choice is that every external dependency is a swappable plugin. Where the agent runs, which AI tool it is, how code is isolated, where issues live, how PRs and CI are read, how you get notified, and how a human attaches to a terminal — all eight are pluggable interfaces resolved at startup. The orchestration engine in packages/core knows nothing concrete about tmux, GitHub, or Claude; it only knows the interfaces.

+ +
Why plugins? The problem space is a matrix of "your runtime × your agent × your tracker × your SCM." Hard-coding any one combination would fork the project for every team. Plugin slots make the matrix a configuration concern (agent-orchestrator.yaml) rather than a code change. New capability = new plugin, not new branches in the core.

+ +

The big picture

+

The core engine sits in the middle. Plugins plug into its eight slots. A polling lifecycle manager drives state forward; a session manager owns CRUD and disk persistence; the Next.js dashboard observes everything over a multiplexed WebSocket.

+ +
+
Figure 1 — Plugin-slot model around the core engine
+
+flowchart TB
+  subgraph CORE["packages/core — Orchestration Engine"]
+    direction TB
+    SM["Session Manager
CRUD · spawn · restore · reconcile"] + LM["Lifecycle Manager
30s poll · state machine · reactions"] + REG["Plugin Registry
discover · resolve · instantiate"] + SM --- LM + REG --- SM + REG --- LM + end + + RT["RUNTIME
tmux · process"]:::runtime + AG["AGENT
claude-code · codex · aider
opencode · cursor · grok · kimi
"]:::agent + WS["WORKSPACE
worktree · clone"]:::workspace + TR["TRACKER
github · linear · gitlab"]:::tracker + SCM["SCM
github · gitlab"]:::scm + NO["NOTIFIER
desktop · slack · discord
webhook · composio · …
"]:::notifier + TE["TERMINAL
iterm2 · web"]:::terminal + + RT --- REG + AG --- REG + WS --- REG + TR --- REG + SCM --- REG + NO --- LM + TE --- SM + + CFG["agent-orchestrator.yaml"]:::cfg --> REG + LM --> WEB["Next.js Dashboard
mux WebSocket → React + xterm.js"]:::web + + classDef runtime fill:#2a1c14,stroke:#f2845c,color:#ffd9c7; + classDef agent fill:#16223a,stroke:#6ea8ff,color:#cfe2ff; + classDef workspace fill:#11281f,stroke:#4fd1a1,color:#c4f0df; + classDef tracker fill:#2a2410,stroke:#f5c451,color:#f6e6b0; + classDef scm fill:#1f1830,stroke:#9d7bff,color:#ddd0ff; + classDef notifier fill:#2a1320,stroke:#f06595,color:#ffc9de; + classDef terminal fill:#102528,stroke:#38c7d4,color:#bdf0f5; + classDef cfg fill:#1b2330,stroke:#6b7689,color:#e6ebf2; + classDef web fill:#151b27,stroke:#6ea8ff,color:#e6ebf2; +
+
+ +
+
Figure 2 — Data flow at a glance
+
+flowchart LR
+  A["agent-orchestrator.yaml"] --> B["Config Loader
Zod validate"] + B --> C["Plugin Registry"] + C --> D["Session Manager"] + D --> E["Lifecycle Manager
poll loop"] + E --> F["Events"] + F --> G["Notifiers"] + D -. flat files .-> H[("~/.agent-orchestrator")] + E --> I["Next.js API routes"] + I --> J["mux WebSocket"] + J --> K["React Dashboard
+ xterm.js"] + classDef d fill:#151b27,stroke:#2f3a4f,color:#e6ebf2; + class A,B,C,D,E,F,G,H,I,J,K d; +
+
+
+ + +
+

2Monorepo & build order

+

AO is a pnpm workspace (pnpm 9.15.4, package.json) with roughly 30 packages. Cross-package dependencies use the workspace:* protocol so the whole graph builds from source.

+ +
# root build — pnpm -r build, dependency-ordered
+packages/
+  core/            # engine: types, config, registry, session + lifecycle managers
+  cli/             # the `ao` command — depends on every plugin
+  web/             # Next.js 15 dashboard (App Router, React 19, xterm.js)
+  ao/              # global CLI wrapper (thin shim around cli)
+  notifier-macos/  # native macOS notifier helper
+  integration-tests/
+  plugins/         # 25 plugin packages, one per implementation
+    agent-claude-code  agent-codex  agent-aider  agent-opencode
+    agent-cursor  agent-grok  agent-kimicode
+    runtime-tmux  runtime-process
+    workspace-worktree  workspace-clone
+    tracker-github  tracker-linear  tracker-gitlab
+    scm-github  scm-gitlab
+    notifier-desktop  notifier-slack  notifier-discord  notifier-webhook
+    notifier-composio  notifier-dashboard  notifier-openclaw
+    terminal-iterm2  terminal-web
+ +
+
Figure 3 — Monorepo dependency & build order
+
+flowchart TD
+  CORE["@aoagents/ao-core
types · config · registry
session + lifecycle managers
"]:::core + PLUGINS["25 plugin packages
@aoagents/ao-plugin-*"]:::plug + CLI["@aoagents/ao-cli
the `ao` command"]:::leaf + WEB["@aoagents/ao-web
Next.js dashboard"]:::leaf + AO["ao
global shim"]:::leaf + + CORE --> PLUGINS + CORE --> CLI + CORE --> WEB + PLUGINS --> CLI + CLI --> AO + + classDef core fill:#16223a,stroke:#6ea8ff,color:#cfe2ff; + classDef plug fill:#11281f,stroke:#4fd1a1,color:#c4f0df; + classDef leaf fill:#151b27,stroke:#2f3a4f,color:#e6ebf2; +
+
+

Build order is therefore core → plugins → cli / web (cli and web build in parallel once their deps are ready). Core is the only barrel-exported package (packages/core/src/index.ts) and forms the stable public API every plugin imports as @aoagents/ao-core.

+
+ + +
+

3The 8 plugin slots

+

All slot interfaces live in packages/core/src/types.ts. Each is async-first (every I/O method returns a Promise), and plugins never call each other directly — they communicate only through the Session object and the lifecycle manager.

+ +
+ Runtime + Agent + Workspace + Tracker + SCM + Notifier + Terminal + Lifecycle (non-pluggable) +
+ +
+
+

Runtime

types.ts:394
+

Where agents execute. Owns the process/PTY: create, destroy, sendMessage, getOutput, isAlive.

+
tmuxprocess
+
+
+

Agent

types.ts:477
+

Which AI tool. Owns introspection: getLaunchCommand, getActivityState, isProcessRunning, getSessionInfo, setupWorkspaceHooks.

+
claude-codecodexaideropencodecursorgrokkimicode
+
+
+

Workspace

types.ts:656
+

Code isolation. create, destroy, list, plus optional exists/restore for session revival.

+
worktreeclone
+
+
+

Tracker

types.ts:713
+

Issue tracking. getIssue, isCompleted, branchName, generatePrompt, optional listIssues/createIssue.

+
githublineargitlab
+
+
+

SCM

types.ts:794
+

PRs, CI, reviews. Largest interface: detectPR, getCIChecks, getReviews, getMergeability, mergePR, batch enrichSessionsPRBatch.

+
githubgitlab
+
+
+

Notifier

types.ts:1170
+

Delivery. notify(event), optional notifyWithActions and post. The only slot that can register multiple instances.

+
desktopslackdiscordwebhookcomposiodashboardopenclaw
+
+
+

Terminal

types.ts:1204
+

Human attach UI. openSession, openAll, optional isSessionOpen.

+
iterm2web
+
+
+

Lifecycle

core (built-in)
+

The state machine + polling loop. Not pluggable — it is the engine itself, the single authority on terminal decisions.

+
lifecycle-manager.ts
+
+
+ +

The registry & the PluginModule contract

+

The plugin registry (packages/core/src/plugin-registry.ts) discovers, resolves, and instantiates plugins. Built-ins are a hard-coded table keyed by slot:name (plugin-registry.ts:39); external plugins come from the config's plugins[] via npm package or local path (plugin-registry.ts:533). Every import is normalized to a PluginModule shape (plugin-registry.ts:303), accepting either a direct module or a default export.

+ +

Every plugin default-exports a PluginModule<T> (types.ts:1747): a manifest, a create() factory, and an optional detect() for binary availability. Config is validated once in create() and captured in a closure — never re-validated per call.

+ +
import type { PluginModule, Runtime } from "@aoagents/ao-core";
+
+export const manifest = {
+  name: "tmux",            // must match the {name} package suffix
+  slot: "runtime" as const, // `as const` preserves the literal type
+  description: "tmux session runtime",
+  version: "0.1.0",
+};
+
+export function create(config?: Record<string, unknown>): Runtime {
+  // validate config here, capture in closure
+  return { /* …Runtime methods… */ };
+}
+
+export function detect(): boolean { /* is the binary installed? */ }
+
+export default { manifest, create, detect } satisfies PluginModule<Runtime>;
+ +

The Session object (types.ts:280) is the shared currency that flows through every slot. Key fields: id, projectId, status (legacy), lifecycle (canonical truth, types.ts:297), activity, branch, pr, workspacePath, runtimeHandle, and a free-form metadata map.

+
+ + +
+

4Session lifecycle

+

A session's truth is stored as a canonical lifecycle with separate state and reason fields (plus parallel pr and runtime sub-states). The dashboard's familiar status labels are derived from this canonical truth, never stored as the source.

+ +
Why split canonical vs legacy status? A flat status like ci_failed conflates three independent facts: is the agent process alive, what is the PR's review state, and what is the orchestrator doing about it. The canonical model keeps these orthogonal (session.state / pr.state / runtime.state), which is what makes the reconciliation invariant possible — the session manager can record a runtime fact without accidentally deciding the session is dead. deriveLegacyStatus() then flattens the orthogonal truth back into one label only for display and backward compatibility.

+ +

Canonical states & terminal reasons

+

Defined as Zod enums in packages/core/src/lifecycle-state.ts:50. There are 8 session states and a rich reason vocabulary; the terminal set is just {done, terminated} (lifecycle-state.ts:131).

+ +
+
+

Session states (lifecycle-state.ts:50)

+ + + + + + + + + + +
StateMeaning
not_startedSpawned, agent not yet running
workingAgent actively processing
idleQuiet, awaiting external event
needs_inputBlocked on a user decision
stuckIdle past threshold / unrecoverable
detectingRuntime unknown — probing
doneterminal — completed successfully
terminatedterminal — killed / errored / cleaned
+
+
+

Terminal reasons (lifecycle-state.ts:60)

+ + + + + + + + + + +
ReasonMaps to
research_completedone
manually_killedterminated → killed
runtime_lostterminated → killed
agent_process_exitedterminated
probe_failureterminated → errored
error_in_processterminated → errored
auto_cleanupterminated → cleanup
pr_mergedterminated → cleanup
+
+
+

The lifecycle also tracks a parallel PR sub-state (none/open/merged/closed with reasons like ci_failing, changes_requested, approved, merge_ready) and a runtime sub-state (unknown/alive/exited/missing/probe_failed) — see lifecycle-state.ts:87 and :108.

+ +

Legacy-status derivation

+

deriveLegacyStatus(lifecycle) (lifecycle-state.ts:432) flattens the canonical triple into the single status the dashboard kanban understands. Terminal and operational states map directly; for an idle/working session with an open PR, the PR sub-state decides the label.

+ +
+
Figure 4 — Legacy status flow (derived for display)
+
+flowchart LR
+  spawning --> working --> pr_open
+  pr_open --> ci_failed
+  pr_open --> review_pending
+  ci_failed --> changes_requested
+  review_pending --> approved
+  changes_requested --> approved
+  approved --> mergeable --> merged --> cleanup --> done
+  classDef s fill:#16223a,stroke:#6ea8ff,color:#cfe2ff;
+  classDef bad fill:#2a1320,stroke:#f06595,color:#ffc9de;
+  classDef good fill:#11281f,stroke:#4fd1a1,color:#c4f0df;
+  class spawning,working,pr_open,review_pending s;
+  class ci_failed,changes_requested bad;
+  class approved,mergeable,merged,cleanup,done good;
+    
+
+ +

The canonical state machine

+
+
Figure 5 — Canonical session state machine (states · transitions · terminal reasons)
+
+stateDiagram-v2
+  [*] --> not_started: spawn_requested
+  not_started --> working: agent_acknowledged
+  working --> idle: task quiet
+  idle --> working: new activity
+  working --> needs_input: awaiting_user_input
+  idle --> needs_input: awaiting_user_input
+  needs_input --> working: input received
+  working --> detecting: runtime probe ambiguous
+  idle --> detecting: runtime_lost
+  detecting --> working: runtime alive
+  detecting --> stuck: probe_failure
+  detecting --> terminated: runtime_lost / agent_process_exited
+  working --> stuck: error_in_process
+  stuck --> working: recovered
+  idle --> done: research_complete
+  working --> done: research_complete
+  idle --> terminated: pr_merged / auto_cleanup
+  working --> terminated: manually_killed
+  stuck --> terminated: probe_failure / error_in_process
+  done --> [*]
+  terminated --> [*]
+    
+
+
Invariant (#1735): only the lifecycle manager's resolveProbeDecision pipeline (lifecycle-manager.ts:1202) may write a terminal state. The session manager records runtime facts and parks a session in detecting — it never writes terminated directly.
+ +

Polling loop & reactions

+

The lifecycle manager (packages/core/src/lifecycle-manager.ts) runs a polling loop, default 30 s (lifecycle-manager.ts:3016). Each tick, pollAll() (:2866) lists sessions, batch-enriches PR data over GraphQL, then runs checkSession() concurrently. determineStatus() (:901) probes runtime liveness (runtime.isAlive), agent activity (agent.getActivityState), and PR state (scm.enrichSessionsPRBatch).

+ +

When a transition fires, a reaction may execute (executeReaction, :1400). Reactions track per-session attempt counts and escalate to a human after a retry budget or time window.

+ + + + + + + + + + +
Transition / eventReactionAction
→ ci_failedci-failedsend-to-agent, enriched with failed job/step/log
→ changes_requestedchanges-requestedsend-to-agent with review comments
review.pendingbacklog dispatchmaybeDispatchReviewBacklog() — human/bot comments routed separately, throttled 2 min
→ merge.readyapproved-and-greenauto-merge or notify
PR branch behindmerge-conflictsrebase message, one-shot per conflict
session.stuckagent-stucksend-to-agent or notify human
session.needs_inputagent-needs-inputnotify human
+

ci-failed is a persistent reaction key (:116) so its escalation budget accumulates across CI oscillation rather than resetting each cycle.

+
+ + +
+

5Session manager

+

The session manager (packages/core/src/session-manager.ts, factory createSessionManager at :377) owns session CRUD and all disk persistence. It is the only component that mutates session metadata files.

+ + + + + + + + +
MethodRefWhat it does
spawn(config):1188Validate issue → reserve ID atomically → create workspace → create runtime → launch agent. Uses a LIFO CleanupStack to roll back on any failure.
list(projectId?):2235Load metadata from disk, repair legacy→canonical lifecycle on read, enrich each with live runtime state. Results cached ~35 s.
get(id):2386Find + enrich a single session.
kill(id, opts?):2445Idempotent (early-return if already terminal) → destroy runtime → tear down managed workspace → record events.
restore(id)Re-attach to a live runtime and restore metadata; pairs with the agent's getRestoreCommand.
+ +

Stale-runtime reconciliation

+

During list() enrichment, if a session's tmux/process runtime is found dead, the manager persists the fact to disk and parks the session in detecting with reason runtime_lost (session-manager.ts:2303) — crucially before emitting any event, so a mid-event crash can't lose the update. It does not decide the session is terminated; the lifecycle manager's probe pipeline makes that call on the next poll. This is the concrete enforcement of invariant #1735.

+
+ + +
+

6Storage model — flat files, no database

+
Why no database? AO is a single-operator local tool, not a multi-tenant service. Flat files under ~/.agent-orchestrator mean zero setup, trivial inspection (cat a session's JSON), git-style hashing for collision-free multi-checkout, and crash safety via atomic writes + file locks. The cost — no queries, no transactions — simply doesn't bite at this scale.

+ +

Paths are computed in packages/core/src/paths.ts using a project-based v2 layout:

+
~/.agent-orchestrator/
+├─ config.yaml                              # global config — all registered projects
+├─ running.json                             # current `ao start` PID, port, projects
+├─ last-stop.json                           # sessions killed by stop/Ctrl+C (for restore)
+├─ portfolio/                               # preferences.json, registered.json
+└─ projects/{projectId}/
+   ├─ orchestrator.json                     # getOrchestratorPath()  paths.ts:139
+   ├─ sessions/{sessionId}.json          # getSessionPath()       paths.ts:144
+   ├─ worktrees/{sessionId}/             # isolated git workspace paths.ts:124
+   ├─ code-reviews/  feedback-reports/
+ +

Session metadata (packages/core/src/metadata.ts) is JSON written via atomic read-modify-write under a .lock file (mutateMetadata, metadata.ts:376; 5 s lock timeout, corrupt files preserved as .corrupt-{ts}). The lifecycle key is the single source of truth: when present, the stored status is ignored and recomputed via deriveLegacyStatus() on read (metadata.ts:226).

+ +

Config (packages/core/src/config.ts) is YAML validated by a Zod schema (OrchestratorConfigSchema, config.ts:357). findConfigFile() (:756) resolves in priority order: AO_CONFIG_PATH env → search up the directory tree from cwd (git-style) → global ~/.agent-orchestrator/config.yaml → legacy home locations.

+
+ + +
+

7End-to-end data flow: ao start → dashboard

+

ao start (packages/cli/src/commands/start.ts) spawns the dashboard as a managed daemon child (start.ts:939), which in turn launches the Next.js server plus a dedicated direct-terminal WebSocket server (packages/web/server/start-all.ts:108), listening on DIRECT_TERMINAL_PORT (default 14801, direct-terminal-ws.ts:103).

+ +

The browser opens one multiplexed WebSocket (packages/web/server/mux-websocket.ts) carrying three logical channels:

+
    +
  • sessionsSessionBroadcaster polls /api/sessions/patches every 3 s (mux-websocket.ts:138) and broadcasts lightweight SessionPatch[].
  • +
  • terminalTerminalManager attaches a node-pty to tmux attach-session, streaming live output with a 50 KB ring buffer for late subscribers (:466).
  • +
  • notificationsNotificationBroadcaster tails the JSONL store every 1 s (:319).
  • +
+ +
Note on transport: the dashboard's primary real-time path is this mux WebSocket; the React hook useSessionEvents (packages/web/src/hooks/useSessionEvents.ts) applies the broadcast patches and falls back to an HTTP refresh of /api/sessions when session membership changes or data goes stale (~15 s). Terminals render through xterm.js in DirectTerminal.tsx driven by useXtermTerminal + the global MuxProvider WebSocket client.
+ +
+
Figure 6 — Sequence: from ao start to a live dashboard render
+
+sequenceDiagram
+  autonumber
+  actor U as User
+  participant CLI as ao start (CLI)
+  participant DASH as Dashboard daemon
+  participant NEXT as Next.js :3000
+  participant WS as mux WS :14801
+  participant API as /api/sessions/patches
+  participant SM as Session Manager
+  participant LM as Lifecycle Manager
+  participant BR as Browser (React + xterm)
+
+  U->>CLI: ao start
+  CLI->>DASH: spawnManagedDaemonChild("dashboard")
+  DASH->>NEXT: start Next.js (PORT)
+  DASH->>WS: start direct-terminal WS (14801)
+  CLI->>LM: start poll loop (30s)
+  loop every 30s
+    LM->>SM: list() + enrich PRs
+    SM-->>LM: sessions (canonical lifecycle)
+    LM->>LM: determineStatus → reactions
+  end
+  U->>BR: open http://localhost:3000
+  BR->>NEXT: SSR initial page
+  BR->>WS: connect mux, subscribe(sessions,notifications)
+  loop every 3s
+    WS->>API: GET /api/sessions/patches
+    API->>SM: listCached(project)
+    SM-->>API: SessionPatch[]
+    API-->>WS: patches
+    WS-->>BR: {ch:sessions, snapshot}
+  end
+  BR->>WS: {ch:terminal, type:open, tmuxName}
+  WS->>WS: node-pty ⇄ tmux attach-session
+  WS-->>BR: {ch:terminal, type:data} (stream)
+  BR->>BR: xterm renders + kanban updates
+    
+
+
+ + +
+

8Prompt assembly (3 layers)

+

buildPrompt() (packages/core/src/prompt-builder.ts:191) composes a worker agent's system prompt from three layers, returning { systemPrompt, taskPrompt? }:

+ +
+
Figure 7 — 3-layer prompt assembly
+
+flowchart TB
+  L1["Layer 1 — Base prompt
BASE_AGENT_PROMPT (repo) or
BASE_AGENT_PROMPT_NO_REPO · prompt-builder.ts:21
"]:::l + L2["Layer 2 — Config context
project · repo · branch · issue
buildConfigLayer() :111
"]:::l + L3["Layer 3 — User rules
agentRules + agentRulesFile
readUserRules() :162
"]:::l + L1 --> OUT["systemPrompt"]:::o + L2 --> OUT + L3 --> OUT + classDef l fill:#16223a,stroke:#6ea8ff,color:#cfe2ff; + classDef o fill:#11281f,stroke:#4fd1a1,color:#c4f0df; +
+
+ +
    +
  1. Base prompt — system instructions (session lifecycle, ao report commands, git/PR workflow). Two variants: full, or _NO_REPO when the project has no repo configured (prompt-builder.ts:60).
  2. +
  3. Config context — project name/repo/branch/tracker and the task (issue id + pre-fetched issue details), built by buildConfigLayer() (:111). An optional orchestrator back-channel block injects an ao send {sessionId} "…" command (:205).
  4. +
  5. User rules — inline agentRules and/or an external agentRulesFile resolved relative to the project (:162).
  6. +
+

The orchestrator session gets a separate prompt rendered from a markdown template with conditional section blocks (generateOrchestratorPrompt(), packages/core/src/orchestrator-prompt.ts:189) — this is what ao start injects via --append-system-prompt.

+
+ + +
+

9Cross-platform abstractions

+

AO ships first-class on macOS, Linux, and Windows. All OS branching is centralized in packages/core/src/platform.ts (full reference in docs/CROSS_PLATFORM.md).

+ +
The Golden Rule: never write process.platform === "win32" in new code. Use the helpers from @aoagents/ao-core. Inline checks bypass the centralized tests (which mock process.platform) and become silent regressions.
+ + + + + + + + + +
NeedHelperRef
OS checkisWindows() / isMac() / isLinux()platform.ts:15
Default runtime (tmux vs process)getDefaultRuntime():27
Resolve shell (PowerShell vs /bin/sh)getShell():130
Kill process + descendantskillProcessTree(pid, sig?):154
PID listening on a portfindPidByPort(port):189
HOME / SHELL / TMPDIR / PATH / USERgetEnvDefaults():227
+

On Windows the default runtime is process (ConPTY, no tmux); killProcessTree always uses taskkill /F (WM_CLOSE fails for headless processes), while Unix kills the negative-PID process group first. getShell() prefers pwshpowershell.execmd.exe and honors the AO_SHELL escape hatch.

+
+ + +
+

10Code map — where to look

+ + + + + + + + + + + + + + + +
FileResponsibility
core/src/types.tsAll 8 plugin interfaces + Session, PluginModule, manifest
core/src/plugin-registry.tsDiscovery, resolution, instantiation of plugins
core/src/session-manager.tsSession CRUD, spawn, restore, stale-runtime reconciliation
core/src/lifecycle-manager.tsPolling loop, state machine, reactions, probe pipeline
core/src/lifecycle-state.tsCanonical schema + deriveLegacyStatus()
core/src/config.tsYAML config loading + Zod validation
core/src/paths.ts & metadata.tsOn-disk layout + atomic metadata I/O
core/src/prompt-builder.ts3-layer worker prompt assembly
core/src/platform.tsCross-platform helpers (the Golden Rule)
cli/src/commands/start.tsao start/ao stop, daemon spawning, Ctrl+C shutdown
web/server/mux-websocket.tsSessions/terminal/notifications mux WebSocket server
web/src/hooks/useSessionEvents.tsDashboard session-state consumer
web/src/components/Dashboard.tsxKanban board, attention zones
+ + +
+ +
+
+ + + +