Autopilot Skill

SkillDev tools

Use this skill when running an autonomous session-orchestration loop. Chains session-start → session-plan → wave-executor → session-end for N iterations with all 10 kill-switches (SPIRAL, FAILED wave, carryover > 50%, max-hours, max-sessions, resource-overload, token-budget, stall-timeout, sub-threshold confidence, user-abort). Reads Mode-Selector output (Phase B) to decide auto-execute vs. fallback. Writes one autopilot.jsonl record per loop run. Phase C scaffold (issue #277); implementation lives in scripts/lib/autopilot.mjs (Phase C-1 follow-up).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Autopilot Skill skill

What this skill tells your AI

The instructions your AI receives, as published by kanevry/session-orchestrator in skills/autopilot/SKILL.md and read by ahel’s review.

Phase 0.5: Parallel-Aware Preamble

Skip silently when persistence: false in Session Config.

Before any Phase 1 work, run the parallel-aware preamble per skills/_shared/parallel-aware-preamble.md. The preamble detects other active sessions in the worktree-family via findPeers(repoRoot, { mySessionId }), classifies the caller's mode via classifyMode(callerMode) against the exclusivity-matrix, and either:

  • Returns PASS_THROUGH (no other session / always-ok mode) → continue to Phase 1
  • Returns EXCLUSIVE_BLOCKED → fires Exclusive-Conflict AUQ from skills/_shared/parallel-aware-auq.md
  • Returns PROMOTION_OFFER → fires Worktree-Promotion AUQ (via enterWorktree() from scripts/lib/autopilot/worktree-pipeline.mjs — see parallel-aware-auq.md outcome-handling)

On any non-PASS_THROUGH outcome that does not result in immediate exit, append a Deviation to STATE.md via appendDeviationOnDisk(repoRoot, isoTimestamp, message) from scripts/lib/state-md.mjs.

Implementation reference: skills/_shared/parallel-aware-preamble.md § Implementation. AUQ reference: skills/_shared/parallel-aware-auq.md.

Status

Phase C-1.b complete (2026-04-25, issues #295 + #300). Runtime at scripts/lib/autopilot/kill-switches.mjs:18-32 (the frozen KILL_SWITCHES enum is SSOT) enforces all 10 kill-switches:

  • Pre-iteration (6, #295 + #355): max-sessions-reached, max-hours-exceeded, resource-overload, low-confidence-fallback (with iter-1-fallback / iter-2+-exit asymmetry), user-abort, token-budget-exceeded (cumulative tokens ≥ --max-tokens).
  • Post-iteration (1, ADR-364 §3): stall-timeout — no progress marker in autopilot.jsonl within the threshold (default 600s; missing file → no kill).
  • Post-session (3, #300): spiral, failed-wave, carryover-too-high. Read schema-canonical fields off the sessionRunner return shape: agent_summary.{spiral, failed} (numeric counts) and effectiveness.{carryover, planned_issues}. Absent fields → no kill (forward-compatible: a sessionRunner that does not yet emit those fields silently no-ops the post-session gates).

Atomic autopilot.jsonl writer (tmp+rename, schema_version 1) and silent-clamp parseFlags shipped in C-1. autopilot_run_id is passed into sessionRunner via args.autopilotRunId; production callers MUST persist it into the per-iteration sessions.jsonl record (additive optional field, schema_version 1 compatible). See skills/wave-executor/SKILL.md § Return Shape Contract and skills/session-end/SKILL.md § Phase 3.7.

Purpose

Autopilot collapses the per-session attention cost when Mode-Selector is confident enough to make routine decisions autonomously. A productive day commonly ships 3–7 sessions; each manual session-start costs the user 10–60 seconds of context-switch attention. When the session is genuinely routine (mechanical refactor, post-merge housekeeping, repeated follow-ups from a planned epic), that attention cost is pure overhead.

/autopilot reads the Mode-Selector recommendation, executes the recommended session if confidence clears the threshold, then loops — checking kill-switches between iterations. The user invokes the loop once and walks away; autopilot stops itself when work runs out or quality degrades.

This is opt-in by design: autopilot never starts itself. The user must run /autopilot explicitly. Configuration thresholds (--max-sessions, --max-hours, --confidence-threshold) are CLI flags, not Session Config defaults — the user signals intent for THIS run, not a standing policy.

Command Surface

/autopilot [--max-sessions=N] [--max-hours=H] [--confidence-threshold=0.X] [--dry-run]
FlagDefaultBoundsMeaning
--max-sessions51..50Iteration cap (graceful exit when reached)
--max-hours4.00.5..24.0Wall-clock budget for entire loop
--confidence-threshold0.850.0..1.0Minimum selectMode confidence for auto-execute
--dry-runfalsePrint planned iterations without executing

Out-of-range values silently clamp to bounds. --dry-run exits after printing — never invokes session lifecycle.

Loop Semantics

state := { iterations_completed: 0, started_at: now(), kill_switch: null, sessions: [] }

WHILE state.iterations_completed < max-sessions:
  # Pre-iteration kill-switches (6)
  IF aborted: kill_switch := 'user-abort'; break
  IF state.iterations_completed >= max-sessions:
    kill_switch := 'max-sessions-reached'; break
  IF (now() - state.started_at) > max-hours:
    kill_switch := 'max-hours-exceeded'; break
  IF cumulative_tokens_used >= max-tokens:
    kill_switch := 'token-budget-exceeded'; break
  IF resource_verdict() == 'critical' AND peer_count() > autopilot-peer-abort:
    kill_switch := 'resource-overload'; break

  recommendation := mode-selector.selectMode(<live signals from session-start Phase 7.5>)

  IF recommendation.confidence < confidence-threshold:
    IF state.iterations_completed == 0:
      fallback_to_manual()  # iteration 1: hand off cleanly to manual /session flow
    ELSE:
      kill_switch := 'low-confidence-fallback'  # iteration 2+: exit, let user decide
    break

  cap := resource_adaptive_cap()
  session_result := run_session(mode=recommendation.mode, agents_per_wave_cap=cap)
  state.sessions.append(session_result.session_id)

  # Post-iteration kill-switch (1)
  IF stalled(autopilot.jsonl) >= stall-timeout: kill_switch := 'stall-timeout'; break
  # Post-session kill-switches (3)
  IF session_result.spiral_detected: kill_switch := 'spiral'; break
  IF session_result.failed_waves > 0: kill_switch := 'failed-wave'; break
  IF session_result.carryover_ratio > 0.50: kill_switch := 'carryover-too-high'; break

  state.iterations_completed += 1

write_autopilot_jsonl(state, kill_switch)
print_summary(state, kill_switch)

Atomicity rule: iteration boundaries are atomic. A session must complete (/close including the post-session writes) before the next iteration starts. Autopilot does NOT abort sessions mid-flight; kill-switches are checked AFTER each session completes.

Kill-Switches

All 10 kill-switches, grouped by check phase (mirrors the KILL_SWITCHES enum in scripts/lib/autopilot/kill-switches.mjs:18-32):

Kill-switchPhaseTriggerRecovery hint
max-sessions-reachedpre-iterationiterations_completed >= --max-sessionsGraceful — not an error.
max-hours-exceededpre-iterationWall-clock exceeds --max-hoursRe-run with higher --max-hours or address slow waves.
resource-overloadpre-iterationverdict==critical AND peers > autopilot-peer-abortWait for peer sessions to complete or close them.
low-confidence-fallbackpre-iterationconfidence < threshold (iteration 2+)Re-run with lower --confidence-threshold or run next session manually.
user-abortpre-iterationCtrl+C / Esc (AbortSignal)Re-run when ready.
token-budget-exceededpre-iterationcumulative_tokens >= --max-tokens (#355)Re-run with a higher --max-tokens budget or split the work.
stall-timeoutpost-iterationNo progress marker in autopilot.jsonl within threshold (ADR-364 §3; default 600s)Inspect the stalled iteration; missing telemetry file is NOT a kill.
spiralpost-sessionwave-executor spiral detection fires (agent_summary.spiral > 0)Triage the spiraling wave manually; autopilot will not retry.
failed-wavepost-sessionAny wave reports agent_summary.failed > 0Investigate failure mode (test contract drift, env issue). Re-run after fix.
carryover-too-highpost-sessioncarryover/planned > 0.50Last session under-delivered. Reduce scope or split issues before resuming.

Resource-Adaptive Concurrency

Autopilot does NOT hard-block on peer Claude processes. It adapts agents-per-wave cap per iteration based on the most-restrictive resource signal.

TierRAM freeSwapPeersmacOS memory_pressurecap
green≥ 6 GB< 1 GB≤ 2≥ 30% freeSession Config default
warn4–6 GB1–2 GB3–415–30% free4
degraded2–4 GB2–3 GB5–65–15% free2
critical< 2 GB> 3 GB> 6< 5% free0 (coord-direct)

Most-restrictive-signal-wins: [ram=8GB, swap=0, peers=7] → critical (peer rule wins).

Defaults are conservative initial estimates. Phase C-3 follow-up calibrates the swap and memory_pressure thresholds against real autopilot-run effectiveness data.

Production Wiring

Phase C-1 ships runLoop as a pure controller. Phase C-1.c ships buildLiveSignals as the canonical signals-assembly helper. This section documents the in-process driver protocol (Option B from #301): how Claude — running as the coordinator in a chat session — drives runLoop between manual /session invocations. The headless wrapper (Option A, scripts/autopilot.mjs CLI spawning claude -p) is reserved for Phase C-5.

Dependency-Injection Contract

runLoop requires four injected dependencies:

FieldSignatureSource
modeSelector() => Promise<{mode, confidence, rationale?}>wraps selectMode(await buildLiveSignals())
sessionRunner({mode, autopilotRunId}) => Promise<{session_id, agent_summary?, effectiveness?}>wraps a /session <mode> invocation; reads sessions.jsonl tail to construct return value
resourceEvaluator() => {verdict}wraps evaluate(await probe(), thresholds) from resource-probe.mjs
peerCounter() => numberreads claude_processes_count from a fresh probe() snapshot

abortSignal is optional (Ctrl+C / Esc → user-abort kill-switch).

In-Process Driver Skeleton

import { runLoop, parseFlags } from '$PLUGIN_ROOT/scripts/lib/autopilot.mjs';
import { buildLiveSignals } from '$PLUGIN_ROOT/scripts/lib/build-live-signals.mjs';
import { selectMode } from '$PLUGIN_ROOT/scripts/lib/mode-selector.mjs';
import { probe, evaluate } from '$PLUGIN_ROOT/scripts/lib/resource-probe.mjs';

const flags = parseFlags(process.argv.slice(2));

const modeSelector = async () => {
  // Each iteration rebuilds signals from current disk state. STATE.md will be
  // freshly idle-reset by the previous /close, sessions.jsonl will have the
  // new tail entry, etc. This is the contract: live signals every iteration.
  // `repoRoot` is passed explicitly (#1071) rather than left to the helper's
  // cwd default. Do NOT hand-write a `backlogLimit` here — the window default
  // lives once, in backlog-scan.mjs (`DEFAULT_BACKLOG_LIMIT`); see
  // skills/session-start/phase-7-5-mode-selector.md for the same contract.
  const signals = await buildLiveSignals({ repoRoot: process.cwd() });
  return selectMode(signals);
};

const resourceEvaluator = () => {
  const snapshot = probeSync();   // or cached snapshot if probe is async
  return evaluate(snapshot, thresholds);
};

const peerCounter = () => {
  // Synchronous-friendly count from a recent probe snapshot.
  return latestSnapshot.claude_processes_count ?? 0;
};

const sessionRunner = async ({ mode, autopilotRunId }) => {
  // The coordinator (Claude) invokes /session <mode> manually here. After the
  // session completes (/close runs, sessions.jsonl appended), this function
  // reads the tail entry and projects it into the runLoop return-shape.
  const tail = readSessionsJsonlTail(1);   // last line, normalized
  return {
    session_id: tail.session_id,
    agent_summary: tail.agent_summary,    // {complete, partial, failed, spiral}
    effectiveness: tail.effectiveness,    // {planned_issues, carryover, completion_rate, ...}
  };
};

const result = await runLoop({
  ...flags,
  modeSelector,
  sessionRunner,
  resourceEvaluator,
  peerCounter,
});

Why In-Process First

The in-process driver has Claude (the coordinator) call /session <mode> between runLoop iterations, with runLoop orchestrating the kill-switches. Trade-offs:

  • Pro: zero new infra. Reuses canonical kill-switch logic. Validates buildLiveSignals against real Phase 7.5 swap before headless complexity. Each iteration carries inter-session memory through STATE.md / sessions.jsonl / learnings.
  • Con: not truly autonomous — Claude must stay in the chat. Doesn't deliver walk-away UX. That's Phase C-5's job.

autopilot_run_id Propagation

When runLoop invokes sessionRunner({mode, autopilotRunId}), the per-iteration sessions.jsonl record MUST carry autopilot_run_id: <id>. session-end Phase 3.7 writes this field. Manual sessions write null or omit it — readers treat both identically per the v1 schema additive convention. See skills/session-end/session-metrics-write.md.

Acceptance Signals

A live /autopilot invocation against this wiring produces a non-zero confidence recommendation when at least one signal source is populated (state-md rec fields, sessions.jsonl tail, learnings, or backlog). Confidence at 0.0 with all four sources populated is a Mode-Selector heuristic bug (file as [Mode-Selector v1.x quirk] issue), not an autopilot bug.

Pre-Loop Verdict Gate (dispatcher → autopilot handoff — #682)

When the cross-repo dispatcher (skills/dispatcher/SKILL.md) routes into an autopilot launch, a pre-loop suitability verdict decides whether the launch may proceed WITHOUT per-selection operator confirmation. This is distinct from — and runs BEFORE — the loop's 10 kill-switches:

  • The verdict gate is a PRE-LAUNCH decision. It is computed ONCE, at the dispatcher → runLoop handoff, before the first iteration starts. It answers "may I launch this repo autonomously, or must I ask first?" — NOT "should I stop the running loop?"
  • The 10 kill-switches are PER-ITERATION and reused UNCHANGED. Once runLoop starts, the frozen KILL_SWITCHES enum (scripts/lib/autopilot/kill-switches.mjs:18-32) governs when the loop stops, exactly as documented above. The verdict gate adds NO new kill-switch, modifies NONE of the existing 10, and does not re-implement any of them.
  • The gate engine is computeSuitabilityVerdict(deps) from scripts/lib/autonomy/suitability.mjs — a pure four-gate AND (confidence ≥ floor; kill-switch fired-rate < 0.2 over the recent runs, omitted below 5 runs; CI ≠ red; resource ≠ critical). The dispatcher gathers every signal and passes it in (DI); the engine reads no files.
  • Kill-switch history feeds G2 via readRecentAutopilotRuns from scripts/lib/autopilot/recent-runs.mjs, which reads THIS repo's .orchestrator/metrics/autopilot.jsonl (newest-last, never throws). The verdict's G2 gate counts those records and reads each one's persisted kill_switch field — it never re-enumerates or re-derives the switches; it reads the history the loop already wrote.
  • FAIL-CLOSED launch wiring: the dispatcher launches without confirmation ONLY when autonomy === 'autonomous-gated' AND verdict.suitable === true. Every other case (any non-autonomous-gated dial, a CI-red / resource-critical / low-confidence verdict) informs the operator and asks before launch. resolveDispatcherAutonomy defaults to 'off' when unconfigured, so an absent config forces inform + ask. See skills/dispatcher/SKILL.md § Phase 1.5 for the full sourcing table and invariant.
  • null signals are honest, not failures (NICE-b). On a CI-fetch or resource-probe failure the dispatcher passes ci = null / resourceVerdict = null (not a synthesized { status: undefined } or a fabricated 'green'). Each null ⇒ the gate passes + warns — it surfaces a warning the operator sees, it does not block on its own.
  • forcedFail is reachable end-to-end (NICE-c). When CI is red OR resource is critical, verdict.suitable === false REGARDLESS of confidence (the engine words the rationale FORCED: CI red / FORCED: resource critical), so even under autonomous-gated the launch falls to inform + ask. Reachability depends on the dispatcher wiring the live signals through (CI wrapped as { status }, the real resource verdict string) rather than masking them — see skills/dispatcher/SKILL.md § Phase 1.5.

This gate does NOT change the loop. Autopilot remains opt-in, and the kill-switch contract is unchanged — the verdict gate only governs HOW the loop is entered (auto vs. confirm).

Telemetry

One record per /autopilot invocation, written to .orchestrator/metrics/autopilot.jsonl via atomic tmp + rename. See "Autopilot Loop" (#277; archived in the private Meta-Vault) § Output for the full schema.

Each iteration's sessions.jsonl entry gets an additional optional field autopilot_run_id (string or null) so retros can join across the two files without schema changes.

Manual sessions write autopilot_run_id: null (or omit the field — both treated identically by readers per the v1 schema additive convention).

Integration with Other Skills

  • mode-selector.mjs::selectMode — sole source of mode + confidence per iteration. Autopilot does not implement its own mode logic; v1.x quirks affect autopilot exactly as they affect manual session-start.
  • resource-probe.mjs::probe + evaluate — extended in Phase C-2 with swap and memory_pressure signals. Existing consumers (manual session-start, wave-executor) benefit from the new signals automatically.
  • session-start / session-plan / wave-executor / session-end — invoked unmodified. Autopilot is a controller around the existing session lifecycle, not a replacement.
  • session-registry.mjs — peer-count signal source. Autopilot reads but does not write to the registry beyond the standard hook.
  • mode-selector-accuracy — autopilot iterations write accuracy learnings exactly like manual sessions (Phase B-4 contract). The chosen field reflects autopilot's auto-execute decision, which equals recommendation.mode when confidence ≥ threshold.

Critical Rules

  • Never auto-merge or auto-push beyond /close defaults. /close already pushes to origin; autopilot does not add PR creation, merge, or force-push behavior.
  • Iteration boundaries are atomic. Never abort a running session to start a new one.
  • Kill-switches checked AFTER each session. Even if a kill-switch will fire after iteration N, iteration N completes cleanly first.
  • Iteration 1 sub-threshold falls back to manual; iteration 2+ exits with kill-switch. This asymmetry is intentional — see PRD Q8.
  • autopilot.jsonl is the SOLE writer's responsibility of autopilot.mjs. Other skills must not append to or rewrite this file.
  • Mode-Selector contract is read-only here. Autopilot does not modify selectMode output, does not re-rank alternatives, does not patch confidence values.

Anti-Patterns

  • Do not invoke /autopilot from inside a running session. The skill is a top-level command; nested invocation is undefined behavior.
  • Do not modify autopilot.jsonl schema additively without bumping schema_version. Readers MUST treat unknown fields as a forward-compat signal, not as corruption.
  • Do not bypass selectMode to force-run a specific mode. If you want to run a specific mode, use /session [mode] manually — that is autopilot's fallback path.
  • Do not lower --confidence-threshold below 0.5 in production. The Mode-Selector fallback table treats < 0.5 as suggestion-only; autopilot at that threshold becomes a random-walk over modes.
  • Do not implement kill-switch logic in this skill. Kill-switch enforcement is in scripts/lib/autopilot.mjs. The skill documents the contract; the runtime enforces it.

Configuration

Single-story /autopilot takes no Session Config block. Multi-story (autopilot.bg-isolation, --multi-story, --deconflict-paths) was removed in 4.0.0 — see docs/migration-v4.md and docs/session-config-reference.md § "Autopilot Multi-Story (#431) — removed".

References

  • PRD: "Autopilot Loop" (#277; archived in the private Meta-Vault)
  • Implementation (Phase C-1 + C-1.b): scripts/lib/autopilot.mjs — exports runLoop, parseFlags, writeAutopilotJsonl, KILL_SWITCHES, FLAG_BOUNDS, SCHEMA_VERSION, DEFAULT_PEER_ABORT_THRESHOLD, DEFAULT_JSONL_PATH, DEFAULT_CARRYOVER_THRESHOLD
  • Tests (Phase C-1 + C-1.b): tests/lib/autopilot.test.mjs
  • Command file: commands/autopilot.md
  • Mode-Selector contract: skills/mode-selector/SKILL.md
  • Resource probe: scripts/lib/resource-probe.mjs
  • Session registry: scripts/lib/session-registry.mjs
  • Wave-executor return shape: skills/wave-executor/SKILL.md § Return Shape Contract
  • Sessions.jsonl writer: skills/session-end/session-metrics-write.md
  • Pre-loop verdict gate (#682): scripts/lib/autonomy/suitability.mjs (computeSuitabilityVerdict) · scripts/lib/config/dispatcher-autonomy.mjs (resolveDispatcherAutonomy) · scripts/lib/autopilot/recent-runs.mjs (readRecentAutopilotRuns) · skills/dispatcher/SKILL.md § Phase 1.5
  • Epic: #271 v3.2 Autopilot — Autonomous Session Orchestration
  • Issues: #277 Phase C scaffold, #295 Phase C-1 runtime, #300 Phase C-1.b follow-up
  • Phase A PRD: "STATE.md Recommendations Contract" (#271; archived in the private Meta-Vault)
  • Phase B PRD: "Mode Selector" (#276; archived in the private Meta-Vault)

Open Questions (Phase C-1 to resolve)

  • --confidence-threshold=auto — let autopilot self-tune from accumulated mode-selector-accuracy learnings? Requires ≥ 20 accuracy learnings before useful.
  • STATE.md autopilot-active: true field — should other Claude sessions detect via the session-registry and refuse to start during an autopilot run? Dogfooding will inform.
  • failed-wave granularity — distinguish "agent failed but was retried successfully" from "wave ended with un-recovered failures"? Requires wave-executor schema audit.

Signals

GitHub stars
50
Forks
7
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
autopilot-kanevry
Source
github.com/kanevry/session-orchestrator