diff --git a/frontend/src/landing/content/docs/architecture.mdx b/frontend/src/landing/content/docs/architecture.mdx
new file mode 100644
index 000000000..dacbe9b96
--- /dev/null
+++ b/frontend/src/landing/content/docs/architecture.mdx
@@ -0,0 +1,340 @@
+---
+title: Architecture
+description: How Agent Orchestrator fits together — plugin slots, session lifecycle, event bus, prompt assembly, and activity detection.
+---
+
+Agent Orchestrator (AO) is a Node.js orchestrator that spawns and manages parallel AI coding agents across isolated git worktrees. Every moving part is a plugin; the core provides the state machine, event bus, and prompt assembly that ties them together.
+
+## The 8 Plugin Slots
+
+Each abstraction in AO is a named interface defined in `packages/core/src/types.ts`. Seven of the eight slots are pluggable at runtime; the eighth (Lifecycle) is built into core and cannot be replaced.
+
+| Slot | Default | Purpose | Interface |
+|------|---------|---------|-----------|
+| Runtime | [tmux](/docs/plugins/runtimes/tmux) | Where agent sessions execute (tmux, process, docker, k8s) | `Runtime` |
+| Agent | [claude-code](/docs/plugins/agents/claude-code) | Which AI coding tool is launched | `Agent` |
+| Workspace | [worktree](/docs/plugins/workspaces/worktree) | Code isolation — each session gets its own git worktree or clone | `Workspace` |
+| Tracker | [github](/docs/plugins/trackers/github) | Issue tracking (GitHub Issues, Linear, GitLab) | `Tracker` |
+| SCM | [github](/docs/plugins/scm/github) | PR lifecycle, CI checks, and code reviews | `SCM` |
+| Notifier | [desktop](/docs/plugins/notifiers/desktop) | Push notifications to the human (desktop, Slack, webhook) | `Notifier` |
+| Terminal | [iterm2](/docs/plugins/terminals/iterm2) | How humans view and interact with running sessions | `Terminal` |
+| Lifecycle | core (non-pluggable) | State machine, poll loop, and reaction engine | `LifecycleManager` |
+
+
+The Lifecycle slot is not pluggable. It is instantiated by core and wired to all other plugins automatically. You configure its behaviour (poll interval, reactions, thresholds) through `agent-orchestrator.yaml` rather than by replacing the implementation.
+
+
+---
+
+## Session Status Lifecycle
+
+Every session moves through a well-defined set of statuses. The values are defined by the `SESSION_STATUS` constant in `packages/core/src/types.ts`.
+
+```
+spawning
+ │
+ ▼
+working ──────────────────────────────────────────────► stuck
+ │ ▲
+ ▼ │
+pr_open ──────────────────────────────────────────────► stuck
+ │
+ ├──► ci_failed
+ │
+ ├──► review_pending
+ │
+ ├──► changes_requested
+ │
+ └──► approved
+ │
+ ▼
+ mergeable
+ │
+ ▼
+ merged ──► cleanup ──► done
+```
+
+Terminal statuses (session is dead and will no longer be polled): `killed`, `terminated`, `done`, `cleanup`, `errored`, `merged`.
+
+| Status | Description |
+|--------|-------------|
+| `spawning` | Session is being created — worktree, branch, and tmux window are initialising |
+| `working` | Agent is active; no PR yet |
+| `pr_open` | Agent has pushed a PR; CI and reviews are pending |
+| `ci_failed` | One or more CI checks on the PR are failing |
+| `review_pending` | PR has been submitted for review; waiting for a decision |
+| `changes_requested` | Reviewer(s) have requested changes |
+| `approved` | PR is approved but not yet mergeable (e.g. still behind base) |
+| `mergeable` | PR is approved, CI is green, and it can be merged |
+| `merged` | PR has been merged (terminal) |
+| `cleanup` | Post-merge cleanup in progress (terminal) |
+| `done` | Session completed cleanly (terminal) |
+| `needs_input` | Agent is waiting for a permission prompt or human input |
+| `stuck` | Agent has been idle beyond the configured `agent-stuck` threshold |
+| `errored` | Unexpected error — session is dead (terminal) |
+| `killed` | Session was explicitly killed or the PR was closed (terminal) |
+| `idle` | Agent process is alive but has not produced activity for an extended period |
+| `terminated` | Session was terminated externally (terminal) |
+
+### How transitions are determined
+
+The lifecycle manager calls `determineStatus(session)` on every poll cycle. The logic follows this cascade:
+
+1. **Runtime liveness** — If the runtime reports the session is not alive, return `killed`.
+2. **Agent activity** — `getActivityState()` is called; `waiting_input` maps to `needs_input`, `exited` maps to `killed`, and idle beyond the configured threshold maps to `stuck`.
+3. **PR auto-detection** — If no PR is recorded and the agent has a branch, `scm.detectPR()` is called once per cycle to catch PRs created without a metadata hook.
+4. **PR state** — If a PR exists, the SCM plugin provides CI status, review decision, and merge readiness to determine `ci_failed`, `review_pending`, `changes_requested`, `approved`, `mergeable`, or `merged`.
+5. **Default** — Fall back to `working` (or preserve `stuck`/`needs_input`).
+
+---
+
+## Event Bus
+
+After each status transition, the lifecycle manager constructs a typed `OrchestratorEvent` and fans it out to all configured notifiers and reaction handlers. Events have four priority levels: `urgent`, `action`, `warning`, and `info`.
+
+Priority is inferred by `inferPriority()` in `lifecycle-manager.ts`:
+
+- **urgent** — events containing `stuck`, `needs_input`, or `errored`
+- **action** — events containing `approved`, `ready`, `merged`, or `completed`
+- **warning** — events containing `fail`, `changes_requested`, or `conflicts`
+- **info** — everything else, including all `summary.*` events
+
+| `event.type` | Priority | When emitted |
+|---|---|---|
+| `session.spawned` | info | Session transitions out of `spawning` |
+| `session.working` | info | Session enters `working` |
+| `session.exited` | info | Agent process exits |
+| `session.killed` | info | Session is killed |
+| `session.idle` | info | Session enters `idle` |
+| `session.stuck` | urgent | Session exceeds the `agent-stuck` threshold |
+| `session.needs_input` | urgent | Agent is waiting on a permission prompt |
+| `session.errored` | urgent | Session enters `errored` |
+| `pr.created` | info | Session transitions to `pr_open` |
+| `pr.updated` | info | PR title or state changes |
+| `pr.merged` | action | PR is merged |
+| `pr.closed` | info | PR is closed without merging |
+| `ci.passing` | action | CI checks recover from failing to passing |
+| `ci.failing` | warning | Session enters `ci_failed` |
+| `ci.fix_sent` | info | CI fix message sent to agent |
+| `ci.fix_failed` | warning | CI fix attempt failed |
+| `review.pending` | info | Session enters `review_pending` |
+| `review.approved` | action | Session enters `approved` |
+| `review.changes_requested` | warning | Session enters `changes_requested` |
+| `review.comments_sent` | info | Review comments forwarded to agent |
+| `review.comments_unresolved` | warning | Unresolved review comments still present |
+| `automated_review.found` | warning | Bot/automated review comments detected |
+| `automated_review.fix_sent` | info | Automated review fix sent to agent |
+| `merge.ready` | action | Session enters `mergeable` |
+| `merge.conflicts` | warning | PR has merge conflicts |
+| `merge.completed` | action | Session enters `merged` |
+| `reaction.triggered` | info | A configured reaction fired |
+| `reaction.escalated` | urgent | A reaction exceeded its retry/escalation threshold |
+| `summary.all_complete` | info | All sessions have reached terminal statuses |
+
+For the webhook wire format, see [Webhook Notifier](/docs/plugins/notifiers/webhook). For configuring which events trigger automated reactions, see [Reactions](/docs/configuration/reactions).
+
+---
+
+## Poll Loop
+
+The lifecycle manager runs a recurring poll loop. The default interval is **30 seconds** (configurable via `start(intervalMs)`). Each cycle:
+
+1. Lists all active sessions via `sessionManager.list()`.
+2. **Batch-fetches PR enrichment data** — a single GraphQL query retrieves CI status, review decision, and merge readiness for all open PRs at once, replacing N×3 individual REST calls with one request.
+3. Checks each session concurrently — `checkSession(session)` calls `determineStatus()`, detects transitions, fires events, and evaluates reactions.
+4. Prunes stale tracker entries for sessions that no longer exist.
+5. Checks whether all sessions are complete and fires `summary.all_complete` if so (emitted once per batch, not repeatedly).
+
+The dashboard then receives these state changes via SSE at a 5-second cadence. The poll loop and SSE cadence are independent — the dashboard may show state that is up to 5 seconds behind the last poll cycle.
+
+---
+
+## Prompt Assembly (3 Layers)
+
+Every agent session is launched with a composed prompt built by `buildPrompt()` in `packages/core/src/prompt-builder.ts`. The three layers are always concatenated in order:
+
+### Layer 1 — Base prompt (fixed)
+
+`BASE_AGENT_PROMPT` provides identity, session lifecycle rules, git workflow guidance, and PR best practices. It is identical across all sessions. For projects without a remote repository, a trimmed variant (`BASE_AGENT_PROMPT_NO_REPO`) is used instead — it omits PR and CI instructions that do not apply.
+
+### Layer 2 — Config context (per-project)
+
+Built from the project configuration. Includes:
+
+- Project name and ID
+- Repository (`owner/repo`)
+- Default branch
+- Tracker plugin name
+- Issue ID and issue body (when spawning from a tracker issue)
+- Reaction hints — lists which events will auto-send instructions back to the agent
+
+### Layer 3 — User rules (per-project)
+
+Loaded from `agentRules` (inline string in `agent-orchestrator.yaml`) and/or `agentRulesFile` (path to a file, relative to the project root). Both are concatenated when present. If neither is provided, this layer is omitted.
+
+An explicit `userPrompt` is appended after Layer 3 as "Additional Instructions" — it has the highest precedence and overrides anything above it.
+
+### Orchestrator rules
+
+The `orchestratorRules` field in `ProjectConfig` is reserved for orchestrator-role sessions but is not applied by `buildPrompt()`. Orchestrator sessions receive a completely different prompt generated by `generateOrchestratorPrompt()` — see the next section.
+
+---
+
+## Orchestrator Prompt
+
+Orchestrator sessions do not receive the standard three-layer prompt. Instead, `generateOrchestratorPrompt()` in `packages/core/src/orchestrator-prompt.ts` builds a standalone prompt that provides:
+
+- **Role rules** — read-only investigations only; never own a PR; never use `tmux send-keys` directly; always use `ao send` / `ao spawn` to delegate.
+- **Project info** — name, repo, default branch, session prefix, local path, dashboard port.
+- **Quick-start commands** — `ao status`, `ao spawn`, `ao batch-spawn`, `ao session claim-pr`, `ao send`, `ao open`.
+- **Available `ao` commands table** — full reference adapted to whether a repo is configured.
+- **Session management workflows** — spawning, monitoring, PR takeover, investigation workflow, cleanup.
+- **Dashboard info** — URL and feature summary.
+- **Automated reactions** — lists configured reactions so the orchestrator knows what the system will handle automatically.
+- **Common workflows** — bulk issue processing, handling stuck agents, PR review flow, manual intervention.
+- **Project-specific rules** — content of `orchestratorRules` from `ProjectConfig`, appended last.
+
+For a guide on per-role agents, see [Per-Role Agents](/docs/guides/per-role-agents).
+
+---
+
+## Activity Detection
+
+Every agent plugin must implement `getActivityState(session, readyThresholdMs?)`. This is the most critical method in the agent plugin — the dashboard, lifecycle manager, and stuck-detection all depend on it.
+
+### The 6 activity states
+
+| State | Meaning | When |
+|---|---|---|
+| `active` | Agent is processing — thinking, writing code, running tools | Activity within the last 30 seconds |
+| `ready` | Agent finished its turn and is alive, waiting for input | 30 seconds – 5 minutes since last activity |
+| `idle` | Agent has been quiet for an extended period | More than 5 minutes since last activity (default threshold) |
+| `waiting_input` | Agent is at a permission prompt or asking a question | Permission request detected |
+| `blocked` | Agent hit an error it cannot recover from on its own | Error state detected |
+| `exited` | Agent process is no longer running | `isProcessRunning` returns false |
+
+### The `getActivityState` cascade
+
+Every agent plugin must implement this cascade in order:
+
+```
+1. PROCESS CHECK
+ └─ isProcessRunning() → false → return { state: "exited" }
+
+2. ACTIONABLE STATES
+ └─ checkActivityLogState() → waiting_input or blocked → return immediately
+
+3. NATIVE SIGNAL (agent-specific)
+ └─ session list API, native JSONL timestamp, etc.
+ └─ classify by age: active (<30s) / ready (30s–threshold) / idle (>threshold)
+
+4. JSONL ENTRY FALLBACK (mandatory)
+ └─ getActivityFallbackState(activityResult, activeWindowMs, threshold)
+ └─ age-based decay: active→ready→idle (never promotes)
+ └─ staleness cap: waiting_input/blocked entries expire after 5 minutes
+
+5. Return null only when there is genuinely no data at all
+```
+
+
+Step 4 (the JSONL entry fallback) is mandatory. Skipping it means `getActivityState` returns `null` whenever the native API fails — the dashboard shows no activity state and stuck-detection breaks for the entire session lifetime. This was a real bug in the OpenCode plugin.
+
+
+### Two JSONL patterns
+
+| Pattern | Used by | How it works |
+|---|---|---|
+| **Agent-native JSONL** | Claude Code, Codex | The agent writes its own JSONL with rich state entries (`permission_request`, `tool_call`, `error`, etc.). `getActivityState` reads the last entry and maps it to activity states. |
+| **AO activity JSONL** | Aider, OpenCode, new agents | The agent implements `recordActivity`, which calls `recordTerminalActivity()` → `classifyTerminalActivity()` → `appendActivityEntry()` to write to `{workspacePath}/.ao/activity.jsonl`. `getActivityState` reads from this file. |
+
+### Thresholds
+
+| Constant | Value | Purpose |
+|---|---|---|
+| `DEFAULT_ACTIVE_WINDOW_MS` | 30 seconds | Activity newer than this is `active`; older is `ready` |
+| `DEFAULT_READY_THRESHOLD_MS` | 5 minutes | `ready` sessions older than this become `idle` |
+| `ACTIVITY_INPUT_STALENESS_MS` | 5 minutes | Deprecated compatibility export. `waiting_input` / `blocked` entries no longer expire by wallclock; they persist until process death or a newer entry overrides them. |
+
+---
+
+## PATH Wrappers
+
+When an agent creates a PR or switches a branch, AO needs to update the session metadata (e.g. write `pr=https://...` or `branch=feat/INT-123`) so the dashboard and lifecycle manager stay in sync. Two mechanisms exist:
+
+**Claude Code — PostToolUse hooks**
+
+Claude Code writes `.claude/settings.json` with a `PostToolUse` hook that fires after every `gh pr create` or `git checkout` command. The hook script calls `update_ao_metadata` directly.
+
+**All other agents — PATH wrappers**
+
+Agents without a native hook system (Codex, Aider, OpenCode, custom agents) use `~/.ao/bin/gh` and `~/.ao/bin/git` shell wrappers. These wrappers are installed to `~/.ao/bin/` by `setupPathWrapperWorkspace(workspacePath)` from `packages/core/src/agent-workspace-hooks.ts`. The function also writes session context to `{workspacePath}/.ao/AGENTS.md` (gitignored — does not touch tracked files).
+
+The wrappers intercept:
+- `gh pr create` — captures the PR URL from stdout and writes `pr=` and `status=pr_open`
+- `gh pr merge` — writes `status=merged`
+- `git checkout -b ` / `git switch -c ` — writes `branch=`
+
+All other commands pass through transparently via `exec "$real_gh" "$@"` or `exec "$real_git" "$@"`.
+
+For the user-facing storage summary, see [Configuration — Where data lives](/docs/configuration#where-data-lives).
+
+---
+
+## Observability
+
+The lifecycle manager, session manager, and plugin registry emit structured telemetry using project observers created by `createProjectObserver()`. Each running process writes a JSON snapshot to:
+
+```
+~/.agent-orchestrator/{hash}-observability/processes/{component}-{pid}.json
+```
+
+The hash is the first 12 characters of the SHA-256 of the config directory path. The `{component}` segment matches the internal observer name (e.g. `lifecycle-manager`, `session-manager`).
+
+The dashboard's `/api/observability` route reads and merges these per-process snapshots to produce a live observability view.
+
+**Feedback reports** from the agent's `bug_report` and `improvement_suggestion` tools are written as flat key-value files at:
+
+```
+~/.agent-orchestrator/{hash}-{projectId}/feedback-reports/*.kv
+```
+
+---
+
+## Data Flow Summary
+
+```
+agent-orchestrator.yaml ──► Config Loader (Zod) ──► Plugin Registry
+ │
+ ┌───────────────┘
+ │
+ ▼
+ Session Manager ◄─── ao spawn / ao session
+ │
+ ▼
+ Lifecycle Manager ────► Events ────► Notifiers
+ │ │ │
+ │ Reactions Webhook
+ │
+ ▼
+ Dashboard API
+ (Next.js App Router)
+ │
+ ┌───────────┴──────────────┐
+ │ │
+ ▼ ▼
+ SSE (5s) WebSocket (terminal)
+ │ │
+ ▼ ▼
+ React UI xterm.js
+```
+
+---
+
+## Next Steps
+
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/changelog.mdx b/frontend/src/landing/content/docs/changelog.mdx
new file mode 100644
index 000000000..f41f38f9f
--- /dev/null
+++ b/frontend/src/landing/content/docs/changelog.mdx
@@ -0,0 +1,31 @@
+---
+title: Changelog
+description: Release notes for AO, including new features, improvements, and fixes by version.
+---
+
+Release notes for AO, including new features, improvements, and fixes by version.
+
+This page is maintained from weekly release notes. Run `ao --version` to check your installed version.
+
+## 0.3.0 "The Rebuild"
+
+April 25, 2026
+
+- Shipped a dashboard rebuild from the ground up with the Warm Terminal design system, a 10x JavaScript bundle reduction, loading and not-found pages, a terminal layout overhaul, mobile UX, sidebar navigation, and instant session switching.
+- Added the multi-project dashboard: one unified view across all configured projects, with portfolio registration, migration flows, project settings, and CLI alignment with the multi-project registry.
+- Launched the website and docs at [ao-agents.com](https://ao-agents.com), with setup guides, API reference material, and plugin documentation in one place.
+- Added Cursor as a first-class agent alongside Claude Code, Codex, Aider, OpenCode, and Hermes, using the same worktrees and PR workflow.
+- Redesigned session lifecycle reporting and UI flow. Session titles are stable through pinned summary metadata, display names are derived from task context, and sessions auto-terminate after PR merge.
+- Promoted Windows to first-class support with cross-platform shell resolution, PID checks, process management, and symlink handling.
+- Hardened the CLI with install-aware `ao update`, startup update notifications, doctor version checks, JSON output for `ao session ls`, config error handling, version mismatch fixes, and default filtering for terminated sessions.
+- Moved terminals and sessions onto a single multiplexed WebSocket connection, reducing connection churn and resource leaks.
+- Added prompt-driven spawn: `ao spawn` can now create sessions from freeform tasks without requiring a tracker issue.
+- Added macOS idle sleep prevention while agents are running, reducing silent CI drops during long sessions.
+- Resolved a critical SSRF vulnerability by overriding Axios to `>=1.15.0` and matched the Gitleaks binary for arm64 runners.
+- Renamed all packages from `@composio` to `@aoagents` for a cleaner ecosystem identity.
+
+## Sources
+
+- [Package changelogs on GitHub](https://github.com/ComposioHQ/agent-orchestrator/tree/main/packages)
+- [GitHub Releases](https://github.com/ComposioHQ/agent-orchestrator/releases)
+- [`main` commit history](https://github.com/ComposioHQ/agent-orchestrator/commits/main)
diff --git a/frontend/src/landing/content/docs/cli.mdx b/frontend/src/landing/content/docs/cli.mdx
new file mode 100644
index 000000000..b825c57ed
--- /dev/null
+++ b/frontend/src/landing/content/docs/cli.mdx
@@ -0,0 +1,379 @@
+---
+title: CLI reference
+description: Every `ao` command, every flag, drawn from the source.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+All commands support `--help`. Use that in place of re-reading this page.
+
+## Command tree
+
+```
+ao
+├── start [project] Start orchestrator + dashboard (auto-creates config on first run)
+├── stop [project] Stop orchestrator + dashboard
+├── status Show all sessions with PR and CI status
+├── spawn [issue] Spawn a single agent session
+├── batch-spawn Spawn sessions for multiple issues, with duplicate detection
+├── send [message…] Send a message to a session with busy detection
+├── review-check [project] Check PRs for review comments and trigger agents
+├── dashboard Start the web dashboard on its own
+├── open [target] Open session(s) in terminal tabs
+├── verify [issue] Mark an issue as verified (or failed) after staging check
+├── doctor Run install, environment, and runtime health checks
+├── update Check for updates and upgrade AO
+├── init (deprecated — use ao start)
+├── config-help Print the config schema and a guide
+├── notify test
+├── session ls, attach, kill, cleanup, restore, claim-pr, remap
+├── setup dashboard, desktop, webhook, slack, discord, composio, openclaw
+└── plugin list, search, create, install, update, uninstall
+```
+
+## Core commands
+
+### `ao start [project]`
+
+> Start orchestrator agent and dashboard (auto-creates config on first run, adds projects by path/URL)
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--no-dashboard` | — | Skip starting the dashboard server |
+| `--no-orchestrator` | — | Skip starting the orchestrator agent |
+| `--rebuild` | — | Clean and rebuild the dashboard before starting |
+| `--dev` | — | Use Next.js dev server with hot reload (monorepo only; ignored in npm installs) |
+| `--interactive` | — | Prompt to configure config settings |
+
+Positional `[project]` — project id, local path, or repo URL. If omitted and no config exists in `cwd`, AO creates `agent-orchestrator.yaml`.
+
+If AO is already running, you'll get an interactive prompt to open/restart/spawn-new/quit. Non-TTY callers get info + `process.exit(0)`.
+
+### `ao stop [project]`
+
+> Stop orchestrator agent and dashboard
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--purge-session` | — | Delete the mapped OpenCode session when stopping |
+| `--all` | — | Stop all running AO instances on this machine |
+
+### `ao status`
+
+> Show all sessions with branch, activity, PR, and CI status
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-p, --project ` | — | Filter to one project |
+| `--json` | — | Output JSON |
+| `-w, --watch` | — | Refresh continuously |
+| `--interval ` | `5` | Refresh interval |
+
+`--watch` and `--json` are mutually exclusive. Falls back to tmux session discovery if no config is found.
+
+### `ao spawn [issue]`
+
+> Spawn a single agent session
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--open` | — | Open the session in a terminal tab |
+| `--agent ` | config default | Override the agent plugin (`claude-code`, `codex`, `cursor`, `aider`, `opencode`) |
+| `--claim-pr ` | — | Immediately claim an existing PR for the spawned session |
+| `--assign-on-github` | — | Assign the claimed PR to the authenticated GitHub user |
+| `--prompt ` | — | Use an inline prompt instead of an issue (max 4096 chars, newlines stripped) |
+
+Positional `[first]` — issue identifier. The project is auto-detected from `cwd`.
+
+
+ The old two-arg form `ao spawn <project> <issue>` is rejected with an error. Use `-p` or run from inside a worktree.
+
+
+### `ao batch-spawn `
+
+> Spawn sessions for multiple issues with duplicate detection
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--open` | — | Open sessions in terminal tabs |
+
+Skips duplicates within the batch and any issues that already have an active session.
+
+### `ao send [message...]`
+
+> Send a message to a session with busy detection and retry
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-f, --file ` | — | Send contents of a file instead of an inline message |
+| `--no-wait` | — | Don't wait for the session to become idle before sending |
+| `--timeout ` | `600` | Max seconds to wait for idle |
+
+Positional: `` required, `[message...]` variadic. One of inline message or `--file` is required. Long messages (>200 chars or multiline) go via tmux paste-buffer + temp file.
+
+### `ao review-check [project]`
+
+> Check PRs for review comments and trigger agents to address them
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--dry-run` | — | Show what would happen; don't nudge agents |
+
+Checks all projects if `[project]` is omitted.
+
+### `ao dashboard`
+
+> Start the web dashboard
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-p, --port ` | config `port` or `3000` | Port to listen on |
+| `--no-open` | — | Don't open the browser |
+| `--rebuild` | — | Clean stale Next.js artifacts and rebuild |
+
+If the build looks stale you'll see a suggestion to run `ao dashboard --rebuild`.
+
+### `ao open [target]`
+
+> Open session(s) in terminal tabs
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-w, --new-window` | — | Open in a new terminal window instead of a tab |
+
+Positional `[target]` — session name, project id, or `"all"`. Defaults to all sessions. Falls back to printing the dashboard URL when the native terminal helper isn't available (anywhere but macOS + iTerm2).
+
+### `ao verify [issue]`
+
+> Mark an issue as verified (or failed) after checking the fix on staging
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-p, --project ` | — | Required if multiple projects |
+| `--fail` | — | Mark as failed instead of passing |
+| `-c, --comment ` | — | Custom comment to add |
+| `-l, --list` | — | List all issues with the merged-unverified label |
+
+`[issue]` is required unless `--list` is passed.
+
+### `ao doctor`
+
+> Run install, environment, and runtime health checks
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--fix` | — | Apply safe fixes for launcher and stale-temp issues |
+| `--test-notify` | — | Send a test notification through each configured notifier |
+
+Runs `ao-doctor.sh` for shell-level checks, then TypeScript checks (version freshness, plugin resolution, notifier connectivity). OpenClaw is the only notifier probed without side effects.
+
+### `ao update`
+
+> Check for updates and upgrade AO to the latest version
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--skip-smoke` | — | Skip smoke tests after rebuilding (git installs only) |
+| `--smoke-only` | — | Run smoke tests without fetching or rebuilding (git installs only) |
+| `--check` | — | Print version info as JSON; don't upgrade |
+
+Behavior depends on how AO was installed — git, npm-global, pnpm-global, or unknown (manual instructions printed).
+
+### `ao config-help`
+
+> Show config schema and guide for creating `agent-orchestrator.yaml`
+
+No flags.
+
+## Notify subcommands
+
+### `ao notify test`
+
+> Send a manual demo notification without spawning sessions
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--template ` | `basic` | Demo template to send |
+| `--to ` | — | Comma-separated notifier refs to target |
+| `--all` | — | Send to all configured, default, and routed notifier refs |
+| `--route ` | — | Send through `urgent`, `action`, `warning`, or `info` routing |
+| `--actions` | — | Include demo actions when supported |
+| `--message ` | — | Override the notification message |
+| `--dry-run` | — | Resolve targets without sending |
+| `--json` | — | Print structured JSON output |
+| `--sink [port]` | — | Add a local webhook target named `sink` |
+
+## Session subcommands
+
+### `ao session ls`
+
+> List all sessions
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-p, --project ` | — | Filter to one project |
+| `-a, --all` | — | Include orchestrator sessions |
+| `--json` | — | Output JSON |
+
+### `ao session attach `
+
+> Attach to a session's tmux window
+
+No flags. Attaches via `tmux attach-session`. Only meaningful with `runtime: tmux`.
+
+### `ao session kill `
+
+> Kill a session and remove its worktree
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--purge-session` | — | Delete the mapped OpenCode session |
+
+### `ao session cleanup`
+
+> Kill sessions where PR is merged or issue is closed
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-p, --project ` | — | Filter to one project |
+| `--dry-run` | — | Show what would be cleaned up |
+
+### `ao session claim-pr [session]`
+
+> Attach an existing PR to a session
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--assign-on-github` | — | Assign the PR to the authenticated user on GitHub |
+
+If `[session]` is omitted, falls back to `AO_SESSION_NAME` / `AO_SESSION` env vars.
+
+### `ao session restore `
+
+> Restore a terminated or crashed session in-place
+
+No flags. Relaunches the agent inside the same worktree and rewires session metadata.
+
+### `ao session remap `
+
+> Re-discover and persist OpenCode session mapping for an AO session
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `-f, --force` | — | Override a stale mapping |
+
+## Setup subcommands
+
+Notifier setup commands that write AO config support `--routing-preset `.
+Valid presets are `urgent-only`, `urgent-action`, and `all`. In interactive
+mode, AO asks which notification priorities the notifier should receive before
+writing `notificationRouting`.
+
+| Command | Purpose |
+|---|---|
+| `ao setup dashboard` | Configure dashboard notification retention and routing |
+| `ao setup desktop` | Configure native desktop notifications |
+| `ao setup webhook` | Configure a generic HTTP webhook |
+| `ao setup slack` | Configure a Slack incoming webhook |
+| `ao setup discord` | Configure a Discord incoming webhook |
+| `ao setup composio` | Open the interactive Composio setup hub |
+| `ao setup composio-slack` | Configure Slack through Composio |
+| `ao setup composio-discord` | Configure Discord webhook mode through Composio |
+| `ao setup composio-discord-bot` | Configure Discord bot mode through Composio |
+| `ao setup composio-mail` | Configure Gmail through Composio |
+| `ao setup openclaw` | Configure OpenClaw gateway notifications |
+
+### `ao setup openclaw`
+
+> Connect AO notifications to an OpenClaw gateway
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--url ` | — | OpenClaw webhook URL |
+| `--token ` | — | Remote/manual fallback; local setup should read `hooks.token` from OpenClaw config |
+| `--openclaw-config-path ` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
+| `--routing-preset ` | — | `urgent-only` · `urgent-action` · `all` |
+| `--refresh` | — | Reuse existing values and rewrite AO config |
+| `--no-test` | — | Skip the setup token probe |
+| `--force` | — | Replace a conflicting `notifiers.openclaw` entry |
+| `--status` | — | Show OpenClaw config, gateway state, and token probe status |
+| `--non-interactive` | — | Skip prompts — auto-detect OpenClaw on localhost |
+
+Interactive mode (TTY) uses `@clack/prompts` with review/change steps for the gateway URL, OpenClaw config path, and routing preset. OpenClaw owns the token in `hooks.token`; AO does not generate it or write shell-profile exports.
+
+## Plugin subcommands
+
+### `ao plugin list`
+
+> List bundled marketplace plugins
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--installed` | — | Only show plugins present in your current config |
+| `--type ` | — | Filter by slot: `agent`, `runtime`, `tracker`, `scm`, `notifier`, `workspace`, `terminal` |
+| `--refresh` | — | Re-fetch the marketplace catalog |
+
+### `ao plugin search `
+
+> Search the bundled marketplace catalog
+
+No flags.
+
+### `ao plugin create [directory]`
+
+> Scaffold a new AO plugin package
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--name ` | prompt | Plugin name |
+| `--slot ` | prompt | Which slot it implements |
+| `--description ` | prompt | Manifest description |
+| `--author ` | prompt | Package author |
+| `--package-name ` | derived | npm package name |
+| `--non-interactive` | — | Use all flags without prompting |
+
+### `ao plugin install `
+
+> Install a plugin into the current config
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--url ` | — | Passed to `ao setup openclaw` when installing `notifier-openclaw` |
+| `--token ` | — | Remote/manual OpenClaw token fallback |
+| `--openclaw-config-path ` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
+
+`` is a marketplace id, npm package name, or local path.
+
+### `ao plugin update [reference]`
+
+> Update installer-managed plugins in the current config
+
+| Flag | Default | Purpose |
+|---|---|---|
+| `--all` | — | Update all managed plugins |
+
+Requires either `[reference]` or `--all`.
+
+### `ao plugin uninstall `
+
+> Remove a plugin from the current config
+
+No flags.
+
+## Environment
+
+A few env vars affect every command:
+
+| Variable | Purpose |
+|---|---|
+| `AO_DATA_DIR` | Override the `~/.agent-orchestrator` base directory. |
+| `AO_LOG_LEVEL` | `error` · `warn` · `info` · `debug` · `trace` |
+| `AO_CONFIG_PATH` | Absolute path to a specific `agent-orchestrator.yaml`. Overrides CWD-based search. |
+| `AO_SESSION_ID` · `AO_SESSION_NAME` | Auto-set inside spawned sessions; used by `claim-pr` default |
+| `AO_PROJECT_ID` | Set by AO inside spawned sessions. Used by `ao spawn` to resolve the active project. |
+| `PORT` | Next.js dashboard port. Overrides `config.port`. |
+| `TERMINAL_PORT` | WebSocket mux server port (default 14800). |
+| `DIRECT_TERMINAL_PORT` | Direct PTY WebSocket port (default 14801). |
+| `NEXT_PUBLIC_TERMINAL_WS_PATH` | Override the WebSocket path used by the terminal client (for reverse-proxy setups). |
+
+Agent-specific env vars are documented on each [agent plugin's page](/docs/plugins/agents).
diff --git a/frontend/src/landing/content/docs/configuration/index.mdx b/frontend/src/landing/content/docs/configuration/index.mdx
new file mode 100644
index 000000000..bb41fdfc9
--- /dev/null
+++ b/frontend/src/landing/content/docs/configuration/index.mdx
@@ -0,0 +1,178 @@
+---
+title: Configuration
+description: How AO stores configuration, what belongs in the global registry, and what you should edit per project.
+---
+
+AO uses configuration in two layers:
+
+- **Global registry** — remembers which projects AO knows about and which repository each project points to.
+- **Local project config** — controls behavior for one project: agent, runtime, workspace, rules, reactions, tracker, SCM, and setup commands.
+
+Most users should let `ao start` create the registry entry, then edit the local `agent-orchestrator.yaml` in the project when they want to change behavior.
+
+
+The old wrapped format with a top-level `projects:` block is still understood for compatibility, but new project-local configs should be flat. Do not put identity fields like `path`, `projectId`, `storageKey`, or `originUrl` in a local project config.
+
+
+## What AO Creates
+
+When you run AO in a repository, it can register the project globally and create a local config file:
+
+```bash
+ao start
+```
+
+The global registry lives at:
+
+```text
+~/.agent-orchestrator/config.yaml
+```
+
+That file is AO-owned. It stores durable identity:
+
+```yaml title="~/.agent-orchestrator/config.yaml"
+projects:
+ my-app:
+ projectId: my-app
+ path: /Users/me/code/my-app
+ storageKey: generated-by-ao
+ repo:
+ owner: acme
+ name: my-app
+ platform: github
+ originUrl: git@github.com:acme/my-app.git
+ defaultBranch: main
+ sessionPrefix: app
+```
+
+The local project config lives inside the repository:
+
+```text
+my-app/agent-orchestrator.yaml
+```
+
+That file is for behavior:
+
+```yaml title="agent-orchestrator.yaml"
+agent: claude-code
+runtime: tmux
+workspace: worktree
+
+symlinks:
+ - .env
+ - .claude
+
+postCreate:
+ - pnpm install
+
+agentRules: |
+ Run tests before pushing.
+ Keep PRs small and focused.
+```
+
+## What To Edit
+
+| Goal | Edit |
+|------|------|
+| Change the default agent or model | Local `agent-orchestrator.yaml` |
+| Add `.env` or tool config files to each worktree | Local `symlinks` |
+| Run setup commands before the agent starts | Local `postCreate` |
+| Disable or tune automated recovery | Local or global `reactions` |
+| Change notification routing for all projects | Global registry |
+| Rename, remove, or relink a registered project | Use AO commands/dashboard project settings |
+| Change where AO stores sessions | Do not hand-edit; re-register/relink if needed |
+
+Do not manually edit `storageKey`, `path`, or `originUrl` unless you are repairing a broken registry entry and already know why it is wrong.
+
+## Local Project Config
+
+A practical local config usually starts with the agent, runtime, workspace, setup, and rules:
+
+```yaml title="agent-orchestrator.yaml"
+agent: claude-code # claude-code | codex | aider | opencode
+runtime: tmux # tmux | process
+workspace: worktree # worktree | clone
+
+agentConfig:
+ permissions: permissionless
+ model: claude-sonnet-4-5
+
+symlinks:
+ - .env
+
+postCreate:
+ - pnpm install
+
+agentRules: |
+ Use pnpm, not npm.
+ Run pnpm test before opening a PR.
+```
+
+See [Projects](/docs/configuration/projects) for the full project behavior reference.
+
+## Global Defaults
+
+You can set defaults that apply across all registered projects in the global registry:
+
+```yaml title="~/.agent-orchestrator/config.yaml"
+defaults:
+ agent: claude-code
+ runtime: tmux
+ workspace: worktree
+ notifiers: [desktop]
+
+notificationRouting:
+ urgent: [desktop, composio]
+ action: [desktop]
+ warning: [composio]
+ info: [composio]
+```
+
+Project-local values override global defaults.
+
+## Config Lookup
+
+AO resolves configuration in this order:
+
+1. `AO_CONFIG_PATH`, if set.
+2. `agent-orchestrator.yaml` or `agent-orchestrator.yml` found by walking upward from the current directory.
+3. The global registry path.
+4. Legacy home-directory fallbacks:
+ - `~/.agent-orchestrator.yaml`
+ - `~/.agent-orchestrator.yml`
+ - `~/.config/agent-orchestrator/config.yaml`
+
+For normal use, run AO from inside the repository and keep `agent-orchestrator.yaml` in the repo root.
+
+## Top-Level Global Keys
+
+| Key | Default | Purpose |
+|-----|---------|---------|
+| `port` | `3000` | Dashboard HTTP port |
+| `terminalPort` | auto from `14800` | tmux terminal WebSocket port |
+| `directTerminalPort` | auto from `14801` | direct PTY WebSocket port |
+| `readyThresholdMs` | `300000` | Time before a ready session is treated as idle |
+| `defaults` | built-ins | Cross-project defaults for agent/runtime/workspace/notifiers |
+| `projects` | `{}` | Registered project identities |
+| `projectOrder` | unset | Optional sidebar/project ordering |
+| `notifiers` | `{}` | Named notifier instances |
+| `notificationRouting` | desktop/composio defaults | Priority-to-notifier routing |
+| `reactions` | built-in defaults | Global automation rules |
+
+## Where Data Lives
+
+AO stores runtime data under `~/.agent-orchestrator/` by storage key. Session files, archived sessions, feedback reports, and observability snapshots are plain files so they can be inspected during debugging.
+
+You usually do not need to configure this. If you need to debug a session, start with:
+
+```bash
+ls ~/.agent-orchestrator
+```
+
+## Next Steps
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/configuration/meta.json b/frontend/src/landing/content/docs/configuration/meta.json
new file mode 100644
index 000000000..717d52ac3
--- /dev/null
+++ b/frontend/src/landing/content/docs/configuration/meta.json
@@ -0,0 +1,5 @@
+{
+ "title": "Configuration",
+ "defaultOpen": false,
+ "pages": ["projects", "reactions", "remote-access"]
+}
diff --git a/frontend/src/landing/content/docs/configuration/projects.mdx b/frontend/src/landing/content/docs/configuration/projects.mdx
new file mode 100644
index 000000000..77f99a1ca
--- /dev/null
+++ b/frontend/src/landing/content/docs/configuration/projects.mdx
@@ -0,0 +1,298 @@
+---
+title: Projects
+description: "Configure one AO project: agent, runtime, workspace, setup commands, rules, tracker, SCM, and per-role overrides."
+---
+
+A project is one repository that AO can run agents against. The global registry remembers the project identity; the local `agent-orchestrator.yaml` controls how sessions behave inside that repo.
+
+Most teams only need these fields:
+
+```yaml title="agent-orchestrator.yaml"
+agent: claude-code
+runtime: tmux
+workspace: worktree
+
+symlinks:
+ - .env
+
+postCreate:
+ - pnpm install
+
+agentRules: |
+ Use pnpm.
+ Run pnpm test before pushing.
+ Keep PRs focused.
+```
+
+## Choose The Agent
+
+`agent` selects the coding agent AO launches for worker sessions.
+
+```yaml
+agent: claude-code
+```
+
+Built-in agents:
+
+| Agent | Use when |
+|-------|----------|
+| `claude-code` | You want the default, most tested path |
+| `codex` | You want OpenAI Codex CLI sessions |
+| `aider` | You already use Aider workflows |
+| `opencode` | You want OpenCode session discovery and resume support |
+
+Agent-specific settings go under `agentConfig`:
+
+```yaml
+agentConfig:
+ permissions: permissionless
+ model: claude-sonnet-4-5
+```
+
+`permissions` accepts:
+
+| Value | Behavior |
+|-------|----------|
+| `permissionless` | Let the agent edit and run commands without prompting |
+| `default` | Use the agent tool's normal permission behavior |
+| `auto-edit` | Auto-approve edits, ask for other actions |
+| `suggest` | Suggest changes without applying them |
+
+The legacy value `skip` is accepted and treated as `permissionless`.
+
+## Choose The Runtime
+
+`runtime` controls where the agent process runs.
+
+```yaml
+runtime: tmux
+```
+
+| Runtime | Use when |
+|---------|----------|
+| `tmux` | You want persistent sessions that survive dashboard reloads and can be attached from a terminal |
+| `process` | You want a lighter direct process runtime and do not need tmux persistence |
+
+Most users should keep `tmux`.
+
+## Choose The Workspace
+
+`workspace` controls how AO isolates each session's code.
+
+```yaml
+workspace: worktree
+```
+
+| Workspace | Use when |
+|-----------|----------|
+| `worktree` | Default. Fast, disk-efficient, creates a git worktree per session |
+| `clone` | Slower, but gives each session a separate clone |
+
+Use `worktree` unless your repository has tooling that behaves badly with git worktrees.
+
+## Prepare Each Session
+
+Use `symlinks` for files agents need but should not be copied or committed:
+
+```yaml
+symlinks:
+ - .env
+ - .claude
+```
+
+Paths are relative to the project root. Missing paths are skipped with a warning.
+
+Use `postCreate` for setup commands that must run inside each new workspace:
+
+```yaml
+postCreate:
+ - pnpm install
+ - cp .env.example .env
+```
+
+If a `postCreate` command fails, AO does not start the agent for that session. Keep these commands deterministic.
+
+## Give Agents Project Rules
+
+Use `agentRules` for short project-specific instructions:
+
+```yaml
+agentRules: |
+ Use conventional commits.
+ Do not touch database migrations unless the issue asks for it.
+ Run pnpm lint and pnpm test before pushing.
+```
+
+Use `agentRulesFile` when the rules are long or already versioned:
+
+```yaml
+agentRulesFile: AGENTS.md
+```
+
+If both are set, AO includes both.
+
+Use `orchestratorRules` for instructions that only apply to the orchestrator session:
+
+```yaml
+orchestratorRules: |
+ Split large issues into small worker tasks.
+ Review worker output before asking for a merge.
+```
+
+## Split Orchestrator And Worker Roles
+
+You can run one agent/model for orchestration and another for implementation:
+
+```yaml
+orchestrator:
+ agent: claude-code
+ agentConfig:
+ model: claude-opus-4
+
+worker:
+ agent: codex
+ agentConfig:
+ model: gpt-5.4
+ permissions: permissionless
+```
+
+Use this when planning/review needs a stronger model but routine implementation can use a faster or cheaper worker.
+
+## Tracker And SCM
+
+AO usually infers tracker and SCM from the registered repository. Override them only when needed.
+
+```yaml
+tracker:
+ plugin: github
+
+scm:
+ plugin: github
+```
+
+Built-in trackers:
+
+| Plugin | Purpose |
+|--------|---------|
+| `github` | GitHub issues |
+| `gitlab` | GitLab issues |
+| `linear` | Linear issues |
+
+Built-in SCM plugins:
+
+| Plugin | Purpose |
+|--------|---------|
+| `github` | GitHub PRs, checks, reviews, merge state |
+| `gitlab` | GitLab merge requests |
+
+Extra keys under `tracker` and `scm` are passed to the plugin:
+
+```yaml
+tracker:
+ plugin: linear
+ teamId: ENG
+
+scm:
+ plugin: github
+ webhook:
+ enabled: true
+ path: /api/webhooks/github
+ secretEnvVar: GITHUB_WEBHOOK_SECRET
+```
+
+For external plugins, use `package` or `path`:
+
+```yaml
+tracker:
+ package: "@acme/ao-plugin-tracker-jira"
+ projectKey: APP
+```
+
+## Tune Automation Per Project
+
+Project-level reactions override global reaction settings:
+
+```yaml
+reactions:
+ ci-failed:
+ retries: 3
+ approved-and-green:
+ auto: false
+```
+
+See [Reactions](/docs/configuration/reactions) for the event list and action behavior.
+
+## Session Recovery
+
+Use these only when you need explicit recovery behavior.
+
+```yaml
+orchestratorSessionStrategy: reuse
+opencodeIssueSessionStrategy: reuse
+```
+
+`orchestratorSessionStrategy` accepts:
+
+| Value | Behavior |
+|-------|----------|
+| `reuse` | Attach to the existing orchestrator session |
+| `delete` | Delete the old session and start a new one |
+| `ignore` | Leave the old session and start another |
+| `delete-new` | Delete any newly detected duplicate |
+| `ignore-new` | Ignore any newly detected duplicate |
+| `kill-previous` | Kill the previous session before starting the new one |
+
+`opencodeIssueSessionStrategy` accepts `reuse`, `delete`, or `ignore`.
+
+## Local Reference
+
+These fields are valid in a local project config:
+
+| Field | Type | Purpose |
+|-------|------|---------|
+| `repo` | `string` | Optional legacy/local repo slug |
+| `defaultBranch` | `string` | Branch PRs target, usually `main` |
+| `agent` | `string` | Default worker agent |
+| `runtime` | `string` | Runtime plugin |
+| `workspace` | `string` | Workspace plugin |
+| `tracker` | `object` | Issue tracker plugin config |
+| `scm` | `object` | Source control plugin config |
+| `symlinks` | `string[]` | Files/directories linked into each workspace |
+| `postCreate` | `string[]` | Commands run after workspace creation |
+| `agentConfig` | `object` | Agent permissions/model/options |
+| `orchestrator` | `object` | Orchestrator role override |
+| `worker` | `object` | Worker role override |
+| `reactions` | `object` | Per-project automation overrides |
+| `agentRules` | `string` | Inline worker instructions |
+| `agentRulesFile` | `string` | Path to a rules file |
+| `orchestratorRules` | `string` | Orchestrator-only instructions |
+| `orchestratorSessionStrategy` | `string` | Duplicate orchestrator recovery behavior |
+| `opencodeIssueSessionStrategy` | `string` | Duplicate OpenCode issue-session behavior |
+| `decomposer` | `object` | Advanced decomposition settings |
+
+Identity fields such as `projectId`, `path`, `storageKey`, `originUrl`, and `sessionPrefix` belong to the global registry, not the local config.
+
+## Common Problems
+
+**The project does not appear in the dashboard**
+Run `ao start` from the repository root so AO can register the project. If the repo moved, remove and re-add or relink the project instead of editing `storageKey` manually.
+
+**The agent starts without environment variables**
+Add `.env` or the relevant tool config directory to `symlinks`. AO does not copy secrets into worktrees by default.
+
+**Setup fails before the agent starts**
+Check `postCreate`. A failing command stops the session before the agent launches.
+
+**Two projects get confusing session names**
+Set `sessionPrefix` in the global project registry or through project registration/settings. Session prefixes must use letters, numbers, underscores, or hyphens.
+
+**GitHub or GitLab calls fail**
+Make sure the corresponding CLI or token is authenticated for the plugin you use. AO does not store provider tokens in project config.
+
+## Next Steps
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/configuration/reactions.mdx b/frontend/src/landing/content/docs/configuration/reactions.mdx
new file mode 100644
index 000000000..7453fc53e
--- /dev/null
+++ b/frontend/src/landing/content/docs/configuration/reactions.mdx
@@ -0,0 +1,193 @@
+---
+title: Reactions
+description: Configure what AO does when CI fails, reviews arrive, agents get stuck, or PRs become ready.
+---
+
+Reactions are AO's automatic responses to session events. They decide whether AO should message an agent, notify a human, or mark a PR as ready for a merge action.
+
+You do not need to configure reactions to start. AO ships with defaults for CI failures, review comments, merge conflicts, stuck agents, input prompts, and completed sessions.
+
+## Common Recipes
+
+### Watch Without Auto-Recovery
+
+Use this while onboarding AO to a sensitive repo. You still see notifications, but AO stops sending recovery prompts to agents automatically.
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ ci-failed:
+ auto: false
+ changes-requested:
+ auto: false
+ bugbot-comments:
+ auto: false
+ merge-conflicts:
+ auto: false
+```
+
+### Give CI Recovery More Attempts
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ ci-failed:
+ retries: 3
+ message: |
+ CI is failing. Read the failing check logs, reproduce locally when possible,
+ fix the smallest cause, and push the correction.
+```
+
+### Escalate Stuck Agents Faster
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ agent-stuck:
+ priority: urgent
+ threshold: "7m"
+```
+
+### Notify On Merge-Ready PRs
+
+`approved-and-green` does not merge by default. It notifies you that the PR is ready.
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ approved-and-green:
+ auto: false
+ action: notify
+ priority: action
+```
+
+
+`auto-merge` is currently an intent flag, not a bypass. AO does not ignore branch protection, reviews, or failing checks.
+
+
+## Default Reactions
+
+| Key | When it fires | Default action | Notes |
+|-----|---------------|----------------|-------|
+| `ci-failed` | CI check fails | `send-to-agent` | Sends the agent a recovery prompt |
+| `changes-requested` | Human requests changes | `send-to-agent` | Sends review feedback to the agent |
+| `bugbot-comments` | Known automation leaves actionable feedback | `send-to-agent` | Keeps bot feedback separate from human reviews |
+| `merge-conflicts` | PR has conflicts | `send-to-agent` | Asks the agent to resolve conflicts |
+| `approved-and-green` | PR is approved and checks pass | `notify` | Does not merge unless future SCM support enables it |
+| `agent-stuck` | Agent has been inactive past threshold | `notify` | Urgent by default |
+| `agent-needs-input` | Agent is waiting for human input | `notify` | Urgent by default |
+| `agent-exited` | Session exits unexpectedly or is killed | `notify` | Urgent by default |
+| `all-complete` | Session finishes cleanly | `notify` | Includes summary by default |
+| `agent-idle` | Reserved | `send-to-agent` | Defined for future idle handling |
+
+## Action Types
+
+| Action | Meaning |
+|--------|---------|
+| `send-to-agent` | Sends `message` into the running agent session |
+| `notify` | Routes an event to configured notifiers |
+| `auto-merge` | Reserved merge intent; currently routes like a notification |
+
+`send-to-agent` is the autonomous recovery path. `notify` is the human awareness path.
+
+## Config Fields
+
+Each reaction accepts:
+
+| Field | Type | Purpose |
+|-------|------|---------|
+| `auto` | `boolean` | Whether AO should perform the automated action |
+| `action` | `send-to-agent` \| `notify` \| `auto-merge` | What AO does when the reaction fires |
+| `message` | `string` | Message sent to the agent or included in notification text |
+| `priority` | `urgent` \| `action` \| `warning` \| `info` | Notification routing priority |
+| `retries` | `number` | Max repeated agent messages before escalation |
+| `escalateAfter` | `number` or duration string | Escalate after attempts or time, whichever applies |
+| `threshold` | duration string | Used by stuck/idle style reactions |
+| `includeSummary` | `boolean` | Include session summary in the notification |
+
+Example:
+
+```yaml
+reactions:
+ changes-requested:
+ action: send-to-agent
+ retries: 2
+ escalateAfter: "30m"
+ message: |
+ Reviewers requested changes. Read the unresolved comments,
+ make the requested fixes, and push an update.
+```
+
+## Notification Routing
+
+Reaction `priority` maps to `notificationRouting` in the global config:
+
+```yaml title="~/.agent-orchestrator/config.yaml"
+notificationRouting:
+ urgent: [desktop, composio]
+ action: [desktop]
+ warning: [composio]
+ info: [composio]
+```
+
+Named notifier instances are configured separately:
+
+```yaml title="~/.agent-orchestrator/config.yaml"
+notifiers:
+ desktop:
+ plugin: desktop
+ slack:
+ plugin: slack
+ webhookUrl: ${SLACK_WEBHOOK_URL}
+```
+
+## Per-Project Overrides
+
+Put reaction overrides in a local project config to affect only that project:
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ ci-failed:
+ retries: 4
+ approved-and-green:
+ priority: info
+```
+
+Project values override global values for the same reaction key.
+
+## CI Failure Flow
+
+CI failure handling happens in two passes:
+
+1. AO sends the configured `ci-failed.message` when the session first enters a failed-CI state.
+2. On the next poll, AO sends a structured follow-up with the failing check names, statuses, and URLs.
+
+The second message does not consume the `ci-failed` retry budget. It exists so the agent gets both the instruction and the specific failing checks.
+
+## Review Backlog Throttle
+
+AO throttles review-comment lookups to avoid hammering GitHub or GitLab. Pending human comments and known automation comments are fetched at most once every two minutes per session.
+
+Force a check when you need one immediately:
+
+```bash
+ao review-check
+```
+
+## Event Reference
+
+| Event | Reaction |
+|-------|----------|
+| `ci.failing` | `ci-failed` |
+| `review.changes_requested` | `changes-requested` |
+| `automated_review.found` | `bugbot-comments` |
+| `merge.conflicts` | `merge-conflicts` |
+| `merge.ready` | `approved-and-green` |
+| `session.stuck` | `agent-stuck` |
+| `session.needs_input` | `agent-needs-input` |
+| `session.killed` | `agent-exited` |
+| `summary.all_complete` | `all-complete` |
+
+## Next Steps
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/configuration/remote-access.mdx b/frontend/src/landing/content/docs/configuration/remote-access.mdx
new file mode 100644
index 000000000..619c160d6
--- /dev/null
+++ b/frontend/src/landing/content/docs/configuration/remote-access.mdx
@@ -0,0 +1,293 @@
+---
+title: Remote Access
+description: Access the AO dashboard from another device or over Tailscale. Covers ports, binding, reverse proxies, power management, and security.
+---
+
+# Remote Access
+
+By default the AO dashboard binds to `localhost:3000` and is only accessible from the machine it runs on. This page explains how to access it from another device — your phone, a second laptop, or a remote machine — using Tailscale or direct network binding.
+
+## Tailscale (recommended)
+
+[Tailscale](https://tailscale.com) creates a private WireGuard mesh network between your devices. Every device gets a stable IP like `100.x.x.x` and a DNS name like `my-laptop.tail1234.ts.net`. No port forwarding, no firewall rules required.
+
+### Setup
+
+1. Install Tailscale on both the machine running AO and the device you want to access it from.
+
+ ```bash
+ # macOS
+ brew install --cask tailscale
+
+ # Ubuntu / Debian
+ curl -fsSL https://tailscale.com/install.sh | sh
+ ```
+
+2. Start Tailscale and authenticate:
+
+ ```bash
+ sudo tailscale up
+ ```
+
+3. Find your machine's Tailscale IP:
+
+ ```bash
+ tailscale ip -4
+ # e.g. 100.64.0.1
+ ```
+
+4. Bind AO to all interfaces so Tailscale traffic can reach it:
+
+ ```yaml
+ # agent-orchestrator.yaml
+ port: 3000
+ ```
+
+ Then start AO with `HOST=0.0.0.0` so it listens on all interfaces (not just localhost):
+
+ ```bash
+ HOST=0.0.0.0 ao start
+ ```
+
+ Access the dashboard from another Tailscale device at:
+
+ ```
+ http://100.64.0.1:3000
+ ```
+
+ Or using the MagicDNS hostname (if you have MagicDNS enabled in your Tailnet):
+
+ ```
+ http://my-laptop.tail1234.ts.net:3000
+ ```
+
+### Tailscale serve (optional — HTTPS)
+
+For HTTPS with a valid certificate, use `tailscale serve`:
+
+```bash
+tailscale serve https:443 / http://localhost:3000
+```
+
+This makes the dashboard available at `https://my-laptop.tail1234.ts.net` with a Let's Encrypt certificate managed by Tailscale. The WebSocket connections for the terminal also work through Tailscale serve.
+
+## Binding to a specific interface
+
+AO uses Next.js for the dashboard. To bind to all interfaces (required for any remote access without Tailscale serve), set the `HOST` environment variable:
+
+```bash
+# Bind to all interfaces
+HOST=0.0.0.0 ao start
+```
+
+Or set it in your shell profile:
+
+```bash
+export HOST=0.0.0.0
+ao start
+```
+
+To bind to a specific IP only (e.g. your Tailscale IP):
+
+```bash
+HOST=100.64.0.1 ao start
+```
+
+The terminal WebSocket ports (`terminalPort` and `directTerminalPort`) also need to be reachable. If you're using Tailscale, the mesh handles this transparently as long as the ports are not firewalled locally.
+
+## macOS: preventing sleep
+
+If you run AO on a Mac, the machine going to sleep will kill the agents and the dashboard. Use `caffeinate` to prevent sleep while AO is running:
+
+```bash
+caffeinate -i ao start
+```
+
+`caffeinate -i` prevents idle sleep (triggered by inactivity) but still allows display sleep. For a machine you want to run headlessly overnight:
+
+```bash
+# Prevent all sleep (including display)
+caffeinate -dims ao start
+```
+
+To run AO persistently in the background as a launchd service, create a plist at `~/Library/LaunchAgents/com.ao.orchestrator.plist`:
+
+```xml
+
+
+
+
+ Label
+ com.ao.orchestrator
+ ProgramArguments
+
+ /usr/local/bin/ao
+ start
+
+ RunAtLoad
+
+ KeepAlive
+
+ EnvironmentVariables
+
+ HOST
+ 0.0.0.0
+
+
+
+```
+
+Load it with:
+
+```bash
+launchctl load ~/Library/LaunchAgents/com.ao.orchestrator.plist
+```
+
+## Reverse proxy
+
+If you want to expose AO over a domain name with TLS, or place it behind an authentication layer, you can front it with nginx or Caddy.
+
+**Important:** AO uses two WebSocket servers (one for tmux-attached terminals, one for direct PTY terminals). Your proxy must forward HTTP upgrade headers for both.
+
+### Environment variables for proxied setups
+
+| Variable | Purpose |
+|----------|---------|
+| `HOST=0.0.0.0` | Bind the Next.js dashboard to all interfaces |
+| `TERMINAL_PORT` | Override the tmux WS server port (server-side) |
+| `DIRECT_TERMINAL_PORT` | Override the direct PTY WS server port (server-side) |
+| `NEXT_PUBLIC_TERMINAL_WS_PATH` | Override the WebSocket base path the browser client dials — required when the proxy rewrites the path |
+
+### nginx
+
+```nginx
+server {
+ listen 443 ssl;
+ server_name ao.example.com;
+
+ # SSL cert config here
+
+ # Dashboard
+ location / {
+ proxy_pass http://127.0.0.1:3000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # Terminal WebSockets — tmux mux
+ location /ws/terminal/ {
+ proxy_pass http://127.0.0.1:14800/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 86400;
+ }
+
+ # Terminal WebSockets — direct PTY
+ location /ws/direct/ {
+ proxy_pass http://127.0.0.1:14801/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 86400;
+ }
+}
+```
+
+Then start AO with the matching path env var so the browser client dials through the proxy:
+
+```bash
+HOST=0.0.0.0 NEXT_PUBLIC_TERMINAL_WS_PATH=/ws/terminal ao start
+```
+
+### Caddy
+
+Caddy handles WebSocket upgrades automatically — no explicit `Upgrade` headers needed:
+
+```
+ao.example.com {
+ reverse_proxy /ws/terminal/* 127.0.0.1:14800
+ reverse_proxy /ws/direct/* 127.0.0.1:14801
+ reverse_proxy 127.0.0.1:3000
+}
+```
+
+Start AO the same way:
+
+```bash
+HOST=0.0.0.0 NEXT_PUBLIC_TERMINAL_WS_PATH=/ws/terminal ao start
+```
+
+### Pinning WebSocket ports
+
+By default AO auto-detects available ports for the WebSocket servers starting at 14800/14801. To pin them (required when configuring a reverse proxy), set them in `agent-orchestrator.yaml`:
+
+```yaml
+port: 3000
+terminalPort: 14800
+directTerminalPort: 14801
+```
+
+Or pass them as environment variables when starting:
+
+```bash
+HOST=0.0.0.0 TERMINAL_PORT=14800 DIRECT_TERMINAL_PORT=14801 ao start
+```
+
+## Accessing the dashboard from mobile
+
+The AO dashboard is a responsive web app — it works on mobile browsers. Connect over Tailscale and open the URL in Safari or Chrome. The Kanban board and session detail views are usable on a phone screen.
+
+**Limitations on mobile:**
+
+- The built-in terminal (xterm.js) works but is difficult to type in on a touch screen. Use it to read agent output; for sending messages use the session detail input field.
+- There is no native mobile app. Notifications go through your configured notifiers (Slack, desktop, etc.) — there is no push notification to the browser.
+
+## Security considerations
+
+**AO has no authentication.** Anyone who can reach the HTTP port can view all sessions, read terminal output, send messages to agents, and trigger merges. Never expose the dashboard port to the public internet.
+
+Mitigations:
+
+- Use Tailscale — the mesh is authenticated and encrypted end-to-end. Only your devices can reach the IP.
+- If you must use a public host, put a reverse proxy with HTTP Basic Auth (nginx, Caddy) in front of AO. See the [Reverse proxy](#reverse-proxy) section above.
+- Firewall the port at the OS level and only allow Tailscale traffic:
+
+ ```bash
+ # UFW example — allow only Tailscale interface
+ sudo ufw allow in on tailscale0 to any port 3000
+ sudo ufw deny 3000
+ ```
+
+**Environment variables and secrets** in agent processes are visible to anyone with dashboard access. Do not run AO on a shared machine without Tailscale or auth.
+
+**Webhook endpoint:** If you expose the dashboard publicly and have GitHub (or another tracker) configured, the `/api/webhooks` endpoint receives push events from GitHub. This endpoint is protected by a webhook secret configured in your tracker settings — verify the secret is set before exposing the port publicly.
+
+For more on project identity, local config, and runtime data, see [Configuration](/docs/configuration).
+
+## Port reference
+
+| Port | Default | Config key | Env var override | Purpose |
+|------|---------|------------|-----------------|---------|
+| `3000` | dashboard HTTP | `port` | `PORT` | Next.js app + API routes |
+| `14800` | tmux terminal WS | `terminalPort` | `TERMINAL_PORT` | WebSocket for tmux-attached terminal |
+| `14801` | direct terminal WS | `directTerminalPort` | `DIRECT_TERMINAL_PORT` | WebSocket for direct PTY terminal |
+
+If you run multiple AO instances on the same machine, change all three ports to avoid `EADDRINUSE` errors:
+
+```yaml
+# Second AO instance
+port: 3001
+terminalPort: 14810
+directTerminalPort: 14811
+```
+
+Note: When `terminalPort` and `directTerminalPort` are not set in the config or as env vars, AO auto-detects a free port pair starting from 14800. Set them explicitly whenever you configure a reverse proxy or firewall rules.
+
+## See also
+
+- [Configuration](/docs/configuration) — global registry and local project config
+- [Projects](/docs/configuration/projects) — per-project behavior options
+- [Troubleshooting](/docs/troubleshooting) — connection and port issues
diff --git a/frontend/src/landing/content/docs/dashboard.mdx b/frontend/src/landing/content/docs/dashboard.mdx
new file mode 100644
index 000000000..cf4d041f0
--- /dev/null
+++ b/frontend/src/landing/content/docs/dashboard.mdx
@@ -0,0 +1,169 @@
+---
+title: Dashboard
+description: Use the AO dashboard to monitor agents, respond to sessions, review PRs, manage projects, and open live terminals.
+---
+
+The AO dashboard is the control room for running agents. It shows what each session is doing, which PRs need review, where agents are blocked, and which projects are active.
+
+Start it with:
+
+```bash
+ao start
+```
+
+By default it opens at:
+
+```text
+http://localhost:3000
+```
+
+## Read The Board
+
+The main dashboard groups sessions by what you need to do next.
+
+| Column | What it means | What you usually do |
+|--------|---------------|---------------------|
+| **Working** | Agent is actively coding or running commands | Let it run |
+| **Pending** | Session exists but has not started useful work yet | Check if it stays here too long |
+| **Review** | PR is open and waiting for review | Review the PR or wait for CI |
+| **Respond** | Agent needs input or hit a blocker | Open the session and reply |
+| **Merge** | PR is approved and checks are green | Merge or let your merge workflow continue |
+
+Completed and terminated sessions appear in the **Done / Terminated** area. Restore a session from there when you need to inspect or continue it.
+
+When multiple projects are registered, the dashboard opens with a project overview. Pick a project to see its board, or use the project filter to scope dashboard, PR, and session views.
+
+## Handle A Session
+
+Click a session card to open its detail page.
+
+Use the detail page to:
+
+- Read the current status, branch, linked issue, and linked PR.
+- See whether the agent is active, idle, blocked, waiting for input, or exited.
+- Inspect PR size, CI checks, review state, and unresolved comments.
+- Send a message to the agent.
+- Kill or restore the session.
+- Open the live terminal.
+
+For review comments, use **Ask Agent to Fix** when the right next step is obvious. Use a manual message when you need to add context or constrain the fix:
+
+```text
+Keep this change limited to the API route. Do not refactor the shared client.
+```
+
+## Review PRs
+
+Open `/prs` to see every PR created by AO-managed sessions.
+
+The PR page lets you filter by:
+
+| Tab | Shows |
+|-----|-------|
+| **All** | Open, merged, and closed PRs |
+| **Open** | PRs still in progress |
+| **Merged** | Completed PRs |
+| **Closed** | Closed but unmerged PRs |
+
+Each PR shows its size, CI state, review decision, and unresolved threads. Use this page when you want a repository-level review queue instead of a session-by-session view.
+
+## Manage Projects
+
+The sidebar shows registered projects. Use it to move between projects, add another repository, or open a project's settings.
+
+Project settings let you edit behavior fields without changing the repository identity. That means you can change things like agent, runtime, tracker, SCM, and reactions, but AO keeps the registered path, storage key, and repository identity stable.
+
+If a project is degraded because its local config is invalid or still in an older wrapped format, the dashboard shows a repair state instead of hiding the project.
+
+## Use The Terminal
+
+Each session detail page includes a live terminal attached to the agent process.
+
+Use the terminal to inspect what the agent is doing, recover from stuck prompts, or run a quick command in the session context. For normal guidance, prefer the message box; it is easier for the agent to treat as instruction.
+
+AO has two terminal backends:
+
+| Backend | Used when | Default port |
+|---------|-----------|--------------|
+| tmux WebSocket mux | `runtime: tmux` | `14800` |
+| direct PTY WebSocket | `runtime: process` | `14801` |
+
+The terminal reconnects automatically when the WebSocket drops. If terminal output works but input feels awkward on mobile, send messages from the session controls instead.
+
+## Use AO On Mobile
+
+On smaller screens, the dashboard switches to a mobile layout:
+
+- A bottom tab bar links to Dashboard, PRs, and Orchestrator.
+- Sessions are ordered by urgency: Respond, Merge, Review, Pending, Working.
+- Tapping a card opens a bottom sheet with quick actions.
+- The terminal opens in a compact full-screen style.
+
+Mobile is useful for checking status and sending short replies. Long terminal work is still better on desktop.
+
+## Orchestrator Sessions
+
+When a project has an orchestrator session, the dashboard links to it from the project header or mobile tab bar.
+
+If multiple orchestrator sessions exist for the same project, `/orchestrators?project={projectId}` lets you choose one or start a new orchestrator session.
+
+Use the orchestrator view when you care about the whole agent fleet instead of one worker session.
+
+## Freshness And Updates
+
+The dashboard receives live session updates over `/api/events`. AO sends snapshots every five seconds. When sessions are added or removed, the dashboard refreshes the full session list from `/api/sessions`.
+
+If GitHub or GitLab rate limits are hit, PR details can become stale for a short time. The dashboard shows a warning banner and refreshes automatically when data is available again.
+
+The favicon and document title also change when sessions need attention, so you can keep the dashboard open in a background tab.
+
+## Routes
+
+| Route | Use for |
+|-------|---------|
+| `/` | Project overview or current project board |
+| `/projects/{projectId}` | One project's board |
+| `/projects/{projectId}/settings` | Project behavior settings |
+| `/prs` | PR review queue |
+| `/sessions/{id}` | Session detail and terminal |
+| `/projects/{projectId}/sessions/{id}` | Project-scoped session detail |
+| `/orchestrators?project={projectId}` | Pick or start an orchestrator session |
+
+Internal API routes such as `/api/events`, `/api/sessions`, `/api/projects`, and `/api/spawn` are used by the dashboard. They are not a stable public API yet.
+
+## Ports
+
+The full dashboard experience uses three ports:
+
+| Service | Default | Config |
+|---------|---------|--------|
+| Dashboard HTTP | `3000` | `port` or `PORT` |
+| tmux terminal WebSocket | auto from `14800` | `terminalPort` or `TERMINAL_PORT` |
+| direct PTY WebSocket | auto from `14801` | `directTerminalPort` or `DIRECT_TERMINAL_PORT` |
+
+For remote access, prefer Tailscale and keep AO off the public internet. See [Remote access](/docs/configuration/remote-access).
+
+## Troubleshooting
+
+**The board is empty**
+Check that the project is registered and that you are viewing the right project filter.
+
+**A session looks stale**
+Wait one poll cycle, then refresh. If PR data is stale, check for a rate-limit banner.
+
+**Terminal does not connect**
+Make sure the terminal WebSocket port is reachable. If you use a reverse proxy, it must forward WebSocket upgrade headers.
+
+**A project cannot be edited**
+Fix the local config first. The dashboard only writes behavior fields when the project config loads cleanly.
+
+**A session needs input**
+Open the session detail page and send a short, direct instruction. The agent receives it in the running session.
+
+## Next Steps
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/examples.mdx b/frontend/src/landing/content/docs/examples.mdx
new file mode 100644
index 000000000..2406b2cb2
--- /dev/null
+++ b/frontend/src/landing/content/docs/examples.mdx
@@ -0,0 +1,264 @@
+---
+title: Examples
+description: Five annotated starter configurations covering GitHub, Linear, multi-project, auto-merge, and Codex setups.
+---
+
+All examples live in the [`examples/` directory](https://github.com/ComposioHQ/agent-orchestrator/tree/main/examples) of the AO repository. Pick the one closest to your setup, copy it to your project root as `agent-orchestrator.yaml`, fill in your repo path and any API keys, and you're ready to spawn agents.
+
+```bash
+cp examples/simple-github.yaml agent-orchestrator.yaml
+```
+
+---
+
+## Simple GitHub
+
+For solo devs on a single GitHub repo with GitHub Issues.
+
+```yaml title="simple-github.yaml"
+# Minimal setup for a single GitHub repo with GitHub Issues
+# Perfect for getting started quickly
+
+dataDir: ~/.agent-orchestrator
+worktreeDir: ~/.worktrees
+
+projects:
+ my-app:
+ repo: owner/my-app
+ path: ~/my-app
+ defaultBranch: main
+```
+
+**Notable settings:**
+
+- Uses the default GitHub tracker — requires `GITHUB_TOKEN` (set automatically by the `gh` CLI).
+- No `defaults:` block needed — AO's built-in defaults (`tmux` runtime, `claude-code` agent, `worktree` workspace) apply automatically.
+- Replace `owner/my-app` and `~/my-app` with your actual GitHub slug and local checkout path.
+
+[View raw file on GitHub](https://github.com/ComposioHQ/agent-orchestrator/blob/main/examples/simple-github.yaml)
+
+---
+
+## Linear Team
+
+For teams that track work in Linear rather than GitHub Issues.
+
+```yaml title="linear-team.yaml"
+# Linear integration with custom team
+# Requires LINEAR_API_KEY environment variable
+
+dataDir: ~/.agent-orchestrator
+worktreeDir: ~/.worktrees
+
+projects:
+ my-app:
+ repo: owner/my-app
+ path: ~/my-app
+ defaultBranch: main
+
+ # Linear tracker integration
+ tracker:
+ plugin: linear
+ teamId: "2a6e9b1b-19cd-4e30-b5bd-7b34dc491c7e"
+
+ # Custom rules for agents
+ agentRules: |
+ Always link Linear tickets in commit messages.
+ Run tests before pushing.
+ Use conventional commits (feat:, fix:, chore:).
+```
+
+**Notable settings:**
+
+- `tracker.plugin: linear` swaps out the default GitHub tracker for the Linear plugin.
+- `tracker.teamId` is your Linear team UUID — find it in Linear → Settings → Team → General.
+- Requires a `LINEAR_API_KEY` environment variable (set it in your shell profile or CI secrets).
+- `agentRules` injects project-specific instructions into every agent's prompt — useful for enforcing commit conventions or test requirements.
+
+[View raw file on GitHub](https://github.com/ComposioHQ/agent-orchestrator/blob/main/examples/linear-team.yaml)
+
+---
+
+## Multi-Project
+
+For managing multiple repositories from a single config, with mixed trackers and Slack notifications.
+
+```yaml title="multi-project.yaml"
+# Managing multiple projects with different trackers
+# Shows how to configure multiple repos with different settings
+
+dataDir: ~/.agent-orchestrator
+worktreeDir: ~/.worktrees
+
+defaults:
+ runtime: tmux
+ agent: claude-code
+ workspace: worktree
+ notifiers: [desktop, slack]
+
+projects:
+ frontend:
+ name: Frontend
+ repo: org/frontend
+ path: ~/frontend
+ defaultBranch: main
+ sessionPrefix: fe
+
+ tracker:
+ plugin: github
+
+ agentRules: |
+ Use TypeScript strict mode.
+ Follow React best practices.
+ Always run `pnpm test` before pushing.
+
+ backend:
+ name: Backend API
+ repo: org/backend
+ path: ~/backend
+ defaultBranch: main
+ sessionPrefix: api
+
+ tracker:
+ plugin: linear
+ teamId: "your-team-id"
+
+ agentRules: |
+ All endpoints require auth middleware.
+ Add OpenAPI docs for new routes.
+ Run `pnpm test` and `pnpm lint` before pushing.
+
+# Slack notifications (requires SLACK_WEBHOOK_URL)
+notifiers:
+ slack:
+ plugin: slack
+ webhook: ${SLACK_WEBHOOK_URL}
+ channel: "#agent-updates"
+
+# Route notifications by priority
+notificationRouting:
+ urgent: [desktop, slack]
+ action: [desktop, slack]
+ warning: [slack]
+ info: [slack]
+```
+
+**Notable settings:**
+
+- `defaults:` sets the runtime, agent, workspace, and notifier list for all projects — individual projects can override any of these.
+- `sessionPrefix` keeps session IDs readable (`fe-123`, `api-456`) when multiple projects run simultaneously.
+- Each project can use a different tracker — `frontend` uses GitHub Issues, `backend` uses Linear.
+- `notifiers.slack.webhook: ${SLACK_WEBHOOK_URL}` reads the webhook URL from an environment variable — never hardcode secrets in config files.
+- `notificationRouting` routes urgent and actionable events to both desktop and Slack, while lower-priority events go to Slack only.
+
+[View raw file on GitHub](https://github.com/ComposioHQ/agent-orchestrator/blob/main/examples/multi-project.yaml)
+
+---
+
+## Auto-Merge
+
+For teams that want maximum automation — PRs merge automatically when approved and CI is green, with auto-retry for failures.
+
+```yaml title="auto-merge.yaml"
+# Aggressive automation with auto-merge
+# Automatically merges approved PRs with passing CI
+
+dataDir: ~/.agent-orchestrator
+worktreeDir: ~/.worktrees
+
+projects:
+ my-app:
+ repo: owner/my-app
+ path: ~/my-app
+ defaultBranch: main
+
+ # Enable auto-merge for this project
+ reactions:
+ approved-and-green:
+ auto: true # Automatically merge when PR is approved and CI passes
+ action: auto-merge
+
+# Global reactions
+reactions:
+ # Auto-retry CI failures up to 3 times
+ ci-failed:
+ auto: true
+ action: send-to-agent
+ retries: 3
+
+ # Auto-address review comments
+ changes-requested:
+ auto: true
+ action: send-to-agent
+ escalateAfter: 1h # Notify human if not resolved in 1 hour
+
+ # Notify when agent is stuck
+ agent-stuck:
+ threshold: 10m
+ action: notify
+ priority: urgent
+```
+
+**Notable settings:**
+
+- `reactions.approved-and-green.auto: true` with `action: auto-merge` enables hands-free merging — no human click required once the PR is approved and CI passes.
+- The project-level `reactions` block overrides global reactions for that project only; global `reactions` apply to all projects unless overridden.
+- `ci-failed.retries: 3` lets AO send the agent back to fix CI up to three times before escalating.
+- `changes-requested.escalateAfter: 1h` ensures a human is notified if an agent can't resolve review comments within an hour.
+- `agent-stuck.threshold: 10m` triggers an urgent desktop notification if an agent goes silent for 10 minutes.
+
+[View raw file on GitHub](https://github.com/ComposioHQ/agent-orchestrator/blob/main/examples/auto-merge.yaml)
+
+---
+
+## Codex Integration
+
+For teams that prefer GPT-4/Codex over Claude Code.
+
+```yaml title="codex-integration.yaml"
+# Using Codex instead of Claude Code
+# Demonstrates using a different AI agent
+
+dataDir: ~/.agent-orchestrator
+worktreeDir: ~/.worktrees
+
+defaults:
+ agent: codex # Use Codex instead of Claude Code
+ runtime: tmux
+ workspace: worktree
+
+projects:
+ my-app:
+ repo: owner/my-app
+ path: ~/my-app
+ defaultBranch: main
+
+ # Codex-specific configuration
+ agentConfig:
+ model: gpt-4
+ permissions: default
+
+ agentRules: |
+ Write clean, well-documented code.
+ Follow project conventions.
+ Run tests before pushing.
+```
+
+**Notable settings:**
+
+- `defaults.agent: codex` changes the agent for all projects — no need to set it per project.
+- `agentConfig.model: gpt-4` is passed directly to the Codex plugin; valid values depend on your OpenAI subscription and which models the Codex plugin supports.
+- `agentConfig.permissions: default` uses the plugin's built-in permission set — change to `full` to allow broader file access, or `readonly` to restrict writes.
+- Requires an `OPENAI_API_KEY` environment variable.
+
+[View raw file on GitHub](https://github.com/ComposioHQ/agent-orchestrator/blob/main/examples/codex-integration.yaml)
+
+---
+
+## Next Steps
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/faq.mdx b/frontend/src/landing/content/docs/faq.mdx
new file mode 100644
index 000000000..19f4cfa6c
--- /dev/null
+++ b/frontend/src/landing/content/docs/faq.mdx
@@ -0,0 +1,64 @@
+---
+title: FAQ
+description: The questions people actually ask before adopting AO.
+---
+
+import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
+
+
+
+ Tabs are fine for one or two. AO handles the bookkeeping once you have more than that: per-session worktrees, per-session branches, automatic CI/review reactions, one dashboard, cost tracking, and a state machine that knows what each agent is doing.
+
+
+
+ No. AO supports Claude Code, Codex, Cursor, Aider, and OpenCode today. Set `agent:` per project (or per spawn with `--agent`). See [Agents](/docs/plugins/agents).
+
+
+
+ No, intentionally. AO will get a PR to a mergeable state — CI green, approvals satisfied — and stop. The merge is your call.
+
+
+
+ No. AO polls `gh` / `glab` for PR state. Webhooks are an optional latency optimisation for the review loop — wire them up only if you need near-instant reactions.
+
+
+
+ Yes. `gh` and `glab` both support enterprise hosts — configure them via their own tools. For GitLab, also set `trackerConfig.host` / `scmConfig.host` in AO.
+
+
+
+ Partially — see [Platforms › Windows](/docs/platforms#windows). You need `runtime: process` instead of tmux, and desktop notifications are a no-op. Everything else (spawning, PR flow, review loop, CI recovery) works the same.
+
+
+
+ `~/.agent-orchestrator/{hash}-{projectId}/`. No database. Everything is flat files you can inspect.
+
+
+
+ Yes. The dashboard is a plain Next.js app — port-forward or put it behind your reverse proxy. AO has no built-in auth, so use your usual SSO / basic-auth layer. Use `runtime: process` in containers.
+
+
+
+ AO itself is MIT-licensed and free. Agents cost whatever their underlying API costs (Anthropic, OpenAI, etc.) — AO shows per-session cost on the dashboard for agents that report it (Claude Code today).
+
+
+
+ Your code stays in your worktrees on your machine. AO doesn't upload anything — it just coordinates local processes and makes GitHub API calls through your authenticated CLIs.
+
+
+
+ Yes. `ao plugin create --slot --name ` scaffolds a starter. Plugins are tiny Node packages exporting a manifest + `create()` function.
+
+
+
+ Because the answer to "should AO support Linear?" (or Discord, or Codex, or …) is always yes, but bundling every integration into core makes the CLI heavy. Plugins let you pick what you need.
+
+
+
+ The agent modifies the worktree — that's the whole point. AO itself doesn't touch `main`, doesn't force-push, and doesn't merge. The session works on its own branch.
+
+
+
+ `npm uninstall -g @aoagents/ao`, then `rm -rf ~/.agent-orchestrator` if you want to wipe all session history.
+
+
diff --git a/frontend/src/landing/content/docs/guides/ci-recovery.mdx b/frontend/src/landing/content/docs/guides/ci-recovery.mdx
new file mode 100644
index 000000000..1402cec80
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/ci-recovery.mdx
@@ -0,0 +1,98 @@
+---
+title: CI recovery
+description: How AO detects red CI and nudges the agent to investigate without you babysitting.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+When your PR's CI goes red, AO notices before you do and tells the agent to fix it. You get a notification, the agent pushes a fix, and the cycle repeats until CI is green.
+
+## How it works
+
+1. The **SCM plugin** polls the PR's check runs via `gh pr checks` (or `glab`).
+2. The **lifecycle manager** transitions the session to `ci_failed` when any required check fails.
+3. The **agent plugin** is woken with a prompt like *"CI failed on PR #42. The failing checks are X and Y. Investigate and push a fix."*
+4. The **notifier plugin** pings you (desktop, Slack, Discord, whatever you configured).
+
+No webhooks, no CI integration to install. The `gh` CLI you already authenticated with is doing the work.
+
+## What the agent sees
+
+When the session transitions to `ci_failed`, AO injects a short context block into the agent prompt:
+
+- The failing check names
+- A link to the run log (the agent can fetch it with `gh run view` via the PATH wrapper)
+- The PR number
+- The branch name
+
+The agent's next response usually inspects the log, reproduces the failure locally if it can, and pushes a commit. Then CI re-runs and the loop closes.
+
+## Configurable behavior
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ ciFailed:
+ enabled: true # default
+ maxRetries: 3 # stop after 3 automatic rounds
+ cooldownSeconds: 30 # wait before nudging the agent again
+```
+
+If the agent has tried three times and CI is still red, the session transitions to `blocked` and AO stops nudging — you take it from there.
+
+## Manual retry
+
+Tell AO to re-check a specific PR right now:
+
+```bash
+ao review-check # check every project
+ao review-check myproject
+ao review-check --dry-run # show what would happen, don't nudge
+```
+
+## When it doesn't kick in
+
+- **PR not linked to a session.** If you created the PR manually and didn't run `ao session claim-pr`, AO doesn't know about it.
+- **Check status is `neutral` or `skipped`.** AO only reacts to `failure` and `error`.
+- **PR is in draft.** AO waits for ready-for-review before treating CI as binding.
+- **`reactions.ciFailed.enabled: false`** in your config.
+
+
+ Claiming a PR retroactively: `ao session claim-pr 123 ` — AO now tracks its CI.
+
+
+## Linking a manually-opened PR
+
+Sometimes you want the agent to work on an existing PR instead of an issue:
+
+```bash
+ao spawn --claim-pr 123 --assign-on-github
+```
+
+This creates a session, points it at PR #123, and (with `--assign-on-github`) assigns the PR to you so it shows up in your GitHub filters. Subsequent CI failures flow into the normal recovery loop.
+
+## How the CI failure flow works
+
+When CI goes red, AO sends two distinct messages to the agent in sequence:
+
+1. **Reaction message (poll cycle N).** The lifecycle manager detects that CI transitioned to `ci_failed` and fires the `ci-failed` reaction. This sends the configured `message` (or the default: *"CI failed on PR #42…"*) to the agent via `ao send`.
+
+2. **Detailed follow-up (poll cycle N+1, ~30 s later).** On the next poll cycle, AO calls `formatCIFailureMessage()` with the actual failing checks and sends a second message with each check's name, conclusion status, and a direct link to the run log. This is delivered via `sessionManager.send()` directly — it does **not** go through `executeReaction()`, so it does not consume the `ci-failed` reaction's retry budget.
+
+The follow-up is unconditional. Even if you have set a custom `ci-failed.message`, the detailed check list arrives on the next poll regardless — your custom message is the first nudge; the structured check data is always the second.
+
+AO fingerprints the set of failing checks (name + status + conclusion). If the same failure set is still present on a subsequent cycle, AO skips re-sending to avoid spamming the agent. When the failure set changes (e.g. one check is fixed and a new one appears), the fingerprint changes and both messages are sent again.
+
+## Escalation
+
+`retries` controls how many reaction attempts AO makes before giving up and escalating to a human:
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ ci-failed:
+ retries: 3 # escalate after 3 failed attempts (default: unlimited)
+ escalateAfter: "1h" # …or after a wall-clock duration, whichever comes first
+```
+
+`escalateAfter` accepts either a duration string (e.g. `"30m"`, `"1h"`) or an integer attempt count. When either threshold is crossed, AO emits a `reaction.escalated` event and notifies you via the configured notifier instead of sending another message to the agent.
+
+See [Reactions configuration](/docs/configuration/reactions) for the full set of options.
diff --git a/frontend/src/landing/content/docs/guides/index.mdx b/frontend/src/landing/content/docs/guides/index.mdx
new file mode 100644
index 000000000..5323dc84c
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/index.mdx
@@ -0,0 +1,15 @@
+---
+title: Guides
+description: Real scenarios — parallel issues, CI recovery, review loops, multi-project setups.
+---
+
+These guides are task-shaped: each one covers a single thing you'd actually want AO to do for you, end to end.
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/guides/meta.json b/frontend/src/landing/content/docs/guides/meta.json
new file mode 100644
index 000000000..59c40b402
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/meta.json
@@ -0,0 +1,12 @@
+{
+ "title": "Guides",
+ "defaultOpen": false,
+ "pages": [
+ "per-role-agents",
+ "parallel-issues",
+ "ci-recovery",
+ "review-loop",
+ "reactions",
+ "multi-project"
+ ]
+}
diff --git a/frontend/src/landing/content/docs/guides/multi-project.mdx b/frontend/src/landing/content/docs/guides/multi-project.mdx
new file mode 100644
index 000000000..81f139017
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/multi-project.mdx
@@ -0,0 +1,97 @@
+---
+title: Multi-project
+description: Run AO against several repos from one dashboard, with per-project plugins and configs.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+One `agent-orchestrator.yaml` can drive many projects. Each project gets its own repo, tracker, plugin overrides, and — most importantly — its own dashboard lane.
+
+## Config shape
+
+```yaml title="agent-orchestrator.yaml"
+runtime: tmux # global default
+agent: claude-code
+
+projects:
+ web:
+ repo: ComposioHQ/agent-orchestrator
+ agent: claude-code
+
+ api:
+ repo: myorg/api
+ agent: codex # per-project override
+ tracker: linear
+ trackerConfig:
+ teamId: TEAM-123
+
+ marketing:
+ repo: myorg/site
+ agent: cursor
+ workspace: clone # full clone instead of worktree
+```
+
+Project IDs (`web`, `api`, `marketing`) are what you pass to `--project` / `-p`.
+
+## Adding a project
+
+Two ways:
+
+- **By path**: `ao start ~/code/new-repo` — AO infers a project ID from the directory name and appends it.
+- **By URL**: `ao start https://github.com/owner/repo` — AO clones it under `~/.agent-orchestrator/` and registers it.
+
+Both update `agent-orchestrator.yaml` in place. Diff it if you're curious.
+
+## Spawning in the right project
+
+`ao spawn` auto-detects the project when you're in a worktree belonging to it. Otherwise name it:
+
+```bash
+ao spawn 42 -p api
+ao batch-spawn 10 11 12 -p web
+```
+
+Or pass no project and AO will prompt.
+
+## Per-project plugin overrides
+
+Anything defined at the top level is a default. Override per project:
+
+```yaml
+tracker: github # default
+notifier:
+ - type: slack
+ webhookUrl: ${SLACK_GLOBAL}
+
+projects:
+ internal:
+ repo: myorg/internal
+ tracker: linear
+ notifier:
+ - type: discord
+ webhookUrl: ${DISCORD_INTERNAL}
+```
+
+The `internal` project uses Linear + Discord. Every other project uses GitHub + Slack.
+
+## Status across projects
+
+```bash
+ao status # all projects
+ao status -p api # one project
+ao status --json | jq # scripted
+ao status --watch # live-updating table
+```
+
+The dashboard shows all projects as swim lanes.
+
+## Stopping selectively
+
+```bash
+ao stop api # stop one project's orchestrator + dashboard
+ao stop --all # stop everything
+```
+
+
+ Each project can have a different agent, tracker, notifier, and workspace — that's the whole point of the plugin system. Don't force uniformity; let teams use what fits.
+
diff --git a/frontend/src/landing/content/docs/guides/parallel-issues.mdx b/frontend/src/landing/content/docs/guides/parallel-issues.mdx
new file mode 100644
index 000000000..6282cdd2e
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/parallel-issues.mdx
@@ -0,0 +1,84 @@
+---
+title: Parallel issues
+description: Spawn agents on several issues at once without them stepping on each other.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+The whole point of AO is running agents in parallel. Each session gets its own git worktree, so agents can't stomp each other's branches.
+
+## Batch spawn
+
+`ao batch-spawn` takes one or more issue identifiers:
+
+```bash
+ao batch-spawn 42 43 44
+```
+
+AO:
+
+- Skips any issue that already has an active session
+- De-duplicates within the batch
+- Creates a worktree per agent
+- Reports a summary when all are spawned
+
+For a quick interactive version, just run `ao spawn` a few times — `batch-spawn` adds the duplicate detection.
+
+## One agent, many issues? No.
+
+Each spawn is one agent working on exactly one issue. If you want the same issue worked on by two different agents (e.g. Claude Code and Codex) in parallel, pass `--agent`:
+
+```bash
+ao spawn 42 --agent claude-code
+ao spawn 42 --agent codex
+```
+
+AO creates two distinct sessions. Compare the PRs side by side.
+
+## Isolation guarantees
+
+| What's isolated | How |
+|---|---|
+| File edits | Each session has its own worktree under `~/.worktrees/{projectId}/{sessionId}/`. Session metadata lives under `~/.agent-orchestrator/{hash}-{projectId}/sessions/{sessionId}` — the worktree itself is in `~/.worktrees/`. |
+| Branch name | AO names the branch after the session ID, avoiding collisions |
+| Agent state | Each agent's native session files (Claude JSONL, Codex session, etc.) are session-scoped |
+| Terminal | Each session owns its own tmux window (or child process) |
+
+## What isn't isolated
+
+- **Your project's node_modules.** Workspaces clone/share the repo; if your agent runs `npm install` it updates the worktree's `node_modules`, which is fine. If your agent runs `pnpm install` with a shared store, the store is shared.
+- **External side effects.** If an agent opens a PR, triggers CI, or posts to a notifier, those effects are visible to you and everyone else immediately.
+
+## Watching them move
+
+On the dashboard, each card shows:
+
+- Issue title + number
+- Current lifecycle state (spawning → working → pr_open → ci_failed / review_pending / mergeable / merged)
+- Activity (active, ready, idle, waiting_input, blocked)
+- Cost (agent-reported when available)
+
+The Kanban columns update as state changes — you don't need to reload.
+
+## From the CLI
+
+```bash
+# List everything at a glance
+ao status
+
+# Keep it on-screen
+ao status --watch
+
+# Filter to one project
+ao status -p my-repo --json
+```
+
+## When you want fewer
+
+- `ao session kill ` — kill one
+- `ao session cleanup` — kill everything whose PR merged or issue closed (safe; archives metadata)
+- `ao session cleanup --dry-run` — preview first
+
+
+ Need them all gone? `ao stop --all` halts every running AO instance on this machine.
+
diff --git a/frontend/src/landing/content/docs/guides/per-role-agents.mdx b/frontend/src/landing/content/docs/guides/per-role-agents.mdx
new file mode 100644
index 000000000..ed09116f2
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/per-role-agents.mdx
@@ -0,0 +1,150 @@
+---
+title: Per-role agents
+description: Run a reasoning-heavy agent for orchestration and a fast agent for workers — globally or per-project.
+---
+
+## Why split roles
+
+AO runs two distinct types of sessions: an **orchestrator** and one or more **workers**. The orchestrator is a supervisor — it reads session state, spawns workers with `ao spawn`, nudges stuck sessions with `ao send`, and never owns a PR or writes code. Workers actually implement features, fix CI failures, and push branches. Because these workloads are so different, they benefit from different models: orchestrators need broad reasoning over many sessions (reach for a large model like Opus); workers need fast, focused execution on a single task (a smaller or specialised model like Sonnet or Codex).
+
+## Global default (whole config)
+
+Set default agent and model for both roles under `defaults:` in your `agent-orchestrator.yaml`. These apply to every project unless overridden.
+
+```yaml
+defaults:
+ orchestrator:
+ agent: claude-code
+ worker:
+ agent: codex
+```
+
+
+`defaults.orchestrator` and `defaults.worker` accept only the `agent` key at the global defaults level. To set `agentConfig` (model, permissions, etc.), use the per-project `orchestrator` and `worker` blocks described below.
+
+
+## Per-project override
+
+The per-project `orchestrator` and `worker` blocks accept both `agent` and `agentConfig`, giving you full control including model selection:
+
+```yaml
+projects:
+ myapp:
+ repo: org/myapp
+ path: ~/code/myapp
+ orchestrator:
+ agent: claude-code
+ agentConfig:
+ model: claude-opus-4
+ worker:
+ agent: codex
+ agentConfig:
+ model: gpt-5-codex
+```
+
+Per-project values take precedence over `defaults`. Any project that does not define `orchestrator` or `worker` inherits from `defaults`.
+
+## Recipe 1: Opus orchestrator, Sonnet workers
+
+Use a large reasoning model to supervise and a fast model for execution:
+
+```yaml
+defaults:
+ orchestrator:
+ agent: claude-code
+ worker:
+ agent: claude-code
+
+projects:
+ myapp:
+ repo: org/myapp
+ path: ~/code/myapp
+ orchestrator:
+ agent: claude-code
+ agentConfig:
+ model: claude-opus-4
+ worker:
+ agent: claude-code
+ agentConfig:
+ model: claude-sonnet-4
+```
+
+## Recipe 2: Claude Code orchestrator, Codex workers
+
+Keep the orchestrator on Claude Code (good for tool-heavy coordination) and use Codex for cost-effective worker sessions:
+
+```yaml
+defaults:
+ orchestrator:
+ agent: claude-code
+ worker:
+ agent: codex
+
+projects:
+ myapp:
+ repo: org/myapp
+ path: ~/code/myapp
+ orchestrator:
+ agent: claude-code
+ agentConfig:
+ model: claude-opus-4
+ worker:
+ agent: codex
+ agentConfig:
+ model: gpt-5-codex
+```
+
+## Recipe 3: Same agent, different models
+
+Keep `agent: claude-code` for both roles but override the model per role. Useful when you want identical tooling but different cost/latency profiles:
+
+```yaml
+projects:
+ myapp:
+ repo: org/myapp
+ path: ~/code/myapp
+ orchestrator:
+ agent: claude-code
+ agentConfig:
+ model: claude-opus-4
+ worker:
+ agent: claude-code
+ agentConfig:
+ model: claude-haiku-4
+```
+
+## What the orchestrator actually does
+
+The orchestrator prompt, injected by `ao start`, enforces the following rules:
+
+- **Read-only by design.** The orchestrator may inspect session status, logs, PR state, and CI output — but it must never edit repository files, run implementation code, or create its own PR. All code changes are delegated to worker sessions.
+- **Spawns workers via `ao spawn`.** Pass an issue ID (GitHub: `#123`, Linear: `INT-1234`) or `--claim-pr` to attach an existing PR to a new worker.
+- **Nudges stuck workers via `ao send`.** Sends instructions or clarification directly to a worker's terminal. Always uses `ao send` — never raw `tmux send-keys`.
+- **Monitors with `ao status`.** Gets a live view of every session's PR, CI, and review state across all projects.
+- **Cleans up via `ao session kill` and `ao session cleanup`.** Removes dead or merged sessions.
+
+The orchestrator is itself a long-running agent session (`-orchestrator`) launched with `ao start`. It has access to all `ao` CLI commands and your project configuration but is explicitly prohibited from owning a branch or PR.
+
+## Role metadata
+
+AO records a `role` field in each session's metadata file with the value `orchestrator` or `worker`. The lifecycle manager and dashboard use this field to display sessions differently (orchestrator sessions get a distinct label and are excluded from PR-tracking flows). You never need to set `role` manually — AO assigns it automatically: `ao start` creates the orchestrator session; `ao spawn` (called from the orchestrator) creates worker sessions tagged as `worker`.
+
+## Gotchas
+
+- **Missing agent binary.** If you set `defaults.worker.agent: codex` but Codex is not installed, `ao spawn` will fail when a worker session starts. Run `ao doctor` to verify all configured agents are available before starting a run.
+- **Inheritance order.** Workers inherit the top-level `defaults` unless overridden at the project level. A project-level `worker` block fully replaces the default — it is not merged field-by-field, so specify every field you need.
+- **Separate rule sets.** `orchestratorRules` and `agentRules` in the project config apply independently to orchestrator and worker sessions respectively. See the [Projects configuration reference](/docs/configuration/projects) for details.
+
+## Next steps
+
+
+
+ Full per-project config syntax including orchestrator/worker fields.
+
+
+ How the orchestrator prompt is assembled and injected.
+
+
+ Available agent plugins and their configuration options.
+
+
diff --git a/frontend/src/landing/content/docs/guides/reactions.mdx b/frontend/src/landing/content/docs/guides/reactions.mdx
new file mode 100644
index 000000000..15b2fad15
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/reactions.mdx
@@ -0,0 +1,132 @@
+---
+title: Reaction recipes
+description: Practical reaction configurations — watch-only mode, auto-merge, custom CI messages, and bot handling.
+---
+
+Reactions are configured under `reactions:` (global) or `projects.*.reactions:` (per-project). Every recipe here uses the correct schema field names; see the [Reactions reference](/docs/configuration/reactions) for the full list of keys and their defaults.
+
+## Recipe: Watch-only mode
+
+Disable all automated agent messages while keeping notifications active. Useful during onboarding or in sensitive projects where you want to observe before letting AO act:
+
+```yaml
+reactions:
+ ci-failed:
+ auto: false
+ changes-requested:
+ auto: false
+ bugbot-comments:
+ auto: false
+ agent-idle:
+ auto: false
+```
+
+`auto: false` suppresses the `send-to-agent` action but does not silence notifiers. Reactions whose default action is `notify` (like `agent-stuck`, `agent-exited`, and `all-complete`) continue to fire their notifications regardless of this flag.
+
+## Recipe: Enable auto-merge
+
+`approved-and-green` defaults to `auto: false` (notify-only). Opt in explicitly:
+
+```yaml
+reactions:
+ approved-and-green:
+ auto: true
+ action: auto-merge
+```
+
+
+The `auto-merge` action currently calls `notifyHuman()` internally — it does not perform a real merge. Actual merging still depends on your repository's branch protection rules and the "Allow auto-merge" setting on GitHub. AO does not bypass these gates. Treat `auto-merge` as an opt-in signal for when the SCM plugin adds real merge support.
+
+
+## Recipe: Custom CI failure message
+
+Override the default `message` to give the agent more precise recovery instructions:
+
+```yaml
+reactions:
+ ci-failed:
+ message: |
+ CI is failing. Run `pnpm test` locally and fix the failing tests before pushing again. Prioritise type errors first.
+```
+
+The two-pass CI behavior still applies: AO sends your custom `message` on the first pass (transition), then follows up in the next poll cycle with a second message listing every failing check by name and URL. Your `message` override affects only the first pass — the structured check list fires regardless.
+
+## Recipe: Aggressive escalation
+
+Tighten the retry and escalation windows for urgent projects where you want fast human handoff:
+
+```yaml
+reactions:
+ ci-failed:
+ retries: 1
+ escalateAfter: "5m"
+ changes-requested:
+ retries: 1
+ escalateAfter: "5m"
+```
+
+With `retries: 1` and `escalateAfter: "5m"`, AO will escalate after one failed recovery attempt or five minutes — whichever comes first.
+
+## Recipe: Ignore bugbot noise
+
+Keep human `changes-requested` reactions fully active but silence automated bot review nudges:
+
+```yaml
+reactions:
+ bugbot-comments:
+ auto: false
+```
+
+AO identifies bot comments by author login. The current hardcoded bot list in `scm-github` includes `cursor[bot]`, `github-actions[bot]`, `codecov[bot]`, and `sonarcloud[bot]`. Any review comment from these accounts triggers `bugbot-comments` rather than `changes-requested`. Setting `auto: false` here stops AO from nudging the agent about bot feedback while leaving human review comments fully active.
+
+## Recipe: Per-project overrides
+
+Suppress a noisy notification for one project while leaving everything default for others:
+
+```yaml
+reactions:
+ all-complete:
+ auto: true
+ action: notify
+
+projects:
+ myapp:
+ repo: org/myapp
+ path: ~/code/myapp
+ reactions:
+ all-complete:
+ auto: false # suppress the "all sessions done" notification for this project
+
+ otherproj:
+ repo: org/otherproj
+ path: ~/code/otherproj
+ # inherits global defaults — all-complete notification fires as normal
+```
+
+Per-project reaction blocks are merged with the global config; the project value wins for every field that is specified.
+
+## Recipe: Stuck session threshold
+
+Extend the `agent-stuck` threshold for projects with long-running builds where `10m` of inactivity is normal:
+
+```yaml
+reactions:
+ agent-stuck:
+ threshold: "30m"
+```
+
+The `threshold` field is used exclusively by `agent-stuck`. A session must be continuously idle for longer than `threshold` before the reaction fires and the session status transitions to `stuck`.
+
+## Where to go next
+
+
+
+ Full schema, default values, escalation semantics, and the two-pass CI design.
+
+
+ Step-by-step walkthrough of AO's CI failure recovery loop.
+
+
+ How AO handles review comments and the changes-requested reaction.
+
+
diff --git a/frontend/src/landing/content/docs/guides/review-loop.mdx b/frontend/src/landing/content/docs/guides/review-loop.mdx
new file mode 100644
index 000000000..0bf95deb7
--- /dev/null
+++ b/frontend/src/landing/content/docs/guides/review-loop.mdx
@@ -0,0 +1,128 @@
+---
+title: Review loop
+description: When a reviewer asks for changes, AO replays the feedback to the agent so the fix shows up without you copy-pasting anything.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+Reviews are the part of code review that most agents get wrong: they treat them as one-shot, so the reviewer ends up repeating themselves. AO closes that loop — when a `REQUEST_CHANGES` review lands, the agent sees the review body and the inline comments in context and pushes a fix.
+
+## What triggers it
+
+The SCM plugin watches your PR for:
+
+- A new review with state `CHANGES_REQUESTED`
+- New comments on an existing review
+- Line-level comments ("inline comments")
+
+When any of these appear, the session transitions to `changes_requested` and the agent is woken with the full review body + all unresolved inline comments.
+
+## What the agent sees
+
+A single structured prompt with:
+
+- Reviewer's top-level message (the review summary)
+- Each unresolved inline comment, formatted as `path:line — comment`
+- A pointer to the PR head SHA
+
+
+ AO reads unresolved review threads. Once you resolve a thread on GitHub, it drops out of the next nudge — so you can thumbs-up the ones the agent addressed and only the remaining ones make it back to the agent.
+
+
+## Configurable behavior
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ reviewRequested:
+ enabled: true # default
+ includeResolved: false # default: only unresolved threads
+ maxRetries: 3
+```
+
+`maxRetries` matters more than you'd think — occasionally a reviewer and agent will disagree about the right fix, and you don't want the agent stuck in a loop.
+
+## Manually replay a review
+
+```bash
+# Check all tracked PRs now
+ao review-check
+
+# One project
+ao review-check myproject
+
+# Dry run: show who would get nudged
+ao review-check --dry-run
+```
+
+## Best practices
+
+- **Use inline comments for mechanical changes.** "Rename this variable", "move this into a helper" — agents handle these well.
+- **Use the top-level review message for design-level feedback.** Agents are better at responding to a coherent paragraph than to a dozen small inline nits.
+- **Resolve threads as they're addressed.** Keeps the agent's next nudge focused.
+
+## Approvals
+
+An `APPROVED` review doesn't trigger the agent — it transitions the session toward `mergeable`. AO never auto-merges; that's your call.
+
+## Automated review (bugbot) detection
+
+Not every review comment is from a human. AO recognises a hardcoded list of known automation accounts and routes their comments to the separate `bugbot-comments` reaction instead of `changes-requested`. This lets you handle them differently — for example, treat advisory bot feedback as informational while still requiring a human approve before the agent acts on it.
+
+**GitHub** (`scm-github`) treats the following as bots:
+
+| Login | Tool |
+|---|---|
+| `cursor[bot]` | Cursor AI |
+| `github-actions[bot]` | GitHub Actions |
+| `codecov[bot]` | Codecov |
+| `sonarcloud[bot]` | SonarCloud |
+| `dependabot[bot]` | Dependabot |
+| `renovate[bot]` | Renovate |
+| `codeclimate[bot]` | Code Climate |
+| `deepsource-autofix[bot]` | DeepSource |
+| `snyk-bot` | Snyk |
+| `lgtm-com[bot]` | LGTM |
+
+**GitLab** (`scm-gitlab`) treats the following as bots (in addition to any username matching `project_\d+_bot` or ending in `[bot]`):
+
+| Login | Tool |
+|---|---|
+| `gitlab-bot` | GitLab built-in |
+| `ghost` | Deleted / system user |
+| `dependabot[bot]` | Dependabot |
+| `renovate[bot]` | Renovate |
+| `sast-bot` | GitLab SAST |
+| `codeclimate[bot]` | Code Climate |
+| `sonarcloud[bot]` | SonarCloud |
+| `snyk-bot` | Snyk |
+
+A typical configuration pairing:
+
+```yaml title="agent-orchestrator.yaml"
+reactions:
+ changes-requested:
+ auto: true
+ priority: "action" # human review comment — act immediately
+
+ bugbot-comments:
+ auto: true
+ priority: "info" # advisory bot feedback — log and proceed
+```
+
+The bot list is hardcoded in each SCM plugin and is not currently configurable via `agent-orchestrator.yaml`.
+
+## Review polling throttle
+
+To avoid hammering the GitHub / GitLab API on busy repositories, AO throttles `getPendingComments` and `getAutomatedComments` calls to **at most once every 2 minutes per session** (`REVIEW_BACKLOG_THROTTLE_MS = 2 * 60 * 1000`). The throttle is in-memory and resets on daemon restart.
+
+Practical consequence: after a review comment lands on the PR, there can be **up to a 2-minute delay** before AO reacts. This is expected and by design.
+
+If you need an immediate check outside the polling cadence, run:
+
+```bash
+ao review-check # check all tracked PRs right now
+ao review-check myproject # one project only
+ao review-check --dry-run # show what would be sent, don't send
+```
+
+`ao review-check` is a standalone CLI command that calls the GitHub API directly and is not subject to the in-process throttle.
diff --git a/frontend/src/landing/content/docs/index.mdx b/frontend/src/landing/content/docs/index.mdx
new file mode 100644
index 000000000..c31f237e3
--- /dev/null
+++ b/frontend/src/landing/content/docs/index.mdx
@@ -0,0 +1,80 @@
+---
+title: Introduction
+description: Learn what Agent Orchestrator does, when to use it, and where to start.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+Agent Orchestrator (**AO**) runs AI coding agents in isolated git worktrees and keeps track of the work until it becomes a pull request.
+
+Use it when you have several well-scoped issues and want agents to work on them at the same time without sharing a checkout, terminal, or branch. AO starts each session, watches the agent, tracks the PR, and shows the state of every session in one dashboard.
+
+
+ If you are new to AO, install it first, then run the quickstart against one small issue. Start with [Installation](/docs/installation), then [Quickstart](/docs/quickstart).
+
+
+## What AO Is For
+
+AO is useful when you want to:
+
+- Run more than one coding agent at the same time.
+- Keep every agent in its own worktree, branch, and session.
+- Watch agent terminals and PR state from one dashboard.
+- Let AO wake an agent when CI fails or a review requests changes.
+- Use different agent CLIs across projects or roles, such as Claude Code, Codex, Cursor, Aider, and OpenCode.
+
+AO works best for issues that have a clear outcome: failing tests, small features, focused refactors, migrations, documentation updates, and review follow-ups.
+
+## What AO Does Not Do
+
+AO does not replace review. It helps agents keep working, but you still decide what gets merged.
+
+| AO handles | You still handle |
+| --- | --- |
+| Creating isolated workspaces | Choosing good issues |
+| Launching and monitoring agents | Reviewing code and behavior |
+| Tracking PR, CI, and review status | Deciding when to merge |
+| Cleaning up merged sessions | Setting repo-specific rules and expectations |
+
+## How A Session Moves
+
+```text
+issue -> ao spawn -> worktree + agent -> pull request -> CI/review loop -> merge -> cleanup
+```
+
+The dashboard shows this lifecycle as session cards. Open a card to see the live terminal, current activity state, issue link, PR link, and worktree path.
+
+## How AO Fits Together
+
+AO is built from plugins. The default setup works out of the box for common GitHub workflows, and you can swap pieces when your setup is different.
+
+| Plugin slot | What it controls | Examples |
+| --- | --- | --- |
+| Agent | Which coding tool writes changes | Claude Code, Codex, Cursor, Aider, OpenCode |
+| Runtime | How the agent process runs | tmux, child process |
+| Workspace | Where code is checked out | git worktree, full clone |
+| Tracker | Where issues come from | GitHub, GitLab, Linear |
+| SCM | How PRs, reviews, and CI are read | GitHub, GitLab |
+| Notifier | Where AO sends updates | desktop, Slack, Discord, webhook |
+
+Most users start with the defaults and only edit `agent-orchestrator.yaml` when they need a different agent, runtime, tracker, or notification target.
+
+## Platform Support
+
+Windows support is actively improving. Use the process runtime instead of tmux by setting defaults.runtime: process in agent-orchestrator.yaml. See Platforms for details.>}
+/>
+
+## Where To Go Next
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/installation.mdx b/frontend/src/landing/content/docs/installation.mdx
new file mode 100644
index 000000000..2b6731f9d
--- /dev/null
+++ b/frontend/src/landing/content/docs/installation.mdx
@@ -0,0 +1,209 @@
+---
+title: Installation
+description: Install AO, authenticate the tools it controls, and verify your first project.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+import { Tab, Tabs } from "fumadocs-ui/components/tabs";
+import { Step, Steps } from "fumadocs-ui/components/steps";
+import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
+
+This page gets your machine ready to run Agent Orchestrator. By the end, the `ao` command should be on your `PATH`, your source-control CLI should be authenticated, and `ao doctor` should be able to explain what is ready or missing.
+
+
+ The published package is `@aoagents/ao`. It provides the global `ao` command and includes the built-in CLI, dashboard, and plugin packages.
+
+
+## Prerequisites
+
+Windows support is actively improving. Use defaults.runtime: process instead of tmux. See Platforms.>}
+/>
+
+Install these before running AO:
+
+| Tool | Required | Why AO needs it |
+| --- | --- | --- |
+| Node.js 20+ | Yes | Runs the AO CLI, dashboard, and plugins. |
+| Git | Yes | Creates worktrees, branches, commits, and cleanup operations. |
+| GitHub CLI `gh` | For GitHub projects | Reads issues, PRs, reviews, and CI status as your user. |
+| tmux | Default on macOS/Linux | Keeps long-running agent sessions attachable and recoverable. |
+| An agent CLI | Yes | The worker that writes code, such as Claude Code, Codex, Cursor, Aider, or OpenCode. |
+
+If you plan to use GitLab or Linear, install and authenticate their CLIs or credentials as described on the related plugin pages. A GitHub-only setup only needs `gh`.
+
+## Install AO
+
+
+
+ ```bash
+ npm install -g @aoagents/ao
+ ```
+
+
+ ```bash
+ pnpm add -g @aoagents/ao
+ ```
+
+
+ ```bash
+ yarn global add @aoagents/ao
+ ```
+
+
+ ```bash
+ git clone https://github.com/ComposioHQ/agent-orchestrator
+ cd agent-orchestrator
+ pnpm install
+ pnpm build
+ pnpm --filter @aoagents/ao link --global
+ ```
+
+
+
+Confirm the command is available:
+
+```bash
+ao --version
+```
+
+## Authenticate Your Tools
+
+
+
+
+### Authenticate source control
+
+For GitHub projects, AO uses `gh` for issue lookup, PR discovery, reviews, CI checks, and merge readiness.
+
+```bash
+gh auth login
+gh auth status
+```
+
+The authenticated account must be able to read the repository, create branches, push branches, open PRs, and read CI status.
+
+
+
+
+### Install and sign in to one agent
+
+AO launches an agent CLI inside each worker session. Start with the one you already use, then add more later if you want per-project or per-role choices.
+
+
+
+ ```bash
+ npm install -g @anthropic-ai/claude-code
+ claude
+ ```
+
+
+ ```bash
+ npm install -g @openai/codex
+ codex
+ ```
+
+
+ ```bash
+ curl https://cursor.com/install -fsS | bash
+ agent --help
+ ```
+
+
+ ```bash
+ pip install aider-install
+ aider-install
+ aider --help
+ ```
+
+
+ ```bash
+ npm install -g opencode-ai
+ opencode --help
+ ```
+
+
+
+
+
+
+### Start AO in a repo
+
+Run `ao start` from a repository you want AO to manage:
+
+```bash
+cd ~/code/my-repo
+ao start
+```
+
+If there is no `agent-orchestrator.yaml`, AO creates one. It also starts the dashboard and an orchestrator session for the project.
+
+You can also start from a path or clone from a URL:
+
+```bash
+ao start ~/code/my-repo
+ao start https://github.com/your-org/your-repo
+```
+
+
+
+
+### Run the doctor check
+
+```bash
+ao doctor
+```
+
+`ao doctor` checks the launcher, config, plugin resolution, runtime tools, source-control authentication, notifier setup, and stale temporary files. If it reports a fixable issue, run:
+
+```bash
+ao doctor --fix
+```
+
+
+
+
+## Windows Setup
+
+Windows support is in progress. Use the process runtime instead of tmux:
+
+```yaml title="agent-orchestrator.yaml"
+defaults:
+ runtime: process
+```
+
+Use Slack, Discord, webhook, or another network notifier for alerts. Desktop notifications and iTerm2 integration are not available on Windows.
+
+## Common Install Issues
+
+
+
+ Your global package bin directory is not on `PATH`. Check your package manager's global bin path, then reopen your shell and run `ao --version` again.
+
+
+ Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is authenticated.
+
+
+ Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container.
+
+
+ Install one supported agent CLI and run it once outside AO so it can complete its own sign-in flow.
+
+
+ `ao start` scans for a free port and prints the final dashboard URL. Use the URL in the command output.
+
+
+ Run `ao dashboard --rebuild` to clean stale dashboard build artifacts and start again.
+
+
+
+## Next
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/meta.json b/frontend/src/landing/content/docs/meta.json
new file mode 100644
index 000000000..0b0e64d6f
--- /dev/null
+++ b/frontend/src/landing/content/docs/meta.json
@@ -0,0 +1,24 @@
+{
+ "title": "Docs",
+ "pages": [
+ "---Getting Started---",
+ "index",
+ "installation",
+ "quickstart",
+ "platforms",
+ "---Learn---",
+ "guides",
+ "plugins",
+ "---Reference---",
+ "cli",
+ "configuration",
+ "architecture",
+ "examples",
+ "dashboard",
+ "---Help---",
+ "troubleshooting",
+ "faq",
+ "migration",
+ "changelog"
+ ]
+}
diff --git a/frontend/src/landing/content/docs/migration.mdx b/frontend/src/landing/content/docs/migration.mdx
new file mode 100644
index 000000000..8cd2f302b
--- /dev/null
+++ b/frontend/src/landing/content/docs/migration.mdx
@@ -0,0 +1,57 @@
+---
+title: Migration
+description: Breaking changes between AO versions and how to upgrade cleanly.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+
+ AO is in active development and follows semver pre-1.0. Breaking changes are called out here with migration steps.
+
+
+## From `@composio/agent-orchestrator` to `@aoagents/ao`
+
+The npm scope moved to `@aoagents/ao`. If you installed under the old name:
+
+```bash
+npm uninstall -g @composio/agent-orchestrator
+npm install -g @aoagents/ao
+```
+
+Your config and data directory (`~/.agent-orchestrator`) don't change — no data migration needed. GitHub org and repo URLs (`ComposioHQ/agent-orchestrator`) didn't change either.
+
+## `ao spawn ` → `ao spawn `
+
+The old two-argument form is rejected with an error. Pick the project via `-p` or by running from inside its worktree:
+
+```bash
+# old
+ao spawn myproject 42
+
+# new
+ao spawn 42 -p myproject
+# or, from inside the project's worktree:
+ao spawn 42
+```
+
+## `ao init` → `ao start`
+
+`ao init` has been removed. Use `ao start` instead; it auto-creates `agent-orchestrator.yaml` on first run and opens the dashboard in one step.
+
+## Upgrading
+
+Preferred:
+
+```bash
+ao update
+```
+
+The command detects your install method (`npm-global`, `pnpm-global`, git, or unknown) and picks the right upgrade path. Override behaviour with `--skip-smoke` or `--smoke-only` for git installs.
+
+## Before major upgrades
+
+1. `ao status` — see what's running.
+2. `ao session cleanup` — archive finished sessions.
+3. `ao stop --all` — halt every AO instance.
+4. Upgrade.
+5. `ao doctor` on the new version.
diff --git a/frontend/src/landing/content/docs/platforms.mdx b/frontend/src/landing/content/docs/platforms.mdx
new file mode 100644
index 000000000..e6f2196f0
--- /dev/null
+++ b/frontend/src/landing/content/docs/platforms.mdx
@@ -0,0 +1,155 @@
+---
+title: Platforms
+description: Choose the right AO runtime, terminal, and notification setup for macOS, Linux, Windows, and remote hosts.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+AO's core workflow is the same on every platform: create an isolated workspace, run an agent, track the PR, react to CI and review events, and show progress in the dashboard.
+
+The platform differences come from the tools used to run and attach to long-lived sessions.
+
+Windows support is actively improving. Use the process runtime instead of tmux.>}
+/>
+
+## Recommended Setup
+
+| Platform | Runtime | Notifications | Notes |
+| --- | --- | --- | --- |
+| macOS | `tmux` | `desktop`, Slack, Discord, webhook | Best local experience. iTerm2 attach support is macOS-only. |
+| Linux | `tmux` | `desktop`, Slack, Discord, webhook | Best server and workstation setup. Desktop notifications need `notify-send`. |
+| Windows | `process` | Slack, Discord, webhook | Native tmux and iTerm2 are unavailable. Windows support is in progress. |
+| Container or remote VM | `process` or `tmux` | Slack, Discord, webhook | Use persistent storage for AO data and protect the dashboard with your own auth. |
+
+## macOS
+
+macOS is the default local development target.
+
+Install tmux before using the default runtime:
+
+```bash
+brew install tmux
+```
+
+Recommended config:
+
+```yaml title="agent-orchestrator.yaml"
+defaults:
+ runtime: tmux
+ notifiers:
+ - desktop
+```
+
+What works well on macOS:
+
+| Capability | Status |
+| --- | --- |
+| tmux-backed worker sessions | Supported |
+| Browser dashboard terminal | Supported |
+| iTerm2 attach/open helpers | Supported |
+| Desktop notifications | Supported |
+| macOS idle sleep prevention while AO runs | Supported |
+
+
+ AO can prevent idle sleep on macOS while agents run, but it cannot override hardware lid-close sleep. Use normal clamshell mode if you need the machine available while closed.
+
+
+## Linux
+
+Linux is a good fit for local workstations, remote development machines, and always-on hosts.
+
+Install tmux with your distribution package manager:
+
+```bash
+sudo apt install tmux
+```
+
+Recommended config:
+
+```yaml title="agent-orchestrator.yaml"
+defaults:
+ runtime: tmux
+ notifiers:
+ - desktop
+```
+
+What to know:
+
+| Capability | Status |
+| --- | --- |
+| tmux-backed worker sessions | Supported |
+| Browser dashboard terminal | Supported |
+| iTerm2 attach/open helpers | Not available |
+| Desktop notifications | Supported when `notify-send` is installed |
+| Remote dashboard access | Supported through port forwarding, Tailscale, or your proxy |
+
+If you are running AO on a headless Linux host, prefer Slack, Discord, or webhook notifications over desktop notifications.
+
+## Windows
+
+
+ The native Windows path uses `runtime: process`. The core spawn and PR workflow is available, but tmux-specific workflows and iTerm2 helpers do not apply.
+
+
+Recommended config:
+
+```yaml title="agent-orchestrator.yaml"
+defaults:
+ runtime: process
+ notifiers:
+ - slack
+```
+
+What works:
+
+| Capability | Status |
+| --- | --- |
+| `ao start`, `ao stop`, `ao dashboard` | Supported |
+| `ao spawn` and `ao spawn --prompt` | Supported through the process runtime |
+| GitHub, GitLab, and Linear tracker/SCM integrations | Supported when their CLIs or credentials are configured |
+| Browser dashboard terminal | Supported through the direct PTY server |
+| Slack, Discord, webhook, Composio, and OpenClaw notifiers | Supported |
+
+What is limited:
+
+| Capability | Status | Use instead |
+| --- | --- | --- |
+| `runtime: tmux` | Not available natively | `runtime: process` |
+| iTerm2 terminal integration | Not available | Browser dashboard terminal |
+| Desktop notifier | No-op on Windows | Slack, Discord, webhook, Composio, or OpenClaw |
+| tmux attach commands | Not available | Dashboard terminal and AO session commands |
+
+Use PowerShell or Git Bash for normal CLI commands. If a command behaves differently because of shell quoting, put longer instructions in a file and send them with `ao send --file`.
+
+## Containers And Remote Hosts
+
+AO can run on a remote machine or inside a container, but the dashboard has no built-in authentication. Put it behind your own access layer before exposing it beyond localhost.
+
+Recommended config for containers:
+
+```yaml title="agent-orchestrator.yaml"
+defaults:
+ runtime: process
+ notifiers:
+ - webhook
+```
+
+Operational notes:
+
+- Mount or preserve AO's data directory so session metadata survives restarts.
+- Forward the dashboard port, usually `3000`, only to trusted networks.
+- Prefer network notifiers over desktop notifications.
+- Use `tmux` only if it is installed and you want attachable terminal sessions inside the host.
+
+## Choosing A Runtime
+
+| Runtime | Best for | Tradeoff |
+| --- | --- | --- |
+| `tmux` | macOS/Linux machines where you want durable, attachable terminal sessions | Requires tmux and does not work natively on Windows |
+| `process` | Windows, containers, and simpler process-managed environments | Less of the workflow is tmux-attachable |
+
+Start with the recommended runtime for your OS. Change it only when the default runtime does not match where AO is running.
diff --git a/frontend/src/landing/content/docs/plugins/agents/aider.mdx b/frontend/src/landing/content/docs/plugins/agents/aider.mdx
new file mode 100644
index 000000000..87cae4d30
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/agents/aider.mdx
@@ -0,0 +1,56 @@
+---
+title: Aider
+description: Aider pair-programming CLI. No session resume; AO tracks activity via terminal classification.
+---
+
+import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
+
+
+
+ Slot: agent · Name: aider · Binary: aider
+
+
+[Aider](https://aider.chat) is a pair-programming CLI built around explicit file edits. It doesn't have a session-resume concept, but it does work well for small, focused issues.
+
+
+
+## Install
+
+```bash
+pip install aider-install && aider-install
+```
+
+Set your API key via Aider's normal config (`~/.aider.conf.yml` or env vars). AO doesn't manage credentials for you.
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+agent: aider
+```
+
+## How it works
+
+- **Launch:** `aider` runs in the worktree. AO pipes the issue prompt in via the normal Aider UX.
+- **Activity tracking:** Aider doesn't emit a structured event log. AO writes `{workspace}/.ao/activity.jsonl` based on terminal output classification — the pattern matcher knows Aider's common prompts (diff review, confirm apply, etc.).
+- **PR + git tracking:** PATH wrappers for `gh` / `git` record PRs and commits.
+- **Session resume:** Not supported.
+
+## Environment variables
+
+| Variable | Set by AO | Purpose |
+|---|---|---|
+| `AO_SESSION_ID` | ✓ | AO session id |
+| `AO_ISSUE_ID` | ✓ | Issue identifier |
+| `PATH` | ✓ | Prepends `~/.ao/bin` |
+| `GH_PATH` | ✓ | Absolute path to real `gh` |
+
+## Troubleshooting
+
+
+
+ Aider prompts before applying diffs. If you want fully autonomous runs, configure Aider's `--yes-always` via `~/.aider.conf.yml`. AO won't inject this for you — it's your call whether the agent should auto-apply.
+
+
+ AO doesn't parse Aider's cost output. The cost column stays empty.
+
+
diff --git a/frontend/src/landing/content/docs/plugins/agents/claude-code.mdx b/frontend/src/landing/content/docs/plugins/agents/claude-code.mdx
new file mode 100644
index 000000000..5506b87da
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/agents/claude-code.mdx
@@ -0,0 +1,66 @@
+---
+title: Claude Code
+description: Anthropic's CLI coding agent. The default agent in AO.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+
+
+
+[Claude Code](https://docs.anthropic.com/en/docs/claude-code) is Anthropic's CLI coding agent. It's AO's default because it has first-class session resume, rich JSONL event logs, and a native hook system AO can register against.
+
+
+
+## Install
+
+```bash
+npm install -g @anthropic-ai/claude-code
+```
+
+Then sign in once:
+
+```bash
+claude
+```
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+agent: claude-code
+```
+
+That's it — there are no plugin-level config keys.
+
+## How it works
+
+- **Launch:** `claude` runs inside the worktree. AO sets `CLAUDECODE=""`, `AO_SESSION_ID`, and `AO_ISSUE_ID` in the environment.
+- **Activity tracking:** AO registers a PostToolUse hook in `.claude/settings.json` on workspace setup. The hook writes an AO activity entry each time Claude calls a tool, so the dashboard's `active/ready/idle` states stay accurate.
+- **Session resume:** `claude --resume ` rehydrates the previous chat. AO maps its session ID to Claude's via the JSONL at `~/.claude/projects//...jsonl`.
+- **Cost reporting:** AO reads Claude's per-message cost field from the JSONL — visible on the dashboard card.
+
+## Environment variables
+
+| Variable | Set by AO | Purpose |
+|---|---|---|
+| `AO_SESSION_ID` | ✓ | Unique AO session identifier |
+| `AO_ISSUE_ID` | ✓ (when spawned from an issue) | Issue identifier |
+| `CLAUDECODE` | ✓ | Signals to Claude that it's running under a harness |
+
+Your Anthropic API key comes from wherever `claude` normally reads it — `~/.claude/`, env, etc. AO doesn't touch it.
+
+## Troubleshooting
+
+
+
+ `npm install -g @anthropic-ai/claude-code`, then reopen your terminal so the PATH picks it up.
+
+
+ AO registers the PostToolUse hook in `.claude/settings.json` on spawn. If the hook got removed, run `ao spawn` again — AO re-installs it idempotently.
+
+
+ Claude's session JSONL lives at `~/.claude/projects//.jsonl`. If that file is gone (cleaned up, moved), resume won't work — spawn fresh.
+
+
diff --git a/frontend/src/landing/content/docs/plugins/agents/codex.mdx b/frontend/src/landing/content/docs/plugins/agents/codex.mdx
new file mode 100644
index 000000000..5d99333e3
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/agents/codex.mdx
@@ -0,0 +1,62 @@
+---
+title: Codex
+description: OpenAI's Codex CLI. Full session resume and native JSONL event stream.
+---
+
+import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
+
+
+
+ Slot: agent · Name: codex · Binary: codex
+
+
+[OpenAI Codex CLI](https://github.com/openai/codex) is a terminal coding agent from OpenAI. AO hooks into it via PATH wrappers and reads its native session JSONL for activity + cost.
+
+
+
+## Install
+
+```bash
+npm install -g @openai/codex
+```
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+agent: codex
+```
+
+No plugin-level config.
+
+## How it works
+
+- **Launch:** `codex` runs inside the worktree. AO sets `CODEX_DISABLE_UPDATE_CHECK=1` to keep the agent quiet about updates mid-session.
+- **Activity tracking:** Codex writes session events to `~/.codex/sessions/YYYY/MM/DD/.jsonl`. AO reads the last entry to determine `active/ready/idle/waiting_input/blocked`.
+- **PR + git tracking:** AO installs wrappers at `~/.ao/bin/gh` and `~/.ao/bin/git`. When Codex runs `gh pr create`, the wrapper records the PR number in the session metadata.
+- **Session resume:** `codex resume ` — AO stores the thread id in session metadata and replays it.
+
+## Environment variables
+
+| Variable | Set by AO | Purpose |
+|---|---|---|
+| `AO_SESSION_ID` | ✓ | AO session id |
+| `AO_ISSUE_ID` | ✓ | Issue identifier |
+| `PATH` | ✓ | Prepends `~/.ao/bin` for the wrappers |
+| `GH_PATH` | ✓ | Absolute path to the real `gh`, used by the wrapper |
+| `CODEX_DISABLE_UPDATE_CHECK` | ✓ (`1`) | Skip version check |
+
+OpenAI API credentials come from wherever `codex` reads them — AO doesn't set them.
+
+## Troubleshooting
+
+
+
+ npm-global install sometimes misses the PATH. Run `which codex` — if empty, add your global bin (`npm config get prefix` + `/bin`) to PATH.
+
+
+ AO reads `~/.codex/sessions/`. If the date-sharded path doesn't match your clock (e.g. Docker container with wrong TZ), sessions look stale — align the clocks.
+
+
+ The PATH wrapper writes session metadata on `gh pr create`. If `gh` was invoked with an absolute path (bypassing the wrapper) AO won't see it. `ao session claim-pr ` fixes this retroactively.
+
+
diff --git a/frontend/src/landing/content/docs/plugins/agents/cursor.mdx b/frontend/src/landing/content/docs/plugins/agents/cursor.mdx
new file mode 100644
index 000000000..32ea1fae8
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/agents/cursor.mdx
@@ -0,0 +1,68 @@
+---
+title: Cursor
+description: Cursor Agent CLI — IDE-like editing from the terminal, with PATH-wrapper PR tracking.
+---
+
+import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
+
+
+
+ Slot: agent · Name: cursor · Binary: agent
+
+
+The [Cursor Agent CLI](https://cursor.com/docs) brings Cursor's editing model to a terminal. AO spawns it per session, wraps `gh`/`git`, and tracks activity via the AO activity log.
+
+
+
+## Install
+
+```bash
+curl https://cursor.com/install -fsS | bash
+```
+
+Verify:
+
+```bash
+agent --help
+```
+
+You should see "Cursor Agent" in the output. If you don't, the `agent` binary in your PATH is something else (e.g. an unrelated tool) — reorder your PATH so the Cursor one wins.
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+agent: cursor
+```
+
+No plugin-level config.
+
+## How it works
+
+- **Launch:** AO runs `agent` inside the worktree.
+- **Activity tracking:** AO writes a JSONL to `{workspace}/.ao/activity.jsonl` each poll cycle based on terminal output classification. The dashboard reads from there.
+- **PR + git tracking:** PATH wrappers at `~/.ao/bin/gh` / `~/.ao/bin/git` intercept calls to record PR metadata.
+- **Session resume:** Not supported — every spawn is a fresh Cursor session.
+
+## Detection
+
+The plugin's `detect()` checks `agent --help` output for the strings `Cursor Agent` and `--approve-mcps` to avoid false positives with other binaries named `agent`.
+
+## Environment variables
+
+| Variable | Set by AO | Purpose |
+|---|---|---|
+| `AO_SESSION_ID` | ✓ | AO session id |
+| `AO_ISSUE_ID` | ✓ | Issue identifier |
+| `PATH` | ✓ | Prepends `~/.ao/bin` |
+| `GH_PATH` | ✓ | Absolute path to real `gh` |
+
+## Troubleshooting
+
+
+
+ The plugin only recognises the real Cursor binary by its help output. Run `agent --help` — if you see a different tool's help, remove or shadow it, or reinstall Cursor Agent.
+
+
+ Cursor doesn't emit its own session JSONL. AO classifies based on terminal output — if your terminal is being captured by another tool, the classification can drift. `ao doctor` will flag this.
+
+
diff --git a/frontend/src/landing/content/docs/plugins/agents/index.mdx b/frontend/src/landing/content/docs/plugins/agents/index.mdx
new file mode 100644
index 000000000..809ab3485
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/agents/index.mdx
@@ -0,0 +1,67 @@
+---
+title: Agents overview
+description: Pick the agent that writes the code. AO supports five today, and they're swappable per-project.
+---
+
+The agent is the piece that actually writes code. Everything else in AO — worktrees, runtimes, notifiers — is plumbing around it.
+
+
+**PR metadata is captured automatically.** Non-Claude-Code agents (Codex, Cursor, Aider, OpenCode) use `~/.ao/bin/gh` and `~/.ao/bin/git` PATH wrappers that intercept commands like `gh pr create` and record the resulting PR number and branch so the dashboard stays in sync. Claude Code uses PostToolUse hooks in `.claude/settings.json` instead — the same metadata is written through Claude Code's native hook system. Both approaches are set up transparently by AO; you don't need to configure anything.
+
+
+## Supported agents
+
+| Agent | Slot name | Binary | Session resume |
+|---|---|---|---|
+| [Claude Code](/docs/plugins/agents/claude-code) | `claude-code` | `claude` | ✅ `--resume` |
+| [Codex](/docs/plugins/agents/codex) | `codex` | `codex` | ✅ `codex resume ` |
+| [Cursor](/docs/plugins/agents/cursor) | `cursor` | `agent` | ❌ |
+| [Aider](/docs/plugins/agents/aider) | `aider` | `aider` | ❌ |
+| [OpenCode](/docs/plugins/agents/opencode) | `opencode` | `opencode` | ✅ via OpenCode session API |
+
+
+
+
+
+
+
+
+
+## Choosing
+
+- **Want session resume** (pick up where you left off)? Claude Code, Codex, or OpenCode.
+- **One-shot per session** is fine? Any of the five.
+- **Pair-programming style editor**? Aider.
+- **Native IDE-like behavior in the terminal**? Cursor.
+
+## How AO talks to them
+
+Two patterns exist:
+
+- **Agent-native hooks** — Claude Code ships a PostToolUse hook in `.claude/settings.json`. AO's workspace bootstrap installs it; the agent writes activity events directly to AO's JSONL.
+- **PATH wrappers** — Codex, Cursor, Aider, and OpenCode don't have a hook system. AO installs small shell wrappers in `~/.ao/bin/gh` and `~/.ao/bin/git` and prepends them to `PATH`. When the agent runs `gh pr create` or `git commit`, the wrapper records metadata (PR number, branch, etc.) so the dashboard knows what the agent did.
+
+Neither approach requires you to change your agent's config — AO sets it up transparently.
+
+## Mixing agents
+
+Per-project overrides:
+
+```yaml
+agent: claude-code # global default
+projects:
+ api:
+ repo: myorg/api
+ agent: codex
+ web:
+ repo: myorg/web
+ agent: cursor
+```
+
+You can also override at spawn time:
+
+```bash
+ao spawn 42 --agent codex
+```
+
+Great for A/B testing which agent does better on a specific kind of issue.
diff --git a/frontend/src/landing/content/docs/plugins/agents/meta.json b/frontend/src/landing/content/docs/plugins/agents/meta.json
new file mode 100644
index 000000000..92ba7bbe2
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/agents/meta.json
@@ -0,0 +1,10 @@
+{
+ "title": "Agents",
+ "pages": [
+ "claude-code",
+ "codex",
+ "cursor",
+ "aider",
+ "opencode"
+ ]
+}
diff --git a/frontend/src/landing/content/docs/plugins/agents/opencode.mdx b/frontend/src/landing/content/docs/plugins/agents/opencode.mdx
new file mode 100644
index 000000000..48e3417e3
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/agents/opencode.mdx
@@ -0,0 +1,57 @@
+---
+title: OpenCode
+description: OpenCode terminal agent. Uses the OpenCode session API for resume and discovery.
+---
+
+import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
+
+
+
+[OpenCode](https://opencode.ai) is an open-source terminal coding agent. It has a structured session API, which means AO can discover, resume, and track its sessions reliably.
+
+
+
+## Install
+
+```bash
+npm install -g opencode-ai
+```
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+agent: opencode
+```
+
+No plugin-level config.
+
+## How it works
+
+- **Launch:** `opencode` starts in the worktree with `AO:` as the session title — this is how AO finds it back later.
+- **Session discovery:** `opencode session list --format json` returns the list; AO matches on the title prefix.
+- **Resume:** `opencode --session ` rehydrates the state.
+- **Activity tracking:** Primary signal is the OpenCode session's `updatedAt`. AO falls back to its own activity JSONL if the session API doesn't answer.
+- **PR + git tracking:** PATH wrappers for `gh` / `git`.
+
+## Environment variables
+
+| Variable | Set by AO | Purpose |
+|---|---|---|
+| `AO_SESSION_ID` | ✓ | AO session id |
+| `AO_ISSUE_ID` | ✓ | Issue identifier |
+| `PATH` | ✓ | Prepends `~/.ao/bin` |
+| `GH_PATH` | ✓ | Absolute path to real `gh` |
+
+## Troubleshooting
+
+
+
+ `ao session remap ` re-discovers and persists the OpenCode session mapping. Use `--force` to override a stale mapping.
+
+
+ If the OpenCode session API doesn't respond, AO falls back to the activity JSONL, which uses age-based decay. If even that returns `idle` forever, check that `opencode session list` works on its own.
+
+
diff --git a/frontend/src/landing/content/docs/plugins/authoring.mdx b/frontend/src/landing/content/docs/plugins/authoring.mdx
new file mode 100644
index 000000000..cd8798eb7
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/authoring.mdx
@@ -0,0 +1,593 @@
+---
+title: Authoring a plugin
+description: Write a custom runtime, agent, workspace, tracker, SCM, notifier, or terminal plugin for AO.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+import { Step, Steps } from "fumadocs-ui/components/steps";
+import { Card, Cards } from "fumadocs-ui/components/card";
+
+AO's seven user-pluggable slots (Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal) are all resolved at startup from the plugin registry. Each slot is backed by a TypeScript interface defined in `packages/core/src/types.ts`. Adding a new tracker, notifier, or any other integration is a matter of publishing an npm package — or pointing to a local path — that exports a manifest, a `create()` factory, and an optional `detect()` probe as its default export.
+
+---
+
+## The contract
+
+Every plugin module must satisfy `PluginModule`, where `T` is the interface for its slot:
+
+```typescript
+export interface PluginModule {
+ manifest: PluginManifest;
+ create(config?: Record): T;
+ detect?(): boolean;
+}
+
+export interface PluginManifest {
+ name: string; // must match the package-name suffix (see below)
+ slot: PluginSlot; // use "as const" to preserve the literal type
+ description: string;
+ version: string;
+ displayName?: string;
+}
+```
+
+### `manifest`
+
+The static description of your plugin. Every field is required except `displayName`.
+
+### `create(config?)`
+
+Called once during startup. `config` contains whatever key-value pairs appear under your plugin's YAML block. Validate everything here; store validated values in a closure. Return an object that implements the slot interface.
+
+### `detect()` (optional)
+
+Return `true` when the system has what your plugin needs — a binary on PATH, an environment variable, a running process. AO uses this to warn about missing dependencies at startup rather than at first use.
+
+### Default export
+
+```typescript
+import type { PluginModule, Notifier } from "@aoagents/ao-core";
+
+export const manifest = {
+ name: "my-notifier",
+ slot: "notifier" as const,
+ description: "Send alerts to My Service",
+ version: "0.1.0",
+};
+
+export function create(config?: Record): Notifier {
+ // validate here, return interface implementation
+}
+
+export function detect(): boolean {
+ return Boolean(process.env["MY_SERVICE_TOKEN"]);
+}
+
+export default { manifest, create, detect } satisfies PluginModule;
+```
+
+---
+
+## Manifest rules
+
+- **`name`** must match the suffix of your package name. `@acme/ao-plugin-notifier-pagerduty` → `name: "pagerduty"`. The registry looks up plugins by `slot:name` key.
+- **`slot`** must use `as const` to preserve the string literal type — `"notifier" as const`, not `"notifier"`.
+- **`version`** follows semver. Start at `0.1.0`.
+- **`displayName`** is optional. When omitted, `name` is used in log output.
+
+---
+
+## Create a plugin with `ao plugin create`
+
+The fastest way to start is `ao plugin create`. It runs an interactive prompt (using `@clack/prompts`) and writes a ready-to-build package.
+
+
+
+
+### Run the scaffold command
+
+```bash
+ao plugin create
+```
+
+You can also pass `[directory]` as a positional argument, or supply any field with flags to skip individual prompts:
+
+```bash
+ao plugin create ./my-plugins/pagerduty \
+ --slot notifier \
+ --name "PagerDuty" \
+ --description "Route urgent alerts to PagerDuty" \
+ --author "Alice" \
+ --package-name "ao-plugin-notifier-pagerduty"
+```
+
+Add `--non-interactive` to require all fields to be provided via flags (useful in CI).
+
+
+
+
+### Answer the interactive prompts
+
+The CLI asks for four things, in order:
+
+| Prompt | Example input |
+|--------|--------------|
+| Plugin display name | `PagerDuty` |
+| Plugin slot | `notifier` (selected from a list of all 7 slots) |
+| Short description | `Route urgent alerts to PagerDuty` |
+| Author | `Alice` |
+| Package name | `ao-plugin-notifier-pagerduty` (pre-filled from slot + name) |
+
+
+
+
+### Inspect the generated files
+
+The scaffold is written to `./{normalized-name}/` (or the directory you provided). The generated structure is:
+
+```
+pagerduty/
+├── package.json # type: module, main: dist/index.js, exports map
+├── tsconfig.json # ES2022 + Node16 modules, strict mode
+├── README.md # AO config snippets for local and npm installs
+├── .gitignore # dist/, node_modules/
+└── src/
+ └── index.ts # manifest + stub create() + default export
+```
+
+The generated `src/index.ts` looks like this:
+
+```typescript
+import type { PluginModule } from "@aoagents/ao-core";
+
+export const manifest = {
+ name: "pagerduty",
+ slot: "notifier" as const,
+ description: "Route urgent alerts to PagerDuty",
+ version: "0.1.0",
+ displayName: "PagerDuty",
+};
+
+const plugin: PluginModule = {
+ manifest,
+ create(config?: Record) {
+ return {
+ name: manifest.name,
+ config: config ?? {},
+ // TODO: replace this placeholder with a real notifier implementation.
+ };
+ },
+};
+
+export default plugin;
+```
+
+
+
+
+### Implement the slot interface
+
+Replace the placeholder `create()` body with a real implementation. Import the slot interface from `@aoagents/ao-core`:
+
+```typescript
+import type { PluginModule, Notifier } from "@aoagents/ao-core";
+import { validateUrl } from "@aoagents/ao-core";
+```
+
+See [Slot interfaces at a glance](#slot-interfaces-at-a-glance) for the methods you need to implement.
+
+Then build:
+
+```bash
+npm install
+npm run build
+```
+
+
+
+
+### Register it in `agent-orchestrator.yaml`
+
+For local development, use `source: local`:
+
+```yaml
+plugins:
+ - name: pagerduty
+ source: local
+ path: ./pagerduty
+
+notifiers:
+ pagerduty-urgent:
+ plugin: pagerduty
+ routing_key: "your-integration-key"
+
+notificationRouting:
+ urgent: [pagerduty-urgent]
+```
+
+Once published to npm, switch to `source: registry` (or `source: npm`):
+
+```yaml
+plugins:
+ - name: pagerduty
+ source: registry
+ package: "ao-plugin-notifier-pagerduty"
+ version: "^1.0.0"
+```
+
+
+
+
+---
+
+## Slot interfaces at a glance
+
+All interfaces are defined in `packages/core/src/types.ts` and exported from `@aoagents/ao-core`.
+
+### Runtime
+
+| Method | Signature | Required |
+|--------|-----------|---------|
+| `create` | `(config: RuntimeCreateConfig) => Promise` | yes |
+| `destroy` | `(handle: RuntimeHandle) => Promise` | yes |
+| `sendMessage` | `(handle: RuntimeHandle, message: string) => Promise` | yes |
+| `getOutput` | `(handle: RuntimeHandle, lines?: number) => Promise` | yes |
+| `isAlive` | `(handle: RuntimeHandle) => Promise` | yes |
+| `getMetrics` | `(handle: RuntimeHandle) => Promise` | optional |
+| `getAttachInfo` | `(handle: RuntimeHandle) => Promise` | optional |
+
+### Agent
+
+Agent plugins have the richest interface. Methods are split into required and optional:
+
+**Required:**
+
+| Method | Purpose | Returns `null`? |
+|--------|---------|----------------|
+| `getLaunchCommand` | Shell command to start the agent | No |
+| `getEnvironment` | Env vars for the process (must include `~/.ao/bin` in PATH) | No |
+| `detectActivity` | Terminal-output activity classification (deprecated but required) | No |
+| `getActivityState` | JSONL/API-based activity detection | Yes (if no data) |
+| `isProcessRunning` | Check whether the process is still alive | No (returns `false`) |
+| `getSessionInfo` | Extract summary, cost, session ID from agent data | Yes |
+
+**Optional:**
+
+| Method | Purpose | When to skip |
+|--------|---------|-------------|
+| `getRestoreCommand` | Resume a previous session | Agent has no resume capability |
+| `setupWorkspaceHooks` | Install metadata hooks for PR tracking | Never — required for the dashboard |
+| `postLaunchSetup` | Post-launch config | Only if no post-launch work is needed |
+| `recordActivity` | Write terminal-derived activity to JSONL | Agent has native JSONL (Claude Code) |
+
+
+`setupWorkspaceHooks` is marked optional in the TypeScript interface but is critical in practice. Without it, PRs created by your agent will never appear in the dashboard. See [Agent plugin specifics](#agent-plugin-specifics) for the two patterns (agent-native hooks vs PATH wrappers).
+
+
+### Workspace
+
+| Method | Signature | Required |
+|--------|-----------|---------|
+| `create` | `(config: WorkspaceCreateConfig) => Promise` | yes |
+| `destroy` | `(workspacePath: string) => Promise` | yes |
+| `list` | `(projectId: string) => Promise` | yes |
+| `postCreate` | `(info: WorkspaceInfo, project: ProjectConfig) => Promise` | optional |
+| `exists` | `(workspacePath: string) => Promise` | optional |
+| `restore` | `(config: WorkspaceCreateConfig, workspacePath: string) => Promise` | optional |
+
+### Tracker
+
+| Method | Signature | Required |
+|--------|-----------|---------|
+| `getIssue` | `(identifier: string, project: ProjectConfig) => Promise` | yes |
+| `isCompleted` | `(identifier: string, project: ProjectConfig) => Promise` | yes |
+| `issueUrl` | `(identifier: string, project: ProjectConfig) => string` | yes |
+| `branchName` | `(identifier: string, project: ProjectConfig) => string` | yes |
+| `generatePrompt` | `(identifier: string, project: ProjectConfig) => Promise` | yes |
+| `issueLabel` | `(url: string, project: ProjectConfig) => string` | optional |
+| `listIssues` | `(filters: IssueFilters, project: ProjectConfig) => Promise` | optional |
+| `updateIssue` | `(identifier: string, update: IssueUpdate, project: ProjectConfig) => Promise` | optional |
+| `createIssue` | `(input: CreateIssueInput, project: ProjectConfig) => Promise` | optional |
+
+### SCM
+
+The richest interface — covers PR lifecycle, CI tracking, review tracking, and merge readiness.
+
+**Required:** `detectPR`, `getPRState`, `mergePR`, `closePR`, `getCIChecks`, `getCISummary`, `getReviews`, `getReviewDecision`, `getPendingComments`, `getAutomatedComments`, `getMergeability`
+
+**Optional:** `verifyWebhook`, `parseWebhook`, `resolvePR`, `assignPRToCurrentUser`, `checkoutPR`, `getPRSummary`, `enrichSessionsPRBatch`
+
+### Notifier
+
+| Method | Signature | Required |
+|--------|-----------|---------|
+| `notify` | `(event: OrchestratorEvent) => Promise` | yes |
+| `notifyWithActions` | `(event: OrchestratorEvent, actions: NotifyAction[]) => Promise` | optional |
+| `post` | `(message: string, context?: NotifyContext) => Promise` | optional |
+
+### Terminal
+
+| Method | Signature | Required |
+|--------|-----------|---------|
+| `openSession` | `(session: Session) => Promise` | yes |
+| `openAll` | `(sessions: Session[]) => Promise` | yes |
+| `isSessionOpen` | `(session: Session) => Promise` | optional |
+
+---
+
+## Config validation in `create()`
+
+Validate all config once at load time. Store validated values in a closure. Never re-validate inside individual methods.
+
+```typescript
+export function create(config?: Record): Notifier {
+ // Resolve config or fall back to an environment variable.
+ const url = (config?.url as string | undefined) ?? process.env["WEBHOOK_URL"];
+
+ if (!url) {
+ // Warn for missing optional config — don't throw.
+ // Throw only when a required field is missing at method call time.
+ console.warn("[notifier-webhook] No url configured — notifications will be no-ops");
+ } else {
+ validateUrl(url, "notifier-webhook"); // throws on malformed URL
+ }
+
+ // Custom headers are optional — silently ignore non-string values.
+ const customHeaders: Record = {};
+ const rawHeaders = config?.headers;
+ if (rawHeaders && typeof rawHeaders === "object" && !Array.isArray(rawHeaders)) {
+ for (const [k, v] of Object.entries(rawHeaders)) {
+ if (typeof v === "string") customHeaders[k] = v;
+ }
+ }
+
+ return {
+ name: "webhook",
+ async notify(event) {
+ if (!url) return;
+ await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", ...customHeaders },
+ body: JSON.stringify({ type: "notification", event }),
+ });
+ },
+ };
+}
+```
+
+---
+
+## Loading paths
+
+AO resolves plugins through four mechanisms, tried in this order:
+
+### 1. Bundled
+
+Plugins under `packages/plugins/` in the AO monorepo are registered automatically. No config is required.
+
+### 2. Registry / npm (`source: registry` or `source: npm`)
+
+An npm package. AO installs it into its internal plugin cache on first use and keeps the version pinned in `agent-orchestrator.yaml`.
+
+```yaml
+plugins:
+ - name: pagerduty
+ source: registry
+ package: "ao-plugin-notifier-pagerduty"
+ version: "^1.0.0"
+```
+
+Use `ao plugin install ao-plugin-notifier-pagerduty` to add this block automatically.
+
+### 3. Local path (`source: local`)
+
+A filesystem path. Useful during development. Path is resolved relative to `agent-orchestrator.yaml`.
+
+```yaml
+plugins:
+ - name: pagerduty
+ source: local
+ path: ./plugins/pagerduty
+```
+
+AO will import `dist/index.js` (falling back to `index.js`) from that directory.
+
+### 4. Inline shortcut
+
+For `tracker`, `scm`, and `notifier` blocks you can embed the `package` or `path` key directly instead of adding a separate `plugins[]` entry. `collectExternalPluginConfigs` in `packages/core/src/config.ts` auto-promotes these inline references to the `plugins[]` array at load time.
+
+```yaml
+projects:
+ myapp:
+ tracker:
+ package: "@acme/ao-plugin-tracker-jira"
+ version: "^1.0.0"
+ projectKey: "MYAPP"
+```
+
+You can also pass `plugin:` to assert that the loaded plugin's `manifest.name` must match a specific value:
+
+```yaml
+projects:
+ myapp:
+ tracker:
+ plugin: jira
+ package: "@acme/ao-plugin-tracker-jira"
+ version: "^1.0.0"
+```
+
+---
+
+## `ao plugin` subcommands
+
+| Command | Description |
+|---------|-------------|
+| `ao plugin list` | List plugins from the bundled marketplace catalog. Add `--installed` to show only what's in your config. Filter by slot with `--type `. Add `--refresh` to pull the latest catalog from the registry. |
+| `ao plugin search ` | Search the bundled catalog by name, package, description, or slot. |
+| `ao plugin create [dir]` | Scaffold a new plugin package interactively. |
+| `ao plugin install ` | Install a plugin by marketplace ID, package name, or local path. Writes the entry to `agent-orchestrator.yaml`. |
+| `ao plugin update [reference]` | Update an installer-managed plugin. Pass `--all` to update everything. |
+| `ao plugin uninstall ` | Remove a plugin from the config. |
+
+---
+
+## Core utilities available to plugins
+
+All utilities are exported from `@aoagents/ao-core`. The source lives in `packages/core/src/index.ts`.
+
+**Shell safety and HTTP:**
+
+| Export | Purpose |
+|--------|---------|
+| `shellEscape` | Safely escape command-line arguments. Use for every argument passed to child processes. |
+| `validateUrl` | Validate a webhook URL and throw a descriptive error on failure. |
+
+**Activity detection (agent plugins):**
+
+| Export | Purpose |
+|--------|---------|
+| `readLastJsonlEntry` | Efficiently read the last entry from an agent's native JSONL log. |
+| `readLastActivityEntry` | Read the last entry from the AO activity JSONL (`{workspace}/.ao/activity.jsonl`). |
+| `checkActivityLogState` | Extract `waiting_input` or `blocked` from an activity entry (with staleness cap). Returns `null` for other states. |
+| `getActivityFallbackState` | Age-based decay fallback: converts an activity entry into `active` / `ready` / `idle` using entry timestamp. |
+| `recordTerminalActivity` | Shared `recordActivity` implementation — classifies, deduplicates, and appends to the activity JSONL. |
+| `classifyTerminalActivity` | Classify terminal output via a `detectActivity` function. |
+| `appendActivityEntry` | Low-level JSONL append for activity entries. |
+
+**Workspace setup (agent plugins):**
+
+| Export | Purpose |
+|--------|---------|
+| `setupPathWrapperWorkspace` | Install `~/.ao/bin/gh` and `~/.ao/bin/git` PATH wrappers and write `.ao/AGENTS.md` in the workspace. Required for PATH-wrapper agents (Codex, Aider, OpenCode). |
+| `buildAgentPath` | Prepend `~/.ao/bin` to a PATH string. |
+| `normalizeAgentPermissionMode` | Normalize legacy permission mode aliases (e.g. `"skip"` → `"permissionless"`). |
+
+**Constants:**
+
+| Export | Value |
+|--------|-------|
+| `DEFAULT_READY_THRESHOLD_MS` | `300_000` (5 min) — ready → idle threshold |
+| `DEFAULT_ACTIVE_WINDOW_MS` | `30_000` (30 s) — active → ready window |
+| `ACTIVITY_INPUT_STALENESS_MS` | Deprecated compatibility export (`300_000`); waiting_input/blocked no longer expire by wallclock |
+| `PREFERRED_GH_PATH` | `/usr/local/bin/gh` |
+| `CI_STATUS` | `{ PENDING, PASSING, FAILING, NONE }` |
+| `ACTIVITY_STATE` | `{ ACTIVE, READY, IDLE, WAITING_INPUT, BLOCKED, EXITED }` |
+| `SESSION_STATUS` | Full session status constant map |
+
+**Types:** `Session`, `ProjectConfig`, `RuntimeHandle` (and everything else in `types.ts`).
+
+---
+
+## Testing
+
+Tests live in `src/__tests__/index.test.ts` and run with Vitest.
+
+Minimum test coverage for every plugin:
+
+- Manifest values (`name`, `slot`, `version`)
+- Shape of the object returned by `create()`
+- Every public method: happy path, not-found case (returns `null`), error path (throws)
+- Config validation: missing required field, invalid value
+
+Mock everything external — CLI binaries, HTTP calls, file I/O:
+
+```typescript
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import pluginModule from "../index.js";
+
+vi.mock("node:fs/promises", () => ({ readFile: vi.fn() }));
+
+describe("manifest", () => {
+ it("has correct slot and name", () => {
+ expect(pluginModule.manifest.slot).toBe("notifier");
+ expect(pluginModule.manifest.name).toBe("pagerduty");
+ });
+});
+
+describe("create()", () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it("throws when routing_key is missing", () => {
+ expect(() => pluginModule.create({})).toThrow("routing_key");
+ });
+
+ it("returns a notifier with notify()", () => {
+ const notifier = pluginModule.create({ routing_key: "test-key" });
+ expect(typeof notifier.notify).toBe("function");
+ });
+});
+```
+
+For agent plugins, the required `getActivityState` tests are listed in the CLAUDE.md "Agent Plugin Implementation Standards" section.
+
+---
+
+## Common pitfalls
+
+| Pitfall | Correct approach |
+|---------|-----------------|
+| Hardcoded secrets (API keys, tokens) | Read from `process.env`, throw if required and missing |
+| Shell injection | Use `shellEscape()` for every argument passed to child processes |
+| Reading large log files in full | Use `readLastJsonlEntry()` or stream the tail |
+| Config validation inside methods | Validate once in `create()`, capture validated values in a closure |
+| Silently swallowing errors | Either throw with `{ cause: err }` or return `null` — never no-op |
+
+---
+
+## Agent plugin specifics
+
+Agent plugins have significantly more surface area than other slot types. The most critical method is `getActivityState`, which powers the dashboard, stuck-detection, and the lifecycle manager's reaction engine.
+
+
+**Step 4 of the `getActivityState` cascade (`getActivityFallbackState`) is mandatory.** If you skip it, `getActivityState` returns `null` whenever the native API is unavailable (binary not found, session lookup failed, timeout). The dashboard shows no activity state and stuck-detection stops working entirely. This was a real production bug in the OpenCode plugin.
+
+
+The required 4-step cascade:
+
+```typescript
+async getActivityState(session, readyThresholdMs?): Promise {
+ const threshold = readyThresholdMs ?? DEFAULT_READY_THRESHOLD_MS;
+
+ // 1. PROCESS CHECK — always first
+ const running = await this.isProcessRunning(session.runtimeHandle!);
+ if (!running) return { state: "exited", timestamp: new Date() };
+
+ // 2. ACTIONABLE STATES — waiting_input / blocked from JSONL
+ const activityResult = await readLastActivityEntry(session.workspacePath);
+ const actionable = checkActivityLogState(activityResult);
+ if (actionable) return actionable;
+
+ // 3. NATIVE SIGNAL — agent-specific API (preferred when available)
+ try {
+ const nativeTimestamp = await this.getNativeTimestamp(session);
+ if (nativeTimestamp) {
+ const age = Date.now() - nativeTimestamp.getTime();
+ const activeWindowMs = Math.min(DEFAULT_ACTIVE_WINDOW_MS, threshold);
+ const state = age < activeWindowMs ? "active" : age < threshold ? "ready" : "idle";
+ return { state, timestamp: nativeTimestamp };
+ }
+ } catch {
+ // fall through to JSONL fallback
+ }
+
+ // 4. JSONL ENTRY FALLBACK — mandatory safety net
+ const activeWindowMs = Math.min(DEFAULT_ACTIVE_WINDOW_MS, threshold);
+ const fallback = getActivityFallbackState(activityResult, activeWindowMs, threshold);
+ if (fallback) return fallback;
+
+ // 5. No data at all
+ return null;
+}
+```
+
+For agents that don't have a native session API (most new agents), skip step 3 and rely entirely on the AO activity JSONL written by `recordActivity`. The full specification — including hook patterns, `isProcessRunning` requirements, and the 7 required test cases — is in the "Agent Plugin Implementation Standards" section of `CLAUDE.md` in the project root.
+
+---
+
+## Next steps
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/plugins/index.mdx b/frontend/src/landing/content/docs/plugins/index.mdx
new file mode 100644
index 000000000..fe9e5859b
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/index.mdx
@@ -0,0 +1,87 @@
+---
+title: Plugin catalog
+description: Every plugin that ships with AO, grouped by slot. Mix and match in your `agent-orchestrator.yaml`.
+---
+
+AO has **eight plugin slots**. Only one plugin per slot is active at a time, and every slot has a sensible default — you don't have to configure anything unless you want to.
+
+## Slots at a glance
+
+| Slot | Default | What it does |
+|---|---|---|
+| **Agent** | `claude-code` | Which AI tool writes the code |
+| **Runtime** | `tmux` (macOS/Linux), `process` (Windows) | Where the agent process runs |
+| **Workspace** | `worktree` | Per-session code isolation |
+| **Tracker** | `github` | Where issues live |
+| **SCM** | `github` | PRs, CI, reviews |
+| **Notifier** | `desktop` | Who pings you when something happens |
+| **Terminal** | `iterm2` on macOS | How you attach to a running agent |
+| **Lifecycle** | built-in | State machine + polling loop (not pluggable) |
+
+## Agents
+
+
+
+
+
+
+
+
+
+## Runtimes
+
+
+
+
+
+
+## Workspaces
+
+
+
+
+
+
+## Trackers
+
+
+
+
+
+
+
+## SCM
+
+
+
+
+
+
+## Notifiers
+
+
+
+
+
+
+
+
+
+
+
+## Terminals
+
+
+
+
+
+
+## Writing your own
+
+Every plugin is a small Node package that exports a `manifest` + `create()` function. See **[Authoring a plugin](/docs/plugins/authoring)** for the full contract, loading paths, and the core utilities available to plugins.
+
+The fastest path is `ao plugin create` — it scaffolds a working starter:
+
+```bash
+ao plugin create --slot notifier --name pagerduty
+```
diff --git a/frontend/src/landing/content/docs/plugins/meta.json b/frontend/src/landing/content/docs/plugins/meta.json
new file mode 100644
index 000000000..4218065f5
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/meta.json
@@ -0,0 +1,14 @@
+{
+ "title": "Plugins",
+ "defaultOpen": false,
+ "pages": [
+ "agents",
+ "runtimes",
+ "workspaces",
+ "trackers",
+ "scm",
+ "notifiers",
+ "terminals",
+ "authoring"
+ ]
+}
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/composio.mdx b/frontend/src/landing/content/docs/plugins/notifiers/composio.mdx
new file mode 100644
index 000000000..72f62f84a
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/composio.mdx
@@ -0,0 +1,163 @@
+---
+title: Composio notifier
+description: Route notifications through the Composio toolkit — Slack, Discord, or Gmail.
+---
+
+
+
+
+ Slot: notifier · Name: composio
+
+
+
+
+
+Uses the [Composio toolkit](https://composio.dev) to deliver notifications. Handy when you already have Composio set up for other agent tooling — a single credential covers Slack, Discord, and Gmail. Discord supports a low-friction webhook mode and an advanced bot mode.
+
+## Setup
+
+Run the interactive Composio setup hub:
+
+```bash
+ao setup composio
+```
+
+The hub lets you choose Slack, Discord webhook, Discord bot, or Gmail. AO asks for the Composio API key, `userId`, required target details, which notification priorities the notifier should receive, then reviews the config before writing. When you run `ao setup composio`, the selected app is written to the canonical `notifiers.composio` target. You can skip the app picker with `--slack`, `--discord-webhook`, `--discord-bot`, or `--gmail`. Discord webhook mode does not need a Composio connected account; Discord bot mode uses a bot token once to create a connected account and stores only the resulting `connectedAccountId`. Gmail mode uses an existing Gmail connected account, or creates a Composio connect link from an existing Gmail auth config.
+
+Dedicated commands use the same guided setup but write app-specific targets:
+
+```bash
+ao setup composio-slack # writes notifiers.composio-slack
+ao setup composio-discord # writes notifiers.composio-discord
+ao setup composio-discord-bot # writes notifiers.composio-discord-bot
+ao setup composio-mail # writes notifiers.composio-mail
+```
+
+You can also run the scriptable Slack setup directly:
+
+```bash
+export COMPOSIO_API_KEY=ak_...
+ao setup composio-slack --user-id aoagent --channel "#agents" --non-interactive
+```
+
+All Composio setup commands accept `--routing-preset urgent-only`,
+`--routing-preset urgent-action`, or `--routing-preset all`.
+
+If you already know the Slack connected account id, pass it directly:
+
+```bash
+ao setup composio-slack --connected-account-id ca_... --non-interactive
+```
+
+Run the Discord webhook setup when you only need channel notifications:
+
+```bash
+export COMPOSIO_API_KEY=ak_...
+ao setup composio-discord --webhook-url "https://discord.com/api/webhooks/..." --non-interactive
+```
+
+Run the Discord bot setup when you need a bot identity and channel-id based delivery:
+
+```bash
+export COMPOSIO_API_KEY=ak_...
+ao setup composio-discord-bot --channel-id "1234567890" --bot-token "$DISCORD_BOT_TOKEN" --non-interactive
+```
+
+The bot token is used once to create a Composio connected account. AO writes only the resulting `connectedAccountId`.
+
+Run the Gmail setup when you want AO notifications by email:
+
+```bash
+export COMPOSIO_API_KEY=ak_...
+ao setup composio-mail --email-to alerts@example.com --non-interactive
+```
+
+Connect Gmail in Composio first, or choose Gmail in `ao setup composio` and generate a connect link from an existing Gmail auth config. AO does not create Gmail OAuth/auth configs. The setup command finds an active Gmail connected account for the configured `userId` and writes its `connectedAccountId`. If multiple Gmail accounts are available, pass the one AO should use:
+
+```bash
+ao setup composio-mail --email-to alerts@example.com --connected-account-id ca_...
+```
+
+You can also ask AO to print a Composio Gmail connect URL:
+
+```bash
+ao setup composio-mail --email-to alerts@example.com --connect
+```
+
+AO uses an existing Composio Gmail auth config for that link. If you need a specific custom Gmail auth config, pass it explicitly:
+
+```bash
+ao setup composio-mail --email-to alerts@example.com --connect --auth-config-id ac_...
+```
+
+If Google blocks a Gmail connection, fix the Gmail auth config in Composio, then rerun `ao setup composio-mail`.
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+notifiers:
+ composio:
+ plugin: composio
+ defaultApp: slack # slack | discord | gmail
+ userId: aoagent # Composio user id
+ connectedAccountId: ca_... # preferred when available
+ channelName: "#agents" # Slack
+ emailTo: alerts@example.com # required when defaultApp=gmail
+ composioApiKey: ak_... # optional override; otherwise uses env
+
+ composio-discord:
+ plugin: composio
+ defaultApp: discord
+ mode: webhook
+ webhookUrl: https://discord.com/api/webhooks/...
+ userId: aoagent
+
+ composio-discord-bot:
+ plugin: composio
+ defaultApp: discord
+ mode: bot
+ channelId: "1234567890"
+ userId: aoagent
+ connectedAccountId: ca_...
+
+ composio-mail:
+ plugin: composio
+ defaultApp: gmail
+ emailTo: alerts@example.com
+ userId: aoagent
+ connectedAccountId: ca_...
+
+ composio-slack:
+ plugin: composio
+ defaultApp: slack
+ userId: aoagent
+ connectedAccountId: ca_...
+ channelName: "#agents"
+```
+
+## Config
+
+| Key | Required | Default | What it does |
+| -------------------- | --------------- | ---------------------- | -------------------------------------------------------------------- |
+| `defaultApp` | ✓ | `slack` | Which Composio app to target: `slack`, `discord`, or `gmail` |
+| `mode` | Discord only | auto | `webhook` uses Discord webhooks; `bot` uses bot channel messages |
+| `userId` | optional | `aoagent` | Composio user id for connected-account lookup |
+| `entityId` | optional | — | Backward-compatible alias for `userId` |
+| `authConfigId` | setup only | — | Existing Composio Gmail auth config used by `--connect` |
+| `connectedAccountId` | Slack/Gmail/bot | — | Specific connected account to use; not used for Discord webhook mode |
+| `channelName` | for Slack | — | Slack channel name |
+| `channelId` | Discord bot | — | Discord channel id for bot mode |
+| `webhookUrl` | Discord webhook | — | Discord webhook URL for webhook mode |
+| `emailTo` | ✓ when `gmail` | — | Recipient address |
+| `composioApiKey` | optional | env `COMPOSIO_API_KEY` | Override the API key from config |
+| `toolVersion` | optional | app default | Tool version passed to `@composio/core` |
+| `toolVersions` | optional | — | App-specific tool versions, for example `{ slack: "20260508_00" }` |
+
+Slack defaults to `20260508_00`; Discord defaults to `20260429_01`; Gmail defaults to `20260506_01`. Discord uses Composio's `discordbot` toolkit for both webhook and bot modes. Webhook mode calls `DISCORDBOT_EXECUTE_WEBHOOK` with the Discord webhook id/token from `webhookUrl`, so no Composio Discord connect link is required.
+If `mode` is omitted for Discord, AO uses `webhook` when `webhookUrl` is present and `bot` otherwise.
+
+## When to use
+
+- Your org already has Composio set up.
+- You want to route notifications to Gmail as a fallback for people who don't live in Slack/Discord.
+- You'd rather manage one Composio credential than three webhook URLs.
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/dashboard.mdx b/frontend/src/landing/content/docs/plugins/notifiers/dashboard.mdx
new file mode 100644
index 000000000..1bfbd407b
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/dashboard.mdx
@@ -0,0 +1,36 @@
+---
+title: Dashboard notifier
+description: Retain AO notifications in the local web dashboard.
+---
+
+The dashboard notifier records AO notifications in a local JSONL store and streams
+the latest retained records to the web dashboard over the existing local mux
+WebSocket.
+
+```yaml
+notifiers:
+ dashboard:
+ plugin: dashboard
+ limit: 50
+
+notificationRouting:
+ urgent: [dashboard]
+ action: [dashboard]
+```
+
+## Setup
+
+```bash
+ao setup dashboard
+ao setup dashboard --limit 100 --routing-preset all
+ao setup dashboard --status
+```
+
+`limit` controls how many recent notifications are retained for browser reloads
+and dashboard restarts. The default is `50`.
+
+## Test
+
+```bash
+ao notify test --to dashboard --template basic
+```
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/desktop.mdx b/frontend/src/landing/content/docs/plugins/notifiers/desktop.mdx
new file mode 100644
index 000000000..ef62d1e2a
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/desktop.mdx
@@ -0,0 +1,55 @@
+---
+title: Desktop notifier
+description: Native macOS / Linux notifications. Silent no-op on Windows.
+---
+
+
+
+ Slot: notifier · Name: desktop
+
+
+
+
+## Setup
+
+Run the setup command on macOS:
+
+```bash
+ao setup desktop
+```
+
+AO asks which desktop backend to use and which notification priorities desktop
+should receive: `urgent-only`, `urgent-action`, or `all`. For scriptable setup,
+pass `--routing-preset `.
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+notifiers:
+ desktop:
+ plugin: desktop
+ backend: ao-app
+ dashboardUrl: http://localhost:3000
+
+notificationRouting:
+ urgent: [desktop]
+ action: [desktop]
+```
+
+| Config key | Default | What it does |
+|---|---|---|
+| `backend` | `auto` | `ao-app`, `terminal-notifier`, `osascript`, or auto fallback |
+| `dashboardUrl` | dashboard port | URL opened from desktop notification actions |
+| `appPath` | macOS app install path | Custom AO Notifier.app path |
+
+## How it works
+
+- **macOS:** `osascript` runs a `display notification` AppleScript — shows in the Notification Center.
+- **Linux:** `notify-send` from `libnotify-bin`. Most desktop environments ship this out of the box.
+- **Windows:** logs a warning line and returns. No toast support (yet).
+
+## Troubleshooting
+
+- **macOS 14+ silence.** System Settings → Notifications → Terminal (or your terminal app) → Allow notifications.
+- **Linux notify-send not found.** `sudo apt install libnotify-bin` (Debian/Ubuntu) or equivalent.
+- **Windows shows no toast.** Expected — this notifier is a no-op on Windows. Add a Discord/Slack/webhook notifier in the same list.
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/discord.mdx b/frontend/src/landing/content/docs/plugins/notifiers/discord.mdx
new file mode 100644
index 000000000..d841f906b
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/discord.mdx
@@ -0,0 +1,93 @@
+---
+title: Discord notifier
+description: Discord webhook with rich embeds, retry handling, and thread support.
+---
+
+
+
+
+ Slot: notifier · Name: discord
+
+
+
+
+
+## Setup
+
+Run the setup command:
+
+```bash
+ao setup discord
+```
+
+If Discord is already configured, the command asks whether to keep the existing
+webhook URL, change it, create a new one, or cancel. If you already have a
+Discord webhook URL, paste it when prompted. If not, the command shows Discord
+links and waits while you create one. From the setup steps screen you can paste
+the URL, show the steps again, go back to the previous options, or cancel. The
+change-webhook path also lets you paste a new URL, view creation steps, go back,
+or cancel.
+
+1. Open **https://discord.com/app**.
+2. Open the target server and channel.
+3. Open **Edit Channel → Integrations → Webhooks**.
+4. Create a new webhook.
+5. Copy the webhook URL and paste it into AO.
+
+The command sends a test message before writing AO config.
+Before saving, AO also asks which notification priorities Discord should receive:
+`urgent-only`, `urgent-action`, or `all`. For scriptable setup, pass
+`--routing-preset `.
+
+Useful setup checks:
+
+```bash
+ao setup discord --status
+ao setup discord --refresh
+```
+
+```yaml title="agent-orchestrator.yaml"
+notifiers:
+ discord:
+ plugin: discord
+ webhookUrl: https://discord.com/api/webhooks/...
+ username: "Agent Orchestrator"
+ avatarUrl: https://...png # optional
+ threadId: "1234567890" # optional; post into a thread
+ retries: 2
+ retryDelayMs: 1000
+```
+
+## Config
+
+| Key | Required | Default | What it does |
+| -------------- | -------- | -------------------- | ------------------------------------------------------------------- |
+| `webhookUrl` | ✓ | — | Full `discord.com/api/webhooks/...` URL bound to the target channel |
+| `username` | optional | `Agent Orchestrator` | Override webhook default username per message |
+| `avatarUrl` | optional | — | Override webhook default avatar with an image URL |
+| `threadId` | optional | — | Post into an existing thread |
+| `retries` | optional | `2` | Retry count on 429 / 5xx / network errors |
+| `retryDelayMs` | optional | `1000` | Base retry delay (exponential backoff) |
+
+Discord webhook URLs are tied to the channel selected in Discord. To send to a
+different channel, create or copy a webhook URL from that channel.
+
+## Rich embeds
+
+AO uses Discord embeds, not plain text. Each event shows:
+
+- Colored sidebar (blue for info, red for CI failure, amber for review requested, green for merged)
+- Issue / PR link as a clickable title
+- Session id as a field
+- Short reason as the description
+
+## Retry + rate limiting
+
+Handles Discord's `Retry-After` header on 429 responses — waits the requested interval and retries. 5xx errors use exponential backoff up to `retries` attempts.
+
+## Testing
+
+```bash
+ao notify test --to discord --template basic
+ao notify test --to discord --template merge-ready --actions
+```
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/index.mdx b/frontend/src/landing/content/docs/plugins/notifiers/index.mdx
new file mode 100644
index 000000000..f05d58e05
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/index.mdx
@@ -0,0 +1,70 @@
+---
+title: Notifiers overview
+description: Who gets pinged when an agent needs you. Seven notifiers ship; pick one or stack several.
+---
+
+Notifiers deliver AO events — session stuck, PR opened, review requested, CI failed — to wherever you actually pay attention.
+
+
+
+
+
+
+
+
+
+
+
+## Stacking notifiers
+
+Notifier config is a map. Add as many named notifiers as you want, then route
+priorities with `notificationRouting`:
+
+```yaml
+notifiers:
+ desktop:
+ plugin: desktop
+ discord:
+ plugin: discord
+ webhookUrl: ${DISCORD_URL}
+ slack:
+ plugin: slack
+ webhookUrl: ${SLACK_URL}
+
+notificationRouting:
+ urgent: [desktop, discord, slack]
+ action: [discord, slack]
+ warning: [slack]
+ info: []
+```
+
+Setup commands can write this routing for you with `--routing-preset
+urgent-only`, `urgent-action`, or `all`. AO does not dedupe across channels, so
+route only the priorities each destination should receive.
+
+## Testing
+
+```bash
+ao notify test --to slack --template basic
+ao notify test --route urgent
+ao notify test --all --dry-run
+```
+
+Use `--to` for one target, `--route` for a priority route, or `--all` for every
+configured/default/routed notifier. Without `--dry-run`, real providers receive
+a real test message.
+
+## What gets notified
+
+| Event | When |
+|---|---|
+| `session.spawned` | `ao spawn` succeeds |
+| `session.working` | Agent is actively editing code |
+| `pr.opened` | Agent pushed a branch and opened a PR |
+| `ci.failed` | Required check fails |
+| `review.requested` | Reviewer asks for changes |
+| `session.mergeable` | CI green, no blockers |
+| `session.merged` | PR merged |
+| `session.blocked` | Agent is stuck — you need to intervene |
+
+You can tune which events fire via [`reactions` config](/docs/configuration).
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/meta.json b/frontend/src/landing/content/docs/plugins/notifiers/meta.json
new file mode 100644
index 000000000..609d8a46e
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Notifiers",
+ "pages": ["dashboard", "desktop", "discord", "slack", "webhook", "composio", "openclaw"]
+}
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/openclaw.mdx b/frontend/src/landing/content/docs/plugins/notifiers/openclaw.mdx
new file mode 100644
index 000000000..b3f5fc256
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/openclaw.mdx
@@ -0,0 +1,98 @@
+---
+title: OpenClaw notifier
+description: Deliver notifications through a local OpenClaw gateway — personal, low-latency, private.
+---
+
+
+
+ Slot: notifier · Name: openclaw
+
+
+
+
+[OpenClaw](https://github.com/composio-dev/openclaw) is a local gateway that routes AO events into whatever personal alert channel you've wired up — phone push, email, a local sound, etc. If it's running on your machine, this notifier uses it.
+
+## Setup
+
+Run AO's interactive helper:
+
+```bash
+ao setup openclaw
+```
+
+It will:
+
+1. Auto-detect OpenClaw on `localhost` (or prompt for the URL).
+2. Read `hooks.token` from your OpenClaw config.
+3. If the token is missing, show where to add/generate it from the OpenClaw side.
+4. Write only `agent-orchestrator.yaml`; AO does not generate the token or write shell-profile exports.
+5. Let you review and change the URL, OpenClaw config path, or routing preset before saving.
+
+Non-interactive variant:
+
+```bash
+ao setup openclaw --non-interactive --url http://127.0.0.1:18789/hooks/agent --routing-preset urgent-action
+```
+
+Refresh an existing setup or inspect status:
+
+```bash
+ao setup openclaw --refresh
+ao setup openclaw --status
+```
+
+## Use
+
+After setup, your config looks like:
+
+```yaml title="agent-orchestrator.yaml"
+notifiers:
+ openclaw:
+ plugin: openclaw
+ url: http://127.0.0.1:18789/hooks/agent
+ openclawConfigPath: ~/.openclaw/openclaw.json
+ wakeMode: now
+```
+
+The OpenClaw config keeps the actual secret:
+
+```json title="~/.openclaw/openclaw.json"
+{
+ "hooks": {
+ "enabled": true,
+ "token": "",
+ "allowRequestSessionKey": true,
+ "allowedSessionKeyPrefixes": ["hook:"]
+ }
+}
+```
+
+## Config
+
+| Key | Default | What it does |
+|---|---|---|
+| `url` | `http://127.0.0.1:18789/hooks/agent` | OpenClaw hook endpoint |
+| `openclawConfigPath` | `~/.openclaw/openclaw.json` | Local OpenClaw config path that contains `hooks.token` |
+| `token` | — | Remote/manual fallback. Avoid for local setups because it stores the secret in AO config |
+| `name` | — | Identifier shown in OpenClaw |
+| `sessionKeyPrefix` | — | Prefix for OpenClaw session keys |
+| `wakeMode` | `next-heartbeat` | `now` = deliver immediately, `next-heartbeat` = batch on next OpenClaw tick |
+| `deliver` | `true` | Disable to record-only without waking the user |
+| `retries` | `3` | Retry count on transient failures |
+| `retryDelayMs` | `1000` | Base retry delay |
+| `healthSummaryPath` | — | Optional local path where OpenClaw writes a summary AO can pick up |
+
+## Token precedence
+
+Token is read in this order:
+
+1. `token` in `agent-orchestrator.yaml` if explicitly configured
+2. `hooks.token` from `openclawConfigPath`
+3. Env var `OPENCLAW_HOOKS_TOKEN` as a legacy fallback
+
+`ao setup openclaw --status` probes the OpenClaw gateway and token. To send a
+real AO test notification through the configured notifier, run:
+
+```bash
+ao notify test --to openclaw --template basic
+```
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/slack.mdx b/frontend/src/landing/content/docs/plugins/notifiers/slack.mdx
new file mode 100644
index 000000000..43b1dc5f3
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/slack.mdx
@@ -0,0 +1,105 @@
+---
+title: Slack notifier
+description: Slack incoming webhook. Minimal config, no bot token required.
+---
+
+
+
+
+ Slot: notifier · Name: slack
+
+
+
+
+
+## Setup
+
+Run the setup command:
+
+```bash
+ao setup slack
+```
+
+If Slack is already configured, the command asks whether to keep the existing
+webhook URL, change it, create a new one, or cancel. If you already have a
+Slack incoming webhook URL, paste it when prompted. If not, the command shows
+the Slack apps URL and waits while you create one. From the setup steps screen
+you can paste the URL, show the steps again, go back to the previous options, or
+cancel. The change-webhook path also lets you paste a new URL, view creation
+steps, go back, or cancel.
+
+1. Open **https://api.slack.com/apps**.
+2. Create a new app, or select an existing app.
+3. Open **Incoming Webhooks** and turn on **Activate Incoming Webhooks**.
+4. Select **Add New Webhook to Workspace**.
+5. Pick the channel AO should post to, authorize, and copy the webhook URL.
+
+The command sends a test message before writing AO config.
+Before saving, AO also asks which notification priorities Slack should receive:
+`urgent-only`, `urgent-action`, or `all`. For scriptable setup, pass
+`--routing-preset `.
+
+Useful setup checks:
+
+```bash
+ao setup slack --status
+ao setup slack --refresh
+```
+
+```yaml title="agent-orchestrator.yaml"
+notifiers:
+ slack:
+ plugin: slack
+ webhookUrl: https://hooks.slack.com/services/...
+ username: "Agent Orchestrator"
+ channel: "#agents" # optional; should match the webhook's channel
+```
+
+## Config
+
+| Key | Required | Default | What it does |
+| ------------ | -------- | -------------------- | --------------------------------------------------------------------------------------------------- |
+| `webhookUrl` | ✓ | — | Slack incoming webhook URL |
+| `channel` | optional | webhook default | Informational/compatibility channel name; should match the channel chosen when creating the webhook |
+| `username` | optional | `Agent Orchestrator` | Requested display name |
+
+## Notes
+
+- No bot token / OAuth to manage. Incoming webhooks are the simplest integration Slack offers.
+- Slack incoming webhook URLs are tied to the channel selected during setup. Do not rely on `channel` to reroute messages to a different channel.
+- For private channels, the installing Slack user must already be in the channel before creating the webhook.
+- Treat the webhook URL as a secret; do not commit it publicly.
+- This notifier does **not** handle `Retry-After` headers — if you're sending at high volume, use the [webhook notifier](/docs/plugins/notifiers/webhook) with retries configured.
+- Messages use Slack Block Kit (header + section + context blocks). Action URLs render as inline links in the context block.
+
+
+
+ ```json
+ {
+ "blocks": [
+ {
+ "type": "header",
+ "text": { "type": "plain_text", "text": "🔔 pr.created — sess_01HXY...", "emoji": true }
+ },
+ {
+ "type": "section",
+ "text": { "type": "mrkdwn", "text": "PR opened: fix(auth): handle token expiry" }
+ },
+ {
+ "type": "context",
+ "elements": [
+ {
+ "type": "mrkdwn",
+ "text": "*Project:* myproject | *Priority:* info | *Time:* "
+ }
+ ]
+ },
+ {
+ "type": "section",
+ "text": { "type": "mrkdwn", "text": ":github: " }
+ }
+ ]
+ }
+ ```
+
+
diff --git a/frontend/src/landing/content/docs/plugins/notifiers/webhook.mdx b/frontend/src/landing/content/docs/plugins/notifiers/webhook.mdx
new file mode 100644
index 000000000..306876a9c
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/notifiers/webhook.mdx
@@ -0,0 +1,147 @@
+---
+title: Webhook notifier
+description: Generic HTTP POST. Retries, exponential backoff, custom headers.
+---
+
+
+
+
+ Slot: notifier · Name: webhook
+
+
+
+
+
+Point it at any HTTPS endpoint. AO POSTs a JSON event. Use this for PagerDuty-style integrations, custom dashboards, or anything we don't have a dedicated notifier for.
+
+## Setup
+
+Run the setup command:
+
+```bash
+ao setup webhook
+```
+
+If a webhook notifier is already configured, the command asks whether to use the
+existing webhook URL, add a new webhook URL, or cancel. The add-new path lets you
+paste a different endpoint, go back, or cancel before AO sends the setup test.
+Before saving, AO also asks which notification priorities the webhook should
+receive: `urgent-only`, `urgent-action`, or `all`. For scriptable setup, pass
+`--routing-preset `.
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+notifiers:
+ webhook:
+ plugin: webhook
+ url: https://example.com/ao-events
+ headers:
+ Authorization: "Bearer ${MY_TOKEN}"
+ X-Source: "agent-orchestrator"
+ retries: 5 # default 3
+ retryDelayMs: 2000 # default 1000
+
+notificationRouting:
+ urgent: [webhook]
+ action: [webhook]
+```
+
+## Config
+
+| Key | Required | Default | What it does |
+| -------------- | -------- | ------- | ------------------------------------------------------------ |
+| `url` | ✓ | — | HTTPS endpoint that accepts POST |
+| `headers` | optional | `{}` | Custom request headers (supports `${ENV_VAR}` interpolation) |
+| `retries` | optional | `3` | Retry count on 429/5xx/network errors |
+| `retryDelayMs` | optional | `1000` | Base retry delay (exponential backoff) |
+
+## Payload
+
+### Envelope
+
+Every POST has a `type` discriminant. Most events use `notification`:
+
+```json
+{
+ "type": "notification",
+ "event": {
+ "id": "3f6a1b2c-...",
+ "type": "pr.created",
+ "priority": "info",
+ "sessionId": "sess_01HXY...",
+ "projectId": "myproject",
+ "timestamp": "2026-04-17T15:00:00.000Z",
+ "message": "PR opened: fix(auth): handle token expiry",
+ "data": {}
+ }
+}
+```
+
+When actions are attached (e.g. approve/retry buttons in supported notifiers):
+
+```json
+{
+ "type": "notification_with_actions",
+ "event": { "...": "same fields as above" },
+ "actions": [{ "label": "View PR", "url": "https://github.com/owner/repo/pull/123" }]
+}
+```
+
+Free-form messages sent via `ao send` arrive as:
+
+```json
+{
+ "type": "message",
+ "message": "Hello from AO",
+ "context": {}
+}
+```
+
+### Event types
+
+| `event.type` | Default priority | When emitted |
+| ---------------------------- | ---------------- | ------------------------------------------------- |
+| `session.spawned` | `info` | Session created |
+| `session.working` | `info` | Agent begins working on the issue |
+| `session.exited` | `info` | Agent process exited cleanly |
+| `session.killed` | `info` | Session was manually killed |
+| `session.idle` | `info` | Agent has been idle for a while |
+| `session.stuck` | `urgent` | Agent appears stuck (no activity) |
+| `session.needs_input` | `urgent` | Agent is blocked waiting for user input |
+| `session.errored` | `urgent` | Agent hit an unrecoverable error |
+| `pr.created` | `info` | PR opened by the agent |
+| `pr.updated` | `info` | PR updated (new commits pushed) |
+| `pr.merged` | `action` | PR was merged |
+| `pr.closed` | `info` | PR was closed without merging |
+| `ci.passing` | `info` | CI checks passed |
+| `ci.failing` | `warning` | CI checks failed |
+| `ci.fix_sent` | `info` | Agent sent a CI fix to itself |
+| `ci.fix_failed` | `warning` | Agent's CI fix attempt failed |
+| `review.pending` | `info` | PR review requested |
+| `review.approved` | `action` | PR approved by reviewer |
+| `review.changes_requested` | `warning` | Reviewer requested changes |
+| `review.comments_sent` | `info` | Review comments sent to agent |
+| `review.comments_unresolved` | `info` | Agent left unresolved review comments |
+| `automated_review.found` | `warning` | Automated review tool (e.g. BugBot) left comments |
+| `automated_review.fix_sent` | `info` | Agent sent fixes for automated review comments |
+| `merge.ready` | `action` | PR is approved + CI green — ready to merge |
+| `merge.conflicts` | `warning` | Merge conflicts detected |
+| `merge.completed` | `action` | PR was successfully merged |
+| `reaction.triggered` | `info` | A configured reaction was triggered |
+| `reaction.escalated` | `urgent` | A reaction was escalated to a human notifier |
+| `summary.all_complete` | `info` | All sessions in the project completed |
+
+### Signature verification
+
+The webhook notifier does **not** sign outgoing requests. There is no `X-Signature` or HMAC header added to POSTs. To secure your endpoint:
+
+- Use a secret `Authorization` header (set via `headers.Authorization` in config, passed as `${MY_TOKEN}`).
+- Restrict inbound traffic to your AO host via IP allowlist or VPN.
+- Use a reverse proxy with mutual TLS if needed.
+
+## Why use this over a specific notifier
+
+- You want a single endpoint regardless of Slack/Discord/email routing (handled downstream).
+- You're building an internal AO dashboard of your own.
+- You want structured events for observability tooling (Datadog, Honeycomb, etc.).
diff --git a/frontend/src/landing/content/docs/plugins/runtimes/index.mdx b/frontend/src/landing/content/docs/plugins/runtimes/index.mdx
new file mode 100644
index 000000000..041f8f843
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/runtimes/index.mdx
@@ -0,0 +1,23 @@
+---
+title: Runtimes overview
+description: Where the agent process runs — tmux on macOS/Linux, plain child process everywhere.
+---
+
+The runtime is where your agent's terminal actually lives. Two options ship:
+
+| Plugin | Best for | Binary needed |
+|---|---|---|
+| [tmux](/docs/plugins/runtimes/tmux) | macOS / Linux default. You can attach interactively. | `tmux` |
+| [process](/docs/plugins/runtimes/process) | Windows, Docker, CI-like environments | — |
+
+
+
+
+
+
+## Choosing
+
+- If you can install `tmux` and you want to occasionally drop into a live session, use `runtime: tmux`.
+- If you're on Windows, in a container, or just want fewer moving parts, use `runtime: process`.
+
+Both runtimes expose the agent's terminal to the dashboard's xterm.js session — attaching via tmux is a *bonus*, not a requirement.
diff --git a/frontend/src/landing/content/docs/plugins/runtimes/meta.json b/frontend/src/landing/content/docs/plugins/runtimes/meta.json
new file mode 100644
index 000000000..39a8a8a62
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/runtimes/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Runtimes",
+ "pages": ["tmux", "process"]
+}
diff --git a/frontend/src/landing/content/docs/plugins/runtimes/process.mdx b/frontend/src/landing/content/docs/plugins/runtimes/process.mdx
new file mode 100644
index 000000000..e0b08495c
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/runtimes/process.mdx
@@ -0,0 +1,39 @@
+---
+title: process
+description: Cross-platform child-process runtime. Required on Windows.
+---
+
+
+
+ Slot: runtime · Name: process
+
+
+Spawns agents as plain child processes — no tmux involved. Use this on Windows (where tmux isn't available) and in any environment where you'd rather not depend on tmux.
+
+
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+runtime: process
+```
+
+No plugin-level config.
+
+## How it works
+
+- AO spawns the agent via Node's `child_process.spawn` with `shell: true`.
+- Stdout + stderr are captured in a rolling 1000-line buffer.
+- The dashboard reads from that buffer over the same WebSocket the tmux runtime uses — you won't notice a difference in the UI.
+- `isProcessRunning` uses a PID-based signal-0 check.
+
+## What you lose vs tmux
+
+- **No tmux attach.** `ao session attach` doesn't work. Use the dashboard terminal instead.
+- **No reconnecting to the agent's TTY.** If you `ao stop` the orchestrator, the child process goes with it. The agent's own session-resume features (Claude `--resume`, Codex `resume`, etc.) still work — that's a different layer.
+
+## When to pick it
+
+- **Windows** — this is the only runtime that works.
+- **Docker / CI-like environments** — fewer moving parts, no tmux install.
+- **You never attach interactively** — the dashboard terminal covers all your attach needs anyway.
diff --git a/frontend/src/landing/content/docs/plugins/runtimes/tmux.mdx b/frontend/src/landing/content/docs/plugins/runtimes/tmux.mdx
new file mode 100644
index 000000000..a776b9473
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/runtimes/tmux.mdx
@@ -0,0 +1,62 @@
+---
+title: tmux
+description: Default runtime on macOS and Linux. Each agent gets its own tmux window.
+---
+
+
+
+ Slot: runtime · Name: tmux
+
+
+AO uses [tmux](https://github.com/tmux/tmux) as the default runtime on macOS and Linux. Each session lives in its own tmux window under a single AO-managed session, and you can attach to any of them interactively.
+
+Windows has no tmux. Use process instead.>} />
+
+## Install
+
+macOS:
+
+```bash
+brew install tmux
+```
+
+Debian/Ubuntu:
+
+```bash
+sudo apt install tmux
+```
+
+Arch:
+
+```bash
+sudo pacman -S tmux
+```
+
+AO needs tmux 3.2 or newer.
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+runtime: tmux
+```
+
+No plugin-level config — this is the default on macOS/Linux so you can also omit the key entirely.
+
+## How it works
+
+- Every AO-managed process lives in a tmux window inside a per-project tmux session.
+- Agents start via `tmux send-keys` so their output is captured.
+- For long commands (>200 characters), AO writes a temp bash script and sources it, because `tmux send-keys` truncates paste buffers.
+
+## Attaching
+
+```bash
+ao session attach
+```
+
+Or from inside tmux, switch windows manually. You can detach at any time (`Ctrl-b d`) and AO keeps running.
+
+## Troubleshooting
+
+- **`tmux server not running`.** `ao start` launches the server for you; if you killed it, run `ao start` again.
+- **Window disappears after agent exits.** Intentional — AO cleans up finished windows. The session metadata is archived under `~/.agent-orchestrator/.../archive/`.
diff --git a/frontend/src/landing/content/docs/plugins/scm/github.mdx b/frontend/src/landing/content/docs/plugins/scm/github.mdx
new file mode 100644
index 000000000..15af31e5a
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/scm/github.mdx
@@ -0,0 +1,83 @@
+---
+title: GitHub SCM
+description: PRs, reviews, and CI status via the gh CLI. Optional webhook for real-time events.
+---
+
+
+
+ Slot: scm · Name: github
+
+
+
+
+The default SCM. Uses `gh` for PR + review + CI polling. Optional webhook support for reduced poll latency.
+
+## Setup
+
+```bash
+gh auth login
+```
+
+```yaml title="agent-orchestrator.yaml"
+scm: github
+projects:
+ myproject:
+ repo: owner/repo
+```
+
+## Webhook endpoint
+
+AO's dashboard receives webhook events at:
+
+```
+POST /api/webhooks
+```
+
+The absolute URL is `https:///api/webhooks`. To expose your local dashboard publicly, see [Remote access](/docs/configuration/remote-access).
+
+### GitHub repo settings
+
+In your GitHub repo go to **Settings → Webhooks → Add webhook**:
+
+- **Payload URL**: `https:///api/webhooks`
+- **Content type**: `application/json`
+- **Secret**: the value of your `secretEnvVar` environment variable
+- **Events**: Pull requests, Push, Workflow runs, Pull request reviews, Check suites
+
+### Config
+
+```yaml
+scm: github
+projects:
+ myproject:
+ repo: owner/repo
+ scm:
+ webhook:
+ secretEnvVar: GITHUB_WEBHOOK_SECRET # env var holding the HMAC secret
+```
+
+Full `webhook.*` sub-object:
+
+| Field | Default | Description |
+|---|---|---|
+| `enabled` | `true` | Enable or disable webhook processing |
+| `path` | `/api/webhooks` | Override the receive path |
+| `secretEnvVar` | — | Name of the env var holding the HMAC secret |
+| `signatureHeader` | `x-hub-signature-256` | Header carrying the HMAC-SHA256 signature |
+| `eventHeader` | `x-github-event` | Header carrying the event type |
+| `deliveryHeader` | `x-github-delivery` | Header carrying the delivery UUID |
+| `maxBodyBytes` | unlimited | Reject payloads larger than this (bytes) |
+
+**Signature**: HMAC-SHA256 over the raw request body. AO compares the computed digest against the value in `x-hub-signature-256` using a constant-time comparison.
+
+Polling is still active as a fallback — webhooks are a latency optimisation, not a hard dependency.
+
+## Batch enrichment (performance)
+
+On every poll cycle AO issues a **single batched GraphQL query** that fetches PR state, CI check results, review decisions, and merge readiness for all active sessions in a project at once. This replaces N × 3 REST calls with a single request.
+
+For rate-limit debugging: even with dozens of concurrent sessions, AO stays well under GitHub's primary rate limit. If you do hit limits, the batch enricher backs off and queues — see [Troubleshooting](/docs/troubleshooting).
+
+## Rate limits
+
+AO paces `gh` calls per-project. If you hit limits anyway, batch-enrichment backs off and queues — see [Troubleshooting](/docs/troubleshooting) for the diagnostic steps.
diff --git a/frontend/src/landing/content/docs/plugins/scm/gitlab.mdx b/frontend/src/landing/content/docs/plugins/scm/gitlab.mdx
new file mode 100644
index 000000000..94b641827
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/scm/gitlab.mdx
@@ -0,0 +1,100 @@
+---
+title: GitLab SCM
+description: Merge requests, discussions, and pipelines via the glab CLI.
+---
+
+
+
+ Slot: scm · Name: gitlab
+
+
+
+
+## Setup
+
+```bash
+glab auth login
+```
+
+```yaml title="agent-orchestrator.yaml"
+scm: gitlab
+scmConfig:
+ host: gitlab.com # default; override for self-hosted
+```
+
+## Mapping
+
+| Concept | GitHub term | GitLab term |
+|---|---|---|
+| Review request | Pull request | Merge request |
+| Review | Review | Discussion / note |
+| CI status | Check runs | Pipelines / jobs |
+
+AO normalises these internally — the dashboard and lifecycle state machine don't know which you use.
+
+## Webhook endpoint
+
+AO's dashboard receives webhook events at:
+
+```
+POST /api/webhooks
+```
+
+The absolute URL is `https:///api/webhooks`. To expose your local dashboard publicly, see [Remote access](/docs/configuration/remote-access).
+
+### GitLab project settings
+
+In your GitLab project go to **Settings → Webhooks → Add new webhook**:
+
+- **URL**: `https:///api/webhooks`
+- **Secret token**: the value of your `secretEnvVar` environment variable
+- **Trigger events**: Merge request events, Pipeline events, Push events, Note events (review comments)
+
+### Config
+
+```yaml
+scm: gitlab
+projects:
+ myproject:
+ repo: group/project
+ scm:
+ webhook:
+ secretEnvVar: GITLAB_WEBHOOK_TOKEN # env var holding the token
+```
+
+Full `webhook.*` sub-object:
+
+| Field | Default | Description |
+|---|---|---|
+| `enabled` | `true` | Enable or disable webhook processing |
+| `path` | `/api/webhooks` | Override the receive path |
+| `secretEnvVar` | — | Name of the env var holding the token |
+| `signatureHeader` | `x-gitlab-token` | Header carrying the secret token |
+| `eventHeader` | `x-gitlab-event` | Header carrying the event type |
+| `deliveryHeader` | `x-gitlab-event-uuid` | Header carrying the delivery UUID |
+| `maxBodyBytes` | unlimited | Reject payloads larger than this (bytes) |
+
+**Verification**: GitLab sends the configured secret as a literal string in `X-Gitlab-Token`. AO compares this value directly (no HMAC — unlike GitHub's SHA-256 approach).
+
+Polling is still active as a fallback — webhooks are a latency optimisation, not a hard dependency.
+
+## Automated review authors
+
+AO ignores review comments from known bot accounts so they don't block the merge-readiness check. The full list:
+
+**Hardcoded bots:**
+
+| Username |
+|---|
+| `gitlab-bot` |
+| `ghost` |
+| `dependabot[bot]` |
+| `renovate[bot]` |
+| `sast-bot` |
+| `codeclimate[bot]` |
+| `sonarcloud[bot]` |
+| `snyk-bot` |
+
+**Runtime catch-all:** any username matching `/^project_\d+_bot/` (GitLab project access tokens) or ending in `[bot]`.
+
+See [Review loop — bot detection](/docs/guides/review-loop#automated-review-bugbot-detection) for how AO uses this list during the review-pending → mergeable transition.
diff --git a/frontend/src/landing/content/docs/plugins/scm/index.mdx b/frontend/src/landing/content/docs/plugins/scm/index.mdx
new file mode 100644
index 000000000..9b61d720e
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/scm/index.mdx
@@ -0,0 +1,29 @@
+---
+title: SCM overview
+description: PRs, reviews, CI status. The tracker watches issues; the SCM plugin watches code review.
+---
+
+The **SCM** plugin is everything to do with the pull/merge-request side: opening PRs, reading their review state, and polling CI checks. Two ship.
+
+
+
+
+
+
+## Why separate from the tracker
+
+You might host code on GitHub but track issues in Linear. Or your company migrated from GitLab to GitHub but kept the tracker elsewhere. SCM and tracker are independent knobs:
+
+```yaml
+scm: github
+tracker: linear
+```
+
+## What the SCM plugin polls
+
+- **PR state** — open / closed / merged
+- **Review decisions** — approved / changes_requested / commented
+- **Inline comments** — unresolved threads feed the [review loop](/docs/guides/review-loop)
+- **CI checks** — pass / fail / in-progress, required vs optional, feed the [CI recovery loop](/docs/guides/ci-recovery)
+
+Polling interval defaults to a few seconds and is tuned per-endpoint to stay well under rate limits.
diff --git a/frontend/src/landing/content/docs/plugins/scm/meta.json b/frontend/src/landing/content/docs/plugins/scm/meta.json
new file mode 100644
index 000000000..61903c068
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/scm/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "SCM",
+ "pages": ["github", "gitlab"]
+}
diff --git a/frontend/src/landing/content/docs/plugins/terminals/index.mdx b/frontend/src/landing/content/docs/plugins/terminals/index.mdx
new file mode 100644
index 000000000..29131abb4
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/terminals/index.mdx
@@ -0,0 +1,19 @@
+---
+title: Terminals overview
+description: How you attach to a running agent. iTerm2 on macOS, or the dashboard's built-in xterm.js.
+---
+
+The **terminal** plugin is what `ao open` and `ao session attach` use. If you only ever look at the dashboard, you don't need to think about it.
+
+
+
+
+
+
+## Default
+
+AO picks `iterm2` on macOS when iTerm2 is installed; falls back to `web` otherwise. You can pin the choice:
+
+```yaml
+terminal: web
+```
diff --git a/frontend/src/landing/content/docs/plugins/terminals/iterm2.mdx b/frontend/src/landing/content/docs/plugins/terminals/iterm2.mdx
new file mode 100644
index 000000000..5f164a106
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/terminals/iterm2.mdx
@@ -0,0 +1,36 @@
+---
+title: iTerm2
+description: Open attached tabs in iTerm2 via AppleScript. macOS only.
+---
+
+
+
+ Slot: terminal · Name: iterm2
+
+
+
+
+The macOS attach experience. When you run `ao open`, it opens each session in a new iTerm2 tab (or window, with `--new-window`).
+
+## Setup
+
+Install [iTerm2](https://iterm2.com), then:
+
+```yaml title="agent-orchestrator.yaml"
+terminal: iterm2
+```
+
+No plugin-level config.
+
+## How it works
+
+Uses `osascript` + AppleScript under the hood. Requires `runtime: tmux` — an iTerm2 tab that's not attached to a tmux window is just a blank shell.
+
+## Troubleshooting
+
+- **iTerm2 won't take focus.** macOS Accessibility permission needed. System Settings → Privacy & Security → Accessibility → iTerm.
+- **"open-iterm-tab: command not found".** Reinstall AO (`npm install -g @aoagents/ao`) — the helper binary lives in the ao package.
+
+## Alternatives
+
+If you're not on macOS — or iTerm2 isn't your thing — use [`terminal: web`](/docs/plugins/terminals/web). The dashboard's xterm.js terminal is identical in feature set.
diff --git a/frontend/src/landing/content/docs/plugins/terminals/meta.json b/frontend/src/landing/content/docs/plugins/terminals/meta.json
new file mode 100644
index 000000000..406106248
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/terminals/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Terminals",
+ "pages": ["iterm2", "web"]
+}
diff --git a/frontend/src/landing/content/docs/plugins/terminals/web.mdx b/frontend/src/landing/content/docs/plugins/terminals/web.mdx
new file mode 100644
index 000000000..c53d1a596
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/terminals/web.mdx
@@ -0,0 +1,37 @@
+---
+title: Web
+description: Dashboard xterm.js terminal. Cross-platform. The only option on Windows.
+---
+
+
+
+ Slot: terminal · Name: web
+
+
+
+
+The dashboard already has an xterm.js terminal connected to every session. The `web` plugin just points `ao open` at the right dashboard URL.
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+terminal: web
+terminalConfig:
+ dashboardUrl: http://localhost:3000 # default
+```
+
+| Config key | Default | What it does |
+|---|---|---|
+| `dashboardUrl` | `http://localhost:3000` | Base URL for the dashboard. Override when the dashboard runs on another host/port. |
+
+## How it works
+
+When you run `ao open `, the plugin prints the session's dashboard URL. If you're on macOS/Linux, it also tries to open it in your default browser.
+
+Since the terminal UI lives in the dashboard itself, there's no per-session state here — this plugin is just routing + convenience.
+
+## Why this exists
+
+- Windows has no `osascript` and no AppleScript-equivalent that's worth writing a plugin for.
+- You're running AO on a remote host — no local terminal to attach to.
+- You prefer browser tabs over terminal tabs.
diff --git a/frontend/src/landing/content/docs/plugins/trackers/github.mdx b/frontend/src/landing/content/docs/plugins/trackers/github.mdx
new file mode 100644
index 000000000..4581748e3
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/trackers/github.mdx
@@ -0,0 +1,50 @@
+---
+title: GitHub tracker
+description: Issues via the gh CLI. Zero API tokens to manage — gh auth login handles it.
+---
+
+
+
+ Slot: tracker · Name: github
+
+
+
+
+The default tracker. Uses the [`gh` CLI](https://cli.github.com) for everything, so you never paste a PAT into AO.
+
+## Setup
+
+```bash
+gh auth login
+```
+
+Pick **GitHub.com → HTTPS → Login with a web browser**. Then:
+
+```yaml title="agent-orchestrator.yaml"
+tracker: github
+projects:
+ myproject:
+ repo: owner/repo
+```
+
+`repo` must be `owner/name` (not a URL) — it's what `gh` expects.
+
+## How it's used
+
+| Operation | `gh` command invoked |
+|---|---|
+| Fetch issue | `gh issue view --json title,body,...` |
+| Create issue | `gh issue create` |
+| Comment on issue | `gh issue comment` |
+| Close issue | `gh issue close` |
+| List issues | `gh issue list` |
+
+## Config
+
+No plugin-level config keys. The `github` tracker doesn't need anything beyond your `gh` auth.
+
+## Troubleshooting
+
+- **`gh: authentication required`** — run `gh auth login`.
+- **`could not resolve to an Issue`** — the issue number is wrong, or `repo` doesn't match the project.
+- **Rate limits** — AO batches and paces issue lookups, but heavy bursts can still hit limits. `gh` reports them clearly in logs.
diff --git a/frontend/src/landing/content/docs/plugins/trackers/gitlab.mdx b/frontend/src/landing/content/docs/plugins/trackers/gitlab.mdx
new file mode 100644
index 000000000..db0766f93
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/trackers/gitlab.mdx
@@ -0,0 +1,51 @@
+---
+title: GitLab tracker
+description: GitLab issues via the glab CLI. Self-hosted instances supported.
+---
+
+
+
+ Slot: tracker · Name: gitlab
+
+
+
+
+Uses [`glab`](https://gitlab.com/gitlab-org/cli) the same way the GitHub tracker uses `gh`.
+
+## Setup
+
+```bash
+glab auth login
+```
+
+```yaml title="agent-orchestrator.yaml"
+tracker:
+ name: gitlab
+ host: gitlab.com # default; override for self-hosted
+projects:
+ myproject:
+ repo: group/project
+```
+
+| Config key | Default | What it does |
+|---|---|---|
+| `host` | `gitlab.com` | GitLab hostname — override for self-hosted instances |
+
+
+`host` belongs on the **`tracker`** block (plugin-level config), not inside `trackerConfig`. `trackerConfig` is for per-project passthrough fields (labels, assignee, milestone). Putting `host` inside `trackerConfig` will not be read.
+
+
+## Self-hosted
+
+```yaml
+tracker:
+ name: gitlab
+ host: gitlab.mycorp.com
+```
+
+`glab` reads its own hostname config from `~/.config/glab-cli/`. Make sure that matches.
+
+## Limits
+
+- Issue field coverage maps 1:1 with `gh`'s GitHub equivalents.
+- Label / milestone filtering works via `glab issue list --label `.
diff --git a/frontend/src/landing/content/docs/plugins/trackers/index.mdx b/frontend/src/landing/content/docs/plugins/trackers/index.mdx
new file mode 100644
index 000000000..fc7dc034e
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/trackers/index.mdx
@@ -0,0 +1,26 @@
+---
+title: Trackers overview
+description: Where your issues live. AO fetches them, assigns them to sessions, and updates them as work progresses.
+---
+
+The **tracker** plugin is how AO fetches issues, creates new ones, and links sessions to them. Three trackers ship.
+
+
+
+
+
+
+
+## What a tracker does
+
+1. **Fetch an issue** by id — used by `ao spawn 42` to build the agent's prompt.
+2. **Create an issue** — used by a few helper workflows (e.g. CI self-healing can file a follow-up).
+3. **Close / comment** — the agent's wrappers use these when it wraps up work.
+4. **Return the issue URL** so the dashboard can link to it.
+
+Tracker and SCM are separate plugins on purpose — you might track issues in Linear but host code on GitHub. Just set them independently:
+
+```yaml
+tracker: linear
+scm: github
+```
diff --git a/frontend/src/landing/content/docs/plugins/trackers/linear.mdx b/frontend/src/landing/content/docs/plugins/trackers/linear.mdx
new file mode 100644
index 000000000..291df4c03
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/trackers/linear.mdx
@@ -0,0 +1,69 @@
+---
+title: Linear tracker
+description: Linear issues. Direct API key or Composio-mediated — AO picks the right transport automatically.
+---
+
+
+
+ Slot: tracker · Name: linear
+
+
+
+
+The Linear tracker supports two transports and auto-picks between them:
+
+- **Composio-mediated** — when `COMPOSIO_API_KEY` is set. Takes priority.
+- **Direct API** — when `LINEAR_API_KEY` is set.
+
+## Setup
+
+### Direct Linear API
+
+1. [Create a personal API key](https://linear.app/settings/api) in Linear.
+2. Export it:
+ ```bash
+ export LINEAR_API_KEY=lin_api_...
+ ```
+3. Configure AO:
+ ```yaml
+ tracker: linear
+ projects:
+ api:
+ tracker: linear
+ trackerConfig:
+ teamId: TEAM-123 # required for issue creation
+ workspaceSlug: myteam # optional, used to build issue URLs
+ ```
+
+### Composio-mediated
+
+If you already use the [Composio toolkit](https://composio.dev):
+
+```bash
+export COMPOSIO_API_KEY=...
+export COMPOSIO_ENTITY_ID=... # optional; defaults to "default"
+```
+
+AO detects this and routes Linear calls through Composio — no separate Linear token needed. `COMPOSIO_ENTITY_ID` is optional; if omitted it defaults to `"default"`.
+
+## Per-project config
+
+| Key | Required | What it does |
+|---|---|---|
+| `teamId` | ✓ (for `createIssue`) | Linear team the issue lives in |
+| `workspaceSlug` | optional | Used to render `https://linear.app/{slug}/issue/{id}` URLs |
+
+## Branch names
+
+Linear issues expose a `branchName` field — the string shown in the **Copy git branch name** button in the Linear UI (formatted according to your workspace settings in **Settings → Integrations → GitHub → Branch format**).
+
+When AO spawns an agent for a Linear issue it:
+
+1. Reads the issue's `branchName` field from the Linear API (if present and git-safe).
+2. Falls back to `feat/` if `branchName` is absent or contains characters that aren't valid in a branch name.
+
+You can customise the format globally via Linear's branch format settings — AO will pick up whatever Linear generates automatically.
+
+## Issue identifiers
+
+Linear uses `TEAM-42`-style identifiers, not numbers. `ao spawn TEAM-42` works as you'd expect.
diff --git a/frontend/src/landing/content/docs/plugins/trackers/meta.json b/frontend/src/landing/content/docs/plugins/trackers/meta.json
new file mode 100644
index 000000000..c428325da
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/trackers/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Trackers",
+ "pages": ["github", "gitlab", "linear"]
+}
diff --git a/frontend/src/landing/content/docs/plugins/workspaces/clone.mdx b/frontend/src/landing/content/docs/plugins/workspaces/clone.mdx
new file mode 100644
index 000000000..2fa6ce45c
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/workspaces/clone.mdx
@@ -0,0 +1,49 @@
+---
+title: clone
+description: Full per-session git clone. Use when worktrees fight your tooling.
+---
+
+
+
+ Slot: workspace · Name: clone
+
+
+Every session gets a full `git clone`. Heavier on disk than `worktree`, but simpler for any tooling that gets confused by the worktree layout.
+
+
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+workspace: clone
+workspaceConfig:
+ cloneDir: ~/.ao-clones # default
+```
+
+| Config key | Default | What it does |
+|---|---|---|
+| `cloneDir` | `~/.ao-clones` | Base directory for all clones |
+
+## How it works
+
+- On spawn, AO runs `git clone --reference /{sessionId}`. The `--reference` flag means the new clone borrows objects from your existing checkout — much faster than a cold clone.
+- The agent gets a fully independent working tree with its own `.git`.
+- On cleanup, the directory is removed outright.
+
+## When to pick this over worktree
+
+- Your build tool walks `.git` and breaks on worktrees (rare but happens).
+- You want each session to have a completely fresh `node_modules` / target dir with no shared parent.
+- You're experimenting with agents on different remotes.
+
+## Knobs
+
+```yaml
+projects:
+ web:
+ workspace: clone
+ postCreate:
+ - pnpm install --frozen-lockfile
+```
+
+`postCreate` runs after the clone succeeds. Useful for installing deps before the agent wakes up — the first impression matters.
diff --git a/frontend/src/landing/content/docs/plugins/workspaces/index.mdx b/frontend/src/landing/content/docs/plugins/workspaces/index.mdx
new file mode 100644
index 000000000..c495dee77
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/workspaces/index.mdx
@@ -0,0 +1,24 @@
+---
+title: Workspaces overview
+description: How each agent gets its own copy of the repo — a git worktree or a full clone.
+---
+
+Every session needs an isolated filesystem so agents don't stomp each other. AO handles this with a **workspace** plugin. Two options ship:
+
+| Plugin | Isolation | Disk usage | Speed |
+|---|---|---|---|
+| [worktree](/docs/plugins/workspaces/worktree) | Per-session branch, shared `.git` | Low (shared object DB) | Fast (no clone) |
+| [clone](/docs/plugins/workspaces/clone) | Per-session full clone | High | Slower (initial clone) |
+
+
+
+
+
+
+## Choosing
+
+- **Default:** `worktree`. It's what almost everyone should use.
+- **Pick `clone`** if:
+ - Your build tooling doesn't handle shared `.git` correctly (some language toolchains get confused).
+ - You want every agent to have its own node_modules / cargo target / etc. without sharing a parent.
+ - You're deliberately pointing agents at a different remote per session.
diff --git a/frontend/src/landing/content/docs/plugins/workspaces/meta.json b/frontend/src/landing/content/docs/plugins/workspaces/meta.json
new file mode 100644
index 000000000..d08f41c46
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/workspaces/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Workspaces",
+ "pages": ["worktree", "clone"]
+}
diff --git a/frontend/src/landing/content/docs/plugins/workspaces/worktree.mdx b/frontend/src/landing/content/docs/plugins/workspaces/worktree.mdx
new file mode 100644
index 000000000..e528637b5
--- /dev/null
+++ b/frontend/src/landing/content/docs/plugins/workspaces/worktree.mdx
@@ -0,0 +1,55 @@
+---
+title: worktree
+description: Default workspace. Each session is a git worktree pointing at a fresh branch.
+---
+
+
+
+ Slot: workspace · Name: worktree
+
+
+The default workspace plugin. Each session gets a [git worktree](https://git-scm.com/docs/git-worktree) with its own branch. All worktrees share the underlying `.git` object database, so disk usage stays low even with dozens of parallel agents.
+
+
+
+## Use
+
+```yaml title="agent-orchestrator.yaml"
+workspace: worktree
+workspaceConfig:
+ worktreeDir: ~/.worktrees # default
+```
+
+| Config key | Default | What it does |
+|---|---|---|
+| `worktreeDir` | `~/.worktrees` | Base directory for all worktrees |
+
+## How it works
+
+- On spawn, AO runs `git worktree add /{sessionId} -b ` against the project's upstream.
+- The agent works inside that worktree. Commits land on its dedicated branch.
+- On cleanup, AO runs `git worktree remove` and prunes the branch if it was never pushed.
+
+## Per-project knobs
+
+```yaml
+projects:
+ api:
+ repo: myorg/api
+ workspace: worktree
+ symlinks:
+ - .env.local
+ - node_modules
+ postCreate:
+ - pnpm install
+```
+
+| Knob | Purpose |
+|---|---|
+| `symlinks` | Files/dirs to symlink from the source repo into each worktree (e.g. `.env.local`, a shared cache) |
+| `postCreate` | Shell commands to run in each new worktree after creation (e.g. `pnpm install`) |
+
+## Troubleshooting
+
+- **"fatal: working tree already exists".** A previous session didn't clean up. `ao session cleanup` or manually `git worktree remove --force `.
+- **Branch protection rejects the agent's push.** Expected — the agent pushes its own branch, not to your protected `main`.
diff --git a/frontend/src/landing/content/docs/quickstart.mdx b/frontend/src/landing/content/docs/quickstart.mdx
new file mode 100644
index 000000000..6e6bbc1b3
--- /dev/null
+++ b/frontend/src/landing/content/docs/quickstart.mdx
@@ -0,0 +1,154 @@
+---
+title: Quickstart
+description: Start AO, spawn one worker session, and follow it from task to pull request.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+import { Tab, Tabs } from "fumadocs-ui/components/tabs";
+import { Step, Steps } from "fumadocs-ui/components/steps";
+
+This quickstart walks through the smallest useful AO loop: start the dashboard, create one worker session, watch it work, and clean it up after the PR is merged.
+
+
+ Complete [Installation](/docs/installation) first. You need `ao`, Git, one authenticated source-control CLI such as `gh`, and one signed-in agent CLI.
+
+
+## Pick A Safe First Task
+
+Use a repository where you can push a branch and open a pull request. For the first run, choose a task that is easy to review:
+
+- Fix a small bug with a clear failure.
+- Update a short documentation page.
+- Add a narrow test.
+- Make a small refactor with obvious acceptance criteria.
+
+Avoid broad tasks like “improve auth” or “clean up the app.” AO can run many agents, but each worker still needs a task with a visible finish line.
+
+## Run The First Session
+
+
+
+
+### Start AO
+
+From the repository you want AO to manage:
+
+```bash
+cd ~/code/my-repo
+ao start
+```
+
+On the first run, AO creates `agent-orchestrator.yaml`, starts the dashboard, and starts an orchestrator session for the project. The dashboard URL is printed in the terminal, usually `http://localhost:3000`.
+
+Keep this terminal running. It owns the dashboard and lifecycle polling.
+
+
+
+
+### Spawn one worker
+
+Open a second terminal in the same repository.
+
+
+
+ ```bash
+ ao spawn 42
+ ```
+
+ Replace `42` with the issue number. AO fetches the issue through `gh`, creates a worktree, starts the configured worker agent, and gives it the issue context.
+
+
+ ```bash
+ ao spawn --prompt "Update the README install section to mention Node 20"
+ ```
+
+ Use this when the task is not tracked in GitHub, GitLab, or Linear yet. Keep the prompt specific and reviewable.
+
+
+
+The command prints the session id and dashboard URL. Session names use the project prefix, for example `myrepo-1`.
+
+
+
+
+### Watch the dashboard
+
+Open the session card. You should see:
+
+- The worker activity state: active, ready, idle, waiting for input, blocked, or exited.
+- The worktree path and branch name.
+- The live terminal output.
+- The PR link after the agent creates one.
+- CI and review state after a PR exists.
+
+Use `ao status` when you want the same high-level view in a terminal:
+
+```bash
+ao status
+```
+
+
+
+
+### Intervene only when needed
+
+AO routes routine feedback back to the worker:
+
+- CI failure: AO sends failure context to the session.
+- Requested changes: AO sends the review feedback to the session.
+- Merge conflicts: AO can ask the worker to rebase or resolve the conflict.
+- Agent stuck or waiting for input: AO notifies you.
+
+If you need to give the worker a direct instruction, use `ao send`:
+
+```bash
+ao send myrepo-1 "Keep the fix smaller. Do not refactor the API layer."
+```
+
+Use `ao send` instead of raw terminal input. It preserves AO's busy detection, retry handling, and message formatting.
+
+
+
+
+### Review and merge
+
+When the PR is green and ready, review it like any other pull request. AO does not merge by default.
+
+After the PR is merged or the issue is closed, clean up completed sessions:
+
+```bash
+ao session cleanup --dry-run
+ao session cleanup
+```
+
+
+
+
+## What AO Created
+
+| Item | What it means |
+| --- | --- |
+| `agent-orchestrator.yaml` | Project config: plugins, projects, reactions, runtime, and notifier choices. |
+| Orchestrator session | A coordinating session started by `ao start`; it supervises worker sessions. |
+| Worker session | The agent process that works on one issue or prompt. |
+| Worktree | An isolated checkout for the worker's branch. |
+| Session metadata | Files under `~/.agent-orchestrator/...` that let AO track branch, PR, status, and runtime state. |
+
+## If Something Looks Wrong
+
+| Symptom | First check |
+| --- | --- |
+| Dashboard is not updating | Make sure the `ao start` terminal is still running. |
+| `ao spawn` warns that AO is not running | Start AO with `ao start` before spawning. |
+| GitHub issue or PR data is missing | Run `gh auth status` and check the `repo` field in `agent-orchestrator.yaml`. |
+| Agent started but does nothing | Open the session terminal and send a clear instruction with `ao send`. |
+| Windows spawn fails with tmux errors | Set `defaults.runtime: process` in `agent-orchestrator.yaml`. |
+
+## Next
+
+
+
+
+
+
+
diff --git a/frontend/src/landing/content/docs/troubleshooting.mdx b/frontend/src/landing/content/docs/troubleshooting.mdx
new file mode 100644
index 000000000..2084c0d56
--- /dev/null
+++ b/frontend/src/landing/content/docs/troubleshooting.mdx
@@ -0,0 +1,108 @@
+---
+title: Troubleshooting
+description: Common AO problems and concrete fixes. Most start with `ao doctor`.
+---
+
+import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
+
+## First step: `ao doctor`
+
+```bash
+ao doctor
+ao doctor --fix # auto-repair safe things
+ao doctor --test-notify # send a test through each notifier
+```
+
+Covers: install health, plugin resolution, notifier connectivity, stale temp files.
+
+## Install & environment
+
+
+
+ The npm global bin isn't on your PATH. Find it with `npm config get prefix` and add `/bin` to your shell's PATH.
+
+
+ Expected. Use `runtime: process` in your config. See [Platforms](/docs/platforms#windows).
+
+
+ Run `gh auth login`. AO uses `gh` for every GitHub interaction.
+
+
+ AO auto-picks the next free port. You'll see the actual URL in the `ao start` output.
+
+
+ Stale Next.js artifacts. Run `ao dashboard --rebuild`.
+
+
+
+## Agents & sessions
+
+
+
+ The agent never reached idle state. Run `ao status --watch` to see what's happening — if it's legitimately working (e.g. large install step), wait. Otherwise `ao session kill ` and spawn again.
+
+
+ `getActivityState` reads the agent's JSONL / activity log. If that file is missing or stale, you'll see wrong states. `ao doctor` flags this. See the agent's individual plugin page under [Plugins › Agents](/docs/plugins/agents) for recovery.
+
+
+ The PATH wrapper didn't record it (possible if the agent called `gh` by absolute path). Use `ao session claim-pr ` to link retroactively.
+
+
+ `ao session restore ` relaunches the agent in the same worktree. If restore keeps failing, check the agent plugin's resume docs — some agents don't support resume at all (Cursor, Aider).
+
+
+ Run `ao session remap ` to re-discover the OpenCode mapping. Use `--force` if a stale mapping is stuck.
+
+
+
+## GitHub / SCM
+
+
+
+ AO paces calls, but heavy projects can still hit limits. The lifecycle manager backs off automatically. If you see sustained rate-limit errors, check that you're logged in as yourself (`gh auth status`) — unauthenticated calls have much lower limits.
+
+
+ `reactions.ciFailed.enabled: false` in your config, or the PR is in draft. See [CI recovery › When it doesn't kick in](/docs/guides/ci-recovery#when-it-doesnt-kick-in).
+
+
+ Review feedback is only replayed on `CHANGES_REQUESTED` — not on plain `COMMENTED` reviews. See [Review loop](/docs/guides/review-loop).
+
+
+ HMAC signature check failing. Verify your `secretEnvVar` is exported and matches the secret you set on GitHub.
+
+
+
+## Notifications
+
+
+
+ System Settings → Notifications → allow your terminal app.
+
+
+ Expected. Add Discord, Slack, or a webhook notifier in the same `notifier:` list.
+
+
+ The slack notifier doesn't honor Retry-After. Use the generic [webhook notifier](/docs/plugins/notifiers/webhook) with `retries` tuned up.
+
+
+
+## Windows-specific
+
+
+
+ Expected on Windows. The iTerm2 helper only runs on macOS. Use the dashboard's built-in terminal (the printed URL takes you there).
+
+
+ Shell quoting issue. Pass long prompts via `--prompt` with simple ASCII, or via `ao send --file `.
+
+
+
+## Still stuck
+
+- Check the [FAQ](/docs/faq).
+- Open an issue: [`ComposioHQ/agent-orchestrator`](https://github.com/ComposioHQ/agent-orchestrator/issues).
+When opening an issue, include:
+
+- Output of `ao --version`
+- The contents of `~/.agent-orchestrator/{hash}-observability/processes/` (one JSON snapshot per process; each contains traces, health, and metrics)
+- Your `agent-orchestrator.yaml` (redact any secrets)