Let coding agents ship overnight — safely, verifiably, resumably.
OvernightAgent (oa) is a Node/TypeScript CLI that runs coding agents
(claude · codex · opencode) unattended overnight against a queue of task
plans. You leave a plan on the desk, close the laptop lid, and wake up to a
SUMMARY.md with committed code, verification results, and any issues
flagged for review.
Every task runs in an isolated git worktree. Every step passes through
four verify gates (tail protocol · commit-since-start · user command ·
AI review). Failures trigger a fix-loop with the reviewer's findings
injected as context. Interrupted runs resume cleanly via git reset
git clean, with zero ambiguity about what was mid-flight.
- Why OvernightAgent
- Features
- Install
- Quick start
- How it works
- CLI reference
- State layout
- Architecture
- Design docs & ADRs
- Development
- Status — v0
- License
Coding agents are great at 20-minute tasks under a human's eye. They're less great at 8-hour queues where:
- Any single step failure cascades silently into the next.
- Uncommitted "work" bleeds between retries.
- A crashed run leaves you with no clear way to pick up where you were.
- "Did it actually verify?" is answered by re-reading the chat log.
oa is the supervisor layer that solves these directly:
| Pain | oa's answer |
|---|---|
| Agent drift between steps | Four-gate verify pipeline after every step |
| Dirty worktree on retry | git reset --hard HEAD && git clean -fdx between attempts (ADR-0003) |
| Mid-run crash | Pidfile + run.resume event; next oa rerun rewinds and re-enters |
| "What actually happened last night?" | Structured events.jsonl + auto-rendered SUMMARY.md |
| Agent lock-in | Single AgentAdapter interface; adapters for claude, codex, opencode |
- Worktree-per-task isolation. Every task checks out on
oa/<slug>-<id>branched frommain. Your working tree is never touched. - Four-gate verify pipeline per step: tail-message protocol
(
oa-statusfenced block) → commit-since-step-start → user-supplied verify command → AI reviewer. Any gate fails → fix-loop or mark blocked. - Fix-loop with context injection. Reviewer findings are spliced into the next attempt's prompt, so the agent sees exactly what to address.
- Structured event stream.
runs/<planId>/events.jsonlwith 36 typed event kinds (Zod-validated viaEventSchema). One line per event, append-only,O_APPEND-atomic. - Clean resume. Detect stale pidfile → rewind in-flight worktrees →
flip mid-step progress back to
pending→ emitrun.resume→ re-enter the supervisor loop. No manual untangling. - Detached daemon.
oa run --detachspawns a long-lived supervisor with its own pidfile and AF_UNIX control socket. Close the shell, close the laptop — it keeps working. - Operator control socket.
oa status/oa stop [--now]talk JSON over a local socket; the supervisor answers with live step/attempt snapshots or graceful/forced shutdown. - Atomic-everything. Temp+rename for every disk write, schema-versioned JSON, single-writer convention per per-task state file.
- Morning report. Auto-rendered
SUMMARY.mdper run — task outcomes, step tables, open P0/P1 issues, links to per-attempt prompts and logs. - Compact-recovery hook. When Claude Code auto-compacts a session,
a
SessionStart[compact]hook re-injects the task context (PROGRESS.md, current prompt, step pointer) so the agent resumes without losing its place. Installed automatically byoa shims install(ADR-0015). - Stall detection. Soft/hard attempt thresholds give operators an early warning before a step exhausts its budget, and inject a P0-styled stall warning into the agent's prompt so it can self-correct or escalate.
- Error budget. Optional plan-level circuit-breaker (
warnAfter/stopAfter) that stops scheduling tasks once too many steps are blocked, preventing systematic failures from wasting compute. - Sandbox-exec isolation. On macOS,
oa run --sandboxwraps each adapter subprocess in asandbox-execSeatbelt profile — kernel-level filesystem confinement that prevents the agent from reading secrets or writing outside the worktree. Opt-in for v0.2 (ADR-0016). - Rate-limit backoff. When an adapter run is rate-limited (claude
stream-json
rate_limit_error/overloaded_errorevents; codex / opencode stderr-matched429/ quota / overloaded phrases), the supervisor sleeps on any provider-suppliedretry-afterhint (or the configured default) and re-invokes the same prompt without advancing the verify attempt counter. Transport failures no longer cascade intoblocked-needs-human. Defaults: 60 s wait, 3 retries; override viaplan.overrides.rateLimitBackoff(ADR-0017). - Stream-json-aware tail parser.
parseTailauto-detects Claude's--output-format stream-jsoncapture and concatenates assistanttextblocks before running theoa-status/oa-reviewfence regex, so the tail gate sees the real user-visible output instead of JSON-escaped newlines. - Live heartbeat observability. Adapters classify their child's
output stream in real time and emit
step.heartbeatevents (session.init/api.retry/tool.use/assistant.delta/ratelimited) so operators can tell "agent is producing tokens slowly" from "agent is wedged" without waiting forstep.agent.exit. A supervisor-side 60 s dead-air watchdog emits a synthetic heartbeat when no adapter signal has fired.oa tailfilters these in its pretty view;oa tail --rawpasses them through for analysis. - Defer-to-FINDINGS on fix-loop exhaustion. When the review-fix
loop runs out of attempts with blocking issues still open, remaining
findings are appended to the task's
FINDINGS.md(with astep.findings.deferredevent) instead of hard-blocking the whole task — the next step's prompt reads "Findings so far" and picks up the slack. Opt out by tightening theblockOnpriority gate in the plan override. - Multi-agent. One CLI, three executors. Pick your executor + reviewer independently per task; mix claude-opus for the hard parts and codex/opencode for the grind.
Requires Node ≥ 22 (for JSON import attributes).
pnpm add -g @soulerou/oa-cli
# or: npm install -g @soulerou/oa-cliVerify:
oa --version
oa --helpgit clone https://github.com/a20185/OvernightAgent.git
cd OvernightAgent
pnpm install
pnpm -r build
pnpm --filter @soulerou/oa-cli link --global # makes `oa` available on PATHDrive oa from inside your coding agent with slash commands — one command
copies the bundled shim markdown into each host's convention-specific dir:
oa shims install --host all # installs all three with host defaults
oa shims install --host claude # project scope: ./.claude/{commands,skills}/
oa shims install --host claude --scope user # user scope: ~/.claude/{commands,skills}/
oa shims install --host codex # ~/.codex/prompts/
oa shims install --host opencode # ~/.config/opencode/commands/
oa shims install --host all --dry-run # preview without writing
oa shims install --host claude --force # overwrite local editsYou now get /oa-intake, /oa-queue, /oa-plan, /oa-status inside
your host agent.
Compact-recovery hook (Claude Code). When installed for the claude
host, oa shims install also merges a SessionStart[compact] hook into
.claude/settings.json. This hook fires automatically whenever Claude Code
auto-compacts a session mid-task. It re-injects the current task context
(PROGRESS.md, prompt path, and step pointer) so the agent can resume
without losing its place. The hook is keyed by the sentinel
# oa:hook=compact-recovery:v1 — re-running oa shims install will
upgrade it in-place without duplicating entries.
Five commands from zero to a running overnight queue:
# 1. Submit a task (via the /oa-intake shim or by hand with a JSON payload)
oa intake submit --payload-file /tmp/task-one.json # prints: t_2026-04-20_0001
# 2. Queue it
oa queue add t_2026-04-20_0001
# 3. Seal the plan
oa plan create --from-queue --budget 28800 # prints: p_2026-04-20_0001
# 4. Launch detached
oa run --detach p_2026-04-20_0001
# 5. Go to bed. In the morning:
oa status # or tail the log:
oa tail # live events, pretty-printed
cat $OA_HOME/runs/p_2026-04-20_0001/SUMMARY.mdIf anything crashed mid-run, pick up where you left off:
oa rerun p_2026-04-20_0001 # or --detach┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ intake │───▶│ queue │───▶│ plan │───▶│ run(s) │
│ submit │ │ add/ls │ │ create │ │ --detach │
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
│ │
▼ ▼
tasks/<id>/ runs/<planId>/events.jsonl
inbox row runs/<planId>/SUMMARY.md
control.sock + oa.pid
Inside a single step, the supervisor drives the following loop until the
step lands done, hits the attempt budget, or is marked blocked:
assemblePrompt # inject progress, findings, refs, tail protocol
│
▼
adapter.run ─────────▶ stdout/stderr # claude -p / codex exec / opencode run
│
▼
verifyTail ─▶ verifyCommit ─▶ verifyCmd ─▶ reviewer # four-gate verify
│ │
│ ├─▶ issues? yes ─▶ fix-loop + retry
▼ ▼ no
step.end(done) step.end(blocked | done)
Event stream (excerpt). Every state transition writes one JSONL line:
{"ts":"...","kind":"run.start","planId":"p_...","hostInfo":{...}}
{"ts":"...","kind":"task.start","taskId":"t_..."}
{"ts":"...","kind":"step.start","taskId":"t_...","stepN":1}
{"ts":"...","kind":"step.attempt.start","taskId":"t_...","stepN":1,"attempt":1}
{"ts":"...","kind":"step.verify.tail.ok","taskId":"t_...","stepN":1,"attempt":1}
{"ts":"...","kind":"step.verify.review.fail","taskId":"t_...","issues":[{"priority":"P1",...}]}
{"ts":"...","kind":"step.attempt.start","taskId":"t_...","stepN":1,"attempt":2}
{"ts":"...","kind":"step.end","taskId":"t_...","stepN":1,"status":"done"}
{"ts":"...","kind":"task.end","taskId":"t_...","status":"done"}
{"ts":"...","kind":"run.stop","reason":"completed"}| Command | What it does |
|---|---|
oa intake submit --payload-file <abs> |
Validate + mint taskId + write per-task files |
oa intake list [--status <s>] |
Tabular inbox view |
oa intake show <id> |
Pretty-print intake.json + steps.json |
oa intake rm <id> -y |
Remove from inbox |
oa queue add <ids...> |
Append to the queue |
oa queue ls · rm <id> · clear |
Manage the queue |
oa plan create [--from-queue|--tasks <ids>] [--budget <s>] [--parallel <n>] |
Seal a plan |
oa plan show <planId> · ls |
Inspect plans |
oa run [planId] [--detach] [--dry-run] [--sandbox] |
Run foreground or daemon; or print ordering; --sandbox wraps adapter spawns in macOS sandbox-exec |
oa stop [planId] [--now] |
Graceful stop (or force with --now); socket → pidfile fallback |
oa status [planId] [--json] |
Live snapshot from the control socket, or events-derived |
oa tail [planId] [--raw] [--once] |
Follow events.jsonl (pretty or verbatim) |
oa rerun <planId> [--detach] |
Resume after crash / stop; rewinds in-flight worktrees |
oa summary <planId> [--stdout] |
(Re-)render SUMMARY.md from events |
oa archive <id> |
Move a task or run folder to _archive/ |
oa shims install [--host <h>] [--scope <s>] [--dry-run] [--force] |
Copy bundled slash-command markdown into the host's convention dir |
Run oa <cmd> --help for the full option list on any subcommand.
The one-and-only root directory: $OA_HOME (defaults to $HOME/.oa/).
$OA_HOME/
├─ tasks.json # inbox index (every task, any status)
├─ queue/queue.json # FIFO id list
├─ tasks/<taskId>/ # per-task files
│ ├─ intake.json # the full sealed submission
│ ├─ steps.json # parsed top-level steps
│ ├─ HANDOFF.md # human-readable context block
│ ├─ PROGRESS.md # live step status table
│ ├─ FINDINGS.md # append-only notes from the agent
│ ├─ source-plan.md # verbatim markdown submitted at intake
│ └─ refs/ # materialized references (ADR-0007)
├─ worktrees/<taskId>/ # `git worktree add` target; branch `oa/<slug>-<id>`
├─ plans/<planId>.json # sealed plan (immutable after seal)
├─ runs/<planId>/
│ ├─ events.jsonl # canonical event stream
│ ├─ SUMMARY.md # auto-rendered morning report
│ ├─ oa.pid # daemon pidfile
│ ├─ control.sock # AF_UNIX control socket
│ └─ <taskId>/step-NN/attempt-NN/
│ ├─ prompt.md # full assembled prompt
│ ├─ stdout.log # captured agent stdout
│ └─ stderr.log # captured agent stderr
└─ _archive/ # `oa archive` destination
Every JSON file carries schemaVersion: 1. Every write is writeFileAtomic
(temp + rename). Schemas are Zod, .strict() for closed shapes.
Environment variables set by the supervisor per adapter spawn:
| Variable | Purpose |
|---|---|
OA_HOME |
State root (default $HOME/.oa/). Override to isolate test runs. |
OA_TASK_DIR |
Absolute path to the current task's directory under $OA_HOME/tasks/<taskId>/. Used by compact-recovery hook to re-read PROGRESS.md. |
OA_CURRENT_PROMPT |
Absolute path to the current attempt's prompt.md. Used by compact-recovery hook to re-inject the full prompt after compaction. |
OA_RESUME |
Set to 1 when the supervisor entry is invoked in resume mode (oa rerun). |
oa is a pnpm monorepo published under the @soulerou scope:
| Package | Role |
|---|---|
@soulerou/oa-core |
Schemas, paths, id mint, atomic JSON, worktree manager, intake parser, verify pipeline, fix-loop, events reader/writer, supervisor (runPlan + resumePlan), daemon launcher, pidfile, control socket, SUMMARY renderer |
@soulerou/oa-cli |
Commander-based CLI; every subcommand wraps an @soulerou/oa-core API. Bundles the host-agent shims under dist/shims/ and exposes them via oa shims install. |
@soulerou/oa-adapter-claude |
Headless claude -p invoker + session_id parser (stream-json) |
@soulerou/oa-adapter-codex |
Headless codex exec invoker |
@soulerou/oa-adapter-opencode |
Headless opencode run invoker |
packages/oa-shims/{claude,codex,opencode} |
Slash-command resource bundles — pure markdown, no JS. Source of what oa shims install ships. Not published separately. |
The supervisor is agent-agnostic — it depends only on the AgentAdapter
interface (ADR-0009) and resolves concrete adapters via a lazy registry.
Adding a fourth executor is a ~60-line adapter package plus a registry
id. Schema-level enum tightening makes accidental misroutes a compile
error.
Everything interesting was argued out in writing before it was coded:
- Design doc — full §1–§8 system design
- Implementation plan — the 13-phase roadmap this repo was built against
- ADRs 0001–0017 — every major decision with context + alternatives:
| ADR | Topic |
|---|---|
| 0001 | Branch and commit hygiene |
| 0002 | Worktree per taskList + absolute paths |
| 0003 | Clean rewind on resume |
| 0004 | Verification pipeline + fix loop |
| 0005 | Runs as events.jsonl + summary |
| 0006 | Context injection per step |
| 0007 | References tiered handling |
| 0008 | Agent tail-message protocol |
| 0009 | AgentAdapter interface |
| 0010 | Process model — detached supervisor |
| 0011 | Strategy as orthogonal toggles |
| 0012 | Daemon control via Unix socket |
| 0013 | ESLint path-discipline enforcement gaps |
| 0014 | Scoped npm publish, cycle break, bundled shims |
| 0015 | Harness hardening: compact-recovery hook, stall detection, error budget |
| 0016 | macOS sandbox-exec profile around adapter runs |
| 0017 | Rate-limit detection + backoff around adapter runs |
pnpm -r build # compile every package (oa-cli also bundles shims)
pnpm -r test # vitest — 519 tests across 5 packages
pnpm -r lint # eslint
pnpm -r typecheck # tsc --noEmitpnpm version:patch # or version:minor / version:major — bumps all 5 packages together
pnpm release:dry-run # show what `pnpm -r publish` would upload
pnpm release # four-gate verify + `pnpm -r publish --access public`pnpm refuses to publish from a dirty working tree; commit first. The scope
@soulerou must be owned by the publishing npm account. See
ADR-0014.
- Absolute paths at every worktree API boundary.
assertAbs(p)is mandatory; ESLint bans barepath.joinin**/worktree*.tsand**/paths*.ts(usepath.resolve). See ADR-0002 + ADR-0013. - Atomic writes only.
writeJsonAtomic/writeFileAtomic(temp + rename). Neverfs.writeFilethe target directly. - Schema-versioned JSON. Every on-disk shape carries
schemaVersion: 1. - TDD per task. Failing test → minimal implementation → passing test → single commit. Sabotage-check load-bearing assertions (break prod, see the test red, restore).
- TypeScript 6 +
module: NodeNext+verbatimModuleSyntax⇒ relative imports insrc/must spell.jseven though the file is.ts.tsc --build . --force(nottsc -p . --force).
- Unit + integration tests live next to their subject.
- Integration tests create a tmp
$OA_HOMEinos.tmpdir()andrm -rfit inafterEach. - Cross-process tests fork real Node children to exercise file-lock contention and pidfile ownership.
All v0.2 hardening features are complete; 536 tests pass.
Published to npm under @soulerou (see ADR-0014).
v0.4 adds (on top of ADR-0015/0016/0017):
- Live heartbeat observability — adapters classify child output in real
time and emit
step.heartbeatevents so operators can tell a slow agent from a wedged one while a run is in-flight. - Defer-to-FINDINGS on fix-loop exhaustion — remaining blocking
issues are appended to
FINDINGS.md(+step.findings.deferredevent) instead of hard-blocking the task. intake.verify.requireCommit— opt out of the commit-since-step-start gate for validation-only steps.- Codex adapter now uses a positional prompt argument (was:
--prompt-file); default model bumped togpt-5.4. - Misc fixes:
stdin:'ignore'on every spawn (fixes codex-exec hang); claude rate-limit detector matchesresult.is_error+api_error_status=429;oa statuspicks the latest plan bycreatedAtrather than readdir order.
v0.3 added (see ADR-0017):
- Rate-limit detection + supervisor backoff wrapper
(
step.ratelimit.wait/.retry/.give_upevents; plan overriderateLimitBackoff: { defaultWaitMs, maxRetries, maxWaitMs? }) - Stream-json-aware tail parser (unwraps Claude's
--output-format stream-jsonbefore theoa-status/oa-reviewfence regex runs)
v0.2 adds (see ADR-0015 and ADR-0016):
- Compact-recovery hook for Claude Code (auto-re-injects context after compaction)
- Stall detection with soft/hard attempt thresholds +
step.stallevent - Plan-level error budget (
warnAfter/stopAfter) withplan.budget.warn+plan.budget.exhaustedevents skippedtask status for budget-exhausted tasks- macOS sandbox-exec isolation (
oa run --sandbox) - Supervisor-set env vars
OA_TASK_DIR+OA_CURRENT_PROMPTper adapter spawn
Known v0 limits (slated for post-v0 follow-up):
parallel.max > 1is accepted by the schema but the supervisor is sequential. Two tasks never run concurrently in v0.runs/<planId>/reviewer-default-prompt.mdis materialized once per run — would race if parallel mode lands. Add a per-task suffix then.- ADR-0008 promised
oa-core/prompts/protocol-status.md+protocol-review.md; blocks are currently inlined as constants inverify/context.ts+verify/review.ts. Deferred (see the ADR). oa shimshas nouninstall/updatesubcommands; reversal is manualrm. v0 scope (see ADR-0014 follow-ups).
See HANDOFF.md + PROGRESS.md for the
session-level audit trail.
Released under the MIT License. Copyright © 2026 Souler Ou.