# ADR 0003: Routines as /autopilot Cloud-Execution Path

> Status: ACCEPTED · session main-2026-05-19-deep-2 · issue #438
> Source research: docs/research/2026-05-19-routines-evaluation.md (doc-state snapshot 2026-05-19)
> Project-instruction file resolution: this repo's root context file is `CLAUDE.md` on Claude Code / Cursor IDE and `AGENTS.md` on Codex CLI — transparent aliases per [skills/_shared/instruction-file-resolution.md](../../skills/_shared/instruction-file-resolution.md). Wherever this ADR says "the project's CLAUDE.md", the alias rule applies.

## Context

`/autopilot` (Phase C-1.b, issues #295 + #300; STALL_TIMEOUT added Phase C-2 / ADR-364 #371) is **local-only by construction**: `runLoop` (`scripts/lib/autopilot/loop.mjs:66-287`) is a pure in-process controller driven by Claude *inside an open chat session*, and `skills/autopilot/SKILL.md:218` names the exact gap — *"not truly autonomous — Claude must stay in the chat. Doesn't deliver walk-away UX."* When the laptop closes, the loop dies.

Anthropic's **Routines** (research preview, shipped 14 Apr 2026 — https://code.claude.com/docs/en/routines) is the direct candidate for that gap: a saved Claude Code configuration (prompt + repositories + connectors + ≥1 trigger) that "execute[s] on Anthropic-managed cloud infrastructure, so [it] keep[s] working when your laptop is closed." This is precisely the work-class `.claude/rules/loop-and-monitor.md` LM-004 already routes away from `/loop` — "work that must run when no session is open … cross-repo … persist across machine restarts" — while LM-005 forbids reimplementing `/autopilot` as an unguarded scheduler because that "loses both [kill-switches and telemetry]". The decision factor from issue #438 was explicit: **adopt only if Routines preserve our kill-switches or expose a stop-condition we could feed spiral-detection into.**

The W2 research note (`docs/research/2026-05-19-routines-evaluation.md`) resolved the trade-off matrix against cited evidence. The decisive findings, sourced there:

- **The runtime enforces 10 kill-switches, not 9.** `scripts/lib/autopilot/kill-switches.mjs:18-32` `Object.freeze` enumerates ten; the issue's "9" is the pre-`STALL_TIMEOUT` count, the SKILL.md "8" is staler still — the runtime is SSOT (research note "Our Code-State", Gap Matrix row 1).
- **Routines have no autonomy guard at the loop level.** "no permission-mode picker and no approval prompts during a run"; the only documented bound is a per-account daily run cap (Finding 3). **There is no stop-condition expression, max-iteration cap, max-turn cap, or budget kill-switch** — the single most attractive hypothetical from issue #438 *does not exist* in the documented surface (Gap Matrix rows 1-3).
- **But repo-committed hooks AND repo-declared plugins DO run in cloud sessions.** The cloud-config carry-over table (https://code.claude.com/docs/en/claude-code-on-the-web) confirms `.claude/settings.json` hooks → "Yes → Part of the clone" and repo-declared plugins → "Yes → Installed at session start from the marketplace"; constraint: *only* committed hooks run, user-level `~/.claude` hooks do not (Finding 7). Our `hooks/hooks.json` destructive-guard substrate is therefore preservable in-cloud.
- **Green status ≠ task success.** A green run only means "the session started and exited without an infrastructure error"; the operator must read the transcript to confirm what Claude did (Finding 6).
- **Routines win exactly one Gap-Matrix row** — laptop-closed durability — and lose or only partially match every autonomy-safety row (research note "Net", lines 189-194). `/loop`'s recurring-task expiry is **7 days, not 3** (Finding 9), and its 1-minute minimum interval contrasts with Routines' 1-hour minimum and no-open-session requirement — confirming the two primitives address different slots, consistent with LM-001's routing tree.

The interacting decision is ADR 0002 (#437, Agent Teams as wave substrate, `docs/research/2026-05-19-agent-teams-evaluation.md`): both evaluate replacing a load-bearing local abstraction with an Anthropic-native substrate, and both land on **Adapter, not Adopt**, for the same structural reason — the native primitive supplies raw capability but not the safety surface (`docs/adr/0001-context-vs-orchestration.md`'s house thesis). The two adapters compose: an Agent-Teams-backed wave could itself run inside a Routine-fired cloud session, but only under the same one-bounded-session constraint this ADR sets.

## Decision

**Decision: Adapter.** Adopt is refuted on evidence and Stay is too conservative: keep `runLoop` plus all 10 kill-switches (`scripts/lib/autopilot/kill-switches.mjs:18-32`) as the autonomy loop brain, and add an **opt-in path that packages exactly one bounded `/autopilot` session per Routine fire** — never a multi-iteration in-cloud loop. The kill-switch logic moves *inside the dispatched prompt and the repo-committed `hooks/hooks.json` substrate* (Finding 7 confirms our committed hooks and repo-declared plugin run in cloud, so `pre-bash-destructive-guard` / `enforce-scope` / `enforce-commands` stay live); `autopilot.jsonl` and the session-end STATE.md/sessions.jsonl writes are made durable by committing them on a `claude/`-prefixed branch before the ephemeral clone is reclaimed. **Scope is hard-bounded to one-session-per-fire because the loop-level kill-switches (`SPIRAL`, `CARRYOVER_TOO_HIGH`, `STALL_TIMEOUT`, `MAX_SESSIONS_REACHED`, `TOKEN_BUDGET_EXCEEDED`) have no cloud enforcement seam: Routines expose no stop-condition primitive (Gap Matrix rows 1-3), so any in-cloud iteration loop would run *unguarded* — exactly the LM-005-forbidden failure mode.** Adopt (replace the local loop) is rejected: it would forfeit ten verified guards to gain one durability property from a research-preview surface whose limits "may change". The Adapter is consistent with ADR-364's "adapt, don't replace" posture and the graduated-adoption discipline (`docs/adr/2026-05-10-364-remote-agent-substrate.md:99`): ship behind a Session Config flag, default off, telemetry-gated promotion — and it must not be wired into production until the empirical spin-up follow-up (below) confirms the in-cloud hook + telemetry-persistence behaviour the docs only imply.

## Consequences

**What we build (behind a default-off Session Config flag):**

- A Routine-packaging path that emits one Routine config (prompt + this repo + a single bounded `/autopilot` invocation) per fire. The dispatched prompt carries the kill-switch *intent* declaratively; the mechanical guards are the repo-committed `hooks/hooks.json` entries (Finding 7 — destructive-guard, scope-enforce, command-enforce all run in the cloud session once the plugin is declared in the cloned repo's `.claude/settings.json`).
- A durable-telemetry shim: the in-cloud session commits `.orchestrator/metrics/autopilot.jsonl` (schema_version 1, `writeAutopilotJsonl` `telemetry.mjs:74-86`) and the session-end `sessions.jsonl` + STATE.md Recommendation fields onto a `claude/`-prefixed branch before the ephemeral clone is reclaimed (Finding 7 memory-reclamation warning). Without this commit step the JSONL is lost on the fresh-clone teardown (Gap Matrix "Compatibility with session-end" = PARTIAL — runnable, telemetry persistence unsolved).

**What we keep unchanged:** `runLoop` + the 10-switch evaluators (`preIterationKillSwitch`/`postSessionKillSwitch`, `kill-switches.mjs:54-174`) remain the only multi-iteration autonomy substrate. Convergence-monitoring (`skills/convergence-monitoring/SKILL.md`) and wave-executor spiral detection stay local. The local loop is unaffected; the Adapter is additive.

**Hard precondition (blocking before any production wiring):** the W2 "Empirical" section is N/A docs-only and the spin-up was *deferred* — the research-preview disclaimer makes any empirical number perishable. No production wiring until a follow-up spike packages this repo as a real cloud Routine and verifies, against the live surface: (1) `hooks/hooks.json` destructive-guard actually fires in the cloud session; (2) the session-end `claude/`-branch commit survives the ephemeral-clone reclamation; (3) the per-account daily-run-cap ceiling (third-party reports ~15 runs/account — Finding 5 — but the docs state no fixed number) against a one-session-per-fire cadence.

**Billing UNVERIFIED:** Routines "draw down subscription usage the same way interactive sessions do" plus a daily run cap (Finding 5); there is **no per-run pre-execution budget gate** — `TOKEN_BUDGET_EXCEEDED` cannot be enforced before a cloud fire, only observed post-hoc at the usage page. The ~15-runs/account default is third-party (InfoQ/9to5Mac), not the canonical docs, and must be measured in the empirical spike, not assumed.

**Ecosystem interactions:**

- **ADR 0002 (#437, Agent Teams):** independent Adapter verdict, same rationale (native substrate supplies capability, not the safety surface). The two compose only under this ADR's one-bounded-session constraint — an Agent-Teams wave inside a Routine fire is still one bounded session, not a cloud loop.
- **`.claude/rules/loop-and-monitor.md`:** LM-004 already routes the overnight/cross-repo/survives-restart class to Routines/Desktop; this ADR makes that routing concrete for the `/autopilot` housekeeping subset *without* contradicting LM-005 (no unguarded scheduler replaces `/autopilot`; the loop brain stays local and guarded). LM-001's primitive-selection tree is unchanged — Routines remain the "periodic, unbounded-by-session, must-survive-restart" leaf, distinct from `/loop` (≤7-day, session-scoped) and Monitor (streamable trigger).

## Follow-ups

Issues to file:

1. **Empirical Routine spin-up + billing/trace observation** (the deferred W2 spike — **hard precondition, blocks production wiring**). Package this repo as a cloud Routine; verify in-cloud `hooks/hooks.json` destructive-guard firing, `claude/`-branch session-end-write survival across the ephemeral clone, and measure the daily-run-cap ceiling + actual subscription draw-down against a one-session-per-fire cadence. Record the doc-state snapshot date; treat numbers as perishable per the research-preview disclaimer.
2. **Durable `autopilot.jsonl` via `claude/`-branch commit** — implement the telemetry shim that commits `.orchestrator/metrics/autopilot.jsonl` + `sessions.jsonl` + STATE.md Recommendation fields onto a `claude/`-prefixed branch before clone reclamation; this is the structural fix for the Gap-Matrix "telemetry persistence unsolved" partial.
3. **Stop-condition primitive watch** — track Routines docs for any halt-on-condition / max-turn / max-iteration / budget-expression surface. If/when one ships, re-open this ADR: a verified stop-condition is the *only* evidence that would upgrade the verdict from one-bounded-session-per-fire toward a guarded multi-iteration cloud loop (the issue #438 "attractive if…" case that does not exist today).

## Implementation Status — deep-3 (2026-05-19)

Adapter verdict stands. Durable-telemetry framework shipped (inert); empirical spin-up **PENDING-EMPIRICAL** (user has no Anthropic Routines access this session):

- **Framework shipped**: `scripts/lib/autopilot/durable-telemetry.mjs` — `durableCommit()` + `withDurableCommit()`, `enabled: false` default (no-op for local execution; cloud spike flips `true`). Inert seam wired at both `writeAutopilotJsonl()` call sites in `scripts/lib/autopilot/loop.mjs`. Security-hardened post-W4: `SAFE_BRANCH_RE` allowlist + `cwd` confinement guard + `JSON.stringify` on all git-command interpolations (W4-Q5-MED-1/2). 11 unit tests added (`tests/lib/autopilot/durable-telemetry.test.mjs`).
- **Empirical test plan documented**: `docs/research/2026-05-19-deep-3-routines-empirical.md` (299 lines, 8 sections, copy-paste-ready Routine spec + JSONL result schema, status PENDING-EMPIRICAL).
- **Stop-condition watch established**: quarterly re-check baseline set 2026-05-19 (no stop-condition primitive exists in current Routines docs — confirmed W1 D6; next check 2026-08-19). A verified stop-condition primitive is the ONLY evidence that re-opens this ADR toward a guarded multi-iteration cloud loop.
- **Known follow-up gap** (W4 architect-reviewer MED-1): `durableCommit` call sites currently pass only `autopilot.jsonl`; ADR 0003:32 mandates `autopilot.jsonl` + `sessions.jsonl` + STATE.md. This MUST be closed before any `enabled: true` flip — tracked as a deep-3 follow-up issue.

W4 architect-reviewer verdict: PROCEED_WITH_FOLLOWUPS (framework shape correct + adapter-discipline-compliant; 3-file completeness + test coverage are pre-`enabled:true` gates).
