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

@ -63,7 +63,7 @@ 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`. |
@ -96,7 +96,7 @@ 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. |

View File

@ -11,7 +11,7 @@ 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. |

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

@ -8,7 +8,7 @@ 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. |
@ -26,7 +26,7 @@ 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. |

View File

@ -41,7 +41,7 @@ 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 |
@ -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

@ -19,7 +19,7 @@ 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. |
@ -71,7 +71,7 @@ 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. |

View File

@ -1,10 +1,5 @@
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";
@ -55,9 +50,7 @@ export async function generateStaticParams() {
return source.generateParams(); return source.generateParams();
} }
export async function generateMetadata({ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
params,
}: PageProps): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
const page = source.getPage(slug); const page = source.getPage(slug);
if (!page) { if (!page) {

View File

@ -199,11 +199,7 @@
#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 */
@ -512,7 +508,10 @@ pre.shiki code {
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 {

View File

@ -51,10 +51,9 @@ const links: LinkItemType[] = [
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;
@ -66,14 +65,7 @@ async function GitHubStars() {
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"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" /> <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> </svg>
{stars} {stars}
@ -83,24 +75,14 @@ async function GitHubStars() {
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" }}
search={{ options: { type: "static" } }}
>
<DocsLayout <DocsLayout
tree={source.pageTree} tree={source.pageTree}
links={links} links={links}
nav={{ nav={{
title: ( title: (
<span className="flex items-center gap-2 font-semibold"> <span className="flex items-center gap-2 font-semibold">
<img <img src="/ao-logo.svg" alt="" aria-hidden="true" width={22} height={22} className="h-[22px] w-[22px]" />
src="/ao-logo.svg"
alt=""
aria-hidden="true"
width={22}
height={22}
className="h-[22px] w-[22px]"
/>
<span className="text-[var(--color-text-primary)]">AO</span> <span className="text-[var(--color-text-primary)]">AO</span>
</span> </span>
), ),

View File

@ -27,13 +27,19 @@ export default async function LandingPage() {
<LandingAbout /> <LandingAbout />
<LandingAgentsBar /> <LandingAgentsBar />
<LandingFeatures /> <LandingFeatures />
<div id="workflow"><LandingWorkflow /></div> <div id="workflow">
<div id="usecases"><LandingUseCases /></div> <LandingWorkflow />
</div>
<div id="usecases">
<LandingUseCases />
</div>
<LandingHowItWorks /> <LandingHowItWorks />
<LandingVideo /> <LandingVideo />
<LandingStats stats={githubStats} /> <LandingStats stats={githubStats} />
<LandingTestimonials /> <LandingTestimonials />
<div id="quickstart"><LandingQuickStart /></div> <div id="quickstart">
<LandingQuickStart />
</div>
<LandingCTA /> <LandingCTA />
<footer className="py-12 px-8 text-center text-[var(--landing-muted)] opacity-30 text-[0.8125rem] border-t border-white/[0.04]"> <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 MIT Licensed · Open Source

View File

@ -14,10 +14,16 @@ export function LandingAbout() {
<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
href="/docs/"
className="underline decoration-[var(--landing-border-default)] underline-offset-4 hover:text-white"
>
docs quickstart and configuration guides
</a>
.
</p> </p>
{/* Config preview — show how simple setup is */} {/* Config preview — show how simple setup is */}

View File

@ -35,14 +35,8 @@ export function LandingAgentsBar() {
<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}
className="w-8 h-8 rounded-md object-contain"
/>
<div className="text-[0.6875rem] font-mono text-[var(--landing-muted)] opacity-50">
{agent.name}
</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -15,13 +15,11 @@ export function LandingDifferentiators() {
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>{" "}
agent orchestrator
</h2> </h2>
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.7] max-w-[36rem] mb-12"> <p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.7] max-w-[36rem] mb-12">
Conductor, T3 Code, and Codex App are native Mac apps. AO runs in Conductor, T3 Code, and Codex App are native Mac apps. AO runs in your browser, works on any OS, and you can
your browser, works on any OS, and you can self-host or extend it. self-host or extend it.
</p> </p>
</div> </div>
<div className="landing-reveal landing-card rounded-2xl overflow-hidden"> <div className="landing-reveal landing-card rounded-2xl overflow-hidden">
@ -45,12 +43,8 @@ export function LandingDifferentiators() {
key={row.feature} key={row.feature}
className={i < rows.length - 1 ? "border-b border-[var(--landing-border-subtle)]" : ""} 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"> <td className="px-6 py-3.5 text-[0.8125rem] text-[var(--landing-fg)]/80">{row.feature}</td>
{row.feature} <td className="px-6 py-3.5 text-center text-[rgba(134,239,172,0.8)]"></td>
</td>
<td className="px-6 py-3.5 text-center text-[rgba(134,239,172,0.8)]">
</td>
<td className="px-6 py-3.5 text-center text-[0.75rem] text-[var(--landing-muted)] opacity-40"> <td className="px-6 py-3.5 text-center text-[0.75rem] text-[var(--landing-muted)] opacity-40">
{row.others} {row.others}
</td> </td>

View File

@ -145,21 +145,15 @@ export function LandingFeatures() {
marginBottom: "1.5rem", marginBottom: "1.5rem",
transformOrigin: "center top", transformOrigin: "center top",
transition: "transform 0.4s ease, opacity 0.4s ease, border-color 0.2s ease", transition: "transform 0.4s ease, opacity 0.4s ease, border-color 0.2s ease",
...(stack ...(stack ? { position: "sticky", top: `${BASE_TOP + i * STACK_GAP}px`, zIndex: i + 1 } : null),
? { position: "sticky", top: `${BASE_TOP + i * STACK_GAP}px`, zIndex: i + 1 }
: null),
}} }}
> >
<div> <div>
<div className="font-mono text-xs tracking-[0.1em] text-[var(--landing-muted)] opacity-50 mb-4"> <div className="font-mono text-xs tracking-[0.1em] text-[var(--landing-muted)] opacity-50 mb-4">
{f.n} {f.n}
</div> </div>
<h3 className="font-sans font-[680] tracking-tight text-[1.375rem] mb-4"> <h3 className="font-sans font-[680] tracking-tight text-[1.375rem] mb-4">{f.title}</h3>
{f.title} <p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.7] max-w-[28rem]">{f.desc}</p>
</h3>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.7] max-w-[28rem]">
{f.desc}
</p>
</div> </div>
<FeatureDemo kind={f.demo} /> <FeatureDemo kind={f.demo} />
</div> </div>
@ -196,13 +190,8 @@ function ParallelBack() {
className="bg-[rgba(255,240,220,0.035)] border border-[var(--landing-border-subtle)] rounded-xl p-3 flex flex-col" className="bg-[rgba(255,240,220,0.035)] border border-[var(--landing-border-subtle)] rounded-xl p-3 flex flex-col"
> >
<div className="flex items-center gap-1.5 mb-1"> <div className="flex items-center gap-1.5 mb-1">
<span <span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: a.color }} />
className="w-1.5 h-1.5 rounded-full shrink-0" <span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/85 truncate">{a.name}</span>
style={{ background: a.color }}
/>
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/85 truncate">
{a.name}
</span>
</div> </div>
<div className="font-mono text-[0.625rem] text-[var(--landing-muted)] opacity-65 mb-auto truncate"> <div className="font-mono text-[0.625rem] text-[var(--landing-muted)] opacity-65 mb-auto truncate">
{a.task} {a.task}
@ -243,13 +232,8 @@ function ParallelFront() {
key={a.name} key={a.name}
className="flex items-center gap-2 bg-[rgba(255,240,220,0.04)] border border-[var(--landing-border-subtle)] rounded-md px-2.5 py-1.5" className="flex items-center gap-2 bg-[rgba(255,240,220,0.04)] border border-[var(--landing-border-subtle)] rounded-md px-2.5 py-1.5"
> >
<span <span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: a.color }} />
className="w-1.5 h-1.5 rounded-full shrink-0" <span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/85 truncate">{a.name}</span>
style={{ background: a.color }}
/>
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/85 truncate">
{a.name}
</span>
</div> </div>
))} ))}
</div> </div>
@ -284,9 +268,7 @@ function RecoveryBack() {
<div className="p-5 h-full flex flex-col font-mono text-[0.6875rem]"> <div className="p-5 h-full flex flex-col font-mono text-[0.6875rem]">
<div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]"> <div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]">
<span className="text-[var(--landing-fg)]/80">PR #312 · feat/user-auth</span> <span className="text-[var(--landing-fg)]/80">PR #312 · feat/user-auth</span>
<span className="text-[0.5625rem] uppercase tracking-[0.1em] text-[var(--landing-muted-dim)]"> <span className="text-[0.5625rem] uppercase tracking-[0.1em] text-[var(--landing-muted-dim)]">healing</span>
healing
</span>
</div> </div>
<div className="flex-1 space-y-1.5 overflow-hidden"> <div className="flex-1 space-y-1.5 overflow-hidden">
{visible.map((s, i) => { {visible.map((s, i) => {
@ -304,9 +286,7 @@ function RecoveryBack() {
key={`${i}-${s.text}`} key={`${i}-${s.text}`}
className={`flex items-baseline gap-2.5 ${isLast ? "landing-stream-line" : ""}`} className={`flex items-baseline gap-2.5 ${isLast ? "landing-stream-line" : ""}`}
> >
<span className="text-[var(--landing-muted-dim)] opacity-50 w-9 shrink-0"> <span className="text-[var(--landing-muted-dim)] opacity-50 w-9 shrink-0">{s.time}</span>
{s.time}
</span>
<span className={`${color} truncate`}>{s.text}</span> <span className={`${color} truncate`}>{s.text}</span>
</div> </div>
); );
@ -320,16 +300,12 @@ function RecoveryFront() {
return ( return (
<div className="grid grid-cols-2 gap-2.5 p-5 w-full h-full items-stretch"> <div className="grid grid-cols-2 gap-2.5 p-5 w-full h-full items-stretch">
<div className="bg-[rgba(248,113,113,0.05)] border border-[rgba(248,113,113,0.18)] rounded-xl p-3 flex flex-col items-center justify-center gap-1"> <div className="bg-[rgba(248,113,113,0.05)] border border-[rgba(248,113,113,0.18)] rounded-xl p-3 flex flex-col items-center justify-center gap-1">
<span className="font-mono text-[0.5rem] tracking-[0.12em] uppercase text-[rgba(248,113,113,0.7)]"> <span className="font-mono text-[0.5rem] tracking-[0.12em] uppercase text-[rgba(248,113,113,0.7)]">before</span>
before
</span>
<span className="text-[1.75rem] leading-none text-[rgba(248,113,113,0.85)]"></span> <span className="text-[1.75rem] leading-none text-[rgba(248,113,113,0.85)]"></span>
<span className="font-mono text-[0.625rem] text-[var(--landing-fg)]/70">12/48</span> <span className="font-mono text-[0.625rem] text-[var(--landing-fg)]/70">12/48</span>
</div> </div>
<div className="bg-[rgba(134,239,172,0.05)] border border-[rgba(134,239,172,0.2)] rounded-xl p-3 flex flex-col items-center justify-center gap-1"> <div className="bg-[rgba(134,239,172,0.05)] border border-[rgba(134,239,172,0.2)] rounded-xl p-3 flex flex-col items-center justify-center gap-1">
<span className="font-mono text-[0.5rem] tracking-[0.12em] uppercase text-[rgba(134,239,172,0.7)]"> <span className="font-mono text-[0.5rem] tracking-[0.12em] uppercase text-[rgba(134,239,172,0.7)]">after</span>
after
</span>
<span className="text-[1.75rem] leading-none text-[rgba(134,239,172,0.85)]"></span> <span className="text-[1.75rem] leading-none text-[rgba(134,239,172,0.85)]"></span>
<span className="font-mono text-[0.625rem] text-[var(--landing-fg)]/70">48/48</span> <span className="font-mono text-[0.625rem] text-[var(--landing-fg)]/70">48/48</span>
</div> </div>
@ -357,9 +333,7 @@ function PluginsBack() {
return ( return (
<div className="p-5 h-full flex flex-col"> <div className="p-5 h-full flex flex-col">
<div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]"> <div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]">
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/80"> <span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/80">agent-orchestrator.yaml</span>
agent-orchestrator.yaml
</span>
<span className="font-mono text-[0.5625rem] tracking-[0.1em] uppercase text-[var(--landing-muted-dim)]"> <span className="font-mono text-[0.5625rem] tracking-[0.1em] uppercase text-[var(--landing-muted-dim)]">
7 slots 7 slots
</span> </span>
@ -369,9 +343,7 @@ function PluginsBack() {
const val = s.values[(tick + i) % s.values.length]; const val = s.values[(tick + i) % s.values.length];
return ( return (
<div key={s.slot} className="flex items-center gap-3"> <div key={s.slot} className="flex items-center gap-3">
<span className="text-[var(--landing-muted-dim)] w-[4.5rem] shrink-0"> <span className="text-[var(--landing-muted-dim)] w-[4.5rem] shrink-0">{s.slot}:</span>
{s.slot}:
</span>
<span <span
key={val} key={val}
className="landing-chip-swap inline-block px-2 py-[1px] rounded-md bg-[rgba(255,240,220,0.05)] text-[var(--landing-fg)]/85 border border-[var(--landing-border-subtle)]" className="landing-chip-swap inline-block px-2 py-[1px] rounded-md bg-[rgba(255,240,220,0.05)] text-[var(--landing-fg)]/85 border border-[var(--landing-border-subtle)]"
@ -448,9 +420,7 @@ function DashboardBack() {
return prev.map((c) => ({ ...c, col: 0 as 0 | 1 | 2 })); return prev.map((c) => ({ ...c, col: 0 as 0 | 1 | 2 }));
} }
const oldest = advanceable[0]; const oldest = advanceable[0];
return prev.map((c) => return prev.map((c) => (c.id === oldest.id ? { ...c, col: (c.col + 1) as 0 | 1 | 2 } : c));
c.id === oldest.id ? { ...c, col: (c.col + 1) as 0 | 1 | 2 } : c,
);
}); });
}, 2400); }, 2400);
return () => clearInterval(id); return () => clearInterval(id);
@ -459,9 +429,7 @@ function DashboardBack() {
return ( return (
<div className="p-5 h-full flex flex-col"> <div className="p-5 h-full flex flex-col">
<div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]"> <div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]">
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/80"> <span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/80">my-saas-app · 4 sessions</span>
my-saas-app · 4 sessions
</span>
<span className="font-mono text-[0.5625rem] tracking-[0.1em] uppercase text-[var(--landing-muted-dim)] flex items-center gap-1.5"> <span className="font-mono text-[0.5625rem] tracking-[0.1em] uppercase text-[var(--landing-muted-dim)] flex items-center gap-1.5">
<span className="w-1 h-1 rounded-full bg-[rgba(134,239,172,0.7)] landing-sse-pulse" /> <span className="w-1 h-1 rounded-full bg-[rgba(134,239,172,0.7)] landing-sse-pulse" />
sse sse
@ -482,13 +450,8 @@ function DashboardBack() {
> >
<div className="text-[var(--landing-fg)]/85 truncate mb-1">{c.title}</div> <div className="text-[var(--landing-fg)]/85 truncate mb-1">{c.title}</div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span <span className="w-1 h-1 rounded-full shrink-0" style={{ background: c.color }} />
className="w-1 h-1 rounded-full shrink-0" <span className="font-mono text-[0.5rem] text-[var(--landing-muted-dim)] truncate">{c.agent}</span>
style={{ background: c.color }}
/>
<span className="font-mono text-[0.5rem] text-[var(--landing-muted-dim)] truncate">
{c.agent}
</span>
</div> </div>
</div> </div>
))} ))}
@ -546,12 +509,9 @@ function DashboardFront() {
{stream.map((l) => ( {stream.map((l) => (
<div <div
key={l.id} key={l.id}
className={`truncate transition-opacity duration-200 ${ className={`truncate transition-opacity duration-200 ${l.exiting ? "opacity-0" : "landing-stream-line"}`}
l.exiting ? "opacity-0" : "landing-stream-line"
}`}
> >
<span className="text-[rgba(134,239,172,0.7)]"></span>{" "} <span className="text-[rgba(134,239,172,0.7)]"></span> <span className="opacity-70">{l.text}</span>
<span className="opacity-70">{l.text}</span>
</div> </div>
))} ))}
</div> </div>

View File

@ -16,9 +16,8 @@ export function LandingHero({ starsLabel }: LandingHeroProps) {
<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">

View File

@ -78,12 +78,9 @@ export function LandingHowItWorks() {
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
</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 steps to{" "} Three steps to <em className="italic text-[var(--landing-muted)]">orchestration</em>
<em className="italic text-[var(--landing-muted)]">orchestration</em>
</h2> </h2>
</div> </div>
@ -110,11 +107,7 @@ export function LandingHowItWorks() {
}} }}
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
? "1 1 0%"
: "0 1 15rem"
: "0 0 auto",
transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)", transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)",
}} }}
> >
@ -132,17 +125,13 @@ export function LandingHowItWorks() {
<h3 <h3
className="font-sans font-[680] tracking-tight text-[1.375rem] leading-[1.15]" className="font-sans font-[680] tracking-tight text-[1.375rem] leading-[1.15]"
style={{ style={{
color: isActive color: isActive ? "var(--landing-fg)" : "var(--landing-muted)",
? "var(--landing-fg)"
: "var(--landing-muted)",
transition: "color 0.4s ease", transition: "color 0.4s ease",
maxWidth: isActive ? "100%" : "11rem", maxWidth: isActive ? "100%" : "11rem",
}} }}
> >
{step.title.replace(` ${step.titleEm}`, "")}{" "} {step.title.replace(` ${step.titleEm}`, "")}{" "}
<em className="italic text-[var(--landing-muted)]"> <em className="italic text-[var(--landing-muted)]">{step.titleEm}</em>
{step.titleEm}
</em>
</h3> </h3>
{/* Expanding body */} {/* Expanding body */}
@ -225,7 +214,9 @@ function CliDemo() {
<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">
5 agents working · Dashboard http://localhost:3000
</span>
</div> </div>
</div> </div>
</div> </div>
@ -242,19 +233,22 @@ function DashboardDemo() {
<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="Working"
cards={[
{ title: "Add user auth flow", meta: "#42 · feat/auth", agent: "claude-code" }, { title: "Add user auth flow", meta: "#42 · feat/auth", agent: "claude-code" },
{ title: "Fix pagination bug", meta: "#43 · fix/pagination", agent: "codex" }, { title: "Fix pagination bug", meta: "#43 · fix/pagination", agent: "codex" },
]} /> ]}
<DashColumn title="Pending" cards={[ />
{ title: "Add rate limiting", meta: "#44 · PR #312", agent: "aider" }, <DashColumn title="Pending" cards={[{ title: "Add rate limiting", meta: "#44 · PR #312", agent: "aider" }]} />
]} /> <DashColumn
<DashColumn title="Review" cards={[ title="Review"
{ title: "Update API tests", meta: "#45 · PR #310", agent: "claude-code", amber: true }, cards={[{ title: "Update API tests", meta: "#45 · PR #310", agent: "claude-code", amber: true }]}
]} /> />
<DashColumn title="Merged" cards={[ <DashColumn
{ title: "Refactor DB layer", meta: "#46 · PR #308", agent: "opencode", done: true }, title="Merged"
]} /> cards={[{ title: "Refactor DB layer", meta: "#46 · PR #308", agent: "opencode", done: true }]}
/>
</div> </div>
</div> </div>
); );
@ -269,7 +263,10 @@ function PrsDemo() {
{ 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
key={pr.branch}
className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-xl px-5 py-4 flex items-center justify-between"
>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="font-mono text-xs text-[var(--landing-fg)]/70">{pr.branch}</div> <div className="font-mono text-xs text-[var(--landing-fg)]/70">{pr.branch}</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)]">{pr.title}</div> <div className="text-[0.8125rem] text-[var(--landing-muted)]">{pr.title}</div>
@ -298,14 +295,20 @@ function DashColumn({ title, cards }: { title: string; cards: DashCardData[] })
{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
key={card.meta}
className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-lg p-2.5 mb-1.5 text-[0.6875rem]"
>
<div className="text-[var(--landing-fg)]/70 mb-1">{card.title}</div> <div className="text-[var(--landing-fg)]/70 mb-1">{card.title}</div>
<div className="font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-50">{card.meta}</div> <div className="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"> <div className="flex items-center gap-1 mt-1 font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-60">
{card.done ? ( {card.done ? (
<span></span> <span></span>
) : ( ) : (
<span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: card.amber ? "rgba(251,191,36,0.7)" : "rgba(134,239,172,0.7)" }} /> <span
className="inline-block w-1.5 h-1.5 rounded-full"
style={{ background: card.amber ? "rgba(251,191,36,0.7)" : "rgba(134,239,172,0.7)" }}
/>
)} )}
{card.agent} {card.agent}
</div> </div>

View File

@ -36,17 +36,26 @@ export function LandingNav() {
</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
href="/docs"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Docs Docs
</a> </a>
</li> </li>
<li> <li>
<a href="#features" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"> <a
href="#features"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Features Features
</a> </a>
</li> </li>
<li> <li>
<a href="#how" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"> <a
href="#how"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
How It Works How It Works
</a> </a>
</li> </li>

View File

@ -1,6 +1,16 @@
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",
title: "Install",
desc: "One command. No dependencies beyond Node.js.",
cmd: "npm i -g @aoagents/ao",
},
{
num: "STEP 02",
title: "Configure",
desc: "Create an agent-orchestrator.yaml. Pick your agents, tracker, and notifiers.",
cmd: "ao start",
},
{ num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" }, { num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" },
]; ];
@ -12,8 +22,7 @@ export function LandingQuickStart() {
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">
@ -22,12 +31,8 @@ export function LandingQuickStart() {
<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"> <h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight">{s.title}</h3>
{s.title} <p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">{s.desc}</p>
</h3>
<p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">
{s.desc}
</p>
<div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg"> <div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg">
<span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd} <span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd}
</div> </div>
@ -35,7 +40,10 @@ export function LandingQuickStart() {
))} ))}
</div> </div>
<div className="landing-reveal mt-8 text-center"> <div className="landing-reveal mt-8 text-center">
<a href="/docs/" className="landing-card inline-flex rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline hover:text-white transition-colors"> <a
href="/docs/"
className="landing-card inline-flex rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Explore docs for setup and workflows Explore docs for setup and workflows
</a> </a>
</div> </div>

View File

@ -16,16 +16,11 @@ export function LandingStats({ stats }: LandingStatsProps) {
<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}
className="landing-card rounded-2xl py-8 px-6 text-center"
>
<div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1"> <div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1">
{stat.number} {stat.number}
</div> </div>
<div className="text-xs text-[var(--landing-muted)] opacity-60"> <div className="text-xs text-[var(--landing-muted)] opacity-60">{stat.label}</div>
{stat.label}
</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -4,8 +4,7 @@ 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",
@ -44,10 +43,7 @@ export function LandingTestimonials() {
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),
ROTATE_MS,
);
return () => window.clearTimeout(t); return () => window.clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, paused]); }, [active, paused]);
@ -65,11 +61,7 @@ export function LandingTestimonials() {
</h2> </h2>
</div> </div>
<div <div className="landing-reveal mt-16" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
className="landing-reveal mt-16"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Quote — fades on change */} {/* Quote — fades on change */}
<div className="min-h-[8rem] max-w-[58rem]"> <div className="min-h-[8rem] max-w-[58rem]">
<blockquote <blockquote
@ -128,10 +120,7 @@ export function LandingTestimonials() {
</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
@ -140,12 +129,8 @@ export function LandingTestimonials() {
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 className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60">
{t.role}
</div>
</div> </div>
</div> </div>

View File

@ -135,8 +135,7 @@ export function LandingUseCases() {
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>
@ -157,10 +156,8 @@ export function LandingUseCases() {
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 <div
@ -201,10 +198,7 @@ export function LandingUseCases() {
<div className="font-mono text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-accent)] opacity-80"> <div className="font-mono text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-accent)] opacity-80">
{c.eyebrow} {c.eyebrow}
</div> </div>
<h3 <h3 className="font-sans font-[680] text-[1.3125rem] tracking-tight" style={{ marginTop: "1rem" }}>
className="font-sans font-[680] text-[1.3125rem] tracking-tight"
style={{ marginTop: "1rem" }}
>
{c.title} {c.title}
</h3> </h3>
<p <p
@ -218,12 +212,9 @@ export function LandingUseCases() {
style={{ marginTop: "auto", padding: "0.75rem 0.875rem" }} style={{ marginTop: "auto", padding: "0.75rem 0.875rem" }}
> >
<div className="whitespace-nowrap overflow-hidden text-ellipsis"> <div className="whitespace-nowrap overflow-hidden text-ellipsis">
<span className={dim}>{c.prefix}</span>{" "} <span className={dim}>{c.prefix}</span> <span className={fg}>{c.cmd}</span>
<span className={fg}>{c.cmd}</span>
</div>
<div className={`whitespace-nowrap overflow-hidden text-ellipsis ${ok}`}>
{c.outcome}
</div> </div>
<div className={`whitespace-nowrap overflow-hidden text-ellipsis ${ok}`}> {c.outcome}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -14,12 +14,42 @@ type Milestone = {
}; };
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)
@ -58,10 +88,7 @@ export function LandingWorkflow() {
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),
{ threshold: 0.25 },
);
ob.observe(el); ob.observe(el);
return () => ob.disconnect(); return () => ob.disconnect();
}, []); }, []);
@ -96,10 +123,7 @@ export function LandingWorkflow() {
// 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),
STEP_MS,
);
return () => window.clearTimeout(t); return () => window.clearTimeout(t);
}, [active, paused, inView]); }, [active, paused, inView]);
@ -189,9 +213,7 @@ export function LandingWorkflow() {
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>
<div className="font-mono text-[0.6875rem] tracking-[0.08em] text-[var(--landing-accent)] opacity-80 mt-1"> <div className="font-mono text-[0.6875rem] tracking-[0.08em] text-[var(--landing-accent)] opacity-80 mt-1">
{cur.key} {cur.key}
</div> </div>
@ -208,7 +230,15 @@ export function LandingWorkflow() {
> >
<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
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)" /> <circle cx={CENTER_X} cy={arcTop(0)} r={4} fill="var(--landing-accent)" />
{/* ticks */} {/* ticks */}
@ -257,7 +287,6 @@ export function LandingWorkflow() {
> >
<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>
); );

View File

@ -145,11 +145,6 @@ export function PageConstellation() {
}, []); }, []);
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

@ -12,7 +12,7 @@ export function ScrollRevealProvider({ children }: { children: React.ReactNode }
} }
}); });
}, },
{ threshold: 0.1, rootMargin: "-50px" } { threshold: 0.1, rootMargin: "-50px" },
); );
document.querySelectorAll(".landing-reveal").forEach((el) => { document.querySelectorAll(".landing-reveal").forEach((el) => {

View File

@ -19,29 +19,19 @@ export function DocsMissingPage() {
<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>
<div className="docs-missing-content"> <div className="docs-missing-content">
<section className="docs-missing-copy"> <section className="docs-missing-copy">
<h2> <h2>This page checked out the wrong worktree.</h2>
This page checked out the wrong worktree.
</h2>
<p> <p>
The docs were rebuilt, and this URL did not survive the merge. Start from the docs The docs were rebuilt, and this URL did not survive the merge. Start from the docs index, or use
index, or use search in the sidebar to find where it landed. search in the sidebar to find where it landed.
</p> </p>
<div className="docs-missing-actions"> <div className="docs-missing-actions">
<Link <Link href="/docs" className="docs-missing-primary">
href="/docs"
className="docs-missing-primary"
>
Browse docs Browse docs
</Link> </Link>
<Link <Link href="/" className="docs-missing-secondary">
href="/"
className="docs-missing-secondary"
>
Home Home
</Link> </Link>
</div> </div>

View File

@ -45,9 +45,7 @@ function Cell({ platform, status }: { platform: "macos" | "linux" | "windows"; s
> >
<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 <span
style={{ style={{
fontSize: "0.75rem", fontSize: "0.75rem",
@ -73,12 +71,7 @@ function Cell({ platform, status }: { platform: "macos" | "linux" | "windows"; s
); );
} }
export function PlatformSupport({ export function PlatformSupport({ macos = "full", linux = "full", windows = "full", note }: PlatformSupportProps) {
macos = "full",
linux = "full",
windows = "full",
note,
}: PlatformSupportProps) {
return ( return (
<div style={{ margin: "1.25rem 0" }}> <div style={{ margin: "1.25rem 0" }}>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}> <div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>

View File

@ -10,7 +10,7 @@ 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` |
@ -21,7 +21,9 @@ Each abstraction in AO is a named interface defined in `packages/core/src/types.
| 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>
--- ---
@ -57,7 +59,7 @@ 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 |
@ -100,7 +102,7 @@ Priority is inferred by `inferPriority()` in `lifecycle-manager.ts`:
- **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 |
@ -205,7 +207,7 @@ 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) |
@ -237,20 +239,22 @@ 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

@ -38,7 +38,7 @@ 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 |
@ -54,7 +54,7 @@ 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 |
@ -63,7 +63,7 @@ If AO is already running, you'll get an interactive prompt to open/restart/spawn
> 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 |
@ -76,7 +76,7 @@ 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 |
@ -86,7 +86,8 @@ If AO is already running, you'll get an interactive prompt to open/restart/spawn
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...>`
@ -94,7 +95,7 @@ Positional `[first]` — issue identifier. The project is auto-detected from `cw
> 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.
@ -104,7 +105,7 @@ 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 |
@ -116,7 +117,7 @@ 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.
@ -126,7 +127,7 @@ 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 |
@ -138,7 +139,7 @@ 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).
@ -148,7 +149,7 @@ 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 |
@ -161,7 +162,7 @@ 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 |
@ -172,7 +173,7 @@ 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 |
@ -192,7 +193,7 @@ 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 |
@ -210,7 +211,7 @@ 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 |
@ -226,7 +227,7 @@ 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`
@ -234,7 +235,7 @@ No flags. Attaches via `tmux attach-session`. Only meaningful with `runtime: tmu
> 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 |
@ -243,7 +244,7 @@ No flags. Attaches via `tmux attach-session`. Only meaningful with `runtime: tmu
> 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.
@ -259,7 +260,7 @@ 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
@ -270,7 +271,7 @@ 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 |
@ -288,7 +289,7 @@ writing `notificationRouting`.
> 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` |
@ -308,7 +309,7 @@ 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 |
@ -324,7 +325,7 @@ 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 |
@ -337,7 +338,7 @@ No flags.
> 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` |
@ -349,7 +350,7 @@ 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`.
@ -365,7 +366,7 @@ 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. |

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
@ -73,7 +75,7 @@ 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` |
@ -147,7 +149,7 @@ 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 |
@ -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

@ -35,7 +35,7 @@ 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 |
@ -52,7 +52,7 @@ 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 |
@ -69,7 +69,7 @@ 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 |
@ -84,7 +84,7 @@ 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 |
@ -173,7 +173,7 @@ 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 |
@ -181,7 +181,7 @@ Built-in trackers:
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 |
@ -234,7 +234,7 @@ 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 |
@ -249,7 +249,7 @@ 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 |
@ -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,13 +58,14 @@ 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 |
@ -79,7 +80,7 @@ reactions:
## 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 |
@ -91,7 +92,7 @@ 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 |
@ -173,7 +174,7 @@ 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` |
@ -188,6 +189,14 @@ ao review-check <session-id>
<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

@ -153,7 +153,7 @@ 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) |
@ -270,7 +270,7 @@ 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 |

View File

@ -22,7 +22,7 @@ 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 |
@ -59,7 +59,7 @@ 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 |
@ -84,7 +84,7 @@ 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` |
@ -120,7 +120,7 @@ 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 |
@ -136,7 +136,7 @@ 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` |
@ -164,6 +164,14 @@ Open the session detail page and send a short, direct instruction. The agent rec
<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

@ -11,23 +11,28 @@ import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
</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
`--agent`). See [Agents](/docs/plugins/agents).
</Accordion> </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
call.
</Accordion> </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
up only if you need near-instant reactions.
</Accordion> </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
`trackerConfig.host` / `scmConfig.host` in AO.
</Accordion> </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
desktop notifications are a no-op. Everything else (spawning, PR flow, review loop, CI recovery) works the same.
</Accordion> </Accordion>
<Accordion title="Where does session state live?"> <Accordion title="Where does session state live?">
@ -35,15 +40,18 @@ import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
</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,
so use your usual SSO / basic-auth layer. Use `runtime: process` in containers.
</Accordion> </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
shows per-session cost on the dashboard for agents that report it (Claude Code today).
</Accordion> </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
and makes GitHub API calls through your authenticated CLIs.
</Accordion> </Accordion>
<Accordion title="Can I write my own plugin?"> <Accordion title="Can I write my own plugin?">
@ -51,11 +59,13 @@ import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
</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
integration into core makes the CLI heavy. Plugins let you pick what you need.
</Accordion> </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
doesn't merge. The session works on its own branch.
</Accordion> </Accordion>
<Accordion title="How do I completely uninstall?"> <Accordion title="How do I completely uninstall?">

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

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

@ -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

@ -38,7 +38,7 @@ 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 |
@ -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

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

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
@ -71,7 +72,7 @@ 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 |
@ -86,7 +87,7 @@ Not every review comment is from a human. AO recognises a hardcoded list of know
**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 |

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
@ -30,7 +31,7 @@ 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 |
@ -49,7 +50,7 @@ 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 |
@ -65,16 +66,38 @@ Most users start with the defaults and only edit `agent-orchestrator.yaml` when
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
title="Installation"
description="Install AO, authenticate your tools, and run the doctor check."
href="/docs/installation"
/>
<Card title="Quickstart" description="Spawn one agent on one issue and watch it open a PR." href="/docs/quickstart" /> <Card title="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="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
<Card title="Plugins" description="See which agents, runtimes, trackers, SCMs, terminals, and notifiers are available." href="/docs/plugins" /> title="Guides"
<Card title="Troubleshooting" description="Fix common install, dashboard, runtime, and session problems." href="/docs/troubleshooting" /> 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,7 +11,8 @@ 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
@ -20,13 +21,18 @@ This page gets your machine ready to run Agent Orchestrator. By the end, the `ao
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. |
@ -38,29 +44,12 @@ If you plan to use GitLab or Linear, install and authenticate their CLIs or cred
## 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>
<Tab value="pnpm">
```bash
pnpm add -g @aoagents/ao
```
</Tab>
<Tab value="yarn">
```bash
yarn global add @aoagents/ao
```
</Tab>
<Tab value="from source"> <Tab value="from source">
```bash ```bash git clone https://github.com/ComposioHQ/agent-orchestrator cd agent-orchestrator pnpm install pnpm build
git clone https://github.com/ComposioHQ/agent-orchestrator pnpm --filter @aoagents/ao link --global ```
cd agent-orchestrator
pnpm install
pnpm build
pnpm --filter @aoagents/ao link --global
```
</Tab> </Tab>
</Tabs> </Tabs>
@ -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>
@ -181,10 +144,12 @@ Use Slack, Discord, webhook, or another network notifier for alerts. Desktop not
<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
shell and run `ao --version` again.
</Accordion> </Accordion>
<Accordion title="`gh` is not authenticated"> <Accordion title="`gh` is not authenticated">
Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is authenticated. Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is
authenticated.
</Accordion> </Accordion>
<Accordion title="tmux is missing"> <Accordion title="tmux is missing">
Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container. Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container.
@ -204,6 +169,10 @@ Use Slack, Discord, webhook, or another network notifier for alerts. Desktop not
<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
title="Configuration"
description="Understand the generated `agent-orchestrator.yaml`."
href="/docs/configuration"
/>
<Card title="Platforms" description="Choose the right runtime and notifier for your OS." href="/docs/platforms" /> <Card title="Platforms" description="Choose the right runtime and notifier for your OS." href="/docs/platforms" />
</Cards> </Cards>

View File

@ -19,7 +19,7 @@ The platform differences come from the tools used to run and attach to long-live
## 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. |
@ -47,7 +47,7 @@ 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 |
@ -55,7 +55,8 @@ What works well on macOS:
| 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
@ -80,7 +81,7 @@ 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 |
@ -92,7 +93,8 @@ If you are running AO on a headless Linux host, prefer Slack, Discord, or webhoo
## 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:
@ -107,7 +109,7 @@ 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 |
@ -117,7 +119,7 @@ What works:
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 |
@ -148,7 +150,7 @@ 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 |

View File

@ -7,7 +7,9 @@ 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.
@ -38,7 +40,7 @@ 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` |
@ -48,7 +50,8 @@ agent: aider
<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
`~/.aider.conf.yml`. AO won't inject this for you — it's your call whether the agent should auto-apply.
</Accordion> </Accordion>
<Accordion title="Dashboard cost shows $0"> <Accordion title="Dashboard cost shows $0">
AO doesn't parse Aider's cost output. The cost column stays empty. AO doesn't parse Aider's cost output. The cost column stays empty.

View File

@ -7,7 +7,9 @@ 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.
@ -44,7 +46,7 @@ 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 |

View File

@ -7,7 +7,9 @@ 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.
@ -38,7 +40,7 @@ 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 |

View File

@ -7,7 +7,9 @@ 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.
@ -50,7 +52,7 @@ 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` |
@ -60,9 +62,11 @@ The plugin's `detect()` checks `agent --help` output for the strings `Cursor Age
<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
tool's help, remove or shadow it, or reinstall Cursor Agent.
</Accordion> </Accordion>
<Accordion title="Dashboard never leaves `active`"> <Accordion title="Dashboard never leaves `active`">
Cursor doesn't emit its own session JSONL. AO classifies based on terminal output — if your terminal is being captured by another tool, the classification can drift. `ao doctor` will flag this. Cursor doesn't emit its own session JSONL. AO classifies based on terminal output — if your terminal is being
captured by another tool, the classification can drift. `ao doctor` will flag this.
</Accordion> </Accordion>
</Accordions> </Accordions>

View File

@ -6,13 +6,17 @@ 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` | ❌ |
@ -20,11 +24,21 @@ The agent is the piece that actually writes code. Everything else in AO — work
| [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
name="Claude Code"
logo="claude-code"
href="/docs/plugins/agents/claude-code"
description="Anthropic's CLI coding agent."
/>
<PluginCard name="Codex" logo="codex" href="/docs/plugins/agents/codex" description="OpenAI's Codex CLI." /> <PluginCard name="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="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="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI." />
<PluginCard name="OpenCode" logo="opencode" href="/docs/plugins/agents/opencode" description="OpenCode terminal agent." /> <PluginCard
name="OpenCode"
logo="opencode"
href="/docs/plugins/agents/opencode"
description="OpenCode terminal agent."
/>
</PluginGrid> </PluginGrid>
## Choosing ## Choosing

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

@ -7,7 +7,9 @@ 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.
@ -39,7 +41,7 @@ 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` |

View File

@ -111,7 +111,7 @@ 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` |
@ -227,7 +227,7 @@ 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 |
@ -243,7 +243,7 @@ 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 |
@ -254,20 +254,22 @@ Agent plugins have the richest interface. Methods are split into required and op
**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 |
@ -278,7 +280,7 @@ Agent plugins have the richest interface. Methods are split into required and op
### 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 |
@ -300,7 +302,7 @@ 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 |
@ -308,7 +310,7 @@ The richest interface — covers PR lifecycle, CI tracking, review tracking, and
### 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 |
@ -421,7 +423,7 @@ 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. |
@ -438,14 +440,14 @@ 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. |
@ -457,7 +459,7 @@ All utilities are exported from `@aoagents/ao-core`. The source lives in `packag
**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"`). |
@ -465,7 +467,7 @@ All utilities are exported from `@aoagents/ao-core`. The source lives in `packag
**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 |
@ -525,7 +527,7 @@ 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 |
@ -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
title="Architecture"
description="Understand the plugin registry, session lifecycle, and how slots wire together."
href="/docs/architecture"
/>
<Card title="Plugin catalog" description="Browse every bundled plugin grouped by slot." href="/docs/plugins" /> <Card title="Plugin catalog" description="Browse every bundled plugin grouped by slot." href="/docs/plugins" />
<Card title="Configuration reference" description="How plugins are referenced in agent-orchestrator.yaml." href="/docs/configuration" /> <Card
title="Configuration reference"
description="How plugins are referenced in agent-orchestrator.yaml."
href="/docs/configuration"
/>
</Cards> </Cards>

View File

@ -8,7 +8,7 @@ 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 |
@ -21,59 +21,169 @@ AO has **eight plugin slots**. Only one plugin per slot is active at a time, and
## 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"
href="/docs/plugins/notifiers/dashboard"
description="Retained notifications inside the AO dashboard."
/>
<PluginCard
name="Desktop"
logo="apple"
href="/docs/plugins/notifiers/desktop"
description="Native macOS/Linux notifications. No-op on Windows."
/>
<PluginCard
name="Discord"
logo="discord"
href="/docs/plugins/notifiers/discord"
description="Webhook-based Discord messages with rich embeds."
/>
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhooks." /> <PluginCard name="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
<PluginCard name="Composio" logo="composio" href="/docs/plugins/notifiers/composio" description="Route through the Composio toolkit — Slack, Discord, or Gmail." /> name="Webhook"
<PluginCard name="OpenClaw" logo="openclaw" href="/docs/plugins/notifiers/openclaw" description="Local OpenClaw gateway for personal notifications." /> 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

@ -5,10 +5,17 @@ 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
@ -37,7 +44,7 @@ notificationRouting:
``` ```
| 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 |

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"
href="/docs/plugins/notifiers/dashboard"
description="Retained notifications inside the AO dashboard."
/>
<PluginCard
name="Desktop"
logo="apple"
href="/docs/plugins/notifiers/desktop"
description="Native macOS/Linux notifications. No-op on Windows."
/>
<PluginCard
name="Discord"
logo="discord"
href="/docs/plugins/notifiers/discord"
description="Discord webhook with rich embeds."
/>
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhook." /> <PluginCard name="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
<PluginCard name="Composio" logo="composio" href="/docs/plugins/notifiers/composio" description="Route through Composio — Slack, Discord, or Gmail." /> name="Webhook"
<PluginCard name="OpenClaw" logo="openclaw" href="/docs/plugins/notifiers/openclaw" description="Local OpenClaw gateway for personal alerts." /> 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
@ -57,7 +87,7 @@ 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 |

View File

@ -5,7 +5,9 @@ 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" />
@ -70,7 +72,7 @@ The OpenClaw config keeps the actual secret:
## 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 |

View File

@ -6,13 +6,23 @@ 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

@ -5,7 +5,9 @@ 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

@ -5,12 +5,23 @@ 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

@ -5,7 +5,9 @@ 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" />
@ -59,7 +61,7 @@ projects:
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 |

View File

@ -5,7 +5,9 @@ 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" />
@ -25,7 +27,7 @@ scmConfig:
## 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 |
@ -65,7 +67,7 @@ projects:
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 |
@ -85,7 +87,7 @@ 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]` |

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

@ -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

@ -5,7 +5,9 @@ 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

@ -5,7 +5,9 @@ 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" />
@ -21,7 +23,7 @@ terminalConfig:
``` ```
| 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

@ -5,7 +5,9 @@ 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" />
@ -32,7 +34,7 @@ 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` |

View File

@ -5,7 +5,9 @@ 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" />
@ -28,11 +30,12 @@ projects:
``` ```
| 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

@ -5,7 +5,9 @@ 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" />
@ -49,7 +51,7 @@ 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 |

View File

@ -5,7 +5,9 @@ description: Full per-session git clone. Use when worktrees fight your tooling.
<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>workspace</code> · Name: <code>clone</code></span> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>workspace</code> · Name: <code>clone</code>
</span>
</div> </div>
Every session gets a full `git clone`. Heavier on disk than `worktree`, but simpler for any tooling that gets confused by the worktree layout. Every session gets a full `git clone`. Heavier on disk than `worktree`, but simpler for any tooling that gets confused by the worktree layout.
@ -21,7 +23,7 @@ workspaceConfig:
``` ```
| Config key | Default | What it does | | Config key | Default | What it does |
|---|---|---| | ---------- | -------------- | ----------------------------- |
| `cloneDir` | `~/.ao-clones` | Base directory for all clones | | `cloneDir` | `~/.ao-clones` | Base directory for all clones |
## How it works ## How it works

View File

@ -6,13 +6,23 @@ description: How each agent gets its own copy of the repo — a git worktree or
Every session needs an isolated filesystem so agents don't stomp each other. AO handles this with a **workspace** plugin. Two options ship: Every session needs an isolated filesystem so agents don't stomp each other. AO handles this with a **workspace** plugin. Two options ship:
| Plugin | Isolation | Disk usage | Speed | | Plugin | Isolation | Disk usage | Speed |
|---|---|---|---| | --------------------------------------------- | --------------------------------- | ---------------------- | ---------------------- |
| [worktree](/docs/plugins/workspaces/worktree) | Per-session branch, shared `.git` | Low (shared object DB) | Fast (no clone) | | [worktree](/docs/plugins/workspaces/worktree) | Per-session branch, shared `.git` | Low (shared object DB) | Fast (no clone) |
| [clone](/docs/plugins/workspaces/clone) | Per-session full clone | High | Slower (initial clone) | | [clone](/docs/plugins/workspaces/clone) | Per-session full clone | High | Slower (initial clone) |
<PluginGrid> <PluginGrid>
<PluginCard name="worktree" logo="github" href="/docs/plugins/workspaces/worktree" description="git worktree. Default, fast, disk-efficient." /> <PluginCard
<PluginCard name="clone" logo="github" href="/docs/plugins/workspaces/clone" description="Full per-session clone. Use when tools struggle with worktrees." /> name="worktree"
logo="github"
href="/docs/plugins/workspaces/worktree"
description="git worktree. Default, fast, disk-efficient."
/>
<PluginCard
name="clone"
logo="github"
href="/docs/plugins/workspaces/clone"
description="Full per-session clone. Use when tools struggle with worktrees."
/>
</PluginGrid> </PluginGrid>
## Choosing ## Choosing

View File

@ -5,7 +5,9 @@ description: Default workspace. Each session is a git worktree pointing at a fre
<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>workspace</code> · Name: <code>worktree</code></span> <span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
Slot: <code>workspace</code> · Name: <code>worktree</code>
</span>
</div> </div>
The default workspace plugin. Each session gets a [git worktree](https://git-scm.com/docs/git-worktree) with its own branch. All worktrees share the underlying `.git` object database, so disk usage stays low even with dozens of parallel agents. The default workspace plugin. Each session gets a [git worktree](https://git-scm.com/docs/git-worktree) with its own branch. All worktrees share the underlying `.git` object database, so disk usage stays low even with dozens of parallel agents.
@ -21,7 +23,7 @@ workspaceConfig:
``` ```
| Config key | Default | What it does | | Config key | Default | What it does |
|---|---|---| | ------------- | -------------- | -------------------------------- |
| `worktreeDir` | `~/.worktrees` | Base directory for all worktrees | | `worktreeDir` | `~/.worktrees` | Base directory for all worktrees |
## How it works ## How it works
@ -45,7 +47,7 @@ projects:
``` ```
| Knob | Purpose | | Knob | Purpose |
|---|---| | ------------ | ------------------------------------------------------------------------------------------------- |
| `symlinks` | Files/dirs to symlink from the source repo into each worktree (e.g. `.env.local`, a shared cache) | | `symlinks` | Files/dirs to symlink from the source repo into each worktree (e.g. `.env.local`, a shared cache) |
| `postCreate` | Shell commands to run in each new worktree after creation (e.g. `pnpm install`) | | `postCreate` | Shell commands to run in each new worktree after creation (e.g. `pnpm install`) |

View File

@ -10,7 +10,8 @@ import { Step, Steps } from "fumadocs-ui/components/steps";
This quickstart walks through the smallest useful AO loop: start the dashboard, create one worker session, watch it work, and clean it up after the PR is merged. This quickstart walks through the smallest useful AO loop: start the dashboard, create one worker session, watch it work, and clean it up after the PR is merged.
<Callout type="info" title="Before you start"> <Callout type="info" title="Before you start">
Complete [Installation](/docs/installation) first. You need `ao`, Git, one authenticated source-control CLI such as `gh`, and one signed-in agent CLI. Complete [Installation](/docs/installation) first. You need `ao`, Git, one authenticated source-control CLI such as
`gh`, and one signed-in agent CLI.
</Callout> </Callout>
## Pick A Safe First Task ## Pick A Safe First Task
@ -56,6 +57,7 @@ Open a second terminal in the same repository.
``` ```
Replace `42` with the issue number. AO fetches the issue through `gh`, creates a worktree, starts the configured worker agent, and gives it the issue context. Replace `42` with the issue number. AO fetches the issue through `gh`, creates a worktree, starts the configured worker agent, and gives it the issue context.
</Tab> </Tab>
<Tab value="Freeform prompt"> <Tab value="Freeform prompt">
```bash ```bash
@ -63,6 +65,7 @@ Open a second terminal in the same repository.
``` ```
Use this when the task is not tracked in GitHub, GitLab, or Linear yet. Keep the prompt specific and reviewable. Use this when the task is not tracked in GitHub, GitLab, or Linear yet. Keep the prompt specific and reviewable.
</Tab> </Tab>
</Tabs> </Tabs>
@ -127,7 +130,7 @@ ao session cleanup
## What AO Created ## What AO Created
| Item | What it means | | Item | What it means |
| --- | --- | | ------------------------- | ------------------------------------------------------------------------------------------------ |
| `agent-orchestrator.yaml` | Project config: plugins, projects, reactions, runtime, and notifier choices. | | `agent-orchestrator.yaml` | Project config: plugins, projects, reactions, runtime, and notifier choices. |
| Orchestrator session | A coordinating session started by `ao start`; it supervises worker sessions. | | Orchestrator session | A coordinating session started by `ao start`; it supervises worker sessions. |
| Worker session | The agent process that works on one issue or prompt. | | Worker session | The agent process that works on one issue or prompt. |
@ -137,7 +140,7 @@ ao session cleanup
## If Something Looks Wrong ## If Something Looks Wrong
| Symptom | First check | | Symptom | First check |
| --- | --- | | --------------------------------------- | ----------------------------------------------------------------------------- |
| Dashboard is not updating | Make sure the `ao start` terminal is still running. | | Dashboard is not updating | Make sure the `ao start` terminal is still running. |
| `ao spawn` warns that AO is not running | Start AO with `ao start` before spawning. | | `ao spawn` warns that AO is not running | Start AO with `ao start` before spawning. |
| GitHub issue or PR data is missing | Run `gh auth status` and check the `repo` field in `agent-orchestrator.yaml`. | | GitHub issue or PR data is missing | Run `gh auth status` and check the `repo` field in `agent-orchestrator.yaml`. |
@ -147,8 +150,20 @@ ao session cleanup
## Next ## Next
<Cards> <Cards>
<Card title="Parallel issues" description="Spawn several worker sessions without duplicating active work." href="/docs/guides/parallel-issues" /> <Card
title="Parallel issues"
description="Spawn several worker sessions without duplicating active work."
href="/docs/guides/parallel-issues"
/>
<Card title="CI recovery" description="See how AO reacts when checks fail." href="/docs/guides/ci-recovery" /> <Card title="CI recovery" description="See how AO reacts when checks fail." href="/docs/guides/ci-recovery" />
<Card title="Configuration" description="Tune agents, runtimes, projects, reactions, and notifiers." href="/docs/configuration" /> <Card
<Card title="Dashboard" description="Understand session cards, attention zones, and live terminal views." href="/docs/dashboard" /> title="Configuration"
description="Tune agents, runtimes, projects, reactions, and notifiers."
href="/docs/configuration"
/>
<Card
title="Dashboard"
description="Understand session cards, attention zones, and live terminal views."
href="/docs/dashboard"
/>
</Cards> </Cards>

View File

@ -59,13 +59,17 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil
<Accordions> <Accordions>
<Accordion title="GitHub API rate limit"> <Accordion title="GitHub API rate limit">
AO paces calls, but heavy projects can still hit limits. The lifecycle manager backs off automatically. If you see sustained rate-limit errors, check that you're logged in as yourself (`gh auth status`) — unauthenticated calls have much lower limits. AO paces calls, but heavy projects can still hit limits. The lifecycle manager backs off automatically. If you see
sustained rate-limit errors, check that you're logged in as yourself (`gh auth status`) — unauthenticated calls have
much lower limits.
</Accordion> </Accordion>
<Accordion title="CI recovery never fires"> <Accordion title="CI recovery never fires">
`reactions.ciFailed.enabled: false` in your config, or the PR is in draft. See [CI recovery When it doesn't kick in](/docs/guides/ci-recovery#when-it-doesnt-kick-in). `reactions.ciFailed.enabled: false` in your config, or the PR is in draft. See [CI recovery When it doesn't kick
in](/docs/guides/ci-recovery#when-it-doesnt-kick-in).
</Accordion> </Accordion>
<Accordion title="Review loop never fires"> <Accordion title="Review loop never fires">
Review feedback is only replayed on `CHANGES_REQUESTED` — not on plain `COMMENTED` reviews. See [Review loop](/docs/guides/review-loop). Review feedback is only replayed on `CHANGES_REQUESTED` — not on plain `COMMENTED` reviews. See [Review
loop](/docs/guides/review-loop).
</Accordion> </Accordion>
<Accordion title="Webhook delivers but nothing happens"> <Accordion title="Webhook delivers but nothing happens">
HMAC signature check failing. Verify your `secretEnvVar` is exported and matches the secret you set on GitHub. HMAC signature check failing. Verify your `secretEnvVar` is exported and matches the secret you set on GitHub.
@ -82,7 +86,8 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil
Expected. Add Discord, Slack, or a webhook notifier in the same `notifier:` list. Expected. Add Discord, Slack, or a webhook notifier in the same `notifier:` list.
</Accordion> </Accordion>
<Accordion title="Slack webhook 429s under load"> <Accordion title="Slack webhook 429s under load">
The slack notifier doesn't honor Retry-After. Use the generic [webhook notifier](/docs/plugins/notifiers/webhook) with `retries` tuned up. The slack notifier doesn't honor Retry-After. Use the generic [webhook notifier](/docs/plugins/notifiers/webhook)
with `retries` tuned up.
</Accordion> </Accordion>
</Accordions> </Accordions>

View File

@ -14,16 +14,13 @@ const FALLBACK_STATS: GitHubRepoStats = {
export async function getGitHubRepoStats(): Promise<GitHubRepoStats> { export async function getGitHubRepoStats(): Promise<GitHubRepoStats> {
try { try {
const response = await fetch( const response = await fetch("https://api.github.com/repos/ComposioHQ/agent-orchestrator", {
"https://api.github.com/repos/ComposioHQ/agent-orchestrator",
{
next: { revalidate: 3600 }, next: { revalidate: 3600 },
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"User-Agent": "ao-website", "User-Agent": "ao-website",
}, },
}, });
);
if (!response.ok) { if (!response.ok) {
return FALLBACK_STATS; return FALLBACK_STATS;

View File

@ -103,7 +103,9 @@ a {
.landing-card { .landing-card {
background: var(--landing-card-bg); background: var(--landing-card-bg);
border: 1px solid var(--landing-border-subtle); border: 1px solid var(--landing-border-subtle);
transition: border-color 0.2s, background 0.2s; transition:
border-color 0.2s,
background 0.2s;
} }
.landing-card:hover { .landing-card:hover {
@ -155,7 +157,9 @@ a {
.landing-reveal { .landing-reveal {
opacity: 0; opacity: 0;
transform: translateY(32px); transform: translateY(32px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out; transition:
opacity 0.8s ease-out,
transform 0.8s ease-out;
} }
.landing-reveal.visible { .landing-reveal.visible {
@ -413,110 +417,288 @@ a {
These unlayered rules (outside any @layer) beat ALL named layers and restore These unlayered rules (outside any @layer) beat ALL named layers and restore
the correct values for every margin/padding class used in the landing page. */ the correct values for every margin/padding class used in the landing page. */
.landing-page .mt-1 { margin-top: calc(var(--spacing) * 1); } .landing-page .mt-1 {
.landing-page .mt-6 { margin-top: calc(var(--spacing) * 6); } margin-top: calc(var(--spacing) * 1);
.landing-page .mt-8 { margin-top: calc(var(--spacing) * 8); } }
.landing-page .mt-10 { margin-top: calc(var(--spacing) * 10); } .landing-page .mt-6 {
.landing-page .mt-12 { margin-top: calc(var(--spacing) * 12); } margin-top: calc(var(--spacing) * 6);
.landing-page .mt-16 { margin-top: calc(var(--spacing) * 16); } }
.landing-page .mt-20 { margin-top: calc(var(--spacing) * 20); } .landing-page .mt-8 {
margin-top: calc(var(--spacing) * 8);
}
.landing-page .mt-10 {
margin-top: calc(var(--spacing) * 10);
}
.landing-page .mt-12 {
margin-top: calc(var(--spacing) * 12);
}
.landing-page .mt-16 {
margin-top: calc(var(--spacing) * 16);
}
.landing-page .mt-20 {
margin-top: calc(var(--spacing) * 20);
}
.landing-page .mb-1 { margin-bottom: calc(var(--spacing) * 1); } .landing-page .mb-1 {
.landing-page .mb-2 { margin-bottom: calc(var(--spacing) * 2); } margin-bottom: calc(var(--spacing) * 1);
.landing-page .mb-3 { margin-bottom: calc(var(--spacing) * 3); } }
.landing-page .mb-4 { margin-bottom: calc(var(--spacing) * 4); } .landing-page .mb-2 {
.landing-page .mb-5 { margin-bottom: calc(var(--spacing) * 5); } margin-bottom: calc(var(--spacing) * 2);
.landing-page .mb-6 { margin-bottom: calc(var(--spacing) * 6); } }
.landing-page .mb-8 { margin-bottom: calc(var(--spacing) * 8); } .landing-page .mb-3 {
.landing-page .mb-10 { margin-bottom: calc(var(--spacing) * 10); } margin-bottom: calc(var(--spacing) * 3);
.landing-page .mb-12 { margin-bottom: calc(var(--spacing) * 12); } }
.landing-page .mb-16 { margin-bottom: calc(var(--spacing) * 16); } .landing-page .mb-4 {
.landing-page .mb-1\.5 { margin-bottom: calc(var(--spacing) * 1.5); } margin-bottom: calc(var(--spacing) * 4);
}
.landing-page .mb-5 {
margin-bottom: calc(var(--spacing) * 5);
}
.landing-page .mb-6 {
margin-bottom: calc(var(--spacing) * 6);
}
.landing-page .mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
.landing-page .mb-10 {
margin-bottom: calc(var(--spacing) * 10);
}
.landing-page .mb-12 {
margin-bottom: calc(var(--spacing) * 12);
}
.landing-page .mb-16 {
margin-bottom: calc(var(--spacing) * 16);
}
.landing-page .mb-1\.5 {
margin-bottom: calc(var(--spacing) * 1.5);
}
.landing-page .ml-1 { margin-left: calc(var(--spacing) * 1); } .landing-page .ml-1 {
.landing-page .ml-2 { margin-left: calc(var(--spacing) * 2); } margin-left: calc(var(--spacing) * 1);
.landing-page .ml-1\.5 { margin-left: calc(var(--spacing) * 1.5); } }
.landing-page .ml-2 {
margin-left: calc(var(--spacing) * 2);
}
.landing-page .ml-1\.5 {
margin-left: calc(var(--spacing) * 1.5);
}
.landing-page .mr-1 { margin-right: calc(var(--spacing) * 1); } .landing-page .mr-1 {
.landing-page .mr-1\.5 { margin-right: calc(var(--spacing) * 1.5); } margin-right: calc(var(--spacing) * 1);
}
.landing-page .mr-1\.5 {
margin-right: calc(var(--spacing) * 1.5);
}
.landing-page .p-2 { padding: calc(var(--spacing) * 2); } .landing-page .p-2 {
.landing-page .p-3 { padding: calc(var(--spacing) * 3); } padding: calc(var(--spacing) * 2);
.landing-page .p-6 { padding: calc(var(--spacing) * 6); } }
.landing-page .p-7 { padding: calc(var(--spacing) * 7); } .landing-page .p-3 {
.landing-page .p-8 { padding: calc(var(--spacing) * 8); } padding: calc(var(--spacing) * 3);
.landing-page .p-2\.5 { padding: calc(var(--spacing) * 2.5); } }
.landing-page .p-6 {
padding: calc(var(--spacing) * 6);
}
.landing-page .p-7 {
padding: calc(var(--spacing) * 7);
}
.landing-page .p-8 {
padding: calc(var(--spacing) * 8);
}
.landing-page .p-2\.5 {
padding: calc(var(--spacing) * 2.5);
}
.landing-page .pt-10 { padding-top: calc(var(--spacing) * 10); } .landing-page .pt-10 {
.landing-page .pt-32 { padding-top: calc(var(--spacing) * 32); } padding-top: calc(var(--spacing) * 10);
}
.landing-page .pt-32 {
padding-top: calc(var(--spacing) * 32);
}
.landing-page .pb-20 { padding-bottom: calc(var(--spacing) * 20); } .landing-page .pb-20 {
padding-bottom: calc(var(--spacing) * 20);
}
.landing-page .px-2 { padding-left: calc(var(--spacing) * 2); padding-right: calc(var(--spacing) * 2); } .landing-page .px-2 {
.landing-page .px-3 { padding-left: calc(var(--spacing) * 3); padding-right: calc(var(--spacing) * 3); } padding-left: calc(var(--spacing) * 2);
.landing-page .px-4 { padding-left: calc(var(--spacing) * 4); padding-right: calc(var(--spacing) * 4); } padding-right: calc(var(--spacing) * 2);
.landing-page .px-5 { padding-left: calc(var(--spacing) * 5); padding-right: calc(var(--spacing) * 5); } }
.landing-page .px-6 { padding-left: calc(var(--spacing) * 6); padding-right: calc(var(--spacing) * 6); } .landing-page .px-3 {
.landing-page .px-8 { padding-left: calc(var(--spacing) * 8); padding-right: calc(var(--spacing) * 8); } padding-left: calc(var(--spacing) * 3);
.landing-page .px-3\.5 { padding-left: calc(var(--spacing) * 3.5); padding-right: calc(var(--spacing) * 3.5); } padding-right: calc(var(--spacing) * 3);
}
.landing-page .px-4 {
padding-left: calc(var(--spacing) * 4);
padding-right: calc(var(--spacing) * 4);
}
.landing-page .px-5 {
padding-left: calc(var(--spacing) * 5);
padding-right: calc(var(--spacing) * 5);
}
.landing-page .px-6 {
padding-left: calc(var(--spacing) * 6);
padding-right: calc(var(--spacing) * 6);
}
.landing-page .px-8 {
padding-left: calc(var(--spacing) * 8);
padding-right: calc(var(--spacing) * 8);
}
.landing-page .px-3\.5 {
padding-left: calc(var(--spacing) * 3.5);
padding-right: calc(var(--spacing) * 3.5);
}
.landing-page .py-1 { padding-top: calc(var(--spacing) * 1); padding-bottom: calc(var(--spacing) * 1); } .landing-page .py-1 {
.landing-page .py-2 { padding-top: calc(var(--spacing) * 2); padding-bottom: calc(var(--spacing) * 2); } padding-top: calc(var(--spacing) * 1);
.landing-page .py-3 { padding-top: calc(var(--spacing) * 3); padding-bottom: calc(var(--spacing) * 3); } padding-bottom: calc(var(--spacing) * 1);
.landing-page .py-4 { padding-top: calc(var(--spacing) * 4); padding-bottom: calc(var(--spacing) * 4); } }
.landing-page .py-6 { padding-top: calc(var(--spacing) * 6); padding-bottom: calc(var(--spacing) * 6); } .landing-page .py-2 {
.landing-page .py-8 { padding-top: calc(var(--spacing) * 8); padding-bottom: calc(var(--spacing) * 8); } padding-top: calc(var(--spacing) * 2);
.landing-page .py-20 { padding-top: calc(var(--spacing) * 20); padding-bottom: calc(var(--spacing) * 20); } padding-bottom: calc(var(--spacing) * 2);
.landing-page .py-40 { padding-top: calc(var(--spacing) * 40); padding-bottom: calc(var(--spacing) * 40); } }
.landing-page .py-1\.5 { padding-top: calc(var(--spacing) * 1.5); padding-bottom: calc(var(--spacing) * 1.5); } .landing-page .py-3 {
.landing-page .py-2\.5 { padding-top: calc(var(--spacing) * 2.5); padding-bottom: calc(var(--spacing) * 2.5); } padding-top: calc(var(--spacing) * 3);
.landing-page .py-3\.5 { padding-top: calc(var(--spacing) * 3.5); padding-bottom: calc(var(--spacing) * 3.5); } padding-bottom: calc(var(--spacing) * 3);
}
.landing-page .py-4 {
padding-top: calc(var(--spacing) * 4);
padding-bottom: calc(var(--spacing) * 4);
}
.landing-page .py-6 {
padding-top: calc(var(--spacing) * 6);
padding-bottom: calc(var(--spacing) * 6);
}
.landing-page .py-8 {
padding-top: calc(var(--spacing) * 8);
padding-bottom: calc(var(--spacing) * 8);
}
.landing-page .py-20 {
padding-top: calc(var(--spacing) * 20);
padding-bottom: calc(var(--spacing) * 20);
}
.landing-page .py-40 {
padding-top: calc(var(--spacing) * 40);
padding-bottom: calc(var(--spacing) * 40);
}
.landing-page .py-1\.5 {
padding-top: calc(var(--spacing) * 1.5);
padding-bottom: calc(var(--spacing) * 1.5);
}
.landing-page .py-2\.5 {
padding-top: calc(var(--spacing) * 2.5);
padding-bottom: calc(var(--spacing) * 2.5);
}
.landing-page .py-3\.5 {
padding-top: calc(var(--spacing) * 3.5);
padding-bottom: calc(var(--spacing) * 3.5);
}
/* Arbitrary padding values */ /* Arbitrary padding values */
.landing-page .py-\[100px\] { padding-top: 100px; padding-bottom: 100px; } .landing-page .py-\[100px\] {
.landing-page .py-\[120px\] { padding-top: 120px; padding-bottom: 120px; } padding-top: 100px;
.landing-page .pt-\[60px\] { padding-top: 60px; } padding-bottom: 100px;
.landing-page .pb-\[120px\] { padding-bottom: 120px; } }
.landing-page .py-\[120px\] {
padding-top: 120px;
padding-bottom: 120px;
}
.landing-page .pt-\[60px\] {
padding-top: 60px;
}
.landing-page .pb-\[120px\] {
padding-bottom: 120px;
}
/* Border colors fumadocs * { border-color: var(--color-fd-border) } persists from docs /* Border colors fumadocs * { border-color: var(--color-fd-border) } persists from docs
navigation. In light mode --color-fd-border = #d6d3d1 which looks bright on the dark navigation. In light mode --color-fd-border = #d6d3d1 which looks bright on the dark
landing background, overriding explicit border-[var(--landing-border-*)] utilities. */ landing background, overriding explicit border-[var(--landing-border-*)] utilities. */
.landing-page .border-\[var\(--landing-border-subtle\)\] { border-color: var(--landing-border-subtle); } .landing-page .border-\[var\(--landing-border-subtle\)\] {
.landing-page .border-\[var\(--landing-border-default\)\] { border-color: var(--landing-border-default); } border-color: var(--landing-border-subtle);
.landing-page .border-\[var\(--landing-border-strong\)\] { border-color: var(--landing-border-strong); } }
.landing-page .border-\[var\(--landing-border-default\)\] {
border-color: var(--landing-border-default);
}
.landing-page .border-\[var\(--landing-border-strong\)\] {
border-color: var(--landing-border-strong);
}
/* Font weight — fumadocs heading reset zeroes h1-h6 { font-weight: inherit } */ /* Font weight — fumadocs heading reset zeroes h1-h6 { font-weight: inherit } */
.landing-page .font-\[680\] { font-weight: 680; } .landing-page .font-\[680\] {
font-weight: 680;
}
/* Font size (clamp values) — fumadocs heading reset zeroes h1-h6 { font-size: inherit } */ /* Font size (clamp values) — fumadocs heading reset zeroes h1-h6 { font-size: inherit } */
.landing-page .text-\[clamp\(1\.75rem\,4vw\,2\.75rem\)\] { font-size: clamp(1.75rem, 4vw, 2.75rem); } .landing-page .text-\[clamp\(1\.75rem\,4vw\,2\.75rem\)\] {
.landing-page .text-\[clamp\(1\.375rem\,3vw\,2rem\)\] { font-size: clamp(1.375rem, 3vw, 2rem); } font-size: clamp(1.75rem, 4vw, 2.75rem);
.landing-page .text-\[clamp\(2rem\,4vw\,3rem\)\] { font-size: clamp(2rem, 4vw, 3rem); } }
.landing-page .text-\[clamp\(2rem\,5vw\,3\.5rem\)\] { font-size: clamp(2rem, 5vw, 3.5rem); } .landing-page .text-\[clamp\(1\.375rem\,3vw\,2rem\)\] {
font-size: clamp(1.375rem, 3vw, 2rem);
}
.landing-page .text-\[clamp\(2rem\,4vw\,3rem\)\] {
font-size: clamp(2rem, 4vw, 3rem);
}
.landing-page .text-\[clamp\(2rem\,5vw\,3\.5rem\)\] {
font-size: clamp(2rem, 5vw, 3.5rem);
}
/* Responsive md: utilities fumadocs bundles .hidden but not all md: variants, /* Responsive md: utilities fumadocs bundles .hidden but not all md: variants,
so its higher-priority layer keeps elements hidden / wrong grid at desktop widths. */ so its higher-priority layer keeps elements hidden / wrong grid at desktop widths. */
@media (min-width: 768px) { @media (min-width: 768px) {
.landing-page .md\:flex { display: flex; } .landing-page .md\:flex {
.landing-page .md\:block { display: block; } display: flex;
.landing-page .md\:hidden { display: none; } }
.landing-page .md\:block {
.landing-page .md\:flex-row { flex-direction: row; } display: block;
}
.landing-page .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } .landing-page .md\:hidden {
.landing-page .md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } display: none;
.landing-page .md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.landing-page .md\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.landing-page .md\:grid-cols-\[1fr_auto_1fr_1fr\] { grid-template-columns: 1fr auto 1fr 1fr; } .landing-page .md\:flex-row {
flex-direction: row;
.landing-page .md\:gap-0 { gap: 0; } }
.landing-page .md\:gap-6 { gap: calc(var(--spacing) * 6); }
.landing-page .md\:gap-12 { gap: calc(var(--spacing) * 12); } .landing-page .md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
.landing-page .md\:items-center { align-items: center; } }
.landing-page .md\:items-baseline { align-items: baseline; } .landing-page .md\:grid-cols-3 {
.landing-page .md\:items-start { align-items: flex-start; } grid-template-columns: repeat(3, minmax(0, 1fr));
}
.landing-page .md\:order-1 { order: 1; } .landing-page .md\:grid-cols-4 {
.landing-page .md\:order-2 { order: 2; } grid-template-columns: repeat(4, minmax(0, 1fr));
}
.landing-page .md\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.landing-page .md\:grid-cols-\[1fr_auto_1fr_1fr\] {
grid-template-columns: 1fr auto 1fr 1fr;
}
.landing-page .md\:gap-0 {
gap: 0;
}
.landing-page .md\:gap-6 {
gap: calc(var(--spacing) * 6);
}
.landing-page .md\:gap-12 {
gap: calc(var(--spacing) * 12);
}
.landing-page .md\:items-center {
align-items: center;
}
.landing-page .md\:items-baseline {
align-items: baseline;
}
.landing-page .md\:items-start {
align-items: flex-start;
}
.landing-page .md\:order-1 {
order: 1;
}
.landing-page .md\:order-2 {
order: 2;
}
} }

View File

@ -9,27 +9,31 @@ touch a developer's real AO installation.
## Two tiers ## Two tiers
| Tier | What | Where | | Tier | What | Where |
|------|------|-------| | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| **Comprehensive (primary)** | A cross-platform Go suite that builds `ao` and exercises the full behaviour. Runs natively on **ubuntu + macOS + windows** — the only way to cover the OS-specific process-detach paths (`setsid` vs `CREATE_NEW_PROCESS_GROUP`) and `os.UserConfigDir()` resolution. | `backend/internal/cli/e2e_test.go` (build tag `e2e`) | | **Comprehensive (primary)** | A cross-platform Go suite that builds `ao` and exercises the full behaviour. Runs natively on **ubuntu + macOS + windows** — the only way to cover the OS-specific process-detach paths (`setsid` vs `CREATE_NEW_PROCESS_GROUP`) and `os.UserConfigDir()` resolution. | `backend/internal/cli/e2e_test.go` (build tag `e2e`) |
| **Fresh-install (hardening)** | Proves a freshly installed binary works on a clean machine with no Go toolchain and no developer state. | `test/cli/Dockerfile` + `test/cli/install-check.sh` | | **Fresh-install (hardening)** | Proves a freshly installed binary works on a clean machine with no Go toolchain and no developer state. | `test/cli/Dockerfile` + `test/cli/install-check.sh` |
## Run it ## Run it
**The Go suite (fastest, cross-platform):** **The Go suite (fastest, cross-platform):**
```bash ```bash
cd backend cd backend
go test -tags e2e ./internal/cli/... # run it go test -tags e2e ./internal/cli/... # run it
go test -tags e2e -v -run TestE2E ./internal/cli/... # verbose: prints every command + output go test -tags e2e -v -run TestE2E ./internal/cli/... # verbose: prints every command + output
``` ```
It builds its own `ao` binary; `git` must be on PATH (required by `doctor`). It builds its own `ao` binary; `git` must be on PATH (required by `doctor`).
`-v` logs each `ao` invocation and its full output, which is the audit trail you `-v` logs each `ao` invocation and its full output, which is the audit trail you
get for free from `go test`. get for free from `go test`.
**Fresh-machine install, in a clean container:** **Fresh-machine install, in a clean container:**
```bash ```bash
docker build -f test/cli/Dockerfile -t ao-cli-smoke . docker build -f test/cli/Dockerfile -t ao-cli-smoke .
docker run --rm --init ao-cli-smoke docker run --rm --init ao-cli-smoke
``` ```
> `--init` gives the container a real PID-1 reaper (tini) so the daemon the > `--init` gives the container a real PID-1 reaper (tini) so the daemon the
> check starts is reaped after `stop` instead of lingering as a zombie. > check starts is reaped after `stop` instead of lingering as a zombie.