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 # OS
.DS_Store .DS_Store
Thumbs.db 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. 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:** **Source files to edit:**
- `backend/internal/httpd/controllers/dto.go` — request/response shapes. - `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. - `backend/internal/httpd/apispec/specgen/build.go` — operation registry; add a `schemaNames` entry for any new named type.
**Regenerate after editing:** **Regenerate after editing:**
```bash ```bash
npm run api # runs api:spec then api:ts in sequence npm run api # runs api:spec then api:ts in sequence
``` ```
This is equivalent to running: This is equivalent to running:
```bash ```bash
npm run api:spec # cd backend && go generate ./internal/httpd/apispec/... 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 npm run api:ts # npx openapi-typescript@7.4.4 backend/internal/httpd/apispec/openapi.yaml -o frontend/src/api/schema.ts
``` ```
**Verify:** **Verify:**
```bash ```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) 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 route. Run `ao <command> --help` for the authoritative flag shape; the table
below groups what's on `main` today. below groups what's on `main` today.
| Lane | Command | Purpose | | Lane | Command | Purpose |
|---|---|---| | ------------ | ------------------------------------ | ------------------------------------------------------------ |
| Daemon | `ao start` | Start the daemon in the background and wait for `/readyz`. | | Daemon | `ao start` | Start the daemon in the background and wait for `/readyz`. |
| Daemon | `ao stop` | Graceful shutdown via loopback `POST /shutdown`. | | Daemon | `ao stop` | Graceful shutdown via loopback `POST /shutdown`. |
| Daemon | `ao status` | Report PID/port/health/readiness from `running.json`. | | Daemon | `ao status` | Report PID/port/health/readiness from `running.json`. |
| Daemon | `ao daemon` | Hidden internal entrypoint used by `ao start`. | | 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 add` | Register a local git repo as a project. |
| Project | `ao project ls` | List registered projects. | | Project | `ao project ls` | List registered projects. |
| Project | `ao project get <id>` | Fetch one project. | | Project | `ao project get <id>` | Fetch one project. |
| Project | `ao project rm <id>` | Remove a project. | | Project | `ao project rm <id>` | Remove a project. |
| Session | `ao spawn` | Spawn a worker session in a registered 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 ls` | List sessions (filter by project, include terminated). |
| Session | `ao session get <id>` | Fetch one session. | | Session | `ao session get <id>` | Fetch one session. |
| Session | `ao session kill <id>` | Terminate a session. | | Session | `ao session kill <id>` | Terminate a session. |
| Session | `ao session rename <id> <name>` | Rename a session. | | Session | `ao session rename <id> <name>` | Rename a session. |
| Session | `ao session restore <id>` | Relaunch a terminated session. | | Session | `ao session restore <id>` | Relaunch a terminated session. |
| Session | `ao session cleanup` | Reclaim eligible workspaces for terminated sessions. | | Session | `ao session cleanup` | Reclaim eligible workspaces for terminated sessions. |
| Session | `ao session claim-pr <session> <pr>` | Attach an existing PR to a session. | | Session | `ao session claim-pr <session> <pr>` | Attach an existing PR to a session. |
| Orchestrator | `ao orchestrator ls` | List orchestrator sessions. | | Orchestrator | `ao orchestrator ls` | List orchestrator sessions. |
| Messaging | `ao send` | Send a message to a running agent session. | | 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 doctor` | Local health checks (config, data dir, DB, `git`, `zellij`). |
| Utility | `ao completion <shell>` | Generate bash/zsh/fish/powershell completions. | | Utility | `ao completion <shell>` | Generate bash/zsh/fish/powershell completions. |
| Utility | `ao version` | Print build metadata. | | Utility | `ao version` | Print build metadata. |
| Internal | `ao hooks <agent> <event>` | Hidden adapter hook callback. | | Internal | `ao hooks <agent> <event>` | Hidden adapter hook callback. |
See [`docs/cli/`](docs/cli/) for the daemon-control intent and command shape. 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 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. exposing it beyond loopback would be a security regression.
| Var | Default | Purpose | | Var | Default | Purpose |
|---|---|---| | --------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
| `AO_PORT` | `3001` | Bind port; daemon fails fast if taken. | | `AO_PORT` | `3001` | Bind port; daemon fails fast if taken. |
| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout (Go duration). | | `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout (Go duration). |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful-shutdown hard cap. | | `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful-shutdown hard cap. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID + port handshake path. | | `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_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_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`. | | `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`. | | `GITHUB_TOKEN` | _(unset)_ | Used by the GitHub SCM and tracker adapters. Falls back to `gh auth token`. |
Health check: Health check:

View File

@ -10,13 +10,13 @@ Start with [architecture.md](architecture.md) for the current backend model and
## Reference docs ## Reference docs
| Doc | What it covers | | Doc | What it covers |
|-----|----------------| | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| [architecture.md](architecture.md) | Current backend model, package layout, status derivation, persistence/CDC, and load-bearing rules. | | [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. | | [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. | | [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. | | [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. | | [stack.md](stack.md) | Accepted library/runtime choices, pending stack decisions, and dependencies explicitly avoided for V1. |
## Mental model ## 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. Hook metadata changes publish `session.updated`. The frontend listens to `session.created`, `session.terminated`, and `session.updated` and invalidates the workspace query.
## Acceptance Criteria ## Acceptance Criteria
Agent adapter behavior: Agent adapter behavior:

View File

@ -7,17 +7,17 @@ call runtime, workspace, tracker, or agent adapters in-process.
## Current commands ## Current commands
| Command | Purpose | | Command | Purpose |
|---|---| | ----------------------------- | ------------------------------------------------------------------------------------------- |
| `ao start` | Start the daemon in the background and wait for `/readyz`. | | `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` | Report daemon state from `running.json`, process liveness, `/healthz`, and `/readyz`. |
| `ao status --json` | Emit the same daemon state as machine-readable JSON. | | `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 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` | Check config, data directory, DB-file presence, daemon state, `git`, and optional `zellij`. |
| `ao doctor --json` | Emit doctor checks as JSON. | | `ao doctor --json` | Emit doctor checks as JSON. |
| `ao completion <shell>` | Generate completions for `bash`, `zsh`, `fish`, or `powershell`. | | `ao completion <shell>` | Generate completions for `bash`, `zsh`, `fish`, or `powershell`. |
| `ao version` / `ao --version` | Print build metadata. | | `ao version` / `ao --version` | Print build metadata. |
| `ao daemon` | Hidden internal daemon entrypoint used by `ao start`. | | `ao daemon` | Hidden internal daemon entrypoint used by `ao start`. |
`go run .` in `backend/` remains a compatibility wrapper around the daemon. `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: The CLI and daemon share the same environment-driven config:
| Var | Default | Purpose | | Var | Default | Purpose |
|---|---|---| | --------------------- | ------------------------------------------------- | ---------------------- |
| `AO_PORT` | `3001` | Loopback daemon port. | | `AO_PORT` | `3001` | Loopback daemon port. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID/port handshake. | | `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID/port handshake. |
| `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite data directory. | | `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite data directory. |
| `AO_REQUEST_TIMEOUT` | `60s` | REST request timeout. | | `AO_REQUEST_TIMEOUT` | `60s` | REST request timeout. |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap. | | `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap. |
The daemon always binds `127.0.0.1`. 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 ## Field catalog (legacy `projects.<id>`) and target home
| YAML field | Type | Storage today | Target | | YAML field | Type | Storage today | Target |
|---|---|---|---| | --------------------------------- | ---------------------- | ----------------------------------- | ---------------------------------------------------- |
| `name` | string | `projects.display_name` | done | | `name` | string | `projects.display_name` | done |
| `repo` | string | `projects.repo_origin_url` | done | | `repo` | string | `projects.repo_origin_url` | done |
| `path` | string | `projects.path` | done | | `path` | string | `projects.path` | done |
| `defaultBranch` | string | hardcoded `"main"` | `projects.default_branch` | | `defaultBranch` | string | hardcoded `"main"` | `projects.default_branch` |
| `sessionPrefix` | string | derived | `projects.session_prefix` | | `sessionPrefix` | string | derived | `projects.session_prefix` |
| `agentConfig` | `{model, permissions}` | **`projects.agent_config` (typed)** | **done (this PR)** | | `agentConfig` | `{model, permissions}` | **`projects.agent_config` (typed)** | **done (this PR)** |
| `orchestrator`/`worker` overrides | `{agent, agentConfig}` | — | typed role-override columns/blob | | `orchestrator`/`worker` overrides | `{agent, agentConfig}` | — | typed role-override columns/blob |
| `env` | `map[string]string` | — | `project_env` table (key/value rows) | | `env` | `map[string]string` | — | `project_env` table (key/value rows) |
| `symlinks` | `[]string` | — | `projects.symlinks` (JSON) | | `symlinks` | `[]string` | — | `projects.symlinks` (JSON) |
| `postCreate` | `[]string` | — | `projects.post_create` (JSON) | | `postCreate` | `[]string` | — | `projects.post_create` (JSON) |
| `agentRules` / `agentRulesFile` | string | partial (`SpawnConfig.AgentRules`) | `projects.agent_rules*` | | `agentRules` / `agentRulesFile` | string | partial (`SpawnConfig.AgentRules`) | `projects.agent_rules*` |
| `orchestratorRules` | string | — | `projects.orchestrator_rules` | | `orchestratorRules` | string | — | `projects.orchestrator_rules` |
| `tracker` | `{plugin, …}` | DTO stub only | `projects.tracker` (typed blob) + adapter validation | | `tracker` | `{plugin, …}` | DTO stub only | `projects.tracker` (typed blob) + adapter validation |
| `scm` | `{plugin, webhook{…}}` | DTO stub only | `projects.scm` (typed blob) + adapter validation | | `scm` | `{plugin, webhook{…}}` | DTO stub only | `projects.scm` (typed blob) + adapter validation |
| `opencodeIssueSessionStrategy` | enum | — | `projects.opencode_session_strategy` | | `opencodeIssueSessionStrategy` | enum | — | `projects.opencode_session_strategy` |
| `reactions` | per-project overrides | — | `project_reactions` (own slice) | | `reactions` | per-project overrides | — | `project_reactions` (own slice) |
## Typed model ## Typed model
@ -112,7 +112,7 @@ agent adapter.
## Sequencing (one slice per PR) ## 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. pattern end to end.
2. **Project identity scalars**`default_branch`, `session_prefix` (stop 2. **Project identity scalars**`default_branch`, `session_prefix` (stop
hardcoding/deriving them). hardcoding/deriving them).

View File

@ -18,27 +18,27 @@ invariants.
## Accepted stack ## Accepted stack
| Area | Decision | Status | Rationale | | Area | Decision | Status | Rationale |
|------|----------|--------|-----------| | ------------------ | ----------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Backend language | Go 1.25.7 | Implemented | Matches `backend/go.mod`; small daemon, strong stdlib, easy local distribution. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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`. | | 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`. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. | | Packaging | `github.com/goreleaser/goreleaser` | Planned | Cross-platform release automation, checksums, and future Homebrew support. |
## Pending decisions ## Pending decisions
@ -70,15 +70,15 @@ config surface appears.
## Explicitly avoided for V1 ## Explicitly avoided for V1
| Avoid | Reason | | Avoid | Reason |
|-------|--------| | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| GORM | AO needs explicit transactional SQL and CDC-triggered writes. | | GORM | AO needs explicit transactional SQL and CDC-triggered writes. |
| Gin/Fiber | `net/http` + `chi` is enough for a local daemon API. | | 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. | | `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. | | `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. | | 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. | | 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. | | Multi-sink CDC fan-out | Start with one durable local delivery path; add fan-out later if needed. |
## Current stack mapping ## Current stack mapping

View File

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

View File

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

View File

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

View File

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

View File

@ -6,124 +6,106 @@ import { source } from "@/lib/source";
import "./docs.css"; import "./docs.css";
function GithubIcon({ size = 16 }: { size?: number } = {}) { function GithubIcon({ size = 16 }: { size?: number } = {}) {
return ( return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"> <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" /> <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> </svg>
); );
} }
function XIcon() { function XIcon() {
return ( return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <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" /> <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> </svg>
); );
} }
function DiscordIcon() { function DiscordIcon() {
return ( return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> <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" /> <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> </svg>
); );
} }
const links: LinkItemType[] = [ const links: LinkItemType[] = [
{ {
type: "icon", type: "icon",
label: "X (Twitter)", label: "X (Twitter)",
icon: <XIcon />, icon: <XIcon />,
text: "X", text: "X",
url: "https://x.com/aoagents", url: "https://x.com/aoagents",
external: true, external: true,
}, },
{ {
type: "icon", type: "icon",
label: "Discord", label: "Discord",
icon: <DiscordIcon />, icon: <DiscordIcon />,
text: "Discord", text: "Discord",
url: "https://discord.gg/UZv7JjxbwG", url: "https://discord.gg/UZv7JjxbwG",
external: true, external: true,
}, },
]; ];
async function GitHubStars() { async function GitHubStars() {
let stars: string | null = null; let stars: string | null = null;
try { try {
const res = await fetch( const res = await fetch("https://api.github.com/repos/ComposioHQ/agent-orchestrator", {
"https://api.github.com/repos/ComposioHQ/agent-orchestrator", next: { revalidate: 3600 },
{ next: { revalidate: 3600 } }, });
); if (res.ok) {
if (res.ok) { const data = await res.json();
const data = await res.json(); const count = data.stargazers_count as number;
const count = data.stargazers_count as number; stars = count >= 1000 ? `${(count / 1000).toFixed(1)}K` : String(count);
stars = count >= 1000 ? `${(count / 1000).toFixed(1)}K` : String(count); }
} } catch {
} catch { /* GitHub API failed — hide stars */
/* GitHub API failed — hide stars */ }
}
return stars ? ( return stars ? (
<span className="inline-flex items-center gap-1 text-[var(--color-text-muted)]"> <span className="inline-flex items-center gap-1 text-[var(--color-text-muted)]">
<svg <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
width="12" <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" />
height="12" </svg>
viewBox="0 0 24 24" {stars}
fill="none" </span>
stroke="currentColor" ) : null;
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 }) { export default function Layout({ children }: { children: ReactNode }) {
return ( return (
<RootProvider <RootProvider theme={{ enabled: true, defaultTheme: "dark" }} search={{ options: { type: "static" } }}>
theme={{ enabled: true, defaultTheme: "dark" }} <DocsLayout
search={{ options: { type: "static" } }} tree={source.pageTree}
> links={links}
<DocsLayout nav={{
tree={source.pageTree} title: (
links={links} <span className="flex items-center gap-2 font-semibold">
nav={{ <img src="/ao-logo.svg" alt="" aria-hidden="true" width={22} height={22} className="h-[22px] w-[22px]" />
title: ( <span className="text-[var(--color-text-primary)]">AO</span>
<span className="flex items-center gap-2 font-semibold"> </span>
<img ),
src="/ao-logo.svg" }}
alt="" sidebar={{
aria-hidden="true" defaultOpenLevel: 1,
width={22} collapsible: true,
height={22} banner: (
className="h-[22px] w-[22px]" <a
/> href="https://github.com/ComposioHQ/agent-orchestrator"
<span className="text-[var(--color-text-primary)]">AO</span> target="_blank"
</span> 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"
}} >
sidebar={{ <GithubIcon />
defaultOpenLevel: 1, <span>ComposioHQ/agent-orchestrator</span>
collapsible: true, <GitHubStars />
banner: ( </a>
<a ),
href="https://github.com/ComposioHQ/agent-orchestrator" }}
target="_blank" >
rel="noreferrer noopener" {children}
className="flex items-center gap-2 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors py-1" </DocsLayout>
> </RootProvider>
<GithubIcon /> );
<span>ComposioHQ/agent-orchestrator</span> }
<GitHubStars />
</a>
),
}}
>
{children}
</DocsLayout>
</RootProvider>
);
}

View File

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

View File

@ -1,32 +1,32 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Agent Orchestrator", title: "Agent Orchestrator",
description: 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.", "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: { openGraph: {
type: "website", type: "website",
url: "https://aoagents.dev/landing", url: "https://aoagents.dev/landing",
siteName: "Agent Orchestrator", siteName: "Agent Orchestrator",
title: "Agent Orchestrator", title: "Agent Orchestrator",
description: 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.", "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" }], images: [{ url: "/og-image.png", width: 1024, height: 1024, alt: "Agent Orchestrator" }],
}, },
twitter: { twitter: {
card: "summary", card: "summary",
site: "@aoagents", site: "@aoagents",
creator: "@aoagents", creator: "@aoagents",
title: "Agent Orchestrator", title: "Agent Orchestrator",
description: 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.", "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"], images: ["/og-image.png"],
}, },
alternates: { alternates: {
canonical: "https://aoagents.dev/", canonical: "https://aoagents.dev/",
}, },
}; };
export default function LandingLayout({ children }: { children: React.ReactNode }) { 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"; import { formatCompactNumber, getGitHubRepoStats } from "../../lib/github-repo";
export default async function LandingPage() { export default async function LandingPage() {
const githubStats = await getGitHubRepoStats(); const githubStats = await getGitHubRepoStats();
return ( return (
<ScrollRevealProvider> <ScrollRevealProvider>
<PageConstellation /> <PageConstellation />
<div className="relative z-10"> <div className="relative z-10">
<LandingNav /> <LandingNav />
<LandingHero starsLabel={formatCompactNumber(githubStats.stars)} /> <LandingHero starsLabel={formatCompactNumber(githubStats.stars)} />
<LandingAbout /> <LandingAbout />
<LandingAgentsBar /> <LandingAgentsBar />
<LandingFeatures /> <LandingFeatures />
<div id="workflow"><LandingWorkflow /></div> <div id="workflow">
<div id="usecases"><LandingUseCases /></div> <LandingWorkflow />
<LandingHowItWorks /> </div>
<LandingVideo /> <div id="usecases">
<LandingStats stats={githubStats} /> <LandingUseCases />
<LandingTestimonials /> </div>
<div id="quickstart"><LandingQuickStart /></div> <LandingHowItWorks />
<LandingCTA /> <LandingVideo />
<footer className="py-12 px-8 text-center text-[var(--landing-muted)] opacity-30 text-[0.8125rem] border-t border-white/[0.04]"> <LandingStats stats={githubStats} />
MIT Licensed · Open Source <LandingTestimonials />
</footer> <div id="quickstart">
</div> <LandingQuickStart />
</ScrollRevealProvider> </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"; import "../styles/globals.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Agent Orchestrator", title: "Agent Orchestrator",
description: "Open-source platform for running parallel AI coding agents.", description: "Open-source platform for running parallel AI coding agents.",
}; };
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body>{children}</body> <body>{children}</body>
</html> </html>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,85 +1,94 @@
"use client"; "use client";
function XIcon() { function XIcon() {
return ( return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <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" /> <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> </svg>
); );
} }
function DiscordIcon() { function DiscordIcon() {
return ( return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <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" /> <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> </svg>
); );
} }
function GithubIcon() { function GithubIcon() {
return ( return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <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" /> <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> </svg>
); );
} }
export function LandingNav() { export function LandingNav() {
return ( 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"> <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 <a
href="#" href="#"
className="inline-flex items-center gap-2 text-base font-semibold text-white no-underline font-sans font-[680] tracking-tight" 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" /> <img src="/ao-logo.svg" alt="" aria-hidden="true" width={28} height={28} className="h-7 w-7" />
Agent Orchestrator Agent Orchestrator
</a> </a>
<ul className="hidden md:flex items-center gap-8 list-none"> <ul className="hidden md:flex items-center gap-8 list-none">
<li> <li>
<a href="/docs" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"> <a
Docs href="/docs"
</a> className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
</li> >
<li> Docs
<a href="#features" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"> </a>
Features </li>
</a> <li>
</li> <a
<li> href="#features"
<a href="#how" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"> className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
How It Works >
</a> Features
</li> </a>
</ul> </li>
<div className="flex items-center gap-2"> <li>
<a <a
href="https://x.com/aoagents" href="#how"
target="_blank" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
rel="noopener noreferrer" >
aria-label="X (Twitter)" How It Works
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white" </a>
> </li>
<XIcon /> </ul>
</a> <div className="flex items-center gap-2">
<a <a
href="https://discord.gg/UZv7JjxbwG" href="https://x.com/aoagents"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Discord" 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" className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
> >
<DiscordIcon /> <XIcon />
</a> </a>
<a <a
href="https://github.com/ComposioHQ/agent-orchestrator" href="https://discord.gg/UZv7JjxbwG"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="GitHub" aria-label="Discord"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white" className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
> >
<GithubIcon /> <DiscordIcon />
</a> </a>
</div> <a
</nav> 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 = [ 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 01",
{ num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" }, 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() { export function LandingQuickStart() {
return ( 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%)]"> <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="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6"> <div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
Get started in 60 seconds Get started in 60 seconds
</div> </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"> <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{" "} Three commands to <em className="italic text-[var(--landing-muted)]">launch</em>
<em className="italic text-[var(--landing-muted)]">launch</em> </h2>
</h2> </div>
</div> <div className="grid grid-cols-1 md:grid-cols-3 gap-5 mt-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mt-12"> {steps.map((s) => (
{steps.map((s) => ( <div key={s.num} className="landing-reveal landing-card rounded-2xl p-7">
<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">
<div className="font-mono text-[0.625rem] tracking-[0.1em] text-[var(--landing-muted)] opacity-40 mb-3"> {s.num}
{s.num} </div>
</div> <h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight">{s.title}</h3>
<h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight"> <p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">{s.desc}</p>
{s.title} <div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg">
</h3> <span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd}
<p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4"> </div>
{s.desc} </div>
</p> ))}
<div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg"> </div>
<span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd} <div className="landing-reveal mt-8 text-center">
</div> <a
</div> 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"
</div> >
<div className="landing-reveal mt-8 text-center"> Explore docs for setup and workflows
<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"> </a>
Explore docs for setup and workflows </div>
</a> </section>
</div> );
</section>
);
} }

View File

@ -1,51 +1,46 @@
import type { GitHubRepoStats } from "@/lib/github-repo"; import type { GitHubRepoStats } from "@/lib/github-repo";
interface LandingStatsProps { interface LandingStatsProps {
stats: GitHubRepoStats; stats: GitHubRepoStats;
} }
export function LandingStats({ stats }: LandingStatsProps) { export function LandingStats({ stats }: LandingStatsProps) {
const cards = [ const cards = [
{ number: stats.stars.toLocaleString(), label: "GitHub Stars" }, { number: stats.stars.toLocaleString(), label: "GitHub Stars" },
{ number: stats.forks.toLocaleString(), label: "Forks" }, { number: stats.forks.toLocaleString(), label: "Forks" },
{ number: stats.openIssues.toLocaleString(), label: "Open Issues" }, { number: stats.openIssues.toLocaleString(), label: "Open Issues" },
{ number: stats.watchers.toLocaleString(), label: "Watchers" }, { number: stats.watchers.toLocaleString(), label: "Watchers" },
]; ];
return ( return (
<section className="py-20 px-6 max-w-[72rem] mx-auto"> <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"> <div className="landing-reveal grid grid-cols-2 md:grid-cols-4 gap-5">
{cards.map((stat) => ( {cards.map((stat) => (
<div <div key={stat.label} className="landing-card rounded-2xl py-8 px-6 text-center">
key={stat.label} <div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1">
className="landing-card rounded-2xl py-8 px-6 text-center" {stat.number}
> </div>
<div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1"> <div className="text-xs text-[var(--landing-muted)] opacity-60">{stat.label}</div>
{stat.number} </div>
</div> ))}
<div className="text-xs text-[var(--landing-muted)] opacity-60"> </div>
{stat.label} <div className="landing-reveal text-center mt-8">
</div> <a
</div> href="https://github.com/ComposioHQ/agent-orchestrator"
))} target="_blank"
</div> rel="noopener noreferrer"
<div className="landing-reveal text-center mt-8"> 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"
<a >
href="https://github.com/ComposioHQ/agent-orchestrator" <span className="text-[rgba(251,191,36,0.7)] text-sm"></span>
target="_blank" <span className="font-mono text-xs text-[var(--landing-fg)] opacity-80">{stats.stars.toLocaleString()}</span>
rel="noopener noreferrer" <span>stars on GitHub</span>
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" </a>
> <br />
<span className="text-[rgba(251,191,36,0.7)] text-sm"></span> <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="font-mono text-xs text-[var(--landing-fg)] opacity-80">{stats.stars.toLocaleString()}</span> <span className="w-2 h-2 rounded-full bg-[rgba(134,239,172,0.7)] animate-pulse" />
<span>stars on GitHub</span> Built with itself this repo is managed by Agent Orchestrator
</a> </div>
<br /> </div>
<div className="landing-card inline-flex items-center gap-2 rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)]"> </section>
<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"; import { useEffect, useState } from "react";
const testimonials = [ const testimonials = [
{ {
quote: quote: "Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.",
"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",
img: "https://i.pravatar.cc/120?img=13", name: "Staff Engineer",
name: "Staff Engineer", role: "Series B Startup",
role: "Series B Startup", },
}, {
{ quote:
quote: "The auto CI recovery alone saves me hours a week. Agents fix their own broken tests. I just review and merge.",
"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",
img: "https://i.pravatar.cc/120?img=32", name: "Solo Founder",
name: "Solo Founder", role: "Indie SaaS",
role: "Indie SaaS", },
}, {
{ quote:
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.",
"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",
img: "https://i.pravatar.cc/120?img=8", name: "Eng Lead",
name: "Eng Lead", role: "20-person team",
role: "20-person team", },
},
]; ];
const ROTATE_MS = 5500; const ROTATE_MS = 5500;
export function LandingTestimonials() { export function LandingTestimonials() {
const [active, setActive] = useState(0); const [active, setActive] = useState(0);
const [show, setShow] = useState(true); const [show, setShow] = useState(true);
const [paused, setPaused] = useState(false); const [paused, setPaused] = useState(false);
const change = (next: number) => { const change = (next: number) => {
if (next === active) return; if (next === active) return;
setShow(false); setShow(false);
window.setTimeout(() => { window.setTimeout(() => {
setActive(next); setActive(next);
setShow(true); setShow(true);
}, 240); }, 240);
}; };
useEffect(() => { useEffect(() => {
if (paused) return; if (paused) return;
const t = window.setTimeout( const t = window.setTimeout(() => change((active + 1) % testimonials.length), ROTATE_MS);
() => change((active + 1) % testimonials.length), return () => window.clearTimeout(t);
ROTATE_MS, // eslint-disable-next-line react-hooks/exhaustive-deps
); }, [active, paused]);
return () => window.clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, paused]);
const t = testimonials[active]; const t = testimonials[active];
return ( return (
<section className="py-20 px-6 pb-[120px] max-w-[72rem] mx-auto"> <section className="py-20 px-6 pb-[120px] max-w-[72rem] mx-auto">
<div className="landing-reveal"> <div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6"> <div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
What engineers say What engineers say
</div> </div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px]"> <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> Trusted by <em className="italic text-[var(--landing-muted)]">builders</em>
</h2> </h2>
</div> </div>
<div <div className="landing-reveal mt-16" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
className="landing-reveal mt-16" {/* Quote — fades on change */}
onMouseEnter={() => setPaused(true)} <div className="min-h-[8rem] max-w-[58rem]">
onMouseLeave={() => setPaused(false)} <blockquote
> style={{
{/* Quote — fades on change */} opacity: show ? 1 : 0,
<div className="min-h-[8rem] max-w-[58rem]"> transform: show ? "translateY(0)" : "translateY(8px)",
<blockquote transition: "opacity 0.4s ease, transform 0.4s cubic-bezier(0.22,1,0.36,1)",
style={{ }}
opacity: show ? 1 : 0, 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)]"
transform: show ? "translateY(0)" : "translateY(8px)", >
transition: "opacity 0.4s ease, transform 0.4s cubic-bezier(0.22,1,0.36,1)", &ldquo;{t.quote}&rdquo;
}} </blockquote>
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)]" </div>
>
&ldquo;{t.quote}&rdquo;
</blockquote>
</div>
{/* Bottom row — author cluster on the left, step counter on the right */} {/* 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 justify-between gap-6 mt-14 flex-wrap">
<div className="flex items-center gap-5"> <div className="flex items-center gap-5">
<div className="flex items-center"> <div className="flex items-center">
{testimonials.map((item, i) => { {testimonials.map((item, i) => {
const isActive = i === active; const isActive = i === active;
const size = isActive ? 56 : 44; const size = isActive ? 56 : 44;
return ( return (
<button <button
key={item.name} key={item.name}
onClick={() => change(i)} onClick={() => change(i)}
aria-label={`Show testimonial from ${item.name}`} aria-label={`Show testimonial from ${item.name}`}
aria-pressed={isActive} aria-pressed={isActive}
className="rounded-full overflow-hidden cursor-pointer shrink-0 p-0" className="rounded-full overflow-hidden cursor-pointer shrink-0 p-0"
style={{ style={{
width: size, width: size,
height: size, height: size,
marginLeft: i === 0 ? 0 : -14, marginLeft: i === 0 ? 0 : -14,
zIndex: isActive ? 30 : 10 - i, zIndex: isActive ? 30 : 10 - i,
border: `2px solid ${isActive ? "var(--landing-accent)" : "var(--landing-card-bg)"}`, border: `2px solid ${isActive ? "var(--landing-accent)" : "var(--landing-card-bg)"}`,
opacity: isActive ? 1 : 0.7, opacity: isActive ? 1 : 0.7,
boxShadow: isActive ? "0 4px 16px rgba(0,0,0,0.35)" : "none", boxShadow: isActive ? "0 4px 16px rgba(0,0,0,0.35)" : "none",
transition: 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", "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 */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={item.img} src={item.img}
alt={item.name} alt={item.name}
width={size} width={size}
height={size} height={size}
className="w-full h-full object-cover" className="w-full h-full object-cover"
style={{ style={{
filter: isActive ? "grayscale(0)" : "grayscale(1)", filter: isActive ? "grayscale(0)" : "grayscale(1)",
transition: "filter 0.4s ease", transition: "filter 0.4s ease",
}} }}
/> />
</button> </button>
); );
})} })}
</div> </div>
{/* Vertical divider */} {/* Vertical divider */}
<div <div className="w-px h-10 shrink-0" style={{ background: "var(--landing-border-default)" }} />
className="w-px h-10 shrink-0"
style={{ background: "var(--landing-border-default)" }}
/>
{/* Author — fades on change */} {/* Author — fades on change */}
<div <div
style={{ style={{
opacity: show ? 1 : 0, opacity: show ? 1 : 0,
transition: "opacity 0.4s ease 0.05s", transition: "opacity 0.4s ease 0.05s",
}} }}
> >
<div className="text-[0.9375rem] font-medium text-[var(--landing-fg)]"> <div className="text-[0.9375rem] font-medium text-[var(--landing-fg)]">{t.name}</div>
{t.name} <div className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60">{t.role}</div>
</div> </div>
<div className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60"> </div>
{t.role}
</div>
</div>
</div>
{/* Step counter — fills the right side */} {/* Step counter — fills the right side */}
<div className="flex items-baseline gap-2 font-mono"> <div className="flex items-baseline gap-2 font-mono">
<span <span
className="text-[clamp(2.25rem,5vw,3.5rem)] font-[680] tracking-tight leading-none tabular-nums" className="text-[clamp(2.25rem,5vw,3.5rem)] font-[680] tracking-tight leading-none tabular-nums"
style={{ style={{
color: "var(--landing-fg)", color: "var(--landing-fg)",
opacity: show ? 1 : 0.4, opacity: show ? 1 : 0.4,
transition: "opacity 0.3s ease", transition: "opacity 0.3s ease",
}} }}
> >
{String(active + 1).padStart(2, "0")} {String(active + 1).padStart(2, "0")}
</span> </span>
<span className="text-[var(--landing-muted)] opacity-40 text-lg"> <span className="text-[var(--landing-muted)] opacity-40 text-lg">
/ {String(testimonials.length).padStart(2, "0")} / {String(testimonials.length).padStart(2, "0")}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
); );
} }

View File

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

View File

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

View File

@ -3,153 +3,148 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
export function PageConstellation() { export function PageConstellation() {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const dpr = Math.min(window.devicePixelRatio || 1, 2); const dpr = Math.min(window.devicePixelRatio || 1, 2);
type Dot = { x: number; y: number; vx: number; vy: number }; type Dot = { x: number; y: number; vx: number; vy: number };
let dots: Dot[] = []; let dots: Dot[] = [];
let width = 0; let width = 0;
let height = 0; let height = 0;
const mouse = { x: -10000, y: -10000, active: false }; const mouse = { x: -10000, y: -10000, active: false };
const seed = () => { const seed = () => {
const target = Math.min(140, Math.max(40, Math.floor((width * height) / 18000))); const target = Math.min(140, Math.max(40, Math.floor((width * height) / 18000)));
dots = Array.from({ length: target }, () => ({ dots = Array.from({ length: target }, () => ({
x: Math.random() * width, x: Math.random() * width,
y: Math.random() * height, y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.1, vx: (Math.random() - 0.5) * 0.1,
vy: (Math.random() - 0.5) * 0.1, vy: (Math.random() - 0.5) * 0.1,
})); }));
}; };
const resize = () => { const resize = () => {
width = window.innerWidth; width = window.innerWidth;
height = window.innerHeight; height = window.innerHeight;
canvas.width = Math.floor(width * dpr); canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr); canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`; canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`; canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
seed(); seed();
}; };
resize(); resize();
const onMove = (e: MouseEvent) => { const onMove = (e: MouseEvent) => {
mouse.x = e.clientX; mouse.x = e.clientX;
mouse.y = e.clientY; mouse.y = e.clientY;
mouse.active = true; mouse.active = true;
}; };
const onLeave = () => { const onLeave = () => {
mouse.active = false; mouse.active = false;
mouse.x = -10000; mouse.x = -10000;
mouse.y = -10000; mouse.y = -10000;
}; };
window.addEventListener("resize", resize); window.addEventListener("resize", resize);
window.addEventListener("mousemove", onMove); window.addEventListener("mousemove", onMove);
document.addEventListener("mouseleave", onLeave); document.addEventListener("mouseleave", onLeave);
window.addEventListener("blur", onLeave); window.addEventListener("blur", onLeave);
const LINK_DIST = 105; const LINK_DIST = 105;
const MOUSE_DIST = 165; const MOUSE_DIST = 165;
const draw = () => { const draw = () => {
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
for (const d of dots) { for (const d of dots) {
d.x += d.vx; d.x += d.vx;
d.y += d.vy; d.y += d.vy;
if (d.x < 0 || d.x > width) d.vx *= -1; if (d.x < 0 || d.x > width) d.vx *= -1;
if (d.y < 0 || d.y > height) d.vy *= -1; if (d.y < 0 || d.y > height) d.vy *= -1;
} }
ctx.lineWidth = 0.5; ctx.lineWidth = 0.5;
for (let i = 0; i < dots.length; i++) { for (let i = 0; i < dots.length; i++) {
for (let j = i + 1; j < dots.length; j++) { for (let j = i + 1; j < dots.length; j++) {
const a = dots[i]; const a = dots[i];
const b = dots[j]; const b = dots[j];
const dx = a.x - b.x; const dx = a.x - b.x;
const dy = a.y - b.y; const dy = a.y - b.y;
const distSq = dx * dx + dy * dy; const distSq = dx * dx + dy * dy;
if (distSq < LINK_DIST * LINK_DIST) { if (distSq < LINK_DIST * LINK_DIST) {
const dist = Math.sqrt(distSq); const dist = Math.sqrt(distSq);
const op = (1 - dist / LINK_DIST) * 0.05; const op = (1 - dist / LINK_DIST) * 0.05;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`; ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(a.x, a.y); ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y); ctx.lineTo(b.x, b.y);
ctx.stroke(); ctx.stroke();
} }
} }
} }
if (mouse.active) { if (mouse.active) {
ctx.lineWidth = 0.6; ctx.lineWidth = 0.6;
for (const d of dots) { for (const d of dots) {
const dx = d.x - mouse.x; const dx = d.x - mouse.x;
const dy = d.y - mouse.y; const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy; const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) { if (distSq < MOUSE_DIST * MOUSE_DIST) {
const dist = Math.sqrt(distSq); const dist = Math.sqrt(distSq);
const op = (1 - dist / MOUSE_DIST) * 0.15; const op = (1 - dist / MOUSE_DIST) * 0.15;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`; ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(d.x, d.y); ctx.moveTo(d.x, d.y);
ctx.lineTo(mouse.x, mouse.y); ctx.lineTo(mouse.x, mouse.y);
ctx.stroke(); ctx.stroke();
} }
} }
} }
for (const d of dots) { for (const d of dots) {
let op = 0.11; let op = 0.11;
if (mouse.active) { if (mouse.active) {
const dx = d.x - mouse.x; const dx = d.x - mouse.x;
const dy = d.y - mouse.y; const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy; const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) { if (distSq < MOUSE_DIST * MOUSE_DIST) {
op += (1 - Math.sqrt(distSq) / MOUSE_DIST) * 0.22; op += (1 - Math.sqrt(distSq) / MOUSE_DIST) * 0.22;
} }
} }
ctx.fillStyle = `rgba(240, 236, 232, ${op})`; ctx.fillStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath(); ctx.beginPath();
ctx.arc(d.x, d.y, 1.0, 0, Math.PI * 2); ctx.arc(d.x, d.y, 1.0, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
} }
}; };
let raf = 0; let raf = 0;
const loop = () => { const loop = () => {
draw(); draw();
raf = requestAnimationFrame(loop); raf = requestAnimationFrame(loop);
}; };
if (reducedMotion) draw(); if (reducedMotion) draw();
else loop(); else loop();
return () => { return () => {
cancelAnimationFrame(raf); cancelAnimationFrame(raf);
window.removeEventListener("resize", resize); window.removeEventListener("resize", resize);
window.removeEventListener("mousemove", onMove); window.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseleave", onLeave); document.removeEventListener("mouseleave", onLeave);
window.removeEventListener("blur", onLeave); window.removeEventListener("blur", onLeave);
}; };
}, []); }, []);
return ( return (
<canvas <canvas ref={canvasRef} className="fixed inset-0 pointer-events-none" style={{ zIndex: 0 }} aria-hidden="true" />
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"; import { useEffect } from "react";
export function ScrollRevealProvider({ children }: { children: React.ReactNode }) { export function ScrollRevealProvider({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.classList.add("visible"); entry.target.classList.add("visible");
} }
}); });
}, },
{ threshold: 0.1, rootMargin: "-50px" } { threshold: 0.1, rootMargin: "-50px" },
); );
document.querySelectorAll(".landing-reveal").forEach((el) => { document.querySelectorAll(".landing-reveal").forEach((el) => {
observer.observe(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"; import { DocsBody, DocsPage } from "fumadocs-ui/page";
export function DocsMissingPage() { export function DocsMissingPage() {
return ( return (
<DocsPage <DocsPage
toc={[]} toc={[]}
tableOfContent={{ tableOfContent={{
enabled: false, enabled: false,
}} }}
breadcrumb={{ breadcrumb={{
enabled: true, enabled: true,
includePage: false, includePage: false,
}} }}
footer={{ footer={{
enabled: false, enabled: false,
}} }}
> >
<DocsBody> <DocsBody>
<div className="not-prose docs-missing-wrap"> <div className="not-prose docs-missing-wrap">
<div className="docs-missing-card"> <div className="docs-missing-card">
<div className="docs-missing-label"> <div className="docs-missing-label">docs / checkout failed</div>
docs / checkout failed <div className="docs-missing-content">
</div> <section className="docs-missing-copy">
<div className="docs-missing-content"> <h2>This page checked out the wrong worktree.</h2>
<section className="docs-missing-copy"> <p>
<h2> The docs were rebuilt, and this URL did not survive the merge. Start from the docs index, or use
This page checked out the wrong worktree. search in the sidebar to find where it landed.
</h2> </p>
<p> <div className="docs-missing-actions">
The docs were rebuilt, and this URL did not survive the merge. Start from the docs <Link href="/docs" className="docs-missing-primary">
index, or use search in the sidebar to find where it landed. Browse docs
</p> </Link>
<div className="docs-missing-actions"> <Link href="/" className="docs-missing-secondary">
<Link Home
href="/docs" </Link>
className="docs-missing-primary" </div>
> </section>
Browse docs <div className="docs-missing-terminal">
</Link> <div className="docs-missing-dots">
<Link <span className="docs-missing-dot-red" />
href="/" <span className="docs-missing-dot-yellow" />
className="docs-missing-secondary" <span className="docs-missing-dot-green" />
> </div>
Home <p className="docs-missing-command">$ ao docs resolve</p>
</Link> <p className="docs-missing-status">status: missing</p>
</div> <p>next: /docs</p>
</section> </div>
<div className="docs-missing-terminal"> </div>
<div className="docs-missing-dots"> </div>
<span className="docs-missing-dot-red" /> </div>
<span className="docs-missing-dot-yellow" /> </DocsBody>
<span className="docs-missing-dot-green" /> </DocsPage>
</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/. */ /** Aliases: request name -> filename under /docs/logos/. */
const ALIASES: Record<string, string> = { const ALIASES: Record<string, string> = {
claude: "claude-code", claude: "claude-code",
chatgpt: "openai", chatgpt: "openai",
}; };
/** Extension for each logo file. Defaults to svg; override for raster assets. */ /** Extension for each logo file. Defaults to svg; override for raster assets. */
const LOGO_EXT: Record<string, string> = { const LOGO_EXT: Record<string, string> = {
aider: "png", aider: "png",
}; };
/** Brands with a real asset file under /public/docs/logos/. */ /** Brands with a real asset file under /public/docs/logos/. */
const FILE_LOGOS = new Set([ const FILE_LOGOS = new Set([
"aider", "aider",
"anthropic", "anthropic",
"apple", "apple",
"claude-code", "claude-code",
"codex", "codex",
"composio", "composio",
"cursor", "cursor",
"discord", "discord",
"docker", "docker",
"github", "github",
"gitlab", "gitlab",
"iterm2", "iterm2",
"linear", "linear",
"linux", "linux",
"microsoft", "microsoft",
"openai", "openai",
"openclaw", "openclaw",
"opencode", "opencode",
"slack", "slack",
"tmux", "tmux",
"web", "web",
"webhook", "webhook",
"windows", "windows",
]); ]);
export interface LogoProps { export interface LogoProps {
/** Brand name (case-insensitive). */ /** Brand name (case-insensitive). */
name: string; name: string;
/** Size in pixels. Default 20. */ /** Size in pixels. Default 20. */
size?: number; size?: number;
className?: string; className?: string;
/** Accepted for backwards compat with existing MDX; no-op. */ /** Accepted for backwards compat with existing MDX; no-op. */
color?: boolean; color?: boolean;
} }
export function Logo({ name, size = 20, className }: LogoProps) { export function Logo({ name, size = 20, className }: LogoProps) {
const key = name.toLowerCase(); const key = name.toLowerCase();
const resolved = ALIASES[key] ?? key; const resolved = ALIASES[key] ?? key;
const baseStyle: CSSProperties = { flexShrink: 0, width: size, height: size }; const baseStyle: CSSProperties = { flexShrink: 0, width: size, height: size };
if (FILE_LOGOS.has(resolved)) { if (FILE_LOGOS.has(resolved)) {
const ext = LOGO_EXT[resolved] ?? "svg"; const ext = LOGO_EXT[resolved] ?? "svg";
return ( return (
<img <img
src={`/docs/logos/${resolved}.${ext}`} src={`/docs/logos/${resolved}.${ext}`}
alt="" alt=""
aria-hidden="true" aria-hidden="true"
className={className} className={className}
style={baseStyle} style={baseStyle}
width={size} width={size}
height={size} height={size}
/> />
); );
} }
const initial = name.charAt(0).toUpperCase(); const initial = name.charAt(0).toUpperCase();
return ( return (
<span <span
className={className} className={className}
style={{ style={{
...baseStyle, ...baseStyle,
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
borderRadius: size * 0.22, borderRadius: size * 0.22,
backgroundColor: "var(--color-bg-elevated, #2a2827)", backgroundColor: "var(--color-bg-elevated, #2a2827)",
color: "#ffffff", color: "#ffffff",
fontSize: size * 0.55, fontSize: size * 0.55,
fontWeight: 700, fontWeight: 700,
lineHeight: 1, lineHeight: 1,
fontFamily: "var(--font-geist-sans), ui-sans-serif, system-ui", fontFamily: "var(--font-geist-sans), ui-sans-serif, system-ui",
}} }}
aria-hidden="true" aria-hidden="true"
> >
{initial} {initial}
</span> </span>
); );
} }

View File

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

View File

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

View File

@ -10,28 +10,28 @@ import { PlatformSupport } from "./PlatformSupport";
import { PluginCard, PluginGrid } from "./PluginCard"; import { PluginCard, PluginGrid } from "./PluginCard";
function Accordions({ children }: { children: ReactNode }) { 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 }) { function Accordion({ title, children }: { title: ReactNode; children: ReactNode }) {
return ( return (
<details className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-4 py-3"> <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)]"> <summary className="cursor-pointer list-none text-sm font-medium text-[var(--color-text-primary)]">
{title} {title}
</summary> </summary>
<div className="mt-3 text-sm text-[var(--color-text-secondary)]">{children}</div> <div className="mt-3 text-sm text-[var(--color-text-secondary)]">{children}</div>
</details> </details>
); );
} }
export function getMDXComponents(): MDXComponents { export function getMDXComponents(): MDXComponents {
return { return {
...defaultMdxComponents, ...defaultMdxComponents,
Accordion, Accordion,
Accordions, Accordions,
Logo, Logo,
PlatformSupport, PlatformSupport,
PluginCard, PluginCard,
PluginGrid, 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. 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 | | Slot | Default | Purpose | Interface |
|------|---------|---------|-----------| | --------- | ----------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
| Runtime | [tmux](/docs/plugins/runtimes/tmux) | Where agent sessions execute (tmux, process, docker, k8s) | `Runtime` | | 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` | | 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` | | 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` | | 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` | | 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` | | 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` | | 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` | | Lifecycle | core (non-pluggable) | State machine, poll loop, and reaction engine | `LifecycleManager` |
<Callout type="info"> <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> </Callout>
--- ---
@ -56,25 +58,25 @@ pr_open ────────────────────────
Terminal statuses (session is dead and will no longer be polled): `killed`, `terminated`, `done`, `cleanup`, `errored`, `merged`. Terminal statuses (session is dead and will no longer be polled): `killed`, `terminated`, `done`, `cleanup`, `errored`, `merged`.
| Status | Description | | Status | Description |
|--------|-------------| | ------------------- | ----------------------------------------------------------------------------- |
| `spawning` | Session is being created — worktree, branch, and tmux window are initialising | | `spawning` | Session is being created — worktree, branch, and tmux window are initialising |
| `working` | Agent is active; no PR yet | | `working` | Agent is active; no PR yet |
| `pr_open` | Agent has pushed a PR; CI and reviews are pending | | `pr_open` | Agent has pushed a PR; CI and reviews are pending |
| `ci_failed` | One or more CI checks on the PR are failing | | `ci_failed` | One or more CI checks on the PR are failing |
| `review_pending` | PR has been submitted for review; waiting for a decision | | `review_pending` | PR has been submitted for review; waiting for a decision |
| `changes_requested` | Reviewer(s) have requested changes | | `changes_requested` | Reviewer(s) have requested changes |
| `approved` | PR is approved but not yet mergeable (e.g. still behind base) | | `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 | | `mergeable` | PR is approved, CI is green, and it can be merged |
| `merged` | PR has been merged (terminal) | | `merged` | PR has been merged (terminal) |
| `cleanup` | Post-merge cleanup in progress (terminal) | | `cleanup` | Post-merge cleanup in progress (terminal) |
| `done` | Session completed cleanly (terminal) | | `done` | Session completed cleanly (terminal) |
| `needs_input` | Agent is waiting for a permission prompt or human input | | `needs_input` | Agent is waiting for a permission prompt or human input |
| `stuck` | Agent has been idle beyond the configured `agent-stuck` threshold | | `stuck` | Agent has been idle beyond the configured `agent-stuck` threshold |
| `errored` | Unexpected error — session is dead (terminal) | | `errored` | Unexpected error — session is dead (terminal) |
| `killed` | Session was explicitly killed or the PR was closed (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 | | `idle` | Agent process is alive but has not produced activity for an extended period |
| `terminated` | Session was terminated externally (terminal) | | `terminated` | Session was terminated externally (terminal) |
### How transitions are determined ### 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` - **warning** — events containing `fail`, `changes_requested`, or `conflicts`
- **info** — everything else, including all `summary.*` events - **info** — everything else, including all `summary.*` events
| `event.type` | Priority | When emitted | | `event.type` | Priority | When emitted |
|---|---|---| | ---------------------------- | -------- | -------------------------------------------------- |
| `session.spawned` | info | Session transitions out of `spawning` | | `session.spawned` | info | Session transitions out of `spawning` |
| `session.working` | info | Session enters `working` | | `session.working` | info | Session enters `working` |
| `session.exited` | info | Agent process exits | | `session.exited` | info | Agent process exits |
| `session.killed` | info | Session is killed | | `session.killed` | info | Session is killed |
| `session.idle` | info | Session enters `idle` | | `session.idle` | info | Session enters `idle` |
| `session.stuck` | urgent | Session exceeds the `agent-stuck` threshold | | `session.stuck` | urgent | Session exceeds the `agent-stuck` threshold |
| `session.needs_input` | urgent | Agent is waiting on a permission prompt | | `session.needs_input` | urgent | Agent is waiting on a permission prompt |
| `session.errored` | urgent | Session enters `errored` | | `session.errored` | urgent | Session enters `errored` |
| `pr.created` | info | Session transitions to `pr_open` | | `pr.created` | info | Session transitions to `pr_open` |
| `pr.updated` | info | PR title or state changes | | `pr.updated` | info | PR title or state changes |
| `pr.merged` | action | PR is merged | | `pr.merged` | action | PR is merged |
| `pr.closed` | info | PR is closed without merging | | `pr.closed` | info | PR is closed without merging |
| `ci.passing` | action | CI checks recover from failing to passing | | `ci.passing` | action | CI checks recover from failing to passing |
| `ci.failing` | warning | Session enters `ci_failed` | | `ci.failing` | warning | Session enters `ci_failed` |
| `ci.fix_sent` | info | CI fix message sent to agent | | `ci.fix_sent` | info | CI fix message sent to agent |
| `ci.fix_failed` | warning | CI fix attempt failed | | `ci.fix_failed` | warning | CI fix attempt failed |
| `review.pending` | info | Session enters `review_pending` | | `review.pending` | info | Session enters `review_pending` |
| `review.approved` | action | Session enters `approved` | | `review.approved` | action | Session enters `approved` |
| `review.changes_requested` | warning | Session enters `changes_requested` | | `review.changes_requested` | warning | Session enters `changes_requested` |
| `review.comments_sent` | info | Review comments forwarded to agent | | `review.comments_sent` | info | Review comments forwarded to agent |
| `review.comments_unresolved` | warning | Unresolved review comments still present | | `review.comments_unresolved` | warning | Unresolved review comments still present |
| `automated_review.found` | warning | Bot/automated review comments detected | | `automated_review.found` | warning | Bot/automated review comments detected |
| `automated_review.fix_sent` | info | Automated review fix sent to agent | | `automated_review.fix_sent` | info | Automated review fix sent to agent |
| `merge.ready` | action | Session enters `mergeable` | | `merge.ready` | action | Session enters `mergeable` |
| `merge.conflicts` | warning | PR has merge conflicts | | `merge.conflicts` | warning | PR has merge conflicts |
| `merge.completed` | action | Session enters `merged` | | `merge.completed` | action | Session enters `merged` |
| `reaction.triggered` | info | A configured reaction fired | | `reaction.triggered` | info | A configured reaction fired |
| `reaction.escalated` | urgent | A reaction exceeded its retry/escalation threshold | | `reaction.escalated` | urgent | A reaction exceeded its retry/escalation threshold |
| `summary.all_complete` | info | All sessions have reached terminal statuses | | `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). 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 ### The 6 activity states
| State | Meaning | When | | State | Meaning | When |
|---|---|---| | --------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| `active` | Agent is processing — thinking, writing code, running tools | Activity within the last 30 seconds | | `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 | | `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) | | `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 | | `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 | | `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 | | `exited` | Agent process is no longer running | `isProcessRunning` returns false |
### The `getActivityState` cascade ### The `getActivityState` cascade
@ -237,23 +239,25 @@ Every agent plugin must implement this cascade in order:
``` ```
<Callout type="warn"> <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> </Callout>
### Two JSONL patterns ### Two JSONL patterns
| Pattern | Used by | How it works | | 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. | | **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. | | **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 ### Thresholds
| Constant | Value | Purpose | | Constant | Value | Purpose |
|---|---|---| | ----------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_ACTIVE_WINDOW_MS` | 30 seconds | Activity newer than this is `active`; older is `ready` | | `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` | | `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. | | `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). 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: The wrappers intercept:
- `gh pr create` — captures the PR URL from stdout and writes `pr=<url>` and `status=pr_open` - `gh pr create` — captures the PR URL from stdout and writes `pr=<url>` and `status=pr_open`
- `gh pr merge` — writes `status=merged` - `gh pr merge` — writes `status=merged`
- `git checkout -b <branch>` / `git switch -c <branch>` — writes `branch=<name>` - `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 ## Next Steps
<Cards> <Cards>
<Card title="Project Configuration" href="/docs/configuration/projects" description="Configure projects, agents, trackers, and reactions in agent-orchestrator.yaml." /> <Card
<Card title="Authoring Plugins" href="/docs/plugins/authoring" description="Build custom runtime, agent, tracker, SCM, notifier, or terminal plugins." /> title="Project Configuration"
<Card title="Reactions" href="/docs/configuration/reactions" description="Set up automated reactions to CI failures, review comments, and merge events." /> href="/docs/configuration/projects"
<Card title="Configuration" href="/docs/configuration" description="Understand the global registry, local project config, and where AO stores runtime data." /> 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> </Cards>

View File

@ -37,13 +37,13 @@ ao
> Start orchestrator agent and dashboard (auto-creates config on first run, adds projects by path/URL) > Start orchestrator agent and dashboard (auto-creates config on first run, adds projects by path/URL)
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ------------------- | ------- | ------------------------------------------------------------------------------- |
| `--no-dashboard` | — | Skip starting the dashboard server | | `--no-dashboard` | — | Skip starting the dashboard server |
| `--no-orchestrator` | — | Skip starting the orchestrator agent | | `--no-orchestrator` | — | Skip starting the orchestrator agent |
| `--rebuild` | — | Clean and rebuild the dashboard before starting | | `--rebuild` | — | Clean and rebuild the dashboard before starting |
| `--dev` | — | Use Next.js dev server with hot reload (monorepo only; ignored in npm installs) | | `--dev` | — | Use Next.js dev server with hot reload (monorepo only; ignored in npm installs) |
| `--interactive` | — | Prompt to configure config settings | | `--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`. 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 > Stop orchestrator agent and dashboard
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ----------------- | ------- | ------------------------------------------------ |
| `--purge-session` | — | Delete the mapped OpenCode session when stopping | | `--purge-session` | — | Delete the mapped OpenCode session when stopping |
| `--all` | — | Stop all running AO instances on this machine | | `--all` | — | Stop all running AO instances on this machine |
### `ao status` ### `ao status`
> Show all sessions with branch, activity, PR, and CI status > Show all sessions with branch, activity, PR, and CI status
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ---------------------- | ------- | --------------------- |
| `-p, --project <id>` | — | Filter to one project | | `-p, --project <id>` | — | Filter to one project |
| `--json` | — | Output JSON | | `--json` | — | Output JSON |
| `-w, --watch` | — | Refresh continuously | | `-w, --watch` | — | Refresh continuously |
| `--interval <seconds>` | `5` | Refresh interval | | `--interval <seconds>` | `5` | Refresh interval |
`--watch` and `--json` are mutually exclusive. Falls back to tmux session discovery if no config is found. `--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 > Spawn a single agent session
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | -------------------- | -------------- | --------------------------------------------------------------------------------- |
| `--open` | — | Open the session in a terminal tab | | `--open` | — | Open the session in a terminal tab |
| `--agent <name>` | config default | Override the agent plugin (`claude-code`, `codex`, `cursor`, `aider`, `opencode`) | | `--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 | | `--claim-pr <pr>` | — | Immediately claim an existing PR for the spawned session |
| `--assign-on-github` | — | Assign the claimed PR to the authenticated GitHub user | | `--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) | | `--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`. Positional `[first]` — issue identifier. The project is auto-detected from `cwd`.
<Callout type="warn"> <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> </Callout>
### `ao batch-spawn <issues...>` ### `ao batch-spawn <issues...>`
> Spawn sessions for multiple issues with duplicate detection > Spawn sessions for multiple issues with duplicate detection
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | -------- | ------- | ------------------------------ |
| `--open` | — | Open sessions in terminal tabs | | `--open` | — | Open sessions in terminal tabs |
Skips duplicates within the batch and any issues that already have an active session. 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 > Send a message to a session with busy detection and retry
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | --------------------- | ------- | -------------------------------------------------------- |
| `-f, --file <path>` | — | Send contents of a file instead of an inline message | | `-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 | | `--no-wait` | — | Don't wait for the session to become idle before sending |
| `--timeout <seconds>` | `600` | Max seconds to wait for idle | | `--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. 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 > Check PRs for review comments and trigger agents to address them
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ----------- | ------- | ------------------------------------------ |
| `--dry-run` | — | Show what would happen; don't nudge agents | | `--dry-run` | — | Show what would happen; don't nudge agents |
Checks all projects if `[project]` is omitted. Checks all projects if `[project]` is omitted.
@ -125,11 +126,11 @@ Checks all projects if `[project]` is omitted.
> Start the web dashboard > Start the web dashboard
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ------------------- | ----------------------- | ----------------------------------------- |
| `-p, --port <port>` | config `port` or `3000` | Port to listen on | | `-p, --port <port>` | config `port` or `3000` | Port to listen on |
| `--no-open` | — | Don't open the browser | | `--no-open` | — | Don't open the browser |
| `--rebuild` | — | Clean stale Next.js artifacts and rebuild | | `--rebuild` | — | Clean stale Next.js artifacts and rebuild |
If the build looks stale you'll see a suggestion to run `ao dashboard --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 > Open session(s) in terminal tabs
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ------------------ | ------- | ---------------------------------------------- |
| `-w, --new-window` | — | Open in a new terminal window instead of a tab | | `-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). 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 > Mark an issue as verified (or failed) after checking the fix on staging
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | --------------------- | ------- | ------------------------------------------------ |
| `-p, --project <id>` | — | Required if multiple projects | | `-p, --project <id>` | — | Required if multiple projects |
| `--fail` | — | Mark as failed instead of passing | | `--fail` | — | Mark as failed instead of passing |
| `-c, --comment <msg>` | — | Custom comment to add | | `-c, --comment <msg>` | — | Custom comment to add |
| `-l, --list` | — | List all issues with the merged-unverified label | | `-l, --list` | — | List all issues with the merged-unverified label |
`[issue]` is required unless `--list` is passed. `[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 > Run install, environment, and runtime health checks
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | --------------- | ------- | --------------------------------------------------------- |
| `--fix` | — | Apply safe fixes for launcher and stale-temp issues | | `--fix` | — | Apply safe fixes for launcher and stale-temp issues |
| `--test-notify` | — | Send a test notification through each configured notifier | | `--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. 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 > Check for updates and upgrade AO to the latest version
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | -------------- | ------- | ------------------------------------------------------------------ |
| `--skip-smoke` | — | Skip smoke tests after rebuilding (git installs only) | | `--skip-smoke` | — | Skip smoke tests after rebuilding (git installs only) |
| `--smoke-only` | — | Run smoke tests without fetching or 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 | | `--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). 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 > Send a manual demo notification without spawning sessions
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | -------------------- | ------- | ------------------------------------------------------------- |
| `--template <name>` | `basic` | Demo template to send | | `--template <name>` | `basic` | Demo template to send |
| `--to <refs>` | — | Comma-separated notifier refs to target | | `--to <refs>` | — | Comma-separated notifier refs to target |
| `--all` | — | Send to all configured, default, and routed notifier refs | | `--all` | — | Send to all configured, default, and routed notifier refs |
| `--route <priority>` | — | Send through `urgent`, `action`, `warning`, or `info` routing | | `--route <priority>` | — | Send through `urgent`, `action`, `warning`, or `info` routing |
| `--actions` | — | Include demo actions when supported | | `--actions` | — | Include demo actions when supported |
| `--message <text>` | — | Override the notification message | | `--message <text>` | — | Override the notification message |
| `--dry-run` | — | Resolve targets without sending | | `--dry-run` | — | Resolve targets without sending |
| `--json` | — | Print structured JSON output | | `--json` | — | Print structured JSON output |
| `--sink [port]` | — | Add a local webhook target named `sink` | | `--sink [port]` | — | Add a local webhook target named `sink` |
## Session subcommands ## Session subcommands
@ -209,11 +210,11 @@ No flags.
> List all sessions > List all sessions
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | -------------------- | ------- | ----------------------------- |
| `-p, --project <id>` | — | Filter to one project | | `-p, --project <id>` | — | Filter to one project |
| `-a, --all` | — | Include orchestrator sessions | | `-a, --all` | — | Include orchestrator sessions |
| `--json` | — | Output JSON | | `--json` | — | Output JSON |
### `ao session attach <session>` ### `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 > Kill a session and remove its worktree
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ----------------- | ------- | ---------------------------------- |
| `--purge-session` | — | Delete the mapped OpenCode session | | `--purge-session` | — | Delete the mapped OpenCode session |
### `ao session cleanup` ### `ao session cleanup`
> Kill sessions where PR is merged or issue is closed > Kill sessions where PR is merged or issue is closed
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | -------------------- | ------- | ----------------------------- |
| `-p, --project <id>` | — | Filter to one project | | `-p, --project <id>` | — | Filter to one project |
| `--dry-run` | — | Show what would be cleaned up | | `--dry-run` | — | Show what would be cleaned up |
### `ao session claim-pr <pr> [session]` ### `ao session claim-pr <pr> [session]`
> Attach an existing PR to a session > Attach an existing PR to a session
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | -------------------- | ------- | ------------------------------------------------- |
| `--assign-on-github` | — | Assign the PR to the authenticated user on GitHub | | `--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. 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 > Re-discover and persist OpenCode session mapping for an AO session
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ------------- | ------- | ------------------------ |
| `-f, --force` | — | Override a stale mapping | | `-f, --force` | — | Override a stale mapping |
## Setup subcommands ## 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 mode, AO asks which notification priorities the notifier should receive before
writing `notificationRouting`. writing `notificationRouting`.
| Command | Purpose | | Command | Purpose |
|---|---| | ------------------------------- | ------------------------------------------------------ |
| `ao setup dashboard` | Configure dashboard notification retention and routing | | `ao setup dashboard` | Configure dashboard notification retention and routing |
| `ao setup desktop` | Configure native desktop notifications | | `ao setup desktop` | Configure native desktop notifications |
| `ao setup webhook` | Configure a generic HTTP webhook | | `ao setup webhook` | Configure a generic HTTP webhook |
| `ao setup slack` | Configure a Slack incoming webhook | | `ao setup slack` | Configure a Slack incoming webhook |
| `ao setup discord` | Configure a Discord incoming webhook | | `ao setup discord` | Configure a Discord incoming webhook |
| `ao setup composio` | Open the interactive Composio setup hub | | `ao setup composio` | Open the interactive Composio setup hub |
| `ao setup composio-slack` | Configure Slack through Composio | | `ao setup composio-slack` | Configure Slack through Composio |
| `ao setup composio-discord` | Configure Discord webhook mode 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-discord-bot` | Configure Discord bot mode through Composio |
| `ao setup composio-mail` | Configure Gmail through Composio | | `ao setup composio-mail` | Configure Gmail through Composio |
| `ao setup openclaw` | Configure OpenClaw gateway notifications | | `ao setup openclaw` | Configure OpenClaw gateway notifications |
### `ao setup openclaw` ### `ao setup openclaw`
> Connect AO notifications to an OpenClaw gateway > Connect AO notifications to an OpenClaw gateway
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ------------------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
| `--url <url>` | — | OpenClaw webhook URL | | `--url <url>` | — | OpenClaw webhook URL |
| `--token <token>` | — | Remote/manual fallback; local setup should read `hooks.token` from OpenClaw config | | `--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` | | `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
| `--routing-preset <preset>` | — | `urgent-only` · `urgent-action` · `all` | | `--routing-preset <preset>` | — | `urgent-only` · `urgent-action` · `all` |
| `--refresh` | — | Reuse existing values and rewrite AO config | | `--refresh` | — | Reuse existing values and rewrite AO config |
| `--no-test` | — | Skip the setup token probe | | `--no-test` | — | Skip the setup token probe |
| `--force` | — | Replace a conflicting `notifiers.openclaw` entry | | `--force` | — | Replace a conflicting `notifiers.openclaw` entry |
| `--status` | — | Show OpenClaw config, gateway state, and token probe status | | `--status` | — | Show OpenClaw config, gateway state, and token probe status |
| `--non-interactive` | — | Skip prompts — auto-detect OpenClaw on localhost | | `--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. 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 > List bundled marketplace plugins
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | --------------- | ------- | ----------------------------------------------------------------------------------------- |
| `--installed` | — | Only show plugins present in your current config | | `--installed` | — | Only show plugins present in your current config |
| `--type <slot>` | — | Filter by slot: `agent`, `runtime`, `tracker`, `scm`, `notifier`, `workspace`, `terminal` | | `--type <slot>` | — | Filter by slot: `agent`, `runtime`, `tracker`, `scm`, `notifier`, `workspace`, `terminal` |
| `--refresh` | — | Re-fetch the marketplace catalog | | `--refresh` | — | Re-fetch the marketplace catalog |
### `ao plugin search <query>` ### `ao plugin search <query>`
@ -323,24 +324,24 @@ No flags.
> Scaffold a new AO plugin package > Scaffold a new AO plugin package
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ----------------------- | ------- | ------------------------------- |
| `--name <name>` | prompt | Plugin name | | `--name <name>` | prompt | Plugin name |
| `--slot <slot>` | prompt | Which slot it implements | | `--slot <slot>` | prompt | Which slot it implements |
| `--description <text>` | prompt | Manifest description | | `--description <text>` | prompt | Manifest description |
| `--author <name>` | prompt | Package author | | `--author <name>` | prompt | Package author |
| `--package-name <name>` | derived | npm package name | | `--package-name <name>` | derived | npm package name |
| `--non-interactive` | — | Use all flags without prompting | | `--non-interactive` | — | Use all flags without prompting |
### `ao plugin install <reference>` ### `ao plugin install <reference>`
> Install a plugin into the current config > Install a plugin into the current config
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ------------------------------- | --------------------------- | ----------------------------------------------------------------- |
| `--url <url>` | — | Passed to `ao setup openclaw` when installing `notifier-openclaw` | | `--url <url>` | — | Passed to `ao setup openclaw` when installing `notifier-openclaw` |
| `--token <token>` | — | Remote/manual OpenClaw token fallback | | `--token <token>` | — | Remote/manual OpenClaw token fallback |
| `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` | | `--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. `<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 > Update installer-managed plugins in the current config
| Flag | Default | Purpose | | Flag | Default | Purpose |
|---|---|---| | ------- | ------- | -------------------------- |
| `--all` | — | Update all managed plugins | | `--all` | — | Update all managed plugins |
Requires either `[reference]` or `--all`. Requires either `[reference]` or `--all`.
@ -364,16 +365,16 @@ No flags.
A few env vars affect every command: A few env vars affect every command:
| Variable | Purpose | | Variable | Purpose |
|---|---| | ----------------------------------- | ------------------------------------------------------------------------------------ |
| `AO_DATA_DIR` | Override the `~/.agent-orchestrator` base directory. | | `AO_DATA_DIR` | Override the `~/.agent-orchestrator` base directory. |
| `AO_LOG_LEVEL` | `error` · `warn` · `info` · `debug` · `trace` | | `AO_LOG_LEVEL` | `error` · `warn` · `info` · `debug` · `trace` |
| `AO_CONFIG_PATH` | Absolute path to a specific `agent-orchestrator.yaml`. Overrides CWD-based search. | | `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_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. | | `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`. | | `PORT` | Next.js dashboard port. Overrides `config.port`. |
| `TERMINAL_PORT` | WebSocket mux server port (default 14800). | | `TERMINAL_PORT` | WebSocket mux server port (default 14800). |
| `DIRECT_TERMINAL_PORT` | Direct PTY WebSocket port (default 14801). | | `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). | | `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). 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. 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"> <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> </Callout>
## What AO Creates ## What AO Creates
@ -72,15 +74,15 @@ agentRules: |
## What To Edit ## What To Edit
| Goal | Edit | | Goal | Edit |
|------|------| | ------------------------------------------------ | ---------------------------------------------- |
| Change the default agent or model | Local `agent-orchestrator.yaml` | | Change the default agent or model | Local `agent-orchestrator.yaml` |
| Add `.env` or tool config files to each worktree | Local `symlinks` | | Add `.env` or tool config files to each worktree | Local `symlinks` |
| Run setup commands before the agent starts | Local `postCreate` | | Run setup commands before the agent starts | Local `postCreate` |
| Disable or tune automated recovery | Local or global `reactions` | | Disable or tune automated recovery | Local or global `reactions` |
| Change notification routing for all projects | Global registry | | Change notification routing for all projects | Global registry |
| Rename, remove, or relink a registered project | Use AO commands/dashboard project settings | | 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 | | 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. 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: A practical local config usually starts with the agent, runtime, workspace, setup, and rules:
```yaml title="agent-orchestrator.yaml" ```yaml title="agent-orchestrator.yaml"
agent: claude-code # claude-code | codex | aider | opencode agent: claude-code # claude-code | codex | aider | opencode
runtime: tmux # tmux | process runtime: tmux # tmux | process
workspace: worktree # worktree | clone workspace: worktree # worktree | clone
agentConfig: agentConfig:
permissions: permissionless permissions: permissionless
@ -146,18 +148,18 @@ For normal use, run AO from inside the repository and keep `agent-orchestrator.y
## Top-Level Global Keys ## Top-Level Global Keys
| Key | Default | Purpose | | Key | Default | Purpose |
|-----|---------|---------| | --------------------- | ------------------------- | ------------------------------------------------------------ |
| `port` | `3000` | Dashboard HTTP port | | `port` | `3000` | Dashboard HTTP port |
| `terminalPort` | auto from `14800` | tmux terminal WebSocket port | | `terminalPort` | auto from `14800` | tmux terminal WebSocket port |
| `directTerminalPort` | auto from `14801` | direct PTY WebSocket port | | `directTerminalPort` | auto from `14801` | direct PTY WebSocket port |
| `readyThresholdMs` | `300000` | Time before a ready session is treated as idle | | `readyThresholdMs` | `300000` | Time before a ready session is treated as idle |
| `defaults` | built-ins | Cross-project defaults for agent/runtime/workspace/notifiers | | `defaults` | built-ins | Cross-project defaults for agent/runtime/workspace/notifiers |
| `projects` | `{}` | Registered project identities | | `projects` | `{}` | Registered project identities |
| `projectOrder` | unset | Optional sidebar/project ordering | | `projectOrder` | unset | Optional sidebar/project ordering |
| `notifiers` | `{}` | Named notifier instances | | `notifiers` | `{}` | Named notifier instances |
| `notificationRouting` | desktop/composio defaults | Priority-to-notifier routing | | `notificationRouting` | desktop/composio defaults | Priority-to-notifier routing |
| `reactions` | built-in defaults | Global automation rules | | `reactions` | built-in defaults | Global automation rules |
## Where Data Lives ## Where Data Lives
@ -172,7 +174,19 @@ ls ~/.agent-orchestrator
## Next Steps ## Next Steps
<Cards> <Cards>
<Card title="Projects" href="/docs/configuration/projects" description="Per-project behavior settings: agents, workspaces, rules, setup, trackers, and SCM." /> <Card
<Card title="Reactions" href="/docs/configuration/reactions" description="Control what AO does when CI fails, reviews arrive, or agents need help." /> title="Projects"
<Card title="Remote access" href="/docs/configuration/remote-access" description="Access the dashboard from another machine without exposing it publicly." /> 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> </Cards>

View File

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

View File

@ -34,12 +34,12 @@ agent: claude-code
Built-in agents: Built-in agents:
| Agent | Use when | | Agent | Use when |
|-------|----------| | ------------- | ------------------------------------------------------ |
| `claude-code` | You want the default, most tested path | | `claude-code` | You want the default, most tested path |
| `codex` | You want OpenAI Codex CLI sessions | | `codex` | You want OpenAI Codex CLI sessions |
| `aider` | You already use Aider workflows | | `aider` | You already use Aider workflows |
| `opencode` | You want OpenCode session discovery and resume support | | `opencode` | You want OpenCode session discovery and resume support |
Agent-specific settings go under `agentConfig`: Agent-specific settings go under `agentConfig`:
@ -51,12 +51,12 @@ agentConfig:
`permissions` accepts: `permissions` accepts:
| Value | Behavior | | Value | Behavior |
|-------|----------| | ---------------- | ----------------------------------------------------- |
| `permissionless` | Let the agent edit and run commands without prompting | | `permissionless` | Let the agent edit and run commands without prompting |
| `default` | Use the agent tool's normal permission behavior | | `default` | Use the agent tool's normal permission behavior |
| `auto-edit` | Auto-approve edits, ask for other actions | | `auto-edit` | Auto-approve edits, ask for other actions |
| `suggest` | Suggest changes without applying them | | `suggest` | Suggest changes without applying them |
The legacy value `skip` is accepted and treated as `permissionless`. 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: tmux
``` ```
| Runtime | Use when | | Runtime | Use when |
|---------|----------| | --------- | ----------------------------------------------------------------------------------------------- |
| `tmux` | You want persistent sessions that survive dashboard reloads and can be attached from a terminal | | `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 | | `process` | You want a lighter direct process runtime and do not need tmux persistence |
Most users should keep `tmux`. Most users should keep `tmux`.
@ -83,10 +83,10 @@ Most users should keep `tmux`.
workspace: worktree workspace: worktree
``` ```
| Workspace | Use when | | Workspace | Use when |
|-----------|----------| | ---------- | ----------------------------------------------------------------- |
| `worktree` | Default. Fast, disk-efficient, creates a git worktree per session | | `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. Use `worktree` unless your repository has tooling that behaves badly with git worktrees.
@ -172,18 +172,18 @@ scm:
Built-in trackers: Built-in trackers:
| Plugin | Purpose | | Plugin | Purpose |
|--------|---------| | -------- | ------------- |
| `github` | GitHub issues | | `github` | GitHub issues |
| `gitlab` | GitLab issues | | `gitlab` | GitLab issues |
| `linear` | Linear issues | | `linear` | Linear issues |
Built-in SCM plugins: Built-in SCM plugins:
| Plugin | Purpose | | Plugin | Purpose |
|--------|---------| | -------- | ---------------------------------------- |
| `github` | GitHub PRs, checks, reviews, merge state | | `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: Extra keys under `tracker` and `scm` are passed to the plugin:
@ -233,13 +233,13 @@ opencodeIssueSessionStrategy: reuse
`orchestratorSessionStrategy` accepts: `orchestratorSessionStrategy` accepts:
| Value | Behavior | | Value | Behavior |
|-------|----------| | --------------- | ----------------------------------------------------- |
| `reuse` | Attach to the existing orchestrator session | | `reuse` | Attach to the existing orchestrator session |
| `delete` | Delete the old session and start a new one | | `delete` | Delete the old session and start a new one |
| `ignore` | Leave the old session and start another | | `ignore` | Leave the old session and start another |
| `delete-new` | Delete any newly detected duplicate | | `delete-new` | Delete any newly detected duplicate |
| `ignore-new` | Ignore any newly detected duplicate | | `ignore-new` | Ignore any newly detected duplicate |
| `kill-previous` | Kill the previous session before starting the new one | | `kill-previous` | Kill the previous session before starting the new one |
`opencodeIssueSessionStrategy` accepts `reuse`, `delete`, or `ignore`. `opencodeIssueSessionStrategy` accepts `reuse`, `delete`, or `ignore`.
@ -248,27 +248,27 @@ opencodeIssueSessionStrategy: reuse
These fields are valid in a local project config: These fields are valid in a local project config:
| Field | Type | Purpose | | Field | Type | Purpose |
|-------|------|---------| | ------------------------------ | ---------- | -------------------------------------------- |
| `repo` | `string` | Optional legacy/local repo slug | | `repo` | `string` | Optional legacy/local repo slug |
| `defaultBranch` | `string` | Branch PRs target, usually `main` | | `defaultBranch` | `string` | Branch PRs target, usually `main` |
| `agent` | `string` | Default worker agent | | `agent` | `string` | Default worker agent |
| `runtime` | `string` | Runtime plugin | | `runtime` | `string` | Runtime plugin |
| `workspace` | `string` | Workspace plugin | | `workspace` | `string` | Workspace plugin |
| `tracker` | `object` | Issue tracker plugin config | | `tracker` | `object` | Issue tracker plugin config |
| `scm` | `object` | Source control plugin config | | `scm` | `object` | Source control plugin config |
| `symlinks` | `string[]` | Files/directories linked into each workspace | | `symlinks` | `string[]` | Files/directories linked into each workspace |
| `postCreate` | `string[]` | Commands run after workspace creation | | `postCreate` | `string[]` | Commands run after workspace creation |
| `agentConfig` | `object` | Agent permissions/model/options | | `agentConfig` | `object` | Agent permissions/model/options |
| `orchestrator` | `object` | Orchestrator role override | | `orchestrator` | `object` | Orchestrator role override |
| `worker` | `object` | Worker role override | | `worker` | `object` | Worker role override |
| `reactions` | `object` | Per-project automation overrides | | `reactions` | `object` | Per-project automation overrides |
| `agentRules` | `string` | Inline worker instructions | | `agentRules` | `string` | Inline worker instructions |
| `agentRulesFile` | `string` | Path to a rules file | | `agentRulesFile` | `string` | Path to a rules file |
| `orchestratorRules` | `string` | Orchestrator-only instructions | | `orchestratorRules` | `string` | Orchestrator-only instructions |
| `orchestratorSessionStrategy` | `string` | Duplicate orchestrator recovery behavior | | `orchestratorSessionStrategy` | `string` | Duplicate orchestrator recovery behavior |
| `opencodeIssueSessionStrategy` | `string` | Duplicate OpenCode issue-session behavior | | `opencodeIssueSessionStrategy` | `string` | Duplicate OpenCode issue-session behavior |
| `decomposer` | `object` | Advanced decomposition settings | | `decomposer` | `object` | Advanced decomposition settings |
Identity fields such as `projectId`, `path`, `storageKey`, `originUrl`, and `sessionPrefix` belong to the global registry, not the local config. 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 ## Next Steps
<Cards> <Cards>
<Card title="Reactions" href="/docs/configuration/reactions" description="Configure what AO does when CI, reviews, or agent state changes need attention." /> <Card
<Card title="Remote access" href="/docs/configuration/remote-access" description="Open the dashboard from another machine without exposing it publicly." /> title="Reactions"
<Card title="Plugin docs" href="/docs/plugins" description="Agent, tracker, SCM, notifier, runtime, terminal, and workspace plugins." /> 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> </Cards>

View File

@ -58,31 +58,32 @@ reactions:
``` ```
<Callout type="warning"> <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> </Callout>
## Default Reactions ## Default Reactions
| Key | When it fires | Default action | Notes | | Key | When it fires | Default action | Notes |
|-----|---------------|----------------|-------| | -------------------- | ------------------------------------------- | --------------- | --------------------------------------------------- |
| `ci-failed` | CI check fails | `send-to-agent` | Sends the agent a recovery prompt | | `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 | | `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 | | `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 | | `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 | | `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-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-needs-input` | Agent is waiting for human input | `notify` | Urgent by default |
| `agent-exited` | Session exits unexpectedly or is killed | `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 | | `all-complete` | Session finishes cleanly | `notify` | Includes summary by default |
| `agent-idle` | Reserved | `send-to-agent` | Defined for future idle handling | | `agent-idle` | Reserved | `send-to-agent` | Defined for future idle handling |
## Action Types ## Action Types
| Action | Meaning | | Action | Meaning |
|--------|---------| | --------------- | ----------------------------------------------------------- |
| `send-to-agent` | Sends `message` into the running agent session | | `send-to-agent` | Sends `message` into the running agent session |
| `notify` | Routes an event to configured notifiers | | `notify` | Routes an event to configured notifiers |
| `auto-merge` | Reserved merge intent; currently routes like a notification | | `auto-merge` | Reserved merge intent; currently routes like a notification |
`send-to-agent` is the autonomous recovery path. `notify` is the human awareness path. `send-to-agent` is the autonomous recovery path. `notify` is the human awareness path.
@ -90,16 +91,16 @@ reactions:
Each reaction accepts: Each reaction accepts:
| Field | Type | Purpose | | Field | Type | Purpose |
|-------|------|---------| | ---------------- | ------------------------------------------- | ---------------------------------------------------------- |
| `auto` | `boolean` | Whether AO should perform the automated action | | `auto` | `boolean` | Whether AO should perform the automated action |
| `action` | `send-to-agent` \| `notify` \| `auto-merge` | What AO does when the reaction fires | | `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 | | `message` | `string` | Message sent to the agent or included in notification text |
| `priority` | `urgent` \| `action` \| `warning` \| `info` | Notification routing priority | | `priority` | `urgent` \| `action` \| `warning` \| `info` | Notification routing priority |
| `retries` | `number` | Max repeated agent messages before escalation | | `retries` | `number` | Max repeated agent messages before escalation |
| `escalateAfter` | `number` or duration string | Escalate after attempts or time, whichever applies | | `escalateAfter` | `number` or duration string | Escalate after attempts or time, whichever applies |
| `threshold` | duration string | Used by stuck/idle style reactions | | `threshold` | duration string | Used by stuck/idle style reactions |
| `includeSummary` | `boolean` | Include session summary in the notification | | `includeSummary` | `boolean` | Include session summary in the notification |
Example: Example:
@ -172,22 +173,30 @@ ao review-check <session-id>
## Event Reference ## Event Reference
| Event | Reaction | | Event | Reaction |
|-------|----------| | -------------------------- | -------------------- |
| `ci.failing` | `ci-failed` | | `ci.failing` | `ci-failed` |
| `review.changes_requested` | `changes-requested` | | `review.changes_requested` | `changes-requested` |
| `automated_review.found` | `bugbot-comments` | | `automated_review.found` | `bugbot-comments` |
| `merge.conflicts` | `merge-conflicts` | | `merge.conflicts` | `merge-conflicts` |
| `merge.ready` | `approved-and-green` | | `merge.ready` | `approved-and-green` |
| `session.stuck` | `agent-stuck` | | `session.stuck` | `agent-stuck` |
| `session.needs_input` | `agent-needs-input` | | `session.needs_input` | `agent-needs-input` |
| `session.killed` | `agent-exited` | | `session.killed` | `agent-exited` |
| `summary.all_complete` | `all-complete` | | `summary.all_complete` | `all-complete` |
## Next Steps ## Next Steps
<Cards> <Cards>
<Card title="Projects" href="/docs/configuration/projects" description="Apply reaction overrides to one project." /> <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
<Card title="Configuration" href="/docs/configuration" description="Understand global registry vs local project config." /> 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> </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 ### Environment variables for proxied setups
| Variable | Purpose | | Variable | Purpose |
|----------|---------| | ------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `HOST=0.0.0.0` | Bind the Next.js dashboard to all interfaces | | `HOST=0.0.0.0` | Bind the Next.js dashboard to all interfaces |
| `TERMINAL_PORT` | Override the tmux WS server port (server-side) | | `TERMINAL_PORT` | Override the tmux WS server port (server-side) |
| `DIRECT_TERMINAL_PORT` | Override the direct PTY 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 | | `NEXT_PUBLIC_TERMINAL_WS_PATH` | Override the WebSocket base path the browser client dials — required when the proxy rewrites the path |
### nginx ### nginx
@ -269,11 +269,11 @@ For more on project identity, local config, and runtime data, see [Configuration
## Port reference ## Port reference
| Port | Default | Config key | Env var override | Purpose | | Port | Default | Config key | Env var override | Purpose |
|------|---------|------------|-----------------|---------| | ------- | ------------------ | -------------------- | ---------------------- | ------------------------------------ |
| `3000` | dashboard HTTP | `port` | `PORT` | Next.js app + API routes | | `3000` | dashboard HTTP | `port` | `PORT` | Next.js app + API routes |
| `14800` | tmux terminal WS | `terminalPort` | `TERMINAL_PORT` | WebSocket for tmux-attached terminal | | `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 | | `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: 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. The main dashboard groups sessions by what you need to do next.
| Column | What it means | What you usually do | | Column | What it means | What you usually do |
|--------|---------------|---------------------| | ----------- | -------------------------------------------------- | ----------------------------------------- |
| **Working** | Agent is actively coding or running commands | Let it run | | **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 | | **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 | | **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 | | **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 | | **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. 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: The PR page lets you filter by:
| Tab | Shows | | Tab | Shows |
|-----|-------| | ---------- | ---------------------------- |
| **All** | Open, merged, and closed PRs | | **All** | Open, merged, and closed PRs |
| **Open** | PRs still in progress | | **Open** | PRs still in progress |
| **Merged** | Completed PRs | | **Merged** | Completed PRs |
| **Closed** | Closed but unmerged 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. 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: AO has two terminal backends:
| Backend | Used when | Default port | | Backend | Used when | Default port |
|---------|-----------|--------------| | -------------------- | ------------------ | ------------ |
| tmux WebSocket mux | `runtime: tmux` | `14800` | | tmux WebSocket mux | `runtime: tmux` | `14800` |
| direct PTY WebSocket | `runtime: process` | `14801` | | 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. 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 ## Routes
| Route | Use for | | Route | Use for |
|-------|---------| | ------------------------------------- | ----------------------------------------- |
| `/` | Project overview or current project board | | `/` | Project overview or current project board |
| `/projects/{projectId}` | One project's board | | `/projects/{projectId}` | One project's board |
| `/projects/{projectId}/settings` | Project behavior settings | | `/projects/{projectId}/settings` | Project behavior settings |
| `/prs` | PR review queue | | `/prs` | PR review queue |
| `/sessions/{id}` | Session detail and terminal | | `/sessions/{id}` | Session detail and terminal |
| `/projects/{projectId}/sessions/{id}` | Project-scoped session detail | | `/projects/{projectId}/sessions/{id}` | Project-scoped session detail |
| `/orchestrators?project={projectId}` | Pick or start an orchestrator session | | `/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. 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: The full dashboard experience uses three ports:
| Service | Default | Config | | Service | Default | Config |
|---------|---------|--------| | ----------------------- | ----------------- | ---------------------------------------------- |
| Dashboard HTTP | `3000` | `port` or `PORT` | | Dashboard HTTP | `3000` | `port` or `PORT` |
| tmux terminal WebSocket | auto from `14800` | `terminalPort` or `TERMINAL_PORT` | | tmux terminal WebSocket | auto from `14800` | `terminalPort` or `TERMINAL_PORT` |
| direct PTY WebSocket | auto from `14801` | `directTerminalPort` or `DIRECT_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). 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 ## Next Steps
<Cards> <Cards>
<Card title="Quickstart" href="/docs/quickstart" description="Start AO, spawn a session, and handle the first PR." /> <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
<Card title="Remote access" href="/docs/configuration/remote-access" description="Open the dashboard from another device safely." /> 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> </Cards>

View File

@ -258,7 +258,19 @@ projects:
## Next Steps ## Next Steps
<Cards> <Cards>
<Card title="Configuration Reference" href="/docs/configuration" description="Full reference for every field in agent-orchestrator.yaml." /> <Card
<Card title="Plugins" href="/docs/plugins" description="Browse available runtime, agent, tracker, SCM, notifier, and terminal plugins." /> title="Configuration Reference"
<Card title="Guides" href="/docs/guides" description="Step-by-step workflows for common setups and automation patterns." /> 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> </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. 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>
<Accordion title="Do I have to use Claude Code?"> <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). No. AO supports Claude Code, Codex, Cursor, Aider, and OpenCode today. Set `agent:` per project (or per spawn with
</Accordion> `--agent`). See [Agents](/docs/plugins/agents).
</Accordion>
<Accordion title="Does AO auto-merge?"> <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. No, intentionally. AO will get a PR to a mergeable state — CI green, approvals satisfied — and stop. The merge is your
</Accordion> call.
</Accordion>
<Accordion title="Does AO need webhooks?"> <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. No. AO polls `gh` / `glab` for PR state. Webhooks are an optional latency optimisation for the review loop — wire them
</Accordion> up only if you need near-instant reactions.
</Accordion>
<Accordion title="Can it use GitHub Enterprise / self-hosted GitLab?"> <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. Yes. `gh` and `glab` both support enterprise hosts — configure them via their own tools. For GitLab, also set
</Accordion> `trackerConfig.host` / `scmConfig.host` in AO.
</Accordion>
<Accordion title="Does it work on Windows?"> <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. Partially — see [Platforms Windows](/docs/platforms#windows). You need `runtime: process` instead of tmux, and
</Accordion> 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?"> <Accordion title="Where does session state live?">
`~/.agent-orchestrator/{hash}-{projectId}/`. No database. Everything is flat files you can inspect. `~/.agent-orchestrator/{hash}-{projectId}/`. No database. Everything is flat files you can inspect.
</Accordion> </Accordion>
<Accordion title="Can I run AO on a remote server?"> <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. Yes. The dashboard is a plain Next.js app — port-forward or put it behind your reverse proxy. AO has no built-in auth,
</Accordion> so use your usual SSO / basic-auth layer. Use `runtime: process` in containers.
</Accordion>
<Accordion title="How much does it cost?"> <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). AO itself is MIT-licensed and free. Agents cost whatever their underlying API costs (Anthropic, OpenAI, etc.) — AO
</Accordion> shows per-session cost on the dashboard for agents that report it (Claude Code today).
</Accordion>
<Accordion title="What happens to my code?"> <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. Your code stays in your worktrees on your machine. AO doesn't upload anything — it just coordinates local processes
</Accordion> and makes GitHub API calls through your authenticated CLIs.
</Accordion>
<Accordion title="Can I write my own plugin?"> <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. Yes. `ao plugin create --slot <slot> --name <name>` scaffolds a starter. Plugins are tiny Node packages exporting a manifest + `create()` function.
</Accordion> </Accordion>
<Accordion title="Why is everything a plugin?"> <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. Because the answer to "should AO support Linear?" (or Discord, or Codex, or …) is always yes, but bundling every
</Accordion> integration into core makes the CLI heavy. Plugins let you pick what you need.
</Accordion>
<Accordion title="Does AO modify my repo's files?"> <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. The agent modifies the worktree — that's the whole point. AO itself doesn't touch `main`, doesn't force-push, and
</Accordion> doesn't merge. The session works on its own branch.
</Accordion>
<Accordion title="How do I completely uninstall?"> <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. `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`). 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. 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). 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. 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" ```yaml title="agent-orchestrator.yaml"
reactions: reactions:
ciFailed: ciFailed:
enabled: true # default enabled: true # default
maxRetries: 3 # stop after 3 automatic rounds maxRetries: 3 # stop after 3 automatic rounds
cooldownSeconds: 30 # wait before nudging the agent again 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. 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: 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. 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" ```yaml title="agent-orchestrator.yaml"
reactions: reactions:
ci-failed: 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 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. These guides are task-shaped: each one covers a single thing you'd actually want AO to do for you, end to end.
<Cards> <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
<Card title="Parallel issues" description="Spawn agents on several issues at once without them stepping on each other." href="/docs/guides/parallel-issues" /> title="Per-role agents"
<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" /> description="Run a reasoning-heavy orchestrator and a fast worker — globally or per-project."
<Card title="Review loop" description="When a reviewer asks for changes, AO replays the feedback to the agent." href="/docs/guides/review-loop" /> href="/docs/guides/per-role-agents"
<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="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> </Cards>

View File

@ -1,12 +1,5 @@
{ {
"title": "Guides", "title": "Guides",
"defaultOpen": false, "defaultOpen": false,
"pages": [ "pages": ["per-role-agents", "parallel-issues", "ci-recovery", "review-loop", "reactions", "multi-project"]
"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 ## Config shape
```yaml title="agent-orchestrator.yaml" ```yaml title="agent-orchestrator.yaml"
runtime: tmux # global default runtime: tmux # global default
agent: claude-code agent: claude-code
projects: projects:
@ -20,7 +20,7 @@ projects:
api: api:
repo: myorg/api repo: myorg/api
agent: codex # per-project override agent: codex # per-project override
tracker: linear tracker: linear
trackerConfig: trackerConfig:
teamId: TEAM-123 teamId: TEAM-123
@ -28,7 +28,7 @@ projects:
marketing: marketing:
repo: myorg/site repo: myorg/site
agent: cursor 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`. 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: Anything defined at the top level is a default. Override per project:
```yaml ```yaml
tracker: github # default tracker: github # default
notifier: notifier:
- type: slack - type: slack
webhookUrl: ${SLACK_GLOBAL} webhookUrl: ${SLACK_GLOBAL}
@ -93,5 +93,6 @@ ao stop --all # stop everything
``` ```
<Callout type="info"> <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> </Callout>

View File

@ -37,12 +37,12 @@ AO creates two distinct sessions. Compare the PRs side by side.
## Isolation guarantees ## Isolation guarantees
| What's isolated | How | | 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/`. | | 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 | | 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 | | 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) | | Terminal | Each session owns its own tmux window (or child process) |
## What isn't isolated ## 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` — kill everything whose PR merged or issue closed (safe; archives metadata)
- `ao session cleanup --dry-run` — preview first - `ao session cleanup --dry-run` — preview first
<Callout type="info"> <Callout type="info">Need them all gone? `ao stop --all` halts every running AO instance on this machine.</Callout>
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"> <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> </Callout>
## Per-project override ## Per-project override
@ -138,13 +139,13 @@ AO records a `role` field in each session's metadata file with the value `orches
## Next steps ## Next steps
<Cards> <Cards>
<Card title="Projects configuration" href="/docs/configuration/projects#orchestrator-and-worker"> <Card title="Projects configuration" href="/docs/configuration/projects#orchestrator-and-worker">
Full per-project config syntax including orchestrator/worker fields. Full per-project config syntax including orchestrator/worker fields.
</Card> </Card>
<Card title="Architecture: orchestrator prompt" href="/docs/architecture#orchestrator-prompt"> <Card title="Architecture: orchestrator prompt" href="/docs/architecture#orchestrator-prompt">
How the orchestrator prompt is assembled and injected. How the orchestrator prompt is assembled and injected.
</Card> </Card>
<Card title="Agent plugins" href="/docs/plugins/agents"> <Card title="Agent plugins" href="/docs/plugins/agents">
Available agent plugins and their configuration options. Available agent plugins and their configuration options.
</Card> </Card>
</Cards> </Cards>

View File

@ -35,7 +35,9 @@ reactions:
``` ```
<Callout type="warning"> <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> </Callout>
## Recipe: Custom CI failure message ## Recipe: Custom CI failure message
@ -95,7 +97,7 @@ projects:
path: ~/code/myapp path: ~/code/myapp
reactions: reactions:
all-complete: 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: otherproj:
repo: org/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 ## Where to go next
<Cards> <Cards>
<Card title="Reactions reference" href="/docs/configuration/reactions"> <Card title="Reactions reference" href="/docs/configuration/reactions">
Full schema, default values, escalation semantics, and the two-pass CI design. Full schema, default values, escalation semantics, and the two-pass CI design.
</Card> </Card>
<Card title="CI recovery guide" href="/docs/guides/ci-recovery"> <Card title="CI recovery guide" href="/docs/guides/ci-recovery">
Step-by-step walkthrough of AO's CI failure recovery loop. Step-by-step walkthrough of AO's CI failure recovery loop.
</Card> </Card>
<Card title="Review loop guide" href="/docs/guides/review-loop"> <Card title="Review loop guide" href="/docs/guides/review-loop">
How AO handles review comments and the changes-requested reaction. How AO handles review comments and the changes-requested reaction.
</Card> </Card>
</Cards> </Cards>

View File

@ -26,7 +26,8 @@ A single structured prompt with:
- A pointer to the PR head SHA - A pointer to the PR head SHA
<Callout type="info"> <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> </Callout>
## Configurable behavior ## Configurable behavior
@ -34,8 +35,8 @@ A single structured prompt with:
```yaml title="agent-orchestrator.yaml" ```yaml title="agent-orchestrator.yaml"
reactions: reactions:
reviewRequested: reviewRequested:
enabled: true # default enabled: true # default
includeResolved: false # default: only unresolved threads includeResolved: false # default: only unresolved threads
maxRetries: 3 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: **GitHub** (`scm-github`) treats the following as bots:
| Login | Tool | | Login | Tool |
|---|---| | ------------------------- | -------------- |
| `cursor[bot]` | Cursor AI | | `cursor[bot]` | Cursor AI |
| `github-actions[bot]` | GitHub Actions | | `github-actions[bot]` | GitHub Actions |
| `codecov[bot]` | Codecov | | `codecov[bot]` | Codecov |
| `sonarcloud[bot]` | SonarCloud | | `sonarcloud[bot]` | SonarCloud |
| `dependabot[bot]` | Dependabot | | `dependabot[bot]` | Dependabot |
| `renovate[bot]` | Renovate | | `renovate[bot]` | Renovate |
| `codeclimate[bot]` | Code Climate | | `codeclimate[bot]` | Code Climate |
| `deepsource-autofix[bot]` | DeepSource | | `deepsource-autofix[bot]` | DeepSource |
| `snyk-bot` | Snyk | | `snyk-bot` | Snyk |
| `lgtm-com[bot]` | LGTM | | `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]`): **GitLab** (`scm-gitlab`) treats the following as bots (in addition to any username matching `project_\d+_bot` or ending in `[bot]`):
| Login | Tool | | Login | Tool |
|---|---| | ------------------ | --------------------- |
| `gitlab-bot` | GitLab built-in | | `gitlab-bot` | GitLab built-in |
| `ghost` | Deleted / system user | | `ghost` | Deleted / system user |
| `dependabot[bot]` | Dependabot | | `dependabot[bot]` | Dependabot |
| `renovate[bot]` | Renovate | | `renovate[bot]` | Renovate |
| `sast-bot` | GitLab SAST | | `sast-bot` | GitLab SAST |
| `codeclimate[bot]` | Code Climate | | `codeclimate[bot]` | Code Climate |
| `sonarcloud[bot]` | SonarCloud | | `sonarcloud[bot]` | SonarCloud |
| `snyk-bot` | Snyk | | `snyk-bot` | Snyk |
A typical configuration pairing: A typical configuration pairing:
@ -102,11 +103,11 @@ A typical configuration pairing:
reactions: reactions:
changes-requested: changes-requested:
auto: true auto: true
priority: "action" # human review comment — act immediately priority: "action" # human review comment — act immediately
bugbot-comments: bugbot-comments:
auto: true 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`. 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. 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"> <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> </Callout>
## What AO Is For ## 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 does not replace review. It helps agents keep working, but you still decide what gets merged.
| AO handles | You still handle | | AO handles | You still handle |
| --- | --- | | ---------------------------------- | -------------------------------------------- |
| Creating isolated workspaces | Choosing good issues | | Creating isolated workspaces | Choosing good issues |
| Launching and monitoring agents | Reviewing code and behavior | | Launching and monitoring agents | Reviewing code and behavior |
| Tracking PR, CI, and review status | Deciding when to merge | | Tracking PR, CI, and review status | Deciding when to merge |
| Cleaning up merged sessions | Setting repo-specific rules and expectations | | Cleaning up merged sessions | Setting repo-specific rules and expectations |
## How A Session Moves ## 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. 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 | | Plugin slot | What it controls | Examples |
| --- | --- | --- | | ----------- | --------------------------------- | ------------------------------------------- |
| Agent | Which coding tool writes changes | Claude Code, Codex, Cursor, Aider, OpenCode | | Agent | Which coding tool writes changes | Claude Code, Codex, Cursor, Aider, OpenCode |
| Runtime | How the agent process runs | tmux, child process | | Runtime | How the agent process runs | tmux, child process |
| Workspace | Where code is checked out | git worktree, full clone | | Workspace | Where code is checked out | git worktree, full clone |
| Tracker | Where issues come from | GitHub, GitLab, Linear | | Tracker | Where issues come from | GitHub, GitLab, Linear |
| SCM | How PRs, reviews, and CI are read | GitHub, GitLab | | SCM | How PRs, reviews, and CI are read | GitHub, GitLab |
| Notifier | Where AO sends updates | desktop, Slack, Discord, webhook | | 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. 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 ## Platform Support
<PlatformSupport <PlatformSupport
macos="full" macos="full"
linux="full" linux="full"
windows="partial" 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.</>} 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 ## Where To Go Next
<Cards> <Cards>
<Card title="Installation" description="Install AO, authenticate your tools, and run the doctor check." href="/docs/installation" /> <Card
<Card title="Quickstart" description="Spawn one agent on one issue and watch it open a PR." href="/docs/quickstart" /> title="Installation"
<Card title="Configuration" description="Understand the `agent-orchestrator.yaml` file." href="/docs/configuration" /> description="Install AO, authenticate your tools, and run the doctor check."
<Card title="Guides" description="Run parallel issues, recover from CI failures, and handle review loops." href="/docs/guides" /> href="/docs/installation"
<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="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> </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. 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"> <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> </Callout>
## Prerequisites ## Prerequisites
<PlatformSupport <PlatformSupport
macos="full" macos="full"
linux="full" linux="full"
windows="partial" 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>.</>} 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: Install these before running AO:
| Tool | Required | Why AO needs it | | Tool | Required | Why AO needs it |
| --- | --- | --- | | --------------- | ---------------------- | ------------------------------------------------------------------------------------ |
| Node.js 20+ | Yes | Runs the AO CLI, dashboard, and plugins. | | Node.js 20+ | Yes | Runs the AO CLI, dashboard, and plugins. |
| Git | Yes | Creates worktrees, branches, commits, and cleanup operations. | | 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. | | 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. | | 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. | | 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`. 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 ## Install AO
<Tabs items={["npm", "pnpm", "yarn", "from source"]}> <Tabs items={["npm", "pnpm", "yarn", "from source"]}>
<Tab value="npm"> <Tab value="npm">```bash npm install -g @aoagents/ao ```</Tab>
```bash <Tab value="pnpm">```bash pnpm add -g @aoagents/ao ```</Tab>
npm install -g @aoagents/ao <Tab value="yarn">```bash yarn global add @aoagents/ao ```</Tab>
``` <Tab value="from source">
</Tab> ```bash git clone https://github.com/ComposioHQ/agent-orchestrator cd agent-orchestrator pnpm install pnpm build
<Tab value="pnpm"> pnpm --filter @aoagents/ao link --global ```
```bash </Tab>
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> </Tabs>
Confirm the command is available: 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. 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"]}> <Tabs items={["Claude Code", "Codex", "Cursor", "Aider", "OpenCode"]}>
<Tab value="Claude Code"> <Tab value="Claude Code">```bash npm install -g @anthropic-ai/claude-code claude ```</Tab>
```bash <Tab value="Codex">```bash npm install -g @openai/codex codex ```</Tab>
npm install -g @anthropic-ai/claude-code <Tab value="Cursor">```bash curl https://cursor.com/install -fsS | bash agent --help ```</Tab>
claude <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>
<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> </Tabs>
</Step> </Step>
@ -180,30 +143,36 @@ Use Slack, Discord, webhook, or another network notifier for alerts. Desktop not
## Common Install Issues ## Common Install Issues
<Accordions> <Accordions>
<Accordion title="`ao` is not found after install"> <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. Your global package bin directory is not on `PATH`. Check your package manager's global bin path, then reopen your
</Accordion> shell and run `ao --version` again.
<Accordion title="`gh` is not authenticated"> </Accordion>
Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is authenticated. <Accordion title="`gh` is not authenticated">
</Accordion> Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is
<Accordion title="tmux is missing"> authenticated.
Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container. </Accordion>
</Accordion> <Accordion title="tmux is missing">
<Accordion title="No agent runtime is detected"> Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container.
Install one supported agent CLI and run it once outside AO so it can complete its own sign-in flow. </Accordion>
</Accordion> <Accordion title="No agent runtime is detected">
<Accordion title="Port 3000 is already in use"> Install one supported agent CLI and run it once outside AO so it can complete its own sign-in flow.
`ao start` scans for a free port and prints the final dashboard URL. Use the URL in the command output. </Accordion>
</Accordion> <Accordion title="Port 3000 is already in use">
<Accordion title="The dashboard fails after an update"> `ao start` scans for a free port and prints the final dashboard URL. Use the URL in the command output.
Run `ao dashboard --rebuild` to clean stale dashboard build artifacts and start again. </Accordion>
</Accordion> <Accordion title="The dashboard fails after an update">
Run `ao dashboard --rebuild` to clean stale dashboard build artifacts and start again.
</Accordion>
</Accordions> </Accordions>
## Next ## Next
<Cards> <Cards>
<Card title="Quickstart" description="Run one worker session from start to pull request." href="/docs/quickstart" /> <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
<Card title="Platforms" description="Choose the right runtime and notifier for your OS." href="/docs/platforms" /> 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> </Cards>

View File

@ -1,24 +1,24 @@
{ {
"title": "Docs", "title": "Docs",
"pages": [ "pages": [
"---Getting Started---", "---Getting Started---",
"index", "index",
"installation", "installation",
"quickstart", "quickstart",
"platforms", "platforms",
"---Learn---", "---Learn---",
"guides", "guides",
"plugins", "plugins",
"---Reference---", "---Reference---",
"cli", "cli",
"configuration", "configuration",
"architecture", "architecture",
"examples", "examples",
"dashboard", "dashboard",
"---Help---", "---Help---",
"troubleshooting", "troubleshooting",
"faq", "faq",
"migration", "migration",
"changelog" "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"; import { Callout } from "fumadocs-ui/components/callout";
<Callout type="info"> <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> </Callout>
## From `@composio/agent-orchestrator` to `@aoagents/ao` ## 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. The platform differences come from the tools used to run and attach to long-lived sessions.
<PlatformSupport <PlatformSupport
macos="full" macos="full"
linux="full" linux="full"
windows="partial" windows="partial"
note={<>Windows support is actively improving. Use the process runtime instead of tmux.</>} note={<>Windows support is actively improving. Use the process runtime instead of tmux.</>}
/> />
## Recommended Setup ## Recommended Setup
| Platform | Runtime | Notifications | Notes | | Platform | Runtime | Notifications | Notes |
| --- | --- | --- | --- | | ---------------------- | ------------------- | ---------------------------------- | -------------------------------------------------------------------------------- |
| macOS | `tmux` | `desktop`, Slack, Discord, webhook | Best local experience. iTerm2 attach support is macOS-only. | | 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`. | | 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. | | 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. | | Container or remote VM | `process` or `tmux` | Slack, Discord, webhook | Use persistent storage for AO data and protect the dashboard with your own auth. |
## macOS ## macOS
@ -46,16 +46,17 @@ defaults:
What works well on macOS: What works well on macOS:
| Capability | Status | | Capability | Status |
| --- | --- | | ----------------------------------------- | --------- |
| tmux-backed worker sessions | Supported | | tmux-backed worker sessions | Supported |
| Browser dashboard terminal | Supported | | Browser dashboard terminal | Supported |
| iTerm2 attach/open helpers | Supported | | iTerm2 attach/open helpers | Supported |
| Desktop notifications | Supported | | Desktop notifications | Supported |
| macOS idle sleep prevention while AO runs | Supported | | macOS idle sleep prevention while AO runs | Supported |
<Callout type="info" title="Lid-close sleep still wins"> <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> </Callout>
## Linux ## Linux
@ -79,20 +80,21 @@ defaults:
What to know: What to know:
| Capability | Status | | Capability | Status |
| --- | --- | | --------------------------- | ----------------------------------------------------------- |
| tmux-backed worker sessions | Supported | | tmux-backed worker sessions | Supported |
| Browser dashboard terminal | Supported | | Browser dashboard terminal | Supported |
| iTerm2 attach/open helpers | Not available | | iTerm2 attach/open helpers | Not available |
| Desktop notifications | Supported when `notify-send` is installed | | Desktop notifications | Supported when `notify-send` is installed |
| Remote dashboard access | Supported through port forwarding, Tailscale, or your proxy | | 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. If you are running AO on a headless Linux host, prefer Slack, Discord, or webhook notifications over desktop notifications.
## Windows ## Windows
<Callout type="warn" title="Windows support is in progress"> <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> </Callout>
Recommended config: Recommended config:
@ -106,22 +108,22 @@ defaults:
What works: What works:
| Capability | Status | | Capability | Status |
| --- | --- | | --------------------------------------------------------- | ------------------------------------------------------- |
| `ao start`, `ao stop`, `ao dashboard` | Supported | | `ao start`, `ao stop`, `ao dashboard` | Supported |
| `ao spawn` and `ao spawn --prompt` | Supported through the process runtime | | `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 | | GitHub, GitLab, and Linear tracker/SCM integrations | Supported when their CLIs or credentials are configured |
| Browser dashboard terminal | Supported through the direct PTY server | | Browser dashboard terminal | Supported through the direct PTY server |
| Slack, Discord, webhook, Composio, and OpenClaw notifiers | Supported | | Slack, Discord, webhook, Composio, and OpenClaw notifiers | Supported |
What is limited: What is limited:
| Capability | Status | Use instead | | Capability | Status | Use instead |
| --- | --- | --- | | --------------------------- | ---------------------- | ---------------------------------------------- |
| `runtime: tmux` | Not available natively | `runtime: process` | | `runtime: tmux` | Not available natively | `runtime: process` |
| iTerm2 terminal integration | Not available | Browser dashboard terminal | | iTerm2 terminal integration | Not available | Browser dashboard terminal |
| Desktop notifier | No-op on Windows | Slack, Discord, webhook, Composio, or OpenClaw | | Desktop notifier | No-op on Windows | Slack, Discord, webhook, Composio, or OpenClaw |
| tmux attach commands | Not available | Dashboard terminal and AO session commands | | 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`. 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 ## Choosing A Runtime
| Runtime | Best for | Tradeoff | | Runtime | Best for | Tradeoff |
| --- | --- | --- | | --------- | ------------------------------------------------------------------------- | --------------------------------------------------- |
| `tmux` | macOS/Linux machines where you want durable, attachable terminal sessions | Requires tmux and does not work natively on Windows | | `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 | | `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. 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"; import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="aider" size={28} /> <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> <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> </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. [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 ## Environment variables
| Variable | Set by AO | Purpose | | Variable | Set by AO | Purpose |
|---|---|---| | --------------- | --------- | -------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id | | `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier | | `AO_ISSUE_ID` | ✓ | Issue identifier |
| `PATH` | ✓ | Prepends `~/.ao/bin` | | `PATH` | ✓ | Prepends `~/.ao/bin` |
| `GH_PATH` | ✓ | Absolute path to real `gh` | | `GH_PATH` | ✓ | Absolute path to real `gh` |
## Troubleshooting ## Troubleshooting
<Accordions> <Accordions>
<Accordion title="Aider hangs on file-edit confirmation"> <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. Aider prompts before applying diffs. If you want fully autonomous runs, configure Aider's `--yes-always` via
</Accordion> `~/.aider.conf.yml`. AO won't inject this for you — it's your call whether the agent should auto-apply.
<Accordion title="Dashboard cost shows $0"> </Accordion>
AO doesn't parse Aider's cost output. The cost column stays empty. <Accordion title="Dashboard cost shows $0">
</Accordion> AO doesn't parse Aider's cost output. The cost column stays empty.
</Accordion>
</Accordions> </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"; import { Callout } from "fumadocs-ui/components/callout";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="claude-code" size={28} /> <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> <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> </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. [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 ## Environment variables
| Variable | Set by AO | Purpose | | Variable | Set by AO | Purpose |
|---|---|---| | --------------- | ------------------------------ | --------------------------------------------------- |
| `AO_SESSION_ID` | ✓ | Unique AO session identifier | | `AO_SESSION_ID` | ✓ | Unique AO session identifier |
| `AO_ISSUE_ID` | ✓ (when spawned from an issue) | Issue identifier | | `AO_ISSUE_ID` | ✓ (when spawned from an issue) | Issue identifier |
| `CLAUDECODE` | ✓ | Signals to Claude that it's running under a harness | | `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. 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"; import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="codex" size={28} /> <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> <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> </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. [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 ## Environment variables
| Variable | Set by AO | Purpose | | Variable | Set by AO | Purpose |
|---|---|---| | ---------------------------- | --------- | --------------------------------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id | | `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier | | `AO_ISSUE_ID` | ✓ | Issue identifier |
| `PATH` | ✓ | Prepends `~/.ao/bin` for the wrappers | | `PATH` | ✓ | Prepends `~/.ao/bin` for the wrappers |
| `GH_PATH` | ✓ | Absolute path to the real `gh`, used by the wrapper | | `GH_PATH` | ✓ | Absolute path to the real `gh`, used by the wrapper |
| `CODEX_DISABLE_UPDATE_CHECK` | ✓ (`1`) | Skip version check | | `CODEX_DISABLE_UPDATE_CHECK` | ✓ (`1`) | Skip version check |
OpenAI API credentials come from wherever `codex` reads them — AO doesn't set them. 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"; import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="cursor" size={28} /> <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> <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> </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. 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 ## Environment variables
| Variable | Set by AO | Purpose | | Variable | Set by AO | Purpose |
|---|---|---| | --------------- | --------- | -------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id | | `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier | | `AO_ISSUE_ID` | ✓ | Issue identifier |
| `PATH` | ✓ | Prepends `~/.ao/bin` | | `PATH` | ✓ | Prepends `~/.ao/bin` |
| `GH_PATH` | ✓ | Absolute path to real `gh` | | `GH_PATH` | ✓ | Absolute path to real `gh` |
## Troubleshooting ## Troubleshooting
<Accordions> <Accordions>
<Accordion title="AO says Cursor Agent isn't detected"> <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. The plugin only recognises the real Cursor binary by its help output. Run `agent --help` — if you see a different
</Accordion> tool's help, remove or shadow it, or reinstall Cursor Agent.
<Accordion title="Dashboard never leaves `active`"> </Accordion>
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 title="Dashboard never leaves `active`">
</Accordion> 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> </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. The agent is the piece that actually writes code. Everything else in AO — worktrees, runtimes, notifiers — is plumbing around it.
<Callout type="info"> <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> </Callout>
## Supported agents ## Supported agents
| Agent | Slot name | Binary | Session resume | | Agent | Slot name | Binary | Session resume |
|---|---|---|---| | ----------------------------------------------- | ------------- | ---------- | ---------------------------- |
| [Claude Code](/docs/plugins/agents/claude-code) | `claude-code` | `claude` | ✅ `--resume` | | [Claude Code](/docs/plugins/agents/claude-code) | `claude-code` | `claude` | ✅ `--resume` |
| [Codex](/docs/plugins/agents/codex) | `codex` | `codex` | ✅ `codex resume <threadId>` | | [Codex](/docs/plugins/agents/codex) | `codex` | `codex` | ✅ `codex resume <threadId>` |
| [Cursor](/docs/plugins/agents/cursor) | `cursor` | `agent` | ❌ | | [Cursor](/docs/plugins/agents/cursor) | `cursor` | `agent` | ❌ |
| [Aider](/docs/plugins/agents/aider) | `aider` | `aider` | ❌ | | [Aider](/docs/plugins/agents/aider) | `aider` | `aider` | ❌ |
| [OpenCode](/docs/plugins/agents/opencode) | `opencode` | `opencode` | ✅ via OpenCode session API | | [OpenCode](/docs/plugins/agents/opencode) | `opencode` | `opencode` | ✅ via OpenCode session API |
<PluginGrid> <PluginGrid>
<PluginCard name="Claude Code" logo="claude-code" href="/docs/plugins/agents/claude-code" description="Anthropic's CLI coding agent." /> <PluginCard
<PluginCard name="Codex" logo="codex" href="/docs/plugins/agents/codex" description="OpenAI's Codex CLI." /> name="Claude Code"
<PluginCard name="Cursor" logo="cursor" href="/docs/plugins/agents/cursor" description="Cursor Agent CLI." /> logo="claude-code"
<PluginCard name="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI." /> href="/docs/plugins/agents/claude-code"
<PluginCard name="OpenCode" logo="opencode" href="/docs/plugins/agents/opencode" description="OpenCode terminal agent." /> 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> </PluginGrid>
## Choosing ## Choosing
@ -48,7 +62,7 @@ Neither approach requires you to change your agent's config — AO sets it up tr
Per-project overrides: Per-project overrides:
```yaml ```yaml
agent: claude-code # global default agent: claude-code # global default
projects: projects:
api: api:
repo: myorg/api repo: myorg/api

View File

@ -1,10 +1,4 @@
{ {
"title": "Agents", "title": "Agents",
"pages": [ "pages": ["claude-code", "codex", "cursor", "aider", "opencode"]
"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"; import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="opencode" size={28} /> <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> <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> </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. [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 ## Environment variables
| Variable | Set by AO | Purpose | | Variable | Set by AO | Purpose |
|---|---|---| | --------------- | --------- | -------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id | | `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier | | `AO_ISSUE_ID` | ✓ | Issue identifier |
| `PATH` | ✓ | Prepends `~/.ao/bin` | | `PATH` | ✓ | Prepends `~/.ao/bin` |
| `GH_PATH` | ✓ | Absolute path to real `gh` | | `GH_PATH` | ✓ | Absolute path to real `gh` |
## Troubleshooting ## Troubleshooting

View File

@ -17,17 +17,17 @@ Every plugin module must satisfy `PluginModule<T>`, where `T` is the interface f
```typescript ```typescript
export interface PluginModule<T = unknown> { export interface PluginModule<T = unknown> {
manifest: PluginManifest; manifest: PluginManifest;
create(config?: Record<string, unknown>): T; create(config?: Record<string, unknown>): T;
detect?(): boolean; detect?(): boolean;
} }
export interface PluginManifest { export interface PluginManifest {
name: string; // must match the package-name suffix (see below) name: string; // must match the package-name suffix (see below)
slot: PluginSlot; // use "as const" to preserve the literal type slot: PluginSlot; // use "as const" to preserve the literal type
description: string; description: string;
version: string; version: string;
displayName?: 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"; import type { PluginModule, Notifier } from "@aoagents/ao-core";
export const manifest = { export const manifest = {
name: "my-notifier", name: "my-notifier",
slot: "notifier" as const, slot: "notifier" as const,
description: "Send alerts to My Service", description: "Send alerts to My Service",
version: "0.1.0", version: "0.1.0",
}; };
export function create(config?: Record<string, unknown>): Notifier { export function create(config?: Record<string, unknown>): Notifier {
// validate here, return interface implementation // validate here, return interface implementation
} }
export function detect(): boolean { 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>; 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: The CLI asks for four things, in order:
| Prompt | Example input | | Prompt | Example input |
|--------|--------------| | ------------------- | ------------------------------------------------------------ |
| Plugin display name | `PagerDuty` | | Plugin display name | `PagerDuty` |
| Plugin slot | `notifier` (selected from a list of all 7 slots) | | Plugin slot | `notifier` (selected from a list of all 7 slots) |
| Short description | `Route urgent alerts to PagerDuty` | | Short description | `Route urgent alerts to PagerDuty` |
| Author | `Alice` | | Author | `Alice` |
| Package name | `ao-plugin-notifier-pagerduty` (pre-filled from slot + name) | | Package name | `ao-plugin-notifier-pagerduty` (pre-filled from slot + name) |
</Step> </Step>
<Step> <Step>
@ -141,22 +141,22 @@ The generated `src/index.ts` looks like this:
import type { PluginModule } from "@aoagents/ao-core"; import type { PluginModule } from "@aoagents/ao-core";
export const manifest = { export const manifest = {
name: "pagerduty", name: "pagerduty",
slot: "notifier" as const, slot: "notifier" as const,
description: "Route urgent alerts to PagerDuty", description: "Route urgent alerts to PagerDuty",
version: "0.1.0", version: "0.1.0",
displayName: "PagerDuty", displayName: "PagerDuty",
}; };
const plugin: PluginModule = { const plugin: PluginModule = {
manifest, manifest,
create(config?: Record<string, unknown>) { create(config?: Record<string, unknown>) {
return { return {
name: manifest.name, name: manifest.name,
config: config ?? {}, config: config ?? {},
// TODO: replace this placeholder with a real notifier implementation. // TODO: replace this placeholder with a real notifier implementation.
}; };
}, },
}; };
export default plugin; export default plugin;
@ -226,15 +226,15 @@ All interfaces are defined in `packages/core/src/types.ts` and exported from `@a
### Runtime ### Runtime
| Method | Signature | Required | | Method | Signature | Required |
|--------|-----------|---------| | --------------- | ------------------------------------------------------------ | -------- |
| `create` | `(config: RuntimeCreateConfig) => Promise<RuntimeHandle>` | yes | | `create` | `(config: RuntimeCreateConfig) => Promise<RuntimeHandle>` | yes |
| `destroy` | `(handle: RuntimeHandle) => Promise<void>` | yes | | `destroy` | `(handle: RuntimeHandle) => Promise<void>` | yes |
| `sendMessage` | `(handle: RuntimeHandle, message: string) => Promise<void>` | yes | | `sendMessage` | `(handle: RuntimeHandle, message: string) => Promise<void>` | yes |
| `getOutput` | `(handle: RuntimeHandle, lines?: number) => Promise<string>` | yes | | `getOutput` | `(handle: RuntimeHandle, lines?: number) => Promise<string>` | yes |
| `isAlive` | `(handle: RuntimeHandle) => Promise<boolean>` | yes | | `isAlive` | `(handle: RuntimeHandle) => Promise<boolean>` | yes |
| `getMetrics` | `(handle: RuntimeHandle) => Promise<RuntimeMetrics>` | optional | | `getMetrics` | `(handle: RuntimeHandle) => Promise<RuntimeMetrics>` | optional |
| `getAttachInfo` | `(handle: RuntimeHandle) => Promise<AttachInfo>` | optional | | `getAttachInfo` | `(handle: RuntimeHandle) => Promise<AttachInfo>` | optional |
### Agent ### Agent
@ -242,52 +242,54 @@ Agent plugins have the richest interface. Methods are split into required and op
**Required:** **Required:**
| Method | Purpose | Returns `null`? | | Method | Purpose | Returns `null`? |
|--------|---------|----------------| | ------------------ | ----------------------------------------------------------------- | -------------------- |
| `getLaunchCommand` | Shell command to start the agent | No | | `getLaunchCommand` | Shell command to start the agent | No |
| `getEnvironment` | Env vars for the process (must include `~/.ao/bin` in PATH) | No | | `getEnvironment` | Env vars for the process (must include `~/.ao/bin` in PATH) | No |
| `detectActivity` | Terminal-output activity classification (deprecated but required) | No | | `detectActivity` | Terminal-output activity classification (deprecated but required) | No |
| `getActivityState` | JSONL/API-based activity detection | Yes (if no data) | | `getActivityState` | JSONL/API-based activity detection | Yes (if no data) |
| `isProcessRunning` | Check whether the process is still alive | No (returns `false`) | | `isProcessRunning` | Check whether the process is still alive | No (returns `false`) |
| `getSessionInfo` | Extract summary, cost, session ID from agent data | Yes | | `getSessionInfo` | Extract summary, cost, session ID from agent data | Yes |
**Optional:** **Optional:**
| Method | Purpose | When to skip | | Method | Purpose | When to skip |
|--------|---------|-------------| | --------------------- | ---------------------------------------- | ------------------------------------- |
| `getRestoreCommand` | Resume a previous session | Agent has no resume capability | | `getRestoreCommand` | Resume a previous session | Agent has no resume capability |
| `setupWorkspaceHooks` | Install metadata hooks for PR tracking | Never — required for the dashboard | | `setupWorkspaceHooks` | Install metadata hooks for PR tracking | Never — required for the dashboard |
| `postLaunchSetup` | Post-launch config | Only if no post-launch work is needed | | `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) | | `recordActivity` | Write terminal-derived activity to JSONL | Agent has native JSONL (Claude Code) |
<Callout type="warn"> <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> </Callout>
### Workspace ### Workspace
| Method | Signature | Required | | Method | Signature | Required |
|--------|-----------|---------| | ------------ | ---------------------------------------------------------------------------------- | -------- |
| `create` | `(config: WorkspaceCreateConfig) => Promise<WorkspaceInfo>` | yes | | `create` | `(config: WorkspaceCreateConfig) => Promise<WorkspaceInfo>` | yes |
| `destroy` | `(workspacePath: string) => Promise<void>` | yes | | `destroy` | `(workspacePath: string) => Promise<void>` | yes |
| `list` | `(projectId: string) => Promise<WorkspaceInfo[]>` | yes | | `list` | `(projectId: string) => Promise<WorkspaceInfo[]>` | yes |
| `postCreate` | `(info: WorkspaceInfo, project: ProjectConfig) => Promise<void>` | optional | | `postCreate` | `(info: WorkspaceInfo, project: ProjectConfig) => Promise<void>` | optional |
| `exists` | `(workspacePath: string) => Promise<boolean>` | optional | | `exists` | `(workspacePath: string) => Promise<boolean>` | optional |
| `restore` | `(config: WorkspaceCreateConfig, workspacePath: string) => Promise<WorkspaceInfo>` | optional | | `restore` | `(config: WorkspaceCreateConfig, workspacePath: string) => Promise<WorkspaceInfo>` | optional |
### Tracker ### Tracker
| Method | Signature | Required | | Method | Signature | Required |
|--------|-----------|---------| | ---------------- | ------------------------------------------------------------------------------------ | -------- |
| `getIssue` | `(identifier: string, project: ProjectConfig) => Promise<Issue>` | yes | | `getIssue` | `(identifier: string, project: ProjectConfig) => Promise<Issue>` | yes |
| `isCompleted` | `(identifier: string, project: ProjectConfig) => Promise<boolean>` | yes | | `isCompleted` | `(identifier: string, project: ProjectConfig) => Promise<boolean>` | yes |
| `issueUrl` | `(identifier: string, project: ProjectConfig) => string` | yes | | `issueUrl` | `(identifier: string, project: ProjectConfig) => string` | yes |
| `branchName` | `(identifier: string, project: ProjectConfig) => string` | yes | | `branchName` | `(identifier: string, project: ProjectConfig) => string` | yes |
| `generatePrompt` | `(identifier: string, project: ProjectConfig) => Promise<string>` | yes | | `generatePrompt` | `(identifier: string, project: ProjectConfig) => Promise<string>` | yes |
| `issueLabel` | `(url: string, project: ProjectConfig) => string` | optional | | `issueLabel` | `(url: string, project: ProjectConfig) => string` | optional |
| `listIssues` | `(filters: IssueFilters, project: ProjectConfig) => Promise<Issue[]>` | optional | | `listIssues` | `(filters: IssueFilters, project: ProjectConfig) => Promise<Issue[]>` | optional |
| `updateIssue` | `(identifier: string, update: IssueUpdate, project: ProjectConfig) => Promise<void>` | optional | | `updateIssue` | `(identifier: string, update: IssueUpdate, project: ProjectConfig) => Promise<void>` | optional |
| `createIssue` | `(input: CreateIssueInput, project: ProjectConfig) => Promise<Issue>` | optional | | `createIssue` | `(input: CreateIssueInput, project: ProjectConfig) => Promise<Issue>` | optional |
### SCM ### SCM
@ -299,18 +301,18 @@ The richest interface — covers PR lifecycle, CI tracking, review tracking, and
### Notifier ### Notifier
| Method | Signature | Required | | Method | Signature | Required |
|--------|-----------|---------| | ------------------- | ----------------------------------------------------------------------- | -------- |
| `notify` | `(event: OrchestratorEvent) => Promise<void>` | yes | | `notify` | `(event: OrchestratorEvent) => Promise<void>` | yes |
| `notifyWithActions` | `(event: OrchestratorEvent, actions: NotifyAction[]) => Promise<void>` | optional | | `notifyWithActions` | `(event: OrchestratorEvent, actions: NotifyAction[]) => Promise<void>` | optional |
| `post` | `(message: string, context?: NotifyContext) => Promise<string \| null>` | optional | | `post` | `(message: string, context?: NotifyContext) => Promise<string \| null>` | optional |
### Terminal ### Terminal
| Method | Signature | Required | | Method | Signature | Required |
|--------|-----------|---------| | --------------- | ---------------------------------------- | -------- |
| `openSession` | `(session: Session) => Promise<void>` | yes | | `openSession` | `(session: Session) => Promise<void>` | yes |
| `openAll` | `(sessions: Session[]) => Promise<void>` | yes | | `openAll` | `(sessions: Session[]) => Promise<void>` | yes |
| `isSessionOpen` | `(session: Session) => Promise<boolean>` | optional | | `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 ```typescript
export function create(config?: Record<string, unknown>): Notifier { export function create(config?: Record<string, unknown>): Notifier {
// Resolve config or fall back to an environment variable. // Resolve config or fall back to an environment variable.
const url = (config?.url as string | undefined) ?? process.env["WEBHOOK_URL"]; const url = (config?.url as string | undefined) ?? process.env["WEBHOOK_URL"];
if (!url) { if (!url) {
// Warn for missing optional config — don't throw. // Warn for missing optional config — don't throw.
// Throw only when a required field is missing at method call time. // Throw only when a required field is missing at method call time.
console.warn("[notifier-webhook] No url configured — notifications will be no-ops"); console.warn("[notifier-webhook] No url configured — notifications will be no-ops");
} else { } else {
validateUrl(url, "notifier-webhook"); // throws on malformed URL validateUrl(url, "notifier-webhook"); // throws on malformed URL
} }
// Custom headers are optional — silently ignore non-string values. // Custom headers are optional — silently ignore non-string values.
const customHeaders: Record<string, string> = {}; const customHeaders: Record<string, string> = {};
const rawHeaders = config?.headers; const rawHeaders = config?.headers;
if (rawHeaders && typeof rawHeaders === "object" && !Array.isArray(rawHeaders)) { if (rawHeaders && typeof rawHeaders === "object" && !Array.isArray(rawHeaders)) {
for (const [k, v] of Object.entries(rawHeaders)) { for (const [k, v] of Object.entries(rawHeaders)) {
if (typeof v === "string") customHeaders[k] = v; if (typeof v === "string") customHeaders[k] = v;
} }
} }
return { return {
name: "webhook", name: "webhook",
async notify(event) { async notify(event) {
if (!url) return; if (!url) return;
await fetch(url, { await fetch(url, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", ...customHeaders }, headers: { "Content-Type": "application/json", ...customHeaders },
body: JSON.stringify({ type: "notification", event }), body: JSON.stringify({ type: "notification", event }),
}); });
}, },
}; };
} }
``` ```
@ -420,14 +422,14 @@ projects:
## `ao plugin` subcommands ## `ao plugin` subcommands
| Command | Description | | 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 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 search <query>` | Search the bundled catalog by name, package, description, or slot. |
| `ao plugin create [dir]` | Scaffold a new plugin package interactively. | | `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 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 update [reference]` | Update an installer-managed plugin. Pass `--all` to update everything. |
| `ao plugin uninstall <reference>` | Remove a plugin from the config. | | `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:** **Shell safety and HTTP:**
| Export | Purpose | | Export | Purpose |
|--------|---------| | ------------- | --------------------------------------------------------------------------------------- |
| `shellEscape` | Safely escape command-line arguments. Use for every argument passed to child processes. | | `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):** **Activity detection (agent plugins):**
| Export | Purpose | | Export | Purpose |
|--------|---------| | -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `readLastJsonlEntry` | Efficiently read the last entry from an agent's native JSONL log. | | `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`). | | `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. | | `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. | | `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. | | `recordTerminalActivity` | Shared `recordActivity` implementation — classifies, deduplicates, and appends to the activity JSONL. |
| `classifyTerminalActivity` | Classify terminal output via a `detectActivity` function. | | `classifyTerminalActivity` | Classify terminal output via a `detectActivity` function. |
| `appendActivityEntry` | Low-level JSONL append for activity entries. | | `appendActivityEntry` | Low-level JSONL append for activity entries. |
**Workspace setup (agent plugins):** **Workspace setup (agent plugins):**
| Export | Purpose | | 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). | | `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. | | `buildAgentPath` | Prepend `~/.ao/bin` to a PATH string. |
| `normalizeAgentPermissionMode` | Normalize legacy permission mode aliases (e.g. `"skip"` → `"permissionless"`). | | `normalizeAgentPermissionMode` | Normalize legacy permission mode aliases (e.g. `"skip"` → `"permissionless"`). |
**Constants:** **Constants:**
| Export | Value | | Export | Value |
|--------|-------| | ----------------------------- | ------------------------------------------------------------------------------------------------ |
| `DEFAULT_READY_THRESHOLD_MS` | `300_000` (5 min) — ready → idle threshold | | `DEFAULT_READY_THRESHOLD_MS` | `300_000` (5 min) — ready → idle threshold |
| `DEFAULT_ACTIVE_WINDOW_MS` | `30_000` (30 s) — active → ready window | | `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 | | `ACTIVITY_INPUT_STALENESS_MS` | Deprecated compatibility export (`300_000`); waiting_input/blocked no longer expire by wallclock |
| `PREFERRED_GH_PATH` | `/usr/local/bin/gh` | | `PREFERRED_GH_PATH` | `/usr/local/bin/gh` |
| `CI_STATUS` | `{ PENDING, PASSING, FAILING, NONE }` | | `CI_STATUS` | `{ PENDING, PASSING, FAILING, NONE }` |
| `ACTIVITY_STATE` | `{ ACTIVE, READY, IDLE, WAITING_INPUT, BLOCKED, EXITED }` | | `ACTIVITY_STATE` | `{ ACTIVE, READY, IDLE, WAITING_INPUT, BLOCKED, EXITED }` |
| `SESSION_STATUS` | Full session status constant map | | `SESSION_STATUS` | Full session status constant map |
**Types:** `Session`, `ProjectConfig`, `RuntimeHandle` (and everything else in `types.ts`). **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() })); vi.mock("node:fs/promises", () => ({ readFile: vi.fn() }));
describe("manifest", () => { describe("manifest", () => {
it("has correct slot and name", () => { it("has correct slot and name", () => {
expect(pluginModule.manifest.slot).toBe("notifier"); expect(pluginModule.manifest.slot).toBe("notifier");
expect(pluginModule.manifest.name).toBe("pagerduty"); expect(pluginModule.manifest.name).toBe("pagerduty");
}); });
}); });
describe("create()", () => { describe("create()", () => {
beforeEach(() => vi.clearAllMocks()); beforeEach(() => vi.clearAllMocks());
it("throws when routing_key is missing", () => { it("throws when routing_key is missing", () => {
expect(() => pluginModule.create({})).toThrow("routing_key"); expect(() => pluginModule.create({})).toThrow("routing_key");
}); });
it("returns a notifier with notify()", () => { it("returns a notifier with notify()", () => {
const notifier = pluginModule.create({ routing_key: "test-key" }); const notifier = pluginModule.create({ routing_key: "test-key" });
expect(typeof notifier.notify).toBe("function"); expect(typeof notifier.notify).toBe("function");
}); });
}); });
``` ```
@ -524,13 +526,13 @@ For agent plugins, the required `getActivityState` tests are listed in the CLAUD
## Common pitfalls ## Common pitfalls
| Pitfall | Correct approach | | Pitfall | Correct approach |
|---------|-----------------| | ------------------------------------ | ------------------------------------------------------------------ |
| Hardcoded secrets (API keys, tokens) | Read from `process.env`, throw if required and missing | | 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 | | Shell injection | Use `shellEscape()` for every argument passed to child processes |
| Reading large log files in full | Use `readLastJsonlEntry()` or stream the tail | | 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 | | 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 | | 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. 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"> <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> </Callout>
The required 4-step cascade: 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 ## Next steps
<Cards> <Cards>
<Card title="Architecture" description="Understand the plugin registry, session lifecycle, and how slots wire together." href="/docs/architecture" /> <Card
<Card title="Plugin catalog" description="Browse every bundled plugin grouped by slot." href="/docs/plugins" /> title="Architecture"
<Card title="Configuration reference" description="How plugins are referenced in agent-orchestrator.yaml." href="/docs/configuration" /> 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> </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 ## Slots at a glance
| Slot | Default | What it does | | Slot | Default | What it does |
|---|---|---| | ------------- | ----------------------------------------- | -------------------------------------------- |
| **Agent** | `claude-code` | Which AI tool writes the code | | **Agent** | `claude-code` | Which AI tool writes the code |
| **Runtime** | `tmux` (macOS/Linux), `process` (Windows) | Where the agent process runs | | **Runtime** | `tmux` (macOS/Linux), `process` (Windows) | Where the agent process runs |
| **Workspace** | `worktree` | Per-session code isolation | | **Workspace** | `worktree` | Per-session code isolation |
| **Tracker** | `github` | Where issues live | | **Tracker** | `github` | Where issues live |
| **SCM** | `github` | PRs, CI, reviews | | **SCM** | `github` | PRs, CI, reviews |
| **Notifier** | `desktop` | Who pings you when something happens | | **Notifier** | `desktop` | Who pings you when something happens |
| **Terminal** | `iterm2` on macOS | How you attach to a running agent | | **Terminal** | `iterm2` on macOS | How you attach to a running agent |
| **Lifecycle** | built-in | State machine + polling loop (not pluggable) | | **Lifecycle** | built-in | State machine + polling loop (not pluggable) |
## Agents ## Agents
<PluginGrid> <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
<PluginCard name="Codex" logo="codex" href="/docs/plugins/agents/codex" description="OpenAI Codex CLI. Session resume via codex resume <threadId>." /> name="Claude Code"
<PluginCard name="Cursor" logo="cursor" href="/docs/plugins/agents/cursor" description="Cursor Agent CLI. One-shot per session." /> logo="claude-code"
<PluginCard name="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI. No resume; PATH wrappers for PR tracking." /> href="/docs/plugins/agents/claude-code"
<PluginCard name="OpenCode" logo="opencode" href="/docs/plugins/agents/opencode" description="OpenCode terminal agent. Session discovery + restore via the OpenCode session API." /> 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> </PluginGrid>
## Runtimes ## Runtimes
<PluginGrid> <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
<PluginCard name="process" logo="windows" href="/docs/plugins/runtimes/process" description="Cross-platform child-process runtime. Required on Windows." /> 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> </PluginGrid>
## Workspaces ## Workspaces
<PluginGrid> <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
<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." /> 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> </PluginGrid>
## Trackers ## Trackers
<PluginGrid> <PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/trackers/github" description="Issues + PRs via the gh CLI." /> <PluginCard
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/trackers/gitlab" description="Issues + MRs via the glab CLI. Self-hosted supported." /> name="GitHub"
<PluginCard name="Linear" logo="linear" href="/docs/plugins/trackers/linear" description="Linear issues. Direct API or Composio-mediated." /> 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> </PluginGrid>
## SCM ## SCM
<PluginGrid> <PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/scm/github" description="PRs, reviews, and CI status via gh." /> <PluginCard
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/scm/gitlab" description="MRs, discussions, pipelines via glab." /> 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> </PluginGrid>
## Notifiers ## Notifiers
<PluginGrid> <PluginGrid>
<PluginCard name="Dashboard" logo="web" href="/docs/plugins/notifiers/dashboard" description="Retained notifications inside the AO dashboard." /> <PluginCard
<PluginCard name="Desktop" logo="apple" href="/docs/plugins/notifiers/desktop" description="Native macOS/Linux notifications. No-op on Windows." /> name="Dashboard"
<PluginCard name="Discord" logo="discord" href="/docs/plugins/notifiers/discord" description="Webhook-based Discord messages with rich embeds." /> logo="web"
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhooks." /> href="/docs/plugins/notifiers/dashboard"
<PluginCard name="Webhook" logo="webhook" href="/docs/plugins/notifiers/webhook" description="Generic HTTP POST with retries and exponential backoff." /> description="Retained notifications inside the AO dashboard."
<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="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> </PluginGrid>
## Terminals ## Terminals
<PluginGrid> <PluginGrid>
<PluginCard name="iTerm2" logo="iterm2" href="/docs/plugins/terminals/iterm2" description="Open attached tabs in iTerm2 via AppleScript (macOS only)." /> <PluginCard
<PluginCard name="Web" logo="web" href="/docs/plugins/terminals/web" description="Dashboard xterm.js session URL. Cross-platform." /> 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> </PluginGrid>
## Writing your own ## Writing your own

View File

@ -1,14 +1,5 @@
{ {
"title": "Plugins", "title": "Plugins",
"defaultOpen": false, "defaultOpen": false,
"pages": [ "pages": ["agents", "runtimes", "workspaces", "trackers", "scm", "notifiers", "terminals", "authoring"]
"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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="composio" size={28} /> <Logo name="composio" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>composio</code> Slot: <code>notifier</code> · Name: <code>composio</code>
</span> </span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="apple" size={28} color /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>desktop</code>
</span>
</div> </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 ## Setup
@ -36,11 +43,11 @@ notificationRouting:
action: [desktop] action: [desktop]
``` ```
| Config key | Default | What it does | | Config key | Default | What it does |
|---|---|---| | -------------- | ---------------------- | ------------------------------------------------------------ |
| `backend` | `auto` | `ao-app`, `terminal-notifier`, `osascript`, or auto fallback | | `backend` | `auto` | `ao-app`, `terminal-notifier`, `osascript`, or auto fallback |
| `dashboardUrl` | dashboard port | URL opened from desktop notification actions | | `dashboardUrl` | dashboard port | URL opened from desktop notification actions |
| `appPath` | macOS app install path | Custom AO Notifier.app path | | `appPath` | macOS app install path | Custom AO Notifier.app path |
## How it works ## 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="discord" size={28} color /> <Logo name="discord" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>discord</code> Slot: <code>notifier</code> · Name: <code>discord</code>
</span> </span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <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. Notifiers deliver AO events — session stuck, PR opened, review requested, CI failed — to wherever you actually pay attention.
<PluginGrid> <PluginGrid>
<PluginCard name="Dashboard" logo="web" href="/docs/plugins/notifiers/dashboard" description="Retained notifications inside the AO dashboard." /> <PluginCard
<PluginCard name="Desktop" logo="apple" href="/docs/plugins/notifiers/desktop" description="Native macOS/Linux notifications. No-op on Windows." /> name="Dashboard"
<PluginCard name="Discord" logo="discord" href="/docs/plugins/notifiers/discord" description="Discord webhook with rich embeds." /> logo="web"
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhook." /> href="/docs/plugins/notifiers/dashboard"
<PluginCard name="Webhook" logo="webhook" href="/docs/plugins/notifiers/webhook" description="Generic HTTP POST. Retries + exponential backoff." /> description="Retained notifications inside the AO dashboard."
<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="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> </PluginGrid>
## Stacking notifiers ## Stacking notifiers
@ -56,15 +86,15 @@ a real test message.
## What gets notified ## What gets notified
| Event | When | | Event | When |
|---|---| | ------------------- | -------------------------------------- |
| `session.spawned` | `ao spawn` succeeds | | `session.spawned` | `ao spawn` succeeds |
| `session.working` | Agent is actively editing code | | `session.working` | Agent is actively editing code |
| `pr.opened` | Agent pushed a branch and opened a PR | | `pr.opened` | Agent pushed a branch and opened a PR |
| `ci.failed` | Required check fails | | `ci.failed` | Required check fails |
| `review.requested` | Reviewer asks for changes | | `review.requested` | Reviewer asks for changes |
| `session.mergeable` | CI green, no blockers | | `session.mergeable` | CI green, no blockers |
| `session.merged` | PR merged | | `session.merged` | PR merged |
| `session.blocked` | Agent is stuck — you need to intervene | | `session.blocked` | Agent is stuck — you need to intervene |
You can tune which events fire via [`reactions` config](/docs/configuration). You can tune which events fire via [`reactions` config](/docs/configuration).

View File

@ -1,4 +1,4 @@
{ {
"title": "Notifiers", "title": "Notifiers",
"pages": ["dashboard", "desktop", "discord", "slack", "webhook", "composio", "openclaw"] "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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="openclaw" size={28} /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>openclaw</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <PlatformSupport macos="full" linux="full" windows="full" />
@ -58,29 +60,29 @@ The OpenClaw config keeps the actual secret:
```json title="~/.openclaw/openclaw.json" ```json title="~/.openclaw/openclaw.json"
{ {
"hooks": { "hooks": {
"enabled": true, "enabled": true,
"token": "<openclaw-hooks-token>", "token": "<openclaw-hooks-token>",
"allowRequestSessionKey": true, "allowRequestSessionKey": true,
"allowedSessionKeyPrefixes": ["hook:"] "allowedSessionKeyPrefixes": ["hook:"]
} }
} }
``` ```
## Config ## Config
| Key | Default | What it does | | Key | Default | What it does |
|---|---|---| | -------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `url` | `http://127.0.0.1:18789/hooks/agent` | OpenClaw hook endpoint | | `url` | `http://127.0.0.1:18789/hooks/agent` | OpenClaw hook endpoint |
| `openclawConfigPath` | `~/.openclaw/openclaw.json` | Local OpenClaw config path that contains `hooks.token` | | `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 | | `token` | — | Remote/manual fallback. Avoid for local setups because it stores the secret in AO config |
| `name` | — | Identifier shown in OpenClaw | | `name` | — | Identifier shown in OpenClaw |
| `sessionKeyPrefix` | — | Prefix for OpenClaw session keys | | `sessionKeyPrefix` | — | Prefix for OpenClaw session keys |
| `wakeMode` | `next-heartbeat` | `now` = deliver immediately, `next-heartbeat` = batch on next OpenClaw tick | | `wakeMode` | `next-heartbeat` | `now` = deliver immediately, `next-heartbeat` = batch on next OpenClaw tick |
| `deliver` | `true` | Disable to record-only without waking the user | | `deliver` | `true` | Disable to record-only without waking the user |
| `retries` | `3` | Retry count on transient failures | | `retries` | `3` | Retry count on transient failures |
| `retryDelayMs` | `1000` | Base retry delay | | `retryDelayMs` | `1000` | Base retry delay |
| `healthSummaryPath` | — | Optional local path where OpenClaw writes a summary AO can pick up | | `healthSummaryPath` | — | Optional local path where OpenClaw writes a summary AO can pick up |
## Token precedence ## 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="slack" size={28} color /> <Logo name="slack" size={28} color />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>slack</code> Slot: <code>notifier</code> · Name: <code>slack</code>
</span> </span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="webhook" size={28} /> <Logo name="webhook" size={28} />
<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>notifier</code> · Name: <code>webhook</code> Slot: <code>notifier</code> · Name: <code>webhook</code>
</span> </span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <PlatformSupport macos="full" linux="full" windows="full" />
@ -64,17 +64,17 @@ Every POST has a `type` discriminant. Most events use `notification`:
```json ```json
{ {
"type": "notification", "type": "notification",
"event": { "event": {
"id": "3f6a1b2c-...", "id": "3f6a1b2c-...",
"type": "pr.created", "type": "pr.created",
"priority": "info", "priority": "info",
"sessionId": "sess_01HXY...", "sessionId": "sess_01HXY...",
"projectId": "myproject", "projectId": "myproject",
"timestamp": "2026-04-17T15:00:00.000Z", "timestamp": "2026-04-17T15:00:00.000Z",
"message": "PR opened: fix(auth): handle token expiry", "message": "PR opened: fix(auth): handle token expiry",
"data": {} "data": {}
} }
} }
``` ```
@ -82,9 +82,9 @@ When actions are attached (e.g. approve/retry buttons in supported notifiers):
```json ```json
{ {
"type": "notification_with_actions", "type": "notification_with_actions",
"event": { "...": "same fields as above" }, "event": { "...": "same fields as above" },
"actions": [{ "label": "View PR", "url": "https://github.com/owner/repo/pull/123" }] "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 ```json
{ {
"type": "message", "type": "message",
"message": "Hello from AO", "message": "Hello from AO",
"context": {} "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: The runtime is where your agent's terminal actually lives. Two options ship:
| Plugin | Best for | Binary needed | | Plugin | Best for | Binary needed |
|---|---|---| | ----------------------------------------- | ---------------------------------------------------- | ------------- |
| [tmux](/docs/plugins/runtimes/tmux) | macOS / Linux default. You can attach interactively. | `tmux` | | [tmux](/docs/plugins/runtimes/tmux) | macOS / Linux default. You can attach interactively. | `tmux` |
| [process](/docs/plugins/runtimes/process) | Windows, Docker, CI-like environments | — | | [process](/docs/plugins/runtimes/process) | Windows, Docker, CI-like environments | — |
<PluginGrid> <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
<PluginCard name="process" logo="windows" href="/docs/plugins/runtimes/process" description="Cross-platform child process. Required on Windows." /> 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> </PluginGrid>
## Choosing ## 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 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`. - 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", "title": "Runtimes",
"pages": ["tmux", "process"] "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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="windows" size={28} color /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>runtime</code> · Name: <code>process</code>
</span>
</div> </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. 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="tmux" size={28} /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>runtime</code> · Name: <code>tmux</code>
</span>
</div> </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. 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 ## 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="github" size={28} /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>scm</code> · Name: <code>github</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <PlatformSupport macos="full" linux="full" windows="full" />
@ -53,20 +55,20 @@ projects:
repo: owner/repo repo: owner/repo
scm: scm:
webhook: 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: Full `webhook.*` sub-object:
| Field | Default | Description | | Field | Default | Description |
|---|---|---| | ----------------- | --------------------- | ------------------------------------------- |
| `enabled` | `true` | Enable or disable webhook processing | | `enabled` | `true` | Enable or disable webhook processing |
| `path` | `/api/webhooks` | Override the receive path | | `path` | `/api/webhooks` | Override the receive path |
| `secretEnvVar` | — | Name of the env var holding the HMAC secret | | `secretEnvVar` | — | Name of the env var holding the HMAC secret |
| `signatureHeader` | `x-hub-signature-256` | Header carrying the HMAC-SHA256 signature | | `signatureHeader` | `x-hub-signature-256` | Header carrying the HMAC-SHA256 signature |
| `eventHeader` | `x-github-event` | Header carrying the event type | | `eventHeader` | `x-github-event` | Header carrying the event type |
| `deliveryHeader` | `x-github-delivery` | Header carrying the delivery UUID | | `deliveryHeader` | `x-github-delivery` | Header carrying the delivery UUID |
| `maxBodyBytes` | unlimited | Reject payloads larger than this (bytes) | | `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. **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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="gitlab" size={28} color /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>scm</code> · Name: <code>gitlab</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <PlatformSupport macos="full" linux="full" windows="full" />
@ -19,16 +21,16 @@ glab auth login
```yaml title="agent-orchestrator.yaml" ```yaml title="agent-orchestrator.yaml"
scm: gitlab scm: gitlab
scmConfig: scmConfig:
host: gitlab.com # default; override for self-hosted host: gitlab.com # default; override for self-hosted
``` ```
## Mapping ## Mapping
| Concept | GitHub term | GitLab term | | Concept | GitHub term | GitLab term |
|---|---|---| | -------------- | ------------ | ----------------- |
| Review request | Pull request | Merge request | | Review request | Pull request | Merge request |
| Review | Review | Discussion / note | | Review | Review | Discussion / note |
| CI status | Check runs | Pipelines / jobs | | CI status | Check runs | Pipelines / jobs |
AO normalises these internally — the dashboard and lifecycle state machine don't know which you use. AO normalises these internally — the dashboard and lifecycle state machine don't know which you use.
@ -59,20 +61,20 @@ projects:
repo: group/project repo: group/project
scm: scm:
webhook: webhook:
secretEnvVar: GITLAB_WEBHOOK_TOKEN # env var holding the token secretEnvVar: GITLAB_WEBHOOK_TOKEN # env var holding the token
``` ```
Full `webhook.*` sub-object: Full `webhook.*` sub-object:
| Field | Default | Description | | Field | Default | Description |
|---|---|---| | ----------------- | --------------------- | ---------------------------------------- |
| `enabled` | `true` | Enable or disable webhook processing | | `enabled` | `true` | Enable or disable webhook processing |
| `path` | `/api/webhooks` | Override the receive path | | `path` | `/api/webhooks` | Override the receive path |
| `secretEnvVar` | — | Name of the env var holding the token | | `secretEnvVar` | — | Name of the env var holding the token |
| `signatureHeader` | `x-gitlab-token` | Header carrying the secret token | | `signatureHeader` | `x-gitlab-token` | Header carrying the secret token |
| `eventHeader` | `x-gitlab-event` | Header carrying the event type | | `eventHeader` | `x-gitlab-event` | Header carrying the event type |
| `deliveryHeader` | `x-gitlab-event-uuid` | Header carrying the delivery UUID | | `deliveryHeader` | `x-gitlab-event-uuid` | Header carrying the delivery UUID |
| `maxBodyBytes` | unlimited | Reject payloads larger than this (bytes) | | `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). **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:** **Hardcoded bots:**
| Username | | Username |
|---| | ------------------ |
| `gitlab-bot` | | `gitlab-bot` |
| `ghost` | | `ghost` |
| `dependabot[bot]` | | `dependabot[bot]` |
| `renovate[bot]` | | `renovate[bot]` |
| `sast-bot` | | `sast-bot` |
| `codeclimate[bot]` | | `codeclimate[bot]` |
| `sonarcloud[bot]` | | `sonarcloud[bot]` |
| `snyk-bot` | | `snyk-bot` |
**Runtime catch-all:** any username matching `/^project_\d+_bot/` (GitLab project access tokens) or ending in `[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. 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> <PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/scm/github" description="PRs, reviews, and CI status via gh." /> <PluginCard
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/scm/gitlab" description="MRs, discussions, pipelines via glab." /> 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> </PluginGrid>
## Why separate from the tracker ## Why separate from the tracker

View File

@ -1,4 +1,4 @@
{ {
"title": "SCM", "title": "SCM",
"pages": ["github", "gitlab"] "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. 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> <PluginGrid>
<PluginCard name="iTerm2" logo="iterm2" href="/docs/plugins/terminals/iterm2" description="Open attached tabs in iTerm2 via AppleScript (macOS only)." /> <PluginCard
<PluginCard name="Web" logo="web" href="/docs/plugins/terminals/web" description="Dashboard xterm.js session URL. Cross-platform." /> 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> </PluginGrid>
## Default ## 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="iterm2" size={28} /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>terminal</code> · Name: <code>iterm2</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="none" windows="none" /> <PlatformSupport macos="full" linux="none" windows="none" />

View File

@ -1,4 +1,4 @@
{ {
"title": "Terminals", "title": "Terminals",
"pages": ["iterm2", "web"] "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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="web" size={28} /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>terminal</code> · Name: <code>web</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <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" ```yaml title="agent-orchestrator.yaml"
terminal: web terminal: web
terminalConfig: 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. | | `dashboardUrl` | `http://localhost:3000` | Base URL for the dashboard. Override when the dashboard runs on another host/port. |
## How it works ## 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="github" size={28} /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>tracker</code> · Name: <code>github</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <PlatformSupport macos="full" linux="full" windows="full" />
@ -31,13 +33,13 @@ projects:
## How it's used ## How it's used
| Operation | `gh` command invoked | | Operation | `gh` command invoked |
|---|---| | ---------------- | ------------------------------------------- |
| Fetch issue | `gh issue view <num> --json title,body,...` | | Fetch issue | `gh issue view <num> --json title,body,...` |
| Create issue | `gh issue create` | | Create issue | `gh issue create` |
| Comment on issue | `gh issue comment` | | Comment on issue | `gh issue comment` |
| Close issue | `gh issue close` | | Close issue | `gh issue close` |
| List issues | `gh issue list` | | List issues | `gh issue list` |
## Config ## 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="gitlab" size={28} color /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>tracker</code> · Name: <code>gitlab</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <PlatformSupport macos="full" linux="full" windows="full" />
@ -21,18 +23,19 @@ glab auth login
```yaml title="agent-orchestrator.yaml" ```yaml title="agent-orchestrator.yaml"
tracker: tracker:
name: gitlab name: gitlab
host: gitlab.com # default; override for self-hosted host: gitlab.com # default; override for self-hosted
projects: projects:
myproject: myproject:
repo: group/project repo: group/project
``` ```
| Config key | Default | What it does | | Config key | Default | What it does |
|---|---|---| | ---------- | ------------ | ---------------------------------------------------- |
| `host` | `gitlab.com` | GitLab hostname — override for self-hosted instances | | `host` | `gitlab.com` | GitLab hostname — override for self-hosted instances |
<Callout type="warn"> <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> </Callout>
## Self-hosted ## 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. The **tracker** plugin is how AO fetches issues, creates new ones, and links sessions to them. Three trackers ship.
<PluginGrid> <PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/trackers/github" description="Issues + PRs via the gh CLI. Default." /> <PluginCard
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/trackers/gitlab" description="Issues + MRs via the glab CLI. Self-hosted supported." /> name="GitHub"
<PluginCard name="Linear" logo="linear" href="/docs/plugins/trackers/linear" description="Linear issues. Direct API key or Composio-mediated." /> 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> </PluginGrid>
## What a tracker does ## 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" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
<Logo name="linear" size={28} color /> <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> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>tracker</code> · Name: <code>linear</code>
</span>
</div> </div>
<PlatformSupport macos="full" linux="full" windows="full" /> <PlatformSupport macos="full" linux="full" windows="full" />
@ -31,8 +33,8 @@ The Linear tracker supports two transports and auto-picks between them:
api: api:
tracker: linear tracker: linear
trackerConfig: trackerConfig:
teamId: TEAM-123 # required for issue creation teamId: TEAM-123 # required for issue creation
workspaceSlug: myteam # optional, used to build issue URLs workspaceSlug: myteam # optional, used to build issue URLs
``` ```
### Composio-mediated ### Composio-mediated
@ -48,10 +50,10 @@ AO detects this and routes Linear calls through Composio — no separate Linear
## Per-project config ## Per-project config
| Key | Required | What it does | | Key | Required | What it does |
|---|---|---| | --------------- | --------------------- | ---------------------------------------------------------- |
| `teamId` | ✓ (for `createIssue`) | Linear team the issue lives in | | `teamId` | ✓ (for `createIssue`) | Linear team the issue lives in |
| `workspaceSlug` | optional | Used to render `https://linear.app/{slug}/issue/{id}` URLs | | `workspaceSlug` | optional | Used to render `https://linear.app/{slug}/issue/{id}` URLs |
## Branch names ## Branch names

View File

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

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