# Orchestrator Autopilot

Deterministic orchestrator framework: keeps a subagent worker fleet at capacity,
routes queue items through a review lifecycle, and ships its own agents —
**one implementation, multiple hosts** (pi + opencode).

The framework is host-agnostic software: a programmatic queue store, a
deterministic lifecycle (ticks, completion attribution, verdict routing), and
thin per-host adapters. Hosts only wire events, gate state, and deliver ticks —
the machinery is shared.

## Getting started

### pi (official package)

```bash
pi install npm:orchestrator-autopilot        # or git:github.com/MAnders333/orchestrator-autopilot
```

Pi loads ONLY the pi adapter (`src/hosts/pi-extension.ts`) plus the shared
skills — the manifest in `package.json` scopes this; opencode code is inert in
a pi install. Prerequisite: the pi-subagents extension must be installed (the
backend spawns workers through its RPC).

Then, in a session: `/autopilot on` — the activation command (ships with the
extension). It enables the tick loop and injects the operating program — the
package's own `prompts/orchestrate.md`, read from the package and sent
verbatim, with no slash indirection and no prompt template. The same procedure
is available on demand as `/orchestrate`, a command the extension REGISTERS
(`pi.registerCommand`); it can be run standalone without the harness.

**Command precedence (measured, not inferred)**: on pi the EXTENSION command
wins over a same-named file in `$PI_CODING_AGENT_DIR/prompts/` — a locally
projected `/orchestrate` (e.g. from your dotfiles) is listed but never runs, so
edits to it are silently dead. On opencode it is the reverse: the command FILE
wins and must be deleted, not merely ignored. Either way the plugin emits one
warning per session naming every competing path and which one actually runs.
See `docs/AUTOPILOT-32-orchestrate-ownership.md`.

### opencode (npm plugin)

Wire the SERVER plugin into `opencode.jsonc` and the decision-panel TUI plugin
into `tui.json`:

```jsonc
// opencode.jsonc
{ "plugin": ["orchestrator-autopilot"] }
```

```jsonc
// tui.json (the decision panel — optional)
{ "plugin": ["orchestrator-autopilot"] }
```

The package's default export IS the opencode plugin (`src/hosts/opencode-entry.ts`
→ `OrchestratorAutopilot`); pi code is not reachable from that entry. It
registers the queue tools (`queue_add`/`queue_list`/`queue_update`/
`queue_dispatch`/`queue_review`/`queue_steer`), `autopilot` (the
on/off/status/capacity toggle, same semantics as pi's `/autopilot`),
`flag_for_review`, the completion wiring (detached worker process-exit →
queue flips + verdict routing), and the auto-dispatch/review/re-dispatch
harness. The `exports["./tui"]` entry registers the TUI decision panel
(proposals / human-review views over the SAME queue store). `/orchestrate`
is registered by the TUI plugin itself (`slashName: "orchestrate"`), not
projected into your opencode command config — and on opencode a same-named
command FILE shadows it, so a leftover projection must be deleted, not
merely ignored. See `docs/AUTOPILOT-32-orchestrate-ownership.md`.

**Runtime prerequisites** (all opencode-side — unlike the pi host, the
opencode plugin does NOT spawn through pi-subagents):

1. **The subagent backend runtime = opencode itself.** The plugin's backend
   (`src/backends/opencode.ts`) spawns detached headless workers/reviewers as
   `opencode run --agent worker|orchestrator-reviewer` child processes
   (completion = process exit). Any opencode binary works; pin a launcher
   that injects secrets/route via `AUTOPILOT_OPENCODE_BIN` (default
   `opencode`) — e.g. an `oc` wrapper. Framework-owned agents are projected
   into opencode's global agents dir at plugin load (idempotent,
   version-stamped).
2. **`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=1`** for detached worker
   runs. The backend also forces it on every spawned child.

**State dir — ONE store per mode layer.** The server plugin and the TUI panel
resolve the queue store through the framework's single `resolveStateDir`
(`src/config.ts`): `AUTOPILOT_STATE_DIR` → the config dir's
`autopilot.host.json` `stateDir` fact → a profile-scoped default. Because
both surfaces read the same config dir, the queue tools and the panel
read/write the same `queue.json` (the TUI's approve/refine keys are the
server tools' queue_update semantics).

### Local development (both hosts)

1. Clone + install: `bun install` (Bun required; the test suite is hermetic).
2. Wire the host you use:
   - **pi**: add `src/hosts/pi-extension.ts` to your settings `extensions` array.
   - **opencode**: add `src/hosts/opencode-plugin.ts` to your `opencode.jsonc`
     `plugin` array (requires the runtime prereqs above — the detached `oc`
     binary + `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=1`).
3. Run `/orchestrate` in your orchestrator session. **The package owns that
   command** — it is registered by the plugin in both hosts (pi:
   `pi.registerCommand`; opencode: a TUI keymap-layer command with
   `slashName: "orchestrate"`) and injects `prompts/orchestrate.md` from the
   package. There is no per-host copy to project, sync or drift-check. If a
   stale `orchestrate.md` still sits in a host command dir, the plugin emits
   one warning naming every competing definition and which one wins.
   `docs/queue-model.md` describes what the framework enforces vs. what the
   agent decides. Your intake sources are FACTS, not procedure: they live in
   `autopilot.config.json` under `workspace.intake`.
4. The package installs its own agents (reviewer + worker) into the host's
   agent dirs at activation — version-stamped, idempotent.
5. Point the orchestrator at the queue store and add the
   `skills/orchestrator-operations/` skill to your agent's skills config for
   the operating rules. The state dir resolves as `AUTOPILOT_STATE_DIR` →
   `<host config dir>/autopilot.host.json` → `~/.local/state/orchestrator/<profile>`.
   The host config dir is the RUNNING host's own (`$PI_CODING_AGENT_DIR` for
   pi; `$OPENCODE_CONFIG_DIR`, else `$XDG_CONFIG_HOME/opencode` →
   `~/.config/opencode`, for both opencode entry points), and `<profile>` is
   that dir's basename — so an opencode session can never fall back into the
   pi profile's store. Declare `stateDir` there: the fallback is a guess, and
   the probe says so.

## Architecture

```
src/
├── core.ts               ← Autopilot lifecycle: ticks (dispatch|intake|review),
│                            completion attribution, verdict routing, review cap
├── queue-store.ts        ← programmatic queue (queue.json), validated transitions
├── backends/             ← runtime executor adapters (the portability seam)
│   ├── types.ts          ← SubagentBackend contract
│   ├── pi.ts             ← pi-subagents RPC spawn + file control channel
│   └── opencode.ts       ← detached `oc run` children (completion = process exit)
├── framework/
│   ├── queue-ops.ts      ← the SIX queue tools, host-agnostic (single impl)
│   ├── runner.ts         ← shared tick machinery: trigger routing (completion/
│   │                        settled/timer/activation) + gate + cooldown — the
│   │                        hosts only WIRE their events to it
│   └── tick-router.ts    ← the delivery gate (interactive/loaded/busy/
│                            compacting + cooldown + message format)
├── agents/               ← framework-owned agents (canonical prompt + installer)
│   ├── install.ts        ← agent registry + per-backend projection, version-stamped
│   ├── reviewer/         ← orchestrator-reviewer (Verdict: PASS/FAIL/NO-CHANGE gate, read-only)
│   └── worker/           ← worker (worktree isolation, commit-early, full tools)
└── hosts/                ← one file per host (logic + tool adapter together)
    ├── pi-extension.ts   ← pi extension: tools + ticks + lifecycle events
    ├── opencode-entry.ts ← the npm-package default export (exports["."] → the
    │                        opencode plugin opencode loads)
    ├── opencode-plugin.ts← opencode host logic (completion, sweep) + the
    │                        tool()/event() adapter + tick delivery
    └── opencode-tui.ts   ← the decision panel (exports["./tui"]; tui.json)
```

## How the hosts wire it

- **pi**: `settings.json` `extensions` → `src/hosts/pi-extension.ts`
  (activates: installs the reviewer, registers the queue tools + `/autopilot`,
  subscribes to `subagent:async-complete`, ticks the orchestrator session).
  `/autopilot off in <duration>` (e.g. `off in 1h30m`, max 24h) schedules a
  shutdown: autopilot stays ON until the deadline, then flips OFF by itself;
  explicit on/off cancels, status shows the pending deadline, and a restart
  cannot lose it (the enable-gate backstop fires it late). Same semantics via
  the opencode autopilot tool (`action=off`, value `in <duration>`).
- **opencode**: `opencode.jsonc` `plugin` → the npm spec
  `orchestrator-autopilot` (or `src/hosts/opencode-plugin.ts` in local dev).
  The default-export adapter (src/hosts/opencode-entry.ts) registers the six
  queue tools + `autopilot` + `flag_for_review`; completion = backend
  process-exit → `handleAsyncComplete` → queue flips + verdict routing. Its
  `dispose` hook stops the runner/scheduled-off timers. The decision panel
  loads separately via `tui.json` (`exports["./tui"]` →
  `src/hosts/opencode-tui.ts`).
- **Agents** install into the host's own agent dirs at activation — the pi
  reviewer resolves from the runtime (`PI_CODING_AGENT_DIR` / `~/.pi/agent/
  agents`), opencode's from its own agents dir. The framework never invents a
  shared path; each host owns where its agents live. Idempotent,
  version-stamped, never hand-edited (framework-managed between markers).

The **abstraction seam**: the hosts are WIRING ONLY — event sources, gate
semantics, and delivery. All logic is host-agnostic in `src/`:
`framework/runner.ts` (trigger routing + the shared deferral + the harness
queue), `framework/scheduled-off.ts` (the scheduled shutdown: duration parser,
due-check backstop, injected-clock/timer ScheduleManager), `framework/core.ts`
(the engine: flips, verdicts, ticks), `framework/
auto-dispatch.ts` (A/B/C automations + the B26 worktree rule),
`framework/flag-review.ts` (the handover + the deterministic PASS auto-flag),
`tools/queue-ops.ts` (the six queue tools), `config.ts` (state-dir resolution,
the per-session toggle, ONE `autopilotCommand` implementation). The backends
own run identity + completion-event building (`buildCompletionEvent` / the
opencode equivalent); neither host re-implements the review lifecycle, the
toggle, or the event shape.

## Docs

- `docs/queue-model.md` — the AUTHORITATIVE queue model: statuses (proposal / approved / blocked / active / ai-review / human-review / failed / done / rejected), transitions, and the tick behavior (dispatch / intake with proposal-pending suppression / review).
- `docs/release.md` — the release process (`bun run release`): preconditions, boundary rule, the human operator paragraph, bump/commit/tag/publish, and the human-only steps.
- `skills/orchestrator-operations/` — the GENERIC operating skill for consumers (dispatch contract, review-loop judgment, completion standards, the flag_for_review handover). Backend-conditional: `SKILL.md` detects the active host in-session (the `/autopilot` command = pi) and loads only `references/pi.md` or `references/opencode.md`. Add it to your tool's skills config.
- The `flag_for_review` handover tool ships with the package (registered by the pi extension and the opencode plugin).

## Config / portability

Canonical defaults; local setups override via env:

| Var | Purpose |
|---|---|
| `AUTOPILOT_STATE_DIR` | queue store location; overrides `<host config dir>/autopilot.host.json` → `~/.local/state/orchestrator/<profile>` |
| `AUTOPILOT_LIB_DIR` | override lib resolution (published layouts) |
| `AUTOPILOT_OPENCODE_BIN` | opencode launcher (default `opencode`; a local wrapper can pin its own binary) |
| `AUTOPILOT_OPENCODE_RUNS_DIR` | opencode run records (default `~/.local/state/orchestrator-opencode/runs`) |
| `AUTOPILOT_*` (config) | `AUTOPILOT_MAX_SLOTS`, `AUTOPILOT_QUEUE_LOW`, `AUTOPILOT_WORKER_AGENTS`, `AUTOPILOT_REVIEWER_AGENTS`, `AUTOPILOT_REVIEW_CAP`, `AUTOPILOT_SWEEP_INTERVAL_MS` |

## Tests

```
bun test                      # 120 hermetic (fake oc backend, fake pi API)
OPENCODE_E2E=1 bun test       # + real `oc run` e2e
PI_E2E=1 bun test             # + real pi-subagents e2e
```
