chore: add prettier config and CI auto-formatter (#166)

* chore: add prettier config and CI auto-formatter

Adds .prettierrc and .prettierignore (config only, no local enforcement).
Formatting runs in CI via the prettier.yml workflow: on every push to a
non-main branch, Prettier rewrites changed files and commits the result back
using GITHUB_TOKEN. Developers never need to run Prettier locally.

Intentionally excludes husky/lint-staged — local pre-commit hooks are the
wrong layer for a formatter that the whole team doesn't need installed.

Also adds .envrc.local to .gitignore for personal local shell overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 27336650d2ee

* chore: format with prettier [skip ci]

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
yyovil 2026-06-10 09:09:17 +05:30 committed by GitHub
parent 5071364f91
commit 5982051651
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
117 changed files with 5205 additions and 4639 deletions

38
.github/workflows/prettier.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Prettier
# Auto-formats the codebase on every push and commits the result back.
# Formatting is a CI concern — developers never need to run Prettier locally
# and formatted output never shows up as local uncommitted changes.
#
# GitHub Actions does not re-trigger workflows on commits made with GITHUB_TOKEN,
# so there is no feedback loop risk.
on:
push:
branches-ignore:
- main
- "entire/**"
- "worktree-**"
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Format with Prettier
run: npx --yes prettier@3 --write .
- name: Commit formatted files
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git diff --quiet && exit 0
git add -A
git commit -m "chore: format with prettier [skip ci]"
git push

3
.gitignore vendored
View File

@ -46,3 +46,6 @@ session-events.jsonl.*
# OS
.DS_Store
Thumbs.db
# Personal local overrides (not for the team)
.envrc.local

17
.prettierignore Normal file
View File

@ -0,0 +1,17 @@
# Generated — never hand-edit; regenerated by `npm run api` / sqlc / openapi-typescript
frontend/src/api/schema.ts
backend/internal/httpd/apispec/openapi.yaml
# Build outputs
frontend/dist
frontend/dist-electron
frontend/release
frontend/test-results
frontend/playwright-report
# Lockfiles
package-lock.json
frontend/package-lock.json
# Go uses gofmt, not Prettier
backend/

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"useTabs": true,
"tabWidth": 2,
"printWidth": 120,
"singleQuote": false,
"trailingComma": "all",
"semi": true,
"arrowParens": "always"
}

View File

@ -92,21 +92,25 @@ For code entry points:
The daemon API is code-first. The OpenAPI spec and frontend TypeScript types are generated artifacts — edit the source, then regenerate.
**Source files to edit:**
- `backend/internal/httpd/controllers/dto.go` — request/response shapes.
- `backend/internal/httpd/apispec/specgen/build.go` — operation registry; add a `schemaNames` entry for any new named type.
**Regenerate after editing:**
```bash
npm run api # runs api:spec then api:ts in sequence
```
This is equivalent to running:
```bash
npm run api:spec # cd backend && go generate ./internal/httpd/apispec/...
npm run api:ts # npx openapi-typescript@7.4.4 backend/internal/httpd/apispec/openapi.yaml -o frontend/src/api/schema.ts
```
**Verify:**
```bash
cd backend && go test ./internal/httpd/... # spec drift + route/spec parity tests (does not cover schema.ts — that is checked by the api-drift CI job)
```

View File

@ -62,30 +62,30 @@ The CLI is intentionally thin: every product command resolves to a daemon HTTP
route. Run `ao <command> --help` for the authoritative flag shape; the table
below groups what's on `main` today.
| Lane | Command | Purpose |
|---|---|---|
| Daemon | `ao start` | Start the daemon in the background and wait for `/readyz`. |
| Daemon | `ao stop` | Graceful shutdown via loopback `POST /shutdown`. |
| Daemon | `ao status` | Report PID/port/health/readiness from `running.json`. |
| Daemon | `ao daemon` | Hidden internal entrypoint used by `ao start`. |
| Project | `ao project add` | Register a local git repo as a project. |
| Project | `ao project ls` | List registered projects. |
| Project | `ao project get <id>` | Fetch one project. |
| Project | `ao project rm <id>` | Remove a project. |
| Session | `ao spawn` | Spawn a worker session in a registered project. |
| Session | `ao session ls` | List sessions (filter by project, include terminated). |
| Session | `ao session get <id>` | Fetch one session. |
| Session | `ao session kill <id>` | Terminate a session. |
| Session | `ao session rename <id> <name>` | Rename a session. |
| Session | `ao session restore <id>` | Relaunch a terminated session. |
| Session | `ao session cleanup` | Reclaim eligible workspaces for terminated sessions. |
| Session | `ao session claim-pr <session> <pr>` | Attach an existing PR to a session. |
| Orchestrator | `ao orchestrator ls` | List orchestrator sessions. |
| Messaging | `ao send` | Send a message to a running agent session. |
| Utility | `ao doctor` | Local health checks (config, data dir, DB, `git`, `zellij`). |
| Utility | `ao completion <shell>` | Generate bash/zsh/fish/powershell completions. |
| Utility | `ao version` | Print build metadata. |
| Internal | `ao hooks <agent> <event>` | Hidden adapter hook callback. |
| Lane | Command | Purpose |
| ------------ | ------------------------------------ | ------------------------------------------------------------ |
| Daemon | `ao start` | Start the daemon in the background and wait for `/readyz`. |
| Daemon | `ao stop` | Graceful shutdown via loopback `POST /shutdown`. |
| Daemon | `ao status` | Report PID/port/health/readiness from `running.json`. |
| Daemon | `ao daemon` | Hidden internal entrypoint used by `ao start`. |
| Project | `ao project add` | Register a local git repo as a project. |
| Project | `ao project ls` | List registered projects. |
| Project | `ao project get <id>` | Fetch one project. |
| Project | `ao project rm <id>` | Remove a project. |
| Session | `ao spawn` | Spawn a worker session in a registered project. |
| Session | `ao session ls` | List sessions (filter by project, include terminated). |
| Session | `ao session get <id>` | Fetch one session. |
| Session | `ao session kill <id>` | Terminate a session. |
| Session | `ao session rename <id> <name>` | Rename a session. |
| Session | `ao session restore <id>` | Relaunch a terminated session. |
| Session | `ao session cleanup` | Reclaim eligible workspaces for terminated sessions. |
| Session | `ao session claim-pr <session> <pr>` | Attach an existing PR to a session. |
| Orchestrator | `ao orchestrator ls` | List orchestrator sessions. |
| Messaging | `ao send` | Send a message to a running agent session. |
| Utility | `ao doctor` | Local health checks (config, data dir, DB, `git`, `zellij`). |
| Utility | `ao completion <shell>` | Generate bash/zsh/fish/powershell completions. |
| Utility | `ao version` | Print build metadata. |
| Internal | `ao hooks <agent> <event>` | Hidden adapter hook callback. |
See [`docs/cli/`](docs/cli/) for the daemon-control intent and command shape.
@ -95,16 +95,16 @@ All configuration is env-driven; the daemon takes no config file. The bind
host is hard-coded to `127.0.0.1` — the daemon has no auth, CORS, or TLS, and
exposing it beyond loopback would be a security regression.
| Var | Default | Purpose |
|---|---|---|
| `AO_PORT` | `3001` | Bind port; daemon fails fast if taken. |
| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout (Go duration). |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful-shutdown hard cap. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID + port handshake path. |
| `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite DB, WAL files, managed state. |
| `AO_AGENT` | `claude-code` | Default agent adapter id used by `ao spawn`. |
| `AO_SESSION_ID` | _(unset)_ | Set inside spawned sessions; read by `ao send` and `ao hooks`. |
| `GITHUB_TOKEN` | _(unset)_ | Used by the GitHub SCM and tracker adapters. Falls back to `gh auth token`. |
| Var | Default | Purpose |
| --------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
| `AO_PORT` | `3001` | Bind port; daemon fails fast if taken. |
| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout (Go duration). |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful-shutdown hard cap. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID + port handshake path. |
| `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite DB, WAL files, managed state. |
| `AO_AGENT` | `claude-code` | Default agent adapter id used by `ao spawn`. |
| `AO_SESSION_ID` | _(unset)_ | Set inside spawned sessions; read by `ao send` and `ao hooks`. |
| `GITHUB_TOKEN` | _(unset)_ | Used by the GitHub SCM and tracker adapters. Falls back to `gh auth token`. |
Health check:

View File

@ -10,13 +10,13 @@ Start with [architecture.md](architecture.md) for the current backend model and
## Reference docs
| Doc | What it covers |
|-----|----------------|
| [architecture.md](architecture.md) | Current backend model, package layout, status derivation, persistence/CDC, and load-bearing rules. |
| Doc | What it covers |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| [architecture.md](architecture.md) | Current backend model, package layout, status derivation, persistence/CDC, and load-bearing rules. |
| [backend-code-structure.md](backend-code-structure.md) | Package ownership rules for the Go backend: domain, services, ports, adapters, storage, HTTP, CLI, and daemon wiring. |
| [cli/README.md](cli/README.md) | CLI commands and daemon control surface. |
| [status.md](status.md) | Current implementation shape, build/test command, and next integration work. |
| [stack.md](stack.md) | Accepted library/runtime choices, pending stack decisions, and dependencies explicitly avoided for V1. |
| [cli/README.md](cli/README.md) | CLI commands and daemon control surface. |
| [status.md](status.md) | Current implementation shape, build/test command, and next integration work. |
| [stack.md](stack.md) | Accepted library/runtime choices, pending stack decisions, and dependencies explicitly avoided for V1. |
## Mental model

View File

@ -90,7 +90,6 @@ The workspace adapter prefers:
Hook metadata changes publish `session.updated`. The frontend listens to `session.created`, `session.terminated`, and `session.updated` and invalidates the workspace query.
## Acceptance Criteria
Agent adapter behavior:

View File

@ -7,17 +7,17 @@ call runtime, workspace, tracker, or agent adapters in-process.
## Current commands
| Command | Purpose |
|---|---|
| `ao start` | Start the daemon in the background and wait for `/readyz`. |
| `ao status` | Report daemon state from `running.json`, process liveness, `/healthz`, and `/readyz`. |
| `ao status --json` | Emit the same daemon state as machine-readable JSON. |
| `ao stop` | Gracefully stop the daemon via loopback `POST /shutdown` after verifying daemon identity. |
| `ao doctor` | Check config, data directory, DB-file presence, daemon state, `git`, and optional `zellij`. |
| `ao doctor --json` | Emit doctor checks as JSON. |
| `ao completion <shell>` | Generate completions for `bash`, `zsh`, `fish`, or `powershell`. |
| `ao version` / `ao --version` | Print build metadata. |
| `ao daemon` | Hidden internal daemon entrypoint used by `ao start`. |
| Command | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------------------- |
| `ao start` | Start the daemon in the background and wait for `/readyz`. |
| `ao status` | Report daemon state from `running.json`, process liveness, `/healthz`, and `/readyz`. |
| `ao status --json` | Emit the same daemon state as machine-readable JSON. |
| `ao stop` | Gracefully stop the daemon via loopback `POST /shutdown` after verifying daemon identity. |
| `ao doctor` | Check config, data directory, DB-file presence, daemon state, `git`, and optional `zellij`. |
| `ao doctor --json` | Emit doctor checks as JSON. |
| `ao completion <shell>` | Generate completions for `bash`, `zsh`, `fish`, or `powershell`. |
| `ao version` / `ao --version` | Print build metadata. |
| `ao daemon` | Hidden internal daemon entrypoint used by `ao start`. |
`go run .` in `backend/` remains a compatibility wrapper around the daemon.
@ -25,13 +25,13 @@ call runtime, workspace, tracker, or agent adapters in-process.
The CLI and daemon share the same environment-driven config:
| Var | Default | Purpose |
|---|---|---|
| `AO_PORT` | `3001` | Loopback daemon port. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID/port handshake. |
| `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite data directory. |
| `AO_REQUEST_TIMEOUT` | `60s` | REST request timeout. |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap. |
| Var | Default | Purpose |
| --------------------- | ------------------------------------------------- | ---------------------- |
| `AO_PORT` | `3001` | Loopback daemon port. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID/port handshake. |
| `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite data directory. |
| `AO_REQUEST_TIMEOUT` | `60s` | REST request timeout. |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap. |
The daemon always binds `127.0.0.1`.

View File

@ -40,24 +40,24 @@ rather than an escape-hatch map.
## Field catalog (legacy `projects.<id>`) and target home
| YAML field | Type | Storage today | Target |
|---|---|---|---|
| `name` | string | `projects.display_name` | done |
| `repo` | string | `projects.repo_origin_url` | done |
| `path` | string | `projects.path` | done |
| `defaultBranch` | string | hardcoded `"main"` | `projects.default_branch` |
| `sessionPrefix` | string | derived | `projects.session_prefix` |
| `agentConfig` | `{model, permissions}` | **`projects.agent_config` (typed)** | **done (this PR)** |
| `orchestrator`/`worker` overrides | `{agent, agentConfig}` | — | typed role-override columns/blob |
| `env` | `map[string]string` | — | `project_env` table (key/value rows) |
| `symlinks` | `[]string` | — | `projects.symlinks` (JSON) |
| `postCreate` | `[]string` | — | `projects.post_create` (JSON) |
| `agentRules` / `agentRulesFile` | string | partial (`SpawnConfig.AgentRules`) | `projects.agent_rules*` |
| `orchestratorRules` | string | — | `projects.orchestrator_rules` |
| `tracker` | `{plugin, …}` | DTO stub only | `projects.tracker` (typed blob) + adapter validation |
| `scm` | `{plugin, webhook{…}}` | DTO stub only | `projects.scm` (typed blob) + adapter validation |
| `opencodeIssueSessionStrategy` | enum | — | `projects.opencode_session_strategy` |
| `reactions` | per-project overrides | — | `project_reactions` (own slice) |
| YAML field | Type | Storage today | Target |
| --------------------------------- | ---------------------- | ----------------------------------- | ---------------------------------------------------- |
| `name` | string | `projects.display_name` | done |
| `repo` | string | `projects.repo_origin_url` | done |
| `path` | string | `projects.path` | done |
| `defaultBranch` | string | hardcoded `"main"` | `projects.default_branch` |
| `sessionPrefix` | string | derived | `projects.session_prefix` |
| `agentConfig` | `{model, permissions}` | **`projects.agent_config` (typed)** | **done (this PR)** |
| `orchestrator`/`worker` overrides | `{agent, agentConfig}` | — | typed role-override columns/blob |
| `env` | `map[string]string` | — | `project_env` table (key/value rows) |
| `symlinks` | `[]string` | — | `projects.symlinks` (JSON) |
| `postCreate` | `[]string` | — | `projects.post_create` (JSON) |
| `agentRules` / `agentRulesFile` | string | partial (`SpawnConfig.AgentRules`) | `projects.agent_rules*` |
| `orchestratorRules` | string | — | `projects.orchestrator_rules` |
| `tracker` | `{plugin, …}` | DTO stub only | `projects.tracker` (typed blob) + adapter validation |
| `scm` | `{plugin, webhook{…}}` | DTO stub only | `projects.scm` (typed blob) + adapter validation |
| `opencodeIssueSessionStrategy` | enum | — | `projects.opencode_session_strategy` |
| `reactions` | per-project overrides | — | `project_reactions` (own slice) |
## Typed model
@ -112,7 +112,7 @@ agent adapter.
## Sequencing (one slice per PR)
1. **agentConfig (typed)***this PR*. Establishes the typed+validated+surfaced
1. **agentConfig (typed)**_this PR_. Establishes the typed+validated+surfaced
pattern end to end.
2. **Project identity scalars**`default_branch`, `session_prefix` (stop
hardcoding/deriving them).

View File

@ -18,27 +18,27 @@ invariants.
## Accepted stack
| Area | Decision | Status | Rationale |
|------|----------|--------|-----------|
| Backend language | Go 1.25.7 | Implemented | Matches `backend/go.mod`; small daemon, strong stdlib, easy local distribution. |
| Backend core | Go stdlib | Implemented | Domain, lifecycle, session, and adapter contracts should stay dependency-light. |
| Frontend shell | Electron + TypeScript | Implemented | Local desktop control plane paired with the daemon. |
| Runtime adapter | `zellij` CLI via `os/exec` | Implemented | Terminal multiplexing fits long-running sessions, attach/debug workflows, and adapter isolation. |
| Terminal PTY | `github.com/creack/pty` | Implemented | PTY-backed terminal sessions with resize/input/output control. |
| Git/worktrees | `git` CLI via `os/exec` | Implemented | Uses real repo behavior, credentials, hooks, LFS, submodules, and user config. |
| HTTP API | `net/http` + `github.com/go-chi/chi/v5` | Implemented | Lightweight, idiomatic router without committing AO to a large web framework. |
| WebSocket | `github.com/coder/websocket` | Implemented | Small WebSocket library for terminal streaming. |
| Storage | SQLite in WAL mode via `database/sql` | Implemented | Local daemon, single writer, many dashboard/API reads, no external DB setup. |
| SQLite driver | `modernc.org/sqlite` | Implemented | Current pure-Go driver in `backend/internal/storage/sqlite`; keep it swappable behind `database/sql`. |
| SQL generation | `github.com/sqlc-dev/sqlc` | Implemented | Hand-written SQL with generated typed methods from `backend/sqlc.yaml`. |
| Migrations | `github.com/pressly/goose/v3` | Implemented | Simple SQL migrations for the embedded/local database. |
| CLI | `github.com/spf13/cobra` | Implemented | Standard command structure for daemon startup, diagnostics, and admin commands. |
| Config | stdlib environment loading + SQLite-backed state/config | Implemented / evolving | `internal/config` handles daemon env/defaults; durable product config belongs in SQLite, so no config framework is selected for V1. |
| Logging | `log/slog` | Implemented | Stdlib structured logging before adding another logging dependency. |
| OpenAPI generation | `github.com/swaggest/openapi-go`, `github.com/swaggest/jsonschema-go`, `gopkg.in/yaml.v3` | Implemented | Generated OpenAPI keeps route contracts close to Go DTOs. |
| Testing | stdlib `testing` | Implemented | Keep pure domain logic and adapter contracts easy to test. |
| Test assertions | `github.com/stretchr/testify/require` | Planned if needed | Concise assertions for higher-level adapter and integration tests; do not add unless tests benefit. |
| Packaging | `github.com/goreleaser/goreleaser` | Planned | Cross-platform release automation, checksums, and future Homebrew support. |
| Area | Decision | Status | Rationale |
| ------------------ | ----------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Backend language | Go 1.25.7 | Implemented | Matches `backend/go.mod`; small daemon, strong stdlib, easy local distribution. |
| Backend core | Go stdlib | Implemented | Domain, lifecycle, session, and adapter contracts should stay dependency-light. |
| Frontend shell | Electron + TypeScript | Implemented | Local desktop control plane paired with the daemon. |
| Runtime adapter | `zellij` CLI via `os/exec` | Implemented | Terminal multiplexing fits long-running sessions, attach/debug workflows, and adapter isolation. |
| Terminal PTY | `github.com/creack/pty` | Implemented | PTY-backed terminal sessions with resize/input/output control. |
| Git/worktrees | `git` CLI via `os/exec` | Implemented | Uses real repo behavior, credentials, hooks, LFS, submodules, and user config. |
| HTTP API | `net/http` + `github.com/go-chi/chi/v5` | Implemented | Lightweight, idiomatic router without committing AO to a large web framework. |
| WebSocket | `github.com/coder/websocket` | Implemented | Small WebSocket library for terminal streaming. |
| Storage | SQLite in WAL mode via `database/sql` | Implemented | Local daemon, single writer, many dashboard/API reads, no external DB setup. |
| SQLite driver | `modernc.org/sqlite` | Implemented | Current pure-Go driver in `backend/internal/storage/sqlite`; keep it swappable behind `database/sql`. |
| SQL generation | `github.com/sqlc-dev/sqlc` | Implemented | Hand-written SQL with generated typed methods from `backend/sqlc.yaml`. |
| Migrations | `github.com/pressly/goose/v3` | Implemented | Simple SQL migrations for the embedded/local database. |
| CLI | `github.com/spf13/cobra` | Implemented | Standard command structure for daemon startup, diagnostics, and admin commands. |
| Config | stdlib environment loading + SQLite-backed state/config | Implemented / evolving | `internal/config` handles daemon env/defaults; durable product config belongs in SQLite, so no config framework is selected for V1. |
| Logging | `log/slog` | Implemented | Stdlib structured logging before adding another logging dependency. |
| OpenAPI generation | `github.com/swaggest/openapi-go`, `github.com/swaggest/jsonschema-go`, `gopkg.in/yaml.v3` | Implemented | Generated OpenAPI keeps route contracts close to Go DTOs. |
| Testing | stdlib `testing` | Implemented | Keep pure domain logic and adapter contracts easy to test. |
| Test assertions | `github.com/stretchr/testify/require` | Planned if needed | Concise assertions for higher-level adapter and integration tests; do not add unless tests benefit. |
| Packaging | `github.com/goreleaser/goreleaser` | Planned | Cross-platform release automation, checksums, and future Homebrew support. |
## Pending decisions
@ -70,15 +70,15 @@ config surface appears.
## Explicitly avoided for V1
| Avoid | Reason |
|-------|--------|
| GORM | AO needs explicit transactional SQL and CDC-triggered writes. |
| Gin/Fiber | `net/http` + `chi` is enough for a local daemon API. |
| `go-git` as the primary Git engine | AO should match installed Git behavior, credentials, hooks, LFS, submodules, and user config. |
| `github.com/spf13/viper` / `github.com/knadh/koanf` by default | Env/default loading plus SQLite-backed config is enough for V1. |
| Temporal / NATS / Kafka / Redis | V1 is a local daemon with SQLite and CDC, not a distributed control plane. |
| Full plugin framework | Keep adapter interfaces narrow until product needs justify a plugin runtime. |
| Multi-sink CDC fan-out | Start with one durable local delivery path; add fan-out later if needed. |
| Avoid | Reason |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| GORM | AO needs explicit transactional SQL and CDC-triggered writes. |
| Gin/Fiber | `net/http` + `chi` is enough for a local daemon API. |
| `go-git` as the primary Git engine | AO should match installed Git behavior, credentials, hooks, LFS, submodules, and user config. |
| `github.com/spf13/viper` / `github.com/knadh/koanf` by default | Env/default loading plus SQLite-backed config is enough for V1. |
| Temporal / NATS / Kafka / Redis | V1 is a local daemon with SQLite and CDC, not a distributed control plane. |
| Full plugin framework | Keep adapter interfaces narrow until product needs justify a plugin runtime. |
| Multi-sink CDC fan-out | Start with one durable local delivery path; add fan-out later if needed. |
## Current stack mapping

View File

@ -1,16 +1,16 @@
{
"name": "agent-orchestrator-frontend",
"version": "0.0.0",
"private": true,
"description": "Electron + TypeScript frontend for the agent-orchestrator rewrite",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"start": "npm run build && electron ."
},
"devDependencies": {
"electron": "^33.0.0",
"typescript": "^5.6.0"
}
"name": "agent-orchestrator-frontend",
"version": "0.0.0",
"private": true,
"description": "Electron + TypeScript frontend for the agent-orchestrator rewrite",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"start": "npm run build && electron ."
},
"devDependencies": {
"electron": "^33.0.0",
"typescript": "^5.6.0"
}
}

View File

@ -2,14 +2,14 @@ import type { Metadata } from "next";
import { DocsMissingPage } from "@/components/docs/DocsMissingPage";
export const metadata: Metadata = {
title: "Docs page not found",
description: "This docs page moved or does not exist.",
robots: {
index: false,
follow: true,
},
title: "Docs page not found",
description: "This docs page moved or does not exist.",
robots: {
index: false,
follow: true,
},
};
export default function Docs404Page() {
return <DocsMissingPage />;
return <DocsMissingPage />;
}

View File

@ -1,85 +1,78 @@
import { notFound } from "next/navigation";
import {
DocsPage,
DocsBody,
DocsTitle,
DocsDescription,
} from "fumadocs-ui/page";
import { DocsPage, DocsBody, DocsTitle, DocsDescription } from "fumadocs-ui/page";
import type { Metadata } from "next";
import { source } from "@/lib/source";
import { getMDXComponents } from "@/components/docs/mdx-components";
interface PageProps {
params: Promise<{ slug?: string[] }>;
params: Promise<{ slug?: string[] }>;
}
export default async function DocsSlugPage({ params }: PageProps) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
const MDX = page.data.body;
const MDX = page.data.body;
return (
<DocsPage
toc={page.data.toc}
full={page.data.full}
tableOfContent={{
style: "clerk",
single: false,
}}
editOnGithub={{
owner: "ComposioHQ",
repo: "agent-orchestrator",
sha: "main",
path: `website/content/docs/${page.file?.path ?? ""}`,
}}
breadcrumb={{
enabled: true,
includePage: true,
}}
footer={{
enabled: true,
}}
>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX components={getMDXComponents()} />
</DocsBody>
</DocsPage>
);
return (
<DocsPage
toc={page.data.toc}
full={page.data.full}
tableOfContent={{
style: "clerk",
single: false,
}}
editOnGithub={{
owner: "ComposioHQ",
repo: "agent-orchestrator",
sha: "main",
path: `website/content/docs/${page.file?.path ?? ""}`,
}}
breadcrumb={{
enabled: true,
includePage: true,
}}
footer={{
enabled: true,
}}
>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX components={getMDXComponents()} />
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
return source.generateParams();
}
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) {
return {
title: "Docs page not found",
description: "This docs page moved or does not exist.",
robots: {
index: false,
follow: true,
},
};
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) {
return {
title: "Docs page not found",
description: "This docs page moved or does not exist.",
robots: {
index: false,
follow: true,
},
};
}
return {
title: page.data.title,
description: page.data.description,
alternates: {
canonical: `https://aoagents.dev${page.url}`,
},
openGraph: {
title: page.data.title,
description: page.data.description,
},
};
return {
title: page.data.title,
description: page.data.description,
alternates: {
canonical: `https://aoagents.dev${page.url}`,
},
openGraph: {
title: page.data.title,
description: page.data.description,
},
};
}

View File

@ -12,63 +12,63 @@
*/
:root {
/* Docs primary accent = AMBER (matches dashboard Orchestrator button + active items) */
--docs-accent: var(--color-accent-amber);
--docs-accent-dim: var(--color-accent-amber-dim);
--docs-accent-border: var(--color-accent-amber-border);
/* Docs primary accent = AMBER (matches dashboard Orchestrator button + active items) */
--docs-accent: var(--color-accent-amber);
--docs-accent-dim: var(--color-accent-amber-dim);
--docs-accent-border: var(--color-accent-amber-border);
--color-fd-background: var(--color-bg-base);
--color-fd-foreground: var(--color-text-primary);
--color-fd-muted: var(--color-bg-elevated);
--color-fd-muted-foreground: var(--color-text-secondary);
--color-fd-popover: var(--color-bg-elevated);
--color-fd-popover-foreground: var(--color-text-primary);
--color-fd-card: var(--color-bg-surface);
--color-fd-card-foreground: var(--color-text-primary);
--color-fd-border: var(--color-border-default);
--color-fd-primary: var(--docs-accent);
--color-fd-primary-foreground: #ffffff;
--color-fd-secondary: var(--color-bg-elevated);
--color-fd-secondary-foreground: var(--color-text-primary);
--color-fd-accent: var(--docs-accent-dim);
--color-fd-accent-foreground: var(--docs-accent);
--color-fd-ring: var(--docs-accent);
--color-fd-background: var(--color-bg-base);
--color-fd-foreground: var(--color-text-primary);
--color-fd-muted: var(--color-bg-elevated);
--color-fd-muted-foreground: var(--color-text-secondary);
--color-fd-popover: var(--color-bg-elevated);
--color-fd-popover-foreground: var(--color-text-primary);
--color-fd-card: var(--color-bg-surface);
--color-fd-card-foreground: var(--color-text-primary);
--color-fd-border: var(--color-border-default);
--color-fd-primary: var(--docs-accent);
--color-fd-primary-foreground: #ffffff;
--color-fd-secondary: var(--color-bg-elevated);
--color-fd-secondary-foreground: var(--color-text-primary);
--color-fd-accent: var(--docs-accent-dim);
--color-fd-accent-foreground: var(--docs-accent);
--color-fd-ring: var(--docs-accent);
}
.dark {
--docs-accent: var(--color-accent-amber);
--docs-accent-dim: var(--color-accent-amber-dim);
--docs-accent-border: var(--color-accent-amber-border);
--docs-accent: var(--color-accent-amber);
--docs-accent-dim: var(--color-accent-amber-dim);
--docs-accent-border: var(--color-accent-amber-border);
--color-fd-background: var(--color-bg-base);
--color-fd-foreground: var(--color-text-primary);
--color-fd-muted: var(--color-bg-surface);
--color-fd-muted-foreground: var(--color-text-secondary);
--color-fd-popover: var(--color-bg-elevated);
--color-fd-popover-foreground: var(--color-text-primary);
--color-fd-card: var(--color-bg-surface);
--color-fd-card-foreground: var(--color-text-primary);
--color-fd-border: var(--color-border-default);
--color-fd-primary: var(--docs-accent);
--color-fd-primary-foreground: var(--color-bg-base);
--color-fd-secondary: var(--color-bg-elevated);
--color-fd-secondary-foreground: var(--color-text-primary);
--color-fd-accent: var(--docs-accent-dim);
--color-fd-accent-foreground: var(--docs-accent);
--color-fd-ring: var(--docs-accent);
--color-fd-background: var(--color-bg-base);
--color-fd-foreground: var(--color-text-primary);
--color-fd-muted: var(--color-bg-surface);
--color-fd-muted-foreground: var(--color-text-secondary);
--color-fd-popover: var(--color-bg-elevated);
--color-fd-popover-foreground: var(--color-text-primary);
--color-fd-card: var(--color-bg-surface);
--color-fd-card-foreground: var(--color-text-primary);
--color-fd-border: var(--color-border-default);
--color-fd-primary: var(--docs-accent);
--color-fd-primary-foreground: var(--color-bg-base);
--color-fd-secondary: var(--color-bg-elevated);
--color-fd-secondary-foreground: var(--color-text-primary);
--color-fd-accent: var(--docs-accent-dim);
--color-fd-accent-foreground: var(--docs-accent);
--color-fd-ring: var(--docs-accent);
}
/* Sidebar-specific dark overrides */
.dark #nd-sidebar {
--color-fd-muted: var(--color-bg-surface);
--color-fd-secondary: var(--color-bg-elevated);
--color-fd-muted-foreground: var(--color-text-secondary);
--color-fd-muted: var(--color-bg-surface);
--color-fd-secondary: var(--color-bg-elevated);
--color-fd-muted-foreground: var(--color-text-secondary);
}
/* Sidebar background — match dashboard's very dark sidebar */
#nd-sidebar {
background-color: var(--color-bg-sidebar) !important;
border-right-color: var(--color-border-subtle) !important;
background-color: var(--color-bg-sidebar) !important;
border-right-color: var(--color-border-subtle) !important;
}
/*
@ -76,7 +76,7 @@
*/
#nd-docs-layout {
background: var(--color-bg-base) !important;
background: var(--color-bg-base) !important;
}
/*
@ -84,17 +84,17 @@
*/
:root {
--color-fd-info: var(--docs-accent);
--color-fd-warning: var(--color-accent-amber);
--color-fd-success: #16a34a;
--color-fd-error: #dc2626;
--color-fd-info: var(--docs-accent);
--color-fd-warning: var(--color-accent-amber);
--color-fd-success: #16a34a;
--color-fd-error: #dc2626;
}
.dark {
--color-fd-info: var(--docs-accent);
--color-fd-warning: var(--color-accent-amber);
--color-fd-success: #22c55e;
--color-fd-error: #ef4444;
--color-fd-info: var(--docs-accent);
--color-fd-warning: var(--color-accent-amber);
--color-fd-success: #22c55e;
--color-fd-error: #ef4444;
}
/*
@ -108,75 +108,75 @@
#nd-sidebar button,
#nd-sidebar-mobile a:not([data-active="true"]),
#nd-sidebar-mobile button {
color: var(--color-text-secondary) !important;
color: var(--color-text-secondary) !important;
}
#nd-sidebar a:not([data-active="true"]):hover,
#nd-sidebar button:hover,
#nd-sidebar-mobile a:not([data-active="true"]):hover,
#nd-sidebar-mobile button:hover {
color: var(--color-text-primary) !important;
color: var(--color-text-primary) !important;
}
/* Sidebar active link: amber accent (desktop + mobile) */
#nd-sidebar a[data-active="true"],
#nd-sidebar-mobile a[data-active="true"] {
color: var(--docs-accent) !important;
color: var(--docs-accent) !important;
}
/* TOC links: warm gray, not blue */
#nd-toc a,
#nd-tocnav a {
color: var(--color-text-secondary) !important;
color: var(--color-text-secondary) !important;
}
#nd-toc a:hover,
#nd-tocnav a:hover {
color: var(--color-text-primary) !important;
color: var(--color-text-primary) !important;
}
#nd-toc a[data-active="true"],
#nd-tocnav a[data-active="true"] {
color: var(--docs-accent) !important;
color: var(--docs-accent) !important;
}
/* Breadcrumb: warm gray */
#nd-page nav a,
#nd-page [aria-label="breadcrumb"] a {
color: var(--color-text-secondary) !important;
text-decoration: none !important;
color: var(--color-text-secondary) !important;
text-decoration: none !important;
}
#nd-page nav a:hover,
#nd-page [aria-label="breadcrumb"] a:hover {
color: var(--color-text-primary) !important;
color: var(--color-text-primary) !important;
}
/* Page title breadcrumb link (the blue "Installation" at top) */
#nd-page a[href^="/docs"] {
color: var(--color-text-secondary) !important;
color: var(--color-text-secondary) !important;
}
#nd-page a[href^="/docs"]:hover {
color: var(--color-text-primary) !important;
color: var(--color-text-primary) !important;
}
/* Step number circles: amber */
#nd-docs-layout .fd-step::before {
background: var(--docs-accent) !important;
background: var(--docs-accent) !important;
}
/* Footer prev/next: warm gray, not blue */
#nd-page footer a,
#nd-page [class*="footer"] a {
color: var(--color-text-secondary) !important;
border-color: var(--color-border-default) !important;
color: var(--color-text-secondary) !important;
border-color: var(--color-border-default) !important;
}
#nd-page footer a:hover,
#nd-page [class*="footer"] a:hover {
color: var(--color-text-primary) !important;
border-color: var(--docs-accent) !important;
color: var(--color-text-primary) !important;
border-color: var(--docs-accent) !important;
}
/*
@ -184,37 +184,33 @@
*/
#nd-docs-layout {
font-family:
var(--font-geist-sans),
ui-sans-serif,
system-ui,
-apple-system,
sans-serif;
font-size: 13px;
letter-spacing: -0.011em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family:
var(--font-geist-sans),
ui-sans-serif,
system-ui,
-apple-system,
sans-serif;
font-size: 13px;
letter-spacing: -0.011em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#nd-docs-layout code,
#nd-docs-layout kbd,
#nd-docs-layout pre {
font-family:
var(--font-jetbrains-mono),
ui-monospace,
"SFMono-Regular",
monospace;
font-family: var(--font-jetbrains-mono), ui-monospace, "SFMono-Regular", monospace;
}
/* Sidebar — smaller text like dashboard */
#nd-sidebar {
font-size: 12.5px;
font-size: 12.5px;
}
/* TOC — compact */
#nd-toc,
#nd-tocnav {
font-size: 11.5px;
font-size: 11.5px;
}
/*
@ -222,28 +218,28 @@
*/
#nd-docs-layout .prose h1 {
color: var(--color-text-primary);
font-weight: 700;
font-size: 1.65em;
letter-spacing: -0.02em;
color: var(--color-text-primary);
font-weight: 700;
font-size: 1.65em;
letter-spacing: -0.02em;
}
#nd-docs-layout .prose h2 {
color: var(--color-text-primary);
font-weight: 680;
font-size: 1.3em;
letter-spacing: -0.015em;
color: var(--color-text-primary);
font-weight: 680;
font-size: 1.3em;
letter-spacing: -0.015em;
}
#nd-docs-layout .prose h3 {
color: var(--color-text-primary);
font-weight: 640;
font-size: 1.1em;
color: var(--color-text-primary);
font-weight: 640;
font-size: 1.1em;
}
#nd-docs-layout .prose h4 {
color: var(--color-text-primary);
font-weight: 620;
color: var(--color-text-primary);
font-weight: 620;
}
/* Heading anchors — strip card border/bg */
@ -251,51 +247,51 @@
#nd-docs-layout .prose h2 a[data-card],
#nd-docs-layout .prose h3 a[data-card],
#nd-docs-layout .prose h4 a[data-card] {
border: none;
background: none;
padding: 0;
color: inherit;
border: none;
background: none;
padding: 0;
color: inherit;
}
/* Links — amber accent like dashboard */
#nd-docs-layout .prose a {
color: var(--docs-accent);
text-decoration: none;
color: var(--docs-accent);
text-decoration: none;
}
#nd-docs-layout .prose a:hover {
text-decoration: underline;
text-underline-offset: 3px;
text-decoration: underline;
text-underline-offset: 3px;
}
/* Tables — dashboard style: uppercase headers, dense */
#nd-docs-layout .prose table {
font-size: 12.5px;
font-size: 12.5px;
}
#nd-docs-layout .prose th {
background-color: var(--color-bg-subtle);
color: var(--color-text-tertiary, var(--color-text-secondary));
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0.5rem 0.75rem;
background-color: var(--color-bg-subtle);
color: var(--color-text-tertiary, var(--color-text-secondary));
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0.5rem 0.75rem;
}
#nd-docs-layout .prose td {
padding: 0.5rem 0.75rem;
padding: 0.5rem 0.75rem;
}
/* Inline code */
#nd-docs-layout :not(pre) > code {
font-size: 0.85em;
padding: 0.12em 0.4em;
border-radius: 4px;
background-color: var(--color-bg-subtle);
border: 1px solid var(--color-border-subtle);
color: var(--docs-accent);
font-weight: 450;
font-size: 0.85em;
padding: 0.12em 0.4em;
border-radius: 4px;
background-color: var(--color-bg-subtle);
border: 1px solid var(--color-border-subtle);
color: var(--docs-accent);
font-weight: 450;
}
/* No underlines in sidebar (desktop + mobile), TOC, or nav */
@ -306,7 +302,7 @@
#nd-toc a,
#nd-tocnav a,
#nd-page nav a {
text-decoration: none !important;
text-decoration: none !important;
}
/*
@ -314,56 +310,56 @@
*/
.shiki span {
color: var(--shiki-light) !important;
background-color: transparent !important;
color: var(--shiki-light) !important;
background-color: transparent !important;
}
.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: transparent !important;
color: var(--shiki-dark) !important;
background-color: transparent !important;
}
#nd-docs-layout pre,
pre.shiki,
pre.shiki code {
background-color: var(--color-bg-inset, var(--color-bg-surface)) !important;
background-color: var(--color-bg-inset, var(--color-bg-surface)) !important;
}
#nd-docs-layout pre {
font-size: 12px;
line-height: 1.7;
overflow-x: auto;
tab-size: 2;
font-size: 12px;
line-height: 1.7;
overflow-x: auto;
tab-size: 2;
}
#nd-docs-layout pre:not(figure *) {
border-radius: 6px;
border: 1px solid var(--color-border-subtle);
padding: 0.875rem 1rem;
border-radius: 6px;
border: 1px solid var(--color-border-subtle);
padding: 0.875rem 1rem;
}
#nd-docs-layout figure pre {
border: none;
border-radius: 0;
margin: 0;
border: none;
border-radius: 0;
margin: 0;
}
#nd-docs-layout figure.shiki {
background-color: var(--color-bg-inset, var(--color-bg-surface)) !important;
border-color: var(--color-border-subtle) !important;
border-radius: 6px;
background-color: var(--color-bg-inset, var(--color-bg-surface)) !important;
border-color: var(--color-border-subtle) !important;
border-radius: 6px;
}
/* Copy button fade on hover */
#nd-docs-layout figure[data-rehype-pretty-code-figure] button,
#nd-docs-layout [data-rehype-pretty-code-figure] button[aria-label] {
opacity: 0;
transition: opacity 0.15s ease;
opacity: 0;
transition: opacity 0.15s ease;
}
#nd-docs-layout figure[data-rehype-pretty-code-figure]:hover button,
#nd-docs-layout [data-rehype-pretty-code-figure]:hover button[aria-label] {
opacity: 1;
opacity: 1;
}
/*
@ -371,25 +367,25 @@ pre.shiki code {
*/
#nd-docs-layout [data-callout] {
border-radius: 6px;
border-left-width: 3px;
padding: 0.625rem 0.875rem;
margin: 1.25rem 0;
font-size: 12.5px;
border-radius: 6px;
border-left-width: 3px;
padding: 0.625rem 0.875rem;
margin: 1.25rem 0;
font-size: 12.5px;
}
/* Force info callout to amber (fumadocs uses --color-fd-info internally) */
#nd-docs-layout [data-callout][data-type="info"] {
--callout-color: var(--docs-accent) !important;
--callout-color: var(--docs-accent) !important;
}
/* Edit on GitHub button — force amber colors */
#nd-docs-layout a[href*="github.com"][href*="/blob/"] {
color: var(--color-text-secondary) !important;
color: var(--color-text-secondary) !important;
}
#nd-docs-layout a[href*="github.com"][href*="/blob/"]:hover {
color: var(--docs-accent) !important;
color: var(--docs-accent) !important;
}
/*
@ -397,16 +393,16 @@ pre.shiki code {
*/
#nd-docs-layout pre::-webkit-scrollbar {
height: 4px;
height: 4px;
}
#nd-docs-layout pre::-webkit-scrollbar-track {
background: transparent;
background: transparent;
}
#nd-docs-layout pre::-webkit-scrollbar-thumb {
background: var(--color-scrollbar, rgba(255, 240, 220, 0.15));
border-radius: 2px;
background: var(--color-scrollbar, rgba(255, 240, 220, 0.15));
border-radius: 2px;
}
/*
@ -414,9 +410,9 @@ pre.shiki code {
*/
[data-search-dialog] {
--color-fd-background: var(--color-bg-elevated);
--color-fd-border: var(--color-border-default);
--color-fd-card: var(--color-bg-surface);
--color-fd-background: var(--color-bg-elevated);
--color-fd-border: var(--color-border-default);
--color-fd-card: var(--color-bg-surface);
}
/*
@ -424,15 +420,15 @@ pre.shiki code {
*/
#nd-docs-layout .prose > * + * {
margin-top: 1em;
margin-top: 1em;
}
#nd-docs-layout .prose > h2 {
margin-top: 2em;
margin-top: 2em;
}
#nd-docs-layout .prose > h3 {
margin-top: 1.5em;
margin-top: 1.5em;
}
/*
@ -440,154 +436,157 @@ pre.shiki code {
*/
#nd-docs-layout .docs-missing-wrap {
display: flex;
min-height: calc(100vh - var(--fd-nav-height, 0px) - 8rem);
align-items: center;
padding-block: 2rem;
display: flex;
min-height: calc(100vh - var(--fd-nav-height, 0px) - 8rem);
align-items: center;
padding-block: 2rem;
}
#nd-docs-layout .docs-missing-card {
width: 100%;
max-width: 56rem;
margin-inline: auto;
overflow: hidden;
border: 1px solid var(--color-border-default);
border-radius: 8px;
background: var(--color-bg-surface);
width: 100%;
max-width: 56rem;
margin-inline: auto;
overflow: hidden;
border: 1px solid var(--color-border-default);
border-radius: 8px;
background: var(--color-bg-surface);
}
#nd-docs-layout .docs-missing-label {
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--color-bg-inset);
color: var(--color-accent-amber);
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--color-bg-inset);
color: var(--color-accent-amber);
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
}
#nd-docs-layout .docs-missing-content {
display: grid;
grid-template-columns: minmax(0, 1fr) 15rem;
gap: 1.5rem;
align-items: start;
padding: 1.5rem;
display: grid;
grid-template-columns: minmax(0, 1fr) 15rem;
gap: 1.5rem;
align-items: start;
padding: 1.5rem;
}
#nd-docs-layout .docs-missing-copy h2 {
max-width: 42rem;
margin: 0;
color: var(--color-text-primary);
font-size: 1.875rem;
font-weight: 650;
line-height: 1.15;
letter-spacing: 0;
max-width: 42rem;
margin: 0;
color: var(--color-text-primary);
font-size: 1.875rem;
font-weight: 650;
line-height: 1.15;
letter-spacing: 0;
}
#nd-docs-layout .docs-missing-copy p {
max-width: 42rem;
margin: 0.75rem 0 0;
color: var(--color-text-secondary);
font-size: 0.875rem;
line-height: 1.65;
max-width: 42rem;
margin: 0.75rem 0 0;
color: var(--color-text-secondary);
font-size: 0.875rem;
line-height: 1.65;
}
#nd-docs-layout .docs-missing-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.5rem;
}
#nd-docs-layout .docs-missing-primary,
#nd-docs-layout .docs-missing-secondary {
display: inline-flex;
height: 2.5rem;
align-items: center;
justify-content: center;
border-radius: 6px;
padding: 0 1rem;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
transition: border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
display: inline-flex;
height: 2.5rem;
align-items: center;
justify-content: center;
border-radius: 6px;
padding: 0 1rem;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
transition:
border-color 0.15s ease,
color 0.15s ease,
opacity 0.15s ease;
}
#nd-docs-layout .docs-missing-primary {
background: var(--color-accent-amber);
color: #1a1918;
background: var(--color-accent-amber);
color: #1a1918;
}
#nd-docs-layout .docs-missing-primary:hover {
opacity: 0.9;
opacity: 0.9;
}
#nd-docs-layout .docs-missing-secondary {
border: 1px solid var(--color-border-default);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-default);
color: var(--color-text-secondary);
}
#nd-docs-layout .docs-missing-secondary:hover {
color: var(--color-text-primary);
color: var(--color-text-primary);
}
#nd-docs-layout .docs-missing-terminal {
min-width: 0;
padding: 1rem;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
background: var(--color-bg-inset);
color: var(--color-text-secondary);
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.6;
min-width: 0;
padding: 1rem;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
background: var(--color-bg-inset);
color: var(--color-text-secondary);
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.6;
}
#nd-docs-layout .docs-missing-terminal p {
margin: 0;
margin: 0;
}
#nd-docs-layout .docs-missing-terminal p + p {
margin-top: 0.375rem;
margin-top: 0.375rem;
}
#nd-docs-layout .docs-missing-dots {
display: flex;
gap: 0.375rem;
margin-bottom: 0.75rem;
display: flex;
gap: 0.375rem;
margin-bottom: 0.75rem;
}
#nd-docs-layout .docs-missing-dots span {
width: 0.5rem;
height: 0.5rem;
border-radius: 999px;
width: 0.5rem;
height: 0.5rem;
border-radius: 999px;
}
#nd-docs-layout .docs-missing-dot-red {
background: #ef4444;
background: #ef4444;
}
#nd-docs-layout .docs-missing-dot-yellow {
background: #f59e0b;
background: #f59e0b;
}
#nd-docs-layout .docs-missing-dot-green {
background: #22c55e;
background: #22c55e;
}
#nd-docs-layout .docs-missing-command {
color: var(--color-text-tertiary);
color: var(--color-text-tertiary);
}
#nd-docs-layout .docs-missing-status {
color: var(--color-text-primary);
font-weight: 600;
color: var(--color-text-primary);
font-weight: 600;
}
@media (max-width: 900px) {
#nd-docs-layout .docs-missing-content {
grid-template-columns: 1fr;
}
#nd-docs-layout .docs-missing-content {
grid-template-columns: 1fr;
}
}

View File

@ -6,124 +6,106 @@ import { source } from "@/lib/source";
import "./docs.css";
function GithubIcon({ size = 16 }: { size?: number } = {}) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
);
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
);
}
function XIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
}
function DiscordIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
);
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
);
}
const links: LinkItemType[] = [
{
type: "icon",
label: "X (Twitter)",
icon: <XIcon />,
text: "X",
url: "https://x.com/aoagents",
external: true,
},
{
type: "icon",
label: "Discord",
icon: <DiscordIcon />,
text: "Discord",
url: "https://discord.gg/UZv7JjxbwG",
external: true,
},
{
type: "icon",
label: "X (Twitter)",
icon: <XIcon />,
text: "X",
url: "https://x.com/aoagents",
external: true,
},
{
type: "icon",
label: "Discord",
icon: <DiscordIcon />,
text: "Discord",
url: "https://discord.gg/UZv7JjxbwG",
external: true,
},
];
async function GitHubStars() {
let stars: string | null = null;
try {
const res = await fetch(
"https://api.github.com/repos/ComposioHQ/agent-orchestrator",
{ next: { revalidate: 3600 } },
);
if (res.ok) {
const data = await res.json();
const count = data.stargazers_count as number;
stars = count >= 1000 ? `${(count / 1000).toFixed(1)}K` : String(count);
}
} catch {
/* GitHub API failed — hide stars */
}
let stars: string | null = null;
try {
const res = await fetch("https://api.github.com/repos/ComposioHQ/agent-orchestrator", {
next: { revalidate: 3600 },
});
if (res.ok) {
const data = await res.json();
const count = data.stargazers_count as number;
stars = count >= 1000 ? `${(count / 1000).toFixed(1)}K` : String(count);
}
} catch {
/* GitHub API failed — hide stars */
}
return stars ? (
<span className="inline-flex items-center gap-1 text-[var(--color-text-muted)]">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
{stars}
</span>
) : null;
return stars ? (
<span className="inline-flex items-center gap-1 text-[var(--color-text-muted)]">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
{stars}
</span>
) : null;
}
export default function Layout({ children }: { children: ReactNode }) {
return (
<RootProvider
theme={{ enabled: true, defaultTheme: "dark" }}
search={{ options: { type: "static" } }}
>
<DocsLayout
tree={source.pageTree}
links={links}
nav={{
title: (
<span className="flex items-center gap-2 font-semibold">
<img
src="/ao-logo.svg"
alt=""
aria-hidden="true"
width={22}
height={22}
className="h-[22px] w-[22px]"
/>
<span className="text-[var(--color-text-primary)]">AO</span>
</span>
),
}}
sidebar={{
defaultOpenLevel: 1,
collapsible: true,
banner: (
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noreferrer noopener"
className="flex items-center gap-2 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors py-1"
>
<GithubIcon />
<span>ComposioHQ/agent-orchestrator</span>
<GitHubStars />
</a>
),
}}
>
{children}
</DocsLayout>
</RootProvider>
);
return (
<RootProvider theme={{ enabled: true, defaultTheme: "dark" }} search={{ options: { type: "static" } }}>
<DocsLayout
tree={source.pageTree}
links={links}
nav={{
title: (
<span className="flex items-center gap-2 font-semibold">
<img src="/ao-logo.svg" alt="" aria-hidden="true" width={22} height={22} className="h-[22px] w-[22px]" />
<span className="text-[var(--color-text-primary)]">AO</span>
</span>
),
}}
sidebar={{
defaultOpenLevel: 1,
collapsible: true,
banner: (
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noreferrer noopener"
className="flex items-center gap-2 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors py-1"
>
<GithubIcon />
<span>ComposioHQ/agent-orchestrator</span>
<GitHubStars />
</a>
),
}}
>
{children}
</DocsLayout>
</RootProvider>
);
}

View File

@ -1,5 +1,5 @@
import { DocsMissingPage } from "@/components/docs/DocsMissingPage";
export default function DocsNotFound() {
return <DocsMissingPage />;
return <DocsMissingPage />;
}

View File

@ -1,32 +1,32 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Agent Orchestrator",
description:
"Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.",
openGraph: {
type: "website",
url: "https://aoagents.dev/landing",
siteName: "Agent Orchestrator",
title: "Agent Orchestrator",
description:
"Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.",
images: [{ url: "/og-image.png", width: 1024, height: 1024, alt: "Agent Orchestrator" }],
},
twitter: {
card: "summary",
site: "@aoagents",
creator: "@aoagents",
title: "Agent Orchestrator",
description:
"Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.",
images: ["/og-image.png"],
},
alternates: {
canonical: "https://aoagents.dev/",
},
title: "Agent Orchestrator",
description:
"Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.",
openGraph: {
type: "website",
url: "https://aoagents.dev/landing",
siteName: "Agent Orchestrator",
title: "Agent Orchestrator",
description:
"Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.",
images: [{ url: "/og-image.png", width: 1024, height: 1024, alt: "Agent Orchestrator" }],
},
twitter: {
card: "summary",
site: "@aoagents",
creator: "@aoagents",
title: "Agent Orchestrator",
description:
"Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.",
images: ["/og-image.png"],
},
alternates: {
canonical: "https://aoagents.dev/",
},
};
export default function LandingLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
return <>{children}</>;
}

View File

@ -16,29 +16,35 @@ import { PageConstellation } from "../../components/PageConstellation";
import { formatCompactNumber, getGitHubRepoStats } from "../../lib/github-repo";
export default async function LandingPage() {
const githubStats = await getGitHubRepoStats();
const githubStats = await getGitHubRepoStats();
return (
<ScrollRevealProvider>
<PageConstellation />
<div className="relative z-10">
<LandingNav />
<LandingHero starsLabel={formatCompactNumber(githubStats.stars)} />
<LandingAbout />
<LandingAgentsBar />
<LandingFeatures />
<div id="workflow"><LandingWorkflow /></div>
<div id="usecases"><LandingUseCases /></div>
<LandingHowItWorks />
<LandingVideo />
<LandingStats stats={githubStats} />
<LandingTestimonials />
<div id="quickstart"><LandingQuickStart /></div>
<LandingCTA />
<footer className="py-12 px-8 text-center text-[var(--landing-muted)] opacity-30 text-[0.8125rem] border-t border-white/[0.04]">
MIT Licensed · Open Source
</footer>
</div>
</ScrollRevealProvider>
);
return (
<ScrollRevealProvider>
<PageConstellation />
<div className="relative z-10">
<LandingNav />
<LandingHero starsLabel={formatCompactNumber(githubStats.stars)} />
<LandingAbout />
<LandingAgentsBar />
<LandingFeatures />
<div id="workflow">
<LandingWorkflow />
</div>
<div id="usecases">
<LandingUseCases />
</div>
<LandingHowItWorks />
<LandingVideo />
<LandingStats stats={githubStats} />
<LandingTestimonials />
<div id="quickstart">
<LandingQuickStart />
</div>
<LandingCTA />
<footer className="py-12 px-8 text-center text-[var(--landing-muted)] opacity-30 text-[0.8125rem] border-t border-white/[0.04]">
MIT Licensed · Open Source
</footer>
</div>
</ScrollRevealProvider>
);
}

View File

@ -2,14 +2,14 @@ import type { Metadata } from "next";
import "../styles/globals.css";
export const metadata: Metadata = {
title: "Agent Orchestrator",
description: "Open-source platform for running parallel AI coding agents.",
title: "Agent Orchestrator",
description: "Open-source platform for running parallel AI coding agents.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>{children}</body>
</html>
);
return (
<html lang="en" suppressHydrationWarning>
<body>{children}</body>
</html>
);
}

View File

@ -1,9 +1,9 @@
import LandingPage from "./landing/page";
export default function HomePage() {
return (
<div className="landing-page min-h-screen">
<LandingPage />
</div>
);
return (
<div className="landing-page min-h-screen">
<LandingPage />
</div>
);
}

View File

@ -1,54 +1,60 @@
export function LandingAbout() {
return (
<div className="bg-[radial-gradient(ellipse_at_top,rgba(255,240,220,0.015)_0%,transparent_70%)]">
<section className="landing-reveal py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
The problem
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-10 max-w-[48rem]">
You&apos;re running AI agents in 10 browser tabs.{" "}
<span className="text-[var(--landing-muted)]">
Checking if PRs landed. Re-running failed CI. Copy-pasting error logs.
</span>
</h2>
return (
<div className="bg-[radial-gradient(ellipse_at_top,rgba(255,240,220,0.015)_0%,transparent_70%)]">
<section className="landing-reveal py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
The problem
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-10 max-w-[48rem]">
You&apos;re running AI agents in 10 browser tabs.{" "}
<span className="text-[var(--landing-muted)]">
Checking if PRs landed. Re-running failed CI. Copy-pasting error logs.
</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.8] max-w-[28rem]">
Agent Orchestrator replaces that with one YAML file. Point it at
your GitHub issues, pick your agents, and walk away. Each agent
spawns in its own git worktree, creates PRs, fixes CI failures,
addresses review comments, and moves toward merge. If you are new, start with the <a href="/docs/" className="underline decoration-[var(--landing-border-default)] underline-offset-4 hover:text-white">docs quickstart and configuration guides</a>.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.8] max-w-[28rem]">
Agent Orchestrator replaces that with one YAML file. Point it at your GitHub issues, pick your agents, and
walk away. Each agent spawns in its own git worktree, creates PRs, fixes CI failures, addresses review
comments, and moves toward merge. If you are new, start with the{" "}
<a
href="/docs/"
className="underline decoration-[var(--landing-border-default)] underline-offset-4 hover:text-white"
>
docs quickstart and configuration guides
</a>
.
</p>
{/* Config preview — show how simple setup is */}
<div className="landing-card rounded-2xl overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-[var(--landing-border-subtle)]">
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<span className="ml-1.5 font-mono text-[0.5625rem] text-[var(--landing-muted-dim)]">
agent-orchestrator.yaml
</span>
</div>
<pre className="px-5 py-4 font-mono text-[0.75rem] leading-[1.9] overflow-x-auto">
<span className="text-[var(--landing-muted-dim)]">agent:</span>{" "}
<span className="text-[var(--landing-fg)]">claude-code</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">tracker:</span>{" "}
<span className="text-[var(--landing-fg)]">github</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">workspace:</span>{" "}
<span className="text-[var(--landing-fg)]">worktree</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">runtime:</span>{" "}
<span className="text-[var(--landing-fg)]">tmux</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">notifier:</span>{" "}
<span className="text-[var(--landing-fg)]">slack</span>
</pre>
</div>
</div>
</section>
</div>
);
{/* Config preview — show how simple setup is */}
<div className="landing-card rounded-2xl overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-[var(--landing-border-subtle)]">
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<span className="ml-1.5 font-mono text-[0.5625rem] text-[var(--landing-muted-dim)]">
agent-orchestrator.yaml
</span>
</div>
<pre className="px-5 py-4 font-mono text-[0.75rem] leading-[1.9] overflow-x-auto">
<span className="text-[var(--landing-muted-dim)]">agent:</span>{" "}
<span className="text-[var(--landing-fg)]">claude-code</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">tracker:</span>{" "}
<span className="text-[var(--landing-fg)]">github</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">workspace:</span>{" "}
<span className="text-[var(--landing-fg)]">worktree</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">runtime:</span>{" "}
<span className="text-[var(--landing-fg)]">tmux</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">notifier:</span>{" "}
<span className="text-[var(--landing-fg)]">slack</span>
</pre>
</div>
</div>
</section>
</div>
);
}

View File

@ -1,51 +1,45 @@
const agents = [
{
name: "Claude Code",
src: "/docs/logos/claude-code.svg",
alt: "Anthropic",
},
{
name: "Codex",
src: "/docs/logos/codex.svg",
alt: "OpenAI",
},
{
name: "Cursor",
src: "/docs/logos/cursor.svg",
alt: "Cursor",
},
{
name: "Aider",
src: "https://aider.chat/assets/logo.svg",
alt: "Aider",
},
{
name: "OpenCode",
src: "/docs/logos/opencode.svg",
alt: "OpenCode",
},
{
name: "Claude Code",
src: "/docs/logos/claude-code.svg",
alt: "Anthropic",
},
{
name: "Codex",
src: "/docs/logos/codex.svg",
alt: "OpenAI",
},
{
name: "Cursor",
src: "/docs/logos/cursor.svg",
alt: "Cursor",
},
{
name: "Aider",
src: "https://aider.chat/assets/logo.svg",
alt: "Aider",
},
{
name: "OpenCode",
src: "/docs/logos/opencode.svg",
alt: "OpenCode",
},
];
export function LandingAgentsBar() {
return (
<div className="landing-reveal text-center px-6 pt-[60px]">
<div className="text-[0.6875rem] tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-40 mb-5">
Works with your favorite AI agents
</div>
<div className="flex items-center justify-center gap-6 flex-wrap">
{agents.map((agent) => (
<div key={agent.name} className="flex flex-col items-center gap-2">
<img
src={agent.src}
alt={agent.alt}
className="w-8 h-8 rounded-md object-contain"
/>
<div className="text-[0.6875rem] font-mono text-[var(--landing-muted)] opacity-50">
{agent.name}
</div>
</div>
))}
</div>
</div>
);
return (
<div className="landing-reveal text-center px-6 pt-[60px]">
<div className="text-[0.6875rem] tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-40 mb-5">
Works with your favorite AI agents
</div>
<div className="flex items-center justify-center gap-6 flex-wrap">
{agents.map((agent) => (
<div key={agent.name} className="flex flex-col items-center gap-2">
<img src={agent.src} alt={agent.alt} className="w-8 h-8 rounded-md object-contain" />
<div className="text-[0.6875rem] font-mono text-[var(--landing-muted)] opacity-50">{agent.name}</div>
</div>
))}
</div>
</div>
);
}

View File

@ -1,33 +1,33 @@
export function LandingCTA() {
return (
<section className="text-center py-40 px-6 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.015)_0%,transparent_60%)]">
<div className="landing-reveal">
<p className="text-[var(--landing-muted)] opacity-50 text-2xl font-sans font-[680] tracking-tight mb-4">
Stop babysitting.
</p>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-2px] mb-4">
Start <em className="italic text-[var(--landing-muted)]">orchestrating.</em>
</h2>
<div className="landing-card inline-flex items-center gap-3 rounded-lg px-6 py-3 font-mono text-[0.9375rem] text-white mb-8">
<span className="text-[var(--landing-muted)] opacity-40">$</span> npm i -g @aoagents/ao
</div>
<div className="flex items-center justify-center gap-4 flex-wrap">
<a
href="/docs"
className="landing-card rounded-lg px-6 py-3 text-[0.9375rem] text-[var(--landing-muted)] no-underline transition-colors hover:text-white"
>
Read Docs
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="liquid-glass-solid rounded-lg px-6 py-3 text-[0.9375rem] no-underline transition-transform hover:scale-[1.03]"
>
View on GitHub
</a>
</div>
</div>
</section>
);
return (
<section className="text-center py-40 px-6 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.015)_0%,transparent_60%)]">
<div className="landing-reveal">
<p className="text-[var(--landing-muted)] opacity-50 text-2xl font-sans font-[680] tracking-tight mb-4">
Stop babysitting.
</p>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-2px] mb-4">
Start <em className="italic text-[var(--landing-muted)]">orchestrating.</em>
</h2>
<div className="landing-card inline-flex items-center gap-3 rounded-lg px-6 py-3 font-mono text-[0.9375rem] text-white mb-8">
<span className="text-[var(--landing-muted)] opacity-40">$</span> npm i -g @aoagents/ao
</div>
<div className="flex items-center justify-center gap-4 flex-wrap">
<a
href="/docs"
className="landing-card rounded-lg px-6 py-3 text-[0.9375rem] text-[var(--landing-muted)] no-underline transition-colors hover:text-white"
>
Read Docs
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="liquid-glass-solid rounded-lg px-6 py-3 text-[0.9375rem] no-underline transition-transform hover:scale-[1.03]"
>
View on GitHub
</a>
</div>
</div>
</section>
);
}

View File

@ -1,64 +1,58 @@
const rows = [
{ feature: "Web-based dashboard", others: "Native Mac apps only" },
{ feature: "Open source (MIT)", others: "Closed source" },
{ feature: "Multi-agent (Claude, Codex, Aider, OpenCode)", others: "Single agent" },
{ feature: "Auto CI failure recovery", others: "Manual" },
{ feature: "Plugin architecture (7 slots)", others: "Fixed integrations" },
{ feature: "Git worktree isolation", others: "Shared workspace" },
{ feature: "Web-based dashboard", others: "Native Mac apps only" },
{ feature: "Open source (MIT)", others: "Closed source" },
{ feature: "Multi-agent (Claude, Codex, Aider, OpenCode)", others: "Single agent" },
{ feature: "Auto CI failure recovery", others: "Manual" },
{ feature: "Plugin architecture (7 slots)", others: "Fixed integrations" },
{ feature: "Git worktree isolation", others: "Shared workspace" },
];
export function LandingDifferentiators() {
return (
<section className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
Why Agent Orchestrator
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-6 max-w-[42rem]">
The only{" "}
<em className="italic text-[var(--landing-muted)]">open-source, web-based</em>{" "}
agent orchestrator
</h2>
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.7] max-w-[36rem] mb-12">
Conductor, T3 Code, and Codex App are native Mac apps. AO runs in
your browser, works on any OS, and you can self-host or extend it.
</p>
</div>
<div className="landing-reveal landing-card rounded-2xl overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--landing-border-subtle)]">
<th className="text-left px-6 py-4 font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-muted)] opacity-40">
Feature
</th>
<th className="text-center px-6 py-4 font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-fg)] opacity-80">
AO
</th>
<th className="text-center px-6 py-4 font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-muted)] opacity-40">
Others
</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr
key={row.feature}
className={i < rows.length - 1 ? "border-b border-[var(--landing-border-subtle)]" : ""}
>
<td className="px-6 py-3.5 text-[0.8125rem] text-[var(--landing-fg)]/80">
{row.feature}
</td>
<td className="px-6 py-3.5 text-center text-[rgba(134,239,172,0.8)]">
</td>
<td className="px-6 py-3.5 text-center text-[0.75rem] text-[var(--landing-muted)] opacity-40">
{row.others}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
return (
<section className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
Why Agent Orchestrator
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-6 max-w-[42rem]">
The only <em className="italic text-[var(--landing-muted)]">open-source, web-based</em> agent orchestrator
</h2>
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.7] max-w-[36rem] mb-12">
Conductor, T3 Code, and Codex App are native Mac apps. AO runs in your browser, works on any OS, and you can
self-host or extend it.
</p>
</div>
<div className="landing-reveal landing-card rounded-2xl overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--landing-border-subtle)]">
<th className="text-left px-6 py-4 font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-muted)] opacity-40">
Feature
</th>
<th className="text-center px-6 py-4 font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-fg)] opacity-80">
AO
</th>
<th className="text-center px-6 py-4 font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-muted)] opacity-40">
Others
</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr
key={row.feature}
className={i < rows.length - 1 ? "border-b border-[var(--landing-border-subtle)]" : ""}
>
<td className="px-6 py-3.5 text-[0.8125rem] text-[var(--landing-fg)]/80">{row.feature}</td>
<td className="px-6 py-3.5 text-center text-[rgba(134,239,172,0.8)]"></td>
<td className="px-6 py-3.5 text-center text-[0.75rem] text-[var(--landing-muted)] opacity-40">
{row.others}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,102 +1,101 @@
interface LandingHeroProps {
starsLabel: string;
starsLabel: string;
}
export function LandingHero({ starsLabel }: LandingHeroProps) {
return (
<div className="relative min-h-screen overflow-hidden">
<section className="relative z-10 flex flex-col items-center justify-center text-center px-6 pt-32 pb-20 min-h-screen">
<div className="landing-fade-rise landing-card inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-[var(--landing-muted)] mb-8">
<span className="w-1.5 h-1.5 rounded-full bg-[rgba(134,239,172,0.7)]" />
Open Source · MIT Licensed · {starsLabel} GitHub Stars
</div>
<h1 className="landing-fade-rise font-sans font-[680] text-[clamp(1.75rem,4vw,2.75rem)] leading-[1] tracking-[-2px] max-w-[56rem]">
Run 30 AI agents in parallel.
<br />
<span className="text-[var(--landing-muted)]">One dashboard.</span>
</h1>
<p className="landing-fade-rise-d1 text-[var(--landing-muted)] text-[0.9375rem] max-w-[38rem] mt-6 leading-[1.7]">
Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode
in isolated git worktrees. Each agent gets its own branch, creates PRs,
fixes CI, and addresses reviews autonomously.
</p>
<div className="landing-fade-rise-d2 flex items-center gap-3 mt-10 flex-wrap justify-center">
<div className="landing-card rounded-lg px-6 py-3 font-mono text-sm">
<span className="text-[var(--landing-muted)] opacity-40">$</span> npx @aoagents/ao start
</div>
<a
href="/docs"
className="landing-card rounded-lg px-6 py-3 text-sm no-underline transition-colors hover:text-white"
>
Read Docs
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="liquid-glass-solid rounded-lg px-6 py-3 text-sm no-underline transition-colors"
>
View on GitHub
</a>
</div>
return (
<div className="relative min-h-screen overflow-hidden">
<section className="relative z-10 flex flex-col items-center justify-center text-center px-6 pt-32 pb-20 min-h-screen">
<div className="landing-fade-rise landing-card inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-[var(--landing-muted)] mb-8">
<span className="w-1.5 h-1.5 rounded-full bg-[rgba(134,239,172,0.7)]" />
Open Source · MIT Licensed · {starsLabel} GitHub Stars
</div>
<h1 className="landing-fade-rise font-sans font-[680] text-[clamp(1.75rem,4vw,2.75rem)] leading-[1] tracking-[-2px] max-w-[56rem]">
Run 30 AI agents in parallel.
<br />
<span className="text-[var(--landing-muted)]">One dashboard.</span>
</h1>
<p className="landing-fade-rise-d1 text-[var(--landing-muted)] text-[0.9375rem] max-w-[38rem] mt-6 leading-[1.7]">
Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode in isolated git worktrees. Each
agent gets its own branch, creates PRs, fixes CI, and addresses reviews autonomously.
</p>
<div className="landing-fade-rise-d2 flex items-center gap-3 mt-10 flex-wrap justify-center">
<div className="landing-card rounded-lg px-6 py-3 font-mono text-sm">
<span className="text-[var(--landing-muted)] opacity-40">$</span> npx @aoagents/ao start
</div>
<a
href="/docs"
className="landing-card rounded-lg px-6 py-3 text-sm no-underline transition-colors hover:text-white"
>
Read Docs
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="liquid-glass-solid rounded-lg px-6 py-3 text-sm no-underline transition-colors"
>
View on GitHub
</a>
</div>
<div className="landing-fade-rise-d2 w-full max-w-[72rem] mt-16">
<div style={{ maxWidth: "62rem", margin: "0 auto" }}>
{/* Laptop screen / lid */}
<div
className="rounded-[14px]"
style={{
padding: 10,
background: "linear-gradient(180deg, #211f1c 0%, #161513 100%)",
border: "1px solid var(--landing-border-default)",
boxShadow: "0 30px 70px -24px rgba(0,0,0,0.65)",
}}
>
<div
className="overflow-hidden"
style={{
borderRadius: 6,
aspectRatio: "16 / 10",
background: "#0c0b0a",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/hero-dashboard.png"
alt="Agent Orchestrator dashboard — live agent sessions flowing from work to review to merge"
className="w-full h-full"
style={{ objectFit: "cover", objectPosition: "top", display: "block" }}
/>
</div>
</div>
{/* Laptop base / hinge */}
<div style={{ width: "112%", marginLeft: "-6%" }}>
<div
style={{
height: 16,
background: "linear-gradient(180deg, #2a2823 0%, #131210 100%)",
borderRadius: "0 0 12px 12px",
borderTop: "1px solid var(--landing-border-default)",
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: 0,
left: "50%",
transform: "translateX(-50%)",
width: "15%",
height: 6,
background: "#0c0b0a",
borderRadius: "0 0 8px 8px",
}}
/>
</div>
</div>
</div>
</div>
</section>
</div>
);
<div className="landing-fade-rise-d2 w-full max-w-[72rem] mt-16">
<div style={{ maxWidth: "62rem", margin: "0 auto" }}>
{/* Laptop screen / lid */}
<div
className="rounded-[14px]"
style={{
padding: 10,
background: "linear-gradient(180deg, #211f1c 0%, #161513 100%)",
border: "1px solid var(--landing-border-default)",
boxShadow: "0 30px 70px -24px rgba(0,0,0,0.65)",
}}
>
<div
className="overflow-hidden"
style={{
borderRadius: 6,
aspectRatio: "16 / 10",
background: "#0c0b0a",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/hero-dashboard.png"
alt="Agent Orchestrator dashboard — live agent sessions flowing from work to review to merge"
className="w-full h-full"
style={{ objectFit: "cover", objectPosition: "top", display: "block" }}
/>
</div>
</div>
{/* Laptop base / hinge */}
<div style={{ width: "112%", marginLeft: "-6%" }}>
<div
style={{
height: 16,
background: "linear-gradient(180deg, #2a2823 0%, #131210 100%)",
borderRadius: "0 0 12px 12px",
borderTop: "1px solid var(--landing-border-default)",
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: 0,
left: "50%",
transform: "translateX(-50%)",
width: "15%",
height: 6,
background: "#0c0b0a",
borderRadius: "0 0 8px 8px",
}}
/>
</div>
</div>
</div>
</div>
</section>
</div>
);
}

View File

@ -5,312 +5,315 @@ import { useEffect, useRef, useState } from "react";
const DURATION_MS = 3000;
const steps = [
{
n: "01",
title: "Configure & assign",
titleEm: "assign",
desc: "Point Agent Orchestrator at your repo with a YAML config. Choose your agent, set up trackers and notifiers. One file, full control.",
tags: ["YAML", "Plugins", "Trackers"],
kind: "cli" as const,
},
{
n: "02",
title: "Agents work",
titleEm: "work",
desc: "Each agent spawns in an isolated worktree. They write code, create PRs, run tests, and fix failures. Monitor everything from the live dashboard, or let them run.",
tags: ["Worktrees", "Live dashboard", "Parallel"],
kind: "dashboard" as const,
},
{
n: "03",
title: "PRs land",
titleEm: "land",
desc: "Agents create pull requests, address review comments, fix CI failures, and get them to mergeable state. Your morning starts with merged PRs, not a backlog.",
tags: ["Pull requests", "CI fixes", "Review"],
kind: "prs" as const,
},
{
n: "01",
title: "Configure & assign",
titleEm: "assign",
desc: "Point Agent Orchestrator at your repo with a YAML config. Choose your agent, set up trackers and notifiers. One file, full control.",
tags: ["YAML", "Plugins", "Trackers"],
kind: "cli" as const,
},
{
n: "02",
title: "Agents work",
titleEm: "work",
desc: "Each agent spawns in an isolated worktree. They write code, create PRs, run tests, and fix failures. Monitor everything from the live dashboard, or let them run.",
tags: ["Worktrees", "Live dashboard", "Parallel"],
kind: "dashboard" as const,
},
{
n: "03",
title: "PRs land",
titleEm: "land",
desc: "Agents create pull requests, address review comments, fix CI failures, and get them to mergeable state. Your morning starts with merged PRs, not a backlog.",
tags: ["Pull requests", "CI fixes", "Review"],
kind: "prs" as const,
},
];
export function LandingHowItWorks() {
const [active, setActive] = useState(0);
const [progress, setProgress] = useState(0);
const [isDesktop, setIsDesktop] = useState(true);
const pausedRef = useRef(false);
const startRef = useRef<number | null>(null);
const [active, setActive] = useState(0);
const [progress, setProgress] = useState(0);
const [isDesktop, setIsDesktop] = useState(true);
const pausedRef = useRef(false);
const startRef = useRef<number | null>(null);
useEffect(() => {
const mq = window.matchMedia("(min-width: 768px)");
const apply = () => setIsDesktop(mq.matches);
apply();
mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
}, []);
useEffect(() => {
const mq = window.matchMedia("(min-width: 768px)");
const apply = () => setIsDesktop(mq.matches);
apply();
mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
}, []);
useEffect(() => {
let raf = 0;
const tick = (now: number) => {
if (startRef.current === null) startRef.current = now;
if (!pausedRef.current) {
const p = Math.min((now - startRef.current) / DURATION_MS, 1);
setProgress(p);
if (p >= 1) {
startRef.current = now;
setActive((a) => (a + 1) % steps.length);
setProgress(0);
}
} else {
startRef.current = now - progress * DURATION_MS;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active]);
useEffect(() => {
let raf = 0;
const tick = (now: number) => {
if (startRef.current === null) startRef.current = now;
if (!pausedRef.current) {
const p = Math.min((now - startRef.current) / DURATION_MS, 1);
setProgress(p);
if (p >= 1) {
startRef.current = now;
setActive((a) => (a + 1) % steps.length);
setProgress(0);
}
} else {
startRef.current = now - progress * DURATION_MS;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active]);
const select = (i: number) => {
if (i === active) return;
startRef.current = null;
setProgress(0);
setActive(i);
};
const select = (i: number) => {
if (i === active) return;
startRef.current = null;
setProgress(0);
setActive(i);
};
return (
<section className="py-[120px] px-6 max-w-[72rem] mx-auto" id="how">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
Process
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px] mb-6">
Three steps to{" "}
<em className="italic text-[var(--landing-muted)]">orchestration</em>
</h2>
</div>
return (
<section className="py-[120px] px-6 max-w-[72rem] mx-auto" id="how">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">Process</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px] mb-6">
Three steps to <em className="italic text-[var(--landing-muted)]">orchestration</em>
</h2>
</div>
<div
className="landing-reveal mt-16 flex flex-col md:flex-row"
style={isDesktop ? { minHeight: 540 } : undefined}
onMouseEnter={() => (pausedRef.current = true)}
onMouseLeave={() => (pausedRef.current = false)}
>
{steps.map((step, i) => {
const isActive = i === active;
return (
<div
key={step.n}
role="button"
tabIndex={0}
aria-expanded={isActive}
onClick={() => select(i)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
select(i);
}
}}
className="relative min-w-0 cursor-pointer overflow-hidden border-l border-[var(--landing-border-subtle)] pl-7 pr-5 py-2 first:border-l-0 first:pl-0 md:first:pl-7"
style={{
flex: isDesktop
? isActive
? "1 1 0%"
: "0 1 15rem"
: "0 0 auto",
transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)",
}}
>
{/* Header — always visible */}
<div
className="font-mono text-[2.75rem] leading-none font-[680] tracking-tight mb-5"
style={{
color: "var(--landing-muted)",
opacity: isActive ? 0.85 : 0.32,
transition: "opacity 0.4s ease",
}}
>
{step.n}
</div>
<h3
className="font-sans font-[680] tracking-tight text-[1.375rem] leading-[1.15]"
style={{
color: isActive
? "var(--landing-fg)"
: "var(--landing-muted)",
transition: "color 0.4s ease",
maxWidth: isActive ? "100%" : "11rem",
}}
>
{step.title.replace(` ${step.titleEm}`, "")}{" "}
<em className="italic text-[var(--landing-muted)]">
{step.titleEm}
</em>
</h3>
<div
className="landing-reveal mt-16 flex flex-col md:flex-row"
style={isDesktop ? { minHeight: 540 } : undefined}
onMouseEnter={() => (pausedRef.current = true)}
onMouseLeave={() => (pausedRef.current = false)}
>
{steps.map((step, i) => {
const isActive = i === active;
return (
<div
key={step.n}
role="button"
tabIndex={0}
aria-expanded={isActive}
onClick={() => select(i)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
select(i);
}
}}
className="relative min-w-0 cursor-pointer overflow-hidden border-l border-[var(--landing-border-subtle)] pl-7 pr-5 py-2 first:border-l-0 first:pl-0 md:first:pl-7"
style={{
flex: isDesktop ? (isActive ? "1 1 0%" : "0 1 15rem") : "0 0 auto",
transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)",
}}
>
{/* Header — always visible */}
<div
className="font-mono text-[2.75rem] leading-none font-[680] tracking-tight mb-5"
style={{
color: "var(--landing-muted)",
opacity: isActive ? 0.85 : 0.32,
transition: "opacity 0.4s ease",
}}
>
{step.n}
</div>
<h3
className="font-sans font-[680] tracking-tight text-[1.375rem] leading-[1.15]"
style={{
color: isActive ? "var(--landing-fg)" : "var(--landing-muted)",
transition: "color 0.4s ease",
maxWidth: isActive ? "100%" : "11rem",
}}
>
{step.title.replace(` ${step.titleEm}`, "")}{" "}
<em className="italic text-[var(--landing-muted)]">{step.titleEm}</em>
</h3>
{/* Expanding body */}
<div
aria-hidden={!isActive}
style={{
opacity: isActive ? 1 : 0,
maxHeight: isActive ? (isDesktop ? 1000 : 900) : 0,
transition: isActive
? "opacity 0.5s ease 0.15s, max-height 0.6s ease"
: "opacity 0.25s ease, max-height 0.4s ease",
overflow: "hidden",
}}
>
<div className="flex gap-5 pt-7" style={{ minWidth: isDesktop ? "30rem" : undefined }}>
{/* Vertical progress bar */}
<div
className="shrink-0 w-[3px] rounded-full overflow-hidden self-stretch"
style={{ background: "var(--landing-border-default)" }}
>
<div
style={{
height: `${(isActive ? progress : 0) * 100}%`,
background: "var(--landing-accent)",
transition: "height 0.08s linear",
}}
/>
</div>
{/* Expanding body */}
<div
aria-hidden={!isActive}
style={{
opacity: isActive ? 1 : 0,
maxHeight: isActive ? (isDesktop ? 1000 : 900) : 0,
transition: isActive
? "opacity 0.5s ease 0.15s, max-height 0.6s ease"
: "opacity 0.25s ease, max-height 0.4s ease",
overflow: "hidden",
}}
>
<div className="flex gap-5 pt-7" style={{ minWidth: isDesktop ? "30rem" : undefined }}>
{/* Vertical progress bar */}
<div
className="shrink-0 w-[3px] rounded-full overflow-hidden self-stretch"
style={{ background: "var(--landing-border-default)" }}
>
<div
style={{
height: `${(isActive ? progress : 0) * 100}%`,
background: "var(--landing-accent)",
transition: "height 0.08s linear",
}}
/>
</div>
<div className="min-w-0">
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.7] max-w-[30rem] mb-6">
{step.desc}
</p>
<div className="mb-6">
{step.kind === "cli" && <CliDemo />}
{step.kind === "dashboard" && <DashboardDemo />}
{step.kind === "prs" && <PrsDemo />}
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-[0.6875rem] tracking-[0.05em] uppercase text-[var(--landing-muted)] opacity-60">
{step.tags.map((t, ti) => (
<span key={t} className="flex items-center gap-3">
{ti > 0 && <span className="opacity-40">·</span>}
{t}
</span>
))}
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
</section>
);
<div className="min-w-0">
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.7] max-w-[30rem] mb-6">
{step.desc}
</p>
<div className="mb-6">
{step.kind === "cli" && <CliDemo />}
{step.kind === "dashboard" && <DashboardDemo />}
{step.kind === "prs" && <PrsDemo />}
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-[0.6875rem] tracking-[0.05em] uppercase text-[var(--landing-muted)] opacity-60">
{step.tags.map((t, ti) => (
<span key={t} className="flex items-center gap-3">
{ti > 0 && <span className="opacity-40">·</span>}
{t}
</span>
))}
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
</section>
);
}
function CliDemo() {
return (
<div className="bg-black/40 rounded-xl overflow-hidden font-mono text-[0.8125rem]">
<div className="flex items-center gap-2 px-4 py-3 bg-[var(--landing-surface)]">
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
</div>
<div className="px-5 py-4 leading-[1.8]">
<div>
<span className="text-[var(--landing-muted)]">$</span>{" "}
<span className="text-white">ao batch-spawn 42 43 44 45 46</span>
</div>
<div className="text-[var(--landing-muted)] opacity-60">&nbsp;</div>
<div className="text-[var(--landing-muted)] opacity-60"> Loading config from agent-orchestrator.yaml</div>
<div className="text-[var(--landing-muted)] opacity-60"> Resolving 5 issues from GitHub</div>
<div className="text-[var(--landing-muted)] opacity-60"> Spawning sessions in worktrees...</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-001 spawned issue #42</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-002 spawned issue #43</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-003 spawned issue #44</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-004 spawned issue #45</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-005 spawned issue #46</div>
<div className="text-[var(--landing-muted)] opacity-60">&nbsp;</div>
<div>
<span className="landing-agent-dot mr-1.5" />
<span className="text-[var(--landing-muted)] opacity-60">5 agents working · Dashboard http://localhost:3000</span>
</div>
</div>
</div>
);
return (
<div className="bg-black/40 rounded-xl overflow-hidden font-mono text-[0.8125rem]">
<div className="flex items-center gap-2 px-4 py-3 bg-[var(--landing-surface)]">
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
</div>
<div className="px-5 py-4 leading-[1.8]">
<div>
<span className="text-[var(--landing-muted)]">$</span>{" "}
<span className="text-white">ao batch-spawn 42 43 44 45 46</span>
</div>
<div className="text-[var(--landing-muted)] opacity-60">&nbsp;</div>
<div className="text-[var(--landing-muted)] opacity-60"> Loading config from agent-orchestrator.yaml</div>
<div className="text-[var(--landing-muted)] opacity-60"> Resolving 5 issues from GitHub</div>
<div className="text-[var(--landing-muted)] opacity-60"> Spawning sessions in worktrees...</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-001 spawned issue #42</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-002 spawned issue #43</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-003 spawned issue #44</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-004 spawned issue #45</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-005 spawned issue #46</div>
<div className="text-[var(--landing-muted)] opacity-60">&nbsp;</div>
<div>
<span className="landing-agent-dot mr-1.5" />
<span className="text-[var(--landing-muted)] opacity-60">
5 agents working · Dashboard http://localhost:3000
</span>
</div>
</div>
</div>
);
}
function DashboardDemo() {
return (
<div className="rounded-2xl overflow-hidden bg-black/30">
<div className="flex items-center gap-2 px-4 py-2.5 bg-[var(--landing-card-bg)] border-b border-[var(--landing-border-subtle)]">
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<span className="text-[0.6875rem] text-[var(--landing-muted)] opacity-50 ml-2">my-saas-app · 5 sessions</span>
</div>
<div className="grid grid-cols-4 gap-2 p-3">
<DashColumn title="Working" cards={[
{ title: "Add user auth flow", meta: "#42 · feat/auth", agent: "claude-code" },
{ title: "Fix pagination bug", meta: "#43 · fix/pagination", agent: "codex" },
]} />
<DashColumn title="Pending" cards={[
{ title: "Add rate limiting", meta: "#44 · PR #312", agent: "aider" },
]} />
<DashColumn title="Review" cards={[
{ title: "Update API tests", meta: "#45 · PR #310", agent: "claude-code", amber: true },
]} />
<DashColumn title="Merged" cards={[
{ title: "Refactor DB layer", meta: "#46 · PR #308", agent: "opencode", done: true },
]} />
</div>
</div>
);
return (
<div className="rounded-2xl overflow-hidden bg-black/30">
<div className="flex items-center gap-2 px-4 py-2.5 bg-[var(--landing-card-bg)] border-b border-[var(--landing-border-subtle)]">
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<span className="text-[0.6875rem] text-[var(--landing-muted)] opacity-50 ml-2">my-saas-app · 5 sessions</span>
</div>
<div className="grid grid-cols-4 gap-2 p-3">
<DashColumn
title="Working"
cards={[
{ title: "Add user auth flow", meta: "#42 · feat/auth", agent: "claude-code" },
{ title: "Fix pagination bug", meta: "#43 · fix/pagination", agent: "codex" },
]}
/>
<DashColumn title="Pending" cards={[{ title: "Add rate limiting", meta: "#44 · PR #312", agent: "aider" }]} />
<DashColumn
title="Review"
cards={[{ title: "Update API tests", meta: "#45 · PR #310", agent: "claude-code", amber: true }]}
/>
<DashColumn
title="Merged"
cards={[{ title: "Refactor DB layer", meta: "#46 · PR #308", agent: "opencode", done: true }]}
/>
</div>
</div>
);
}
function PrsDemo() {
return (
<div className="flex flex-col gap-2.5">
{[
{ branch: "feat/user-auth", title: "Add user authentication flow" },
{ branch: "fix/pagination-offset", title: "Fix off-by-one in cursor pagination" },
{ branch: "feat/rate-limiting", title: "Add Redis-backed rate limiter" },
{ branch: "refactor/db-layer", title: "Extract repository pattern from services" },
].map((pr) => (
<div key={pr.branch} className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-xl px-5 py-4 flex items-center justify-between">
<div className="flex flex-col gap-1">
<div className="font-mono text-xs text-[var(--landing-fg)]/70">{pr.branch}</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)]">{pr.title}</div>
</div>
<div className="font-mono text-[0.625rem] tracking-[0.05em] px-3 py-1 rounded-full bg-[rgba(134,239,172,0.08)] text-[rgba(134,239,172,0.7)]">
Merged
</div>
</div>
))}
</div>
);
return (
<div className="flex flex-col gap-2.5">
{[
{ branch: "feat/user-auth", title: "Add user authentication flow" },
{ branch: "fix/pagination-offset", title: "Fix off-by-one in cursor pagination" },
{ branch: "feat/rate-limiting", title: "Add Redis-backed rate limiter" },
{ branch: "refactor/db-layer", title: "Extract repository pattern from services" },
].map((pr) => (
<div
key={pr.branch}
className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-xl px-5 py-4 flex items-center justify-between"
>
<div className="flex flex-col gap-1">
<div className="font-mono text-xs text-[var(--landing-fg)]/70">{pr.branch}</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)]">{pr.title}</div>
</div>
<div className="font-mono text-[0.625rem] tracking-[0.05em] px-3 py-1 rounded-full bg-[rgba(134,239,172,0.08)] text-[rgba(134,239,172,0.7)]">
Merged
</div>
</div>
))}
</div>
);
}
interface DashCardData {
title: string;
meta: string;
agent: string;
amber?: boolean;
done?: boolean;
title: string;
meta: string;
agent: string;
amber?: boolean;
done?: boolean;
}
function DashColumn({ title, cards }: { title: string; cards: DashCardData[] }) {
return (
<div>
<div className="font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-muted)] opacity-40 px-2 mb-1">
{title}
</div>
{cards.map((card) => (
<div key={card.meta} className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-lg p-2.5 mb-1.5 text-[0.6875rem]">
<div className="text-[var(--landing-fg)]/70 mb-1">{card.title}</div>
<div className="font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-50">{card.meta}</div>
<div className="flex items-center gap-1 mt-1 font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-60">
{card.done ? (
<span></span>
) : (
<span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: card.amber ? "rgba(251,191,36,0.7)" : "rgba(134,239,172,0.7)" }} />
)}
{card.agent}
</div>
</div>
))}
</div>
);
return (
<div>
<div className="font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-muted)] opacity-40 px-2 mb-1">
{title}
</div>
{cards.map((card) => (
<div
key={card.meta}
className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-lg p-2.5 mb-1.5 text-[0.6875rem]"
>
<div className="text-[var(--landing-fg)]/70 mb-1">{card.title}</div>
<div className="font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-50">{card.meta}</div>
<div className="flex items-center gap-1 mt-1 font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-60">
{card.done ? (
<span></span>
) : (
<span
className="inline-block w-1.5 h-1.5 rounded-full"
style={{ background: card.amber ? "rgba(251,191,36,0.7)" : "rgba(134,239,172,0.7)" }}
/>
)}
{card.agent}
</div>
</div>
))}
</div>
);
}

View File

@ -1,85 +1,94 @@
"use client";
function XIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
}
function DiscordIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
);
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
);
}
function GithubIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
);
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
);
}
export function LandingNav() {
return (
<nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-8 py-6 max-w-[80rem] mx-auto bg-[var(--landing-bg)]/90 backdrop-blur-sm">
<a
href="#"
className="inline-flex items-center gap-2 text-base font-semibold text-white no-underline font-sans font-[680] tracking-tight"
>
<img src="/ao-logo.svg" alt="" aria-hidden="true" width={28} height={28} className="h-7 w-7" />
Agent Orchestrator
</a>
<ul className="hidden md:flex items-center gap-8 list-none">
<li>
<a href="/docs" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
Docs
</a>
</li>
<li>
<a href="#features" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
Features
</a>
</li>
<li>
<a href="#how" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
How It Works
</a>
</li>
</ul>
<div className="flex items-center gap-2">
<a
href="https://x.com/aoagents"
target="_blank"
rel="noopener noreferrer"
aria-label="X (Twitter)"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<XIcon />
</a>
<a
href="https://discord.gg/UZv7JjxbwG"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<DiscordIcon />
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<GithubIcon />
</a>
</div>
</nav>
);
return (
<nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-8 py-6 max-w-[80rem] mx-auto bg-[var(--landing-bg)]/90 backdrop-blur-sm">
<a
href="#"
className="inline-flex items-center gap-2 text-base font-semibold text-white no-underline font-sans font-[680] tracking-tight"
>
<img src="/ao-logo.svg" alt="" aria-hidden="true" width={28} height={28} className="h-7 w-7" />
Agent Orchestrator
</a>
<ul className="hidden md:flex items-center gap-8 list-none">
<li>
<a
href="/docs"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Docs
</a>
</li>
<li>
<a
href="#features"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Features
</a>
</li>
<li>
<a
href="#how"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
How It Works
</a>
</li>
</ul>
<div className="flex items-center gap-2">
<a
href="https://x.com/aoagents"
target="_blank"
rel="noopener noreferrer"
aria-label="X (Twitter)"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<XIcon />
</a>
<a
href="https://discord.gg/UZv7JjxbwG"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<DiscordIcon />
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<GithubIcon />
</a>
</div>
</nav>
);
}

View File

@ -1,44 +1,52 @@
const steps = [
{ num: "STEP 01", title: "Install", desc: "One command. No dependencies beyond Node.js.", cmd: "npm i -g @aoagents/ao" },
{ num: "STEP 02", title: "Configure", desc: "Create an agent-orchestrator.yaml. Pick your agents, tracker, and notifiers.", cmd: "ao start" },
{ num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" },
{
num: "STEP 01",
title: "Install",
desc: "One command. No dependencies beyond Node.js.",
cmd: "npm i -g @aoagents/ao",
},
{
num: "STEP 02",
title: "Configure",
desc: "Create an agent-orchestrator.yaml. Pick your agents, tracker, and notifiers.",
cmd: "ao start",
},
{ num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" },
];
export function LandingQuickStart() {
return (
<section className="py-[120px] px-6 max-w-[72rem] mx-auto bg-[radial-gradient(ellipse_at_bottom,rgba(255,255,255,0.015)_0%,transparent_60%)]">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
Get started in 60 seconds
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px] mb-6">
Three commands to{" "}
<em className="italic text-[var(--landing-muted)]">launch</em>
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mt-12">
{steps.map((s) => (
<div key={s.num} className="landing-reveal landing-card rounded-2xl p-7">
<div className="font-mono text-[0.625rem] tracking-[0.1em] text-[var(--landing-muted)] opacity-40 mb-3">
{s.num}
</div>
<h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight">
{s.title}
</h3>
<p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">
{s.desc}
</p>
<div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg">
<span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd}
</div>
</div>
))}
</div>
<div className="landing-reveal mt-8 text-center">
<a href="/docs/" className="landing-card inline-flex rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
Explore docs for setup and workflows
</a>
</div>
</section>
);
return (
<section className="py-[120px] px-6 max-w-[72rem] mx-auto bg-[radial-gradient(ellipse_at_bottom,rgba(255,255,255,0.015)_0%,transparent_60%)]">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
Get started in 60 seconds
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px] mb-6">
Three commands to <em className="italic text-[var(--landing-muted)]">launch</em>
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mt-12">
{steps.map((s) => (
<div key={s.num} className="landing-reveal landing-card rounded-2xl p-7">
<div className="font-mono text-[0.625rem] tracking-[0.1em] text-[var(--landing-muted)] opacity-40 mb-3">
{s.num}
</div>
<h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight">{s.title}</h3>
<p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">{s.desc}</p>
<div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg">
<span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd}
</div>
</div>
))}
</div>
<div className="landing-reveal mt-8 text-center">
<a
href="/docs/"
className="landing-card inline-flex rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Explore docs for setup and workflows
</a>
</div>
</section>
);
}

View File

@ -1,51 +1,46 @@
import type { GitHubRepoStats } from "@/lib/github-repo";
interface LandingStatsProps {
stats: GitHubRepoStats;
stats: GitHubRepoStats;
}
export function LandingStats({ stats }: LandingStatsProps) {
const cards = [
{ number: stats.stars.toLocaleString(), label: "GitHub Stars" },
{ number: stats.forks.toLocaleString(), label: "Forks" },
{ number: stats.openIssues.toLocaleString(), label: "Open Issues" },
{ number: stats.watchers.toLocaleString(), label: "Watchers" },
];
const cards = [
{ number: stats.stars.toLocaleString(), label: "GitHub Stars" },
{ number: stats.forks.toLocaleString(), label: "Forks" },
{ number: stats.openIssues.toLocaleString(), label: "Open Issues" },
{ number: stats.watchers.toLocaleString(), label: "Watchers" },
];
return (
<section className="py-20 px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal grid grid-cols-2 md:grid-cols-4 gap-5">
{cards.map((stat) => (
<div
key={stat.label}
className="landing-card rounded-2xl py-8 px-6 text-center"
>
<div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1">
{stat.number}
</div>
<div className="text-xs text-[var(--landing-muted)] opacity-60">
{stat.label}
</div>
</div>
))}
</div>
<div className="landing-reveal text-center mt-8">
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="landing-card inline-flex items-center gap-2 rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline transition-all hover:text-white mb-3"
>
<span className="text-[rgba(251,191,36,0.7)] text-sm"></span>
<span className="font-mono text-xs text-[var(--landing-fg)] opacity-80">{stats.stars.toLocaleString()}</span>
<span>stars on GitHub</span>
</a>
<br />
<div className="landing-card inline-flex items-center gap-2 rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)]">
<span className="w-2 h-2 rounded-full bg-[rgba(134,239,172,0.7)] animate-pulse" />
Built with itself this repo is managed by Agent Orchestrator
</div>
</div>
</section>
);
return (
<section className="py-20 px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal grid grid-cols-2 md:grid-cols-4 gap-5">
{cards.map((stat) => (
<div key={stat.label} className="landing-card rounded-2xl py-8 px-6 text-center">
<div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1">
{stat.number}
</div>
<div className="text-xs text-[var(--landing-muted)] opacity-60">{stat.label}</div>
</div>
))}
</div>
<div className="landing-reveal text-center mt-8">
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="landing-card inline-flex items-center gap-2 rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline transition-all hover:text-white mb-3"
>
<span className="text-[rgba(251,191,36,0.7)] text-sm"></span>
<span className="font-mono text-xs text-[var(--landing-fg)] opacity-80">{stats.stars.toLocaleString()}</span>
<span>stars on GitHub</span>
</a>
<br />
<div className="landing-card inline-flex items-center gap-2 rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)]">
<span className="w-2 h-2 rounded-full bg-[rgba(134,239,172,0.7)] animate-pulse" />
Built with itself this repo is managed by Agent Orchestrator
</div>
</div>
</section>
);
}

View File

@ -3,170 +3,155 @@
import { useEffect, useState } from "react";
const testimonials = [
{
quote:
"Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.",
img: "https://i.pravatar.cc/120?img=13",
name: "Staff Engineer",
role: "Series B Startup",
},
{
quote:
"The auto CI recovery alone saves me hours a week. Agents fix their own broken tests. I just review and merge.",
img: "https://i.pravatar.cc/120?img=32",
name: "Solo Founder",
role: "Indie SaaS",
},
{
quote:
"We went from 3 PRs/day to 15 PRs/day. The plugin system means we swapped in GitLab and Linear without changing our workflow.",
img: "https://i.pravatar.cc/120?img=8",
name: "Eng Lead",
role: "20-person team",
},
{
quote: "Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.",
img: "https://i.pravatar.cc/120?img=13",
name: "Staff Engineer",
role: "Series B Startup",
},
{
quote:
"The auto CI recovery alone saves me hours a week. Agents fix their own broken tests. I just review and merge.",
img: "https://i.pravatar.cc/120?img=32",
name: "Solo Founder",
role: "Indie SaaS",
},
{
quote:
"We went from 3 PRs/day to 15 PRs/day. The plugin system means we swapped in GitLab and Linear without changing our workflow.",
img: "https://i.pravatar.cc/120?img=8",
name: "Eng Lead",
role: "20-person team",
},
];
const ROTATE_MS = 5500;
export function LandingTestimonials() {
const [active, setActive] = useState(0);
const [show, setShow] = useState(true);
const [paused, setPaused] = useState(false);
const [active, setActive] = useState(0);
const [show, setShow] = useState(true);
const [paused, setPaused] = useState(false);
const change = (next: number) => {
if (next === active) return;
setShow(false);
window.setTimeout(() => {
setActive(next);
setShow(true);
}, 240);
};
const change = (next: number) => {
if (next === active) return;
setShow(false);
window.setTimeout(() => {
setActive(next);
setShow(true);
}, 240);
};
useEffect(() => {
if (paused) return;
const t = window.setTimeout(
() => change((active + 1) % testimonials.length),
ROTATE_MS,
);
return () => window.clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, paused]);
useEffect(() => {
if (paused) return;
const t = window.setTimeout(() => change((active + 1) % testimonials.length), ROTATE_MS);
return () => window.clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, paused]);
const t = testimonials[active];
const t = testimonials[active];
return (
<section className="py-20 px-6 pb-[120px] max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
What engineers say
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px]">
Trusted by <em className="italic text-[var(--landing-muted)]">builders</em>
</h2>
</div>
return (
<section className="py-20 px-6 pb-[120px] max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
What engineers say
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px]">
Trusted by <em className="italic text-[var(--landing-muted)]">builders</em>
</h2>
</div>
<div
className="landing-reveal mt-16"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Quote — fades on change */}
<div className="min-h-[8rem] max-w-[58rem]">
<blockquote
style={{
opacity: show ? 1 : 0,
transform: show ? "translateY(0)" : "translateY(8px)",
transition: "opacity 0.4s ease, transform 0.4s cubic-bezier(0.22,1,0.36,1)",
}}
className="font-sans font-[680] tracking-tight text-[clamp(1.5rem,3.2vw,2.375rem)] leading-[1.3] tracking-[-0.75px] text-[var(--landing-fg)]"
>
&ldquo;{t.quote}&rdquo;
</blockquote>
</div>
<div className="landing-reveal mt-16" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
{/* Quote — fades on change */}
<div className="min-h-[8rem] max-w-[58rem]">
<blockquote
style={{
opacity: show ? 1 : 0,
transform: show ? "translateY(0)" : "translateY(8px)",
transition: "opacity 0.4s ease, transform 0.4s cubic-bezier(0.22,1,0.36,1)",
}}
className="font-sans font-[680] tracking-tight text-[clamp(1.5rem,3.2vw,2.375rem)] leading-[1.3] tracking-[-0.75px] text-[var(--landing-fg)]"
>
&ldquo;{t.quote}&rdquo;
</blockquote>
</div>
{/* Bottom row — author cluster on the left, step counter on the right */}
<div className="flex items-center justify-between gap-6 mt-14 flex-wrap">
<div className="flex items-center gap-5">
<div className="flex items-center">
{testimonials.map((item, i) => {
const isActive = i === active;
const size = isActive ? 56 : 44;
return (
<button
key={item.name}
onClick={() => change(i)}
aria-label={`Show testimonial from ${item.name}`}
aria-pressed={isActive}
className="rounded-full overflow-hidden cursor-pointer shrink-0 p-0"
style={{
width: size,
height: size,
marginLeft: i === 0 ? 0 : -14,
zIndex: isActive ? 30 : 10 - i,
border: `2px solid ${isActive ? "var(--landing-accent)" : "var(--landing-card-bg)"}`,
opacity: isActive ? 1 : 0.7,
boxShadow: isActive ? "0 4px 16px rgba(0,0,0,0.35)" : "none",
transition:
"width 0.4s cubic-bezier(0.22,1,0.36,1), height 0.4s cubic-bezier(0.22,1,0.36,1), opacity 0.4s ease, border-color 0.4s ease",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={item.img}
alt={item.name}
width={size}
height={size}
className="w-full h-full object-cover"
style={{
filter: isActive ? "grayscale(0)" : "grayscale(1)",
transition: "filter 0.4s ease",
}}
/>
</button>
);
})}
</div>
{/* Bottom row — author cluster on the left, step counter on the right */}
<div className="flex items-center justify-between gap-6 mt-14 flex-wrap">
<div className="flex items-center gap-5">
<div className="flex items-center">
{testimonials.map((item, i) => {
const isActive = i === active;
const size = isActive ? 56 : 44;
return (
<button
key={item.name}
onClick={() => change(i)}
aria-label={`Show testimonial from ${item.name}`}
aria-pressed={isActive}
className="rounded-full overflow-hidden cursor-pointer shrink-0 p-0"
style={{
width: size,
height: size,
marginLeft: i === 0 ? 0 : -14,
zIndex: isActive ? 30 : 10 - i,
border: `2px solid ${isActive ? "var(--landing-accent)" : "var(--landing-card-bg)"}`,
opacity: isActive ? 1 : 0.7,
boxShadow: isActive ? "0 4px 16px rgba(0,0,0,0.35)" : "none",
transition:
"width 0.4s cubic-bezier(0.22,1,0.36,1), height 0.4s cubic-bezier(0.22,1,0.36,1), opacity 0.4s ease, border-color 0.4s ease",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={item.img}
alt={item.name}
width={size}
height={size}
className="w-full h-full object-cover"
style={{
filter: isActive ? "grayscale(0)" : "grayscale(1)",
transition: "filter 0.4s ease",
}}
/>
</button>
);
})}
</div>
{/* Vertical divider */}
<div
className="w-px h-10 shrink-0"
style={{ background: "var(--landing-border-default)" }}
/>
{/* Vertical divider */}
<div className="w-px h-10 shrink-0" style={{ background: "var(--landing-border-default)" }} />
{/* Author — fades on change */}
<div
style={{
opacity: show ? 1 : 0,
transition: "opacity 0.4s ease 0.05s",
}}
>
<div className="text-[0.9375rem] font-medium text-[var(--landing-fg)]">
{t.name}
</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60">
{t.role}
</div>
</div>
</div>
{/* Author — fades on change */}
<div
style={{
opacity: show ? 1 : 0,
transition: "opacity 0.4s ease 0.05s",
}}
>
<div className="text-[0.9375rem] font-medium text-[var(--landing-fg)]">{t.name}</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60">{t.role}</div>
</div>
</div>
{/* Step counter — fills the right side */}
<div className="flex items-baseline gap-2 font-mono">
<span
className="text-[clamp(2.25rem,5vw,3.5rem)] font-[680] tracking-tight leading-none tabular-nums"
style={{
color: "var(--landing-fg)",
opacity: show ? 1 : 0.4,
transition: "opacity 0.3s ease",
}}
>
{String(active + 1).padStart(2, "0")}
</span>
<span className="text-[var(--landing-muted)] opacity-40 text-lg">
/ {String(testimonials.length).padStart(2, "0")}
</span>
</div>
</div>
</div>
</section>
);
{/* Step counter — fills the right side */}
<div className="flex items-baseline gap-2 font-mono">
<span
className="text-[clamp(2.25rem,5vw,3.5rem)] font-[680] tracking-tight leading-none tabular-nums"
style={{
color: "var(--landing-fg)",
opacity: show ? 1 : 0.4,
transition: "opacity 0.3s ease",
}}
>
{String(active + 1).padStart(2, "0")}
</span>
<span className="text-[var(--landing-muted)] opacity-40 text-lg">
/ {String(testimonials.length).padStart(2, "0")}
</span>
</div>
</div>
</div>
</section>
);
}

View File

@ -7,64 +7,64 @@ const fg = "text-[var(--landing-fg)]/80";
const ok = "text-[rgba(134,239,172,0.8)]";
type UseCase = {
eyebrow: string;
title: string;
desc: string;
prefix: "$" | "⟡";
cmd: string;
outcome: string;
eyebrow: string;
title: string;
desc: string;
prefix: "$" | "⟡";
cmd: string;
outcome: string;
};
// Real, grounded use cases — real ao commands, reaction keys, and lifecycle states.
const cases: UseCase[] = [
{
eyebrow: "Backlog",
title: "Clear it overnight",
desc: "One agent per issue, each in its own git worktree, all running at once.",
prefix: "$",
cmd: "ao batch-spawn 142 143 144 145",
outcome: "4 worktrees · 4 PRs",
},
{
eyebrow: "CI recovery",
title: "Self-healing builds",
desc: "A check goes red; the agent reads the logs, pushes a fix, and waits for green.",
prefix: "⟡",
cmd: "reaction · ci-failed",
outcome: "ci_failed → mergeable",
},
{
eyebrow: "Review loop",
title: "Answers its own reviews",
desc: "Comments land; the agent addresses each one and re-requests review.",
prefix: "⟡",
cmd: "reaction · changes-requested",
outcome: "changes_requested → approved",
},
{
eyebrow: "Migration",
title: "Grinds through the long ones",
desc: "Hand one agent a sweeping change and let it work file by file until tests pass.",
prefix: "$",
cmd: "ao spawn 305 --agent claude-code",
outcome: "23 files · tests green",
},
{
eyebrow: "Per-role",
title: "Right model per job",
desc: "Claude Code orchestrates, Codex does the work. Pick the tool per task.",
prefix: "$",
cmd: "ao spawn 88 --agent codex",
outcome: "codex #88 · claude-code #91",
},
{
eyebrow: "Multi-project",
title: "Every repo, one screen",
desc: "Register all your repos and supervise their agents from a single dashboard.",
prefix: "$",
cmd: "ao start",
outcome: "3 projects · one dashboard",
},
{
eyebrow: "Backlog",
title: "Clear it overnight",
desc: "One agent per issue, each in its own git worktree, all running at once.",
prefix: "$",
cmd: "ao batch-spawn 142 143 144 145",
outcome: "4 worktrees · 4 PRs",
},
{
eyebrow: "CI recovery",
title: "Self-healing builds",
desc: "A check goes red; the agent reads the logs, pushes a fix, and waits for green.",
prefix: "⟡",
cmd: "reaction · ci-failed",
outcome: "ci_failed → mergeable",
},
{
eyebrow: "Review loop",
title: "Answers its own reviews",
desc: "Comments land; the agent addresses each one and re-requests review.",
prefix: "⟡",
cmd: "reaction · changes-requested",
outcome: "changes_requested → approved",
},
{
eyebrow: "Migration",
title: "Grinds through the long ones",
desc: "Hand one agent a sweeping change and let it work file by file until tests pass.",
prefix: "$",
cmd: "ao spawn 305 --agent claude-code",
outcome: "23 files · tests green",
},
{
eyebrow: "Per-role",
title: "Right model per job",
desc: "Claude Code orchestrates, Codex does the work. Pick the tool per task.",
prefix: "$",
cmd: "ao spawn 88 --agent codex",
outcome: "codex #88 · claude-code #91",
},
{
eyebrow: "Multi-project",
title: "Every repo, one screen",
desc: "Register all your repos and supervise their agents from a single dashboard.",
prefix: "$",
cmd: "ao start",
outcome: "3 projects · one dashboard",
},
];
const N = cases.length;
@ -74,162 +74,153 @@ const CARD_W = 360;
const CARD_H = 440;
export function LandingUseCases() {
const viewportRef = useRef<HTMLDivElement>(null);
const ringRef = useRef<HTMLDivElement>(null);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const viewportRef = useRef<HTMLDivElement>(null);
const ringRef = useRef<HTMLDivElement>(null);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const angle = useRef(0);
const dragging = useRef(false);
const paused = useRef(false);
const reduced = useRef(false);
const start = useRef({ x: 0, a: 0 });
const angle = useRef(0);
const dragging = useRef(false);
const paused = useRef(false);
const reduced = useRef(false);
const start = useRef({ x: 0, a: 0 });
// rAF loop — rotate the ring and fade/scale each card by how far it faces the
// camera. Imperative (no setState) so 60fps stays smooth and re-render-free.
useEffect(() => {
reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let raf = 0;
const loop = () => {
if (!dragging.current && !paused.current && !reduced.current) {
angle.current += 0.12;
}
const a = angle.current;
if (ringRef.current) {
ringRef.current.style.transform = `translateZ(-${RADIUS}px) rotateY(${a}deg)`;
}
cardRefs.current.forEach((el, i) => {
if (!el) return;
const facing = Math.cos(((i * THETA + a) * Math.PI) / 180);
const vis = Math.max(facing, 0);
el.style.opacity = `${0.2 + 0.8 * vis}`;
el.style.transform = `rotateY(${i * THETA}deg) translateZ(${RADIUS}px) scale(${0.9 + 0.1 * vis})`;
});
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
// rAF loop — rotate the ring and fade/scale each card by how far it faces the
// camera. Imperative (no setState) so 60fps stays smooth and re-render-free.
useEffect(() => {
reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let raf = 0;
const loop = () => {
if (!dragging.current && !paused.current && !reduced.current) {
angle.current += 0.12;
}
const a = angle.current;
if (ringRef.current) {
ringRef.current.style.transform = `translateZ(-${RADIUS}px) rotateY(${a}deg)`;
}
cardRefs.current.forEach((el, i) => {
if (!el) return;
const facing = Math.cos(((i * THETA + a) * Math.PI) / 180);
const vis = Math.max(facing, 0);
el.style.opacity = `${0.2 + 0.8 * vis}`;
el.style.transform = `rotateY(${i * THETA}deg) translateZ(${RADIUS}px) scale(${0.9 + 0.1 * vis})`;
});
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
dragging.current = true;
start.current = { x: e.clientX, a: angle.current };
e.currentTarget.setPointerCapture(e.pointerId);
if (viewportRef.current) viewportRef.current.style.cursor = "grabbing";
};
const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
if (!dragging.current) return;
angle.current = start.current.a + (e.clientX - start.current.x) * 0.4;
};
const onPointerUp = () => {
dragging.current = false;
if (viewportRef.current) viewportRef.current.style.cursor = "grab";
};
const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
dragging.current = true;
start.current = { x: e.clientX, a: angle.current };
e.currentTarget.setPointerCapture(e.pointerId);
if (viewportRef.current) viewportRef.current.style.cursor = "grabbing";
};
const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
if (!dragging.current) return;
angle.current = start.current.a + (e.clientX - start.current.x) * 0.4;
};
const onPointerUp = () => {
dragging.current = false;
if (viewportRef.current) viewportRef.current.style.cursor = "grab";
};
return (
<section className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal text-center">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
Use cases
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-4">
One orchestrator, many jobs
</h2>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6] max-w-[34rem] mx-auto mb-12">
Point AO at the work and walk away drag to explore what a single
run can do.
</p>
</div>
return (
<section className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal text-center">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
Use cases
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-4">
One orchestrator, many jobs
</h2>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6] max-w-[34rem] mx-auto mb-12">
Point AO at the work and walk away drag to explore what a single run can do.
</p>
</div>
<div
ref={viewportRef}
onMouseEnter={() => (paused.current = true)}
onMouseLeave={() => {
paused.current = false;
onPointerUp();
}}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
className="landing-reveal relative mx-auto select-none"
style={{
perspective: "1900px",
height: `${CARD_H + 80}px`,
maxWidth: "1120px",
cursor: "grab",
touchAction: "pan-y",
WebkitMaskImage:
"linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
maskImage:
"linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
}}
>
<div
ref={ringRef}
style={{
position: "absolute",
inset: 0,
transformStyle: "preserve-3d",
}}
>
{cases.map((c, i) => (
<div
key={c.eyebrow}
ref={(el) => {
cardRefs.current[i] = el;
}}
style={{
position: "absolute",
left: "50%",
top: "50%",
width: `${CARD_W}px`,
height: `${CARD_H}px`,
marginLeft: `-${CARD_W / 2}px`,
marginTop: `-${CARD_H / 2}px`,
backfaceVisibility: "hidden",
}}
>
<div
className="landing-card rounded-2xl"
style={{
width: "100%",
height: "100%",
padding: "1.875rem",
display: "flex",
flexDirection: "column",
}}
>
<div className="font-mono text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-accent)] opacity-80">
{c.eyebrow}
</div>
<h3
className="font-sans font-[680] text-[1.3125rem] tracking-tight"
style={{ marginTop: "1rem" }}
>
{c.title}
</h3>
<p
className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.65]"
style={{ marginTop: "0.625rem" }}
>
{c.desc}
</p>
<div
className="font-mono text-[0.6875rem] leading-[2] bg-black/30 rounded-lg overflow-hidden"
style={{ marginTop: "auto", padding: "0.75rem 0.875rem" }}
>
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
<span className={dim}>{c.prefix}</span>{" "}
<span className={fg}>{c.cmd}</span>
</div>
<div className={`whitespace-nowrap overflow-hidden text-ellipsis ${ok}`}>
{c.outcome}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
<div
ref={viewportRef}
onMouseEnter={() => (paused.current = true)}
onMouseLeave={() => {
paused.current = false;
onPointerUp();
}}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
className="landing-reveal relative mx-auto select-none"
style={{
perspective: "1900px",
height: `${CARD_H + 80}px`,
maxWidth: "1120px",
cursor: "grab",
touchAction: "pan-y",
WebkitMaskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
maskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
}}
>
<div
ref={ringRef}
style={{
position: "absolute",
inset: 0,
transformStyle: "preserve-3d",
}}
>
{cases.map((c, i) => (
<div
key={c.eyebrow}
ref={(el) => {
cardRefs.current[i] = el;
}}
style={{
position: "absolute",
left: "50%",
top: "50%",
width: `${CARD_W}px`,
height: `${CARD_H}px`,
marginLeft: `-${CARD_W / 2}px`,
marginTop: `-${CARD_H / 2}px`,
backfaceVisibility: "hidden",
}}
>
<div
className="landing-card rounded-2xl"
style={{
width: "100%",
height: "100%",
padding: "1.875rem",
display: "flex",
flexDirection: "column",
}}
>
<div className="font-mono text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-accent)] opacity-80">
{c.eyebrow}
</div>
<h3 className="font-sans font-[680] text-[1.3125rem] tracking-tight" style={{ marginTop: "1rem" }}>
{c.title}
</h3>
<p
className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.65]"
style={{ marginTop: "0.625rem" }}
>
{c.desc}
</p>
<div
className="font-mono text-[0.6875rem] leading-[2] bg-black/30 rounded-lg overflow-hidden"
style={{ marginTop: "auto", padding: "0.75rem 0.875rem" }}
>
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
<span className={dim}>{c.prefix}</span> <span className={fg}>{c.cmd}</span>
</div>
<div className={`whitespace-nowrap overflow-hidden text-ellipsis ${ok}`}> {c.outcome}</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}

View File

@ -1,20 +1,20 @@
export function LandingVideo() {
return (
<section className="landing-reveal px-6 pb-[120px] pt-10 max-w-[72rem] mx-auto">
<div className="text-center mb-5">
<span className="text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-muted)] opacity-50">
See it in action
</span>
</div>
<div className="landing-card rounded-2xl overflow-hidden aspect-video">
<iframe
src="https://www.youtube.com/embed/QdwaeEXOmDs?autoplay=0&rel=0&modestbranding=1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="w-full h-full border-none"
title="Agent Orchestrator Launch Demo"
/>
</div>
</section>
);
return (
<section className="landing-reveal px-6 pb-[120px] pt-10 max-w-[72rem] mx-auto">
<div className="text-center mb-5">
<span className="text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-muted)] opacity-50">
See it in action
</span>
</div>
<div className="landing-card rounded-2xl overflow-hidden aspect-video">
<iframe
src="https://www.youtube.com/embed/QdwaeEXOmDs?autoplay=0&rel=0&modestbranding=1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="w-full h-full border-none"
title="Agent Orchestrator Launch Demo"
/>
</div>
</section>
);
}

View File

@ -7,19 +7,49 @@ import { useEffect, useRef, useState } from "react";
// the active milestone sits under the fixed center line; auto-loops, draggable,
// arrow-key navigable.
type Milestone = {
key: string;
label: string;
desc: string;
icon: "spawn" | "work" | "pr" | "review" | "mergeable" | "merged";
key: string;
label: string;
desc: string;
icon: "spawn" | "work" | "pr" | "review" | "mergeable" | "merged";
};
const milestones: Milestone[] = [
{ key: "spawning", label: "Spawn", icon: "spawn", desc: "Each issue spawns an agent in its own git worktree — isolated branch, isolated context." },
{ key: "working", label: "Work", icon: "work", desc: "The agent writes code, runs the test suite, and commits. Watch it live or let it run." },
{ key: "pr_open", label: "Open PR", icon: "pr", desc: "Work is pushed and a pull request opens against main with a summary of the changes." },
{ key: "review", label: "CI & review", icon: "review", desc: "CI fails? It reads the logs and pushes a fix. Review comments land? It addresses them." },
{ key: "mergeable", label: "Mergeable", icon: "mergeable", desc: "Green checks, approvals in. The PR settles into a clean, mergeable state." },
{ key: "merged", label: "Merged", icon: "merged", desc: "It lands on main, the worktree is archived, and the session is marked done." },
{
key: "spawning",
label: "Spawn",
icon: "spawn",
desc: "Each issue spawns an agent in its own git worktree — isolated branch, isolated context.",
},
{
key: "working",
label: "Work",
icon: "work",
desc: "The agent writes code, runs the test suite, and commits. Watch it live or let it run.",
},
{
key: "pr_open",
label: "Open PR",
icon: "pr",
desc: "Work is pushed and a pull request opens against main with a summary of the changes.",
},
{
key: "review",
label: "CI & review",
icon: "review",
desc: "CI fails? It reads the logs and pushes a fix. Review comments land? It addresses them.",
},
{
key: "mergeable",
label: "Mergeable",
icon: "mergeable",
desc: "Green checks, approvals in. The PR settles into a clean, mergeable state.",
},
{
key: "merged",
label: "Merged",
icon: "merged",
desc: "It lands on main, the worktree is archived, and the session is marked done.",
},
];
// Ruler geometry (viewBox units)
@ -38,293 +68,292 @@ const STEP_MS = 2800;
const arcTop = (dx: number) => PEAK_Y + CURV * dx * dx;
export function LandingWorkflow() {
const [active, setActive] = useState(0);
const [pos, setPos] = useState(0); // current center position in tick units
const [show, setShow] = useState(true);
const [paused, setPaused] = useState(false);
const [inView, setInView] = useState(false);
const [active, setActive] = useState(0);
const [pos, setPos] = useState(0); // current center position in tick units
const [show, setShow] = useState(true);
const [paused, setPaused] = useState(false);
const [inView, setInView] = useState(false);
const wrapRef = useRef<HTMLDivElement>(null);
const svgWrapRef = useRef<HTMLDivElement>(null);
const posRef = useRef(0);
const rafRef = useRef(0);
const drag = useRef<{ active: boolean; startX: number; startPos: number; moved: boolean }>({
active: false,
startX: 0,
startPos: 0,
moved: false,
});
const wrapRef = useRef<HTMLDivElement>(null);
const svgWrapRef = useRef<HTMLDivElement>(null);
const posRef = useRef(0);
const rafRef = useRef(0);
const drag = useRef<{ active: boolean; startX: number; startPos: number; moved: boolean }>({
active: false,
startX: 0,
startPos: 0,
moved: false,
});
useEffect(() => {
const el = wrapRef.current;
if (!el) return;
const ob = new IntersectionObserver(
([entry]) => entry.isIntersecting && setInView(true),
{ threshold: 0.25 },
);
ob.observe(el);
return () => ob.disconnect();
}, []);
useEffect(() => {
const el = wrapRef.current;
if (!el) return;
const ob = new IntersectionObserver(([entry]) => entry.isIntersecting && setInView(true), { threshold: 0.25 });
ob.observe(el);
return () => ob.disconnect();
}, []);
const tweenTo = (target: number) => {
cancelAnimationFrame(rafRef.current);
const start = posRef.current;
const dist = target - start;
if (Math.abs(dist) < 0.001) return;
const dur = 600;
let t0: number | null = null;
const step = (now: number) => {
if (t0 === null) t0 = now;
const p = Math.min((now - t0) / dur, 1);
const e = 1 - Math.pow(1 - p, 3);
const v = start + dist * e;
posRef.current = v;
setPos(v);
if (p < 1) rafRef.current = requestAnimationFrame(step);
};
rafRef.current = requestAnimationFrame(step);
};
const tweenTo = (target: number) => {
cancelAnimationFrame(rafRef.current);
const start = posRef.current;
const dist = target - start;
if (Math.abs(dist) < 0.001) return;
const dur = 600;
let t0: number | null = null;
const step = (now: number) => {
if (t0 === null) t0 = now;
const p = Math.min((now - t0) / dur, 1);
const e = 1 - Math.pow(1 - p, 3);
const v = start + dist * e;
posRef.current = v;
setPos(v);
if (p < 1) rafRef.current = requestAnimationFrame(step);
};
rafRef.current = requestAnimationFrame(step);
};
// Slide to the active milestone whenever it changes
useEffect(() => {
tweenTo(active * TICKS_PER_STEP);
setShow(false);
const id = window.setTimeout(() => setShow(true), 200);
return () => window.clearTimeout(id);
}, [active]);
// Slide to the active milestone whenever it changes
useEffect(() => {
tweenTo(active * TICKS_PER_STEP);
setShow(false);
const id = window.setTimeout(() => setShow(true), 200);
return () => window.clearTimeout(id);
}, [active]);
// Auto-loop
useEffect(() => {
if (!inView || paused) return;
const t = window.setTimeout(
() => setActive((a) => (a + 1) % milestones.length),
STEP_MS,
);
return () => window.clearTimeout(t);
}, [active, paused, inView]);
// Auto-loop
useEffect(() => {
if (!inView || paused) return;
const t = window.setTimeout(() => setActive((a) => (a + 1) % milestones.length), STEP_MS);
return () => window.clearTimeout(t);
}, [active, paused, inView]);
const settle = (rawPos: number) => {
const nearest = Math.max(0, Math.min(milestones.length - 1, Math.round(rawPos / TICKS_PER_STEP)));
if (nearest === active) tweenTo(nearest * TICKS_PER_STEP);
setActive(nearest);
};
const settle = (rawPos: number) => {
const nearest = Math.max(0, Math.min(milestones.length - 1, Math.round(rawPos / TICKS_PER_STEP)));
if (nearest === active) tweenTo(nearest * TICKS_PER_STEP);
setActive(nearest);
};
// Drag to scrub
const onPointerDown = (e: React.PointerEvent) => {
drag.current = { active: true, startX: e.clientX, startPos: posRef.current, moved: false };
(e.target as HTMLElement).setPointerCapture?.(e.pointerId);
setPaused(true);
cancelAnimationFrame(rafRef.current);
};
const onPointerMove = (e: React.PointerEvent) => {
if (!drag.current.active) return;
const widthPx = svgWrapRef.current?.clientWidth ?? W;
const scale = W / widthPx;
const dxTicks = ((e.clientX - drag.current.startX) * scale) / TICK_GAP;
if (Math.abs(dxTicks) > 0.4) drag.current.moved = true;
const v = Math.max(-2, Math.min(MAX_K + 2, drag.current.startPos - dxTicks));
posRef.current = v;
setPos(v);
};
const onPointerUp = () => {
if (!drag.current.active) return;
drag.current.active = false;
settle(posRef.current);
setPaused(false);
};
// Drag to scrub
const onPointerDown = (e: React.PointerEvent) => {
drag.current = { active: true, startX: e.clientX, startPos: posRef.current, moved: false };
(e.target as HTMLElement).setPointerCapture?.(e.pointerId);
setPaused(true);
cancelAnimationFrame(rafRef.current);
};
const onPointerMove = (e: React.PointerEvent) => {
if (!drag.current.active) return;
const widthPx = svgWrapRef.current?.clientWidth ?? W;
const scale = W / widthPx;
const dxTicks = ((e.clientX - drag.current.startX) * scale) / TICK_GAP;
if (Math.abs(dxTicks) > 0.4) drag.current.moved = true;
const v = Math.max(-2, Math.min(MAX_K + 2, drag.current.startPos - dxTicks));
posRef.current = v;
setPos(v);
};
const onPointerUp = () => {
if (!drag.current.active) return;
drag.current.active = false;
settle(posRef.current);
setPaused(false);
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "ArrowRight") {
e.preventDefault();
setActive((a) => Math.min(milestones.length - 1, a + 1));
} else if (e.key === "ArrowLeft") {
e.preventDefault();
setActive((a) => Math.max(0, a - 1));
}
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "ArrowRight") {
e.preventDefault();
setActive((a) => Math.min(milestones.length - 1, a + 1));
} else if (e.key === "ArrowLeft") {
e.preventDefault();
setActive((a) => Math.max(0, a - 1));
}
};
// Build the visible ticks
const ticks: { k: number; x: number; yTop: number; len: number; major: boolean; opacity: number }[] = [];
const lo = Math.floor(pos) - 40;
const hi = Math.ceil(pos) + 40;
for (let k = lo; k <= hi; k++) {
if (k < 0 || k > MAX_K) continue;
const x = CENTER_X + (k - pos) * TICK_GAP;
if (x < 24 || x > W - 24) continue;
const dx = x - CENTER_X;
const major = k % TICKS_PER_STEP === 0;
const edgeFade = Math.max(0.08, 1 - Math.abs(dx) / (W / 2 + 40));
ticks.push({ k, x, yTop: arcTop(dx), len: major ? MAJOR_LEN : MINOR_LEN, major, opacity: edgeFade });
}
// Build the visible ticks
const ticks: { k: number; x: number; yTop: number; len: number; major: boolean; opacity: number }[] = [];
const lo = Math.floor(pos) - 40;
const hi = Math.ceil(pos) + 40;
for (let k = lo; k <= hi; k++) {
if (k < 0 || k > MAX_K) continue;
const x = CENTER_X + (k - pos) * TICK_GAP;
if (x < 24 || x > W - 24) continue;
const dx = x - CENTER_X;
const major = k % TICKS_PER_STEP === 0;
const edgeFade = Math.max(0.08, 1 - Math.abs(dx) / (W / 2 + 40));
ticks.push({ k, x, yTop: arcTop(dx), len: major ? MAJOR_LEN : MINOR_LEN, major, opacity: edgeFade });
}
const cur = milestones[active];
const cur = milestones[active];
return (
<section ref={wrapRef} className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
Lifecycle
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-4">
From issue to merged PR
</h2>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6] max-w-[34rem] mb-12">
Every session walks the same path spawned in isolation, working in parallel, landing on{" "}
<span className="font-mono text-[var(--landing-fg)]">main</span> on its own.
</p>
</div>
return (
<section ref={wrapRef} className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
Lifecycle
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-4">
From issue to merged PR
</h2>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6] max-w-[34rem] mb-12">
Every session walks the same path spawned in isolation, working in parallel, landing on{" "}
<span className="font-mono text-[var(--landing-fg)]">main</span> on its own.
</p>
</div>
<div
className="landing-card rounded-2xl border-[var(--landing-border-subtle)] px-6 sm:px-10 pt-10 pb-12 select-none outline-none"
tabIndex={0}
role="group"
aria-label="Session lifecycle timeline"
onKeyDown={onKeyDown}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Active icon + label */}
<div
className="flex flex-col items-center text-center mb-2"
style={{ opacity: show ? 1 : 0, transition: "opacity 0.35s ease" }}
>
<LifecycleIcon kind={cur.icon} />
<div className="font-sans font-[680] tracking-tight text-[1.5rem] tracking-[-0.5px] mt-3">
{cur.label}
</div>
<div className="font-mono text-[0.6875rem] tracking-[0.08em] text-[var(--landing-accent)] opacity-80 mt-1">
{cur.key}
</div>
</div>
<div
className="landing-card rounded-2xl border-[var(--landing-border-subtle)] px-6 sm:px-10 pt-10 pb-12 select-none outline-none"
tabIndex={0}
role="group"
aria-label="Session lifecycle timeline"
onKeyDown={onKeyDown}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Active icon + label */}
<div
className="flex flex-col items-center text-center mb-2"
style={{ opacity: show ? 1 : 0, transition: "opacity 0.35s ease" }}
>
<LifecycleIcon kind={cur.icon} />
<div className="font-sans font-[680] tracking-tight text-[1.5rem] tracking-[-0.5px] mt-3">{cur.label}</div>
<div className="font-mono text-[0.6875rem] tracking-[0.08em] text-[var(--landing-accent)] opacity-80 mt-1">
{cur.key}
</div>
</div>
{/* Ruler */}
<div
ref={svgWrapRef}
className="cursor-grab active:cursor-grabbing touch-none"
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-auto" role="img" aria-label="Lifecycle scrubber">
{/* center indicator */}
<line x1={CENTER_X} y1={4} x2={CENTER_X} y2={H - 8} stroke="var(--landing-accent)" strokeWidth={2} strokeLinecap="round" />
<circle cx={CENTER_X} cy={arcTop(0)} r={4} fill="var(--landing-accent)" />
{/* Ruler */}
<div
ref={svgWrapRef}
className="cursor-grab active:cursor-grabbing touch-none"
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-auto" role="img" aria-label="Lifecycle scrubber">
{/* center indicator */}
<line
x1={CENTER_X}
y1={4}
x2={CENTER_X}
y2={H - 8}
stroke="var(--landing-accent)"
strokeWidth={2}
strokeLinecap="round"
/>
<circle cx={CENTER_X} cy={arcTop(0)} r={4} fill="var(--landing-accent)" />
{/* ticks */}
{ticks.map((t) => (
<line
key={t.k}
x1={t.x}
y1={t.yTop}
x2={t.x}
y2={t.yTop + t.len}
stroke={t.major ? "var(--landing-border-strong)" : "var(--landing-border-default)"}
strokeWidth={t.major ? 1.6 : 1}
strokeLinecap="round"
style={{ opacity: t.opacity }}
/>
))}
{/* ticks */}
{ticks.map((t) => (
<line
key={t.k}
x1={t.x}
y1={t.yTop}
x2={t.x}
y2={t.yTop + t.len}
stroke={t.major ? "var(--landing-border-strong)" : "var(--landing-border-default)"}
strokeWidth={t.major ? 1.6 : 1}
strokeLinecap="round"
style={{ opacity: t.opacity }}
/>
))}
{/* milestone labels under their major tick (except the centered one) */}
{ticks
.filter((t) => t.major)
.map((t) => {
const idx = t.k / TICKS_PER_STEP;
const isActive = idx === active;
return (
<text
key={`lbl-${t.k}`}
x={t.x}
y={t.yTop + MAJOR_LEN + 16}
textAnchor="middle"
className="font-mono"
fontSize={9}
fill="var(--landing-muted)"
style={{ opacity: isActive ? 0 : t.opacity * 0.7, transition: "opacity 0.3s ease" }}
>
{milestones[idx].label}
</text>
);
})}
</svg>
</div>
{/* milestone labels under their major tick (except the centered one) */}
{ticks
.filter((t) => t.major)
.map((t) => {
const idx = t.k / TICKS_PER_STEP;
const isActive = idx === active;
return (
<text
key={`lbl-${t.k}`}
x={t.x}
y={t.yTop + MAJOR_LEN + 16}
textAnchor="middle"
className="font-mono"
fontSize={9}
fill="var(--landing-muted)"
style={{ opacity: isActive ? 0 : t.opacity * 0.7, transition: "opacity 0.3s ease" }}
>
{milestones[idx].label}
</text>
);
})}
</svg>
</div>
{/* Active description */}
<div
className="text-center max-w-[34rem] mx-auto mt-2"
style={{ opacity: show ? 1 : 0, transition: "opacity 0.35s ease" }}
>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6]">{cur.desc}</p>
</div>
</div>
</section>
);
{/* Active description */}
<div
className="text-center max-w-[34rem] mx-auto mt-2"
style={{ opacity: show ? 1 : 0, transition: "opacity 0.35s ease" }}
>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6]">{cur.desc}</p>
</div>
</div>
</section>
);
}
function LifecycleIcon({ kind }: { kind: Milestone["icon"] }) {
const common = {
width: 30,
height: 30,
viewBox: "0 0 24 24",
fill: "none",
stroke: "var(--landing-accent)",
strokeWidth: 1.6,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
};
switch (kind) {
case "spawn":
return (
<svg {...common}>
<path d="M12 20v-7" />
<path d="M12 13c0-3 2-5 5-5 0 3-2 5-5 5Z" />
<path d="M12 14c0-2.5-1.8-4.2-4.2-4.2C7.8 12.3 9.6 14 12 14Z" />
</svg>
);
case "work":
return (
<svg {...common}>
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M7 9l3 3-3 3" />
<path d="M13 15h4" />
</svg>
);
case "pr":
return (
<svg {...common}>
<circle cx="6" cy="18" r="2.5" />
<circle cx="6" cy="6" r="2.5" />
<circle cx="18" cy="8" r="2.5" />
<path d="M6 8.5v7" />
<path d="M18 10.5c0 4-6 2.5-6 6" />
<path d="M18 5.5V3.5M16.5 5h3" />
</svg>
);
case "review":
return (
<svg {...common}>
<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6Z" />
<circle cx="12" cy="12" r="2.5" />
</svg>
);
case "mergeable":
return (
<svg {...common}>
<circle cx="12" cy="12" r="9" />
<path d="M8 12l2.5 2.5L16 9" />
</svg>
);
case "merged":
return (
<svg {...common}>
<circle cx="6" cy="6" r="2.5" />
<circle cx="6" cy="18" r="2.5" />
<circle cx="18" cy="12" r="2.5" />
<path d="M6 8.5v7" />
<path d="M8.5 6.5c5 0 2 5.5 7 5.5" />
</svg>
);
}
const common = {
width: 30,
height: 30,
viewBox: "0 0 24 24",
fill: "none",
stroke: "var(--landing-accent)",
strokeWidth: 1.6,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
};
switch (kind) {
case "spawn":
return (
<svg {...common}>
<path d="M12 20v-7" />
<path d="M12 13c0-3 2-5 5-5 0 3-2 5-5 5Z" />
<path d="M12 14c0-2.5-1.8-4.2-4.2-4.2C7.8 12.3 9.6 14 12 14Z" />
</svg>
);
case "work":
return (
<svg {...common}>
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M7 9l3 3-3 3" />
<path d="M13 15h4" />
</svg>
);
case "pr":
return (
<svg {...common}>
<circle cx="6" cy="18" r="2.5" />
<circle cx="6" cy="6" r="2.5" />
<circle cx="18" cy="8" r="2.5" />
<path d="M6 8.5v7" />
<path d="M18 10.5c0 4-6 2.5-6 6" />
<path d="M18 5.5V3.5M16.5 5h3" />
</svg>
);
case "review":
return (
<svg {...common}>
<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6Z" />
<circle cx="12" cy="12" r="2.5" />
</svg>
);
case "mergeable":
return (
<svg {...common}>
<circle cx="12" cy="12" r="9" />
<path d="M8 12l2.5 2.5L16 9" />
</svg>
);
case "merged":
return (
<svg {...common}>
<circle cx="6" cy="6" r="2.5" />
<circle cx="6" cy="18" r="2.5" />
<circle cx="18" cy="12" r="2.5" />
<path d="M6 8.5v7" />
<path d="M8.5 6.5c5 0 2 5.5 7 5.5" />
</svg>
);
}
}

View File

@ -3,153 +3,148 @@
import { useEffect, useRef } from "react";
export function PageConstellation() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
type Dot = { x: number; y: number; vx: number; vy: number };
let dots: Dot[] = [];
let width = 0;
let height = 0;
const mouse = { x: -10000, y: -10000, active: false };
type Dot = { x: number; y: number; vx: number; vy: number };
let dots: Dot[] = [];
let width = 0;
let height = 0;
const mouse = { x: -10000, y: -10000, active: false };
const seed = () => {
const target = Math.min(140, Math.max(40, Math.floor((width * height) / 18000)));
dots = Array.from({ length: target }, () => ({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.1,
vy: (Math.random() - 0.5) * 0.1,
}));
};
const seed = () => {
const target = Math.min(140, Math.max(40, Math.floor((width * height) / 18000)));
dots = Array.from({ length: target }, () => ({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.1,
vy: (Math.random() - 0.5) * 0.1,
}));
};
const resize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
seed();
};
resize();
const resize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
seed();
};
resize();
const onMove = (e: MouseEvent) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
mouse.active = true;
};
const onLeave = () => {
mouse.active = false;
mouse.x = -10000;
mouse.y = -10000;
};
const onMove = (e: MouseEvent) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
mouse.active = true;
};
const onLeave = () => {
mouse.active = false;
mouse.x = -10000;
mouse.y = -10000;
};
window.addEventListener("resize", resize);
window.addEventListener("mousemove", onMove);
document.addEventListener("mouseleave", onLeave);
window.addEventListener("blur", onLeave);
window.addEventListener("resize", resize);
window.addEventListener("mousemove", onMove);
document.addEventListener("mouseleave", onLeave);
window.addEventListener("blur", onLeave);
const LINK_DIST = 105;
const MOUSE_DIST = 165;
const LINK_DIST = 105;
const MOUSE_DIST = 165;
const draw = () => {
ctx.clearRect(0, 0, width, height);
const draw = () => {
ctx.clearRect(0, 0, width, height);
for (const d of dots) {
d.x += d.vx;
d.y += d.vy;
if (d.x < 0 || d.x > width) d.vx *= -1;
if (d.y < 0 || d.y > height) d.vy *= -1;
}
for (const d of dots) {
d.x += d.vx;
d.y += d.vy;
if (d.x < 0 || d.x > width) d.vx *= -1;
if (d.y < 0 || d.y > height) d.vy *= -1;
}
ctx.lineWidth = 0.5;
for (let i = 0; i < dots.length; i++) {
for (let j = i + 1; j < dots.length; j++) {
const a = dots[i];
const b = dots[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
const distSq = dx * dx + dy * dy;
if (distSq < LINK_DIST * LINK_DIST) {
const dist = Math.sqrt(distSq);
const op = (1 - dist / LINK_DIST) * 0.05;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
}
ctx.lineWidth = 0.5;
for (let i = 0; i < dots.length; i++) {
for (let j = i + 1; j < dots.length; j++) {
const a = dots[i];
const b = dots[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
const distSq = dx * dx + dy * dy;
if (distSq < LINK_DIST * LINK_DIST) {
const dist = Math.sqrt(distSq);
const op = (1 - dist / LINK_DIST) * 0.05;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
}
if (mouse.active) {
ctx.lineWidth = 0.6;
for (const d of dots) {
const dx = d.x - mouse.x;
const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) {
const dist = Math.sqrt(distSq);
const op = (1 - dist / MOUSE_DIST) * 0.15;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.moveTo(d.x, d.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}
if (mouse.active) {
ctx.lineWidth = 0.6;
for (const d of dots) {
const dx = d.x - mouse.x;
const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) {
const dist = Math.sqrt(distSq);
const op = (1 - dist / MOUSE_DIST) * 0.15;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.moveTo(d.x, d.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}
for (const d of dots) {
let op = 0.11;
if (mouse.active) {
const dx = d.x - mouse.x;
const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) {
op += (1 - Math.sqrt(distSq) / MOUSE_DIST) * 0.22;
}
}
ctx.fillStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.arc(d.x, d.y, 1.0, 0, Math.PI * 2);
ctx.fill();
}
};
for (const d of dots) {
let op = 0.11;
if (mouse.active) {
const dx = d.x - mouse.x;
const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) {
op += (1 - Math.sqrt(distSq) / MOUSE_DIST) * 0.22;
}
}
ctx.fillStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.arc(d.x, d.y, 1.0, 0, Math.PI * 2);
ctx.fill();
}
};
let raf = 0;
const loop = () => {
draw();
raf = requestAnimationFrame(loop);
};
let raf = 0;
const loop = () => {
draw();
raf = requestAnimationFrame(loop);
};
if (reducedMotion) draw();
else loop();
if (reducedMotion) draw();
else loop();
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
window.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseleave", onLeave);
window.removeEventListener("blur", onLeave);
};
}, []);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
window.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseleave", onLeave);
window.removeEventListener("blur", onLeave);
};
}, []);
return (
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none"
style={{ zIndex: 0 }}
aria-hidden="true"
/>
);
return (
<canvas ref={canvasRef} className="fixed inset-0 pointer-events-none" style={{ zIndex: 0 }} aria-hidden="true" />
);
}

View File

@ -3,24 +3,24 @@
import { useEffect } from "react";
export function ScrollRevealProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
}
});
},
{ threshold: 0.1, rootMargin: "-50px" }
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
}
});
},
{ threshold: 0.1, rootMargin: "-50px" },
);
document.querySelectorAll(".landing-reveal").forEach((el) => {
observer.observe(el);
});
document.querySelectorAll(".landing-reveal").forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return () => observer.disconnect();
}, []);
return <>{children}</>;
return <>{children}</>;
}

View File

@ -2,64 +2,54 @@ import Link from "next/link";
import { DocsBody, DocsPage } from "fumadocs-ui/page";
export function DocsMissingPage() {
return (
<DocsPage
toc={[]}
tableOfContent={{
enabled: false,
}}
breadcrumb={{
enabled: true,
includePage: false,
}}
footer={{
enabled: false,
}}
>
<DocsBody>
<div className="not-prose docs-missing-wrap">
<div className="docs-missing-card">
<div className="docs-missing-label">
docs / checkout failed
</div>
<div className="docs-missing-content">
<section className="docs-missing-copy">
<h2>
This page checked out the wrong worktree.
</h2>
<p>
The docs were rebuilt, and this URL did not survive the merge. Start from the docs
index, or use search in the sidebar to find where it landed.
</p>
<div className="docs-missing-actions">
<Link
href="/docs"
className="docs-missing-primary"
>
Browse docs
</Link>
<Link
href="/"
className="docs-missing-secondary"
>
Home
</Link>
</div>
</section>
<div className="docs-missing-terminal">
<div className="docs-missing-dots">
<span className="docs-missing-dot-red" />
<span className="docs-missing-dot-yellow" />
<span className="docs-missing-dot-green" />
</div>
<p className="docs-missing-command">$ ao docs resolve</p>
<p className="docs-missing-status">status: missing</p>
<p>next: /docs</p>
</div>
</div>
</div>
</div>
</DocsBody>
</DocsPage>
);
return (
<DocsPage
toc={[]}
tableOfContent={{
enabled: false,
}}
breadcrumb={{
enabled: true,
includePage: false,
}}
footer={{
enabled: false,
}}
>
<DocsBody>
<div className="not-prose docs-missing-wrap">
<div className="docs-missing-card">
<div className="docs-missing-label">docs / checkout failed</div>
<div className="docs-missing-content">
<section className="docs-missing-copy">
<h2>This page checked out the wrong worktree.</h2>
<p>
The docs were rebuilt, and this URL did not survive the merge. Start from the docs index, or use
search in the sidebar to find where it landed.
</p>
<div className="docs-missing-actions">
<Link href="/docs" className="docs-missing-primary">
Browse docs
</Link>
<Link href="/" className="docs-missing-secondary">
Home
</Link>
</div>
</section>
<div className="docs-missing-terminal">
<div className="docs-missing-dots">
<span className="docs-missing-dot-red" />
<span className="docs-missing-dot-yellow" />
<span className="docs-missing-dot-green" />
</div>
<p className="docs-missing-command">$ ao docs resolve</p>
<p className="docs-missing-status">status: missing</p>
<p>next: /docs</p>
</div>
</div>
</div>
</div>
</DocsBody>
</DocsPage>
);
}

View File

@ -9,92 +9,92 @@ import type { CSSProperties } from "react";
/** Aliases: request name -> filename under /docs/logos/. */
const ALIASES: Record<string, string> = {
claude: "claude-code",
chatgpt: "openai",
claude: "claude-code",
chatgpt: "openai",
};
/** Extension for each logo file. Defaults to svg; override for raster assets. */
const LOGO_EXT: Record<string, string> = {
aider: "png",
aider: "png",
};
/** Brands with a real asset file under /public/docs/logos/. */
const FILE_LOGOS = new Set([
"aider",
"anthropic",
"apple",
"claude-code",
"codex",
"composio",
"cursor",
"discord",
"docker",
"github",
"gitlab",
"iterm2",
"linear",
"linux",
"microsoft",
"openai",
"openclaw",
"opencode",
"slack",
"tmux",
"web",
"webhook",
"windows",
"aider",
"anthropic",
"apple",
"claude-code",
"codex",
"composio",
"cursor",
"discord",
"docker",
"github",
"gitlab",
"iterm2",
"linear",
"linux",
"microsoft",
"openai",
"openclaw",
"opencode",
"slack",
"tmux",
"web",
"webhook",
"windows",
]);
export interface LogoProps {
/** Brand name (case-insensitive). */
name: string;
/** Size in pixels. Default 20. */
size?: number;
className?: string;
/** Accepted for backwards compat with existing MDX; no-op. */
color?: boolean;
/** Brand name (case-insensitive). */
name: string;
/** Size in pixels. Default 20. */
size?: number;
className?: string;
/** Accepted for backwards compat with existing MDX; no-op. */
color?: boolean;
}
export function Logo({ name, size = 20, className }: LogoProps) {
const key = name.toLowerCase();
const resolved = ALIASES[key] ?? key;
const baseStyle: CSSProperties = { flexShrink: 0, width: size, height: size };
const key = name.toLowerCase();
const resolved = ALIASES[key] ?? key;
const baseStyle: CSSProperties = { flexShrink: 0, width: size, height: size };
if (FILE_LOGOS.has(resolved)) {
const ext = LOGO_EXT[resolved] ?? "svg";
return (
<img
src={`/docs/logos/${resolved}.${ext}`}
alt=""
aria-hidden="true"
className={className}
style={baseStyle}
width={size}
height={size}
/>
);
}
if (FILE_LOGOS.has(resolved)) {
const ext = LOGO_EXT[resolved] ?? "svg";
return (
<img
src={`/docs/logos/${resolved}.${ext}`}
alt=""
aria-hidden="true"
className={className}
style={baseStyle}
width={size}
height={size}
/>
);
}
const initial = name.charAt(0).toUpperCase();
return (
<span
className={className}
style={{
...baseStyle,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: size * 0.22,
backgroundColor: "var(--color-bg-elevated, #2a2827)",
color: "#ffffff",
fontSize: size * 0.55,
fontWeight: 700,
lineHeight: 1,
fontFamily: "var(--font-geist-sans), ui-sans-serif, system-ui",
}}
aria-hidden="true"
>
{initial}
</span>
);
const initial = name.charAt(0).toUpperCase();
return (
<span
className={className}
style={{
...baseStyle,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: size * 0.22,
backgroundColor: "var(--color-bg-elevated, #2a2827)",
color: "#ffffff",
fontSize: size * 0.55,
fontWeight: 700,
lineHeight: 1,
fontFamily: "var(--font-geist-sans), ui-sans-serif, system-ui",
}}
aria-hidden="true"
>
{initial}
</span>
);
}

View File

@ -8,95 +8,88 @@ import { Logo } from "./Logo";
type Status = "full" | "partial" | "none";
export interface PlatformSupportProps {
macos?: Status;
linux?: Status;
windows?: Status;
note?: ReactNode;
macos?: Status;
linux?: Status;
windows?: Status;
note?: ReactNode;
}
const LABEL: Record<Status, string> = {
full: "Supported",
partial: "In progress",
none: "Not supported",
full: "Supported",
partial: "In progress",
none: "Not supported",
};
const DOT_COLOR: Record<Status, string> = {
full: "var(--color-accent-amber, #f97316)",
partial: "var(--color-accent-amber-dim, #a3581b)",
none: "var(--color-text-muted, #605e5c)",
full: "var(--color-accent-amber, #f97316)",
partial: "var(--color-accent-amber-dim, #a3581b)",
none: "var(--color-text-muted, #605e5c)",
};
function Cell({ platform, status }: { platform: "macos" | "linux" | "windows"; status: Status }) {
const logoName = platform === "macos" ? "apple" : platform;
const title = platform === "macos" ? "macOS" : platform === "linux" ? "Linux" : "Windows";
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.5rem 0.75rem",
borderRadius: "0.375rem",
border: "1px solid var(--color-fd-border)",
backgroundColor: "var(--color-fd-card)",
flex: "1 1 0",
minWidth: 0,
}}
>
<Logo name={logoName} size={18} />
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-fd-foreground)" }}>
{title}
</span>
<span
style={{
fontSize: "0.75rem",
color: "var(--color-fd-muted-foreground)",
display: "inline-flex",
alignItems: "center",
gap: "0.375rem",
}}
>
<span
style={{
width: "6px",
height: "6px",
borderRadius: "999px",
backgroundColor: DOT_COLOR[status],
display: "inline-block",
}}
/>
{LABEL[status]}
</span>
</div>
</div>
);
const logoName = platform === "macos" ? "apple" : platform;
const title = platform === "macos" ? "macOS" : platform === "linux" ? "Linux" : "Windows";
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.5rem 0.75rem",
borderRadius: "0.375rem",
border: "1px solid var(--color-fd-border)",
backgroundColor: "var(--color-fd-card)",
flex: "1 1 0",
minWidth: 0,
}}
>
<Logo name={logoName} size={18} />
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-fd-foreground)" }}>{title}</span>
<span
style={{
fontSize: "0.75rem",
color: "var(--color-fd-muted-foreground)",
display: "inline-flex",
alignItems: "center",
gap: "0.375rem",
}}
>
<span
style={{
width: "6px",
height: "6px",
borderRadius: "999px",
backgroundColor: DOT_COLOR[status],
display: "inline-block",
}}
/>
{LABEL[status]}
</span>
</div>
</div>
);
}
export function PlatformSupport({
macos = "full",
linux = "full",
windows = "full",
note,
}: PlatformSupportProps) {
return (
<div style={{ margin: "1.25rem 0" }}>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
<Cell platform="macos" status={macos} />
<Cell platform="linux" status={linux} />
<Cell platform="windows" status={windows} />
</div>
{note && (
<p
style={{
margin: "0.5rem 0 0 0",
fontSize: "0.8125rem",
color: "var(--color-fd-muted-foreground)",
}}
>
{note}
</p>
)}
</div>
);
export function PlatformSupport({ macos = "full", linux = "full", windows = "full", note }: PlatformSupportProps) {
return (
<div style={{ margin: "1.25rem 0" }}>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
<Cell platform="macos" status={macos} />
<Cell platform="linux" status={linux} />
<Cell platform="windows" status={windows} />
</div>
{note && (
<p
style={{
margin: "0.5rem 0 0 0",
fontSize: "0.8125rem",
color: "var(--color-fd-muted-foreground)",
}}
>
{note}
</p>
)}
</div>
);
}

View File

@ -7,85 +7,85 @@ import type { ReactNode } from "react";
import { Logo } from "./Logo";
export interface PluginCardProps {
name: string;
logo: string;
href: string;
description: string;
badge?: ReactNode;
name: string;
logo: string;
href: string;
description: string;
badge?: ReactNode;
}
export function PluginCard({ name, logo, href, description, badge }: PluginCardProps) {
return (
<Link
href={href}
style={{
display: "flex",
alignItems: "flex-start",
gap: "0.875rem",
padding: "1rem",
borderRadius: "0.5rem",
border: "1px solid var(--color-fd-border)",
backgroundColor: "var(--color-fd-card)",
textDecoration: "none",
transition: "border-color 150ms, transform 150ms",
}}
className="ao-plugin-card"
>
<span
style={{
width: "36px",
height: "36px",
borderRadius: "0.375rem",
backgroundColor: "var(--color-fd-muted)",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
color: "var(--color-fd-foreground)",
flexShrink: 0,
}}
>
<Logo name={logo} size={22} />
</span>
<span style={{ display: "flex", flexDirection: "column", gap: "0.25rem", minWidth: 0 }}>
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
fontSize: "0.9375rem",
fontWeight: 600,
color: "var(--color-fd-foreground)",
margin: 0,
}}
>
{name}
{badge}
</span>
<span
style={{
fontSize: "0.8125rem",
color: "var(--color-fd-muted-foreground)",
lineHeight: 1.45,
}}
>
{description}
</span>
</span>
</Link>
);
return (
<Link
href={href}
style={{
display: "flex",
alignItems: "flex-start",
gap: "0.875rem",
padding: "1rem",
borderRadius: "0.5rem",
border: "1px solid var(--color-fd-border)",
backgroundColor: "var(--color-fd-card)",
textDecoration: "none",
transition: "border-color 150ms, transform 150ms",
}}
className="ao-plugin-card"
>
<span
style={{
width: "36px",
height: "36px",
borderRadius: "0.375rem",
backgroundColor: "var(--color-fd-muted)",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
color: "var(--color-fd-foreground)",
flexShrink: 0,
}}
>
<Logo name={logo} size={22} />
</span>
<span style={{ display: "flex", flexDirection: "column", gap: "0.25rem", minWidth: 0 }}>
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
fontSize: "0.9375rem",
fontWeight: 600,
color: "var(--color-fd-foreground)",
margin: 0,
}}
>
{name}
{badge}
</span>
<span
style={{
fontSize: "0.8125rem",
color: "var(--color-fd-muted-foreground)",
lineHeight: 1.45,
}}
>
{description}
</span>
</span>
</Link>
);
}
export function PluginGrid({ children }: { children: ReactNode }) {
return (
<div
style={{
display: "grid",
gap: "0.75rem",
gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
margin: "1.25rem 0",
}}
>
{children}
</div>
);
return (
<div
style={{
display: "grid",
gap: "0.75rem",
gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
margin: "1.25rem 0",
}}
>
{children}
</div>
);
}

View File

@ -10,28 +10,28 @@ import { PlatformSupport } from "./PlatformSupport";
import { PluginCard, PluginGrid } from "./PluginCard";
function Accordions({ children }: { children: ReactNode }) {
return <div className="my-6 space-y-3">{children}</div>;
return <div className="my-6 space-y-3">{children}</div>;
}
function Accordion({ title, children }: { title: ReactNode; children: ReactNode }) {
return (
<details className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-4 py-3">
<summary className="cursor-pointer list-none text-sm font-medium text-[var(--color-text-primary)]">
{title}
</summary>
<div className="mt-3 text-sm text-[var(--color-text-secondary)]">{children}</div>
</details>
);
return (
<details className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-4 py-3">
<summary className="cursor-pointer list-none text-sm font-medium text-[var(--color-text-primary)]">
{title}
</summary>
<div className="mt-3 text-sm text-[var(--color-text-secondary)]">{children}</div>
</details>
);
}
export function getMDXComponents(): MDXComponents {
return {
...defaultMdxComponents,
Accordion,
Accordions,
Logo,
PlatformSupport,
PluginCard,
PluginGrid,
};
return {
...defaultMdxComponents,
Accordion,
Accordions,
Logo,
PlatformSupport,
PluginCard,
PluginGrid,
};
}

View File

@ -9,19 +9,21 @@ Agent Orchestrator (AO) is a Node.js orchestrator that spawns and manages parall
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` |
| 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` |
<Callout type="info">
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.
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.
</Callout>
---
@ -56,25 +58,25 @@ pr_open ────────────────────────
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) |
| 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
@ -99,37 +101,37 @@ Priority is inferred by `inferPriority()` in `lifecycle-manager.ts`:
- **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 |
| `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).
@ -204,14 +206,14 @@ Every agent plugin must implement `getActivityState(session, readyThresholdMs?)`
### 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 |
| 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
@ -237,23 +239,25 @@ Every agent plugin must implement this cascade in order:
```
<Callout type="warn">
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.
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.
</Callout>
### 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. |
| 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. |
| 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. |
---
@ -270,6 +274,7 @@ Claude Code writes `.claude/settings.json` with a `PostToolUse` hook that fires
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=<url>` and `status=pr_open`
- `gh pr merge` — writes `status=merged`
- `git checkout -b <branch>` / `git switch -c <branch>` — writes `branch=<name>`
@ -333,8 +338,24 @@ agent-orchestrator.yaml ──► Config Loader (Zod) ──► Plugin Registry
## Next Steps
<Cards>
<Card title="Project Configuration" href="/docs/configuration/projects" description="Configure projects, agents, trackers, and reactions in agent-orchestrator.yaml." />
<Card title="Authoring Plugins" href="/docs/plugins/authoring" description="Build custom runtime, agent, tracker, SCM, notifier, or terminal plugins." />
<Card title="Reactions" href="/docs/configuration/reactions" description="Set up automated reactions to CI failures, review comments, and merge events." />
<Card title="Configuration" href="/docs/configuration" description="Understand the global registry, local project config, and where AO stores runtime data." />
<Card
title="Project Configuration"
href="/docs/configuration/projects"
description="Configure projects, agents, trackers, and reactions in agent-orchestrator.yaml."
/>
<Card
title="Authoring Plugins"
href="/docs/plugins/authoring"
description="Build custom runtime, agent, tracker, SCM, notifier, or terminal plugins."
/>
<Card
title="Reactions"
href="/docs/configuration/reactions"
description="Set up automated reactions to CI failures, review comments, and merge events."
/>
<Card
title="Configuration"
href="/docs/configuration"
description="Understand the global registry, local project config, and where AO stores runtime data."
/>
</Cards>

View File

@ -37,13 +37,13 @@ ao
> 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 |
| 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`.
@ -53,21 +53,21 @@ If AO is already running, you'll get an interactive prompt to open/restart/spawn
> 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 |
| 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 <id>` | — | Filter to one project |
| `--json` | — | Output JSON |
| `-w, --watch` | — | Refresh continuously |
| `--interval <seconds>` | `5` | Refresh interval |
| Flag | Default | Purpose |
| ---------------------- | ------- | --------------------- |
| `-p, --project <id>` | — | Filter to one project |
| `--json` | — | Output JSON |
| `-w, --watch` | — | Refresh continuously |
| `--interval <seconds>` | `5` | Refresh interval |
`--watch` and `--json` are mutually exclusive. Falls back to tmux session discovery if no config is found.
@ -75,27 +75,28 @@ If AO is already running, you'll get an interactive prompt to open/restart/spawn
> Spawn a single agent session
| Flag | Default | Purpose |
|---|---|---|
| `--open` | — | Open the session in a terminal tab |
| `--agent <name>` | config default | Override the agent plugin (`claude-code`, `codex`, `cursor`, `aider`, `opencode`) |
| `--claim-pr <pr>` | — | Immediately claim an existing PR for the spawned session |
| `--assign-on-github` | — | Assign the claimed PR to the authenticated GitHub user |
| `--prompt <text>` | — | Use an inline prompt instead of an issue (max 4096 chars, newlines stripped) |
| Flag | Default | Purpose |
| -------------------- | -------------- | --------------------------------------------------------------------------------- |
| `--open` | — | Open the session in a terminal tab |
| `--agent <name>` | config default | Override the agent plugin (`claude-code`, `codex`, `cursor`, `aider`, `opencode`) |
| `--claim-pr <pr>` | — | Immediately claim an existing PR for the spawned session |
| `--assign-on-github` | — | Assign the claimed PR to the authenticated GitHub user |
| `--prompt <text>` | — | Use an inline prompt instead of an issue (max 4096 chars, newlines stripped) |
Positional `[first]` — issue identifier. The project is auto-detected from `cwd`.
<Callout type="warn">
The old two-arg form `ao spawn &lt;project&gt; &lt;issue&gt;` is rejected with an error. Use `-p` or run from inside a worktree.
The old two-arg form `ao spawn &lt;project&gt; &lt;issue&gt;` is rejected with an error. Use `-p` or run from inside a
worktree.
</Callout>
### `ao batch-spawn <issues...>`
> Spawn sessions for multiple issues with duplicate detection
| Flag | Default | Purpose |
|---|---|---|
| `--open` | — | Open sessions in terminal tabs |
| Flag | Default | Purpose |
| -------- | ------- | ------------------------------ |
| `--open` | — | Open sessions in terminal tabs |
Skips duplicates within the batch and any issues that already have an active session.
@ -103,11 +104,11 @@ Skips duplicates within the batch and any issues that already have an active ses
> Send a message to a session with busy detection and retry
| Flag | Default | Purpose |
|---|---|---|
| `-f, --file <path>` | — | Send contents of a file instead of an inline message |
| `--no-wait` | — | Don't wait for the session to become idle before sending |
| `--timeout <seconds>` | `600` | Max seconds to wait for idle |
| Flag | Default | Purpose |
| --------------------- | ------- | -------------------------------------------------------- |
| `-f, --file <path>` | — | Send contents of a file instead of an inline message |
| `--no-wait` | — | Don't wait for the session to become idle before sending |
| `--timeout <seconds>` | `600` | Max seconds to wait for idle |
Positional: `<session>` required, `[message...]` variadic. One of inline message or `--file` is required. Long messages (>200 chars or multiline) go via tmux paste-buffer + temp file.
@ -115,9 +116,9 @@ Positional: `<session>` required, `[message...]` variadic. One of inline message
> Check PRs for review comments and trigger agents to address them
| Flag | Default | Purpose |
|---|---|---|
| `--dry-run` | — | Show what would happen; don't nudge agents |
| Flag | Default | Purpose |
| ----------- | ------- | ------------------------------------------ |
| `--dry-run` | — | Show what would happen; don't nudge agents |
Checks all projects if `[project]` is omitted.
@ -125,11 +126,11 @@ Checks all projects if `[project]` is omitted.
> Start the web dashboard
| Flag | Default | Purpose |
|---|---|---|
| `-p, --port <port>` | config `port` or `3000` | Port to listen on |
| `--no-open` | — | Don't open the browser |
| `--rebuild` | — | Clean stale Next.js artifacts and rebuild |
| Flag | Default | Purpose |
| ------------------- | ----------------------- | ----------------------------------------- |
| `-p, --port <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`.
@ -137,9 +138,9 @@ If the build looks stale you'll see a suggestion to run `ao dashboard --rebuild`
> Open session(s) in terminal tabs
| Flag | Default | Purpose |
|---|---|---|
| `-w, --new-window` | — | Open in a new terminal window instead of a tab |
| 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).
@ -147,12 +148,12 @@ Positional `[target]` — session name, project id, or `"all"`. Defaults to all
> Mark an issue as verified (or failed) after checking the fix on staging
| Flag | Default | Purpose |
|---|---|---|
| `-p, --project <id>` | — | Required if multiple projects |
| `--fail` | — | Mark as failed instead of passing |
| `-c, --comment <msg>` | — | Custom comment to add |
| `-l, --list` | — | List all issues with the merged-unverified label |
| Flag | Default | Purpose |
| --------------------- | ------- | ------------------------------------------------ |
| `-p, --project <id>` | — | Required if multiple projects |
| `--fail` | — | Mark as failed instead of passing |
| `-c, --comment <msg>` | — | Custom comment to add |
| `-l, --list` | — | List all issues with the merged-unverified label |
`[issue]` is required unless `--list` is passed.
@ -160,10 +161,10 @@ Positional `[target]` — session name, project id, or `"all"`. Defaults to all
> 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 |
| 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.
@ -171,11 +172,11 @@ Runs `ao-doctor.sh` for shell-level checks, then TypeScript checks (version fres
> 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 |
| 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).
@ -191,17 +192,17 @@ No flags.
> Send a manual demo notification without spawning sessions
| Flag | Default | Purpose |
|---|---|---|
| `--template <name>` | `basic` | Demo template to send |
| `--to <refs>` | — | Comma-separated notifier refs to target |
| `--all` | — | Send to all configured, default, and routed notifier refs |
| `--route <priority>` | — | Send through `urgent`, `action`, `warning`, or `info` routing |
| `--actions` | — | Include demo actions when supported |
| `--message <text>` | — | Override the notification message |
| `--dry-run` | — | Resolve targets without sending |
| `--json` | — | Print structured JSON output |
| `--sink [port]` | — | Add a local webhook target named `sink` |
| Flag | Default | Purpose |
| -------------------- | ------- | ------------------------------------------------------------- |
| `--template <name>` | `basic` | Demo template to send |
| `--to <refs>` | — | Comma-separated notifier refs to target |
| `--all` | — | Send to all configured, default, and routed notifier refs |
| `--route <priority>` | — | Send through `urgent`, `action`, `warning`, or `info` routing |
| `--actions` | — | Include demo actions when supported |
| `--message <text>` | — | 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
@ -209,11 +210,11 @@ No flags.
> List all sessions
| Flag | Default | Purpose |
|---|---|---|
| `-p, --project <id>` | — | Filter to one project |
| `-a, --all` | — | Include orchestrator sessions |
| `--json` | — | Output JSON |
| Flag | Default | Purpose |
| -------------------- | ------- | ----------------------------- |
| `-p, --project <id>` | — | Filter to one project |
| `-a, --all` | — | Include orchestrator sessions |
| `--json` | — | Output JSON |
### `ao session attach <session>`
@ -225,26 +226,26 @@ No flags. Attaches via `tmux attach-session`. Only meaningful with `runtime: tmu
> Kill a session and remove its worktree
| Flag | Default | Purpose |
|---|---|---|
| `--purge-session` | — | Delete the mapped OpenCode session |
| 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 <id>` | — | Filter to one project |
| `--dry-run` | — | Show what would be cleaned up |
| Flag | Default | Purpose |
| -------------------- | ------- | ----------------------------- |
| `-p, --project <id>` | — | Filter to one project |
| `--dry-run` | — | Show what would be cleaned up |
### `ao session claim-pr <pr> [session]`
> Attach an existing PR to a session
| Flag | Default | Purpose |
|---|---|---|
| `--assign-on-github` | — | Assign the PR to the authenticated user on GitHub |
| 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.
@ -258,9 +259,9 @@ No flags. Relaunches the agent inside the same worktree and rewires session meta
> Re-discover and persist OpenCode session mapping for an AO session
| Flag | Default | Purpose |
|---|---|---|
| `-f, --force` | — | Override a stale mapping |
| Flag | Default | Purpose |
| ------------- | ------- | ------------------------ |
| `-f, --force` | — | Override a stale mapping |
## Setup subcommands
@ -269,35 +270,35 @@ 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 |
| 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 <url>` | — | OpenClaw webhook URL |
| `--token <token>` | — | Remote/manual fallback; local setup should read `hooks.token` from OpenClaw config |
| `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
| `--routing-preset <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 |
| Flag | Default | Purpose |
| ------------------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
| `--url <url>` | — | OpenClaw webhook URL |
| `--token <token>` | — | Remote/manual fallback; local setup should read `hooks.token` from OpenClaw config |
| `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
| `--routing-preset <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.
@ -307,11 +308,11 @@ Interactive mode (TTY) uses `@clack/prompts` with review/change steps for the ga
> List bundled marketplace plugins
| Flag | Default | Purpose |
|---|---|---|
| `--installed` | — | Only show plugins present in your current config |
| `--type <slot>` | — | Filter by slot: `agent`, `runtime`, `tracker`, `scm`, `notifier`, `workspace`, `terminal` |
| `--refresh` | — | Re-fetch the marketplace catalog |
| Flag | Default | Purpose |
| --------------- | ------- | ----------------------------------------------------------------------------------------- |
| `--installed` | — | Only show plugins present in your current config |
| `--type <slot>` | — | Filter by slot: `agent`, `runtime`, `tracker`, `scm`, `notifier`, `workspace`, `terminal` |
| `--refresh` | — | Re-fetch the marketplace catalog |
### `ao plugin search <query>`
@ -323,24 +324,24 @@ No flags.
> Scaffold a new AO plugin package
| Flag | Default | Purpose |
|---|---|---|
| `--name <name>` | prompt | Plugin name |
| `--slot <slot>` | prompt | Which slot it implements |
| `--description <text>` | prompt | Manifest description |
| `--author <name>` | prompt | Package author |
| `--package-name <name>` | derived | npm package name |
| `--non-interactive` | — | Use all flags without prompting |
| Flag | Default | Purpose |
| ----------------------- | ------- | ------------------------------- |
| `--name <name>` | prompt | Plugin name |
| `--slot <slot>` | prompt | Which slot it implements |
| `--description <text>` | prompt | Manifest description |
| `--author <name>` | prompt | Package author |
| `--package-name <name>` | derived | npm package name |
| `--non-interactive` | — | Use all flags without prompting |
### `ao plugin install <reference>`
> Install a plugin into the current config
| Flag | Default | Purpose |
|---|---|---|
| `--url <url>` | — | Passed to `ao setup openclaw` when installing `notifier-openclaw` |
| `--token <token>` | — | Remote/manual OpenClaw token fallback |
| `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
| Flag | Default | Purpose |
| ------------------------------- | --------------------------- | ----------------------------------------------------------------- |
| `--url <url>` | — | Passed to `ao setup openclaw` when installing `notifier-openclaw` |
| `--token <token>` | — | Remote/manual OpenClaw token fallback |
| `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
`<reference>` is a marketplace id, npm package name, or local path.
@ -348,9 +349,9 @@ No flags.
> Update installer-managed plugins in the current config
| Flag | Default | Purpose |
|---|---|---|
| `--all` | — | Update all managed plugins |
| Flag | Default | Purpose |
| ------- | ------- | -------------------------- |
| `--all` | — | Update all managed plugins |
Requires either `[reference]` or `--all`.
@ -364,16 +365,16 @@ No flags.
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). |
| 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).

View File

@ -11,7 +11,9 @@ AO uses configuration in two layers:
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.
<Callout type="info">
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.
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.
</Callout>
## What AO Creates
@ -72,15 +74,15 @@ agentRules: |
## 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 |
| 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.
@ -89,9 +91,9 @@ Do not manually edit `storageKey`, `path`, or `originUrl` unless you are repairi
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
agent: claude-code # claude-code | codex | aider | opencode
runtime: tmux # tmux | process
workspace: worktree # worktree | clone
agentConfig:
permissions: permissionless
@ -146,18 +148,18 @@ For normal use, run AO from inside the repository and keep `agent-orchestrator.y
## 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 |
| 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
@ -172,7 +174,19 @@ ls ~/.agent-orchestrator
## Next Steps
<Cards>
<Card title="Projects" href="/docs/configuration/projects" description="Per-project behavior settings: agents, workspaces, rules, setup, trackers, and SCM." />
<Card title="Reactions" href="/docs/configuration/reactions" description="Control what AO does when CI fails, reviews arrive, or agents need help." />
<Card title="Remote access" href="/docs/configuration/remote-access" description="Access the dashboard from another machine without exposing it publicly." />
<Card
title="Projects"
href="/docs/configuration/projects"
description="Per-project behavior settings: agents, workspaces, rules, setup, trackers, and SCM."
/>
<Card
title="Reactions"
href="/docs/configuration/reactions"
description="Control what AO does when CI fails, reviews arrive, or agents need help."
/>
<Card
title="Remote access"
href="/docs/configuration/remote-access"
description="Access the dashboard from another machine without exposing it publicly."
/>
</Cards>

View File

@ -1,5 +1,5 @@
{
"title": "Configuration",
"defaultOpen": false,
"pages": ["projects", "reactions", "remote-access"]
"title": "Configuration",
"defaultOpen": false,
"pages": ["projects", "reactions", "remote-access"]
}

View File

@ -34,12 +34,12 @@ 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 | 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`:
@ -51,12 +51,12 @@ agentConfig:
`permissions` accepts:
| Value | Behavior |
|-------|----------|
| 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 |
| `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`.
@ -68,10 +68,10 @@ The legacy value `skip` is accepted and treated as `permissionless`.
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 |
| 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`.
@ -83,10 +83,10 @@ Most users should keep `tmux`.
workspace: worktree
```
| Workspace | Use when |
|-----------|----------|
| Workspace | Use when |
| ---------- | ----------------------------------------------------------------- |
| `worktree` | Default. Fast, disk-efficient, creates a git worktree per session |
| `clone` | Slower, but gives each session a separate clone |
| `clone` | Slower, but gives each session a separate clone |
Use `worktree` unless your repository has tooling that behaves badly with git worktrees.
@ -172,18 +172,18 @@ scm:
Built-in trackers:
| Plugin | Purpose |
|--------|---------|
| Plugin | Purpose |
| -------- | ------------- |
| `github` | GitHub issues |
| `gitlab` | GitLab issues |
| `linear` | Linear issues |
Built-in SCM plugins:
| Plugin | Purpose |
|--------|---------|
| Plugin | Purpose |
| -------- | ---------------------------------------- |
| `github` | GitHub PRs, checks, reviews, merge state |
| `gitlab` | GitLab merge requests |
| `gitlab` | GitLab merge requests |
Extra keys under `tracker` and `scm` are passed to the plugin:
@ -233,13 +233,13 @@ 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 |
| 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`.
@ -248,27 +248,27 @@ opencodeIssueSessionStrategy: reuse
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 |
| 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.
@ -292,7 +292,19 @@ Make sure the corresponding CLI or token is authenticated for the plugin you use
## Next Steps
<Cards>
<Card title="Reactions" href="/docs/configuration/reactions" description="Configure what AO does when CI, reviews, or agent state changes need attention." />
<Card title="Remote access" href="/docs/configuration/remote-access" description="Open the dashboard from another machine without exposing it publicly." />
<Card title="Plugin docs" href="/docs/plugins" description="Agent, tracker, SCM, notifier, runtime, terminal, and workspace plugins." />
<Card
title="Reactions"
href="/docs/configuration/reactions"
description="Configure what AO does when CI, reviews, or agent state changes need attention."
/>
<Card
title="Remote access"
href="/docs/configuration/remote-access"
description="Open the dashboard from another machine without exposing it publicly."
/>
<Card
title="Plugin docs"
href="/docs/plugins"
description="Agent, tracker, SCM, notifier, runtime, terminal, and workspace plugins."
/>
</Cards>

View File

@ -58,31 +58,32 @@ reactions:
```
<Callout type="warning">
`auto-merge` is currently an intent flag, not a bypass. AO does not ignore branch protection, reviews, or failing checks.
`auto-merge` is currently an intent flag, not a bypass. AO does not ignore branch protection, reviews, or failing
checks.
</Callout>
## 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 |
| 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 |
| 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.
@ -90,16 +91,16 @@ reactions:
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 |
| 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:
@ -172,22 +173,30 @@ ao review-check <session-id>
## 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` |
| 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
<Cards>
<Card title="Projects" href="/docs/configuration/projects" description="Apply reaction overrides to one project." />
<Card title="Webhook notifier" href="/docs/plugins/notifiers/webhook" description="Send AO events to any HTTPS endpoint." />
<Card title="Configuration" href="/docs/configuration" description="Understand global registry vs local project config." />
<Card title="Projects" href="/docs/configuration/projects" description="Apply reaction overrides to one project." />
<Card
title="Webhook notifier"
href="/docs/plugins/notifiers/webhook"
description="Send AO events to any HTTPS endpoint."
/>
<Card
title="Configuration"
href="/docs/configuration"
description="Understand global registry vs local project config."
/>
</Cards>

View File

@ -152,11 +152,11 @@ If you want to expose AO over a domain name with TLS, or place it behind an auth
### 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) |
| 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
@ -269,11 +269,11 @@ For more on project identity, local config, and runtime data, see [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 |
| 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:

View File

@ -21,13 +21,13 @@ http://localhost:3000
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 |
| 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.
@ -58,12 +58,12 @@ 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 |
| 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.
@ -83,10 +83,10 @@ Use the terminal to inspect what the agent is doing, recover from stuck prompts,
AO has two terminal backends:
| Backend | Used when | Default port |
|---------|-----------|--------------|
| tmux WebSocket mux | `runtime: tmux` | `14800` |
| direct PTY WebSocket | `runtime: process` | `14801` |
| 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.
@ -119,15 +119,15 @@ The favicon and document title also change when sessions need attention, so you
## 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 |
| 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.
@ -135,11 +135,11 @@ Internal API routes such as `/api/events`, `/api/sessions`, `/api/projects`, and
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` |
| 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).
@ -163,7 +163,15 @@ Open the session detail page and send a short, direct instruction. The agent rec
## Next Steps
<Cards>
<Card title="Quickstart" href="/docs/quickstart" description="Start AO, spawn a session, and handle the first PR." />
<Card title="Configuration" href="/docs/configuration" description="Understand global registry and local project settings." />
<Card title="Remote access" href="/docs/configuration/remote-access" description="Open the dashboard from another device safely." />
<Card title="Quickstart" href="/docs/quickstart" description="Start AO, spawn a session, and handle the first PR." />
<Card
title="Configuration"
href="/docs/configuration"
description="Understand global registry and local project settings."
/>
<Card
title="Remote access"
href="/docs/configuration/remote-access"
description="Open the dashboard from another device safely."
/>
</Cards>

View File

@ -258,7 +258,19 @@ projects:
## Next Steps
<Cards>
<Card title="Configuration Reference" href="/docs/configuration" description="Full reference for every field in agent-orchestrator.yaml." />
<Card title="Plugins" href="/docs/plugins" description="Browse available runtime, agent, tracker, SCM, notifier, and terminal plugins." />
<Card title="Guides" href="/docs/guides" description="Step-by-step workflows for common setups and automation patterns." />
<Card
title="Configuration Reference"
href="/docs/configuration"
description="Full reference for every field in agent-orchestrator.yaml."
/>
<Card
title="Plugins"
href="/docs/plugins"
description="Browse available runtime, agent, tracker, SCM, notifier, and terminal plugins."
/>
<Card
title="Guides"
href="/docs/guides"
description="Step-by-step workflows for common setups and automation patterns."
/>
</Cards>

View File

@ -10,53 +10,63 @@ 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.
</Accordion>
<Accordion title="Do I have to use Claude Code?">
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).
</Accordion>
<Accordion title="Do I have to use Claude Code?">
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).
</Accordion>
<Accordion title="Does AO auto-merge?">
No, intentionally. AO will get a PR to a mergeable state — CI green, approvals satisfied — and stop. The merge is your call.
</Accordion>
<Accordion title="Does AO auto-merge?">
No, intentionally. AO will get a PR to a mergeable state — CI green, approvals satisfied — and stop. The merge is your
call.
</Accordion>
<Accordion title="Does AO need webhooks?">
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.
</Accordion>
<Accordion title="Does AO need webhooks?">
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.
</Accordion>
<Accordion title="Can it use GitHub Enterprise / self-hosted GitLab?">
Yes. `gh` and `glab` both support enterprise hosts — configure them via their own tools. For GitLab, also set `trackerConfig.host` / `scmConfig.host` in AO.
</Accordion>
<Accordion title="Can it use GitHub Enterprise / self-hosted GitLab?">
Yes. `gh` and `glab` both support enterprise hosts — configure them via their own tools. For GitLab, also set
`trackerConfig.host` / `scmConfig.host` in AO.
</Accordion>
<Accordion title="Does it work on Windows?">
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.
</Accordion>
<Accordion title="Does it work on Windows?">
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.
</Accordion>
<Accordion title="Where does session state live?">
`~/.agent-orchestrator/{hash}-{projectId}/`. No database. Everything is flat files you can inspect.
</Accordion>
<Accordion title="Where does session state live?">
`~/.agent-orchestrator/{hash}-{projectId}/`. No database. Everything is flat files you can inspect.
</Accordion>
<Accordion title="Can I run AO on a remote server?">
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.
</Accordion>
<Accordion title="Can I run AO on a remote server?">
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.
</Accordion>
<Accordion title="How much does it cost?">
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).
</Accordion>
<Accordion title="How much does it cost?">
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).
</Accordion>
<Accordion title="What happens to my code?">
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.
</Accordion>
<Accordion title="What happens to my code?">
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.
</Accordion>
<Accordion title="Can I write my own plugin?">
Yes. `ao plugin create --slot <slot> --name <name>` scaffolds a starter. Plugins are tiny Node packages exporting a manifest + `create()` function.
</Accordion>
<Accordion title="Why is everything a plugin?">
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.
</Accordion>
<Accordion title="Why is everything a plugin?">
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.
</Accordion>
<Accordion title="Does AO modify my repo's files?">
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.
</Accordion>
<Accordion title="Does AO modify my repo's files?">
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.
</Accordion>
<Accordion title="How do I completely uninstall?">
`npm uninstall -g @aoagents/ao`, then `rm -rf ~/.agent-orchestrator` if you want to wipe all session history.

View File

@ -11,7 +11,7 @@ When your PR's CI goes red, AO notices before you do and tells the agent to fix
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."*
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.
@ -32,9 +32,9 @@ The agent's next response usually inspects the log, reproduces the failure local
```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
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.
@ -74,7 +74,7 @@ This creates a session, points it at PR #123, and (with `--assign-on-github`) as
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`.
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.
@ -89,7 +89,7 @@ AO fingerprints the set of failing checks (name + status + conclusion). If the s
```yaml title="agent-orchestrator.yaml"
reactions:
ci-failed:
retries: 3 # escalate after 3 failed attempts (default: unlimited)
retries: 3 # escalate after 3 failed attempts (default: unlimited)
escalateAfter: "1h" # …or after a wall-clock duration, whichever comes first
```

View File

@ -6,10 +6,34 @@ description: Real scenarios — parallel issues, CI recovery, review loops, mult
These guides are task-shaped: each one covers a single thing you'd actually want AO to do for you, end to end.
<Cards>
<Card title="Per-role agents" description="Run a reasoning-heavy orchestrator and a fast worker — globally or per-project." href="/docs/guides/per-role-agents" />
<Card title="Parallel issues" description="Spawn agents on several issues at once without them stepping on each other." href="/docs/guides/parallel-issues" />
<Card title="CI recovery" description="How AO notices red CI and nudges the agent to fix it without you babysitting." href="/docs/guides/ci-recovery" />
<Card title="Review loop" description="When a reviewer asks for changes, AO replays the feedback to the agent." href="/docs/guides/review-loop" />
<Card title="Reaction recipes" description="Watch-only mode, auto-merge opt-in, custom CI messages, bot handling." href="/docs/guides/reactions" />
<Card title="Multi-project" description="Run AO against several repos from one dashboard." href="/docs/guides/multi-project" />
<Card
title="Per-role agents"
description="Run a reasoning-heavy orchestrator and a fast worker — globally or per-project."
href="/docs/guides/per-role-agents"
/>
<Card
title="Parallel issues"
description="Spawn agents on several issues at once without them stepping on each other."
href="/docs/guides/parallel-issues"
/>
<Card
title="CI recovery"
description="How AO notices red CI and nudges the agent to fix it without you babysitting."
href="/docs/guides/ci-recovery"
/>
<Card
title="Review loop"
description="When a reviewer asks for changes, AO replays the feedback to the agent."
href="/docs/guides/review-loop"
/>
<Card
title="Reaction recipes"
description="Watch-only mode, auto-merge opt-in, custom CI messages, bot handling."
href="/docs/guides/reactions"
/>
<Card
title="Multi-project"
description="Run AO against several repos from one dashboard."
href="/docs/guides/multi-project"
/>
</Cards>

View File

@ -1,12 +1,5 @@
{
"title": "Guides",
"defaultOpen": false,
"pages": [
"per-role-agents",
"parallel-issues",
"ci-recovery",
"review-loop",
"reactions",
"multi-project"
]
"title": "Guides",
"defaultOpen": false,
"pages": ["per-role-agents", "parallel-issues", "ci-recovery", "review-loop", "reactions", "multi-project"]
}

View File

@ -10,7 +10,7 @@ One `agent-orchestrator.yaml` can drive many projects. Each project gets its own
## Config shape
```yaml title="agent-orchestrator.yaml"
runtime: tmux # global default
runtime: tmux # global default
agent: claude-code
projects:
@ -20,7 +20,7 @@ projects:
api:
repo: myorg/api
agent: codex # per-project override
agent: codex # per-project override
tracker: linear
trackerConfig:
teamId: TEAM-123
@ -28,7 +28,7 @@ projects:
marketing:
repo: myorg/site
agent: cursor
workspace: clone # full clone instead of worktree
workspace: clone # full clone instead of worktree
```
Project IDs (`web`, `api`, `marketing`) are what you pass to `--project` / `-p`.
@ -58,7 +58,7 @@ Or pass no project and AO will prompt.
Anything defined at the top level is a default. Override per project:
```yaml
tracker: github # default
tracker: github # default
notifier:
- type: slack
webhookUrl: ${SLACK_GLOBAL}
@ -93,5 +93,6 @@ ao stop --all # stop everything
```
<Callout type="info">
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.
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.
</Callout>

View File

@ -37,12 +37,12 @@ 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'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
@ -79,6 +79,4 @@ ao status -p my-repo --json
- `ao session cleanup` — kill everything whose PR merged or issue closed (safe; archives metadata)
- `ao session cleanup --dry-run` — preview first
<Callout type="info">
Need them all gone? `ao stop --all` halts every running AO instance on this machine.
</Callout>
<Callout type="info">Need them all gone? `ao stop --all` halts every running AO instance on this machine.</Callout>

View File

@ -20,7 +20,8 @@ defaults:
```
<Callout type="info">
`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.
`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.
</Callout>
## Per-project override
@ -138,13 +139,13 @@ AO records a `role` field in each session's metadata file with the value `orches
## Next steps
<Cards>
<Card title="Projects configuration" href="/docs/configuration/projects#orchestrator-and-worker">
Full per-project config syntax including orchestrator/worker fields.
</Card>
<Card title="Architecture: orchestrator prompt" href="/docs/architecture#orchestrator-prompt">
How the orchestrator prompt is assembled and injected.
</Card>
<Card title="Agent plugins" href="/docs/plugins/agents">
Available agent plugins and their configuration options.
</Card>
<Card title="Projects configuration" href="/docs/configuration/projects#orchestrator-and-worker">
Full per-project config syntax including orchestrator/worker fields.
</Card>
<Card title="Architecture: orchestrator prompt" href="/docs/architecture#orchestrator-prompt">
How the orchestrator prompt is assembled and injected.
</Card>
<Card title="Agent plugins" href="/docs/plugins/agents">
Available agent plugins and their configuration options.
</Card>
</Cards>

View File

@ -35,7 +35,9 @@ reactions:
```
<Callout type="warning">
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.
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.
</Callout>
## Recipe: Custom CI failure message
@ -95,7 +97,7 @@ projects:
path: ~/code/myapp
reactions:
all-complete:
auto: false # suppress the "all sessions done" notification for this project
auto: false # suppress the "all sessions done" notification for this project
otherproj:
repo: org/otherproj
@ -120,13 +122,13 @@ The `threshold` field is used exclusively by `agent-stuck`. A session must be co
## Where to go next
<Cards>
<Card title="Reactions reference" href="/docs/configuration/reactions">
Full schema, default values, escalation semantics, and the two-pass CI design.
</Card>
<Card title="CI recovery guide" href="/docs/guides/ci-recovery">
Step-by-step walkthrough of AO's CI failure recovery loop.
</Card>
<Card title="Review loop guide" href="/docs/guides/review-loop">
How AO handles review comments and the changes-requested reaction.
</Card>
<Card title="Reactions reference" href="/docs/configuration/reactions">
Full schema, default values, escalation semantics, and the two-pass CI design.
</Card>
<Card title="CI recovery guide" href="/docs/guides/ci-recovery">
Step-by-step walkthrough of AO's CI failure recovery loop.
</Card>
<Card title="Review loop guide" href="/docs/guides/review-loop">
How AO handles review comments and the changes-requested reaction.
</Card>
</Cards>

View File

@ -26,7 +26,8 @@ A single structured prompt with:
- A pointer to the PR head SHA
<Callout type="info">
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.
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.
</Callout>
## Configurable behavior
@ -34,8 +35,8 @@ A single structured prompt with:
```yaml title="agent-orchestrator.yaml"
reactions:
reviewRequested:
enabled: true # default
includeResolved: false # default: only unresolved threads
enabled: true # default
includeResolved: false # default: only unresolved threads
maxRetries: 3
```
@ -70,31 +71,31 @@ Not every review comment is from a human. AO recognises a hardcoded list of know
**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 |
| 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 |
| 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:
@ -102,11 +103,11 @@ A typical configuration pairing:
reactions:
changes-requested:
auto: true
priority: "action" # human review comment — act immediately
priority: "action" # human review comment — act immediately
bugbot-comments:
auto: true
priority: "info" # advisory bot feedback — log and proceed
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`.

View File

@ -10,7 +10,8 @@ Agent Orchestrator (**AO**) runs AI coding agents in isolated git worktrees and
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.
<Callout type="info" title="Fastest path">
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).
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).
</Callout>
## What AO Is For
@ -29,12 +30,12 @@ AO works best for issues that have a clear outcome: failing tests, small feature
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 |
| 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
@ -48,33 +49,55 @@ The dashboard shows this lifecycle as session cards. Open a card to see the live
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 |
| 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
<PlatformSupport
macos="full"
linux="full"
windows="partial"
note={<>Windows support is actively improving. Use the process runtime instead of tmux by setting <code>defaults.runtime: process</code> in <code>agent-orchestrator.yaml</code>. See <a href="/docs/platforms">Platforms</a> for details.</>}
macos="full"
linux="full"
windows="partial"
note={
<>
Windows support is actively improving. Use the process runtime instead of tmux by setting{" "}
<code>defaults.runtime: process</code> in <code>agent-orchestrator.yaml</code>. See{" "}
<a href="/docs/platforms">Platforms</a> for details.
</>
}
/>
## Where To Go Next
<Cards>
<Card title="Installation" description="Install AO, authenticate your tools, and run the doctor check." href="/docs/installation" />
<Card title="Quickstart" description="Spawn one agent on one issue and watch it open a PR." href="/docs/quickstart" />
<Card title="Configuration" description="Understand the `agent-orchestrator.yaml` file." href="/docs/configuration" />
<Card title="Guides" description="Run parallel issues, recover from CI failures, and handle review loops." href="/docs/guides" />
<Card title="Plugins" description="See which agents, runtimes, trackers, SCMs, terminals, and notifiers are available." href="/docs/plugins" />
<Card title="Troubleshooting" description="Fix common install, dashboard, runtime, and session problems." href="/docs/troubleshooting" />
<Card
title="Installation"
description="Install AO, authenticate your tools, and run the doctor check."
href="/docs/installation"
/>
<Card title="Quickstart" description="Spawn one agent on one issue and watch it open a PR." href="/docs/quickstart" />
<Card title="Configuration" description="Understand the `agent-orchestrator.yaml` file." href="/docs/configuration" />
<Card
title="Guides"
description="Run parallel issues, recover from CI failures, and handle review loops."
href="/docs/guides"
/>
<Card
title="Plugins"
description="See which agents, runtimes, trackers, SCMs, terminals, and notifiers are available."
href="/docs/plugins"
/>
<Card
title="Troubleshooting"
description="Fix common install, dashboard, runtime, and session problems."
href="/docs/troubleshooting"
/>
</Cards>

View File

@ -11,57 +11,46 @@ 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.
<Callout type="info" title="What AO installs">
The published package is `@aoagents/ao`. It provides the global `ao` command and includes the built-in CLI, dashboard, and plugin packages.
The published package is `@aoagents/ao`. It provides the global `ao` command and includes the built-in CLI, dashboard,
and plugin packages.
</Callout>
## Prerequisites
<PlatformSupport
macos="full"
linux="full"
windows="partial"
note={<>Windows support is actively improving. Use <code>defaults.runtime: process</code> instead of tmux. See <a href="/docs/platforms#windows">Platforms</a>.</>}
macos="full"
linux="full"
windows="partial"
note={
<>
Windows support is actively improving. Use <code>defaults.runtime: process</code> instead of tmux. See{" "}
<a href="/docs/platforms#windows">Platforms</a>.
</>
}
/>
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. |
| 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
<Tabs items={["npm", "pnpm", "yarn", "from source"]}>
<Tab value="npm">
```bash
npm install -g @aoagents/ao
```
</Tab>
<Tab value="pnpm">
```bash
pnpm add -g @aoagents/ao
```
</Tab>
<Tab value="yarn">
```bash
yarn global add @aoagents/ao
```
</Tab>
<Tab value="from source">
```bash
git clone https://github.com/ComposioHQ/agent-orchestrator
cd agent-orchestrator
pnpm install
pnpm build
pnpm --filter @aoagents/ao link --global
```
</Tab>
<Tab value="npm">```bash npm install -g @aoagents/ao ```</Tab>
<Tab value="pnpm">```bash pnpm add -g @aoagents/ao ```</Tab>
<Tab value="yarn">```bash yarn global add @aoagents/ao ```</Tab>
<Tab value="from source">
```bash git clone https://github.com/ComposioHQ/agent-orchestrator cd agent-orchestrator pnpm install pnpm build
pnpm --filter @aoagents/ao link --global ```
</Tab>
</Tabs>
Confirm the command is available:
@ -94,37 +83,11 @@ The authenticated account must be able to read the repository, create branches,
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.
<Tabs items={["Claude Code", "Codex", "Cursor", "Aider", "OpenCode"]}>
<Tab value="Claude Code">
```bash
npm install -g @anthropic-ai/claude-code
claude
```
</Tab>
<Tab value="Codex">
```bash
npm install -g @openai/codex
codex
```
</Tab>
<Tab value="Cursor">
```bash
curl https://cursor.com/install -fsS | bash
agent --help
```
</Tab>
<Tab value="Aider">
```bash
pip install aider-install
aider-install
aider --help
```
</Tab>
<Tab value="OpenCode">
```bash
npm install -g opencode-ai
opencode --help
```
</Tab>
<Tab value="Claude Code">```bash npm install -g @anthropic-ai/claude-code claude ```</Tab>
<Tab value="Codex">```bash npm install -g @openai/codex codex ```</Tab>
<Tab value="Cursor">```bash curl https://cursor.com/install -fsS | bash agent --help ```</Tab>
<Tab value="Aider">```bash pip install aider-install aider-install aider --help ```</Tab>
<Tab value="OpenCode">```bash npm install -g opencode-ai opencode --help ```</Tab>
</Tabs>
</Step>
@ -180,30 +143,36 @@ Use Slack, Discord, webhook, or another network notifier for alerts. Desktop not
## Common Install Issues
<Accordions>
<Accordion title="`ao` is not found after install">
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.
</Accordion>
<Accordion title="`gh` is not authenticated">
Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is authenticated.
</Accordion>
<Accordion title="tmux is missing">
Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container.
</Accordion>
<Accordion title="No agent runtime is detected">
Install one supported agent CLI and run it once outside AO so it can complete its own sign-in flow.
</Accordion>
<Accordion title="Port 3000 is already in use">
`ao start` scans for a free port and prints the final dashboard URL. Use the URL in the command output.
</Accordion>
<Accordion title="The dashboard fails after an update">
Run `ao dashboard --rebuild` to clean stale dashboard build artifacts and start again.
</Accordion>
<Accordion title="`ao` is not found after install">
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.
</Accordion>
<Accordion title="`gh` is not authenticated">
Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is
authenticated.
</Accordion>
<Accordion title="tmux is missing">
Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container.
</Accordion>
<Accordion title="No agent runtime is detected">
Install one supported agent CLI and run it once outside AO so it can complete its own sign-in flow.
</Accordion>
<Accordion title="Port 3000 is already in use">
`ao start` scans for a free port and prints the final dashboard URL. Use the URL in the command output.
</Accordion>
<Accordion title="The dashboard fails after an update">
Run `ao dashboard --rebuild` to clean stale dashboard build artifacts and start again.
</Accordion>
</Accordions>
## Next
<Cards>
<Card title="Quickstart" description="Run one worker session from start to pull request." href="/docs/quickstart" />
<Card title="Configuration" description="Understand the generated `agent-orchestrator.yaml`." href="/docs/configuration" />
<Card title="Platforms" description="Choose the right runtime and notifier for your OS." href="/docs/platforms" />
<Card title="Quickstart" description="Run one worker session from start to pull request." href="/docs/quickstart" />
<Card
title="Configuration"
description="Understand the generated `agent-orchestrator.yaml`."
href="/docs/configuration"
/>
<Card title="Platforms" description="Choose the right runtime and notifier for your OS." href="/docs/platforms" />
</Cards>

View File

@ -1,24 +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"
]
"title": "Docs",
"pages": [
"---Getting Started---",
"index",
"installation",
"quickstart",
"platforms",
"---Learn---",
"guides",
"plugins",
"---Reference---",
"cli",
"configuration",
"architecture",
"examples",
"dashboard",
"---Help---",
"troubleshooting",
"faq",
"migration",
"changelog"
]
}

View File

@ -6,7 +6,7 @@ description: Breaking changes between AO versions and how to upgrade cleanly.
import { Callout } from "fumadocs-ui/components/callout";
<Callout type="info">
AO is in active development and follows semver pre-1.0. Breaking changes are called out here with migration steps.
AO is in active development and follows semver pre-1.0. Breaking changes are called out here with migration steps.
</Callout>
## From `@composio/agent-orchestrator` to `@aoagents/ao`

View File

@ -10,20 +10,20 @@ AO's core workflow is the same on every platform: create an isolated workspace,
The platform differences come from the tools used to run and attach to long-lived sessions.
<PlatformSupport
macos="full"
linux="full"
windows="partial"
note={<>Windows support is actively improving. Use the process runtime instead of tmux.</>}
macos="full"
linux="full"
windows="partial"
note={<>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. |
| 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
@ -46,16 +46,17 @@ defaults:
What works well on macOS:
| Capability | Status |
| --- | --- |
| tmux-backed worker sessions | Supported |
| Browser dashboard terminal | Supported |
| iTerm2 attach/open helpers | Supported |
| Desktop notifications | Supported |
| 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 |
<Callout type="info" title="Lid-close sleep still wins">
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.
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.
</Callout>
## Linux
@ -79,20 +80,21 @@ defaults:
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 |
| 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
<Callout type="warn" title="Windows support is in progress">
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.
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.
</Callout>
Recommended config:
@ -106,22 +108,22 @@ defaults:
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 |
| 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 |
| 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`.
@ -147,9 +149,9 @@ Operational notes:
## 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 |
| 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.

View File

@ -6,8 +6,10 @@ description: Aider pair-programming CLI. No session resume; AO tracks activity v
import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="aider" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>agent</code> · Name: <code>aider</code> · Binary: <code>aider</code></span>
<Logo name="aider" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>agent</code> · Name: <code>aider</code> · Binary: <code>aider</code>
</span>
</div>
[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.
@ -37,20 +39,21 @@ agent: aider
## 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` |
| 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
<Accordions>
<Accordion title="Aider hangs on file-edit confirmation">
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.
</Accordion>
<Accordion title="Dashboard cost shows $0">
AO doesn't parse Aider's cost output. The cost column stays empty.
</Accordion>
<Accordion title="Aider hangs on file-edit confirmation">
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.
</Accordion>
<Accordion title="Dashboard cost shows $0">
AO doesn't parse Aider's cost output. The cost column stays empty.
</Accordion>
</Accordions>

View File

@ -6,8 +6,10 @@ description: Anthropic's CLI coding agent. The default agent in AO.
import { Callout } from "fumadocs-ui/components/callout";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="claude-code" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>agent</code> · Name: <code>claude-code</code> · Binary: <code>claude</code></span>
<Logo name="claude-code" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>agent</code> · Name: <code>claude-code</code> · Binary: <code>claude</code>
</span>
</div>
[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.
@ -43,11 +45,11 @@ That's it — there are no plugin-level config keys.
## 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 |
| 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.

View File

@ -6,8 +6,10 @@ description: OpenAI's Codex CLI. Full session resume and native JSONL event stre
import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="codex" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>agent</code> · Name: <code>codex</code> · Binary: <code>codex</code></span>
<Logo name="codex" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>agent</code> · Name: <code>codex</code> · Binary: <code>codex</code>
</span>
</div>
[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.
@ -37,13 +39,13 @@ No plugin-level config.
## 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 |
| 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.

View File

@ -6,8 +6,10 @@ description: Cursor Agent CLI — IDE-like editing from the terminal, with PATH-
import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="cursor" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>agent</code> · Name: <code>cursor</code> · Binary: <code>agent</code></span>
<Logo name="cursor" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>agent</code> · Name: <code>cursor</code> · Binary: <code>agent</code>
</span>
</div>
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.
@ -49,20 +51,22 @@ The plugin's `detect()` checks `agent --help` output for the strings `Cursor Age
## 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` |
| 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
<Accordions>
<Accordion title="AO says Cursor Agent isn't detected">
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.
</Accordion>
<Accordion title="Dashboard never leaves `active`">
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.
</Accordion>
<Accordion title="AO says Cursor Agent isn't detected">
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.
</Accordion>
<Accordion title="Dashboard never leaves `active`">
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.
</Accordion>
</Accordions>

View File

@ -6,25 +6,39 @@ description: Pick the agent that writes the code. AO supports five today, and th
The agent is the piece that actually writes code. Everything else in AO — worktrees, runtimes, notifiers — is plumbing around it.
<Callout type="info">
**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.
**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.
</Callout>
## 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 <threadId>` |
| [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 |
| 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 <threadId>` |
| [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 |
<PluginGrid>
<PluginCard name="Claude Code" logo="claude-code" href="/docs/plugins/agents/claude-code" description="Anthropic's CLI coding agent." />
<PluginCard name="Codex" logo="codex" href="/docs/plugins/agents/codex" description="OpenAI's Codex CLI." />
<PluginCard name="Cursor" logo="cursor" href="/docs/plugins/agents/cursor" description="Cursor Agent CLI." />
<PluginCard name="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI." />
<PluginCard name="OpenCode" logo="opencode" href="/docs/plugins/agents/opencode" description="OpenCode terminal agent." />
<PluginCard
name="Claude Code"
logo="claude-code"
href="/docs/plugins/agents/claude-code"
description="Anthropic's CLI coding agent."
/>
<PluginCard name="Codex" logo="codex" href="/docs/plugins/agents/codex" description="OpenAI's Codex CLI." />
<PluginCard name="Cursor" logo="cursor" href="/docs/plugins/agents/cursor" description="Cursor Agent CLI." />
<PluginCard name="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI." />
<PluginCard
name="OpenCode"
logo="opencode"
href="/docs/plugins/agents/opencode"
description="OpenCode terminal agent."
/>
</PluginGrid>
## Choosing
@ -48,7 +62,7 @@ Neither approach requires you to change your agent's config — AO sets it up tr
Per-project overrides:
```yaml
agent: claude-code # global default
agent: claude-code # global default
projects:
api:
repo: myorg/api

View File

@ -1,10 +1,4 @@
{
"title": "Agents",
"pages": [
"claude-code",
"codex",
"cursor",
"aider",
"opencode"
]
"title": "Agents",
"pages": ["claude-code", "codex", "cursor", "aider", "opencode"]
}

View File

@ -6,8 +6,10 @@ description: OpenCode terminal agent. Uses the OpenCode session API for resume a
import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="opencode" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>agent</code> · Name: <code>opencode</code> · Binary: <code>opencode</code></span>
<Logo name="opencode" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>agent</code> · Name: <code>opencode</code> · Binary: <code>opencode</code>
</span>
</div>
[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.
@ -38,12 +40,12 @@ No plugin-level config.
## 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` |
| 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

View File

@ -17,17 +17,17 @@ Every plugin module must satisfy `PluginModule<T>`, where `T` is the interface f
```typescript
export interface PluginModule<T = unknown> {
manifest: PluginManifest;
create(config?: Record<string, unknown>): T;
detect?(): boolean;
manifest: PluginManifest;
create(config?: Record<string, unknown>): 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;
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;
}
```
@ -49,18 +49,18 @@ Return `true` when the system has what your plugin needs — a binary on PATH, a
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",
name: "my-notifier",
slot: "notifier" as const,
description: "Send alerts to My Service",
version: "0.1.0",
};
export function create(config?: Record<string, unknown>): Notifier {
// validate here, return interface implementation
// validate here, return interface implementation
}
export function detect(): boolean {
return Boolean(process.env["MY_SERVICE_TOKEN"]);
return Boolean(process.env["MY_SERVICE_TOKEN"]);
}
export default { manifest, create, detect } satisfies PluginModule<Notifier>;
@ -110,13 +110,13 @@ Add `--non-interactive` to require all fields to be provided via flags (useful i
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) |
| 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) |
</Step>
<Step>
@ -141,22 +141,22 @@ The generated `src/index.ts` looks like this:
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",
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<string, unknown>) {
return {
name: manifest.name,
config: config ?? {},
// TODO: replace this placeholder with a real notifier implementation.
};
},
manifest,
create(config?: Record<string, unknown>) {
return {
name: manifest.name,
config: config ?? {},
// TODO: replace this placeholder with a real notifier implementation.
};
},
};
export default plugin;
@ -226,15 +226,15 @@ All interfaces are defined in `packages/core/src/types.ts` and exported from `@a
### Runtime
| Method | Signature | Required |
|--------|-----------|---------|
| `create` | `(config: RuntimeCreateConfig) => Promise<RuntimeHandle>` | yes |
| `destroy` | `(handle: RuntimeHandle) => Promise<void>` | yes |
| `sendMessage` | `(handle: RuntimeHandle, message: string) => Promise<void>` | yes |
| `getOutput` | `(handle: RuntimeHandle, lines?: number) => Promise<string>` | yes |
| `isAlive` | `(handle: RuntimeHandle) => Promise<boolean>` | yes |
| `getMetrics` | `(handle: RuntimeHandle) => Promise<RuntimeMetrics>` | optional |
| `getAttachInfo` | `(handle: RuntimeHandle) => Promise<AttachInfo>` | optional |
| Method | Signature | Required |
| --------------- | ------------------------------------------------------------ | -------- |
| `create` | `(config: RuntimeCreateConfig) => Promise<RuntimeHandle>` | yes |
| `destroy` | `(handle: RuntimeHandle) => Promise<void>` | yes |
| `sendMessage` | `(handle: RuntimeHandle, message: string) => Promise<void>` | yes |
| `getOutput` | `(handle: RuntimeHandle, lines?: number) => Promise<string>` | yes |
| `isAlive` | `(handle: RuntimeHandle) => Promise<boolean>` | yes |
| `getMetrics` | `(handle: RuntimeHandle) => Promise<RuntimeMetrics>` | optional |
| `getAttachInfo` | `(handle: RuntimeHandle) => Promise<AttachInfo>` | optional |
### Agent
@ -242,52 +242,54 @@ Agent plugins have the richest interface. Methods are split into required and op
**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 |
| 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) |
| 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) |
<Callout type="warn">
`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).
`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).
</Callout>
### Workspace
| Method | Signature | Required |
|--------|-----------|---------|
| `create` | `(config: WorkspaceCreateConfig) => Promise<WorkspaceInfo>` | yes |
| `destroy` | `(workspacePath: string) => Promise<void>` | yes |
| `list` | `(projectId: string) => Promise<WorkspaceInfo[]>` | yes |
| `postCreate` | `(info: WorkspaceInfo, project: ProjectConfig) => Promise<void>` | optional |
| `exists` | `(workspacePath: string) => Promise<boolean>` | optional |
| `restore` | `(config: WorkspaceCreateConfig, workspacePath: string) => Promise<WorkspaceInfo>` | optional |
| Method | Signature | Required |
| ------------ | ---------------------------------------------------------------------------------- | -------- |
| `create` | `(config: WorkspaceCreateConfig) => Promise<WorkspaceInfo>` | yes |
| `destroy` | `(workspacePath: string) => Promise<void>` | yes |
| `list` | `(projectId: string) => Promise<WorkspaceInfo[]>` | yes |
| `postCreate` | `(info: WorkspaceInfo, project: ProjectConfig) => Promise<void>` | optional |
| `exists` | `(workspacePath: string) => Promise<boolean>` | optional |
| `restore` | `(config: WorkspaceCreateConfig, workspacePath: string) => Promise<WorkspaceInfo>` | optional |
### Tracker
| Method | Signature | Required |
|--------|-----------|---------|
| `getIssue` | `(identifier: string, project: ProjectConfig) => Promise<Issue>` | yes |
| `isCompleted` | `(identifier: string, project: ProjectConfig) => Promise<boolean>` | yes |
| `issueUrl` | `(identifier: string, project: ProjectConfig) => string` | yes |
| `branchName` | `(identifier: string, project: ProjectConfig) => string` | yes |
| `generatePrompt` | `(identifier: string, project: ProjectConfig) => Promise<string>` | yes |
| `issueLabel` | `(url: string, project: ProjectConfig) => string` | optional |
| `listIssues` | `(filters: IssueFilters, project: ProjectConfig) => Promise<Issue[]>` | optional |
| `updateIssue` | `(identifier: string, update: IssueUpdate, project: ProjectConfig) => Promise<void>` | optional |
| `createIssue` | `(input: CreateIssueInput, project: ProjectConfig) => Promise<Issue>` | optional |
| Method | Signature | Required |
| ---------------- | ------------------------------------------------------------------------------------ | -------- |
| `getIssue` | `(identifier: string, project: ProjectConfig) => Promise<Issue>` | yes |
| `isCompleted` | `(identifier: string, project: ProjectConfig) => Promise<boolean>` | yes |
| `issueUrl` | `(identifier: string, project: ProjectConfig) => string` | yes |
| `branchName` | `(identifier: string, project: ProjectConfig) => string` | yes |
| `generatePrompt` | `(identifier: string, project: ProjectConfig) => Promise<string>` | yes |
| `issueLabel` | `(url: string, project: ProjectConfig) => string` | optional |
| `listIssues` | `(filters: IssueFilters, project: ProjectConfig) => Promise<Issue[]>` | optional |
| `updateIssue` | `(identifier: string, update: IssueUpdate, project: ProjectConfig) => Promise<void>` | optional |
| `createIssue` | `(input: CreateIssueInput, project: ProjectConfig) => Promise<Issue>` | optional |
### SCM
@ -299,18 +301,18 @@ The richest interface — covers PR lifecycle, CI tracking, review tracking, and
### Notifier
| Method | Signature | Required |
|--------|-----------|---------|
| `notify` | `(event: OrchestratorEvent) => Promise<void>` | yes |
| `notifyWithActions` | `(event: OrchestratorEvent, actions: NotifyAction[]) => Promise<void>` | optional |
| `post` | `(message: string, context?: NotifyContext) => Promise<string \| null>` | optional |
| Method | Signature | Required |
| ------------------- | ----------------------------------------------------------------------- | -------- |
| `notify` | `(event: OrchestratorEvent) => Promise<void>` | yes |
| `notifyWithActions` | `(event: OrchestratorEvent, actions: NotifyAction[]) => Promise<void>` | optional |
| `post` | `(message: string, context?: NotifyContext) => Promise<string \| null>` | optional |
### Terminal
| Method | Signature | Required |
|--------|-----------|---------|
| `openSession` | `(session: Session) => Promise<void>` | yes |
| `openAll` | `(sessions: Session[]) => Promise<void>` | yes |
| Method | Signature | Required |
| --------------- | ---------------------------------------- | -------- |
| `openSession` | `(session: Session) => Promise<void>` | yes |
| `openAll` | `(sessions: Session[]) => Promise<void>` | yes |
| `isSessionOpen` | `(session: Session) => Promise<boolean>` | optional |
---
@ -321,37 +323,37 @@ Validate all config once at load time. Store validated values in a closure. Neve
```typescript
export function create(config?: Record<string, unknown>): Notifier {
// Resolve config or fall back to an environment variable.
const url = (config?.url as string | undefined) ?? process.env["WEBHOOK_URL"];
// 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
}
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<string, string> = {};
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;
}
}
// Custom headers are optional — silently ignore non-string values.
const customHeaders: Record<string, string> = {};
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 }),
});
},
};
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 }),
});
},
};
}
```
@ -420,14 +422,14 @@ projects:
## `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 <slot>`. Add `--refresh` to pull the latest catalog from the registry. |
| `ao plugin search <query>` | Search the bundled catalog by name, package, description, or slot. |
| `ao plugin create [dir]` | Scaffold a new plugin package interactively. |
| `ao plugin install <reference>` | 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 <reference>` | Remove a plugin from the config. |
| 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 <slot>`. Add `--refresh` to pull the latest catalog from the registry. |
| `ao plugin search <query>` | Search the bundled catalog by name, package, description, or slot. |
| `ao plugin create [dir]` | Scaffold a new plugin package interactively. |
| `ao plugin install <reference>` | 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 <reference>` | Remove a plugin from the config. |
---
@ -437,42 +439,42 @@ All utilities are exported from `@aoagents/ao-core`. The source lives in `packag
**Shell safety and HTTP:**
| Export | Purpose |
|--------|---------|
| 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. |
| `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. |
| 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"`). |
| 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 |
| 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 |
| `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`).
@ -498,23 +500,23 @@ 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");
});
it("has correct slot and name", () => {
expect(pluginModule.manifest.slot).toBe("notifier");
expect(pluginModule.manifest.name).toBe("pagerduty");
});
});
describe("create()", () => {
beforeEach(() => vi.clearAllMocks());
beforeEach(() => vi.clearAllMocks());
it("throws when routing_key is missing", () => {
expect(() => pluginModule.create({})).toThrow("routing_key");
});
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");
});
it("returns a notifier with notify()", () => {
const notifier = pluginModule.create({ routing_key: "test-key" });
expect(typeof notifier.notify).toBe("function");
});
});
```
@ -524,13 +526,13 @@ For agent plugins, the required `getActivityState` tests are listed in the CLAUD
## 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 |
| 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 |
---
@ -539,7 +541,10 @@ For agent plugins, the required `getActivityState` tests are listed in the CLAUD
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.
<Callout type="warn">
**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.
**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.
</Callout>
The required 4-step cascade:
@ -587,7 +592,15 @@ For agents that don't have a native session API (most new agents), skip step 3 a
## Next steps
<Cards>
<Card title="Architecture" description="Understand the plugin registry, session lifecycle, and how slots wire together." href="/docs/architecture" />
<Card title="Plugin catalog" description="Browse every bundled plugin grouped by slot." href="/docs/plugins" />
<Card title="Configuration reference" description="How plugins are referenced in agent-orchestrator.yaml." href="/docs/configuration" />
<Card
title="Architecture"
description="Understand the plugin registry, session lifecycle, and how slots wire together."
href="/docs/architecture"
/>
<Card title="Plugin catalog" description="Browse every bundled plugin grouped by slot." href="/docs/plugins" />
<Card
title="Configuration reference"
description="How plugins are referenced in agent-orchestrator.yaml."
href="/docs/configuration"
/>
</Cards>

View File

@ -7,73 +7,183 @@ AO has **eight plugin slots**. Only one plugin per slot is active at a time, and
## 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) |
| 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
<PluginGrid>
<PluginCard name="Claude Code" logo="claude-code" href="/docs/plugins/agents/claude-code" description="Anthropic's CLI coding agent. Session resume via --resume." />
<PluginCard name="Codex" logo="codex" href="/docs/plugins/agents/codex" description="OpenAI Codex CLI. Session resume via codex resume <threadId>." />
<PluginCard name="Cursor" logo="cursor" href="/docs/plugins/agents/cursor" description="Cursor Agent CLI. One-shot per session." />
<PluginCard name="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI. No resume; PATH wrappers for PR tracking." />
<PluginCard name="OpenCode" logo="opencode" href="/docs/plugins/agents/opencode" description="OpenCode terminal agent. Session discovery + restore via the OpenCode session API." />
<PluginCard
name="Claude Code"
logo="claude-code"
href="/docs/plugins/agents/claude-code"
description="Anthropic's CLI coding agent. Session resume via --resume."
/>
<PluginCard
name="Codex"
logo="codex"
href="/docs/plugins/agents/codex"
description="OpenAI Codex CLI. Session resume via codex resume <threadId>."
/>
<PluginCard
name="Cursor"
logo="cursor"
href="/docs/plugins/agents/cursor"
description="Cursor Agent CLI. One-shot per session."
/>
<PluginCard
name="Aider"
logo="aider"
href="/docs/plugins/agents/aider"
description="Aider pair-programming CLI. No resume; PATH wrappers for PR tracking."
/>
<PluginCard
name="OpenCode"
logo="opencode"
href="/docs/plugins/agents/opencode"
description="OpenCode terminal agent. Session discovery + restore via the OpenCode session API."
/>
</PluginGrid>
## Runtimes
<PluginGrid>
<PluginCard name="tmux" logo="tmux" href="/docs/plugins/runtimes/tmux" description="Default on macOS/Linux. Each agent gets its own tmux window you can attach to." />
<PluginCard name="process" logo="windows" href="/docs/plugins/runtimes/process" description="Cross-platform child-process runtime. Required on Windows." />
<PluginCard
name="tmux"
logo="tmux"
href="/docs/plugins/runtimes/tmux"
description="Default on macOS/Linux. Each agent gets its own tmux window you can attach to."
/>
<PluginCard
name="process"
logo="windows"
href="/docs/plugins/runtimes/process"
description="Cross-platform child-process runtime. Required on Windows."
/>
</PluginGrid>
## Workspaces
<PluginGrid>
<PluginCard name="worktree" logo="github" href="/docs/plugins/workspaces/worktree" description="git worktree per session. Fast, shares the object DB with your main checkout." />
<PluginCard name="clone" logo="github" href="/docs/plugins/workspaces/clone" description="Full clone per session. Use when your tooling doesn't play well with shared .git." />
<PluginCard
name="worktree"
logo="github"
href="/docs/plugins/workspaces/worktree"
description="git worktree per session. Fast, shares the object DB with your main checkout."
/>
<PluginCard
name="clone"
logo="github"
href="/docs/plugins/workspaces/clone"
description="Full clone per session. Use when your tooling doesn't play well with shared .git."
/>
</PluginGrid>
## Trackers
<PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/trackers/github" description="Issues + PRs via the gh CLI." />
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/trackers/gitlab" description="Issues + MRs via the glab CLI. Self-hosted supported." />
<PluginCard name="Linear" logo="linear" href="/docs/plugins/trackers/linear" description="Linear issues. Direct API or Composio-mediated." />
<PluginCard
name="GitHub"
logo="github"
href="/docs/plugins/trackers/github"
description="Issues + PRs via the gh CLI."
/>
<PluginCard
name="GitLab"
logo="gitlab"
href="/docs/plugins/trackers/gitlab"
description="Issues + MRs via the glab CLI. Self-hosted supported."
/>
<PluginCard
name="Linear"
logo="linear"
href="/docs/plugins/trackers/linear"
description="Linear issues. Direct API or Composio-mediated."
/>
</PluginGrid>
## SCM
<PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/scm/github" description="PRs, reviews, and CI status via gh." />
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/scm/gitlab" description="MRs, discussions, pipelines via glab." />
<PluginCard
name="GitHub"
logo="github"
href="/docs/plugins/scm/github"
description="PRs, reviews, and CI status via gh."
/>
<PluginCard
name="GitLab"
logo="gitlab"
href="/docs/plugins/scm/gitlab"
description="MRs, discussions, pipelines via glab."
/>
</PluginGrid>
## Notifiers
<PluginGrid>
<PluginCard name="Dashboard" logo="web" href="/docs/plugins/notifiers/dashboard" description="Retained notifications inside the AO dashboard." />
<PluginCard name="Desktop" logo="apple" href="/docs/plugins/notifiers/desktop" description="Native macOS/Linux notifications. No-op on Windows." />
<PluginCard name="Discord" logo="discord" href="/docs/plugins/notifiers/discord" description="Webhook-based Discord messages with rich embeds." />
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhooks." />
<PluginCard name="Webhook" logo="webhook" href="/docs/plugins/notifiers/webhook" description="Generic HTTP POST with retries and exponential backoff." />
<PluginCard name="Composio" logo="composio" href="/docs/plugins/notifiers/composio" description="Route through the Composio toolkit — Slack, Discord, or Gmail." />
<PluginCard name="OpenClaw" logo="openclaw" href="/docs/plugins/notifiers/openclaw" description="Local OpenClaw gateway for personal notifications." />
<PluginCard
name="Dashboard"
logo="web"
href="/docs/plugins/notifiers/dashboard"
description="Retained notifications inside the AO dashboard."
/>
<PluginCard
name="Desktop"
logo="apple"
href="/docs/plugins/notifiers/desktop"
description="Native macOS/Linux notifications. No-op on Windows."
/>
<PluginCard
name="Discord"
logo="discord"
href="/docs/plugins/notifiers/discord"
description="Webhook-based Discord messages with rich embeds."
/>
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhooks." />
<PluginCard
name="Webhook"
logo="webhook"
href="/docs/plugins/notifiers/webhook"
description="Generic HTTP POST with retries and exponential backoff."
/>
<PluginCard
name="Composio"
logo="composio"
href="/docs/plugins/notifiers/composio"
description="Route through the Composio toolkit — Slack, Discord, or Gmail."
/>
<PluginCard
name="OpenClaw"
logo="openclaw"
href="/docs/plugins/notifiers/openclaw"
description="Local OpenClaw gateway for personal notifications."
/>
</PluginGrid>
## Terminals
<PluginGrid>
<PluginCard name="iTerm2" logo="iterm2" href="/docs/plugins/terminals/iterm2" description="Open attached tabs in iTerm2 via AppleScript (macOS only)." />
<PluginCard name="Web" logo="web" href="/docs/plugins/terminals/web" description="Dashboard xterm.js session URL. Cross-platform." />
<PluginCard
name="iTerm2"
logo="iterm2"
href="/docs/plugins/terminals/iterm2"
description="Open attached tabs in iTerm2 via AppleScript (macOS only)."
/>
<PluginCard
name="Web"
logo="web"
href="/docs/plugins/terminals/web"
description="Dashboard xterm.js session URL. Cross-platform."
/>
</PluginGrid>
## Writing your own

View File

@ -1,14 +1,5 @@
{
"title": "Plugins",
"defaultOpen": false,
"pages": [
"agents",
"runtimes",
"workspaces",
"trackers",
"scm",
"notifiers",
"terminals",
"authoring"
]
"title": "Plugins",
"defaultOpen": false,
"pages": ["agents", "runtimes", "workspaces", "trackers", "scm", "notifiers", "terminals", "authoring"]
}

View File

@ -4,10 +4,10 @@ description: Route notifications through the Composio toolkit — Slack, Discord
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="composio" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>composio</code>
</span>
<Logo name="composio" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>composio</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />

View File

@ -4,11 +4,18 @@ description: Native macOS / Linux notifications. Silent no-op on Windows.
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="apple" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>notifier</code> · Name: <code>desktop</code></span>
<Logo name="apple" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>desktop</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="none" note="On Windows this notifier logs a warning and does nothing — use Discord, Slack, or webhook instead." />
<PlatformSupport
macos="full"
linux="full"
windows="none"
note="On Windows this notifier logs a warning and does nothing — use Discord, Slack, or webhook instead."
/>
## Setup
@ -36,11 +43,11 @@ notificationRouting:
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 |
| 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

View File

@ -4,10 +4,10 @@ description: Discord webhook with rich embeds, retry handling, and thread suppor
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="discord" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>discord</code>
</span>
<Logo name="discord" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>discord</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />

View File

@ -6,13 +6,43 @@ description: Who gets pinged when an agent needs you. Seven notifiers ship; pick
Notifiers deliver AO events — session stuck, PR opened, review requested, CI failed — to wherever you actually pay attention.
<PluginGrid>
<PluginCard name="Dashboard" logo="web" href="/docs/plugins/notifiers/dashboard" description="Retained notifications inside the AO dashboard." />
<PluginCard name="Desktop" logo="apple" href="/docs/plugins/notifiers/desktop" description="Native macOS/Linux notifications. No-op on Windows." />
<PluginCard name="Discord" logo="discord" href="/docs/plugins/notifiers/discord" description="Discord webhook with rich embeds." />
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhook." />
<PluginCard name="Webhook" logo="webhook" href="/docs/plugins/notifiers/webhook" description="Generic HTTP POST. Retries + exponential backoff." />
<PluginCard name="Composio" logo="composio" href="/docs/plugins/notifiers/composio" description="Route through Composio — Slack, Discord, or Gmail." />
<PluginCard name="OpenClaw" logo="openclaw" href="/docs/plugins/notifiers/openclaw" description="Local OpenClaw gateway for personal alerts." />
<PluginCard
name="Dashboard"
logo="web"
href="/docs/plugins/notifiers/dashboard"
description="Retained notifications inside the AO dashboard."
/>
<PluginCard
name="Desktop"
logo="apple"
href="/docs/plugins/notifiers/desktop"
description="Native macOS/Linux notifications. No-op on Windows."
/>
<PluginCard
name="Discord"
logo="discord"
href="/docs/plugins/notifiers/discord"
description="Discord webhook with rich embeds."
/>
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhook." />
<PluginCard
name="Webhook"
logo="webhook"
href="/docs/plugins/notifiers/webhook"
description="Generic HTTP POST. Retries + exponential backoff."
/>
<PluginCard
name="Composio"
logo="composio"
href="/docs/plugins/notifiers/composio"
description="Route through Composio — Slack, Discord, or Gmail."
/>
<PluginCard
name="OpenClaw"
logo="openclaw"
href="/docs/plugins/notifiers/openclaw"
description="Local OpenClaw gateway for personal alerts."
/>
</PluginGrid>
## Stacking notifiers
@ -56,15 +86,15 @@ 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 |
| 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).

View File

@ -1,4 +1,4 @@
{
"title": "Notifiers",
"pages": ["dashboard", "desktop", "discord", "slack", "webhook", "composio", "openclaw"]
"title": "Notifiers",
"pages": ["dashboard", "desktop", "discord", "slack", "webhook", "composio", "openclaw"]
}

View File

@ -4,8 +4,10 @@ description: Deliver notifications through a local OpenClaw gateway — personal
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="openclaw" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>notifier</code> · Name: <code>openclaw</code></span>
<Logo name="openclaw" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>openclaw</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -58,29 +60,29 @@ The OpenClaw config keeps the actual secret:
```json title="~/.openclaw/openclaw.json"
{
"hooks": {
"enabled": true,
"token": "<openclaw-hooks-token>",
"allowRequestSessionKey": true,
"allowedSessionKeyPrefixes": ["hook:"]
}
"hooks": {
"enabled": true,
"token": "<openclaw-hooks-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 |
| 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

View File

@ -4,10 +4,10 @@ description: Slack incoming webhook. Minimal config, no bot token required.
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="slack" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>slack</code>
</span>
<Logo name="slack" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>slack</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />

View File

@ -4,10 +4,10 @@ description: Generic HTTP POST. Retries, exponential backoff, custom headers.
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="webhook" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>webhook</code>
</span>
<Logo name="webhook" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>webhook</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -64,17 +64,17 @@ 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": {}
}
"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": {}
}
}
```
@ -82,9 +82,9 @@ 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" }]
"type": "notification_with_actions",
"event": { "...": "same fields as above" },
"actions": [{ "label": "View PR", "url": "https://github.com/owner/repo/pull/123" }]
}
```
@ -92,9 +92,9 @@ Free-form messages sent via `ao send` arrive as:
```json
{
"type": "message",
"message": "Hello from AO",
"context": {}
"type": "message",
"message": "Hello from AO",
"context": {}
}
```

View File

@ -5,14 +5,24 @@ description: Where the agent process runs — tmux on macOS/Linux, plain child p
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 | — |
| 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 | — |
<PluginGrid>
<PluginCard name="tmux" logo="tmux" href="/docs/plugins/runtimes/tmux" description="Each agent gets its own tmux window; attach with `ao session attach`." />
<PluginCard name="process" logo="windows" href="/docs/plugins/runtimes/process" description="Cross-platform child process. Required on Windows." />
<PluginCard
name="tmux"
logo="tmux"
href="/docs/plugins/runtimes/tmux"
description="Each agent gets its own tmux window; attach with `ao session attach`."
/>
<PluginCard
name="process"
logo="windows"
href="/docs/plugins/runtimes/process"
description="Cross-platform child process. Required on Windows."
/>
</PluginGrid>
## Choosing
@ -20,4 +30,4 @@ The runtime is where your agent's terminal actually lives. Two options ship:
- 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.
Both runtimes expose the agent's terminal to the dashboard's xterm.js session — attaching via tmux is a _bonus_, not a requirement.

View File

@ -1,4 +1,4 @@
{
"title": "Runtimes",
"pages": ["tmux", "process"]
"title": "Runtimes",
"pages": ["tmux", "process"]
}

View File

@ -4,8 +4,10 @@ description: Cross-platform child-process runtime. Required on Windows.
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="windows" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>runtime</code> · Name: <code>process</code></span>
<Logo name="windows" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>runtime</code> · Name: <code>process</code>
</span>
</div>
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.

View File

@ -4,13 +4,24 @@ description: Default runtime on macOS and Linux. Each agent gets its own tmux wi
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="tmux" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>runtime</code> · Name: <code>tmux</code></span>
<Logo name="tmux" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>runtime</code> · Name: <code>tmux</code>
</span>
</div>
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.
<PlatformSupport macos="full" linux="full" windows="none" note={<>Windows has no tmux. Use <a href="/docs/plugins/runtimes/process">process</a> instead.</>} />
<PlatformSupport
macos="full"
linux="full"
windows="none"
note={
<>
Windows has no tmux. Use <a href="/docs/plugins/runtimes/process">process</a> instead.
</>
}
/>
## Install

View File

@ -4,8 +4,10 @@ description: PRs, reviews, and CI status via the gh CLI. Optional webhook for re
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="github" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>scm</code> · Name: <code>github</code></span>
<Logo name="github" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>scm</code> · Name: <code>github</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -53,20 +55,20 @@ projects:
repo: owner/repo
scm:
webhook:
secretEnvVar: GITHUB_WEBHOOK_SECRET # env var holding the HMAC secret
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) |
| 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.

View File

@ -4,8 +4,10 @@ description: Merge requests, discussions, and pipelines via the glab CLI.
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="gitlab" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>scm</code> · Name: <code>gitlab</code></span>
<Logo name="gitlab" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>scm</code> · Name: <code>gitlab</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -19,16 +21,16 @@ glab auth login
```yaml title="agent-orchestrator.yaml"
scm: gitlab
scmConfig:
host: gitlab.com # default; override for self-hosted
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 |
| 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.
@ -59,20 +61,20 @@ projects:
repo: group/project
scm:
webhook:
secretEnvVar: GITLAB_WEBHOOK_TOKEN # env var holding the token
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) |
| 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).
@ -84,16 +86,16 @@ AO ignores review comments from known bot accounts so they don't block the merge
**Hardcoded bots:**
| Username |
|---|
| `gitlab-bot` |
| `ghost` |
| `dependabot[bot]` |
| `renovate[bot]` |
| `sast-bot` |
| Username |
| ------------------ |
| `gitlab-bot` |
| `ghost` |
| `dependabot[bot]` |
| `renovate[bot]` |
| `sast-bot` |
| `codeclimate[bot]` |
| `sonarcloud[bot]` |
| `snyk-bot` |
| `sonarcloud[bot]` |
| `snyk-bot` |
**Runtime catch-all:** any username matching `/^project_\d+_bot/` (GitLab project access tokens) or ending in `[bot]`.

View File

@ -6,8 +6,18 @@ description: PRs, reviews, CI status. The tracker watches issues; the SCM plugin
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.
<PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/scm/github" description="PRs, reviews, and CI status via gh." />
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/scm/gitlab" description="MRs, discussions, pipelines via glab." />
<PluginCard
name="GitHub"
logo="github"
href="/docs/plugins/scm/github"
description="PRs, reviews, and CI status via gh."
/>
<PluginCard
name="GitLab"
logo="gitlab"
href="/docs/plugins/scm/gitlab"
description="MRs, discussions, pipelines via glab."
/>
</PluginGrid>
## Why separate from the tracker

View File

@ -1,4 +1,4 @@
{
"title": "SCM",
"pages": ["github", "gitlab"]
"title": "SCM",
"pages": ["github", "gitlab"]
}

View File

@ -6,8 +6,18 @@ description: How you attach to a running agent. iTerm2 on macOS, or the dashboar
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.
<PluginGrid>
<PluginCard name="iTerm2" logo="iterm2" href="/docs/plugins/terminals/iterm2" description="Open attached tabs in iTerm2 via AppleScript (macOS only)." />
<PluginCard name="Web" logo="web" href="/docs/plugins/terminals/web" description="Dashboard xterm.js session URL. Cross-platform." />
<PluginCard
name="iTerm2"
logo="iterm2"
href="/docs/plugins/terminals/iterm2"
description="Open attached tabs in iTerm2 via AppleScript (macOS only)."
/>
<PluginCard
name="Web"
logo="web"
href="/docs/plugins/terminals/web"
description="Dashboard xterm.js session URL. Cross-platform."
/>
</PluginGrid>
## Default

View File

@ -4,8 +4,10 @@ description: Open attached tabs in iTerm2 via AppleScript. macOS only.
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="iterm2" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>terminal</code> · Name: <code>iterm2</code></span>
<Logo name="iterm2" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>terminal</code> · Name: <code>iterm2</code>
</span>
</div>
<PlatformSupport macos="full" linux="none" windows="none" />

View File

@ -1,4 +1,4 @@
{
"title": "Terminals",
"pages": ["iterm2", "web"]
"title": "Terminals",
"pages": ["iterm2", "web"]
}

View File

@ -4,8 +4,10 @@ description: Dashboard xterm.js terminal. Cross-platform. The only option on Win
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="web" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>terminal</code> · Name: <code>web</code></span>
<Logo name="web" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>terminal</code> · Name: <code>web</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -17,11 +19,11 @@ The dashboard already has an xterm.js terminal connected to every session. The `
```yaml title="agent-orchestrator.yaml"
terminal: web
terminalConfig:
dashboardUrl: http://localhost:3000 # default
dashboardUrl: http://localhost:3000 # default
```
| Config key | Default | What it does |
|---|---|---|
| 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

View File

@ -4,8 +4,10 @@ description: Issues via the gh CLI. Zero API tokens to manage — gh auth login
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="github" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>tracker</code> · Name: <code>github</code></span>
<Logo name="github" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>tracker</code> · Name: <code>github</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -31,13 +33,13 @@ projects:
## How it's used
| Operation | `gh` command invoked |
|---|---|
| Fetch issue | `gh issue view <num> --json title,body,...` |
| Create issue | `gh issue create` |
| Comment on issue | `gh issue comment` |
| Close issue | `gh issue close` |
| List issues | `gh issue list` |
| Operation | `gh` command invoked |
| ---------------- | ------------------------------------------- |
| Fetch issue | `gh issue view <num> --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

View File

@ -4,8 +4,10 @@ description: GitLab issues via the glab CLI. Self-hosted instances supported.
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="gitlab" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>tracker</code> · Name: <code>gitlab</code></span>
<Logo name="gitlab" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>tracker</code> · Name: <code>gitlab</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -21,18 +23,19 @@ glab auth login
```yaml title="agent-orchestrator.yaml"
tracker:
name: gitlab
host: gitlab.com # default; override for self-hosted
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 |
| Config key | Default | What it does |
| ---------- | ------------ | ---------------------------------------------------- |
| `host` | `gitlab.com` | GitLab hostname — override for self-hosted instances |
<Callout type="warn">
`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.
`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.
</Callout>
## Self-hosted

View File

@ -6,9 +6,24 @@ description: Where your issues live. AO fetches them, assigns them to sessions,
The **tracker** plugin is how AO fetches issues, creates new ones, and links sessions to them. Three trackers ship.
<PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/trackers/github" description="Issues + PRs via the gh CLI. Default." />
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/trackers/gitlab" description="Issues + MRs via the glab CLI. Self-hosted supported." />
<PluginCard name="Linear" logo="linear" href="/docs/plugins/trackers/linear" description="Linear issues. Direct API key or Composio-mediated." />
<PluginCard
name="GitHub"
logo="github"
href="/docs/plugins/trackers/github"
description="Issues + PRs via the gh CLI. Default."
/>
<PluginCard
name="GitLab"
logo="gitlab"
href="/docs/plugins/trackers/gitlab"
description="Issues + MRs via the glab CLI. Self-hosted supported."
/>
<PluginCard
name="Linear"
logo="linear"
href="/docs/plugins/trackers/linear"
description="Linear issues. Direct API key or Composio-mediated."
/>
</PluginGrid>
## What a tracker does

View File

@ -4,8 +4,10 @@ description: Linear issues. Direct API key or Composio-mediated — AO picks the
---
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="linear" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>Slot: <code>tracker</code> · Name: <code>linear</code></span>
<Logo name="linear" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>tracker</code> · Name: <code>linear</code>
</span>
</div>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -31,8 +33,8 @@ The Linear tracker supports two transports and auto-picks between them:
api:
tracker: linear
trackerConfig:
teamId: TEAM-123 # required for issue creation
workspaceSlug: myteam # optional, used to build issue URLs
teamId: TEAM-123 # required for issue creation
workspaceSlug: myteam # optional, used to build issue URLs
```
### Composio-mediated
@ -48,10 +50,10 @@ AO detects this and routes Linear calls through Composio — no separate Linear
## 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 |
| 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

View File

@ -1,4 +1,4 @@
{
"title": "Trackers",
"pages": ["github", "gitlab", "linear"]
"title": "Trackers",
"pages": ["github", "gitlab", "linear"]
}

Some files were not shown because too many files have changed in this diff Show More