
Aep Autopilot
- 50 installs
- 14 repo stars
- Updated July 31, 2026
- memorysaver/agentic-engineering-patterns
Helps with ai & agent building tasks.
About
aep-autopilot is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- aep-autopilot
- AI & Agent Building
- AI-coding skill
Aep Autopilot by the numbers
- 50 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #7,298 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/memorysaver/agentic-engineering-patterns --skill aep-autopilotAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 50 |
|---|---|
| repo stars | ★ 14 |
| Last updated | July 31, 2026 |
| Repository | memorysaver/agentic-engineering-patterns ↗ |
What it does
Helps with ai & agent building tasks.
Files
Autopilot
One command to go autonomous. Initializes state, runs the first tick, and keeps itself ticking until the current layer is complete — all in one invocation. The default driver is goal-driven (/goal, native on both Claude Code and Codex): each tick advances the layer and then the runtime decides whether to re-fire based on a completion condition, so autopilot stops on its own when the layer is done or a human-judgment gate is hit. The fixed-interval /loop driver remains available as a fallback (--loop).
/aep-autopilot # start: goal-driven driver (default) — drives the CURRENT LAYER to completion, then stops
/aep-autopilot --loop 10m # start with the fixed-interval loop driver instead (custom interval)
/aep-autopilot --floor 3m # goal driver with a custom per-tick wait floor (default 5m)
/aep-autopilot status # check progress and escalations
/aep-autopilot stop # gracefully stop the driverScope is one layer per run. The goal driver completes when every story in
the current layer is merged + wrapped, then hands control back so the human
can run the layer gate //aep-reflectand re-invoke/aep-autopilotfor the
next layer. This mirrors the existing pause-at-gate behavior with crisp
termination instead of an infinite loop.
Where this fits:
/aep-envision → /aep-map → /aep-validate
→ /aep-autopilot (goal: "layer N complete")
┌─────────────────────────────────────────────┐
│ tick ① read state │
│ tick ② sync signals │
│ tick ③ wrap completed workspaces │
│ tick ④ GUIDE COMPLETION (quality + merge) │
│ tick ⑤ detect stuck workspaces │
│ tick ⑥ dispatch new work (/aep-launch) │
│ tick ⑦ write state + SURFACE status + WAIT │
│ post-merge-guard monitor deploy health, │
│ revert regressions │
└─────────────────────────────────────────────┘
│ goal evaluator reads the surfaced status line:
│ "is layer N complete, or is autopilot paused?"
├─ no → re-fire next tick (after the wait floor)
└─ yes / paused → STOP (hand back to human)
→ /aep-reflect (after layer completes or autopilot stops)Session: Main session only (never from a feature workspace) State: .dev-workflow/autopilot-state.json (machine-readable), .dev-workflow/autopilot-status.md (human-readable)
---
STOP — Orchestrator Boundaries
Read this section FIRST. It overrides everything below.
You are an ORCHESTRATOR, not an EXECUTOR. All code operations happen inside workspace agents. The main session never reads, reviews, edits, or evaluates workspace code directly. The single most common autopilot failure is violating this boundary.
Executor mode + driver (read once, applies throughout)
Autopilot steers running workspace workers, so it requires a steerable mode whose lifetime is compatible with the driver (see the driver × backend matrix in .claude/skills/aep-executor/references/backends.md). Every "send to workspace" action in this skill is executor.nudge(ws, msg), and every liveness probe is executor.liveness(ws) — the table below is the per-mode implementation of those verbs. Nudge texts shown in the tick protocol are mode-independent; deliver them through your mode's transport.
| Op | native-bg-subagent | claude-bg | codex-subagent | codex-exec | legacy |
|---|---|---|---|---|---|
nudge | SendMessage(to: agentId, msg) + feedback.md | append feedback.md; hard-stuck ≥6 ticks → stop+respawn (recovery prompt) | send_input(<id>, msg) | codex exec resume <id> "<msg>" | tmux send-keys -l + separate Enter |
liveness | TaskList + worktree-activity probe (never roster) | claude agents --json + claude logs <id> tail + git diff | list_agents + git diff | signals + worker.log tail + git diff | capture-pane hash + git diff |
present | TaskOutput <agentId> | claude attach <id> | thread list / /agent | signals + PR | cmux tab / tmux attach |
- Driver compatibility: session-bound workers (native-bg-subagents,
codex-subagents) die with the orchestrator session. Long-lived driver — the goal driver (/goal, default) or the fixed-interval /loop, both running in a living Claude Code session or Codex main thread → any steerable mode. Cron/launchd driver (fresh session per tick) → OS-bound modes only (claude-bg, codex-exec, legacy) and only via `/loop` or an external scheduler — /goal is inherently in-session and cannot drive a fresh-session-per-tick scheduler, so unattended OS-scheduled runs keep using the /loop/codex exec path.
- Post-spawn liveness probe (all modes): after every launch, before counting
the worker as running, confirm the process/agent exists and the worktree shows activity (status.json written or non-empty git diff) within N seconds — scripts/spawn-liveness-probe.sh. On failure, tear the dead spawn down and auto-fall-back to native-bg-subagent. Never treat "state says active" as liveness (the removed claude-team failed exactly here — see docs/decisions/remove-claude-team.md).
- Orphan re-adoption (session-bound modes, by real liveness): if the liveness
probe fails for a workspace state lists as active (agent gone from TaskList / list_agents, or worktree inactive — even if state still says active), it is an orphan, not a failure — re-spawn a worker into the existing worktree with the recovery bootstrap (protocol in aep-executor/references/backends.md).
- workflow / headless (no mid-stage surface): autopilot's tick/nudge
model does not apply. Hands-free batch under Claude Code is the workflow path reached via /aep-dispatch … with workflow, which is its own orchestrator — not something autopilot drives (its human gates park and return to the main agent that authored the workflow, not to autopilot). If detection yields only workflow/headless, report that autopilot needs a steerable mode and stop.
- tmux nudge form (legacy only): multi-line nudges are
tmux send-keys -t <ws>:0.0 -l -- "<msg>" then tmux send-keys -t <ws>:0.0 Enter — a bare send-keys "<msg>" Enter would submit line-by-line.
The tick CHECK runs in a cheap delegate (token isolation)
Each tick splits into CHECK (read + analyze + write state) and ACT (execute). The CHECK is delegated to a cheap, context-isolated agent via executor.check() (Claude Code: a Haiku subagent; Codex: a codex exec cheap one-shot) so the long-lived orchestrator session doesn't accumulate per-tick reading. The CHECK reads only autopilot-state.json + workspace signals/ + gh pr view — never workspace code — and returns a compact action list; the orchestrator then ACTs on it (nudge / wrap / launch / escalate).
This is NOT a violation of the next rule. The CHECK delegate is the
orchestrator offloading its own _signal reading and bookkeeping_ to a cheap
context. It never reads workspace code and never reviews code. The forbidden
thing is spawning a code reviewer (which would read the implementation) from
main — that stays forbidden. Signals-only analysis ≠ code review.
Never Do List
- NEVER use the Agent tool to spawn code reviewers from the main session — this is categorical, not a context problem you can engineer around. Even a worktree-bound reviewer spawned from main pulls workspace code and quality judgments into the orchestrator's context, which is exactly what the boundary forbids; "but I could give it the worktree" is not a valid exception. Instead:
executor.nudge()to trigger the workspace's own gen/eval loop. (Spawning a builder bg subagent/worker at launch and steering it withSendMessage(to: agentId)/send_inputis the nudge transport, not a reviewer spawn — the evaluator is always spawned by the _generator inside the workspace_, never by autopilot.) - NEVER call `gh pr merge` — workspace agents run pre-merge checks (rebase, CI verification, comment resolution) as part of Phase 12. Merging from main bypasses these checks and has caused premature merges where incomplete test results were accepted. Instead:
executor.nudge()telling the workspace to complete Phase 12. - NEVER read workspace source files — only read signal files under
.dev-workflow/signals/. The main session's job is to observe progress via signals, not to understand the code. If you need code reviewed, trigger the workspace's evaluator. - NEVER use `Read`, `Grep`, or `Bash` to inspect workspace code — even "just checking" pulls implementation details into main session context, which leads to the main session forming opinions about code quality and then acting on them (spawning reviewers, suggesting fixes). Stay out of workspace code entirely.
- NEVER write eval-response files — evaluation integrity depends on separation between generator and evaluator. The main session is neither — it's the orchestrator. Writing eval responses breaks the trust model.
If you are about to do any of the above: STOP. Send the instruction to the workspace agent via `executor.nudge()` instead.
Allowed Actions (from main session)
| Action | How |
|---|---|
| Read workspace status | Read .feature-workspaces/<name>/.dev-workflow/signals/status.json |
| Trigger code review | executor.nudge(<ws>, "<trigger text>") — per-mode transport table above |
| Send feedback | Write to .feature-workspaces/<name>/.dev-workflow/signals/feedback.md |
| Nudge stuck agent | executor.nudge(<ws>, "<nudge text>") |
| Surface a human gate | Read signals/needs-human.md; relay the human's answer via the mode's transport |
| Check PR state | gh pr view <number> --json state (observe only — never act on merge) |
Forbidden Actions (from main session)
| Forbidden action | Do this instead |
|---|---|
| Read workspace code | Trigger workspace's gen/eval via executor.nudge() |
| Spawn review agents | Send the review trigger via executor.nudge() |
| Run tests | Workspace handles its own test phases |
| Edit workspace files | Send instructions via executor.nudge() or feedback.md |
| Evaluate code | Monitor eval-response files for results |
| Merge PRs | Workspace agent merges via Phase 12 |
Two Gen/Eval Concerns — Strictly Separate
| Concern | Owner | What it evaluates | Where it runs |
|---|---|---|---|
| Code quality | Workspace agent | Code correctness, security, completeness | Inside the workspace worker |
| Orchestration learning | Autopilot (main session) | Patterns across workspaces: failures, costs, retries | Main session, feeds into /aep-reflect |
The autopilot does NOT evaluate workspace code. It triggers and monitors the workspace's own gen/eval loop. See references/review-trigger.md for detection logic and references/orchestration-learning.md for meta-learning.
---
/aep-autopilot (default — start)
Initialize autopilot, run the first tick, and start the driver (goal-driven by default; --loop for the fixed-interval fallback). This is a single command — no second step needed.
Usage:
/aep-autopilot # default: goal driver — drives the current layer to completion, then stops
/aep-autopilot --floor 3m # goal driver, custom per-tick wait floor (default 5m)
/aep-autopilot --loop 10m # fixed-interval loop driver instead (custom interval) — the fallback
/aep-autopilot --loop 3m # loop driver, faster for active developmentDriver selection rule: the presence of --loop <interval> selects the fixed-interval loop driver; its absence selects the goal driver (the default). --floor <dur> only applies to the goal driver (the bounded per-tick wait, default 5m); --max-turns <n> caps the goal driver as a runaway backstop (default 200).
Prerequisites
# 1. Must be on main workspace (not inside a feature workspace)
pwd | grep -q '.feature-workspaces' && echo "ABORT: Run from main workspace only" && exit 1
# 2. Product context must exist
[ -f product-context.yaml ] || echo "ABORT: Run /aep-envision and /aep-map first"
# 3. Autonomous routing must be enabled
# Check topology.routing.autonomous: true in product-context.yamlVerify these conditions before proceeding:
- Main workspace guard:
pwdmust NOT contain.feature-workspaces - Product context exists:
product-context.yamlmust exist with astoriessection - Autonomous enabled:
topology.routing.autonomous: truemust be set - Stories available: At least one story must be
readyorin_progress - Validated: Product context should have passed
/aep-validate(both passes)
full_auto — strategic master switch
topology.routing.full_auto (default false) is the master switch over the strategic human gates — the "what to build" / architecture layer. With the default, those gates stay with the human:
- `full_auto: false` (default): strategic pauses hold — ambiguous / low-readiness
stories escalate to a human for design (the design-escalation pause below), and the qualitative outcome-contract evaluation pauses for human judgment before a layer advances.
- `full_auto: true` (explicit opt-in only): those strategic pauses auto-proceed
via agent judgment instead of waiting for a human.
full_auto sits above the finer-grained flags under topology.routing (auto_design, auto_outcome_eval, watch.auto_create): full_auto: true implies all of them. The default keeps humans in control of the strategic layer; turning full_auto on removes those pauses only when the user explicitly opts in. See the per-flag behavior in Design Escalation below and in aep-dispatch (readiness-based routing).
Start Protocol
1. Create .dev-workflow/ if it doesn't exist:
mkdir -p .dev-workflow2. Initialize .dev-workflow/autopilot-state.json — see references/state-schema.md for the full schema:
{
"version": 1,
"status": "running",
"started_at": "<ISO8601>",
"last_tick_at": null,
"tick_count": 0,
"workspaces": {},
"escalations": [],
"stats": {
"stories_completed": 0,
"stories_failed": 0,
"total_ticks": 0,
"total_cost_usd": 0
}
}3. Write initial .dev-workflow/autopilot-status.md:
# Autopilot Status
**Status:** Running
**Started:** <timestamp>
**Tick count:** 0
## Active Workspaces
None yet.
## Next Action
First tick will sync signals and dispatch work.4. Resolve the launch mode + driver pair (executor detect() + the driver × backend matrix). On Claude Code the default is native-bg-subagent (no team to create — if any agent-teams team is active, TeamDelete it first, since a live team poisons teamless background spawns). Announce the pair, e.g. "native-bg-subagent + /goal" or "codex-exec + launchd".
5. Run the first tick immediately (see tick protocol below).
6. Start the driver. The default is the goal driver; --loop selects the fixed-interval loop driver. Both keep the orchestrator long-lived (so any steerable mode works); the goal driver additionally self-terminates when the layer is done. The tick body (the 7-step CHECK→ACT protocol below) is the same under either driver — only how the next tick is triggered differs.
6a. Goal driver (default)
Build the goal condition for the current layer and hand it to the host's native /goal primitive. The condition is the success predicate the goal evaluator judges each turn — against the status line the tick surfaces (signals only — never workspace code) — plus a one-line per-turn directive:
Layer <N> of this product is COMPLETE: every story in layer <N> is
status=completed AND its worktree has been wrapped (none remain under
.feature-workspaces/), as shown by the AUTOPILOT status line surfaced this
turn — OR autopilot has entered status=paused requiring human input (design
ambiguity, layer-gate failure, outcome contract, or repeated failure), as
shown by the same status line. Judge ONLY from the surfaced status line,
never from memory. Each turn, run exactly ONE `/aep-autopilot tick`, then end
the turn; never run more than one tick per turn. Stop after <max-turns> turns
if the layer has not completed.- Claude Code (`/goal`, requires v2.1.139+):
/goal <the condition above>/goal starts a turn immediately and, after each turn, a small fast model (Haiku) checks the condition against the conversation; "no" auto-starts the next turn with the evaluator's reason as guidance, "yes" clears the goal and stops. Pair with auto mode so each turn runs unattended. The per-tick wait floor (step ⑦) is what prevents hot-looping — without it the evaluator re-fires the instant a turn ends. (The evaluator reads only the surfaced status line, so the orchestrator boundary holds: it never sees workspace code.)
- Codex (`goals` feature, experimental — enable with `--enable goals`):
/goal <the condition above>Set a token_budget as the hard runaway wall (on exhaustion Codex soft-stops to budget_limited with a wrap-up steer rather than dying). Codex continues the goal only when the thread is idle, the goal is active, and it is within budget; /goal pause · /goal resume · /goal check · /goal clear manage it.
Under either host, keep delegating each tick's CHECK to a cheap context-isolated agent (Haiku subagent / codex exec one-shot) — the goal session is long-lived, so the token-isolation reason from "Execution model" still holds.
6b. Loop driver (fallback — --loop <interval>)
The fixed-interval driver, unchanged from earlier versions. Use it for hosts without /goal, for fully-unattended OS-scheduled runs (cron/launchd — /goal is in-session-only), or when you simply want a fixed cadence. The loop driver does not self-terminate; stop it with /aep-autopilot stop.
Claude Code — `/loop` (GA, in-session, long-lived):
/loop <interval> /aep-autopilot tickWhere <interval> is from --loop flag (default: 5m). /loop invokes /aep-autopilot tick each interval; the tick keeps the main session cheap by delegating its CHECK to a Haiku subagent. Like the goal driver it keeps the session alive, so session-bound modes (native-bg-subagent) work under it.
Codex — two driver options:
- In-thread (long-lived): the orchestrator is a living main thread
(desktop app or interactive CLI) that runs /aep-autopilot tick on a cadence (self-paced or user-prompted). Supports codex-subagent — workers are this thread's subagents, steerable via send_input, visible as threads.
- Scheduled (ephemeral): no native
/loop; schedulecodex exec
externally — each tick is a fresh cheap one-shot (already context-isolated, so no nested CHECK needed). Workers must then be codex-exec (OS-bound; a fresh tick session steers them via codex exec resume). Recommended: a macOS launchd agent with StartInterval=300 (or cron / a while … sleep 300 loop) running:
codex exec -m gpt-5.4-mini -c model_reasoning_effort=low \
-C "$PWD" --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox \
"/aep-autopilot tick" < /dev/null # < /dev/null: exec hangs on stdin otherwiseTell the user the exact scheduler snippet for their platform; AEP does not install it for them.
---
/aep-autopilot tick
The per-tick handler invoked by the driver (goal or loop). Can also be run manually at any time. Idempotent — safe to run multiple times with no state change producing no duplicate actions.
Execution model — CHECK → ACT. A tick is two halves:
- CHECK (cheap, isolated): run the read + analyze + write-state work via
executor.check(prompt, schema) — a Haiku subagent (Claude Code) or a codex exec cheap one-shot (Codex). It reads autopilot-state.json + every workspace signals/ + gh pr view, computes transitions / stuck / dispatch capacity, writes the updated `autopilot-state.json` + `autopilot-status.md`, and returns the compact action list (schema in aep-executor/references/backends.md: {summary, state_written, actions:[{type, workspace, story_id, message, reason}]}). All the token-heavy reading stays in the throwaway agent.
- ACT (orchestrator): execute the returned
actions—nudgevia
executor.nudge(), wrap via /aep-wrap (max one per tick), launch via /aep-launch (max one per tick), escalate/design per the pause protocol. These are few, so the main session stays cheap.
On Codex the whole tick already runs as an isolated cheap codex exec, so theCHECK can run inline (no nested executor.check needed) — the ACT still applies.On Claude Code the long-lived /loop session is exactly why the CHECK isdelegated to a Haiku subagent.
The 7-step protocol below is the content of the CHECK prompt (steps ①②④a④b-detect ⑤⑥-scoring ⑦ = analysis + state write) plus the ACT items it emits (③ wrap, ④b/④c nudges, ⑥ launch, escalations). Full detail in references/tick-protocol.md.
Post-merge guard: after a story wraps and merges, a post-deploy guard step
monitors deploy health and can revert regressions — see
references/post-merge-guard.md.Before every tick, re-read the "STOP — Orchestrator Boundaries" section above.
Summary (annotated `[CHECK]` analysis vs `[ACT]` orchestrator action):
① READ STATE [CHECK] → read .dev-workflow/autopilot-state.json
- Exit if status != "running"
- Exit if tick lock active (previous tick still running)
- Set tick lock
② SYNC SIGNALS [CHECK] → for each workspace in state:
- Read .feature-workspaces/<name>/.dev-workflow/signals/status.json
- Update workspace entry in state (phase, story_status, completion_pct, pr_url, blockers)
- blocked_on == "human" OR needs-human.md has an unresolved entry → emit an
`escalate` action (type human_gate) with the mode-specific answer recipe;
do NOT count the workspace as stuck while gated
- ORPHAN CHECK (session-bound modes): state says active but the agent is gone
(TaskList / list_agents empty for it) → emit a `launch` action that re-spawns
into the EXISTING worktree with the recovery bootstrap (re-adoption, not failure)
③ WRAP COMPLETED [ACT] → for each workspace where story_status == "completed":
(CHECK emits a `wrap` action per completed workspace; orchestrator runs it)
- Run /aep-wrap for this workspace (max ONE per tick — git operations serialize)
- Remove workspace from state after wrap completes
- Break to step ⑦
④ GUIDE COMPLETION [CHECK detect → ACT nudge] → for each workspace, guide toward quality and merge:
(CHECK reads PR/eval state and decides; orchestrator sends the `nudge` actions)
ALL NUDGE ACTIONS USE executor.nudge() — per-mode transport table above
(SendMessage / feedback.md / send_input / exec resume / tmux send-keys).
NEVER spawn code reviewers. NEVER call gh pr merge. Workspace agents own merging.
Decision tree:
has pr_url? → ④a (check state) → OPEN? → ④b (quality gate) → PASS? → ④c (nudge merge)
no pr_url, phase >= 5? → ④b (quality gate) → PASS? → leave alone (workspace creates PR)
phase < 5? → skip
④a. CHECK PR STATE — for workspaces with pr_url set:
- gh pr view <number> --json state
- MERGED → update story_status to "completed" (Step ③ wraps next tick)
- CLOSED → update story_status to "failed"
- OPEN → proceed to ④b/④c
④b. QUALITY GATE — for ALL workspaces at phase >= 5 (pre-PR and post-PR):
- Check for eval-response files in .feature-workspaces/<name>/.dev-workflow/signals/
- If no eval-response with PASS exists → trigger gen/eval via executor.nudge(<ws>):
"Run Phase 5 code review now. Write eval-request.md, spawn the
evaluator via executor.spawn_evaluator (your mode's recipe), and
execute the gen/eval loop per the build skill Phase 5 protocol."
- If stuck at Phase 5 (2+ ticks) → re-trigger via executor.nudge()
- No response after 6 ticks (30 min) → add escalation
- See references/review-trigger.md for full detection logic
④c. GUIDE TO MERGE — when eval PASSED AND pr_url set, nudge toward Phase 12:
- Only nudge ONCE — skip if last_action is already "merge_nudged"
- If eval PASSED but phase < 12 and not yet nudged → executor.nudge(<ws>):
"Your code review eval has PASSED. Proceed to Phase 12: run pre-merge
checks (rebase on the integration branch, verify CI, check comments) then merge the PR.
In autopilot mode, merge when all checks pass without waiting for user
confirmation."
- If phase == 12 and stuck (2+ ticks) → executor.nudge(<ws>):
"Complete Phase 12 merge now: 1) git fetch origin && git rebase origin/\"$(git config --get aep.integration-branch 2>/dev/null || (git show-ref --verify --quiet refs/remotes/origin/develop && echo develop || echo main))\" &&
git push --force-with-lease origin feat/<name> 2) Verify CI green
3) gh pr merge <number> --squash --delete-branch.
Update status.json with story_status completed."
- If phase == 12 and progressing → leave alone
⑤ DETECT STUCK [CHECK detect → ACT nudge] → for each workspace:
- Compare (phase, completion_pct) with previous tick
- No change → run executor.liveness() (mode table above), then increment consecutive_stuck_ticks
- Changed → reset to 0
- blocked_on == "human" → not stuck; it's a gate (handled in ②)
- 6 ticks (30 min) stuck → executor.nudge() (claude-bg: this is the stop+respawn threshold)
- 12 ticks (60 min) stuck → add escalation, consider pausing
⑥ DISPATCH NEW WORK [CHECK score → ACT launch] → if capacity available:
- Read product-context.yaml, run dispatch scoring logic (steps 1-3 from /aep-dispatch)
- available_slots = concurrency_limit - active_workspace_count
- WAVE ORDERING: Dispatch Wave 1 before Wave 2 within each layer.
- LAYER GATE: After completing all stories in a layer, check if a `.5` alignment
layer exists for this layer. If yes, dispatch `.5` layer stories before
advancing to the next integer layer.
- Verify calibration artifacts exist before dispatching `.5` stories
(check `calibration/<type>.yaml` or legacy `design-context.yaml`)
- If missing → add escalation requesting the user to run `/aep-calibrate <type>`
- OUTCOME CONTRACT: If layer has outcome_contract, pause for /aep-reflect evaluation
before advancing to next layer.
- GROUPED CHANGES: If top story has compile_mode: grouped_change, dispatch
the entire change_group as one unit (one workspace, one PR).
- For top-scored ready story (or group):
- Route by readiness_score: >=0.7 → /aep-launch, <0.5 → escalate or auto-design
- If auto_design: true → route through /aep-design automatically (no pause)
- If auto_design: false and readiness < 0.7 → add escalation, PAUSE autopilot
- If attempt_count >= 2 → always ESCALATE
- If well-specified → run /aep-launch (max ONE launch per tick)
- Add new workspace entry to state (with story_ids, wave, readiness_score)
⑦ WRITE STATE + SURFACE + WAIT [CHECK, then driver tail]
- Write .dev-workflow/autopilot-state.json (atomic: write .tmp then rename)
- Append tick summary to .dev-workflow/autopilot-history.jsonl
- Update .dev-workflow/autopilot-status.md
- Increment tick_count, set last_tick_at
- Release tick lock
- SURFACE the AUTOPILOT status line into the transcript (GOAL DRIVER ONLY) —
signals-only, so the goal evaluator can judge "layer complete? paused?"
- WAIT the per-tick floor before ending the turn (GOAL DRIVER ONLY) — the
anti-hot-loop floor (default 5m, `--floor`). CC → Monitor with a hard
timeout (a raw foreground sleep is blocked); Codex → shell sleep.
Under the loop driver, neither the surface nor the wait runs (the `/loop`
interval is the cadence).---
/aep-autopilot status
Read and display the current autopilot state.
cat .dev-workflow/autopilot-status.mdAlso parse .dev-workflow/autopilot-state.json and present:
- Status: running / paused / stopped
- Uptime: since started_at, tick count
- Active workspaces: table with name, story_id, phase, completion_pct, last_action
- Pending escalations: each with type, story_id, reason
- Stats: stories completed, failed, total cost
- If paused: Why paused, what human feedback is expected, how to resume
---
/aep-autopilot stop
Gracefully stop the autopilot and cancel the active driver (goal or loop).
1. Set status: "stopped" in .dev-workflow/autopilot-state.json 2. Update .dev-workflow/autopilot-status.md with stopped state 3. Log stop event to .dev-workflow/autopilot-history.jsonl 4. Cancel the driver:
- Goal driver: clear the active goal —
/goal clear(Claude Code; aliases
stop/off/reset/none/cancel) or /goal clear (Codex). No further turn re-fires. (The goal driver also self-clears when the layer completes, so stop is mainly for early termination.)
- Loop driver: cancel the
/loop(use the loop skill's cancel mechanism),
or remove the cron/launchd job for an OS-scheduled run. 5. native-bg-subagent: background subagents are session-bound — they stop when the orchestrator session ends. To stop early while preserving work, let each in-flight worker reach a phase boundary (or TaskStop <agentId>); all state lives in the worktree + .dev-workflow/, so a later session re-adopts the orphan via the liveness probe. No team to delete.
What happens:
- The active driver is cancelled — no more ticks
- Running workspaces continue autonomously (they don't depend on autopilot)
What does NOT happen:
- Workspaces are NOT killed — they continue their
/aep-buildflow (caveat:
session-bound workers do depend on the lead session staying open — closing the session orphans them; on the next /aep-autopilot start they are re-adopted per the orphan protocol)
- Product context is NOT modified
- No wraps or merges are triggered
Autopilot stopped. Active workspaces continue running independently.
To resume: /aep-autopilot---
Design Escalation
When autopilot encounters a story that needs design input, behavior depends on topology.routing.auto_design:
- `auto_design: false` (default): Autopilot pauses entirely — design decisions may affect other stories and require human judgment.
- `auto_design: true`: Autopilot routes the story through
/aep-designautomatically, then/aep-launch. No pause.
Escalation Conditions (when auto_design: false)
A story triggers design escalation when ANY of:
1. `readiness_score < 0.7` — spec is not dispatch-ready (fewer than 3 acceptance criteria, missing interface obligations, unresolved open questions, etc.) 2. `attempt_count >= 2` — repeated failures suggest the spec is insufficient, not the implementation (always escalates, even with auto_design: true)
Auto-Design Conditions (when auto_design: true)
Instead of pausing, autopilot:
1. Runs /aep-design for the story to refine the spec 2. Re-computes readiness_score after /aep-design 3. If readiness >= 0.7 → /aep-launch 4. If readiness still < 0.5 → escalate (auto-design couldn't resolve the ambiguity)
Pause Protocol
When escalation triggers:
1. Set status: "paused" in .dev-workflow/autopilot-state.json 2. Add escalation entry with detailed context:
{
"type": "design_needed",
"story_id": "PROJ-010",
"reason": "Complexity L with 1 acceptance criterion, UI-heavy activity 'Settings'",
"details": "The story 'Add settings page' lacks specificity...",
"expected_human_action": "Run /aep-design PROJ-010 to refine the spec...",
"created_at": "<ISO8601>",
"acknowledged": false
}3. Write .dev-workflow/autopilot-status.md with:
- Why paused — the specific story and condition
- What needs human attention — what's ambiguous, what decisions require human judgment
- Expected human feedback — specific actions (run /aep-design, add acceptance criteria, etc.)
- Current state — active workspaces still running, stories completed, what's blocked
- Detailed guidelines — why this story can't be auto-designed (e.g., "UI layout decisions require visual design judgment that the agent cannot make autonomously")
4. Log to .dev-workflow/autopilot-history.jsonl
Resuming After Pause
After the human resolves the design issue:
/aep-autopilotThis re-reads the product context (now with refined specs) and re-initializes the driver — under the goal driver it sets a fresh goal for the current layer; under the loop driver it restarts the /loop — then resumes ticking.
---
Guardrails
- Main workspace only — refuse to run if
pwdcontains.feature-workspaces - All code operations happen inside workspace agents — the main session NEVER reads, reviews, edits, or evaluates workspace code directly. It only sends instructions via
executor.nudge()and reads signal files - Never spawn Agent tools for code review — all reviews run inside the workspace worker, triggered via
executor.nudge(). This is the #1 violation to watch for. - Never merge PRs — workspace agents own Phase 12 merge; autopilot only detects already-merged PRs
- Guide workspace agents to merge — when eval passes and CI is green, nudge for Phase 12 completion via
executor.nudge(); do not wait passively for workspace to figure it out - Never dispatch stories with unmet dependencies — even under autonomous mode
- Never treat SKIP-only test results as PASS — at least 1 PASS required for test/integration stories
- Never treat "no checks" as passing — for integration/test stories, require at least one passing check OR explicit eval-response PASS
- Never write eval-response files — that's the workspace evaluator's job
- One wrap per tick — wraps involve git operations that must serialize
- One launch per tick — keeps tick duration under 60 seconds
- Respect WIP limits — never exceed
topology.routing.concurrency_limit - Atomic state writes — write to
.tmpthen rename to prevent corruption - Tick lock — prevent overlapping ticks via
tick_in_progresstimestamp - Goal driver is scoped to ONE layer — the goal condition completes (or
pauses) at the current layer boundary; never widen it to the whole backlog
- Goal evaluator sees signals only — the per-tick surfaced status line MUST
be signals-only (no workspace code, no file contents), preserving the orchestrator boundary; the evaluator never reads code
- Per-tick wait floor is mandatory under the goal driver — without it
/goal re-fires the instant a turn ends and hot-loops, burning tokens
- Always bound the goal —
--max-turns(default 200) and, on Codex, a
token_budget, so a non-converging layer can never run forever
- Pause on design ambiguity — unless
auto_design: true, escalate to human when readiness < 0.7 - Always escalate on repeated failures —
attempt_count >= 2always pauses, even withauto_design: true
---
Next Steps
After autopilot completes a layer or is stopped:
| Action | When |
|---|---|
/aep-reflect | After layer completes — evaluate outcome contracts (Step 2.75), classify feedback |
/aep-autopilot status | Anytime — check progress and escalations |
/aep-autopilot | After resolving a pause — resume the driver (re-sets the layer goal) |
/aep-dispatch | Manual mode — pick a specific story interactively |
Orchestration Learning
How the autopilot uses the gen/eval pattern to evaluate its own orchestration quality — not individual code, but cross-workspace patterns. This is the main session's gen/eval concern, strictly separate from workspace-level code evaluation.
---
Principle: Meta-Evaluation, Not Code Review
| Workspace gen/eval | Orchestration gen/eval |
|---|---|
| "Is this code correct?" | "Is our process working?" |
| Evaluates one story's implementation | Evaluates patterns across all stories |
| Runs inside the workspace worker | Runs in main session (Agent tool, Context B) |
| Triggered by autopilot, executed by workspace | Triggered and executed by autopilot |
| Feeds into: fix code → re-eval | Feeds into: /aep-reflect → update product context |
---
What to Observe
The orchestration learning protocol examines data from across all workspaces in the current autopilot run:
Completion Patterns
- Which stories completed successfully vs failed vs got stuck
- Average time-to-completion by complexity (S/M/L)
- Which modules have higher failure rates
Cost Analysis
- Cost per story (from
status.jsonsignals) - Cost per module — are some modules consistently expensive?
- Cost correlation with complexity rating — is L really 4x S?
Retry Patterns
- Which stories needed multiple attempts (
attempt_countfrom product-context.yaml) - Common failure reasons (from
failure_login signals) - Whether retries with fresh agents succeeded (fresh-agent-retry effectiveness)
Eval Convergence
- How many eval rounds per story (from
eval_rounds_completedin state) - Which scoring dimensions consistently fail (parsed from eval-response files)
- Whether certain feature types (UI, API, security) have worse convergence
Escalation Analysis
- Escalation frequency and causes
- How long before escalations were resolved
- Whether escalation conditions could have been predicted earlier
Time Patterns
- Time spent per phase (derived from signal
last_updatedtimestamps) - Bottleneck phases (where workspaces spend the most time)
- Correlation between context package size and completion time
---
When to Run
Orchestration learning runs at natural checkpoints:
1. Layer Complete
When all stories in a layer are completed and the layer gate passes. This is the primary learning checkpoint — a full cycle of dispatch-build-merge is done.
2. After Escalation
When an escalation is created. Examine: could this have been predicted? Should dispatch scoring or design escalation thresholds change?
3. On Autopilot Stop
When /aep-autopilot stop is called or all layers complete. Summary of the entire run.
---
How to Run
Use the gen/eval pattern's Context B: Parallel Agent Tool Calls from the main session:
Launch Agent(subagent_type="Plan", prompt="<orchestration evaluator prompt>")Evaluator Prompt Template
You are an ORCHESTRATION EVALUATOR. Analyze the autopilot run data and identify
patterns that should inform future product context and dispatch decisions.
## Data Sources
Read these files:
1. .dev-workflow/autopilot-state.json — current state with all workspace data
2. .dev-workflow/autopilot-history.jsonl — tick-by-tick audit trail
3. product-context.yaml — story specs, complexity ratings, dependencies
4. For each completed workspace:
- .feature-workspaces/<name>/.dev-workflow/signals/status.json (if still exists)
- .feature-workspaces/<name>/.dev-workflow/signals/eval-response-\*.md (if exists)
## Analysis Dimensions
1. **Accuracy of estimates:** Did complexity ratings (S/M/L) match actual effort?
2. **Spec quality:** Did stories with more acceptance criteria complete faster/more reliably?
3. **Module patterns:** Are certain modules consistently problematic?
4. **Eval effectiveness:** Did the gen/eval loop catch real issues or just slow things down?
5. **Dispatch efficiency:** Was the scoring formula (CP + value + unblock / complexity) optimal?
6. **Cost efficiency:** Where was money well-spent vs wasted?
## Output Format
Write findings to .dev-workflow/autopilot-learnings.md using this structure:
### Finding: [Title]
**Category:** [estimate_accuracy | spec_quality | module_pattern | eval_effectiveness | dispatch_efficiency | cost_efficiency]
**Evidence:** [specific data points]
**Recommendation:** [actionable change to product context or process]
**Severity:** [info | suggestion | important]---
Output Format
.dev-workflow/autopilot-learnings.md:
# Autopilot Learnings — Layer N
**Generated:** <timestamp>
**Stories analyzed:** N completed, N failed, N in-progress
**Total cost:** $XX.XX
---
## Findings
### Finding: Complexity L stories take 4x longer and fail 2x more than rated
**Category:** estimate_accuracy
**Evidence:** L stories averaged 3.2 hours vs S at 0.8 hours (4x, not 4x as rated).
L stories had 40% failure rate vs S at 20%. PROJ-007 and PROJ-012 both failed on
first attempt.
**Recommendation:** Consider splitting L stories into 2-3 M stories before dispatch.
Update /aep-map guidance to discourage L complexity.
**Severity:** important
### Finding: Auth module stories consistently fail on Security dimension
**Category:** module_pattern
**Evidence:** PROJ-003 and PROJ-008 both received Security score 2 on first eval
round. Both eventually passed after 3 rounds. No other module had security failures.
**Recommendation:** Add explicit security acceptance criteria to auth module stories
in product-context.yaml. Consider adding security-focused evaluator criteria preset
for auth stories.
**Severity:** suggestion
### Finding: Stories with 5+ acceptance criteria completed 30% faster
**Category:** spec_quality
**Evidence:** Stories with ≥5 criteria averaged 1.1 hours. Stories with exactly 3
criteria averaged 1.6 hours. Hypothesis: more criteria = less ambiguity = fewer
false starts.
**Recommendation:** During /aep-map, aim for 5+ criteria per story. Flag stories with
exactly 3 criteria for potential spec refinement.
**Severity:** info---
Integration with /aep-reflect
The learnings file is consumed by /aep-reflect during its feedback classification step:
1. /aep-reflect reads .dev-workflow/autopilot-learnings.md 2. Each finding is classified: Bug, Refinement, Discovery, or Opportunity Shift 3. Findings become updates to product-context.yaml:
- Estimate accuracy → update complexity ratings
- Spec quality → add acceptance criteria to future stories
- Module patterns → update module definitions in architecture section
- Dispatch efficiency → update topology.routing settings
- Cost efficiency → update cost.alerts thresholds
This closes the meta-learning loop:
/aep-autopilot runs → workspaces build → autopilot observes →
learnings → /aep-reflect → product-context updated →
next /aep-autopilot run benefits from improved contextPost-Merge Guard Protocol
The post-merge monitoring window that runs after a story is merged and wrapped. Today autopilot wraps a merged story and forgets it; this guard keeps watching the deployed result for a bounded window, runs the host-aware dogfood against the live environment, and — only when explicitly enabled — can revert a hard service regression. It is the safety net that makes unattended autonomy survivable: the difference between "merged and walked away" and "merged, verified the deploy is healthy, and rolled back if it wasn't".
BOUNDARY REMINDER: This step is an orchestrator action, identical in posture to the rest of the tick. It reads CI/health signals, reads dogfood reports, and runsgh/ deploy / CLI commands — it NEVER reads workspace source code, NEVER spawns reviewers or evaluators from main, and NEVER forms code-quality opinions. The dogfood itself runs viadogfood_method()(seedogfood-validation.md) using the host's native browser tooling, producing a signals-only report the orchestrator consumes. See SKILL.md "STOP — Orchestrator Boundaries".
---
Where this runs
The guard is a post-deploy step that runs after Step ③ wrap in the tick protocol. When a story is merged (④a detects MERGED) and wrapped (③ removes its worktree), the story is not forgotten: its guard_state is opened and subsequent ticks drive it through the monitoring window below until the window closes (healthy) or fires (regression / dogfood issue).
③ wrap completed → open guard_state for the merged story
│
▼
┌─ POST-MERGE GUARD (per merged story, across ticks) ───────────────┐
│ 1. trigger/await deploy (deploy_status: pending→deploying │
│ →deployed | failed) │
│ 2. open monitoring window (window_min, default 15) │
│ 3. each tick within window: │
│ • read health_signals (CI / error-rate / health endpoint) │
│ • run host-aware dogfood against target_url(staging|prod) │
│ 4. classify findings → ONE of two issue paths (below) │
│ 5. window elapsed, all green → close guard_state (healthy) │
└───────────────────────────────────────────────────────────────────┘The guard never blocks dispatch — Steps ④/⑤/⑥ continue normally for in-flight workspaces while a merged story's window is open. The guard is signals-only and adds no per-tick workspace-code reads, so the orchestrator boundary and the <60s tick budget hold.
---
Step PG.1: Trigger / Await Deploy
After wrap, advance the merged story's deploy lifecycle. The host-native deploy trigger is project-specific; the guard treats it as a CLI/CI signal, never as code:
- CI-driven deploy (most projects): the merge to the integration branch already triggered the pipeline. Poll status:
gh run list --branch "$BASE" --limit 1 --json status,conclusion,databaseId
gh run view <id> --json status,conclusion,jobs --jq '.status,.conclusion'- Explicit deploy: if the project declares a deploy command/workflow, dispatch it once and record the run id, then poll as above.
Set guard_state.deploy_status accordingly: pending → deploying → deployed (CI success + deploy URL resolvable) or failed. A failed deploy is itself a hard regression — go straight to the auto-revert / escalate path.
The monitoring window (PG.2/PG.3) opens only once deploy_status == "deployed". Until then the guard waits across ticks (idempotent — see state).
---
Step PG.2: Open the Monitoring Window
Once deployed, open a window of topology.routing.post_merge_guard.window_min minutes (default 15). Record window_opened_at. Each subsequent tick that falls inside the window runs PG.3; once now > window_opened_at + window_min with no firing condition met, the window closes and the guard records the story healthy and clears its guard_state.
---
Step PG.3: Watch Health Signals + Run Host-Aware Dogfood
Within the open window, each tick performs two independent reads:
(a) Health signals
Read every signal named in topology.routing.post_merge_guard.health_signals. These are service-level, signals-only probes — no workspace code.
Coverage precondition. Runcoverage_check(health_signals)(../../../product-context/reflect/references/telemetry-ingestion.md§1.5) first: a signal likeerror_rate/latency_p95that needs a metrics source must be bound (the/aep-mapTelemetry Binding step wired atelemetry_sourcesentry /health_url). An unbound signal is reported as "telemetry binding incomplete — run /aep-map", not treated as green — never infer health from a signal you can't actually read. (ci_status/health_endpoint/smoke_checkare self-describing and need no binding.)
| Signal kind | How the orchestrator reads it (examples) |
|---|---|
ci_status | gh run view <id> --json status,conclusion for the post-merge pipeline |
health_endpoint | curl -fsS --max-time 5 <health_url> (e.g. /healthz, /readyz) → expect 2xx |
error_rate | query the project's metrics/log source for error-rate over the window vs. a baseline |
latency_p95 | same source — p95 latency vs. baseline threshold |
smoke_check | a declared CLI/API smoke command exiting 0 |
A signal is red when it fails its declared threshold (non-2xx health, CI failure, error-rate above baseline + margin, etc.). One transient red is not a regression — require the red to persist across 2 consecutive ticks (or match a declared confirm rule) before treating it as confirmed, to avoid reverting on a deploy-warmup blip.
(b) Host-aware dogfood
Run the dogfood validation against the deployed environment:
method = dogfood_method() # host × mode detection (see dogfood-validation.md)
url = target_url(post_deploy_env) # staging | production, from deploy_targets / CI
run dogfood(method, url) → report (severity/category/repro, signals-only)post_deploy_env comes from topology.routing.dogfood.post_deploy_env (staging | production | none). target_url() resolves config-first then CI fallback (see dogfood-validation.md). The dogfood report uses the unified /agent-browser:dogfood severity/category/repro template, so the downstream classifier is host-agnostic.
---
Step PG.4: Two Issue Paths — Kept Strictly Separate
The design fixes two distinct failure shapes (g4-dogfood-validation-design.md → "發現問題時的行為"). Do not conflate them: a dogfood UX finding is never a revert, and a service regression is never a new backlog story.
Path 1: Dogfood-found UX / functional issues → create story (NOT a revert)
The deploy is healthy at the service level, but the dogfood surfaced a UX or functional defect (broken flow, visual regression, wrong copy, dead link). This is feedback, not an outage.
- Feed the dogfood report to the `/aep-reflect` classifier via the `dogfood_report` adapter (
../../../product-context/_shared/references/telemetry-ingestion.md→ Dogfood-report adapter), which classifies severity/category and auto-creates a bug/refinement story inproduct-context.yaml(links the G6 self-feeding loop). - Stamp `watch_origin: {source: dogfood, external_id: <adapter key>}` on each story you file, using the adapter's deterministic
external_id. This is the same dedupe key/aep-watch'sdogfood_reportsource uses, so if watch also ingests the report neither path double-files — whichever runs first wins and the other no-ops (see the adapter's "No high-water mark — dedupe-only"). - The new story enters the normal dispatch queue — Step ⑥ picks it up on a later tick by
readiness_score. - Never revert for a Path-1 finding. The merged change stays; the fix ships as its own story.
- Record
guard_state.dogfood = {report_path, issues_created:[story_ids]}.
Path 2: Hard service regression → auto_revert policy
A health signal is confirmed red (or the deploy failed). The deployed service is degraded — users are affected now. Behavior is governed by topology.routing.post_merge_guard.auto_revert:
DEFAULT IS CONSERVATIVE — `auto_revert: false`. With auto-revert off (the default), the guard warns and escalates only: it adds a post_merge_regression escalation, pauses if the story is on the critical path, and waits for a human to confirm the revert. Automatic reverting is opt-in and presumes the architectural back-pressure below is in place.- `auto_revert: false` (default): add escalation, do not touch the merge.
{
"type": "post_merge_regression",
"story_id": "<id>",
"reason": "Health signal '<signal>' red for 2 consecutive ticks after merge of <pr>",
"details": "<signal readings vs. baseline; deploy status>",
"expected_human_action": "Investigate the deployed regression. If confirmed, revert with `gh pr revert <number>` (or revert the merge commit) and redeploy; then run /aep-reflect to log the incident.",
"created_at": "<ISO8601>",
"acknowledged": false
}- `auto_revert: true` (opt-in) and regression confirmed:
1. Revert — gh pr revert <number> (opens/auto-merges a revert PR per repo policy) or revert the merge commit on $BASE and push. This is the one sanctioned exception to "never act on the merge" — it is a _recovery_ action, gated behind explicit opt-in, not a normal merge. 2. Record an incident — write .dev-workflow/incidents/<story_id>-<ISO8601>.md (or append to autopilot-history.jsonl with type: incident): the red signals, readings, the reverted PR, and the deploy outcome. 3. Feed `/aep-reflect` — hand the incident to the reflect classifier so the regression becomes a learning + a follow-up story (root-cause / guard hardening), closing the loop the same way Path 1 does for UX issues. 4. Set guard_state.reverted = true so no later tick reverts the same story twice (see state).
---
Architectural Back-Pressure (prerequisites for safe auto-revert)
auto_revert: true is only as safe as the scaffolding that makes a revert clean and a regression detectable. Document these as prerequisites the project should have before enabling auto-revert; they are scaffold-level recommendations, not steps the guard performs:
- Pre-commit hooks — lint/typecheck/format/secret-scan at commit time, so obviously-broken changes never reach the merge that the guard would have to revert.
- Property-based tests — broaden coverage beyond example-based tests so regressions are caught by signals (and by Phase 5 eval) rather than only in production.
- Feature-flag / canary gating — ship merged code dark or to a canary slice; a regression then degrades a fraction of traffic and a "revert" can be a flag flip, far safer and faster than a code revert.
- Audit log — append-only record of every guard action (deploy triggered, signals read, revert performed, incident filed) so auto-revert decisions are reconstructable and reviewable.
Without these, prefer the default auto_revert: false (warn + escalate). The guard should note in its escalation when prerequisites appear absent.
---
Config
topology:
routing:
post_merge_guard:
window_min: 15 # monitoring window length, minutes (default 15)
auto_revert: false # OPT-IN. false = warn + escalate only (conservative default)
health_signals: # service-level, signals-only probes watched during the window
- ci_status # post-merge pipeline conclusion
- health_endpoint # 2xx from /healthz (URL from deploy_targets / CI)
- error_rate # error-rate over window vs. baseline
# - latency_p95
# - smoke_checkReuses topology.routing.deploy_targets.{staging_url,production_url} and topology.routing.dogfood.{post_deploy_env,on_issue} from the G4 dogfood design — the guard does not duplicate URL/method config.
---
State & Idempotency
The guard records its progress per merged story so a re-fired tick never double-acts (double-deploys, double-reverts, double-files an incident). Add a guard_state entry keyed by story_id (alongside workspaces in autopilot-state.json; see state-schema.md):
{
"story_id": "PROJ-003",
"pr_number": 412,
"merged_at": "<ISO8601>",
"deploy_status": "deployed", // pending | deploying | deployed | failed
"window_opened_at": "<ISO8601>",
"health": { "ci_status": "green", "health_endpoint": "green", "error_rate": "green" },
"red_streak": { "error_rate": 0 }, // consecutive red ticks per signal (confirm rule)
"dogfood": { "report_path": null, "issues_created": [] },
"reverted": false,
"incident_path": null,
"last_action": "watching", // watching | dogfood_ran | story_created | escalated | reverted | closed
"closed_at": null
}Idempotency rules:
- Deploy once — only trigger a deploy if
deploy_status == "pending"; otherwise poll. - Revert once — never revert if
reverted == true; the confirmed-red check is short-circuited once reverted. - One escalation per regression — guard against duplicate
post_merge_regressionescalations for the samestory_idwhile unacknowledged. - Close cleanly — when the window elapses with all-green (or after Path-1 story creation / Path-2 revert + incident), set
last_actionaccordingly, setclosed_at, and drop theguard_stateentry on the next tick.
---
Cross-References
- tick-protocol.md — Step ③ wrap (the guard opens immediately after wrap); this guard is the new post-deploy step that runs across subsequent ticks.
dogfood-validation.md—dogfood_method()host × mode detection,target_url(env)resolution, and the unified report format the guard consumes./aep-reflect— the classifier both issue paths feed: Path 1 (UX/functional → new story) and Path 2 (incident → learning + follow-up story).- state-schema.md — where
guard_statelives inautopilot-state.json.
Workspace Gen/Eval Triggering Protocol
How the autopilot detects when a workspace needs code review and triggers the workspace's own gen/eval loop via executor.nudge() — delivered through the workspace's mode transport (SendMessage / feedback.md / send_input / codex exec resume / tmux send-keys; see the table in SKILL.md). The autopilot never evaluates code itself — it triggers and monitors.
Note: Key trigger templates from this file are also inlined in tick-protocol.md Step ④ (GUIDE COMPLETION) to ensure the LLM sees them in context during tick execution.---
Principle: Trigger, Don't Execute
The workspace agent owns code quality evaluation. The autopilot's role is:
1. Detect when gen/eval should be running but isn't 2. Trigger the workspace to run its own Phase 5 gen/eval loop via executor.nudge() 3. Monitor the eval-response files for results 4. Act on results (guide workspace toward merge via executor.nudge(), or let workspace fix issues)
---
Detection Logic
Each tick, for every active workspace, check:
Condition 1: Phase 4 Complete, No Eval Started
workspace.phase >= 5
AND NOT exists(.feature-workspaces/<name>/.dev-workflow/signals/eval-response-*.md)
AND workspace.code_review_triggered == falseMeaning: Implementation is done (or past done) but the workspace hasn't run Phase 5. Likely cause: Agent skipped Phase 5, had a context reset, or moved straight to Phase 9.
Condition 2: Stuck at Phase 5
workspace.phase == 5
AND workspace.consecutive_stuck_ticks >= 2
AND workspace.code_review_triggered == trueMeaning: Workspace is at Phase 5 but making no progress for 10+ minutes after being triggered. Likely cause: Evaluator spawn failed, the nudge never reached the worker, or agent is in a loop.
Condition 3: Phase 10+ Without Recent Eval
workspace.phase >= 10
AND workspace.pr_url is setCheck: does the latest eval-response-*.md file predate the latest PR commit?
# Get latest eval-response timestamp
EVAL_TIME=$(stat -f %m .feature-workspaces/<name>/.dev-workflow/signals/eval-response-*.md 2>/dev/null | sort -n | tail -1)
# Get latest PR commit timestamp
PR_COMMIT_TIME=$(gh pr view <number> --json commits --jq '.commits[-1].committedDate')If eval is older than the latest commit, code has changed since review.
Condition 4: Moved Past Phase 5 Without PASS
workspace.phase > 5
AND latest eval-response shows "Result: FAIL"Meaning: Workspace moved to later phases despite failing evaluation. Action: Send workspace back to Phase 5.
---
Trigger Commands
First Trigger (gentle)
executor.nudge(<workspace-name>,
"Run Phase 5 code review now. Write eval-request.md to .dev-workflow/signals/, spawn an evaluator via executor.spawn_evaluator (your mode's recipe) per the build skill Phase 5 protocol, and execute the gen/eval loop. Check .dev-workflow/signals/feedback.md for context.")Set in state: code_review_triggered = true, code_review_triggered_at = now, last_action = "review_triggered".
Re-trigger (after 3 ticks / 15 min no response)
executor.nudge(<workspace-name>,
"URGENT: Phase 5 code review has not produced results. If you had a context reset, run bash .dev-workflow/init.sh to recover state. Then immediately: 1) Write eval-request.md 2) Spawn the evaluator via executor.spawn_evaluator 3) Execute the gen/eval loop per build Phase 5.")Set: last_action = "review_re_triggered".
Send Back (moved past without PASS)
executor.nudge(<workspace-name>,
"Your latest eval-response shows FAIL but you moved past Phase 5. Go back to Phase 5: fix the FAIL items identified in the eval-response, then re-run the gen/eval loop. Do not proceed to PR until eval passes.")Fresh Review for PR (Phase 10+ with stale eval)
executor.nudge(<workspace-name>,
"Code has changed since your last evaluation. Re-run Phase 5 code review on the current state before proceeding with the PR. Write a new eval-request.md and spawn a fresh evaluator.")---
Monitoring Protocol
Each tick after triggering, check for eval-response files:
ls .feature-workspaces/<name>/.dev-workflow/signals/eval-response-*.md 2>/dev/nullIf eval-response exists:
Read the latest response file. Parse the ## Result: PASS / FAIL line.
PASS:
- Set
eval_rounds_completedto the round number - Workspace can proceed to Phase 9+ (it will do so autonomously)
- Tick step ④c will guide workspace toward Phase 12 merge via
executor.nudge()
FAIL:
- Check if workspace is actively fixing (
phase == 5,completion_pctchanging) → let it work - If stuck → re-trigger (see above)
- Track round count via
eval_rounds_completed
If no eval-response after trigger:
| Ticks since trigger | Action |
|---|---|
| 1-2 | Wait — workspace may be running eval |
| 3 (15 min) | Re-trigger with URGENT message |
| 6 (30 min) | Add escalation: "Workspace not responding to eval trigger" |
---
Escalation
Escalate to human when:
- Workspace has completed 5 eval rounds without PASS (workspace's own max convergence)
- Workspace has not responded to 2 trigger attempts over 30 minutes
- Eval response shows the same findings 3+ consecutive rounds (not converging)
Escalation entry:
{
"type": "eval_not_converging",
"story_id": "<id>",
"workspace": "<name>",
"reason": "Gen/eval loop failed to converge after 5 rounds",
"details": "Persistent failures on [dimensions]. Generator cannot fix: [specific issues].",
"expected_human_action": "Review the eval findings in .feature-workspaces/<name>/.dev-workflow/signals/eval-response-5.md and decide: fix manually, adjust the spec, or defer the story.",
"created_at": "<ISO8601>",
"acknowledged": false
}Autopilot State Schema
All autopilot state files live in .dev-workflow/ on the main workspace (repo root). This reuses the existing .dev-workflow/ pattern already established for workspace agents.
---
.dev-workflow/autopilot-state.json
Machine-readable state file. Read and written by the autopilot tick.
{
"version": 1,
"status": "running",
"started_at": "2026-04-01T10:00:00Z",
"last_tick_at": "2026-04-01T10:25:00Z",
"tick_count": 5,
"tick_in_progress": null,
"workspaces": {
"auth-middleware": {
"story_id": "PROJ-003",
"backend": "native-bg-subagent",
"agent_id": "adfb6cb206155a92e",
"phase": 5,
"phase_name": "code-review",
"story_status": "in_progress",
"completion_pct": 60,
"pr_url": null,
"cost_usd": null,
"completed_at": null,
"failure_log": null,
"last_action": "review_triggered",
"last_action_at": "2026-04-01T10:20:00Z",
"code_review_triggered": true,
"code_review_triggered_at": "2026-04-01T10:20:00Z",
"eval_rounds_completed": 0,
"consecutive_stuck_ticks": 0,
"last_liveness_hash": null,
"blockers": []
}
},
"escalations": [
{
"type": "design_needed",
"story_id": "PROJ-010",
"workspace": null,
"reason": "Complexity L with 1 acceptance criterion, UI-heavy activity",
"details": "Story 'Add settings page' needs UI/UX decisions: layout structure, form grouping, navigation pattern. The current spec has only 'settings page exists' as acceptance criteria.",
"expected_human_action": "Run /aep-design PROJ-010 to refine the spec with concrete acceptance criteria, or add criteria directly to product-context.yaml. Then run /aep-autopilot start to resume.",
"created_at": "2026-04-01T10:15:00Z",
"acknowledged": false
}
],
"guard_state": {
"PROJ-002": {
"pr_number": 142,
"deploy_status": "deployed",
"monitor_until": "2026-04-01T10:40:00Z",
"health": { "ci_status": "green", "error_rate": "ok", "health_endpoint": "ok" },
"dogfood_done": true,
"reverted": false,
"escalated": false
}
},
"stats": {
"stories_completed": 3,
"stories_failed": 0,
"total_ticks": 5,
"total_cost_usd": 12.5
}
}Field Reference
Top-level
| Field | Type | Description |
|---|---|---|
version | number | Schema version (currently 1) |
status | enum | "running", "paused", "stopped" |
started_at | string | ISO8601 timestamp of /aep-autopilot start |
last_tick_at | string\ | null |
tick_count | number | Total ticks completed |
tick_in_progress | string\ | null |
Workspace Entry
| Field | Type | Description |
|---|---|---|
story_id | string | Story ID from product-context.yaml |
backend | string | Executor mode this workspace was launched under: native-bg-subagent, claude-bg, codex-subagent, codex-exec, legacy |
agent_id | string\ | null |
phase | number | Current build phase (0-12) from signal |
phase_name | string | Human-readable phase name from signal |
story_status | string | "in_progress", "in_review", "completed", "failed" |
completion_pct | number | 0-100 from signal |
pr_url | string\ | null |
cost_usd | number\ | null |
completed_at | string\ | null |
failure_log | object\ | null |
last_action | string | Last autopilot action for this workspace |
last_action_at | string | ISO8601 of last action |
code_review_triggered | boolean | Whether autopilot has triggered gen/eval |
code_review_triggered_at | string\ | null |
| eval_rounds_completed | number | How many eval rounds the workspace has completed | | consecutive_stuck_ticks | number | Ticks with no progress change | | last_liveness_hash | string\|null | Hash of the mode's liveness-probe output at last tick (TaskList entry / worktree-activity probe / logs tail / worker.log / tmux pane). Used for liveness comparison. Null on first tick or after restart. | | blockers | string[] | Current blockers from signal |
last_action Values
| Value | Meaning |
|---|---|
"launched" | Workspace just launched via /aep-launch |
"readopted" | Orphaned worker re-spawned into the existing worktree |
"review_triggered" | Gen/eval triggered via executor.nudge() |
"review_re_triggered" | Gen/eval re-triggered after stuck |
"detected_merged" | PR detected as merged by workspace agent |
"detected_closed" | PR detected as closed without merge |
"wrapping" | /aep-wrap in progress |
"merge_nudged" | Sent nudge to proceed to Phase 12 |
"merge_stuck_nudged" | Sent stronger nudge for stuck Phase 12 |
"nudged" | Sent stuck nudge via executor.nudge() |
"escalated_stuck" | Escalated due to prolonged stuck |
"human_gate" | Blocked on a human decision (needs-human.md) |
Escalation Entry
| Field | Type | Description |
|---|---|---|
type | enum | "design_needed", "stuck", "failed", "layer_gate_failed", "eval_not_converging", "human_gate", "post_merge_regression" |
story_id | string | Related story ID |
workspace | string\ | null |
reason | string | One-line reason |
details | string | Detailed explanation of why escalation triggered |
expected_human_action | string | What the human should do |
created_at | string | ISO8601 timestamp |
acknowledged | boolean | Whether human has seen this |
guard_state Entry (post-merge guard, keyed by story_id)
Persists the post-merge guard's per-story state so a tick is idempotent (deploy-once, dogfood-once, revert-once, escalate-once). See references/post-merge-guard.md.
| Field | Type | Description |
|---|---|---|
pr_number | number | Merged PR being guarded |
deploy_status | enum | "pending", "deployed", "failed" |
monitor_until | string | ISO8601 — end of the window_min monitoring window |
health | object | Last reading per configured health_signals key (e.g. ci_status, error_rate) |
dogfood_done | boolean | Whether host-aware post-deploy dogfood has run for this story |
reverted | boolean | Whether a revert was already issued (guards against double-revert) |
escalated | boolean | Whether a post_merge_regression escalation was already emitted |
---
.dev-workflow/autopilot-history.jsonl
Append-only audit trail. One JSON line per tick.
{"tick":1,"at":"2026-04-01T10:00:00Z","status":"running","actions":["initialized","dispatched PROJ-003"],"workspaces_active":1,"stories_completed_total":0}
{"tick":2,"at":"2026-04-01T10:05:00Z","status":"running","actions":["synced 1 workspace","dispatched PROJ-004"],"workspaces_active":2,"stories_completed_total":0}
{"tick":3,"at":"2026-04-01T10:10:00Z","status":"running","actions":["synced 2 workspaces","triggered review for PROJ-003"],"workspaces_active":2,"stories_completed_total":0}
{"tick":4,"at":"2026-04-01T10:15:00Z","status":"paused","actions":["design escalation for PROJ-010"],"workspaces_active":2,"stories_completed_total":0}---
.dev-workflow/autopilot-status.md
Human-readable status file. Updated at the end of every tick.
# Autopilot Status
**Status:** Running
**Started:** 2026-04-01 10:00
**Last tick:** 2026-04-01 10:25 (tick #5)
## Active Workspaces
| Workspace | Story | Phase | Progress | Last Action |
| --------------- | -------- | --------------- | -------- | ---------------- |
| auth-middleware | PROJ-003 | 5 (code-review) | 60% | review triggered |
| user-model | PROJ-004 | 10 (pr-created) | 90% | detected_merged |
## Escalations
### PROJ-010: Design Needed (UNRESOLVED)
**Why:** Complexity L with 1 acceptance criterion, UI-heavy activity 'Settings'
**What needs attention:** Story 'Add settings page' needs UI/UX decisions — layout structure, form grouping, navigation pattern
**Expected action:** Run `/aep-design PROJ-010` to refine the spec, then `/aep-autopilot start`
## Stats
- Stories completed: 3
- Stories failed: 0
- Total cost: $12.50
- Total ticks: 5When paused, the status file includes additional sections:
## PAUSED — Human Attention Required
**Paused at:** 2026-04-01 10:15
**Reason:** Design escalation for PROJ-010
### Why autopilot paused
Story PROJ-010 'Add settings page' was next in the dispatch queue (score: 8.5) but
does not meet the criteria for autonomous implementation:
- Complexity: L (large scope)
- Acceptance criteria: 1 (minimum 3 required for autonomous dispatch)
- Activity: 'Settings' (UI-heavy — requires visual design decisions)
### What decisions need human input
1. **Page layout:** Single page vs tabbed sections vs sidebar navigation
2. **Form grouping:** How to organize settings (profile, notifications, privacy, etc.)
3. **Interaction patterns:** Inline editing vs modal dialogs vs save-all-at-once
### How to resume
1. Run `/aep-design PROJ-010` to work through the design interactively
2. Or add at least 3 specific acceptance criteria to product-context.yaml
3. Then run `/aep-autopilot start` to resume orchestration
### Current state while paused
- 2 workspaces still running (PROJ-003, PROJ-004)
- 3 stories completed so far
- Paused workspaces will continue autonomously---
Tick Lock Mechanism
The tick_in_progress field prevents overlapping ticks:
1. Before tick: Read tick_in_progress. If set and less than 4 minutes old, skip this tick. 2. Start of tick: Set tick_in_progress to current timestamp. Write state immediately. 3. End of tick: Set tick_in_progress to null. Write state.
If a tick crashes (lock never released), the next tick after 4 minutes will clear the stale lock and proceed.
---
Atomic Write Protocol
To prevent state corruption from mid-write crashes:
# Write to temp file
cat > .dev-workflow/autopilot-state.json.tmp << 'EOF'
{ ... }
EOF
# Atomic rename (POSIX guarantees this is atomic)
mv .dev-workflow/autopilot-state.json.tmp .dev-workflow/autopilot-state.jsonThe autopilot skill instructs the agent to use this pattern. In practice, the agent writes the file and the filesystem handles atomicity.
Tick Protocol
The 7-step state machine executed on each autopilot tick (with a ③.5 post-merge guard sub-step between wrap and guide-completion). Each tick is idempotent — running it twice with no external state change produces the same result and takes no duplicate actions.
Target duration: <60 seconds of work per tick (under the goal driver the turn then waits the per-tick floor — step ⑦ — before ending) Invocation: goal driver (default) — /goal "<layer-N condition>" re-fires this tick each turn until the layer completes; loop driver (fallback) — /loop 5m /aep-autopilot tick; or manual /aep-autopilot tick
BOUNDARY REMINDER: The autopilot is an orchestrator. Every action on a workspace isexecutor.nudge()/executor.liveness()— autopilot runs only on steerable, driver-compatible modes (native-bg-subagent / claude-bg / codex-subagent / codex-exec / legacy; see the per-mode transport table in SKILL.md andaep-executor/references/backends.md). The nudge texts in this file are mode-independent — deliver each through the workspace'sbackendtransport (SendMessage(to: agentId)/feedback.md/send_input/codex exec resume/tmux send-keys). Liveness is the post-spawn liveness probe (process exists AND worktree active) — never roster/state membership. Never spawn code reviewers from main, never read workspace source code, never callgh pr merge. See SKILL.md "STOP — Orchestrator Boundaries".
EXECUTION MODEL — CHECK → ACT (see SKILL.md "Execution model"). A tick is two halves:
- CHECK — steps ①②⑤, the read-only/scoring parts of ④⑥, and the ⑦ state write. These run in a cheap, context-isolated agent via
executor.check()(Claude Code Haiku subagent / Codexcodex exec) and produce an action list. The CHECK reads signals only — never workspace code. - ACT — the orchestrator performs the emitted actions: ③ wrap, ③.5 post-merge guard (dogfood / reflect / revert), ④/⑤ nudges, ⑥ launch, escalations.
The action-list schema is {summary, state_written, actions[]}, each action {type, workspace, story_id, message, reason} (full schema in aep-executor/references/backends.md). The step recipes below are both the content of the CHECK prompt and the templates the ACT executes.
---
Step ①: Read State
cat .dev-workflow/autopilot-state.jsonExit conditions:
statusis not"running"→ log "autopilot not running, skipping tick" and exittick_in_progresstimestamp exists AND is less than 4 minutes old → log "previous tick still running, skipping" and exit (prevents overlapping ticks when/loopfires before the previous tick completes)
If proceeding:
- Set
tick_in_progressto current ISO8601 timestamp - Write state immediately (this is the tick lock)
---
Step ②: Sync Signals
Read signal files from all active workspaces and update state:
for ws_name in $(jq -r '.workspaces | keys[]' .dev-workflow/autopilot-state.json); do
signal=".feature-workspaces/$ws_name/.dev-workflow/signals/status.json"
if [ -f "$signal" ]; then
cat "$signal"
fi
doneFor each workspace in state.workspaces:
| Signal field | State field to update |
|---|---|
phase | workspaces[name].phase |
phase_name | workspaces[name].phase_name |
story_status | workspaces[name].story_status |
completion_pct | workspaces[name].completion_pct |
pr_url | workspaces[name].pr_url |
blockers | workspaces[name].blockers |
cost_usd | workspaces[name].cost_usd |
completed_at | workspaces[name].completed_at |
failure_log | workspaces[name].failure_log |
If signal file doesn't exist: Keep previous state values. The workspace may not have written signals yet (still initializing).
If `story_status` is `"failed"`:
- Check
failure_logfor structured error info - Add escalation if
attempt_countexceedsmax_retries(default 3)
If `blocked_on == "human"` (or `needs-human.md` has an unresolved entry):
- The workspace is at a human gate, not stuck — exempt it from stuck
counting and emit an escalate action of type human_gate. The expected_human_action is hub-and-spoke: the human answers in the main session; the orchestrator relays it on the mode's channel — re-spawn a bg subagent into the worktree with the answer (native-bg-subagent, parked) / resume the session with the answer (claude-bg, parked) / send_input (codex-subagent) / codex exec resume <agent_id> "<answer>" (codex-exec, parked) / executor.nudge() (legacy). A parked worker (gate-and-park: its run ended cleanly after recording the gate) is resumed into the same worktree with the answer + recovery bootstrap — do not treat the exited process as crashed or stuck.
- Clear the escalation when the entry gains a
resolved:line.
Orphan check (session-bound modes — native-bg-subagent, codex-subagent) — by real liveness, not roster:
- Apply the post-spawn liveness probe:
the workspace is an orphan when its agent_id no longer appears in TaskList / list_agents (lead restarted, worker crashed, or the spawn never actually started — e.g. the removed claude-team's truncated-launch failure) and/or the worktree shows no live process — even if state/roster still says "active". If the worktree exists with progress, emit a launch action flagged readopt: true — the ACT re-spawns a worker into the existing worktree with the recovery bootstrap ("Run bash .dev-workflow/init.sh to recover state, read .dev-workflow/signals/feedback.md, then continue the /aep-build flow"), then updates agent_id. Do not mark the story failed; do not create a new worktree. Never accept roster/state membership as proof the worker is alive.
---
Step ③: Wrap Completed Workspaces
For each workspace where story_status == "completed":
1. Verify the workspace hasn't already been wrapped (last_action != "wrapping" and last_action != "wrapped") 2. Run /aep-wrap for this workspace:
- This runs on the integration branch (
$BASE):git fetch && git pull --ff-only origin "$BASE", archive OpenSpec change, sync story status to YAML, remove worktree
3. Set last_action = "wrapping" 4. After wrap completes:
- Remove workspace entry from state
- Increment
stats.stories_completed - Add
cost_usdtostats.total_cost_usd
Max ONE wrap per tick. Wraps modify product-context.yaml and involve git operations. Running multiple wraps risks conflicts. If multiple workspaces completed simultaneously, they get wrapped across consecutive ticks.
After wrapping, skip to step ⑦ (write state). The next tick will handle dispatch of newly-ready stories (which the wrap may have unblocked via cascade).
---
Step ③.5: Post-Merge Guard
For each recently-merged story (one Step ③ wrapped within the monitoring window — default applies per post-merge-guard.md), run the post-merge guard. The detail lives in references/post-merge-guard.md; this step defers to it. Within the monitoring window:
1. Watch deploy health — read deploy/CI signals and gh only (no workspace code, no gh pr merge). The orchestrator boundary holds. 2. Run host-aware dogfood — exercise the merged change per the host-aware recipe in post-merge-guard.md.
Two issue paths:
- Dogfood UX / functional issue → route the finding through the
/aep-reflectclassifier, which auto-creates a follow-up story. - Hard regression (deploy health breaks / CI red on the integration branch) → apply the
post_merge_guard.auto_revertpolicy: - DEFAULT (conservative, `auto_revert: false`) → warn + escalate for human decision; do not revert.
- `auto_revert: true` (opt-in) → revert the merge.
Emit any follow-up (reflect story / escalation / revert) as an action; never read workspace source — signals / CI / gh only.
---
Step ④: Guide Completion
This is the most important step. ALL actions here use `executor.nudge()` (delivered via the workspace's mode transport). NEVER spawn Agent tools for review. NEVER call `gh pr merge`. Workspace agents own code review and merging.
For each workspace, guide it through quality gates and toward merge completion. This step combines PR state detection, quality enforcement, and merge guidance.
Decision Tree (quick reference)
For each workspace:
Has pr_url?
├─ YES → ④a: check PR state
│ ├─ MERGED/CLOSED → update state, done with this workspace
│ └─ OPEN → ④b: has eval-response with PASS?
│ ├─ NO → trigger gen/eval via nudge (if not already triggered)
│ └─ YES → ④c: guide to merge via nudge (if not already nudged)
└─ NO, phase >= 5?
├─ YES → ④b: has eval-response with PASS?
│ ├─ NO → trigger gen/eval via nudge (if not already triggered)
│ └─ YES → leave alone (workspace will create PR autonomously)
└─ NO (phase < 5) → skip, still implementingSub-step ④a: Check PR State
For each workspace where pr_url is set:
gh pr view <number> --json state --jq '.state'- If state == `"MERGED"`: Update workspace
story_statusto"completed", setcompleted_atto current ISO8601 timestamp, setlast_action = "detected_merged". The next tick's Step ③ will wrap it. - If state == `"CLOSED"`: Update workspace
story_statusto"failed", addfailure_lognoting PR was closed without merge, setlast_action = "detected_closed". - If state == `"OPEN"`: Proceed to sub-steps ④b and ④c.
Autopilot NEVER calls `gh pr merge`. That is the workspace agent's job (Phase 12 of /aep-build). This eliminates premature-merge bugs where autopilot merges before the workspace agent has finished its full flow.
Sub-step ④b: Quality Gate — Ensure Gen/Eval
Applies to all workspaces at phase >= 5 — both pre-PR (phase 5-9) and post-PR (phase 10+). This is the universal quality gate.
Check `topology.routing.skip_human_eval` first:
skip_human_eval: all→ skip the quality gate entirely for all stories, proceed to ④cskip_human_eval: backend→ skip the quality gate for stories in non-UI modules (checkstory.activity— if null or infrastructure, skip). UI stories still require eval.skip_human_eval: none(default) → apply the full quality gate below
Check whether a passing evaluation exists:
ls .feature-workspaces/<name>/.dev-workflow/signals/eval-response-*.md 2>/dev/nullIf latest eval-response shows "Result: PASS" → quality gate satisfied, proceed to ④c.
If no eval-response exists OR latest shows "Result: FAIL":
Check detection conditions (see references/review-trigger.md for full logic):
1. Phase >= 5, no eval-response, not yet triggered: First trigger needed 2. Phase == 5, stuck 2+ ticks, already triggered: Re-trigger needed 3. Phase >= 10, eval older than latest PR commit: Fresh review needed 4. Phase > 5, latest eval shows FAIL: Send back to Phase 5
Trigger gen/eval via `executor.nudge()` (NEVER spawn an Agent tool):
# First trigger (gentle)
executor.nudge(<workspace-name>,
"Run Phase 5 code review now. Write eval-request.md, spawn the evaluator via executor.spawn_evaluator (your mode's recipe), and execute the gen/eval loop per the build skill Phase 5 protocol. Read .dev-workflow/signals/feedback.md for any additional context.")Set in state: code_review_triggered = true, code_review_triggered_at = now, last_action = "review_triggered".
Re-trigger after 3 ticks (15 min) with no response:
executor.nudge(<workspace-name>,
"URGENT: Phase 5 code review has not started. If you had a context reset, read .dev-workflow/init.sh to recover state, then run Phase 5 immediately.")Set: last_action = "review_re_triggered".
Send back (moved past Phase 5 without PASS):
executor.nudge(<workspace-name>,
"Your latest eval-response shows FAIL but you moved past Phase 5. Go back to Phase 5: fix the FAIL items identified in the eval-response, then re-run the gen/eval loop. Do not proceed to PR until eval passes.")Fresh review for PR (Phase 10+ with stale eval):
executor.nudge(<workspace-name>,
"Code has changed since your last evaluation. Re-run Phase 5 code review on the current state before proceeding with the PR. Write a new eval-request.md and spawn a fresh evaluator.")Escalation: No eval-response after 6 ticks (30 min) post-trigger → before escalating, the workspace must climb the recovery ladder (../../gen-eval/references/recovery-ladder.md): nudge it to work the ladder's rungs (re-scope, decompose, relax non-essential criteria, etc.) first. Only emit the "eval_not_converging" escalation after the ladder is exhausted — i.e. the workspace has reported the ladder spent without a PASS.
Sub-step ④c: Guide to Merge
For workspaces where the quality gate is satisfied (eval PASS exists) AND PR is OPEN.
Guard: only nudge once. Check last_action before sending — if already "merge_nudged", do not re-send the nudge. The workspace received the instruction and is working on it. Re-nudging every tick floods the workspace with duplicate prompts.
1. If `phase < 12` AND `last_action != "merge_nudged"` — workspace hasn't started merge yet:
executor.nudge(<workspace-name>,
"Your code review eval has PASSED. Proceed to Phase 12 now: run pre-merge checks (rebase on main, verify CI, check comments) then merge the PR. In autopilot mode you do not need user confirmation — merge when all Phase 12 checks pass.")Set last_action = "merge_nudged", last_action_at = now.
2. If `phase == 12` AND `consecutive_stuck_ticks >= 2` — workspace started merge but is stuck:
executor.nudge(<workspace-name>,
"Complete Phase 12 merge now: 1) git fetch origin && git rebase origin/\"$(git config --get aep.integration-branch 2>/dev/null || (git show-ref --verify --quiet refs/remotes/origin/develop && echo develop || echo main))\" && git push --force-with-lease origin feat/<name> 2) Verify CI green 3) gh pr merge <number> --squash --delete-branch. Then update status.json with story_status completed.")Set last_action = "merge_stuck_nudged", last_action_at = now.
3. If `phase == 12` AND progressing normally → leave alone.
Monitoring Protocol
Each tick after triggering gen/eval, check for eval-response files:
ls .feature-workspaces/<name>/.dev-workflow/signals/eval-response-*.md 2>/dev/nullPASS: Set eval_rounds_completed to the round number. Workspace can proceed to Phase 9+ (it will do so autonomously). Step ④c will guide toward merge next tick.
FAIL: Check if workspace is actively fixing (phase == 5, completion_pct changing) → let it work. If stuck → re-trigger.
| Ticks since trigger | Action |
|---|---|
| 1-2 | Wait — workspace may be running eval |
| 3 (15 min) | Re-trigger with URGENT message |
| 6 (30 min) | Add escalation: "Workspace not responding to eval trigger" |
---
Step ⑤: Detect Stuck Workspaces
For each workspace, compare current (phase, completion_pct) with the values from the previous tick:
- Different values: Reset
consecutive_stuck_ticksto 0 - Same values: Run liveness check before incrementing (see below)
Liveness Check
When signals are stale (same phase and completion_pct as previous tick), check whether the workspace agent is still actively working before counting it as stuck. Exempt first: if blocked_on == "human", the workspace is gated, not stuck.
Step 1 — Check mode-specific activity and compare against last_liveness_hash stored in the workspace state entry:
| Mode | Activity probe |
|---|---|
| native-bg-subagent | TaskList / TaskOutput <agentId> — task status / output changed? |
| claude-bg | claude agents --json status + `claude logs <agent_id> \ |
| codex-subagent | list_agents status for <agent_id> |
| codex-exec | tail -20 .feature-workspaces/<name>/.dev-workflow/worker.log |
| legacy | tmux capture-pane -t <name>:0.0 -p -S -20 (a zsh pane = never-started spawn) |
- `last_liveness_hash` is null (first tick after launch or restart) → Populate it with the hash of the current probe output. Do NOT increment
consecutive_stuck_ticks. The workspace gets benefit of the doubt on its first stale-signal tick. - The agent no longer exists (bg subagent gone from TaskList,
list_agentsempty,claude agentsshows exited, tmux session missing) → on a session-bound mode this is an orphan: emit the re-adoptionlaunchaction (see Step ②), do NOT count it stuck. On an OS-bound mode a missing process means a crashed/exited agent → incrementconsecutive_stuck_ticks. - Probe output differs from `last_liveness_hash` → Agent is active, signals are lagging. Update
last_liveness_hash. Do NOT incrementconsecutive_stuck_ticks. - Probe output matches `last_liveness_hash` → Proceed to Step 2.
Step 2 — Check for uncommitted code changes:
git -C .feature-workspaces/<workspace-name> diff --stat- Has uncommitted changes → Agent is writing code via tool use (file edits happen but no terminal output scrolls). Do NOT increment
consecutive_stuck_ticks. - No uncommitted changes → Agent is truly idle. Increment
consecutive_stuck_ticks.
Thresholds
| Stuck ticks | Duration | Action |
|---|---|---|
| 3 | 15 min | Check if workspace has blockers. If yes, log but don't nudge. |
| 6 | 30 min | Send nudge via executor.nudge() (see below). Log warning. |
| 12 | 60 min | Add escalation. Consider pausing if on critical path. |
Nudge Command (30 min stuck)
executor.nudge(<workspace-name>,
"You appear stuck at Phase <N> (<phase_name>) for 30 minutes. Check for errors, read .dev-workflow/signals/feedback.md for any instructions, and continue. If you need help, update status.json with blockers.")claude-bg: 6 stuck ticks is the stop+respawn threshold — `claude stop
<agent_id>`, then respawn in the worktree with the recovery bootstrap and
record the newagent_id(recipe inaep-executor/references/claude-native.md).
Escalation (60 min stuck)
Add to escalations[]:
{
"type": "stuck",
"story_id": "<story_id>",
"workspace": "<name>",
"reason": "Workspace stuck at Phase <N> for 60 minutes",
"phase": <N>,
"blockers": [...],
"created_at": "<ISO8601>",
"acknowledged": false
}If the stuck workspace is on the critical path, consider pausing autopilot to get human attention.
---
Step ⑥: Dispatch New Work
Check Capacity
active_count = count of workspaces in state (not wrapped/completed)
concurrency_limit = topology.routing.concurrency_limit from product-context.yaml (default 5)
available_slots = concurrency_limit - active_countIf available_slots <= 0: skip dispatch, log "WIP limit reached".
Run Dispatch Scoring
Reuse the dispatch scoring logic from /aep-dispatch steps 1-3:
1. Determine active layer — find the first layer with incomplete stories 2. Layer gate check — if active layer > 0, verify previous layer gate passed 3. Wave ordering — consult the waves section from product-context.yaml. Within the active layer, dispatch Wave 1 stories before Wave 2, etc. Only advance to the next wave when all stories in the current wave are completed or in_progress. 4. Filter ready queue — stories with status: ready in active layer and current wave, excluding file-conflict stories 5. Compute readiness_score per story (see /aep-dispatch Step 3):
readiness_score = (min(3, acceptance_criteria_count) + interfaces_defined*2 + files_identified*1 + verification_defined*2 + no_open_questions*2) / 106. Compute dispatch_score per story:
dispatch_score = (business_value + unblock_potential + critical_path_urgency + reuse_leverage) / (complexity_cost + ambiguity_penalty + interface_risk)Where business_value uses story.business_value if set, otherwise derived from priority (critical=10, high=7, medium=4, low=1).
Grouped Change Handling
Before scoring individual stories, check for compile_mode: grouped_change:
1. Identify stories sharing the same change_group 2. Score the group as one unit: sum business_value and unblock_potential across the group; use max critical_path_urgency and max reuse_leverage; divide by sum of complexity_cost + max ambiguity_penalty + max interface_risk 3. Use min readiness_score of any story in the group as the group's readiness gate 4. Dispatch the entire group as one unit — one /aep-launch, one workspace, one OpenSpec change containing all grouped stories
Check Routing
For the top-scored story (or group), use readiness_score for routing:
- readiness_score >= 0.7 → dispatch to
/aep-launch - readiness_score 0.5–0.7 → check
topology.routing.full_auto/auto_design: - If
full_auto: true(master switch) orauto_design: true→ auto-route through the non-interactive design resolver (/aep-design, no pause), then/aep-launch - Otherwise → ESCALATE (pause for human design input)
- readiness_score < 0.5 → check
topology.routing.full_auto/auto_design: - If
full_auto: true(master switch) orauto_design: true→ auto-route through the non-interactive design resolver (/aep-design, no pause), then/aep-launch - Otherwise → ESCALATE (pause for human design input)
- `attempt_count >= 2` → always ESCALATE regardless of readiness (repeated failures need human attention)
If escalation triggers: follow the pause protocol from the main SKILL.md. Do not dispatch.
Dispatch
If no escalation:
1. Run /aep-dispatch for the top story or group (autopilot acts as the "user" selecting the story) 2. If routed through /aep-design (auto_design mode): run /aep-design first, then /aep-launch 3. Run /aep-launch for the dispatched story (or group) 4. Add workspace entry to state:
{
"story_id": "<id>",
"story_ids": ["<id>"],
"compile_mode": "single_change",
"change_group": null,
"wave": 1,
"readiness_score": 0.8,
"routed_to": "launch",
"backend": "native-bg-subagent",
"agent_id": "<bare-hex bg-subagent id | bg session id | codex agent id | exec session id | tmux session name>",
"phase": 0,
"phase_name": "initializing",
"story_status": "in_progress",
"completion_pct": 0,
"pr_url": null,
"last_action": "launched",
"last_action_at": "<ISO8601>",
"code_review_triggered": false,
"code_review_triggered_at": null,
"eval_rounds_completed": 0,
"consecutive_stuck_ticks": 0,
"last_liveness_hash": null,
"blockers": []
}For grouped changes: story_ids contains all story IDs in the group, compile_mode is "grouped_change", change_group is the group ID, and story_id is the first story in the group (used as the primary identifier).
Max ONE launch per tick. Launching involves creating a git worktree, spawning a worker (bg subagent / bg session / subagent / exec / tmux), running the post-spawn liveness probe, and delivering a bootstrap prompt — too slow for multiple per tick.
Layer Completion
If all stories in the active layer are completed (after wraps):
1. Suggest running the layer gate integration test 2. If gate passes: update layer_gates[layer].status: passed 3. Outcome contract check: If product.layers[active_layer].outcome_contract exists, decide whether to auto-evaluate or pause:
- Quantitative auto-eval: If
topology.routing.auto_outcome_eval: quantitativeand the contract's metric is quantitative (a measurable threshold) → first runcoverage_check([metric])(../../../product-context/reflect/references/telemetry-ingestion.md§1.5); if the metric isn't bound to a telemetry source (the/aep-mapTelemetry Binding step wasn't done) → pause and escalate "run /aep-map observability step" (do not claim auto-coverage). If covered → auto-evaluate via the telemetry-ingestion recipe (ingest the telemetry, compare against the threshold) and advance without pausing when it passes. If the metric is qualitative, fall through to the pause rule below. - Qualitative / default pause: Otherwise (no
auto_outcome_eval, a qualitative metric, etc.) → pause and add an escalation requesting the user to evaluate the outcome contract before advancing — UNLESStopology.routing.full_auto: true, in which case auto-evaluate via the telemetry-ingestion recipe and advance without pause. Outcome evaluation otherwise requires human judgment (user testing, analytics, qualitative assessment). The user runs/aep-reflectwhich evaluates outcome contracts in Step 2.75. After/aep-reflectcompletes, resume autopilot. - Default (no
auto_outcome_eval/full_autofalse) preserves the current human pause.
4. If no outcome contract or outcome evaluation passes: advance to next layer 5. If gate fails: add escalation, pause autopilot (layer gate failures require human judgment) 6. If all layers complete: stop autopilot, notify human
Orchestration Learning Checkpoint
At natural checkpoints (layer complete, escalation, or autopilot stop), run the orchestration learning protocol from references/orchestration-learning.md. This produces .dev-workflow/autopilot-learnings.md with cross-workspace findings that feed into /aep-reflect.
---
Step ⑦: Write State
1. Atomic write: Write updated state to .dev-workflow/autopilot-state.json.tmp, then rename to .dev-workflow/autopilot-state.json
2. Append tick summary to .dev-workflow/autopilot-history.jsonl:
{
"tick": 42,
"at": "<ISO8601>",
"actions": [
"synced 3 workspaces",
"detected PROJ-003 merged",
"triggered review for PROJ-004"
],
"workspaces_active": 2,
"stories_completed_total": 5
}3. Update status file .dev-workflow/autopilot-status.md — human-readable summary of current state
4. Increment tick_count, set last_tick_at = now
5. Release tick lock — set tick_in_progress to null
Goal-driver tail (steps 6–7 — skip entirely under the loop driver)
The fixed-interval /loop driver stops here: the interval is the cadence and the /loop skill re-fires the next tick. The goal driver instead ends each turn with two extra actions so the /goal evaluator can decide whether to re-fire:
6. Surface the AUTOPILOT status line into the transcript — one compact, signals-only line (no workspace code, no file contents) the goal evaluator judges the completion condition against. Derive every field from autopilot-state.json + product-context layer data:
AUTOPILOT layer=<N> wave=<W> stories=<total> done=<completed> in_progress=<n>
wrapped=<n> ready_remaining=<n> paused=<true|false> escalations=<n> tick=<k>
layer_complete=<true|false> # true ⟺ done==total AND no worktrees remainlayer_complete=true (or paused=true) is exactly what the goal condition keys on. Because the line is signals-only, the evaluator never reads workspace code — the orchestrator boundary holds.
7. Wait the per-tick floor, then end the turn. This is the anti-hot-loop floor — without it /goal re-fires the instant the turn ends. Default 5m (--floor); implement with the host's sanctioned bounded wait:
- Claude Code: the
Monitortool with a hard timeout (a raw foreground
sleep is blocked inside a turn). An early wake on a signals/ change is an allowed optimization but not required — the timeout alone guarantees a bounded, stable cadence.
- Codex: a shell
sleep <floor>(no restriction).
Ending the turn returns control to /goal, whose evaluator reads the surfaced status line and either re-fires the next tick or stops (layer complete / paused). Step ⑤'s stuck detection still runs every tick, so a stalled layer escalates → paused → the goal stops for the human.
---
Workspace State Derivation
The autopilot does NOT maintain a formal FSM enum. It derives the logical state from the combination of fields on each tick:
| Derived state | Condition |
|---|---|
| Initializing | phase == 0 |
| Implementing | phase == 4 |
| Reviewing | phase == 5 |
| Testing | phase >= 6 AND phase <= 8 |
| PR created | phase == 10 AND story_status == "in_review" |
| CI/Review loop | phase == 11 AND story_status == "in_review" |
| Awaiting merge | story_status == "in_review" AND phase >= 11 |
| Completed | story_status == "completed" |
| Failed | story_status == "failed" |
| Stuck | consecutive_stuck_ticks >= 6 |