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

* chore: add prettier config and CI auto-formatter

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

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

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

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

* chore: format with prettier [skip ci]

---------

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

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

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

3
.gitignore vendored
View File

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

17
.prettierignore Normal file
View File

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

9
.prettierrc Normal file
View File

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

View File

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

View File

@ -63,7 +63,7 @@ route. Run `ao <command> --help` for the authoritative flag shape; the table
below groups what's on `main` today.
| Lane | Command | Purpose |
|---|---|---|
| ------------ | ------------------------------------ | ------------------------------------------------------------ |
| Daemon | `ao start` | Start the daemon in the background and wait for `/readyz`. |
| Daemon | `ao stop` | Graceful shutdown via loopback `POST /shutdown`. |
| Daemon | `ao status` | Report PID/port/health/readiness from `running.json`. |
@ -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.
| Var | Default | Purpose |
|---|---|---|
| --------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
| `AO_PORT` | `3001` | Bind port; daemon fails fast if taken. |
| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout (Go duration). |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful-shutdown hard cap. |

View File

@ -11,7 +11,7 @@ Start with [architecture.md](architecture.md) for the current backend model and
## Reference docs
| Doc | What it covers |
|-----|----------------|
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| [architecture.md](architecture.md) | Current backend model, package layout, status derivation, persistence/CDC, and load-bearing rules. |
| [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. |

View File

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

View File

@ -8,7 +8,7 @@ call runtime, workspace, tracker, or agent adapters in-process.
## Current commands
| Command | Purpose |
|---|---|
| ----------------------------- | ------------------------------------------------------------------------------------------- |
| `ao start` | Start the daemon in the background and wait for `/readyz`. |
| `ao status` | Report daemon state from `running.json`, process liveness, `/healthz`, and `/readyz`. |
| `ao status --json` | Emit the same daemon state as machine-readable JSON. |
@ -26,7 +26,7 @@ call runtime, workspace, tracker, or agent adapters in-process.
The CLI and daemon share the same environment-driven config:
| Var | Default | Purpose |
|---|---|---|
| --------------------- | ------------------------------------------------- | ---------------------- |
| `AO_PORT` | `3001` | Loopback daemon port. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID/port handshake. |
| `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite data directory. |

View File

@ -41,7 +41,7 @@ rather than an escape-hatch map.
## Field catalog (legacy `projects.<id>`) and target home
| YAML field | Type | Storage today | Target |
|---|---|---|---|
| --------------------------------- | ---------------------- | ----------------------------------- | ---------------------------------------------------- |
| `name` | string | `projects.display_name` | done |
| `repo` | string | `projects.repo_origin_url` | done |
| `path` | string | `projects.path` | done |
@ -112,7 +112,7 @@ agent adapter.
## Sequencing (one slice per PR)
1. **agentConfig (typed)***this PR*. Establishes the typed+validated+surfaced
1. **agentConfig (typed)**_this PR_. Establishes the typed+validated+surfaced
pattern end to end.
2. **Project identity scalars**`default_branch`, `session_prefix` (stop
hardcoding/deriving them).

View File

@ -19,7 +19,7 @@ invariants.
## Accepted stack
| Area | Decision | Status | Rationale |
|------|----------|--------|-----------|
| ------------------ | ----------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Backend language | Go 1.25.7 | Implemented | Matches `backend/go.mod`; small daemon, strong stdlib, easy local distribution. |
| Backend core | Go stdlib | Implemented | Domain, lifecycle, session, and adapter contracts should stay dependency-light. |
| Frontend shell | Electron + TypeScript | Implemented | Local desktop control plane paired with the daemon. |
@ -71,7 +71,7 @@ config surface appears.
## Explicitly avoided for V1
| Avoid | Reason |
|-------|--------|
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| GORM | AO needs explicit transactional SQL and CDC-triggered writes. |
| Gin/Fiber | `net/http` + `chi` is enough for a local daemon API. |
| `go-git` as the primary Git engine | AO should match installed Git behavior, credentials, hooks, LFS, submodules, and user config. |

View File

@ -1,10 +1,5 @@
import { notFound } from "next/navigation";
import {
DocsPage,
DocsBody,
DocsTitle,
DocsDescription,
} from "fumadocs-ui/page";
import { DocsPage, DocsBody, DocsTitle, DocsDescription } from "fumadocs-ui/page";
import type { Metadata } from "next";
import { source } from "@/lib/source";
import { getMDXComponents } from "@/components/docs/mdx-components";
@ -55,9 +50,7 @@ export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) {

View File

@ -199,11 +199,7 @@
#nd-docs-layout code,
#nd-docs-layout kbd,
#nd-docs-layout pre {
font-family:
var(--font-jetbrains-mono),
ui-monospace,
"SFMono-Regular",
monospace;
font-family: var(--font-jetbrains-mono), ui-monospace, "SFMono-Regular", monospace;
}
/* Sidebar — smaller text like dashboard */
@ -512,7 +508,10 @@ pre.shiki code {
font-size: 0.875rem;
font-weight: 600;
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 {

View File

@ -51,10 +51,9 @@ const links: LinkItemType[] = [
async function GitHubStars() {
let stars: string | null = null;
try {
const res = await fetch(
"https://api.github.com/repos/ComposioHQ/agent-orchestrator",
{ next: { revalidate: 3600 } },
);
const res = await fetch("https://api.github.com/repos/ComposioHQ/agent-orchestrator", {
next: { revalidate: 3600 },
});
if (res.ok) {
const data = await res.json();
const count = data.stargazers_count as number;
@ -66,14 +65,7 @@ async function GitHubStars() {
return stars ? (
<span className="inline-flex items-center gap-1 text-[var(--color-text-muted)]">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
{stars}
@ -83,24 +75,14 @@ async function GitHubStars() {
export default function Layout({ children }: { children: ReactNode }) {
return (
<RootProvider
theme={{ enabled: true, defaultTheme: "dark" }}
search={{ options: { type: "static" } }}
>
<RootProvider theme={{ enabled: true, defaultTheme: "dark" }} search={{ options: { type: "static" } }}>
<DocsLayout
tree={source.pageTree}
links={links}
nav={{
title: (
<span className="flex items-center gap-2 font-semibold">
<img
src="/ao-logo.svg"
alt=""
aria-hidden="true"
width={22}
height={22}
className="h-[22px] w-[22px]"
/>
<img src="/ao-logo.svg" alt="" aria-hidden="true" width={22} height={22} className="h-[22px] w-[22px]" />
<span className="text-[var(--color-text-primary)]">AO</span>
</span>
),

View File

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

View File

@ -14,10 +14,16 @@ export function LandingAbout() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.8] max-w-[28rem]">
Agent Orchestrator replaces that with one YAML file. Point it at
your GitHub issues, pick your agents, and walk away. Each agent
spawns in its own git worktree, creates PRs, fixes CI failures,
addresses review comments, and moves toward merge. If you are new, start with the <a href="/docs/" className="underline decoration-[var(--landing-border-default)] underline-offset-4 hover:text-white">docs quickstart and configuration guides</a>.
Agent Orchestrator replaces that with one YAML file. Point it at your GitHub issues, pick your agents, and
walk away. Each agent spawns in its own git worktree, creates PRs, fixes CI failures, addresses review
comments, and moves toward merge. If you are new, start with the{" "}
<a
href="/docs/"
className="underline decoration-[var(--landing-border-default)] underline-offset-4 hover:text-white"
>
docs quickstart and configuration guides
</a>
.
</p>
{/* Config preview — show how simple setup is */}

View File

@ -35,14 +35,8 @@ export function LandingAgentsBar() {
<div className="flex items-center justify-center gap-6 flex-wrap">
{agents.map((agent) => (
<div key={agent.name} className="flex flex-col items-center gap-2">
<img
src={agent.src}
alt={agent.alt}
className="w-8 h-8 rounded-md object-contain"
/>
<div className="text-[0.6875rem] font-mono text-[var(--landing-muted)] opacity-50">
{agent.name}
</div>
<img src={agent.src} alt={agent.alt} className="w-8 h-8 rounded-md object-contain" />
<div className="text-[0.6875rem] font-mono text-[var(--landing-muted)] opacity-50">{agent.name}</div>
</div>
))}
</div>

View File

@ -15,13 +15,11 @@ export function LandingDifferentiators() {
Why Agent Orchestrator
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-6 max-w-[42rem]">
The only{" "}
<em className="italic text-[var(--landing-muted)]">open-source, web-based</em>{" "}
agent orchestrator
The only <em className="italic text-[var(--landing-muted)]">open-source, web-based</em> agent orchestrator
</h2>
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.7] max-w-[36rem] mb-12">
Conductor, T3 Code, and Codex App are native Mac apps. AO runs in
your browser, works on any OS, and you can self-host or extend it.
Conductor, T3 Code, and Codex App are native Mac apps. AO runs in your browser, works on any OS, and you can
self-host or extend it.
</p>
</div>
<div className="landing-reveal landing-card rounded-2xl overflow-hidden">
@ -45,12 +43,8 @@ export function LandingDifferentiators() {
key={row.feature}
className={i < rows.length - 1 ? "border-b border-[var(--landing-border-subtle)]" : ""}
>
<td className="px-6 py-3.5 text-[0.8125rem] text-[var(--landing-fg)]/80">
{row.feature}
</td>
<td className="px-6 py-3.5 text-center text-[rgba(134,239,172,0.8)]">
</td>
<td className="px-6 py-3.5 text-[0.8125rem] text-[var(--landing-fg)]/80">{row.feature}</td>
<td className="px-6 py-3.5 text-center text-[rgba(134,239,172,0.8)]"></td>
<td className="px-6 py-3.5 text-center text-[0.75rem] text-[var(--landing-muted)] opacity-40">
{row.others}
</td>

View File

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

View File

@ -16,9 +16,8 @@ export function LandingHero({ starsLabel }: LandingHeroProps) {
<span className="text-[var(--landing-muted)]">One dashboard.</span>
</h1>
<p className="landing-fade-rise-d1 text-[var(--landing-muted)] text-[0.9375rem] max-w-[38rem] mt-6 leading-[1.7]">
Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode
in isolated git worktrees. Each agent gets its own branch, creates PRs,
fixes CI, and addresses reviews autonomously.
Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode in isolated git worktrees. Each
agent gets its own branch, creates PRs, fixes CI, and addresses reviews autonomously.
</p>
<div className="landing-fade-rise-d2 flex items-center gap-3 mt-10 flex-wrap justify-center">
<div className="landing-card rounded-lg px-6 py-3 font-mono text-sm">

View File

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

View File

@ -36,17 +36,26 @@ export function LandingNav() {
</a>
<ul className="hidden md:flex items-center gap-8 list-none">
<li>
<a href="/docs" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
<a
href="/docs"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Docs
</a>
</li>
<li>
<a href="#features" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
<a
href="#features"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Features
</a>
</li>
<li>
<a href="#how" className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
<a
href="#how"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
How It Works
</a>
</li>

View File

@ -1,6 +1,16 @@
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" },
];
@ -12,8 +22,7 @@ export function LandingQuickStart() {
Get started in 60 seconds
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px] mb-6">
Three commands to{" "}
<em className="italic text-[var(--landing-muted)]">launch</em>
Three commands to <em className="italic text-[var(--landing-muted)]">launch</em>
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mt-12">
@ -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">
{s.num}
</div>
<h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight">
{s.title}
</h3>
<p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">
{s.desc}
</p>
<h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight">{s.title}</h3>
<p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">{s.desc}</p>
<div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg">
<span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd}
</div>
@ -35,7 +40,10 @@ export function LandingQuickStart() {
))}
</div>
<div className="landing-reveal mt-8 text-center">
<a href="/docs/" className="landing-card inline-flex rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline hover:text-white transition-colors">
<a
href="/docs/"
className="landing-card inline-flex rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Explore docs for setup and workflows
</a>
</div>

View File

@ -16,16 +16,11 @@ export function LandingStats({ stats }: LandingStatsProps) {
<section className="py-20 px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal grid grid-cols-2 md:grid-cols-4 gap-5">
{cards.map((stat) => (
<div
key={stat.label}
className="landing-card rounded-2xl py-8 px-6 text-center"
>
<div key={stat.label} className="landing-card rounded-2xl py-8 px-6 text-center">
<div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1">
{stat.number}
</div>
<div className="text-xs text-[var(--landing-muted)] opacity-60">
{stat.label}
</div>
<div className="text-xs text-[var(--landing-muted)] opacity-60">{stat.label}</div>
</div>
))}
</div>

View File

@ -4,8 +4,7 @@ import { useEffect, useState } from "react";
const testimonials = [
{
quote:
"Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.",
quote: "Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.",
img: "https://i.pravatar.cc/120?img=13",
name: "Staff Engineer",
role: "Series B Startup",
@ -44,10 +43,7 @@ export function LandingTestimonials() {
useEffect(() => {
if (paused) return;
const t = window.setTimeout(
() => change((active + 1) % testimonials.length),
ROTATE_MS,
);
const t = window.setTimeout(() => change((active + 1) % testimonials.length), ROTATE_MS);
return () => window.clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, paused]);
@ -65,11 +61,7 @@ export function LandingTestimonials() {
</h2>
</div>
<div
className="landing-reveal mt-16"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
<div className="landing-reveal mt-16" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
{/* Quote — fades on change */}
<div className="min-h-[8rem] max-w-[58rem]">
<blockquote
@ -128,10 +120,7 @@ export function LandingTestimonials() {
</div>
{/* Vertical divider */}
<div
className="w-px h-10 shrink-0"
style={{ background: "var(--landing-border-default)" }}
/>
<div className="w-px h-10 shrink-0" style={{ background: "var(--landing-border-default)" }} />
{/* Author — fades on change */}
<div
@ -140,12 +129,8 @@ export function LandingTestimonials() {
transition: "opacity 0.4s ease 0.05s",
}}
>
<div className="text-[0.9375rem] font-medium text-[var(--landing-fg)]">
{t.name}
</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60">
{t.role}
</div>
<div className="text-[0.9375rem] font-medium text-[var(--landing-fg)]">{t.name}</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60">{t.role}</div>
</div>
</div>

View File

@ -135,8 +135,7 @@ export function LandingUseCases() {
One orchestrator, many jobs
</h2>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6] max-w-[34rem] mx-auto mb-12">
Point AO at the work and walk away drag to explore what a single
run can do.
Point AO at the work and walk away drag to explore what a single run can do.
</p>
</div>
@ -157,10 +156,8 @@ export function LandingUseCases() {
maxWidth: "1120px",
cursor: "grab",
touchAction: "pan-y",
WebkitMaskImage:
"linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
maskImage:
"linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
WebkitMaskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
maskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
}}
>
<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">
{c.eyebrow}
</div>
<h3
className="font-sans font-[680] text-[1.3125rem] tracking-tight"
style={{ marginTop: "1rem" }}
>
<h3 className="font-sans font-[680] text-[1.3125rem] tracking-tight" style={{ marginTop: "1rem" }}>
{c.title}
</h3>
<p
@ -218,12 +212,9 @@ export function LandingUseCases() {
style={{ marginTop: "auto", padding: "0.75rem 0.875rem" }}
>
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
<span className={dim}>{c.prefix}</span>{" "}
<span className={fg}>{c.cmd}</span>
</div>
<div className={`whitespace-nowrap overflow-hidden text-ellipsis ${ok}`}>
{c.outcome}
<span className={dim}>{c.prefix}</span> <span className={fg}>{c.cmd}</span>
</div>
<div className={`whitespace-nowrap overflow-hidden text-ellipsis ${ok}`}> {c.outcome}</div>
</div>
</div>
</div>

View File

@ -14,12 +14,42 @@ type 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: "pr_open", label: "Open PR", icon: "pr", desc: "Work is pushed and a pull request opens against main with a summary of the changes." },
{ key: "review", label: "CI & review", icon: "review", desc: "CI fails? It reads the logs and pushes a fix. Review comments land? It addresses them." },
{ key: "mergeable", label: "Mergeable", icon: "mergeable", desc: "Green checks, approvals in. The PR settles into a clean, mergeable state." },
{ key: "merged", label: "Merged", icon: "merged", desc: "It lands on main, the worktree is archived, and the session is marked done." },
{
key: "spawning",
label: "Spawn",
icon: "spawn",
desc: "Each issue spawns an agent in its own git worktree — isolated branch, isolated context.",
},
{
key: "working",
label: "Work",
icon: "work",
desc: "The agent writes code, runs the test suite, and commits. Watch it live or let it run.",
},
{
key: "pr_open",
label: "Open PR",
icon: "pr",
desc: "Work is pushed and a pull request opens against main with a summary of the changes.",
},
{
key: "review",
label: "CI & review",
icon: "review",
desc: "CI fails? It reads the logs and pushes a fix. Review comments land? It addresses them.",
},
{
key: "mergeable",
label: "Mergeable",
icon: "mergeable",
desc: "Green checks, approvals in. The PR settles into a clean, mergeable state.",
},
{
key: "merged",
label: "Merged",
icon: "merged",
desc: "It lands on main, the worktree is archived, and the session is marked done.",
},
];
// Ruler geometry (viewBox units)
@ -58,10 +88,7 @@ export function LandingWorkflow() {
useEffect(() => {
const el = wrapRef.current;
if (!el) return;
const ob = new IntersectionObserver(
([entry]) => entry.isIntersecting && setInView(true),
{ threshold: 0.25 },
);
const ob = new IntersectionObserver(([entry]) => entry.isIntersecting && setInView(true), { threshold: 0.25 });
ob.observe(el);
return () => ob.disconnect();
}, []);
@ -96,10 +123,7 @@ export function LandingWorkflow() {
// Auto-loop
useEffect(() => {
if (!inView || paused) return;
const t = window.setTimeout(
() => setActive((a) => (a + 1) % milestones.length),
STEP_MS,
);
const t = window.setTimeout(() => setActive((a) => (a + 1) % milestones.length), STEP_MS);
return () => window.clearTimeout(t);
}, [active, paused, inView]);
@ -189,9 +213,7 @@ export function LandingWorkflow() {
style={{ opacity: show ? 1 : 0, transition: "opacity 0.35s ease" }}
>
<LifecycleIcon kind={cur.icon} />
<div className="font-sans font-[680] tracking-tight text-[1.5rem] tracking-[-0.5px] mt-3">
{cur.label}
</div>
<div className="font-sans font-[680] tracking-tight text-[1.5rem] tracking-[-0.5px] mt-3">{cur.label}</div>
<div className="font-mono text-[0.6875rem] tracking-[0.08em] text-[var(--landing-accent)] opacity-80 mt-1">
{cur.key}
</div>
@ -208,7 +230,15 @@ export function LandingWorkflow() {
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-auto" role="img" aria-label="Lifecycle scrubber">
{/* center indicator */}
<line x1={CENTER_X} y1={4} x2={CENTER_X} y2={H - 8} stroke="var(--landing-accent)" strokeWidth={2} strokeLinecap="round" />
<line
x1={CENTER_X}
y1={4}
x2={CENTER_X}
y2={H - 8}
stroke="var(--landing-accent)"
strokeWidth={2}
strokeLinecap="round"
/>
<circle cx={CENTER_X} cy={arcTop(0)} r={4} fill="var(--landing-accent)" />
{/* ticks */}
@ -257,7 +287,6 @@ export function LandingWorkflow() {
>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6]">{cur.desc}</p>
</div>
</div>
</section>
);

View File

@ -145,11 +145,6 @@ export function PageConstellation() {
}, []);
return (
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none"
style={{ zIndex: 0 }}
aria-hidden="true"
/>
<canvas 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) => {

View File

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

View File

@ -45,9 +45,7 @@ function Cell({ platform, status }: { platform: "macos" | "linux" | "windows"; s
>
<Logo name={logoName} size={18} />
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-fd-foreground)" }}>
{title}
</span>
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-fd-foreground)" }}>{title}</span>
<span
style={{
fontSize: "0.75rem",
@ -73,12 +71,7 @@ function Cell({ platform, status }: { platform: "macos" | "linux" | "windows"; s
);
}
export function PlatformSupport({
macos = "full",
linux = "full",
windows = "full",
note,
}: PlatformSupportProps) {
export function PlatformSupport({ macos = "full", linux = "full", windows = "full", note }: PlatformSupportProps) {
return (
<div style={{ margin: "1.25rem 0" }}>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>

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.
| Slot | Default | Purpose | Interface |
|------|---------|---------|-----------|
| --------- | ----------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
| Runtime | [tmux](/docs/plugins/runtimes/tmux) | Where agent sessions execute (tmux, process, docker, k8s) | `Runtime` |
| Agent | [claude-code](/docs/plugins/agents/claude-code) | Which AI coding tool is launched | `Agent` |
| Workspace | [worktree](/docs/plugins/workspaces/worktree) | Code isolation — each session gets its own git worktree or clone | `Workspace` |
@ -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` |
<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>
---
@ -57,7 +59,7 @@ pr_open ────────────────────────
Terminal statuses (session is dead and will no longer be polled): `killed`, `terminated`, `done`, `cleanup`, `errored`, `merged`.
| Status | Description |
|--------|-------------|
| ------------------- | ----------------------------------------------------------------------------- |
| `spawning` | Session is being created — worktree, branch, and tmux window are initialising |
| `working` | Agent is active; no PR yet |
| `pr_open` | Agent has pushed a PR; CI and reviews are pending |
@ -100,7 +102,7 @@ Priority is inferred by `inferPriority()` in `lifecycle-manager.ts`:
- **info** — everything else, including all `summary.*` events
| `event.type` | Priority | When emitted |
|---|---|---|
| ---------------------------- | -------- | -------------------------------------------------- |
| `session.spawned` | info | Session transitions out of `spawning` |
| `session.working` | info | Session enters `working` |
| `session.exited` | info | Agent process exits |
@ -205,7 +207,7 @@ Every agent plugin must implement `getActivityState(session, readyThresholdMs?)`
### The 6 activity states
| State | Meaning | When |
|---|---|---|
| --------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| `active` | Agent is processing — thinking, writing code, running tools | Activity within the last 30 seconds |
| `ready` | Agent finished its turn and is alive, waiting for input | 30 seconds 5 minutes since last activity |
| `idle` | Agent has been quiet for an extended period | More than 5 minutes since last activity (default threshold) |
@ -237,20 +239,22 @@ Every agent plugin must implement this cascade in order:
```
<Callout type="warn">
Step 4 (the JSONL entry fallback) is mandatory. Skipping it means `getActivityState` returns `null` whenever the native API fails — the dashboard shows no activity state and stuck-detection breaks for the entire session lifetime. This was a real bug in the OpenCode plugin.
Step 4 (the JSONL entry fallback) is mandatory. Skipping it means `getActivityState` returns `null` whenever the
native API fails — the dashboard shows no activity state and stuck-detection breaks for the entire session lifetime.
This was a real bug in the OpenCode plugin.
</Callout>
### Two JSONL patterns
| Pattern | Used by | How it works |
|---|---|---|
| ---------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Agent-native JSONL** | Claude Code, Codex | The agent writes its own JSONL with rich state entries (`permission_request`, `tool_call`, `error`, etc.). `getActivityState` reads the last entry and maps it to activity states. |
| **AO activity JSONL** | Aider, OpenCode, new agents | The agent implements `recordActivity`, which calls `recordTerminalActivity()` → `classifyTerminalActivity()` → `appendActivityEntry()` to write to `{workspacePath}/.ao/activity.jsonl`. `getActivityState` reads from this file. |
### Thresholds
| Constant | Value | Purpose |
|---|---|---|
| ----------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_ACTIVE_WINDOW_MS` | 30 seconds | Activity newer than this is `active`; older is `ready` |
| `DEFAULT_READY_THRESHOLD_MS` | 5 minutes | `ready` sessions older than this become `idle` |
| `ACTIVITY_INPUT_STALENESS_MS` | 5 minutes | Deprecated compatibility export. `waiting_input` / `blocked` entries no longer expire by wallclock; they persist until process death or a newer entry overrides them. |
@ -270,6 +274,7 @@ Claude Code writes `.claude/settings.json` with a `PostToolUse` hook that fires
Agents without a native hook system (Codex, Aider, OpenCode, custom agents) use `~/.ao/bin/gh` and `~/.ao/bin/git` shell wrappers. These wrappers are installed to `~/.ao/bin/` by `setupPathWrapperWorkspace(workspacePath)` from `packages/core/src/agent-workspace-hooks.ts`. The function also writes session context to `{workspacePath}/.ao/AGENTS.md` (gitignored — does not touch tracked files).
The wrappers intercept:
- `gh pr create` — captures the PR URL from stdout and writes `pr=<url>` and `status=pr_open`
- `gh pr merge` — writes `status=merged`
- `git checkout -b <branch>` / `git switch -c <branch>` — writes `branch=<name>`
@ -333,8 +338,24 @@ agent-orchestrator.yaml ──► Config Loader (Zod) ──► Plugin Registry
## Next Steps
<Cards>
<Card title="Project Configuration" href="/docs/configuration/projects" description="Configure projects, agents, trackers, and reactions in agent-orchestrator.yaml." />
<Card title="Authoring Plugins" href="/docs/plugins/authoring" description="Build custom runtime, agent, tracker, SCM, notifier, or terminal plugins." />
<Card title="Reactions" href="/docs/configuration/reactions" description="Set up automated reactions to CI failures, review comments, and merge events." />
<Card title="Configuration" href="/docs/configuration" description="Understand the global registry, local project config, and where AO stores runtime data." />
<Card
title="Project Configuration"
href="/docs/configuration/projects"
description="Configure projects, agents, trackers, and reactions in agent-orchestrator.yaml."
/>
<Card
title="Authoring Plugins"
href="/docs/plugins/authoring"
description="Build custom runtime, agent, tracker, SCM, notifier, or terminal plugins."
/>
<Card
title="Reactions"
href="/docs/configuration/reactions"
description="Set up automated reactions to CI failures, review comments, and merge events."
/>
<Card
title="Configuration"
href="/docs/configuration"
description="Understand the global registry, local project config, and where AO stores runtime data."
/>
</Cards>

View File

@ -38,7 +38,7 @@ ao
> Start orchestrator agent and dashboard (auto-creates config on first run, adds projects by path/URL)
| Flag | Default | Purpose |
|---|---|---|
| ------------------- | ------- | ------------------------------------------------------------------------------- |
| `--no-dashboard` | — | Skip starting the dashboard server |
| `--no-orchestrator` | — | Skip starting the orchestrator agent |
| `--rebuild` | — | Clean and rebuild the dashboard before starting |
@ -54,7 +54,7 @@ If AO is already running, you'll get an interactive prompt to open/restart/spawn
> Stop orchestrator agent and dashboard
| Flag | Default | Purpose |
|---|---|---|
| ----------------- | ------- | ------------------------------------------------ |
| `--purge-session` | — | Delete the mapped OpenCode session when stopping |
| `--all` | — | Stop all running AO instances on this machine |
@ -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
| Flag | Default | Purpose |
|---|---|---|
| ---------------------- | ------- | --------------------- |
| `-p, --project <id>` | — | Filter to one project |
| `--json` | — | Output JSON |
| `-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
| Flag | Default | Purpose |
|---|---|---|
| -------------------- | -------------- | --------------------------------------------------------------------------------- |
| `--open` | — | Open the session in a terminal tab |
| `--agent <name>` | config default | Override the agent plugin (`claude-code`, `codex`, `cursor`, `aider`, `opencode`) |
| `--claim-pr <pr>` | — | Immediately claim an existing PR for the spawned session |
@ -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`.
<Callout type="warn">
The old two-arg form `ao spawn &lt;project&gt; &lt;issue&gt;` is rejected with an error. Use `-p` or run from inside a worktree.
The old two-arg form `ao spawn &lt;project&gt; &lt;issue&gt;` is rejected with an error. Use `-p` or run from inside a
worktree.
</Callout>
### `ao batch-spawn <issues...>`
@ -94,7 +95,7 @@ Positional `[first]` — issue identifier. The project is auto-detected from `cw
> Spawn sessions for multiple issues with duplicate detection
| Flag | Default | Purpose |
|---|---|---|
| -------- | ------- | ------------------------------ |
| `--open` | — | Open sessions in terminal tabs |
Skips duplicates within the batch and any issues that already have an active session.
@ -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
| Flag | Default | Purpose |
|---|---|---|
| --------------------- | ------- | -------------------------------------------------------- |
| `-f, --file <path>` | — | Send contents of a file instead of an inline message |
| `--no-wait` | — | Don't wait for the session to become idle before sending |
| `--timeout <seconds>` | `600` | Max seconds to wait for idle |
@ -116,7 +117,7 @@ Positional: `<session>` required, `[message...]` variadic. One of inline message
> Check PRs for review comments and trigger agents to address them
| Flag | Default | Purpose |
|---|---|---|
| ----------- | ------- | ------------------------------------------ |
| `--dry-run` | — | Show what would happen; don't nudge agents |
Checks all projects if `[project]` is omitted.
@ -126,7 +127,7 @@ Checks all projects if `[project]` is omitted.
> Start the web dashboard
| Flag | Default | Purpose |
|---|---|---|
| ------------------- | ----------------------- | ----------------------------------------- |
| `-p, --port <port>` | config `port` or `3000` | Port to listen on |
| `--no-open` | — | Don't open the browser |
| `--rebuild` | — | Clean stale Next.js artifacts and rebuild |
@ -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
| Flag | Default | Purpose |
|---|---|---|
| ------------------ | ------- | ---------------------------------------------- |
| `-w, --new-window` | — | Open in a new terminal window instead of a tab |
Positional `[target]` — session name, project id, or `"all"`. Defaults to all sessions. Falls back to printing the dashboard URL when the native terminal helper isn't available (anywhere but macOS + iTerm2).
@ -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
| Flag | Default | Purpose |
|---|---|---|
| --------------------- | ------- | ------------------------------------------------ |
| `-p, --project <id>` | — | Required if multiple projects |
| `--fail` | — | Mark as failed instead of passing |
| `-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
| Flag | Default | Purpose |
|---|---|---|
| --------------- | ------- | --------------------------------------------------------- |
| `--fix` | — | Apply safe fixes for launcher and stale-temp issues |
| `--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
| Flag | Default | Purpose |
|---|---|---|
| -------------- | ------- | ------------------------------------------------------------------ |
| `--skip-smoke` | — | Skip smoke tests after rebuilding (git installs only) |
| `--smoke-only` | — | Run smoke tests without fetching or rebuilding (git installs only) |
| `--check` | — | Print version info as JSON; don't upgrade |
@ -192,7 +193,7 @@ No flags.
> Send a manual demo notification without spawning sessions
| Flag | Default | Purpose |
|---|---|---|
| -------------------- | ------- | ------------------------------------------------------------- |
| `--template <name>` | `basic` | Demo template to send |
| `--to <refs>` | — | Comma-separated notifier refs to target |
| `--all` | — | Send to all configured, default, and routed notifier refs |
@ -210,7 +211,7 @@ No flags.
> List all sessions
| Flag | Default | Purpose |
|---|---|---|
| -------------------- | ------- | ----------------------------- |
| `-p, --project <id>` | — | Filter to one project |
| `-a, --all` | — | Include orchestrator sessions |
| `--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
| Flag | Default | Purpose |
|---|---|---|
| ----------------- | ------- | ---------------------------------- |
| `--purge-session` | — | Delete the mapped OpenCode session |
### `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
| Flag | Default | Purpose |
|---|---|---|
| -------------------- | ------- | ----------------------------- |
| `-p, --project <id>` | — | Filter to one project |
| `--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
| Flag | Default | Purpose |
|---|---|---|
| -------------------- | ------- | ------------------------------------------------- |
| `--assign-on-github` | — | Assign the PR to the authenticated user on GitHub |
If `[session]` is omitted, falls back to `AO_SESSION_NAME` / `AO_SESSION` env vars.
@ -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
| Flag | Default | Purpose |
|---|---|---|
| ------------- | ------- | ------------------------ |
| `-f, --force` | — | Override a stale mapping |
## Setup subcommands
@ -270,7 +271,7 @@ mode, AO asks which notification priorities the notifier should receive before
writing `notificationRouting`.
| Command | Purpose |
|---|---|
| ------------------------------- | ------------------------------------------------------ |
| `ao setup dashboard` | Configure dashboard notification retention and routing |
| `ao setup desktop` | Configure native desktop notifications |
| `ao setup webhook` | Configure a generic HTTP webhook |
@ -288,7 +289,7 @@ writing `notificationRouting`.
> Connect AO notifications to an OpenClaw gateway
| Flag | Default | Purpose |
|---|---|---|
| ------------------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
| `--url <url>` | — | OpenClaw webhook URL |
| `--token <token>` | — | Remote/manual fallback; local setup should read `hooks.token` from OpenClaw config |
| `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
@ -308,7 +309,7 @@ Interactive mode (TTY) uses `@clack/prompts` with review/change steps for the ga
> List bundled marketplace plugins
| Flag | Default | Purpose |
|---|---|---|
| --------------- | ------- | ----------------------------------------------------------------------------------------- |
| `--installed` | — | Only show plugins present in your current config |
| `--type <slot>` | — | Filter by slot: `agent`, `runtime`, `tracker`, `scm`, `notifier`, `workspace`, `terminal` |
| `--refresh` | — | Re-fetch the marketplace catalog |
@ -324,7 +325,7 @@ No flags.
> Scaffold a new AO plugin package
| Flag | Default | Purpose |
|---|---|---|
| ----------------------- | ------- | ------------------------------- |
| `--name <name>` | prompt | Plugin name |
| `--slot <slot>` | prompt | Which slot it implements |
| `--description <text>` | prompt | Manifest description |
@ -337,7 +338,7 @@ No flags.
> Install a plugin into the current config
| Flag | Default | Purpose |
|---|---|---|
| ------------------------------- | --------------------------- | ----------------------------------------------------------------- |
| `--url <url>` | — | Passed to `ao setup openclaw` when installing `notifier-openclaw` |
| `--token <token>` | — | Remote/manual OpenClaw token fallback |
| `--openclaw-config-path <path>` | `~/.openclaw/openclaw.json` | OpenClaw config path that contains `hooks.token` |
@ -349,7 +350,7 @@ No flags.
> Update installer-managed plugins in the current config
| Flag | Default | Purpose |
|---|---|---|
| ------- | ------- | -------------------------- |
| `--all` | — | Update all managed plugins |
Requires either `[reference]` or `--all`.
@ -365,7 +366,7 @@ No flags.
A few env vars affect every command:
| Variable | Purpose |
|---|---|
| ----------------------------------- | ------------------------------------------------------------------------------------ |
| `AO_DATA_DIR` | Override the `~/.agent-orchestrator` base directory. |
| `AO_LOG_LEVEL` | `error` · `warn` · `info` · `debug` · `trace` |
| `AO_CONFIG_PATH` | Absolute path to a specific `agent-orchestrator.yaml`. Overrides CWD-based search. |

View File

@ -11,7 +11,9 @@ AO uses configuration in two layers:
Most users should let `ao start` create the registry entry, then edit the local `agent-orchestrator.yaml` in the project when they want to change behavior.
<Callout type="info">
The old wrapped format with a top-level `projects:` block is still understood for compatibility, but new project-local configs should be flat. Do not put identity fields like `path`, `projectId`, `storageKey`, or `originUrl` in a local project config.
The old wrapped format with a top-level `projects:` block is still understood for compatibility, but new project-local
configs should be flat. Do not put identity fields like `path`, `projectId`, `storageKey`, or `originUrl` in a local
project config.
</Callout>
## What AO Creates
@ -73,7 +75,7 @@ agentRules: |
## What To Edit
| Goal | Edit |
|------|------|
| ------------------------------------------------ | ---------------------------------------------- |
| Change the default agent or model | Local `agent-orchestrator.yaml` |
| Add `.env` or tool config files to each worktree | Local `symlinks` |
| Run setup commands before the agent starts | Local `postCreate` |
@ -147,7 +149,7 @@ For normal use, run AO from inside the repository and keep `agent-orchestrator.y
## Top-Level Global Keys
| Key | Default | Purpose |
|-----|---------|---------|
| --------------------- | ------------------------- | ------------------------------------------------------------ |
| `port` | `3000` | Dashboard HTTP port |
| `terminalPort` | auto from `14800` | tmux terminal WebSocket port |
| `directTerminalPort` | auto from `14801` | direct PTY WebSocket port |
@ -172,7 +174,19 @@ ls ~/.agent-orchestrator
## Next Steps
<Cards>
<Card title="Projects" href="/docs/configuration/projects" description="Per-project behavior settings: agents, workspaces, rules, setup, trackers, and SCM." />
<Card title="Reactions" href="/docs/configuration/reactions" description="Control what AO does when CI fails, reviews arrive, or agents need help." />
<Card title="Remote access" href="/docs/configuration/remote-access" description="Access the dashboard from another machine without exposing it publicly." />
<Card
title="Projects"
href="/docs/configuration/projects"
description="Per-project behavior settings: agents, workspaces, rules, setup, trackers, and SCM."
/>
<Card
title="Reactions"
href="/docs/configuration/reactions"
description="Control what AO does when CI fails, reviews arrive, or agents need help."
/>
<Card
title="Remote access"
href="/docs/configuration/remote-access"
description="Access the dashboard from another machine without exposing it publicly."
/>
</Cards>

View File

@ -35,7 +35,7 @@ agent: claude-code
Built-in agents:
| Agent | Use when |
|-------|----------|
| ------------- | ------------------------------------------------------ |
| `claude-code` | You want the default, most tested path |
| `codex` | You want OpenAI Codex CLI sessions |
| `aider` | You already use Aider workflows |
@ -52,7 +52,7 @@ agentConfig:
`permissions` accepts:
| Value | Behavior |
|-------|----------|
| ---------------- | ----------------------------------------------------- |
| `permissionless` | Let the agent edit and run commands without prompting |
| `default` | Use the agent tool's normal permission behavior |
| `auto-edit` | Auto-approve edits, ask for other actions |
@ -69,7 +69,7 @@ runtime: tmux
```
| Runtime | Use when |
|---------|----------|
| --------- | ----------------------------------------------------------------------------------------------- |
| `tmux` | You want persistent sessions that survive dashboard reloads and can be attached from a terminal |
| `process` | You want a lighter direct process runtime and do not need tmux persistence |
@ -84,7 +84,7 @@ workspace: worktree
```
| Workspace | Use when |
|-----------|----------|
| ---------- | ----------------------------------------------------------------- |
| `worktree` | Default. Fast, disk-efficient, creates a git worktree per session |
| `clone` | Slower, but gives each session a separate clone |
@ -173,7 +173,7 @@ scm:
Built-in trackers:
| Plugin | Purpose |
|--------|---------|
| -------- | ------------- |
| `github` | GitHub issues |
| `gitlab` | GitLab issues |
| `linear` | Linear issues |
@ -181,7 +181,7 @@ Built-in trackers:
Built-in SCM plugins:
| Plugin | Purpose |
|--------|---------|
| -------- | ---------------------------------------- |
| `github` | GitHub PRs, checks, reviews, merge state |
| `gitlab` | GitLab merge requests |
@ -234,7 +234,7 @@ opencodeIssueSessionStrategy: reuse
`orchestratorSessionStrategy` accepts:
| Value | Behavior |
|-------|----------|
| --------------- | ----------------------------------------------------- |
| `reuse` | Attach to the existing orchestrator session |
| `delete` | Delete the old session and start a new one |
| `ignore` | Leave the old session and start another |
@ -249,7 +249,7 @@ opencodeIssueSessionStrategy: reuse
These fields are valid in a local project config:
| Field | Type | Purpose |
|-------|------|---------|
| ------------------------------ | ---------- | -------------------------------------------- |
| `repo` | `string` | Optional legacy/local repo slug |
| `defaultBranch` | `string` | Branch PRs target, usually `main` |
| `agent` | `string` | Default worker agent |
@ -292,7 +292,19 @@ Make sure the corresponding CLI or token is authenticated for the plugin you use
## Next Steps
<Cards>
<Card title="Reactions" href="/docs/configuration/reactions" description="Configure what AO does when CI, reviews, or agent state changes need attention." />
<Card title="Remote access" href="/docs/configuration/remote-access" description="Open the dashboard from another machine without exposing it publicly." />
<Card title="Plugin docs" href="/docs/plugins" description="Agent, tracker, SCM, notifier, runtime, terminal, and workspace plugins." />
<Card
title="Reactions"
href="/docs/configuration/reactions"
description="Configure what AO does when CI, reviews, or agent state changes need attention."
/>
<Card
title="Remote access"
href="/docs/configuration/remote-access"
description="Open the dashboard from another machine without exposing it publicly."
/>
<Card
title="Plugin docs"
href="/docs/plugins"
description="Agent, tracker, SCM, notifier, runtime, terminal, and workspace plugins."
/>
</Cards>

View File

@ -58,13 +58,14 @@ reactions:
```
<Callout type="warning">
`auto-merge` is currently an intent flag, not a bypass. AO does not ignore branch protection, reviews, or failing checks.
`auto-merge` is currently an intent flag, not a bypass. AO does not ignore branch protection, reviews, or failing
checks.
</Callout>
## Default Reactions
| Key | When it fires | Default action | Notes |
|-----|---------------|----------------|-------|
| -------------------- | ------------------------------------------- | --------------- | --------------------------------------------------- |
| `ci-failed` | CI check fails | `send-to-agent` | Sends the agent a recovery prompt |
| `changes-requested` | Human requests changes | `send-to-agent` | Sends review feedback to the agent |
| `bugbot-comments` | Known automation leaves actionable feedback | `send-to-agent` | Keeps bot feedback separate from human reviews |
@ -79,7 +80,7 @@ reactions:
## Action Types
| Action | Meaning |
|--------|---------|
| --------------- | ----------------------------------------------------------- |
| `send-to-agent` | Sends `message` into the running agent session |
| `notify` | Routes an event to configured notifiers |
| `auto-merge` | Reserved merge intent; currently routes like a notification |
@ -91,7 +92,7 @@ reactions:
Each reaction accepts:
| Field | Type | Purpose |
|-------|------|---------|
| ---------------- | ------------------------------------------- | ---------------------------------------------------------- |
| `auto` | `boolean` | Whether AO should perform the automated action |
| `action` | `send-to-agent` \| `notify` \| `auto-merge` | What AO does when the reaction fires |
| `message` | `string` | Message sent to the agent or included in notification text |
@ -173,7 +174,7 @@ ao review-check <session-id>
## Event Reference
| Event | Reaction |
|-------|----------|
| -------------------------- | -------------------- |
| `ci.failing` | `ci-failed` |
| `review.changes_requested` | `changes-requested` |
| `automated_review.found` | `bugbot-comments` |
@ -188,6 +189,14 @@ ao review-check <session-id>
<Cards>
<Card title="Projects" href="/docs/configuration/projects" description="Apply reaction overrides to one project." />
<Card title="Webhook notifier" href="/docs/plugins/notifiers/webhook" description="Send AO events to any HTTPS endpoint." />
<Card title="Configuration" href="/docs/configuration" description="Understand global registry vs local project config." />
<Card
title="Webhook notifier"
href="/docs/plugins/notifiers/webhook"
description="Send AO events to any HTTPS endpoint."
/>
<Card
title="Configuration"
href="/docs/configuration"
description="Understand global registry vs local project config."
/>
</Cards>

View File

@ -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
| Variable | Purpose |
|----------|---------|
| ------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `HOST=0.0.0.0` | Bind the Next.js dashboard to all interfaces |
| `TERMINAL_PORT` | Override the tmux WS server port (server-side) |
| `DIRECT_TERMINAL_PORT` | Override the direct PTY WS server port (server-side) |
@ -270,7 +270,7 @@ For more on project identity, local config, and runtime data, see [Configuration
## Port reference
| Port | Default | Config key | Env var override | Purpose |
|------|---------|------------|-----------------|---------|
| ------- | ------------------ | -------------------- | ---------------------- | ------------------------------------ |
| `3000` | dashboard HTTP | `port` | `PORT` | Next.js app + API routes |
| `14800` | tmux terminal WS | `terminalPort` | `TERMINAL_PORT` | WebSocket for tmux-attached terminal |
| `14801` | direct terminal WS | `directTerminalPort` | `DIRECT_TERMINAL_PORT` | WebSocket for direct PTY terminal |

View File

@ -22,7 +22,7 @@ http://localhost:3000
The main dashboard groups sessions by what you need to do next.
| Column | What it means | What you usually do |
|--------|---------------|---------------------|
| ----------- | -------------------------------------------------- | ----------------------------------------- |
| **Working** | Agent is actively coding or running commands | Let it run |
| **Pending** | Session exists but has not started useful work yet | Check if it stays here too long |
| **Review** | PR is open and waiting for review | Review the PR or wait for CI |
@ -59,7 +59,7 @@ Open `/prs` to see every PR created by AO-managed sessions.
The PR page lets you filter by:
| Tab | Shows |
|-----|-------|
| ---------- | ---------------------------- |
| **All** | Open, merged, and closed PRs |
| **Open** | PRs still in progress |
| **Merged** | Completed PRs |
@ -84,7 +84,7 @@ Use the terminal to inspect what the agent is doing, recover from stuck prompts,
AO has two terminal backends:
| Backend | Used when | Default port |
|---------|-----------|--------------|
| -------------------- | ------------------ | ------------ |
| tmux WebSocket mux | `runtime: tmux` | `14800` |
| direct PTY WebSocket | `runtime: process` | `14801` |
@ -120,7 +120,7 @@ The favicon and document title also change when sessions need attention, so you
## Routes
| Route | Use for |
|-------|---------|
| ------------------------------------- | ----------------------------------------- |
| `/` | Project overview or current project board |
| `/projects/{projectId}` | One project's board |
| `/projects/{projectId}/settings` | Project behavior settings |
@ -136,7 +136,7 @@ Internal API routes such as `/api/events`, `/api/sessions`, `/api/projects`, and
The full dashboard experience uses three ports:
| Service | Default | Config |
|---------|---------|--------|
| ----------------------- | ----------------- | ---------------------------------------------- |
| Dashboard HTTP | `3000` | `port` or `PORT` |
| tmux terminal WebSocket | auto from `14800` | `terminalPort` or `TERMINAL_PORT` |
| direct PTY WebSocket | auto from `14801` | `directTerminalPort` or `DIRECT_TERMINAL_PORT` |
@ -164,6 +164,14 @@ Open the session detail page and send a short, direct instruction. The agent rec
<Cards>
<Card title="Quickstart" href="/docs/quickstart" description="Start AO, spawn a session, and handle the first PR." />
<Card title="Configuration" href="/docs/configuration" description="Understand global registry and local project settings." />
<Card title="Remote access" href="/docs/configuration/remote-access" description="Open the dashboard from another device safely." />
<Card
title="Configuration"
href="/docs/configuration"
description="Understand global registry and local project settings."
/>
<Card
title="Remote access"
href="/docs/configuration/remote-access"
description="Open the dashboard from another device safely."
/>
</Cards>

View File

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

View File

@ -10,53 +10,63 @@ import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
Tabs are fine for one or two. AO handles the bookkeeping once you have more than that: per-session worktrees, per-session branches, automatic CI/review reactions, one dashboard, cost tracking, and a state machine that knows what each agent is doing.
</Accordion>
<Accordion title="Do I have to use Claude Code?">
No. AO supports Claude Code, Codex, Cursor, Aider, and OpenCode today. Set `agent:` per project (or per spawn with `--agent`). See [Agents](/docs/plugins/agents).
</Accordion>
<Accordion title="Do I have to use Claude Code?">
No. AO supports Claude Code, Codex, Cursor, Aider, and OpenCode today. Set `agent:` per project (or per spawn with
`--agent`). See [Agents](/docs/plugins/agents).
</Accordion>
<Accordion title="Does AO auto-merge?">
No, intentionally. AO will get a PR to a mergeable state — CI green, approvals satisfied — and stop. The merge is your call.
</Accordion>
<Accordion title="Does AO auto-merge?">
No, intentionally. AO will get a PR to a mergeable state — CI green, approvals satisfied — and stop. The merge is your
call.
</Accordion>
<Accordion title="Does AO need webhooks?">
No. AO polls `gh` / `glab` for PR state. Webhooks are an optional latency optimisation for the review loop — wire them up only if you need near-instant reactions.
</Accordion>
<Accordion title="Does AO need webhooks?">
No. AO polls `gh` / `glab` for PR state. Webhooks are an optional latency optimisation for the review loop — wire them
up only if you need near-instant reactions.
</Accordion>
<Accordion title="Can it use GitHub Enterprise / self-hosted GitLab?">
Yes. `gh` and `glab` both support enterprise hosts — configure them via their own tools. For GitLab, also set `trackerConfig.host` / `scmConfig.host` in AO.
</Accordion>
<Accordion title="Can it use GitHub Enterprise / self-hosted GitLab?">
Yes. `gh` and `glab` both support enterprise hosts — configure them via their own tools. For GitLab, also set
`trackerConfig.host` / `scmConfig.host` in AO.
</Accordion>
<Accordion title="Does it work on Windows?">
Partially — see [Platforms Windows](/docs/platforms#windows). You need `runtime: process` instead of tmux, and desktop notifications are a no-op. Everything else (spawning, PR flow, review loop, CI recovery) works the same.
</Accordion>
<Accordion title="Does it work on Windows?">
Partially — see [Platforms Windows](/docs/platforms#windows). You need `runtime: process` instead of tmux, and
desktop notifications are a no-op. Everything else (spawning, PR flow, review loop, CI recovery) works the same.
</Accordion>
<Accordion title="Where does session state live?">
<Accordion title="Where does session state live?">
`~/.agent-orchestrator/{hash}-{projectId}/`. No database. Everything is flat files you can inspect.
</Accordion>
</Accordion>
<Accordion title="Can I run AO on a remote server?">
Yes. The dashboard is a plain Next.js app — port-forward or put it behind your reverse proxy. AO has no built-in auth, so use your usual SSO / basic-auth layer. Use `runtime: process` in containers.
</Accordion>
<Accordion title="Can I run AO on a remote server?">
Yes. The dashboard is a plain Next.js app — port-forward or put it behind your reverse proxy. AO has no built-in auth,
so use your usual SSO / basic-auth layer. Use `runtime: process` in containers.
</Accordion>
<Accordion title="How much does it cost?">
AO itself is MIT-licensed and free. Agents cost whatever their underlying API costs (Anthropic, OpenAI, etc.) — AO shows per-session cost on the dashboard for agents that report it (Claude Code today).
</Accordion>
<Accordion title="How much does it cost?">
AO itself is MIT-licensed and free. Agents cost whatever their underlying API costs (Anthropic, OpenAI, etc.) — AO
shows per-session cost on the dashboard for agents that report it (Claude Code today).
</Accordion>
<Accordion title="What happens to my code?">
Your code stays in your worktrees on your machine. AO doesn't upload anything — it just coordinates local processes and makes GitHub API calls through your authenticated CLIs.
</Accordion>
<Accordion title="What happens to my code?">
Your code stays in your worktrees on your machine. AO doesn't upload anything — it just coordinates local processes
and makes GitHub API calls through your authenticated CLIs.
</Accordion>
<Accordion title="Can I write my own plugin?">
Yes. `ao plugin create --slot <slot> --name <name>` scaffolds a starter. Plugins are tiny Node packages exporting a manifest + `create()` function.
</Accordion>
<Accordion title="Why is everything a plugin?">
Because the answer to "should AO support Linear?" (or Discord, or Codex, or …) is always yes, but bundling every integration into core makes the CLI heavy. Plugins let you pick what you need.
</Accordion>
<Accordion title="Why is everything a plugin?">
Because the answer to "should AO support Linear?" (or Discord, or Codex, or …) is always yes, but bundling every
integration into core makes the CLI heavy. Plugins let you pick what you need.
</Accordion>
<Accordion title="Does AO modify my repo's files?">
The agent modifies the worktree — that's the whole point. AO itself doesn't touch `main`, doesn't force-push, and doesn't merge. The session works on its own branch.
</Accordion>
<Accordion title="Does AO modify my repo's files?">
The agent modifies the worktree — that's the whole point. AO itself doesn't touch `main`, doesn't force-push, and
doesn't merge. The session works on its own branch.
</Accordion>
<Accordion title="How do I completely uninstall?">
`npm uninstall -g @aoagents/ao`, then `rm -rf ~/.agent-orchestrator` if you want to wipe all session history.

View File

@ -11,7 +11,7 @@ When your PR's CI goes red, AO notices before you do and tells the agent to fix
1. The **SCM plugin** polls the PR's check runs via `gh pr checks` (or `glab`).
2. The **lifecycle manager** transitions the session to `ci_failed` when any required check fails.
3. The **agent plugin** is woken with a prompt like *"CI failed on PR #42. The failing checks are X and Y. Investigate and push a fix."*
3. The **agent plugin** is woken with a prompt like _"CI failed on PR #42. The failing checks are X and Y. Investigate and push a fix."_
4. The **notifier plugin** pings you (desktop, Slack, Discord, whatever you configured).
No webhooks, no CI integration to install. The `gh` CLI you already authenticated with is doing the work.
@ -74,7 +74,7 @@ This creates a session, points it at PR #123, and (with `--assign-on-github`) as
When CI goes red, AO sends two distinct messages to the agent in sequence:
1. **Reaction message (poll cycle N).** The lifecycle manager detects that CI transitioned to `ci_failed` and fires the `ci-failed` reaction. This sends the configured `message` (or the default: *"CI failed on PR #42…"*) to the agent via `ao send`.
1. **Reaction message (poll cycle N).** The lifecycle manager detects that CI transitioned to `ci_failed` and fires the `ci-failed` reaction. This sends the configured `message` (or the default: _"CI failed on PR #42…"_) to the agent via `ao send`.
2. **Detailed follow-up (poll cycle N+1, ~30 s later).** On the next poll cycle, AO calls `formatCIFailureMessage()` with the actual failing checks and sends a second message with each check's name, conclusion status, and a direct link to the run log. This is delivered via `sessionManager.send()` directly — it does **not** go through `executeReaction()`, so it does not consume the `ci-failed` reaction's retry budget.

View File

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

View File

@ -1,12 +1,5 @@
{
"title": "Guides",
"defaultOpen": false,
"pages": [
"per-role-agents",
"parallel-issues",
"ci-recovery",
"review-loop",
"reactions",
"multi-project"
]
"pages": ["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">
Each project can have a different agent, tracker, notifier, and workspace — that's the whole point of the plugin system. Don't force uniformity; let teams use what fits.
Each project can have a different agent, tracker, notifier, and workspace — that's the whole point of the plugin
system. Don't force uniformity; let teams use what fits.
</Callout>

View File

@ -38,7 +38,7 @@ AO creates two distinct sessions. Compare the PRs side by side.
## Isolation guarantees
| What's isolated | How |
|---|---|
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| File edits | Each session has its own worktree under `~/.worktrees/{projectId}/{sessionId}/`. Session metadata lives under `~/.agent-orchestrator/{hash}-{projectId}/sessions/{sessionId}` — the worktree itself is in `~/.worktrees/`. |
| Branch name | AO names the branch after the session ID, avoiding collisions |
| Agent state | Each agent's native session files (Claude JSONL, Codex session, etc.) are session-scoped |
@ -79,6 +79,4 @@ ao status -p my-repo --json
- `ao session cleanup` — kill everything whose PR merged or issue closed (safe; archives metadata)
- `ao session cleanup --dry-run` — preview first
<Callout type="info">
Need them all gone? `ao stop --all` halts every running AO instance on this machine.
</Callout>
<Callout type="info">Need them all gone? `ao stop --all` halts every running AO instance on this machine.</Callout>

View File

@ -20,7 +20,8 @@ defaults:
```
<Callout type="info">
`defaults.orchestrator` and `defaults.worker` accept only the `agent` key at the global defaults level. To set `agentConfig` (model, permissions, etc.), use the per-project `orchestrator` and `worker` blocks described below.
`defaults.orchestrator` and `defaults.worker` accept only the `agent` key at the global defaults level. To set
`agentConfig` (model, permissions, etc.), use the per-project `orchestrator` and `worker` blocks described below.
</Callout>
## Per-project override

View File

@ -35,7 +35,9 @@ reactions:
```
<Callout type="warning">
The `auto-merge` action currently calls `notifyHuman()` internally — it does not perform a real merge. Actual merging still depends on your repository's branch protection rules and the "Allow auto-merge" setting on GitHub. AO does not bypass these gates. Treat `auto-merge` as an opt-in signal for when the SCM plugin adds real merge support.
The `auto-merge` action currently calls `notifyHuman()` internally — it does not perform a real merge. Actual merging
still depends on your repository's branch protection rules and the "Allow auto-merge" setting on GitHub. AO does not
bypass these gates. Treat `auto-merge` as an opt-in signal for when the SCM plugin adds real merge support.
</Callout>
## Recipe: Custom CI failure message

View File

@ -26,7 +26,8 @@ A single structured prompt with:
- A pointer to the PR head SHA
<Callout type="info">
AO reads unresolved review threads. Once you resolve a thread on GitHub, it drops out of the next nudge — so you can thumbs-up the ones the agent addressed and only the remaining ones make it back to the agent.
AO reads unresolved review threads. Once you resolve a thread on GitHub, it drops out of the next nudge — so you can
thumbs-up the ones the agent addressed and only the remaining ones make it back to the agent.
</Callout>
## Configurable behavior
@ -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:
| Login | Tool |
|---|---|
| ------------------------- | -------------- |
| `cursor[bot]` | Cursor AI |
| `github-actions[bot]` | GitHub Actions |
| `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]`):
| Login | Tool |
|---|---|
| ------------------ | --------------------- |
| `gitlab-bot` | GitLab built-in |
| `ghost` | Deleted / system user |
| `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.
<Callout type="info" title="Fastest path">
If you are new to AO, install it first, then run the quickstart against one small issue. Start with [Installation](/docs/installation), then [Quickstart](/docs/quickstart).
If you are new to AO, install it first, then run the quickstart against one small issue. Start with
[Installation](/docs/installation), then [Quickstart](/docs/quickstart).
</Callout>
## What AO Is For
@ -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 handles | You still handle |
| --- | --- |
| ---------------------------------- | -------------------------------------------- |
| Creating isolated workspaces | Choosing good issues |
| Launching and monitoring agents | Reviewing code and behavior |
| Tracking PR, CI, and review status | Deciding when to merge |
@ -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.
| Plugin slot | What it controls | Examples |
| --- | --- | --- |
| ----------- | --------------------------------- | ------------------------------------------- |
| Agent | Which coding tool writes changes | Claude Code, Codex, Cursor, Aider, OpenCode |
| Runtime | How the agent process runs | tmux, child process |
| Workspace | Where code is checked out | git worktree, full clone |
@ -65,16 +66,38 @@ Most users start with the defaults and only edit `agent-orchestrator.yaml` when
macos="full"
linux="full"
windows="partial"
note={<>Windows support is actively improving. Use the process runtime instead of tmux by setting <code>defaults.runtime: process</code> in <code>agent-orchestrator.yaml</code>. See <a href="/docs/platforms">Platforms</a> for details.</>}
note={
<>
Windows support is actively improving. Use the process runtime instead of tmux by setting{" "}
<code>defaults.runtime: process</code> in <code>agent-orchestrator.yaml</code>. See{" "}
<a href="/docs/platforms">Platforms</a> for details.
</>
}
/>
## Where To Go Next
<Cards>
<Card title="Installation" description="Install AO, authenticate your tools, and run the doctor check." href="/docs/installation" />
<Card
title="Installation"
description="Install AO, authenticate your tools, and run the doctor check."
href="/docs/installation"
/>
<Card title="Quickstart" description="Spawn one agent on one issue and watch it open a PR." href="/docs/quickstart" />
<Card title="Configuration" description="Understand the `agent-orchestrator.yaml` file." href="/docs/configuration" />
<Card title="Guides" description="Run parallel issues, recover from CI failures, and handle review loops." href="/docs/guides" />
<Card title="Plugins" description="See which agents, runtimes, trackers, SCMs, terminals, and notifiers are available." href="/docs/plugins" />
<Card title="Troubleshooting" description="Fix common install, dashboard, runtime, and session problems." href="/docs/troubleshooting" />
<Card
title="Guides"
description="Run parallel issues, recover from CI failures, and handle review loops."
href="/docs/guides"
/>
<Card
title="Plugins"
description="See which agents, runtimes, trackers, SCMs, terminals, and notifiers are available."
href="/docs/plugins"
/>
<Card
title="Troubleshooting"
description="Fix common install, dashboard, runtime, and session problems."
href="/docs/troubleshooting"
/>
</Cards>

View File

@ -11,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.
<Callout type="info" title="What AO installs">
The published package is `@aoagents/ao`. It provides the global `ao` command and includes the built-in CLI, dashboard, and plugin packages.
The published package is `@aoagents/ao`. It provides the global `ao` command and includes the built-in CLI, dashboard,
and plugin packages.
</Callout>
## Prerequisites
@ -20,13 +21,18 @@ This page gets your machine ready to run Agent Orchestrator. By the end, the `ao
macos="full"
linux="full"
windows="partial"
note={<>Windows support is actively improving. Use <code>defaults.runtime: process</code> instead of tmux. See <a href="/docs/platforms#windows">Platforms</a>.</>}
note={
<>
Windows support is actively improving. Use <code>defaults.runtime: process</code> instead of tmux. See{" "}
<a href="/docs/platforms#windows">Platforms</a>.
</>
}
/>
Install these before running AO:
| Tool | Required | Why AO needs it |
| --- | --- | --- |
| --------------- | ---------------------- | ------------------------------------------------------------------------------------ |
| Node.js 20+ | Yes | Runs the AO CLI, dashboard, and plugins. |
| Git | Yes | Creates worktrees, branches, commits, and cleanup operations. |
| GitHub CLI `gh` | For GitHub projects | Reads issues, PRs, reviews, and CI status as your user. |
@ -38,29 +44,12 @@ If you plan to use GitLab or Linear, install and authenticate their CLIs or cred
## Install AO
<Tabs items={["npm", "pnpm", "yarn", "from source"]}>
<Tab value="npm">
```bash
npm install -g @aoagents/ao
```
</Tab>
<Tab value="pnpm">
```bash
pnpm add -g @aoagents/ao
```
</Tab>
<Tab value="yarn">
```bash
yarn global add @aoagents/ao
```
</Tab>
<Tab value="npm">```bash npm install -g @aoagents/ao ```</Tab>
<Tab value="pnpm">```bash pnpm add -g @aoagents/ao ```</Tab>
<Tab value="yarn">```bash yarn global add @aoagents/ao ```</Tab>
<Tab value="from source">
```bash
git clone https://github.com/ComposioHQ/agent-orchestrator
cd agent-orchestrator
pnpm install
pnpm build
pnpm --filter @aoagents/ao link --global
```
```bash git clone https://github.com/ComposioHQ/agent-orchestrator cd agent-orchestrator pnpm install pnpm build
pnpm --filter @aoagents/ao link --global ```
</Tab>
</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.
<Tabs items={["Claude Code", "Codex", "Cursor", "Aider", "OpenCode"]}>
<Tab value="Claude Code">
```bash
npm install -g @anthropic-ai/claude-code
claude
```
</Tab>
<Tab value="Codex">
```bash
npm install -g @openai/codex
codex
```
</Tab>
<Tab value="Cursor">
```bash
curl https://cursor.com/install -fsS | bash
agent --help
```
</Tab>
<Tab value="Aider">
```bash
pip install aider-install
aider-install
aider --help
```
</Tab>
<Tab value="OpenCode">
```bash
npm install -g opencode-ai
opencode --help
```
</Tab>
<Tab value="Claude Code">```bash npm install -g @anthropic-ai/claude-code claude ```</Tab>
<Tab value="Codex">```bash npm install -g @openai/codex codex ```</Tab>
<Tab value="Cursor">```bash curl https://cursor.com/install -fsS | bash agent --help ```</Tab>
<Tab value="Aider">```bash pip install aider-install aider-install aider --help ```</Tab>
<Tab value="OpenCode">```bash npm install -g opencode-ai opencode --help ```</Tab>
</Tabs>
</Step>
@ -181,10 +144,12 @@ Use Slack, Discord, webhook, or another network notifier for alerts. Desktop not
<Accordions>
<Accordion title="`ao` is not found after install">
Your global package bin directory is not on `PATH`. Check your package manager's global bin path, then reopen your shell and run `ao --version` again.
Your global package bin directory is not on `PATH`. Check your package manager's global bin path, then reopen your
shell and run `ao --version` again.
</Accordion>
<Accordion title="`gh` is not authenticated">
Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is authenticated.
Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is
authenticated.
</Accordion>
<Accordion title="tmux is missing">
Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container.
@ -204,6 +169,10 @@ Use Slack, Discord, webhook, or another network notifier for alerts. Desktop not
<Cards>
<Card title="Quickstart" description="Run one worker session from start to pull request." href="/docs/quickstart" />
<Card title="Configuration" description="Understand the generated `agent-orchestrator.yaml`." href="/docs/configuration" />
<Card
title="Configuration"
description="Understand the generated `agent-orchestrator.yaml`."
href="/docs/configuration"
/>
<Card title="Platforms" description="Choose the right runtime and notifier for your OS." href="/docs/platforms" />
</Cards>

View File

@ -19,7 +19,7 @@ The platform differences come from the tools used to run and attach to long-live
## Recommended Setup
| Platform | Runtime | Notifications | Notes |
| --- | --- | --- | --- |
| ---------------------- | ------------------- | ---------------------------------- | -------------------------------------------------------------------------------- |
| macOS | `tmux` | `desktop`, Slack, Discord, webhook | Best local experience. iTerm2 attach support is macOS-only. |
| Linux | `tmux` | `desktop`, Slack, Discord, webhook | Best server and workstation setup. Desktop notifications need `notify-send`. |
| Windows | `process` | Slack, Discord, webhook | Native tmux and iTerm2 are unavailable. Windows support is in progress. |
@ -47,7 +47,7 @@ defaults:
What works well on macOS:
| Capability | Status |
| --- | --- |
| ----------------------------------------- | --------- |
| tmux-backed worker sessions | Supported |
| Browser dashboard terminal | Supported |
| iTerm2 attach/open helpers | Supported |
@ -55,7 +55,8 @@ What works well on macOS:
| macOS idle sleep prevention while AO runs | Supported |
<Callout type="info" title="Lid-close sleep still wins">
AO can prevent idle sleep on macOS while agents run, but it cannot override hardware lid-close sleep. Use normal clamshell mode if you need the machine available while closed.
AO can prevent idle sleep on macOS while agents run, but it cannot override hardware lid-close sleep. Use normal
clamshell mode if you need the machine available while closed.
</Callout>
## Linux
@ -80,7 +81,7 @@ defaults:
What to know:
| Capability | Status |
| --- | --- |
| --------------------------- | ----------------------------------------------------------- |
| tmux-backed worker sessions | Supported |
| Browser dashboard terminal | Supported |
| 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
<Callout type="warn" title="Windows support is in progress">
The native Windows path uses `runtime: process`. The core spawn and PR workflow is available, but tmux-specific workflows and iTerm2 helpers do not apply.
The native Windows path uses `runtime: process`. The core spawn and PR workflow is available, but tmux-specific
workflows and iTerm2 helpers do not apply.
</Callout>
Recommended config:
@ -107,7 +109,7 @@ defaults:
What works:
| Capability | Status |
| --- | --- |
| --------------------------------------------------------- | ------------------------------------------------------- |
| `ao start`, `ao stop`, `ao dashboard` | Supported |
| `ao spawn` and `ao spawn --prompt` | Supported through the process runtime |
| GitHub, GitLab, and Linear tracker/SCM integrations | Supported when their CLIs or credentials are configured |
@ -117,7 +119,7 @@ What works:
What is limited:
| Capability | Status | Use instead |
| --- | --- | --- |
| --------------------------- | ---------------------- | ---------------------------------------------- |
| `runtime: tmux` | Not available natively | `runtime: process` |
| iTerm2 terminal integration | Not available | Browser dashboard terminal |
| Desktop notifier | No-op on Windows | Slack, Discord, webhook, Composio, or OpenClaw |
@ -148,7 +150,7 @@ Operational notes:
## Choosing A Runtime
| Runtime | Best for | Tradeoff |
| --- | --- | --- |
| --------- | ------------------------------------------------------------------------- | --------------------------------------------------- |
| `tmux` | macOS/Linux machines where you want durable, attachable terminal sessions | Requires tmux and does not work natively on Windows |
| `process` | Windows, containers, and simpler process-managed environments | Less of the workflow is tmux-attachable |

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" }}>
<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>
[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
| Variable | Set by AO | Purpose |
|---|---|---|
| --------------- | --------- | -------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier |
| `PATH` | ✓ | Prepends `~/.ao/bin` |
@ -48,7 +50,8 @@ agent: aider
<Accordions>
<Accordion title="Aider hangs on file-edit confirmation">
Aider prompts before applying diffs. If you want fully autonomous runs, configure Aider's `--yes-always` via `~/.aider.conf.yml`. AO won't inject this for you — it's your call whether the agent should auto-apply.
Aider prompts before applying diffs. If you want fully autonomous runs, configure Aider's `--yes-always` via
`~/.aider.conf.yml`. AO won't inject this for you — it's your call whether the agent should auto-apply.
</Accordion>
<Accordion title="Dashboard cost shows $0">
AO doesn't parse Aider's cost output. The cost column stays empty.

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" }}>
<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>
[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
| Variable | Set by AO | Purpose |
|---|---|---|
| --------------- | ------------------------------ | --------------------------------------------------- |
| `AO_SESSION_ID` | ✓ | Unique AO session identifier |
| `AO_ISSUE_ID` | ✓ (when spawned from an issue) | Issue identifier |
| `CLAUDECODE` | ✓ | Signals to Claude that it's running under a harness |

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" }}>
<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>
[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
| Variable | Set by AO | Purpose |
|---|---|---|
| ---------------------------- | --------- | --------------------------------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier |
| `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" }}>
<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>
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
| Variable | Set by AO | Purpose |
|---|---|---|
| --------------- | --------- | -------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier |
| `PATH` | ✓ | Prepends `~/.ao/bin` |
@ -60,9 +62,11 @@ The plugin's `detect()` checks `agent --help` output for the strings `Cursor Age
<Accordions>
<Accordion title="AO says Cursor Agent isn't detected">
The plugin only recognises the real Cursor binary by its help output. Run `agent --help` — if you see a different tool's help, remove or shadow it, or reinstall Cursor Agent.
The plugin only recognises the real Cursor binary by its help output. Run `agent --help` — if you see a different
tool's help, remove or shadow it, or reinstall Cursor Agent.
</Accordion>
<Accordion title="Dashboard never leaves `active`">
Cursor doesn't emit its own session JSONL. AO classifies based on terminal output — if your terminal is being captured by another tool, the classification can drift. `ao doctor` will flag this.
Cursor doesn't emit its own session JSONL. AO classifies based on terminal output — if your terminal is being
captured by another tool, the classification can drift. `ao doctor` will flag this.
</Accordion>
</Accordions>

View File

@ -6,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.
<Callout type="info">
**PR metadata is captured automatically.** Non-Claude-Code agents (Codex, Cursor, Aider, OpenCode) use `~/.ao/bin/gh` and `~/.ao/bin/git` PATH wrappers that intercept commands like `gh pr create` and record the resulting PR number and branch so the dashboard stays in sync. Claude Code uses PostToolUse hooks in `.claude/settings.json` instead — the same metadata is written through Claude Code's native hook system. Both approaches are set up transparently by AO; you don't need to configure anything.
**PR metadata is captured automatically.** Non-Claude-Code agents (Codex, Cursor, Aider, OpenCode) use `~/.ao/bin/gh`
and `~/.ao/bin/git` PATH wrappers that intercept commands like `gh pr create` and record the resulting PR number and
branch so the dashboard stays in sync. Claude Code uses PostToolUse hooks in `.claude/settings.json` instead — the
same metadata is written through Claude Code's native hook system. Both approaches are set up transparently by AO; you
don't need to configure anything.
</Callout>
## Supported agents
| Agent | Slot name | Binary | Session resume |
|---|---|---|---|
| ----------------------------------------------- | ------------- | ---------- | ---------------------------- |
| [Claude Code](/docs/plugins/agents/claude-code) | `claude-code` | `claude` | ✅ `--resume` |
| [Codex](/docs/plugins/agents/codex) | `codex` | `codex` | ✅ `codex resume <threadId>` |
| [Cursor](/docs/plugins/agents/cursor) | `cursor` | `agent` | ❌ |
@ -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 |
<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="Cursor" logo="cursor" href="/docs/plugins/agents/cursor" description="Cursor Agent CLI." />
<PluginCard name="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI." />
<PluginCard name="OpenCode" logo="opencode" href="/docs/plugins/agents/opencode" description="OpenCode terminal agent." />
<PluginCard
name="OpenCode"
logo="opencode"
href="/docs/plugins/agents/opencode"
description="OpenCode terminal agent."
/>
</PluginGrid>
## Choosing

View File

@ -1,10 +1,4 @@
{
"title": "Agents",
"pages": [
"claude-code",
"codex",
"cursor",
"aider",
"opencode"
]
"pages": ["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" }}>
<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>
[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
| Variable | Set by AO | Purpose |
|---|---|---|
| --------------- | --------- | -------------------------- |
| `AO_SESSION_ID` | ✓ | AO session id |
| `AO_ISSUE_ID` | ✓ | Issue identifier |
| `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:
| Prompt | Example input |
|--------|--------------|
| ------------------- | ------------------------------------------------------------ |
| Plugin display name | `PagerDuty` |
| Plugin slot | `notifier` (selected from a list of all 7 slots) |
| Short description | `Route urgent alerts to PagerDuty` |
@ -227,7 +227,7 @@ All interfaces are defined in `packages/core/src/types.ts` and exported from `@a
### Runtime
| Method | Signature | Required |
|--------|-----------|---------|
| --------------- | ------------------------------------------------------------ | -------- |
| `create` | `(config: RuntimeCreateConfig) => Promise<RuntimeHandle>` | yes |
| `destroy` | `(handle: RuntimeHandle) => Promise<void>` | yes |
| `sendMessage` | `(handle: RuntimeHandle, message: string) => Promise<void>` | yes |
@ -243,7 +243,7 @@ Agent plugins have the richest interface. Methods are split into required and op
**Required:**
| Method | Purpose | Returns `null`? |
|--------|---------|----------------|
| ------------------ | ----------------------------------------------------------------- | -------------------- |
| `getLaunchCommand` | Shell command to start the agent | No |
| `getEnvironment` | Env vars for the process (must include `~/.ao/bin` in PATH) | No |
| `detectActivity` | Terminal-output activity classification (deprecated but required) | No |
@ -254,20 +254,22 @@ Agent plugins have the richest interface. Methods are split into required and op
**Optional:**
| Method | Purpose | When to skip |
|--------|---------|-------------|
| --------------------- | ---------------------------------------- | ------------------------------------- |
| `getRestoreCommand` | Resume a previous session | Agent has no resume capability |
| `setupWorkspaceHooks` | Install metadata hooks for PR tracking | Never — required for the dashboard |
| `postLaunchSetup` | Post-launch config | Only if no post-launch work is needed |
| `recordActivity` | Write terminal-derived activity to JSONL | Agent has native JSONL (Claude Code) |
<Callout type="warn">
`setupWorkspaceHooks` is marked optional in the TypeScript interface but is critical in practice. Without it, PRs created by your agent will never appear in the dashboard. See [Agent plugin specifics](#agent-plugin-specifics) for the two patterns (agent-native hooks vs PATH wrappers).
`setupWorkspaceHooks` is marked optional in the TypeScript interface but is critical in practice. Without it, PRs
created by your agent will never appear in the dashboard. See [Agent plugin specifics](#agent-plugin-specifics) for
the two patterns (agent-native hooks vs PATH wrappers).
</Callout>
### Workspace
| Method | Signature | Required |
|--------|-----------|---------|
| ------------ | ---------------------------------------------------------------------------------- | -------- |
| `create` | `(config: WorkspaceCreateConfig) => Promise<WorkspaceInfo>` | yes |
| `destroy` | `(workspacePath: string) => Promise<void>` | yes |
| `list` | `(projectId: string) => Promise<WorkspaceInfo[]>` | yes |
@ -278,7 +280,7 @@ Agent plugins have the richest interface. Methods are split into required and op
### Tracker
| Method | Signature | Required |
|--------|-----------|---------|
| ---------------- | ------------------------------------------------------------------------------------ | -------- |
| `getIssue` | `(identifier: string, project: ProjectConfig) => Promise<Issue>` | yes |
| `isCompleted` | `(identifier: string, project: ProjectConfig) => Promise<boolean>` | yes |
| `issueUrl` | `(identifier: string, project: ProjectConfig) => string` | yes |
@ -300,7 +302,7 @@ The richest interface — covers PR lifecycle, CI tracking, review tracking, and
### Notifier
| Method | Signature | Required |
|--------|-----------|---------|
| ------------------- | ----------------------------------------------------------------------- | -------- |
| `notify` | `(event: OrchestratorEvent) => Promise<void>` | yes |
| `notifyWithActions` | `(event: OrchestratorEvent, actions: NotifyAction[]) => Promise<void>` | optional |
| `post` | `(message: string, context?: NotifyContext) => Promise<string \| null>` | optional |
@ -308,7 +310,7 @@ The richest interface — covers PR lifecycle, CI tracking, review tracking, and
### Terminal
| Method | Signature | Required |
|--------|-----------|---------|
| --------------- | ---------------------------------------- | -------- |
| `openSession` | `(session: Session) => Promise<void>` | yes |
| `openAll` | `(sessions: Session[]) => Promise<void>` | yes |
| `isSessionOpen` | `(session: Session) => Promise<boolean>` | optional |
@ -421,7 +423,7 @@ projects:
## `ao plugin` subcommands
| Command | Description |
|---------|-------------|
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ao plugin list` | List plugins from the bundled marketplace catalog. Add `--installed` to show only what's in your config. Filter by slot with `--type <slot>`. Add `--refresh` to pull the latest catalog from the registry. |
| `ao plugin search <query>` | Search the bundled catalog by name, package, description, or slot. |
| `ao plugin create [dir]` | Scaffold a new plugin package interactively. |
@ -438,14 +440,14 @@ All utilities are exported from `@aoagents/ao-core`. The source lives in `packag
**Shell safety and HTTP:**
| Export | Purpose |
|--------|---------|
| ------------- | --------------------------------------------------------------------------------------- |
| `shellEscape` | Safely escape command-line arguments. Use for every argument passed to child processes. |
| `validateUrl` | Validate a webhook URL and throw a descriptive error on failure. |
**Activity detection (agent plugins):**
| Export | Purpose |
|--------|---------|
| -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `readLastJsonlEntry` | Efficiently read the last entry from an agent's native JSONL log. |
| `readLastActivityEntry` | Read the last entry from the AO activity JSONL (`{workspace}/.ao/activity.jsonl`). |
| `checkActivityLogState` | Extract `waiting_input` or `blocked` from an activity entry (with staleness cap). Returns `null` for other states. |
@ -457,7 +459,7 @@ All utilities are exported from `@aoagents/ao-core`. The source lives in `packag
**Workspace setup (agent plugins):**
| Export | Purpose |
|--------|---------|
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setupPathWrapperWorkspace` | Install `~/.ao/bin/gh` and `~/.ao/bin/git` PATH wrappers and write `.ao/AGENTS.md` in the workspace. Required for PATH-wrapper agents (Codex, Aider, OpenCode). |
| `buildAgentPath` | Prepend `~/.ao/bin` to a PATH string. |
| `normalizeAgentPermissionMode` | Normalize legacy permission mode aliases (e.g. `"skip"` → `"permissionless"`). |
@ -465,7 +467,7 @@ All utilities are exported from `@aoagents/ao-core`. The source lives in `packag
**Constants:**
| Export | Value |
|--------|-------|
| ----------------------------- | ------------------------------------------------------------------------------------------------ |
| `DEFAULT_READY_THRESHOLD_MS` | `300_000` (5 min) — ready → idle threshold |
| `DEFAULT_ACTIVE_WINDOW_MS` | `30_000` (30 s) — active → ready window |
| `ACTIVITY_INPUT_STALENESS_MS` | Deprecated compatibility export (`300_000`); waiting_input/blocked no longer expire by wallclock |
@ -525,7 +527,7 @@ For agent plugins, the required `getActivityState` tests are listed in the CLAUD
## Common pitfalls
| Pitfall | Correct approach |
|---------|-----------------|
| ------------------------------------ | ------------------------------------------------------------------ |
| Hardcoded secrets (API keys, tokens) | Read from `process.env`, throw if required and missing |
| Shell injection | Use `shellEscape()` for every argument passed to child processes |
| Reading large log files in full | Use `readLastJsonlEntry()` or stream the tail |
@ -539,7 +541,10 @@ For agent plugins, the required `getActivityState` tests are listed in the CLAUD
Agent plugins have significantly more surface area than other slot types. The most critical method is `getActivityState`, which powers the dashboard, stuck-detection, and the lifecycle manager's reaction engine.
<Callout type="warn">
**Step 4 of the `getActivityState` cascade (`getActivityFallbackState`) is mandatory.** If you skip it, `getActivityState` returns `null` whenever the native API is unavailable (binary not found, session lookup failed, timeout). The dashboard shows no activity state and stuck-detection stops working entirely. This was a real production bug in the OpenCode plugin.
**Step 4 of the `getActivityState` cascade (`getActivityFallbackState`) is mandatory.** If you skip it,
`getActivityState` returns `null` whenever the native API is unavailable (binary not found, session lookup failed,
timeout). The dashboard shows no activity state and stuck-detection stops working entirely. This was a real production
bug in the OpenCode plugin.
</Callout>
The required 4-step cascade:
@ -587,7 +592,15 @@ For agents that don't have a native session API (most new agents), skip step 3 a
## Next steps
<Cards>
<Card title="Architecture" description="Understand the plugin registry, session lifecycle, and how slots wire together." href="/docs/architecture" />
<Card
title="Architecture"
description="Understand the plugin registry, session lifecycle, and how slots wire together."
href="/docs/architecture"
/>
<Card title="Plugin catalog" description="Browse every bundled plugin grouped by slot." href="/docs/plugins" />
<Card title="Configuration reference" description="How plugins are referenced in agent-orchestrator.yaml." href="/docs/configuration" />
<Card
title="Configuration reference"
description="How plugins are referenced in agent-orchestrator.yaml."
href="/docs/configuration"
/>
</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
| Slot | Default | What it does |
|---|---|---|
| ------------- | ----------------------------------------- | -------------------------------------------- |
| **Agent** | `claude-code` | Which AI tool writes the code |
| **Runtime** | `tmux` (macOS/Linux), `process` (Windows) | Where the agent process runs |
| **Workspace** | `worktree` | Per-session code isolation |
@ -21,59 +21,169 @@ AO has **eight plugin slots**. Only one plugin per slot is active at a time, and
## Agents
<PluginGrid>
<PluginCard name="Claude Code" logo="claude-code" href="/docs/plugins/agents/claude-code" description="Anthropic's CLI coding agent. Session resume via --resume." />
<PluginCard name="Codex" logo="codex" href="/docs/plugins/agents/codex" description="OpenAI Codex CLI. Session resume via codex resume <threadId>." />
<PluginCard name="Cursor" logo="cursor" href="/docs/plugins/agents/cursor" description="Cursor Agent CLI. One-shot per session." />
<PluginCard name="Aider" logo="aider" href="/docs/plugins/agents/aider" description="Aider pair-programming CLI. No resume; PATH wrappers for PR tracking." />
<PluginCard name="OpenCode" logo="opencode" href="/docs/plugins/agents/opencode" description="OpenCode terminal agent. Session discovery + restore via the OpenCode session API." />
<PluginCard
name="Claude Code"
logo="claude-code"
href="/docs/plugins/agents/claude-code"
description="Anthropic's CLI coding agent. Session resume via --resume."
/>
<PluginCard
name="Codex"
logo="codex"
href="/docs/plugins/agents/codex"
description="OpenAI Codex CLI. Session resume via codex resume <threadId>."
/>
<PluginCard
name="Cursor"
logo="cursor"
href="/docs/plugins/agents/cursor"
description="Cursor Agent CLI. One-shot per session."
/>
<PluginCard
name="Aider"
logo="aider"
href="/docs/plugins/agents/aider"
description="Aider pair-programming CLI. No resume; PATH wrappers for PR tracking."
/>
<PluginCard
name="OpenCode"
logo="opencode"
href="/docs/plugins/agents/opencode"
description="OpenCode terminal agent. Session discovery + restore via the OpenCode session API."
/>
</PluginGrid>
## Runtimes
<PluginGrid>
<PluginCard name="tmux" logo="tmux" href="/docs/plugins/runtimes/tmux" description="Default on macOS/Linux. Each agent gets its own tmux window you can attach to." />
<PluginCard name="process" logo="windows" href="/docs/plugins/runtimes/process" description="Cross-platform child-process runtime. Required on Windows." />
<PluginCard
name="tmux"
logo="tmux"
href="/docs/plugins/runtimes/tmux"
description="Default on macOS/Linux. Each agent gets its own tmux window you can attach to."
/>
<PluginCard
name="process"
logo="windows"
href="/docs/plugins/runtimes/process"
description="Cross-platform child-process runtime. Required on Windows."
/>
</PluginGrid>
## Workspaces
<PluginGrid>
<PluginCard name="worktree" logo="github" href="/docs/plugins/workspaces/worktree" description="git worktree per session. Fast, shares the object DB with your main checkout." />
<PluginCard name="clone" logo="github" href="/docs/plugins/workspaces/clone" description="Full clone per session. Use when your tooling doesn't play well with shared .git." />
<PluginCard
name="worktree"
logo="github"
href="/docs/plugins/workspaces/worktree"
description="git worktree per session. Fast, shares the object DB with your main checkout."
/>
<PluginCard
name="clone"
logo="github"
href="/docs/plugins/workspaces/clone"
description="Full clone per session. Use when your tooling doesn't play well with shared .git."
/>
</PluginGrid>
## Trackers
<PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/trackers/github" description="Issues + PRs via the gh CLI." />
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/trackers/gitlab" description="Issues + MRs via the glab CLI. Self-hosted supported." />
<PluginCard name="Linear" logo="linear" href="/docs/plugins/trackers/linear" description="Linear issues. Direct API or Composio-mediated." />
<PluginCard
name="GitHub"
logo="github"
href="/docs/plugins/trackers/github"
description="Issues + PRs via the gh CLI."
/>
<PluginCard
name="GitLab"
logo="gitlab"
href="/docs/plugins/trackers/gitlab"
description="Issues + MRs via the glab CLI. Self-hosted supported."
/>
<PluginCard
name="Linear"
logo="linear"
href="/docs/plugins/trackers/linear"
description="Linear issues. Direct API or Composio-mediated."
/>
</PluginGrid>
## SCM
<PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/scm/github" description="PRs, reviews, and CI status via gh." />
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/scm/gitlab" description="MRs, discussions, pipelines via glab." />
<PluginCard
name="GitHub"
logo="github"
href="/docs/plugins/scm/github"
description="PRs, reviews, and CI status via gh."
/>
<PluginCard
name="GitLab"
logo="gitlab"
href="/docs/plugins/scm/gitlab"
description="MRs, discussions, pipelines via glab."
/>
</PluginGrid>
## Notifiers
<PluginGrid>
<PluginCard name="Dashboard" logo="web" href="/docs/plugins/notifiers/dashboard" description="Retained notifications inside the AO dashboard." />
<PluginCard name="Desktop" logo="apple" href="/docs/plugins/notifiers/desktop" description="Native macOS/Linux notifications. No-op on Windows." />
<PluginCard name="Discord" logo="discord" href="/docs/plugins/notifiers/discord" description="Webhook-based Discord messages with rich embeds." />
<PluginCard
name="Dashboard"
logo="web"
href="/docs/plugins/notifiers/dashboard"
description="Retained notifications inside the AO dashboard."
/>
<PluginCard
name="Desktop"
logo="apple"
href="/docs/plugins/notifiers/desktop"
description="Native macOS/Linux notifications. No-op on Windows."
/>
<PluginCard
name="Discord"
logo="discord"
href="/docs/plugins/notifiers/discord"
description="Webhook-based Discord messages with rich embeds."
/>
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhooks." />
<PluginCard name="Webhook" logo="webhook" href="/docs/plugins/notifiers/webhook" description="Generic HTTP POST with retries and exponential backoff." />
<PluginCard name="Composio" logo="composio" href="/docs/plugins/notifiers/composio" description="Route through the Composio toolkit — Slack, Discord, or Gmail." />
<PluginCard name="OpenClaw" logo="openclaw" href="/docs/plugins/notifiers/openclaw" description="Local OpenClaw gateway for personal notifications." />
<PluginCard
name="Webhook"
logo="webhook"
href="/docs/plugins/notifiers/webhook"
description="Generic HTTP POST with retries and exponential backoff."
/>
<PluginCard
name="Composio"
logo="composio"
href="/docs/plugins/notifiers/composio"
description="Route through the Composio toolkit — Slack, Discord, or Gmail."
/>
<PluginCard
name="OpenClaw"
logo="openclaw"
href="/docs/plugins/notifiers/openclaw"
description="Local OpenClaw gateway for personal notifications."
/>
</PluginGrid>
## Terminals
<PluginGrid>
<PluginCard name="iTerm2" logo="iterm2" href="/docs/plugins/terminals/iterm2" description="Open attached tabs in iTerm2 via AppleScript (macOS only)." />
<PluginCard name="Web" logo="web" href="/docs/plugins/terminals/web" description="Dashboard xterm.js session URL. Cross-platform." />
<PluginCard
name="iTerm2"
logo="iterm2"
href="/docs/plugins/terminals/iterm2"
description="Open attached tabs in iTerm2 via AppleScript (macOS only)."
/>
<PluginCard
name="Web"
logo="web"
href="/docs/plugins/terminals/web"
description="Dashboard xterm.js session URL. Cross-platform."
/>
</PluginGrid>
## Writing your own

View File

@ -1,14 +1,5 @@
{
"title": "Plugins",
"defaultOpen": false,
"pages": [
"agents",
"runtimes",
"workspaces",
"trackers",
"scm",
"notifiers",
"terminals",
"authoring"
]
"pages": ["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" }}>
<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>
<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
@ -37,7 +44,7 @@ notificationRouting:
```
| Config key | Default | What it does |
|---|---|---|
| -------------- | ---------------------- | ------------------------------------------------------------ |
| `backend` | `auto` | `ao-app`, `terminal-notifier`, `osascript`, or auto fallback |
| `dashboardUrl` | dashboard port | URL opened from desktop notification actions |
| `appPath` | macOS app install path | Custom AO Notifier.app path |

View File

@ -6,13 +6,43 @@ description: Who gets pinged when an agent needs you. Seven notifiers ship; pick
Notifiers deliver AO events — session stuck, PR opened, review requested, CI failed — to wherever you actually pay attention.
<PluginGrid>
<PluginCard name="Dashboard" logo="web" href="/docs/plugins/notifiers/dashboard" description="Retained notifications inside the AO dashboard." />
<PluginCard name="Desktop" logo="apple" href="/docs/plugins/notifiers/desktop" description="Native macOS/Linux notifications. No-op on Windows." />
<PluginCard name="Discord" logo="discord" href="/docs/plugins/notifiers/discord" description="Discord webhook with rich embeds." />
<PluginCard
name="Dashboard"
logo="web"
href="/docs/plugins/notifiers/dashboard"
description="Retained notifications inside the AO dashboard."
/>
<PluginCard
name="Desktop"
logo="apple"
href="/docs/plugins/notifiers/desktop"
description="Native macOS/Linux notifications. No-op on Windows."
/>
<PluginCard
name="Discord"
logo="discord"
href="/docs/plugins/notifiers/discord"
description="Discord webhook with rich embeds."
/>
<PluginCard name="Slack" logo="slack" href="/docs/plugins/notifiers/slack" description="Slack incoming webhook." />
<PluginCard name="Webhook" logo="webhook" href="/docs/plugins/notifiers/webhook" description="Generic HTTP POST. Retries + exponential backoff." />
<PluginCard name="Composio" logo="composio" href="/docs/plugins/notifiers/composio" description="Route through Composio — Slack, Discord, or Gmail." />
<PluginCard name="OpenClaw" logo="openclaw" href="/docs/plugins/notifiers/openclaw" description="Local OpenClaw gateway for personal alerts." />
<PluginCard
name="Webhook"
logo="webhook"
href="/docs/plugins/notifiers/webhook"
description="Generic HTTP POST. Retries + exponential backoff."
/>
<PluginCard
name="Composio"
logo="composio"
href="/docs/plugins/notifiers/composio"
description="Route through Composio — Slack, Discord, or Gmail."
/>
<PluginCard
name="OpenClaw"
logo="openclaw"
href="/docs/plugins/notifiers/openclaw"
description="Local OpenClaw gateway for personal alerts."
/>
</PluginGrid>
## Stacking notifiers
@ -57,7 +87,7 @@ a real test message.
## What gets notified
| Event | When |
|---|---|
| ------------------- | -------------------------------------- |
| `session.spawned` | `ao spawn` succeeds |
| `session.working` | Agent is actively editing code |
| `pr.opened` | Agent pushed a branch and opened a PR |

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" }}>
<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>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -70,7 +72,7 @@ The OpenClaw config keeps the actual secret:
## Config
| Key | Default | What it does |
|---|---|---|
| -------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `url` | `http://127.0.0.1:18789/hooks/agent` | OpenClaw hook endpoint |
| `openclawConfigPath` | `~/.openclaw/openclaw.json` | Local OpenClaw config path that contains `hooks.token` |
| `token` | — | Remote/manual fallback. Avoid for local setups because it stores the secret in AO config |

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:
| Plugin | Best for | Binary needed |
|---|---|---|
| ----------------------------------------- | ---------------------------------------------------- | ------------- |
| [tmux](/docs/plugins/runtimes/tmux) | macOS / Linux default. You can attach interactively. | `tmux` |
| [process](/docs/plugins/runtimes/process) | Windows, Docker, CI-like environments | — |
<PluginGrid>
<PluginCard name="tmux" logo="tmux" href="/docs/plugins/runtimes/tmux" description="Each agent gets its own tmux window; attach with `ao session attach`." />
<PluginCard name="process" logo="windows" href="/docs/plugins/runtimes/process" description="Cross-platform child process. Required on Windows." />
<PluginCard
name="tmux"
logo="tmux"
href="/docs/plugins/runtimes/tmux"
description="Each agent gets its own tmux window; attach with `ao session attach`."
/>
<PluginCard
name="process"
logo="windows"
href="/docs/plugins/runtimes/process"
description="Cross-platform child process. Required on Windows."
/>
</PluginGrid>
## Choosing
@ -20,4 +30,4 @@ The runtime is where your agent's terminal actually lives. Two options ship:
- If you can install `tmux` and you want to occasionally drop into a live session, use `runtime: tmux`.
- If you're on Windows, in a container, or just want fewer moving parts, use `runtime: process`.
Both runtimes expose the agent's terminal to the dashboard's xterm.js session — attaching via tmux is a *bonus*, not a requirement.
Both runtimes expose the agent's terminal to the dashboard's xterm.js session — attaching via tmux is a _bonus_, not a requirement.

View File

@ -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" }}>
<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>
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" }}>
<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>
AO uses [tmux](https://github.com/tmux/tmux) as the default runtime on macOS and Linux. Each session lives in its own tmux window under a single AO-managed session, and you can attach to any of them interactively.
<PlatformSupport macos="full" linux="full" windows="none" note={<>Windows has no tmux. Use <a href="/docs/plugins/runtimes/process">process</a> instead.</>} />
<PlatformSupport
macos="full"
linux="full"
windows="none"
note={
<>
Windows has no tmux. Use <a href="/docs/plugins/runtimes/process">process</a> instead.
</>
}
/>
## Install

View File

@ -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" }}>
<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>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -59,7 +61,7 @@ projects:
Full `webhook.*` sub-object:
| Field | Default | Description |
|---|---|---|
| ----------------- | --------------------- | ------------------------------------------- |
| `enabled` | `true` | Enable or disable webhook processing |
| `path` | `/api/webhooks` | Override the receive path |
| `secretEnvVar` | — | Name of the env var holding the HMAC secret |

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" }}>
<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>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -25,7 +27,7 @@ scmConfig:
## Mapping
| Concept | GitHub term | GitLab term |
|---|---|---|
| -------------- | ------------ | ----------------- |
| Review request | Pull request | Merge request |
| Review | Review | Discussion / note |
| CI status | Check runs | Pipelines / jobs |
@ -65,7 +67,7 @@ projects:
Full `webhook.*` sub-object:
| Field | Default | Description |
|---|---|---|
| ----------------- | --------------------- | ---------------------------------------- |
| `enabled` | `true` | Enable or disable webhook processing |
| `path` | `/api/webhooks` | Override the receive path |
| `secretEnvVar` | — | Name of the env var holding the token |
@ -85,7 +87,7 @@ AO ignores review comments from known bot accounts so they don't block the merge
**Hardcoded bots:**
| Username |
|---|
| ------------------ |
| `gitlab-bot` |
| `ghost` |
| `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.
<PluginGrid>
<PluginCard name="GitHub" logo="github" href="/docs/plugins/scm/github" description="PRs, reviews, and CI status via gh." />
<PluginCard name="GitLab" logo="gitlab" href="/docs/plugins/scm/gitlab" description="MRs, discussions, pipelines via glab." />
<PluginCard
name="GitHub"
logo="github"
href="/docs/plugins/scm/github"
description="PRs, reviews, and CI status via gh."
/>
<PluginCard
name="GitLab"
logo="gitlab"
href="/docs/plugins/scm/gitlab"
description="MRs, discussions, pipelines via glab."
/>
</PluginGrid>
## Why separate from the tracker

View File

@ -6,8 +6,18 @@ description: How you attach to a running agent. iTerm2 on macOS, or the dashboar
The **terminal** plugin is what `ao open` and `ao session attach` use. If you only ever look at the dashboard, you don't need to think about it.
<PluginGrid>
<PluginCard name="iTerm2" logo="iterm2" href="/docs/plugins/terminals/iterm2" description="Open attached tabs in iTerm2 via AppleScript (macOS only)." />
<PluginCard name="Web" logo="web" href="/docs/plugins/terminals/web" description="Dashboard xterm.js session URL. Cross-platform." />
<PluginCard
name="iTerm2"
logo="iterm2"
href="/docs/plugins/terminals/iterm2"
description="Open attached tabs in iTerm2 via AppleScript (macOS only)."
/>
<PluginCard
name="Web"
logo="web"
href="/docs/plugins/terminals/web"
description="Dashboard xterm.js session URL. Cross-platform."
/>
</PluginGrid>
## Default

View File

@ -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" }}>
<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>
<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" }}>
<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>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -21,7 +23,7 @@ terminalConfig:
```
| Config key | Default | What it does |
|---|---|---|
| -------------- | ----------------------- | ---------------------------------------------------------------------------------- |
| `dashboardUrl` | `http://localhost:3000` | Base URL for the dashboard. Override when the dashboard runs on another host/port. |
## How it works

View File

@ -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" }}>
<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>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -32,7 +34,7 @@ projects:
## How it's used
| Operation | `gh` command invoked |
|---|---|
| ---------------- | ------------------------------------------- |
| Fetch issue | `gh issue view <num> --json title,body,...` |
| Create issue | `gh issue create` |
| Comment on issue | `gh issue comment` |

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" }}>
<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>
<PlatformSupport macos="full" linux="full" windows="full" />
@ -28,11 +30,12 @@ projects:
```
| Config key | Default | What it does |
|---|---|---|
| ---------- | ------------ | ---------------------------------------------------- |
| `host` | `gitlab.com` | GitLab hostname — override for self-hosted instances |
<Callout type="warn">
`host` belongs on the **`tracker`** block (plugin-level config), not inside `trackerConfig`. `trackerConfig` is for per-project passthrough fields (labels, assignee, milestone). Putting `host` inside `trackerConfig` will not be read.
`host` belongs on the **`tracker`** block (plugin-level config), not inside `trackerConfig`. `trackerConfig` is for
per-project passthrough fields (labels, assignee, milestone). Putting `host` inside `trackerConfig` will not be read.
</Callout>
## Self-hosted

View File

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

View File

@ -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" }}>
<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>
<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
| Key | Required | What it does |
|---|---|---|
| --------------- | --------------------- | ---------------------------------------------------------- |
| `teamId` | ✓ (for `createIssue`) | Linear team the issue lives in |
| `workspaceSlug` | optional | Used to render `https://linear.app/{slug}/issue/{id}` URLs |

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" }}>
<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>
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 |
|---|---|---|
| ---------- | -------------- | ----------------------------- |
| `cloneDir` | `~/.ao-clones` | Base directory for all clones |
## 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:
| Plugin | Isolation | Disk usage | Speed |
|---|---|---|---|
| --------------------------------------------- | --------------------------------- | ---------------------- | ---------------------- |
| [worktree](/docs/plugins/workspaces/worktree) | Per-session branch, shared `.git` | Low (shared object DB) | Fast (no clone) |
| [clone](/docs/plugins/workspaces/clone) | Per-session full clone | High | Slower (initial clone) |
<PluginGrid>
<PluginCard 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." />
<PluginCard
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>
## 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" }}>
<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>
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 |
|---|---|---|
| ------------- | -------------- | -------------------------------- |
| `worktreeDir` | `~/.worktrees` | Base directory for all worktrees |
## How it works
@ -45,7 +47,7 @@ projects:
```
| Knob | Purpose |
|---|---|
| ------------ | ------------------------------------------------------------------------------------------------- |
| `symlinks` | Files/dirs to symlink from the source repo into each worktree (e.g. `.env.local`, a shared cache) |
| `postCreate` | Shell commands to run in each new worktree after creation (e.g. `pnpm install`) |

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.
<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>
## Pick A Safe First Task
@ -55,14 +56,16 @@ Open a second terminal in the same repository.
ao spawn 42
```
Replace `42` with the issue number. AO fetches the issue through `gh`, creates a worktree, starts the configured worker agent, and gives it the issue context.
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 value="Freeform prompt">
```bash
ao spawn --prompt "Update the README install section to mention Node 20"
```
Use this when the task is not tracked in GitHub, GitLab, or Linear yet. Keep the prompt specific and reviewable.
Use this when the task is not tracked in GitHub, GitLab, or Linear yet. Keep the prompt specific and reviewable.
</Tab>
</Tabs>
@ -127,7 +130,7 @@ ao session cleanup
## What AO Created
| Item | What it means |
| --- | --- |
| ------------------------- | ------------------------------------------------------------------------------------------------ |
| `agent-orchestrator.yaml` | Project config: plugins, projects, reactions, runtime, and notifier choices. |
| Orchestrator session | A coordinating session started by `ao start`; it supervises worker sessions. |
| Worker session | The agent process that works on one issue or prompt. |
@ -137,7 +140,7 @@ ao session cleanup
## If Something Looks Wrong
| Symptom | First check |
| --- | --- |
| --------------------------------------- | ----------------------------------------------------------------------------- |
| Dashboard is not updating | Make sure the `ao start` terminal is still running. |
| `ao spawn` warns that AO is not running | Start AO with `ao start` before spawning. |
| GitHub issue or PR data is missing | Run `gh auth status` and check the `repo` field in `agent-orchestrator.yaml`. |
@ -147,8 +150,20 @@ ao session cleanup
## Next
<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="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" />
<Card
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>

View File

@ -59,13 +59,17 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil
<Accordions>
<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 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 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 title="Webhook delivers but nothing happens">
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.
</Accordion>
<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>
</Accordions>
@ -101,7 +106,7 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil
- Check the [FAQ](/docs/faq).
- Open an issue: [`ComposioHQ/agent-orchestrator`](https://github.com/ComposioHQ/agent-orchestrator/issues).
When opening an issue, include:
When opening an issue, include:
- Output of `ao --version`
- The contents of `~/.agent-orchestrator/{hash}-observability/processes/` (one JSON snapshot per process; each contains traces, health, and metrics)

View File

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

View File

@ -103,7 +103,9 @@ a {
.landing-card {
background: var(--landing-card-bg);
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 {
@ -155,7 +157,9 @@ a {
.landing-reveal {
opacity: 0;
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 {
@ -413,110 +417,288 @@ a {
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. */
.landing-page .mt-1 { margin-top: calc(var(--spacing) * 1); }
.landing-page .mt-6 { margin-top: calc(var(--spacing) * 6); }
.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 .mt-1 {
margin-top: calc(var(--spacing) * 1);
}
.landing-page .mt-6 {
margin-top: calc(var(--spacing) * 6);
}
.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-2 { margin-bottom: calc(var(--spacing) * 2); }
.landing-page .mb-3 { margin-bottom: calc(var(--spacing) * 3); }
.landing-page .mb-4 { 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 .mb-1 {
margin-bottom: calc(var(--spacing) * 1);
}
.landing-page .mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.landing-page .mb-3 {
margin-bottom: calc(var(--spacing) * 3);
}
.landing-page .mb-4 {
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-2 { margin-left: calc(var(--spacing) * 2); }
.landing-page .ml-1\.5 { margin-left: calc(var(--spacing) * 1.5); }
.landing-page .ml-1 {
margin-left: calc(var(--spacing) * 1);
}
.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\.5 { margin-right: calc(var(--spacing) * 1.5); }
.landing-page .mr-1 {
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-3 { padding: calc(var(--spacing) * 3); }
.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 .p-2 {
padding: calc(var(--spacing) * 2);
}
.landing-page .p-3 {
padding: calc(var(--spacing) * 3);
}
.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-32 { padding-top: calc(var(--spacing) * 32); }
.landing-page .pt-10 {
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-3 { padding-left: calc(var(--spacing) * 3); 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 .px-2 {
padding-left: calc(var(--spacing) * 2);
padding-right: calc(var(--spacing) * 2);
}
.landing-page .px-3 {
padding-left: calc(var(--spacing) * 3);
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-2 { padding-top: calc(var(--spacing) * 2); padding-bottom: calc(var(--spacing) * 2); }
.landing-page .py-3 { padding-top: calc(var(--spacing) * 3); 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); }
.landing-page .py-1 {
padding-top: calc(var(--spacing) * 1);
padding-bottom: calc(var(--spacing) * 1);
}
.landing-page .py-2 {
padding-top: calc(var(--spacing) * 2);
padding-bottom: calc(var(--spacing) * 2);
}
.landing-page .py-3 {
padding-top: calc(var(--spacing) * 3);
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 */
.landing-page .py-\[100px\] { padding-top: 100px; padding-bottom: 100px; }
.landing-page .py-\[120px\] { padding-top: 120px; padding-bottom: 120px; }
.landing-page .pt-\[60px\] { padding-top: 60px; }
.landing-page .pb-\[120px\] { padding-bottom: 120px; }
.landing-page .py-\[100px\] {
padding-top: 100px;
padding-bottom: 100px;
}
.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
navigation. In light mode --color-fd-border = #d6d3d1 which looks bright on the dark
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-default\)\] { border-color: var(--landing-border-default); }
.landing-page .border-\[var\(--landing-border-strong\)\] { border-color: var(--landing-border-strong); }
.landing-page .border-\[var\(--landing-border-subtle\)\] {
border-color: var(--landing-border-subtle);
}
.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 } */
.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 } */
.landing-page .text-\[clamp\(1\.75rem\,4vw\,2\.75rem\)\] { font-size: clamp(1.75rem, 4vw, 2.75rem); }
.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); }
.landing-page .text-\[clamp\(1\.75rem\,4vw\,2\.75rem\)\] {
font-size: clamp(1.75rem, 4vw, 2.75rem);
}
.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,
so its higher-priority layer keeps elements hidden / wrong grid at desktop widths. */
@media (min-width: 768px) {
.landing-page .md\:flex { display: flex; }
.landing-page .md\:block { display: block; }
.landing-page .md\:hidden { display: none; }
.landing-page .md\:flex {
display: flex;
}
.landing-page .md\:block {
display: block;
}
.landing-page .md\:hidden {
display: none;
}
.landing-page .md\:flex-row { flex-direction: row; }
.landing-page .md\:flex-row {
flex-direction: row;
}
.landing-page .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.landing-page .md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.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\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.landing-page .md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.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\: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\: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\: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; }
.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
| 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`) |
| **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
**The Go suite (fastest, cross-platform):**
```bash
cd backend
go test -tags e2e ./internal/cli/... # run it
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`).
`-v` logs each `ao` invocation and its full output, which is the audit trail you
get for free from `go test`.
**Fresh-machine install, in a clean container:**
```bash
docker build -f test/cli/Dockerfile -t ao-cli-smoke .
docker run --rm --init ao-cli-smoke
```
> `--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.