
Aep Design
- 50 installs
- 14 repo stars
- Updated July 31, 2026
- memorysaver/agentic-engineering-patterns
Helps with design & ui/ux tasks.
About
aep-design is a Claude Code skill for design & ui/ux. It helps solo builders move faster with AI-assisted development.
- aep-design
- Design & UI/UX
- AI-coding skill
Aep Design by the numbers
- 50 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,240 of 1,880 Design & UI/UX 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-designAdd 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 design & ui/ux tasks.
Files
Executor Abstraction
A reusable abstraction for running implementation work in an isolated workspace, independent of which agent host (Claude Code, Codex) or which mechanism (native background subagents, background sessions, native subagents, exec workers, tmux, dynamic workflows) is available. Lifecycle skills speak one vocabulary of operations; this skill maps each operation to a concrete recipe per mode.
Native-first: Claude Code launches use a native in-process background subagent (native-bg-subagent, the default) or — where the claude --bg flag exists — native background sessions (claude-bg); Codex launches use native subagents (codex-subagent) or headless exec workers (codex-exec). tmux+cmux is the `legacy` mode — selected only by explicit pin (git config aep.executor-backend tmux) or on generic hosts. Every mode runs its worker in an AEP-created git worktree at .feature-workspaces/<ws>.
`claude-team` removed (2026-06): the agent-teams spawn path fails silently
on Claude Code ≥ 2.1.x (truncated launch command in a detached tmux pane; roster
still shows the worker "active"). Replaced by native-bg-subagent + a mandatorypost-spawn liveness probe. See docs/decisions/remove-claude-team.md.This skill is both a utility library and a standalone skill:
- As a library:
/aep-launch,/aep-build, and/aep-autopilotreference its
references/ files for detection, mode selection, and per-operation recipes.
- As a standalone skill: Invoke directly to detect the current host and
report which mode would be selected (useful when debugging "why did it pick X").
---
Why This Exists
The control plane (/aep-dispatch scoring, the .dev-workflow/signals/ protocol) is host-independent. The coupling lived in the execution plane — historically a claude process hosted in tmux, presented through cmux. This abstraction isolates that coupling so the same workflow runs under Claude Code or the Codex desktop app/CLI, using each host's native parallel-agent machinery, with tmux as a pinned fallback rather than a default.
See `docs/decisions/native-first-executor.md` (and the earlier `host-agnostic-executor.md`) for the decision records.
---
How Other Skills Use This
| Skill | What it uses | Operations |
|---|---|---|
/aep-launch | Start the implementation agent + expose it for review | detect, spawn, present |
/aep-build Phase 5 | Spawn the evaluator in the right execution context | detect, spawn_evaluator |
/aep-build | Raise a human decision mid-build | gate |
/aep-autopilot | Run the periodic tick check cheaply; steer workspaces | detect, check, nudge, liveness, gate |
/aep-wrap | Tear down the worker + worktree after merge | teardown |
/aep-dispatch | Resolve the handoff mode; route "…with workflow" runs | detect |
Cross-skill reference path
After sync with the aep- prefix, the references are at:
.claude/skills/aep-executor/references/backends.md # detection, selection, cross-mode protocols
.claude/skills/aep-executor/references/claude-native.md # native-bg-subagent, claude-bg recipes
.claude/skills/aep-executor/references/codex-native.md # codex-subagent, codex-exec recipes + role TOMLs
.claude/skills/aep-executor/references/tmux-session.md # legacy recipesRead backends.md first, then the recipe file for the selected mode.
---
The Operation Contract
Every consumer speaks these verbs. The recipe files supply the implementation per mode.
| Op | Purpose |
|---|---|
detect() | Resolve host + native capabilities + pin, select a mode |
spawn(ws, branch, prompt) | Start an implementation agent bound to the AEP worktree |
spawn_evaluator(ws, role) | Start an evaluator agent (worktree-bound) in the mode's eval context |
nudge(ws, msg) | Send a mid-flight instruction _(steerable modes; pull-based under claude-bg)_ |
liveness(ws) | Is the agent actively working? _(mode-specific signal + git-diff corroboration)_ |
gate(ws) | Surface a worker's human decision: needs-human.md + the mode's transport, answered hub-and-spoke through the main agent (block-in-place or gate-and-park) |
check(prompt, schema) | Run a read-only analysis prompt in a cheap, context-isolated agent; return its JSON result — keeps a long-lived orchestrator session's context small |
monitor(ws) | Read .dev-workflow/signals/status.json — host-independent, never changes |
present(ws) | Human review surface (TaskOutput / claude attach / Codex thread / cmux tab / signals) |
teardown(ws) | Worker + worktree cleanup |
`monitor()` is already abstract. Progress is reported through signal files
at phase boundaries regardless of the executor. Native push channels
(SendMessage, send_input) are an acceleration layer — the signal files remain
the durable, host-agnostic source of truth.
---
The Modes (summary)
| Mode | Backend | Lifetime | Selected when |
|---|---|---|---|
| native-bg-subagent | Agent tool run_in_background, no team | session-bound | Claude Code default + long-lived orchestrator |
| claude-bg | native background sessions | OS-bound | Claude Code, claude --bg present (cron driver / OS-bound need) |
| codex-subagent | native multi_agent (spawn_agent) | session-bound | Codex with a living main thread (desktop app or interactive CLI) |
| codex-exec | headless codex exec --cd workers | OS-bound | Codex + cron driver, or hard isolation demanded |
| legacy | tmux session (+ optional cmux tab) | OS-bound | explicit pin (aep.executor-backend tmux), or generic host w/ tmux |
| workflow | CC dynamic-workflow fan-out | session-bound | explicit opt-in ("…with workflow") + Claude Code (see /aep-workflow) |
| headless | one-shot native subagent | session-bound | last resort |
Read references/backends.md for the detection recipe, the full selection order, the driver × backend compatibility matrix, the human-gate protocol, and orphan re-adoption.
---
Reference Files
| File | Contents | When to read |
|---|---|---|
| `references/backends.md` | Mode matrix, detection, selection order, driver compatibility, gate protocol, orphan re-adoption, check() | Always, before spawning or steering |
| `references/claude-native.md` | native-bg-subagent (default) + claude-bg recipes, --bg availability note | When the selected mode is a Claude native one |
| `references/codex-native.md` | codex-subagent + codex-exec recipes, aep-builder/aep-evaluator role TOMLs, desktop app mapping | When the selected mode is a Codex one |
| `references/tmux-session.md` | legacy recipes (tmux spawn/nudge/liveness, cmux tab ladder) | When legacy is pinned or selected |
---
Standalone Usage
Invoked directly, this skill reports what would happen:
1. Run the detection recipe from references/backends.md. 2. Print: host (claude/codex/generic), executor commands, native capabilities (BG_AVAILABLE, MULTI_AGENT_AVAILABLE), pin, tmux/cmux presence, orchestrator lifetime, and the selected mode with the reason. 3. If the user asked "why not workflow / why not tmux", explain the opt-in/pin gates. (There is no agent-teams mode — claude-team was removed; see docs/decisions/remove-claude-team.md.)
This does not spawn anything — it is a dry-run of detect().
---
Design Decisions
Why native-first, tmux demoted:
- Claude Code's native in-process background subagent (Agent tool,
run_in_background, no team) gives each story its own context window, re-activation steering (SendMessage(to: agentId)), task-output visibility (TaskOutput), and auto-notify on completion — without tmux, cmux, or the agent-teams machinery (whose spawn path is broken; see remove-claude-team.md). Native background sessions (claude --bg/attach/logs/stop/respawn), where the flag exists, add an OS-bound option for cron drivers. Codex multi_agent gives push steering (send_input) and a native approval overlay in both the CLI and the desktop app.
Why AEP still owns the worktree:
- Host-managed worktrees pin their paths (
.claude/worktrees/,
$CODEX_HOME/worktrees) and hide them from the orchestrator's monitor() path. AEP's git worktree add .feature-workspaces/<ws> keeps the location stable and main-visible; native workers are pointed at it by process cwd (enforced) or prompt contract (no hooks — see backends.md).
Why the single `legacy` pin exists (a narrow exception to "no pins"):
- Detection can't distinguish "tmux is installed" from "the user wants the
tmux+cmux workflow". Since native modes now outrank tmux on Claude Code, the users who _prefer_ cmux's clickable tabs need one explicit lever: git config aep.executor-backend tmux (or "…with tmux"). Everything else remains automatic.
Why session-bound vs OS-bound is a first-class axis:
- native-bg-subagents and Codex subagents die with their parent session; bg
sessions, exec workers, and tmux sessions don't. An orchestrator's periodic driver (long-lived /loop vs cron one-shots) therefore constrains the mode — the compatibility matrix in backends.md makes that explicit, and orphan re-adoption (via the real-liveness probe, not roster membership) makes lead restarts non-fatal.
Why human gates are hub-and-spoke (main agent as the console):
- The human shouldn't have to chase worker surfaces. Every mode records the
gate in needs-human.md; the question flows to the main agent, which asks the human and relays the answer. Steerable modes deliver the answer by push (block-in-place); batch/pull modes (native-bg-subagent, workflow, headless, codex-exec, claude-bg) use gate-and-park — the worker commits WIP, returns cleanly, and is resumed into the same worktree with the answer. Parking is cheap because all worker state lives in the worktree + .dev-workflow/, never only in agent context. Direct surfaces (TaskOutput, attach, threads) remain optional conveniences.
Why autopilot needs a steerable, driver-compatible mode:
nudge()presupposes a worker you can reach mid-flight.workflowand
headless collapse a build into one autonomous unit with no mid-stage surface — autopilot does not drive them (the workflow is its own orchestrator; gate-and-park still gives both a human-gate path through the main agent that launched them). All other modes are steerable — native-bg-subagent via SendMessage(to: agentId) + feedback.md, claude-bg degraded to pull-based nudging.
---
Next Step
After detecting/spawning, control returns to the calling skill:
/aep-launch→ the bootstrap was the spawn prompt (native modes) or sent over
tmux (legacy), then /aep-build runs in the workspace
/aep-autopilot→ resumes its tick loop/aep-dispatch→ completes the handoff
Executor Backends
Detection, mode selection, and the cross-backend protocols that make /aep-launch, /aep-build, /aep-autopilot, and /aep-wrap host-agnostic. Read this before spawning or steering any workspace agent. Per-operation recipes live in three sibling files:
| Recipe file | Modes |
|---|---|
| `claude-native.md` | native-bg-subagent, claude-bg |
| `codex-native.md` | codex-subagent, codex-exec |
| `tmux-session.md` | legacy (tmux + optional cmux) |
---
Table of Contents
1. The Mode Matrix 2. Detection 3. Mode Selection 4. Driver × Backend Compatibility 5. Common Recipes (all modes) 6. The Human-Gate Protocol 7. Mode: workflow (dynamic-workflow fan-out) 8. Orphan Re-adoption 9. The Worktree-Context Constraint 10. Legacy B1–B4 Mapping
---
The Mode Matrix
Native modes come first; tmux is the explicit-pin / generic-host fallback. Lifetime is the axis that matters for orchestration: _session-bound_ workers (native-bg-subagents, Codex subagents) die with the orchestrator session; _OS-bound_ workers (bg sessions, exec processes, tmux sessions) survive it.
| Mode | Backend | Lifetime | Spawn | Nudge | Human gate | Present |
|---|---|---|---|---|---|---|
| native-bg-subagent | Agent tool run_in_background, no team | session-bound | Agent tool run_in_background: true, no `team_name`, no active team | feedback.md (pull); SendMessage(to: agentId) best-effort | gate-and-park → main agent (re-spawn w/ answer) | TaskOutput / JSONL output_file |
| claude-bg | native background sessions | OS-bound | cd <worktree> && claude --bg _(only if BG_AVAILABLE; see note)_ | feedback.md (pull); stop/respawn if hard-stuck | gate-and-park → main agent (resume w/ answer); claude attach optional | claude attach / claude logs |
| codex-subagent | native multi_agent | session-bound | spawn_agent(role=aep-builder) | send_input (push) | approval overlay + needs-human.md | /agent (CLI) / thread list (app) |
| codex-exec | headless exec workers | OS-bound | codex exec --cd <worktree> (bg process) | codex exec resume <id> | gate-and-park → main agent (exec resume w/ answer) | signals + PR |
| legacy | tmux session (+ cmux tab) | OS-bound | tmux new-session | tmux send-keys | needs-human.md + tmux attach | cmux tab / tmux attach |
| workflow | CC dynamic workflow fan-out | session-bound | Workflow tool pipeline | none mid-stage (steer at stage boundaries) | gate-and-park → main agent (structured gated result + needs-human.md) | /workflows + signals |
| headless | one-shot native subagent | session-bound | Task/Agent tool, worktree-bound | none | gate-and-park → main agent (re-spawn w/ answer) | signals + PR |
`claude-team` was removed (2026-06). On Claude Code ≥ 2.1.x the agent-teams
spawn path fails silently: the teams runtime pastes the long
claude … --agent-id <name>@<team> --settings '<big JSON>' launch command intoa detachedclaude-swarm-<pid>tmux pane, the--settingsJSON is **truncated
mid-string and never submitted**, so no worker process ever starts — yet the
team roster still lists the member as "active". A live team also **poisons
teamless background spawns** (they auto-route through the same broken tmux
backend). native-bg-subagent replaces it as the Claude Code default. Seedocs/decisions/remove-claude-team.md.Announce the selection. Before spawning, state which mode and why — e.g. "Claude Code → native-bg-subagent: in-process background subagent (Agent tool, run_in_background, no team); pull-steer via feedback.md; verified live by the post-spawn liveness probe."
`native-bg-subagent` success signature. A working spawn returns a
bare-hex `agentId` (e.g.adfb6cb206155a92e) with a JSONLoutput_file,
not an @<team> id. It is non-blocking and auto-notifies on completion.(A foreground in-process subagent also works but blocks the orchestrator turn.)
Pre-spawn: if any team is active, TeamDelete it first — a live teamre-routes teamless spawns into the broken agent-teams tmux backend.
---
Detection
detect() resolves the host, its two executor commands, the native capabilities, any explicit pin, and the presentation surface.
# --- HOST + executor commands ---
# $EXECUTOR interactive session (stays alive — legacy/tmux, evaluator panes)
# $EXECUTOR_EXEC headless one-shot (runs the given prompt to completion, exits)
if [ -n "$CLAUDECODE" ]; then
HOST=claude
EXECUTOR="claude --dangerously-skip-permissions" # interactive; NO -p
EXECUTOR_EXEC="claude -p --dangerously-skip-permissions" # -p/--print = non-interactive
READY_GREP='❯'
elif command -v codex >/dev/null 2>&1 && { [ -n "$CODEX_HOME" ] || env | grep -q '^CODEX_'; }; then
HOST=codex
EXECUTOR="codex --dangerously-bypass-approvals-and-sandbox"
EXECUTOR_EXEC="codex exec --dangerously-bypass-approvals-and-sandbox"
READY_GREP=''
else
HOST=generic
EXECUTOR="${AEP_EXECUTOR:-}"; EXECUTOR_EXEC="${AEP_EXECUTOR_EXEC:-$EXECUTOR}"; READY_GREP=''
fi
[ -z "$EXECUTOR" ] && { echo "executor unresolved — set \$AEP_EXECUTOR or run under Claude Code / Codex"; }
# --- NATIVE CAPABILITIES ---
# NOTE: agent-teams (the old TEAMS_AVAILABLE / CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
# gate) is NO LONGER consulted — claude-team was removed (silent spawn failure;
# see the mode-matrix note). Do not select a mode from the teams flag alone.
BG_AVAILABLE=$([ "$HOST" = claude ] && claude --help 2>/dev/null | grep -q -- '--bg' && echo yes || echo no)
# ^ On Claude Code ≥ 2.1.x the one-shot `claude --bg` spawn flag was REMOVED
# (background agents are now the interactive `claude agents` view, not a
# scriptable flag). On such builds BG_AVAILABLE=no and claude-bg is skipped —
# the Claude Code default is native-bg-subagent (session-bound). If you need an
# OS-bound Claude worker for a cron/launchd driver and `--bg` is gone, that
# driver is unsupported on Claude Code — use Codex codex-exec or a long-lived
# in-session goal/loop driver instead.
MULTI_AGENT_AVAILABLE=$([ "$HOST" = codex ] && codex features list 2>/dev/null | grep -q 'multi_agent.*true' && echo yes || echo no)
WORKFLOW_CAPABLE=$([ "$HOST" = claude ] && echo yes || echo no) # the host agent knows it has the Workflow tool
# --- EXPLICIT PIN (the single manual lever besides "…with workflow") ---
PIN=$(git config --get aep.executor-backend 2>/dev/null || true) # e.g. "tmux"
# --- PRESENTATION (for legacy mode): cmux needs a reachable CLI + a target pane,
# NOT $CMUX_SOCKET (the CLI drives cmux over its socket even when unset) ---
CMUX="$(command -v cmux || echo /Applications/cmux.app/Contents/Resources/bin/cmux)"
if [ -x "$CMUX" ] && { "$CMUX" tree 2>/dev/null | grep -q '◀ here' || [ -n "$CMUX_PANE_ID" ]; }; then
PRESENT=cmux
elif command -v tmux >/dev/null 2>&1; then
PRESENT=tmux
else
PRESENT=none
fiCorrect CLI invocations (verified against Claude Code 2.1.161+ / Codex 0.130.0):
>
| | interactive session →$EXECUTOR| headless one-shot →$EXECUTOR_EXEC|
| ---------- | -------------------------------------------------- | ------------------------------------------------------- |
| claude |claude --dangerously-skip-permissions|claude -p --dangerously-skip-permissions|
| codex |codex --dangerously-bypass-approvals-and-sandbox|codex exec --dangerously-bypass-approvals-and-sandbox|
>
--rc is not a real Claude Code flag. Codex's full-bypass flag is--dangerously-bypass-approvals-and-sandbox(no--yolo/--full-auto).
Orchestrator lifetime is not shell-probable — the agent knows it. You are _long-lived_ when you're an interactive session or a /loop-driven session (Claude Code) or a living Codex main thread (desktop or interactive CLI). You are _ephemeral_ when this invocation is a cron/launchd-spawned one-shot (e.g. a scheduled codex exec autopilot tick). Session-bound modes require a long-lived orchestrator.
---
Mode Selection
Apply in order; first match wins. workflow is the only natural-language opt-in; legacy is the only pin.
workflow IF user explicitly opted in ("…with workflow") AND WORKFLOW_CAPABLE
legacy IF PIN == tmux, or user said "…with tmux"
native-bg-subagent ELIF HOST == claude AND orchestrator is long-lived # default on Claude Code
claude-bg ELIF HOST == claude AND BG_AVAILABLE # OS-bound (cron); only if `claude --bg` exists
codex-subagent ELIF HOST == codex AND MULTI_AGENT_AVAILABLE AND orchestrator is a living main thread
codex-exec ELIF HOST == codex
legacy ELIF PRESENT == cmux or PRESENT == tmux (generic hosts)
headless ELSEBehavior change (2026-06): `claude-team` removed. The agent-teams spawn path fails silently on Claude Code ≥ 2.1.x (see the mode-matrix note), so it is no longer selectable. The Claude Code default is now native-bg-subagent (Agent tool, run_in_background, no team). The teams env flag is ignored. There is no "…with agent team" opt-in — the backend is broken, not merely de-prioritized.
Behavior change vs v1.x: Claude Code with tmux installed no longer auto-selects tmux — native modes win. Users who want the tmux+cmux workflow back pin it: git config aep.executor-backend tmux.
Post-spawn liveness probe is mandatory (never trust a flag or roster).
Selecting a mode does NOT mean its spawn worked. After ANY spawn, before
declaring the worker "running", run the probe in
Post-Spawn Liveness Probe. On failure, tear the
dead spawn down and auto-fall-back to `native-bg-subagent`.
---
Driver × Backend Compatibility
An orchestrator (notably /aep-autopilot) is driven either by a long-lived session or by a cron/launchd scheduler that starts a fresh session per tick. The long-lived class has two in-session variants — the goal driver (/goal, native on both hosts, the autopilot default, self-terminating per layer) and the fixed-interval loop driver (/loop; a living Codex main thread ticking in-thread). Both are equally compatible with every steerable mode. Session-bound workers cannot outlive their parent, so:
| Driver | native-bg-subagent | claude-bg | codex-subagent | codex-exec | legacy |
|---|---|---|---|---|---|
Long-lived session (/goal or /loop, living main thread) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Cron/launchd (fresh session per tick) | ❌ bg subagent dies with the session | ✅ OS-level, any session attaches | ❌ subagents invisible to a new session | ✅ codex exec resume works cross-process | ✅ |
A consumer that needs steering (autopilot) must pick a mode compatible with its driver: on Claude Code, /goal (or /loop) + native-bg-subagent (or claude-bg where --bg exists); on Codex, in-thread /goal (or manual ticks)
codex-subagent, or cron ticks
codex-exec. The goal driver is in-session only — it cannot drive a
fresh-session-per-tick scheduler, so the cron/launchd row is always the /loop/codex exec path.
---
Common Recipes (all modes)
Worktree creation — always AEP-owned, before any spawn
# Resolve $BASE (integration branch) — see git-ref "Integration Branch" (override → develop → main)
BASE=$(git config --get aep.integration-branch 2>/dev/null || true)
[ -z "$BASE" ] && { git show-ref --verify --quiet refs/heads/develop \
|| git show-ref --verify --quiet refs/remotes/origin/develop; } && BASE=develop
BASE=${BASE:-main}
mkdir -p .feature-workspaces
git worktree add -b feat/<ws> .feature-workspaces/<ws> "$BASE"Every mode points its worker at this directory. OS-bound modes bind by process cwd (enforced); session-bound native modes bind by prompt contract (AEP's no-hooks decision: rely on model capability + skill instructions — do not install WorktreeCreate hooks or redirect host-managed worktree paths).
monitor(ws) — host-independent, never changes
cat .feature-workspaces/<ws>/.dev-workflow/signals/status.json
ls .feature-workspaces/<ws>/.dev-workflow/signals/ready-for-review.flag 2>/dev/null
ls .feature-workspaces/<ws>/.dev-workflow/signals/needs-human.md 2>/dev/nullMid-flight feedback is written the same way for every mode (push channels are an acceleration layer, not a replacement — the file is the durable record):
cat >> .feature-workspaces/<ws>/.dev-workflow/signals/feedback.md <<'EOF'
## <date> <time>
Priority: high
<feedback>
EOFWorktree removal — teardown() tail, all modes
# (mode-specific session/agent teardown first — see the recipe files)
git worktree remove .feature-workspaces/<ws> \
|| git worktree remove --force .feature-workspaces/<ws>
git worktree prunePost-Spawn Liveness Probe
Run after EVERY spawn, before declaring the worker "running". A spawn call returning, a flag being set, or a roster/state entry saying "active" is NOT evidence the worker started. (The removed claude-team mode failed exactly here: the launch command was truncated in a tmux pane and never submitted, yet the team roster still showed the member "active" — a silently dead worktree the autopilot only flagged 30+ minutes later.)
A worker is live only if BOTH hold within N seconds (default 90):
1. Process / agent exists — a real worker is running:
- native-bg-subagent: the bg agent appears in
TaskListAND its spawn returned
a bare-hex `agentId` with a JSONL output_file (not an @<team> id)
- claude-bg:
claude agents --jsonshows the sessionrunning - codex-subagent:
list_agentsshows<agent_id> - codex-exec: the
codex execPID is alive - legacy:
pane_current_commandisclaude(notzsh) — azshpane means the
launch command never submitted 2. Worktree shows activity — .dev-workflow/signals/status.json was written OR git -C .feature-workspaces/<ws> diff --stat is non-empty.
bash .claude/skills/aep-executor/scripts/spawn-liveness-probe.sh <ws> <agent_id> [N]
# exit 0 = live; exit 1 = dead spawn (probe failed)On probe failure (dead spawn):
1. Tear down the dead remnant — kill the stuck pane/process, and if a team got created during the attempt, TeamDelete it (a live team poisons the fallback). 2. Do not mark the story failed and do not leave the worktree for the autopilot to time-out on later. 3. Auto-fall-back to `native-bg-subagent` (Agent tool, run_in_background, no team) into the existing worktree, then probe again. native-bg-subagent is the terminal fallback — if it also fails the probe, only then escalate.
This contract is what /aep-launch Step 4 and the autopilot orphan/stuck checks both consume — "roster/state says active" is never accepted as liveness.
check(prompt, schema) — cheap, context-isolated analysis
Run a read-only analysis prompt in a throwaway, cheap-model agent and return its structured JSON. The point is context isolation: the verbose reading (state file + every workspace signals/, gh pr view, …) happens in the cheap agent's own context — only the small JSON result crosses back. The check never reads workspace code (signals only).
Claude Code — Haiku subagent:
Agent/Task tool: model haiku; tools Read, Bash, Glob;
prompt: <the analysis prompt; OUTPUT ONLY the JSON in `schema`>Codex — `codex exec` cheap one-shot:
codex exec -m gpt-5.4-mini -c model_reasoning_effort=low \
-C "$PWD" --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox \
--output-schema /tmp/aep-check.schema.json -o /tmp/aep-check.out.json \
"<the analysis prompt>" < /dev/null
jq . /tmp/aep-check.out.jsonResult schema (the CHECK → ACT contract):
{
"summary": "string — one-line human-readable status",
"state_written": true,
"actions": [
{
"type": "nudge | wrap | launch | escalate | design",
"workspace": "string | null",
"story_id": "string | null",
"message": "string | null — exact text for a nudge",
"reason": "string | null — for escalate/design"
}
]
}---
The Human-Gate Protocol
A worker mid-build can hit a decision only the human can make (design ambiguity, eval non-convergence, Phase 11.5 manual QA). The record is host-agnostic; the transport is per-mode — but the canonical human console is the main agent (hub-and-spoke): the worker's question flows back to the orchestrator, the orchestrator asks the human (AskUserQuestion / plain text in the main session), and relays the answer to the worker. The human never _has_ to visit a worker's surface; per-mode direct interaction (TaskOutput, claude attach, Codex thread, tmux attach) is an optional convenience.
The record (always, every mode): the worker appends to .dev-workflow/signals/needs-human.md and sets "blocked_on": "human" in status.json:
## <ISO8601> — <phase>
**Question:** <the decision needed, with the options considered>
**Context:** <why the worker can't decide autonomously>Two gate styles. The worker's behavior after recording the gate depends on whether its mode has a push channel back into it:
- Block-in-place (steerable modes — codex-subagent, legacy):
the worker raises the gate, keeps doing whatever doesn't depend on the answer, and waits. The answer arrives on the mode's push transport.
- Gate-and-park (batch/pull modes — native-bg-subagent, workflow, headless,
codex-exec, and claude-bg): there is no push channel into a running worker, so the worker parks: commit WIP (or leave the tree clean), update status.json (blocked_on: "human", current phase), then end its run cleanly. The orchestrator detects the gate, gets the human's answer, and resumes a worker into the same worktree — the same recipe as orphan re-adoption, with the answer prepended: "The human decided: <answer>. Run bash .dev-workflow/init.sh to recover state, mark the needs-human entry resolved, then continue the /aep-build flow." Parking is cheap because all state lives in the worktree + .dev-workflow/, not in the agent's context.
The transport (per mode):
| Mode | Style | Worker raises it via | Main agent relays the human's answer via | Optional direct surface |
|---|---|---|---|---|
| native-bg-subagent | gate-and-park | the file (orchestrator detects on next tick) | re-spawn a bg subagent into the same worktree with recovery bootstrap + answer | TaskOutput while it runs |
| claude-bg | gate-and-park | the file (orchestrator detects on next tick) | resume the session with the answer (claude -r <id> ...), or respawn w/ recovery bootstrap + answer | claude attach <id> while it runs |
| codex-subagent | block-in-place | native approval overlay (approvals) / ask the parent thread (decisions) | send_input(<id>, "<answer>") | open the thread (o / app click) |
| codex-exec | gate-and-park | the file (orchestrator detects on next tick) | codex exec resume <id> "<answer>" | — (headless) |
| workflow | gate-and-park | stage returns a structured gated result + the file | continuation run for gated stories with the answer in the prompt (see Mode: workflow) | — (batch) |
| headless | gate-and-park | the file; the one-shot subagent returns with a gated result | re-spawn a one-shot into the same worktree with recovery bootstrap + answer | — (one-shot) |
| legacy | block-in-place | the file | executor.nudge() (tmux send-keys) or feedback.md | tmux attach -t <ws> |
Resolution: after acting on the answer, the worker appends resolved: <summary> under its entry and clears blocked_on. The autopilot escalation queue consumes the same file — an unresolved needs-human.md entry becomes an escalation whose expected_human_action is "answer in the main session" plus the mode-specific relay recipe above. A parked workspace counts as waiting, not stuck and not failed.
---
Mode: workflow (dynamic-workflow fan-out)
This mode is the narrow use of dynamic workflows — running one dispatched
build wave as a fan-out. For the general dynamic-workflow pattern catalog
(classify-route, fan-out/synthesize, adversarial verify, generate-filter,
tournament, loop-until-done) and the judgment of _when a task warrants a workflow
at all_, see `/aep-workflow`.
Claude Code's Workflow tool builds a whole dispatched wave as one deterministic fan-out: one build agent per locked story, each with per-agent worktree isolation, with /aep-dispatch authoring the script (the "…with workflow" path bypasses /aep-launch). With hub-and-spoke gating this is a complete backend, not just a fire-and-forget batch: gates park and return to the main agent for confirmation, then gated stories resume.
// sketch — one agent per story; gates surface in the structured result.
// `stories` is the dispatched wave: { change, worktree, bootstrap } per item.
const BUILD_RESULT = {
type: "object",
properties: {
status: { enum: ["completed", "gated", "failed"] },
question: { type: "string" }, // set when status == "gated" (mirror of needs-human.md)
summary: { type: "string" },
},
required: ["status"],
};
const results = await pipeline(
stories,
(s) =>
agent(
`You operate EXCLUSIVELY in ${s.worktree}. Run /aep-build for OpenSpec change ${s.change}. ${s.bootstrap}
If you hit a decision only the human can make: append it to .dev-workflow/signals/needs-human.md,
set blocked_on:"human" in status.json, commit WIP, and RETURN status "gated" with the question —
do not guess and do not wait.`,
{ phase: "Build", schema: BUILD_RESULT },
),
(built, s) =>
built.status === "completed"
? agent(`Adversarially verify the build for ${s.change} in ${s.worktree}.`, {
phase: "Verify",
schema: BUILD_RESULT,
})
: built,
);
return results;Gate handling (main agent, after the workflow returns): collect status: "gated" items, ask the human each question (AskUserQuestion), then resume each gated story into its existing worktree — either a continuation workflow over the gated subset or individual re-launches — with the recovery bootstrap + the answer ("The human decided: <answer>…"). Workflow resumeFromRunId makes the continuation cheap (completed agents return from cache). Mid-stage there is still no push nudge — steering happens at stage boundaries and through gates.
monitor() is unchanged (the build agents still write signals); progress is also visible in the /workflows view. Autopilot does not drive this mode — the workflow is its own orchestrator; its gates surface to the main agent that authored it.
AEP-created worktrees vs isolation: 'worktree': prefer creating the.feature-workspaces/<ws> worktrees first (launch guardrails apply) andpassing the path in the prompt, so monitor()/wrap paths stay standard. TheWorkflow tool's own isolation: 'worktree' puts agents in host-managedpaths — acceptable for ad-hoc batches, but then signals live outside
.feature-workspaces/and/aep-wrapdoes not apply.
---
Orphan Re-adoption
Session-bound workers (native-bg-subagents, Codex subagents) die when the orchestrator session dies, but their work does not — it lives in the worktree and .dev-workflow/. When an orchestrator (re)starts and finds state claiming an active workspace, decide orphan-vs-live by the real-liveness probe, never by roster/state membership (a roster can show a never-started worker as "active" — the claude-team failure mode). Treat as an orphan when the Post-Spawn Liveness Probe fails — the agent process is gone (TaskList / list_agents / claude agents empty) or the worktree shows no activity:
1. Treat it as an orphan, not a failure — do not mark the story failed. 2. Read signals/status.json for the last known phase. 3. Re-launch a worker into the existing worktree with the current mode's spawn recipe and a recovery bootstrap: "Run bash .dev-workflow/init.sh to recover state, read .dev-workflow/signals/feedback.md, then continue the /aep-build flow from the current phase." 4. Record the new agent_id in orchestrator state.
This is why worker progress must always flow through signals + commits, never live only in an agent's context.
---
The Worktree-Context Constraint
Every spawned worker and evaluator MUST be bound to the workspace worktree — by process cwd (claude-bg, codex-exec, legacy, evaluator execs) or by prompt contract (native-bg-subagent, codex-subagent, headless).
This is not optional. The autopilot orchestrator boundary forbids spawning a reviewer/agent "from main" precisely because such an agent lacks the workspace's files, git state, and eval history. Binding the spawned agent to the worktree gives it exactly that context, so the boundary's intent is satisfied under every mode. The gen/eval separation (generator ≠ evaluator) and the rule that the main session never reads workspace code both still hold — only the spawn mechanism changes.
Codex caveat: spawn_agent has no cwd parameter, so the codex-subagent binding is a directory contract hardened by the aep-builder role's developer_instructions (see codex-native.md). The contract stays inside the workspace-write sandbox because .feature-workspaces/ is under the project root. Hard enforcement is available via codex-exec.
---
Legacy B1–B4 Mapping
For readers of v1.2–v1.5 docs and ADRs:
| Old | New |
|---|---|
| B1 (tmux + cmux tab) | legacy with cmux present |
| B2 (tmux only) | legacy |
| B3 (native subagent) | codex-subagent (Codex) / headless (one-shot fallback) |
| B4 (dynamic workflow) | workflow |
New in v1.6: claude-bg, codex-exec, the human-gate protocol, and orphan re-adoption. See docs/decisions/native-first-executor.md.
Removed 2026-06: claude-team (silent agent-teams spawn failure) — replaced by `native-bg-subagent` as the Claude Code default, plus the mandatory Post-Spawn Liveness Probe. See docs/decisions/remove-claude-team.md.
Claude Code Native Backends — native-bg-subagent & claude-bg
Per-operation recipes for the two Claude Code native modes. Both replace tmux — and the removed `claude-team` — with capabilities built into Claude Code; neither requires tmux, cmux, agent teams, or any hook. Detection and selection live in backends.md — read that first, including the mandatory Post-Spawn Liveness Probe.
`claude-team` was removed (2026-06). On Claude Code ≥ 2.1.x the agent-teams
spawn path fails silently — the launch command is truncated in a detached
claude-swarm-<pid> tmux pane and never submitted, so no worker starts, yet theteam roster still reports the member "active". A live team also **poisons
teamless background spawns** (they re-route through the same broken backend).
native-bg-subagent is the replacement default. Seedocs/decisions/remove-claude-team.md.| Mode | Mechanism | Lifetime | Steering | Human gate |
|---|---|---|---|---|
| native-bg-subagent | Agent tool run_in_background, no team | session-bound (dies with the orchestrator) | SendMessage(to: agentId) / feedback.md | gate-and-park → main agent re-spawns w/ answer |
| claude-bg | native background sessions (claude --bg, if present) | OS-bound (survives the lead session) | feedback.md (pull) + stop/respawn | gate-and-park → main agent relays via session resume |
---
Mode: native-bg-subagent (Claude Code default)
A native in-process background subagent: the Agent tool with run_in_background: true, no `team_name`, spawned while no team is active. It runs asynchronously in the orchestrator session, is non-blocking, and auto-notifies on completion. This is the Claude Code default — it needs no tmux, no agent-teams flag, and no --bg CLI flag.
Success signature (how to know the spawn actually worked)
A working spawn returns a bare-hex `agentId` (e.g. adfb6cb206155a92e) with a JSONL output_file — not an @<team> id. Record that agentId as the workspace agent_id. Then run the [Post-Spawn Liveness Probe](backends.md#post-spawn-liveness-probe) before declaring the worker running.
Prerequisite: no active team
# A live agent-teams team re-routes EVEN teamless background spawns through the
# broken agent-teams tmux backend. If a team exists, shut its members down and
# TeamDelete it BEFORE spawning.
list_agents / TaskList → if a team "aep" exists: shutdown members, then TeamDeletespawn(ws, branch, bootstrap_prompt)
The worktree is created by AEP first (common recipe in backends.md). Then spawn a background subagent with the Agent tool — no `team_name`:
Agent tool:
run_in_background: true
# NO team_name — a team (active or newly created) routes through the broken backend
prompt: |
You operate EXCLUSIVELY in <abs-repo-path>/.feature-workspaces/<ws>
on branch feat/<ws>. cd there first; never edit files outside it.
<bootstrap_prompt> # the /aep-build bootstrap, incl. Prior Lessons
Report progress through .dev-workflow/signals/status.json at phase
boundaries. If you hit a decision only the human can make, follow the
human-gate protocol: append to .dev-workflow/signals/needs-human.md,
set "blocked_on": "human" in status.json, commit WIP, and end your run.Capture the returned bare-hex `agentId` + output_file → state agent_id. The worktree binding is a prompt contract (AEP's no-hooks decision). Then run the liveness probe; on failure follow the re-dispatch-on-failure contract (since this mode is itself the terminal fallback, a probe failure here escalates).
nudge(ws, msg)
SendMessage(to: <agentId>, message: <msg>) # continues the background subagentSendMessage re-activates a previously spawned background subagent with the message. Always also append the same text to signals/feedback.md (the file is the durable, host-agnostic record the worker reads at phase boundaries). If the agent is unreachable / already exited, fall back to the hard-stuck path below.
Hard-stuck (no progress ≥ 6 ticks): TaskStop <agentId>, then re-spawn into the worktree with a recovery prompt — the worktree and .dev-workflow/ carry all state:
Agent tool: run_in_background: true, no team_name, prompt:
"Run bash .dev-workflow/init.sh to recover state, read
.dev-workflow/signals/feedback.md, then continue the /aep-build flow from the
current phase." # record the NEW agentId in stateliveness(ws)
TaskList # is <agentId> still running?
git -C .feature-workspaces/<ws> diff --stat # host-independent corroborationApply the Post-Spawn Liveness Probe: the agent must exist in TaskList AND the worktree must show activity. Never accept "state says active" as liveness. A TaskList-absent agent with worktree progress is an orphan (session restarted) → re-adopt per backends.md.
present(ws)
TaskOutput <agentId> # the JSONL output_file — recent worker outputPlus signals/status.json + the PR. There is no live pane; this mode is a background worker surfaced through its task output and signals.
gate(ws) — human gate (gate-and-park)
There is no guaranteed push channel into a mid-run background subagent, so the worker parks: append to needs-human.md + blocked_on: human, commit WIP, end the run cleanly. The orchestrator detects the gate on its next tick, asks the human in the main session (hub-and-spoke), and re-spawns a bg subagent into the same worktree with the answer + recovery bootstrap:
Agent tool: run_in_background: true, no team_name, prompt:
"The human decided: <answer>. Run bash .dev-workflow/init.sh to recover state,
mark the needs-human entry resolved, clear blocked_on, and continue the
/aep-build flow." # record the new agentIdspawn_evaluator(ws, role)
The background subagent spawns a foreground Task subagent in its own context (one level — a subagent may spawn one subagent) with the evaluator prompt; it inherits the worktree via the prompt contract. The evaluator prompt (from aep-gen-eval/references/agent-contracts.md) is the spawn prompt. Poll for eval-response-<N>.md as a sanity check.
teardown(ws)
TaskStop <agentId> 2>/dev/null || true # stop the bg subagent if still running
# then the common worktree removal from backends.md---
Mode: claude-bg
One native background session per story. claude --bg starts a full Claude instance detached from the current session; it is OS-bound — it survives lead restarts and is attachable from any terminal. This is the only Claude Code mode that survives a cron/launchd fresh-session-per-tick driver (see the driver × backend matrix in backends.md).
`--bg` availability (verify per build). On Claude Code ≥ 2.1.x the one-shot
claude --bgspawn flag was removed —claude agentsis now an interactive
_agent view_, not a scriptable one-shot spawn. The capability probe below gates
this mode: when--bgis absent,BG_AVAILABLE=noand detection skips
claude-bg, leaving native-bg-subagent (session-bound) as the Claude Code
default. If a build re-introduces a scriptable background-spawn flag, update the
spawn recipe here accordingly.
Capability probe
claude --help 2>/dev/null | grep -q -- '--bg' && echo "claude-bg available"spawn(ws, branch, bootstrap_prompt)
The process cwd is the isolation — this mode hard-binds the worker to the worktree, no prompt contract needed:
cd .feature-workspaces/<ws> && claude --bg --dangerously-skip-permissions "<bootstrap_prompt>"
# capture the printed session id → state agent_id; then run the liveness probe
cd - >/dev/nullRecord the session id in orchestrator state (agent_id). List/inspect at any time:
claude agents --json # all background sessions + status
claude logs <id> | tail -40 # recent outputnudge(ws, msg) — degraded (pull)
Background sessions take no push input mid-turn. Two-tier nudge:
1. Normal: append to signals/feedback.md (workers read it at phase boundaries — the existing protocol). 2. Hard-stuck (no progress ≥ 6 ticks): stop and respawn with a recovery prompt — the worktree and .dev-workflow/ carry all state:
claude stop <id>
cd .feature-workspaces/<ws> && claude --bg --dangerously-skip-permissions \
"Run bash .dev-workflow/init.sh to recover state, read .dev-workflow/signals/feedback.md, then continue the /aep-build flow from the current phase."
cd - >/dev/null # record the NEW session id in stateliveness(ws)
claude agents --json | jq '.[] | select(.id=="<id>")' # running / exited
claude logs <id> | tail -5 # output still moving?
git -C .feature-workspaces/<ws> diff --stat # corroborationApply the Post-Spawn Liveness Probe: process exists AND worktree shows activity — never roster/state alone.
present(ws)
claude attach <id> # interactive attach — the native replacement for tmux attachgate(ws) — human gate (gate-and-park)
There is no push channel into a running bg session, so the worker parks: append to needs-human.md + blocked_on: human, commit WIP, end the run cleanly. The orchestrator detects the gate on its next tick, asks the human in the main session (hub-and-spoke — the human does not need to attach), and relays the answer by resuming the worker:
# Resume the same session with the answer (preferred — context intact):
claude -r <agent_id> --bg --dangerously-skip-permissions \
"The human decided: <answer>. Mark the needs-human entry resolved, clear blocked_on, and continue the /aep-build flow."
# Fallback (session not resumable): respawn in the worktree with the recovery bootstrap + answer.
# Record the (new) session id as agent_id.Optional direct surface: while the worker is running, claude attach <id> also works (a blocking permission prompt holds the session and attach surfaces it) — a convenience, not the protocol.
spawn_evaluator(ws, role)
The bg session is a full Claude instance running in the worktree — it spawns a foreground Task subagent with the evaluator prompt.
teardown(ws)
claude stop <id> 2>/dev/null || true
claude rm <id> 2>/dev/null || true # remove from the agents list (transcript kept)
# then the common worktree removal from backends.mdCodex Native Backends — codex-subagent & codex-exec
Per-operation recipes for the two Codex modes. Both apply to the Codex CLI and the Codex desktop app — they share the same Rust runtime, and multi_agent is stable and on by default from runtime 0.130.0 (no app-side toggle). Detection and selection live in backends.md — read that first.
| Mode | Mechanism | Lifetime | Steering | Human gate |
|---|---|---|---|---|
| codex-subagent | native multi_agent (spawn_agent) | session-bound (dies with the parent thread) | send_input (push) | native approval overlay + needs-human.md |
| codex-exec | headless codex exec --cd workers | OS-bound (independent processes) | codex exec resume <id> | gate-and-park → main agent relays via exec resume |
---
The worktree reality (read before choosing)
spawn_agent has no cwd/worktree parameter — subagents share the parent's workspace and sandbox. The Codex app's own "Worktree" environment pins worktrees under $CODEX_HOME/worktrees (path not configurable). So for AEP's invariant — worktree at .feature-workspaces/<ws> — the binding under codex-subagent is a directory contract (prompt + role instructions), not enforcement.
Why the contract is safe in practice: with the parent thread rooted at the repo and workspace-write sandboxing, the writable boundary is the project root — .feature-workspaces/<ws> is inside it, and so is the linked worktree's git metadata (.git/worktrees/...). The worker can do all its git work in the worktree without leaving the sandbox. When the user demands _hard_ cwd enforcement, use codex-exec instead (the process cwd is the worktree).
Custom agent roles (ship with the project)
Commit these to the project's .codex/agents/ — they are project-scoped, so both the CLI and the desktop app discover them in any checkout or worktree.
.codex/agents/aep-builder.toml:
name = "aep-builder"
description = "AEP workspace builder — implements one story inside its assigned git worktree"
developer_instructions = """
You are an AEP workspace builder. Your FIRST action is to cd into the absolute
worktree path given in your prompt (under .feature-workspaces/), then VERIFY it:
`git rev-parse --show-toplevel` MUST be under .feature-workspaces/ and the branch
MUST be feat/<ws> (not the integration branch). If not, do NOT proceed in the
main checkout — run the /aep-build Phase 0 worktree guard to self-heal (it cd's
into or creates the worktree). `spawn_agent` shares the parent's cwd, so this is
the soft binding the Phase 0 guard backstops — never create feat/<ws> in the main
checkout. You operate EXCLUSIVELY inside that directory on its feat/<ws> branch.
Never edit the main checkout or any other worktree. Report progress through
.dev-workflow/signals/status.json at phase boundaries; read
.dev-workflow/signals/feedback.md at phase starts. If you hit a decision only
the human can make, append it to .dev-workflow/signals/needs-human.md and set
"blocked_on": "human" in status.json, then ask the parent thread.
""".codex/agents/aep-evaluator.toml:
name = "aep-evaluator"
description = "AEP evaluator — scores a workspace build against its criteria, never fixes"
developer_instructions = """
You are an AEP EVALUATOR. You work inside the worktree directory given in your
prompt. Read evaluator-criteria.md, the eval-request, the OpenSpec change, and
the git diff against the integration branch. Score honestly per the criteria;
apply hard failure thresholds strictly; never modify code and never modify
verification_steps. Write your findings to
.dev-workflow/signals/eval-response-<N>.md and update pass/fail fields in
.dev-workflow/feature-verification.json, then stop.
"""---
Mode: codex-subagent
One native subagent per story, spawned from the orchestrator's living main thread (desktop thread or interactive CLI session). Session-bound: subagents die with the parent thread, so this mode requires the orchestrator itself to be long-lived (see the driver × backend matrix in backends.md).
Concurrency: agents.max_threads (config.toml, default 6) caps concurrent subagents — effective WIP limit is min(concurrency_limit, max_threads).
spawn(ws, branch, bootstrap_prompt)
AEP creates the worktree first (common recipe in backends.md), then:
spawn_agent(
agent_type: "aep-builder",
message: "Worktree: <abs-repo-path>/.feature-workspaces/<ws> (branch feat/<ws>).
<bootstrap_prompt>"
)
# record the returned agent id → state agent_idnudge(ws, msg)
send_input(agent: <agent_id>, message: <msg>)Also append to signals/feedback.md (durable record).
liveness(ws)
list_agents # thread status
git -C .feature-workspaces/<ws> diff --stat # corroborationpresent(ws)
- CLI:
/agentswitches between agent threads — tell the human which
thread id belongs to <ws>.
- Desktop app: threads run side by side; subagent diff stats appear in the
composer and each subagent has a stable identicon. The human clicks into the thread to watch or steer.
gate(ws) — human gate (block-in-place, hub-and-spoke)
Two native channels, plus the file. In both, the parent thread (main agent) is the human's console — the human answers there; opening the worker thread directly is optional:
- Approvals (sandbox/permission requests): surface natively in the active
thread labeled with the source thread — CLI: press o to open that thread and approve; app: contextual permission prompt, click into the owning thread.
- Non-approval decisions (design ambiguity, eval non-convergence): worker
appends to needs-human.md + blocked_on: human and asks the parent thread; the parent asks the human in the main conversation and relays the answer via send_input(<id>, "<answer>").
spawn_evaluator(ws, role)
Use a bounded headless one-shot with enforced worktree cwd — review is exactly the "bounded analysis" case codex exec is reserved for:
codex exec --cd "<abs>/.feature-workspaces/<ws>" \
--dangerously-bypass-approvals-and-sandbox \
"<evaluator prompt from agent-contracts.md, customized with the workspace paths>" < /dev/nullThe prompt is the spawn — no sleep, no send step, no pane to kill. The exec returns when eval-response-<N>.md is written.
teardown(ws)
close_agent(agent: <agent_id>) # if still running
# then the common worktree removal from backends.md---
Mode: codex-exec
One headless `codex exec` process per story, cwd hard-bound to the worktree. OS-bound: workers survive the orchestrator session, and a _fresh_ session can steer them via codex exec resume. This is the Codex mode for cron/launchd-driven autopilot (each tick is a new codex exec session that cannot see another session's subagents) and for users who demand enforced isolation.
spawn(ws, branch, bootstrap_prompt)
nohup codex exec --cd ".feature-workspaces/<ws>" \
--dangerously-bypass-approvals-and-sandbox \
"<bootstrap_prompt>" < /dev/null > ".feature-workspaces/<ws>/.dev-workflow/worker.log" 2>&1 &
# Recover the session id from the worker log / `codex exec resume --last`
# bookkeeping and record it → state agent_idnudge(ws, msg)
codex exec resume <session-id> --dangerously-bypass-approvals-and-sandbox \
"<msg>" < /dev/nullresume continues the worker's own session with the new instruction — this is the OS-bound steering channel; it works from any later orchestrator session. Also append to signals/feedback.md.
liveness(ws)
cat .feature-workspaces/<ws>/.dev-workflow/signals/status.json # primary signal
git -C .feature-workspaces/<ws> diff --stat # corroboration
tail -5 .feature-workspaces/<ws>/.dev-workflow/worker.log # process outputpresent(ws) / gate(ws) — gate-and-park
Headless — review via signals + the PR. For a gate the worker parks: write needs-human.md + blocked_on: human, commit WIP, finish the run. The orchestrator asks the human in the main session and relays the answer:
codex exec resume <session-id> --dangerously-bypass-approvals-and-sandbox \
"The human decided: <answer>. Mark the needs-human entry resolved, clear blocked_on, and continue the /aep-build flow." < /dev/nullspawn_evaluator(ws, role)
Same as codex-subagent: codex exec --cd <worktree> with the evaluator prompt.
teardown(ws)
The exec process exits on its own when the build completes; nothing to kill. Then the common worktree removal from backends.md.
---
Dogfood / post-deploy validation (Codex)
Host-aware dogfood (dogfood_method()) for Codex resolves by mode, per `dogfood-validation.md`:
- codex-subagent (desktop, GPT-5.4 multimodal): use the **native in-app
browser + computer-use** to drive the app and capture screenshots (computer-use is desktop-only). Fallback: the Playwright skill, then agent-browser CLI.
- codex-exec (headless): write and run a Playwright script (no computer-use
off the desktop app). Fallback: agent-browser CLI → API/curl checks.
Screenshots feed the multimodal evaluator's Visual Design dimension (aep-gen-eval/references/scoring-framework.md). Full selection + target_url() resolution live in dogfood-validation.md.
Host-aware E2E / Dogfood Validation — e2e_tool(target_type) & target_url()
Dogfood/validation picks the right native tool per target type and host, both locally (pre-merge, /aep-build Phase 6) and on staging/production (post-deploy). This closes gap G4b: until now Phase 6 ran only against localhost and only if agent-browser happened to be installed (else the whole phase was skipped), and there was no post-deploy validation at all.
e2e_tool(target_type) generalizes the original web-only dogfood_method() to cover web / mobile / desktop / cli targets (adding webwright, agent-device, and a bash CLI track); dogfood_method() is kept as a := e2e_tool('web') wrapper for back-compat. The project-local e2e-test skill ships a self-contained projection of this matrix in its tool-selection.md (generated by /aep-e2e-skill-scaffolding) — keep the two in sync.
Detection reuses executor.detect() for HOST + mode — read `backends.md` first. The functions here add a method layer on top of that: which validation tool to drive (e2e_tool()) and which URL to point it at (target_url()). All methods emit one unified report format so the downstream classifier is host-agnostic.
---
Table of Contents
1. `e2e_tool(target_type)` — target × host × preference selection 2. `target_url(env)` — URL resolution 3. Unified report format 4. Config block 5. Post-deploy worker & boundary (v1.8.0) 6. Cross-references
---
e2e_tool(target_type) — target × host × preference selection
executor.detect() resolves HOST + mode; this adds a tool probe on top, now generalized over a target type (web | mobile | desktop | cli) so the same selector covers browser, mobile, desktop, and CLI e2e. Each host uses its native capability first and degrades only when that is unavailable. dogfood_method() is preserved as a thin wrapper (:= e2e_tool('web')) so existing callers — /aep-build Phase 6 and the autopilot post-merge guard — are unchanged.
e2e_tool(target_type): # target_type ∈ {web, mobile, desktop, cli}; default web
resolve HOST + mode via executor.detect()
pref = topology.routing.e2e.tool.<target_type> # optional pin; unset or 'auto' = NO pin
if target_type == web and (pref unset or pref == 'auto'):
pref = topology.routing.dogfood.method # legacy back-compat alias of e2e.tool.web
if pref and pref != 'auto': return pref if healthy(pref) else "degrade" # 'auto' is not a tool
if target_type == web:
if HOST == claude:
if agent_browser_healthy(): return "agent-browser" # /agent-browser:dogfood
elif webwright_available(): return "webwright"
else: return "degrade" # non-UI → API/curl; UI → human-eval
if HOST == codex:
if mode == codex-subagent and computer_use_enabled: # desktop app
return "codex-native" # in-app browser + computer-use
elif playwright_available(): return "playwright-script" # GPT-5.4 writes + runs it
elif agent_browser_healthy(): return "agent-browser" # CLI fallback
else: return "degrade" # API checks
# generic host
if playwright_available(): return "playwright-script"
elif agent_browser_healthy(): return "agent-browser"
else: return "degrade"
if target_type == mobile:
if agent_device_healthy(): return "agent-device" # iOS / Android native
else: return "degrade" # API/contract checks only
if target_type == desktop:
if HOST == codex and computer_use_enabled:
return "codex-native" # in-app + computer-use
elif agent_browser_healthy(): return "agent-browser" # Electron via CDP
else: return "degrade"
if target_type == cli:
if bash_available(): return "bash" # run the built binary; assert exit code/stdout/fs.
# at EXEC: if the binary won't build/run, mark SKIP → Tier-1
else: return "degrade" # no shell (≈never)
dogfood_method() := e2e_tool('web') # back-compat wrapper — Phase 6 / post-merge-guard unchanged| Target → host | Native method (default) | Detection | Fallback |
|---|---|---|---|
| web · Claude Code | /agent-browser:dogfood | agent_browser_healthy() | webwright → API/curl / human-eval |
| web · Codex desktop | native in-app browser + computer-use (GPT-5.4 multimodal) | desktop + computer-use enabled | Playwright → agent-browser CLI |
| web · Codex headless / generic | write + run a Playwright script | playwright_available() | agent-browser CLI → API checks |
| mobile · any host | agent-device (iOS/Android native) | agent_device_healthy() | API/contract checks |
| desktop · Codex (computer-use) | native in-app browser + computer-use | desktop + computer-use enabled | agent-browser (Electron/CDP) |
| desktop · other hosts | agent-browser (Electron via CDP) | agent_browser_healthy() | API checks |
| cli · any host | bash (run the built CLI binary; assert exit/stdout/fs) | bash_available() | Tier-1 only (mark SKIP) |
Why web splits multiple ways. Computer-use and the in-app (Atlas) browser
are desktop-only; codex exec (headless) has neither, so it writes andruns a Playwright script (GPT-5.4 does this natively) and falls back to the
agent-browser CLI, then to API/curl. webwright is a Claude-side web
alternative when agent-browser is unavailable. Mobile and desktop are
distinct target tracks — a journey declares its target: and the selectorroutes accordingly.
Health probes
Each tool has a smoke test; a failed probe drops to the next fallback (never a hard FAIL). Definitions are self-contained here (no external reference doc needed):
agent_browser_healthy() { command -v agent-browser >/dev/null 2>&1 && agent-browser navigate about:blank >/tmp/ab-smoke.log 2>&1; }
playwright_available() { command -v npx >/dev/null 2>&1 && npx --no-install playwright --version >/dev/null 2>&1; }
webwright_available() { command -v webwright >/dev/null 2>&1 && webwright --version >/dev/null 2>&1; }
agent_device_healthy() { command -v agent-device >/dev/null 2>&1 && agent-device doctor >/tmp/ad-smoke.log 2>&1; }
bash_available() { command -v bash >/dev/null 2>&1; } # ~always true; gates the cli track (run the built binary)
# codex-native: not a CLI probe — available only on Codex desktop with computer-use enabled.Target-type detection
A journey's target: front-matter is authoritative. When absent, infer from the stack: native-uniwind/React Native/Expo → mobile; tauri/electrobun/Electron → desktop; no web frontend — a CLI entrypoint (bin in package.json, Go cmd/*/main.go, Python console_scripts/[project.scripts]) or a pure library/package (exports only) → cli; a web frontend → web.
---
target_url(env) — URL resolution
target_url(env): # env ∈ {local, staging, production}
if env == local: # unchanged from current Phase 6
source .dev-workflow/ports.env → return $BASE_URL
else:
u = topology.routing.deploy_targets.<env>_url # product-context.yaml
if u: return u # config first
else: return <CI/deploy step output URL> # fallback CI (e.g. preview URL)- `env=local` — source
.dev-workflow/ports.env, return$BASE_URL(the
Phase 6 status quo; ports.env is written by the workspace-setup hook).
- `env=staging|production` — read
topology.routing.deploy_targets.<env>_url first; if unset, read the URL the CI/deploy step printed (e.g. a Vercel/Netlify preview URL or deploy output).
`target_url` is web-only. It returns an HTTP URL, which suits web (andthe Electron/CDPdesktoppath that loads a URL). Mobile (agent-device),
native-bundle desktop, and cli have no URL target. CLI's bash trackruns the built binary directly (no URL) — invoke it as a user would and
assert exit code / stdout / filesystem effects; run it pre-merge/local.
Mobile/native-desktop need a build artifact (.ipa/.apk/app bundle), which
post-deploy doesn't model yet, so run those pre-merge/local against a
simulator/emulator; post-deploy validation stays web-oriented until a per-target
artifact resolver exists.
---
Unified report format
Every method — /agent-browser:dogfood, codex-native, playwright-script, the degrade paths — emits the same severity / category / repro structure as /agent-browser:dogfood, so the downstream classifier never branches on host. Reports are written to .dev-workflow/dogfood-<feature>.md (local) or the post-deploy report path (staging/prod), one entry per finding:
## <finding title>
**Severity:** blocker | major | minor
**Category:** UX | logic | visual | edge-case | accessibility | performance
**Repro:** <ordered steps to reproduce against the target URL>
**Observed:** <what happened> **Expected:** <what should happen>
**Evidence:** <screenshot path / log excerpt>On issue → route per topology.routing.dogfood.on_issue (default create_story): the report is ingested by the `dogfood_report` adapter (product-context/_shared/references/telemetry-ingestion.md → Dogfood-report adapter), which parses each ## finding into a normalized record → the /aep-reflect Step 2 classifier → a bug/refinement story in product-context.yaml → dispatch (the G6 self-feeding loop). Set escalate instead to surface to the human rather than auto-filing.
The report path is the contract. Whatever the trigger — local Phase 6, the
post-deploy post-merge guard, or a standalone / ad-hoc dogfood — write the
unified report to .dev-workflow/dogfood-*.md. That is what makes the findingingestible:/aep-watch'sdogfood_reportsource (or the guard's Path 1) picks
it up on its next pass and runs it through the adapter above. A dogfood that
only prints findings to chat (never writing the report file) is a dead end —
nothing can auto-file it. Auto-creation still obeys the confirmation policy
(full_auto/watch.auto_create); only bug / refinement auto-file, while
calibration / discovery / opportunity-shift / process surface to a human.
Hard service regressions (health signals) are a separate path — they go
through the autopilot post-merge guard's revert policy, not this story-filing
path. Dogfood finds UX/functional issues and files stories; the guard finds
service regressions and decides rollback.
---
Config block
Added under topology.routing in product-context.yaml:
topology:
routing:
deploy_targets:
staging_url: "https://staging.example.com" # optional; missing → fallback CI
production_url: "https://example.com"
e2e:
tool:
web: auto # auto | agent-browser | playwright | webwright | codex-native
mobile: auto # auto | agent-device
desktop: auto # auto | codex-native | agent-browser
cli: auto # auto | bash
dogfood:
method: auto # auto | agent-browser | codex-native | playwright (alias of e2e.tool.web)
post_deploy_env: staging # staging | production | none
on_issue: create_story # create_story | escalate- `e2e.tool.{web,mobile,desktop,cli}` — per-target pin;
auto(default) defers
to e2e_tool(target_type). An explicit value pins the tool (still subject to its health probe — a pinned-but-unhealthy tool degrades).
- `method` — back-compat alias of
e2e.tool.web;autodefers to
dogfood_method() (= e2e_tool('web')). Parallels the aep.executor-backend pin.
- `post_deploy_env` — which environment the post-deploy step validates;
none disables post-deploy dogfood.
- `on_issue` —
create_story(default) orescalate.
---
Post-deploy worker & boundary (v1.8.0)
When the post-deploy step needs a worker to run validation (e.g. a Codex headless Playwright run, or a Claude /agent-browser:dogfood pass), it is spawned as `native-bg-subagent` and confirmed live by the mandatory post-spawn liveness probe before being treated as running — never trust a flag or roster (see `backends.md` → Post-Spawn Liveness Probe).
Screenshots captured by any method feed each host's multimodal evaluator: Claude evaluates natively; Codex is confirmed multimodal (GPT-5.4). This keeps the visual judgment in-host rather than crossing back to the orchestrator.
The orchestrator boundary holds. The post-deploy step reads reports and signals and runs CLIs (gh, deploy tooling, target_url resolution) — it never reads workspace code. The validation worker is bound to its worktree (or runs against the deployed URL); the main session stays at arm's length, consistent with the autopilot orchestrator boundary.
---
Cross-references
- `backends.md` —
executor.detect()(HOST + mode), the
native-bg-subagent default, and the Post-Spawn Liveness Probe.
agentic-development-workflow/build/SKILL.mdPhase 6 — local (pre-merge)
dogfood; calls dogfood_method() with env=local instead of skipping when agent-browser is absent.
patterns/autopilot/references/post-merge-guard.md— the post-deploy step
invokes target_url(staging|production) + dogfood_method() after merge + deploy; hard regressions go through the guard's revert policy.
product-context/reflect(/aep-reflect) — the host-agnostic classifier that
turns a unified dogfood report into a bug/refinement story.
Legacy Backend — tmux Session (+ optional cmux tab)
Per-operation recipes for the legacy mode: a long-lived interactive executor session hosted in tmux, optionally presented through a cmux review tab. This was the v1.x default for Claude Code (backends B1/B2); it is now selected only when the user pins it explicitly (git config aep.executor-backend tmux or "…with tmux") or on a generic host where tmux is the only session mechanism available. Detection and selection live in backends.md.
OS-bound: tmux sessions survive the orchestrator session and work under both the long-lived and cron driver models.
---
spawn(ws, branch, bootstrap_prompt)
The worktree is created by AEP first (common recipe in backends.md). Then:
$EXECUTORis the interactive session command fromdetect()— bare
claude --dangerously-skip-permissions / generic session command, never-p/codex exec. Guard first:
[ -z "$EXECUTOR" ] && { echo "run detect() — \$EXECUTOR unset"; exit 1; }so an unset executor aborts loudly instead of launching a bare login shell.
With cmux available (legacy+cmux): spawn the tmux session only; the cmux review tab is attached _after_ the bootstrap is sent (attaching a surface focuses the tmux composer and blocks external send-keys).
tmux new-session -d -s <ws> -c .feature-workspaces/<ws> "$EXECUTOR"Without cmux:
tmux new-session -d -s <ws> -c .feature-workspaces/<ws> "$EXECUTOR"
echo "Workspace running in tmux session '<ws>'. Watch it live with: tmux attach -t <ws>"Readiness + bootstrap send. Wait for the agent to initialize, then send the prompt. The readiness signal is executor-specific ($READY_GREP from detect()); the send uses -l so a multi-line prompt is entered literally and a single trailing Enter submits it (a bare send-keys "$PROMPT" Enter would let embedded newlines submit the prompt line-by-line):
if [ -n "$READY_GREP" ]; then
for _ in $(seq 1 12); do
tmux capture-pane -t <ws>:0 -p -S -5 | grep -q "$READY_GREP" && break; sleep 2
done
else
sleep 8 # no readiness glyph configured — give the composer time to come up
fi
tmux send-keys -t <ws>:0.0 -l -- "$bootstrap_prompt" # literal text (handles multi-line)
tmux send-keys -t <ws>:0.0 Enter # one submitAttach the cmux review tab (AFTER the bootstrap). Open the tab as a sibling in the pane that holds the orchestrator's own tab — never cmux new-workspace (that makes a separate top-level workspace) and never a bare cmux new-surface (it defaults to an unset $CMUX_WORKSPACE_ID). Resolve the pane from cmux tree (the orchestrator's tab is marked ◀ here), falling back to the surface env vars when we're inside one:
# $CMUX is the CLI path resolved in detect(). Run this only when PRESENT == cmux.
read -r WS PANE < <("$CMUX" tree 2>/dev/null | awk '
/workspace workspace:/ {for (i=1;i<=NF;i++) if ($i ~ /^workspace:/) ws=$i}
/pane pane:/ {for (i=1;i<=NF;i++) if ($i ~ /^pane:/) pane=$i}
/◀ here/ {print ws, pane; exit}')
: "${WS:=$CMUX_WORKSPACE_ID}" "${PANE:=$CMUX_PANE_ID}"
if [ -n "$PANE" ]; then # a target pane exists
SREF=$("$CMUX" new-surface --type terminal --workspace "$WS" --pane "$PANE" --focus true \
| grep -oE 'surface:[0-9]+' | head -1)
"$CMUX" send --surface "$SREF" "tmux attach -t <ws>"$'\n' # trailing newline submits
"$CMUX" rename-tab --surface "$SREF" "<ws>"
else # reachable but no pane → tmux-only
echo "cmux reachable but no target pane — watch with: tmux attach -t <ws>"
fiThe cmux fallback ladder
cmux is a convenience, never a requirement. Nothing functional depends on it; it is purely the human's clickable live-view tab.
cmux tab attachable → clickable review tab (sibling of the orchestrator's tab), live view
tmux present → same session + monitor loop; `tmux attach` to watch"cmux tab attachable" = the cmux CLI is reachable and a target pane resolves (cmux tree shows ◀ here, or $CMUX_PANE_ID is set) — it does not require $CMUX_SOCKET. Reachable-but-no-pane degrades to tmux-only; losing cmux costs only the tab UI. Skills must gate every cmux call on detection and never abort merely because cmux is absent.
---
nudge(ws, msg)
# -l sends the message literally (handles multi-line nudges); a separate Enter submits once.
tmux send-keys -t <ws>:0.0 -l -- "<msg>"
tmux send-keys -t <ws>:0.0 Enter---
liveness(ws)
# Session activity: capture the pane and compare to the last hash
tmux capture-pane -t <ws>:0.0 -p -S -20
# Host-independent fallback / corroboration: uncommitted work in the worktree
git -C .feature-workspaces/<ws> diff --stat---
present(ws)
| Surface | Recipe |
|---|---|
| cmux | the review tab attached at the end of spawn() already shows the live session |
| tmux | tell the human: tmux attach -t <ws> (read-only: tmux attach -t <ws> -r) |
---
gate(ws) — human gate
Worker appends to signals/needs-human.md + sets "blocked_on": "human" in status.json (the host-agnostic protocol). The orchestrator surfaces it: "workspace <ws> needs a decision — tmux attach -t <ws>, answer in the session (or write to signals/feedback.md), detach."
---
spawn_evaluator(ws, role)
The generator spawns the evaluator in a bottom tmux pane (eval-protocol Context A):
# Split current tmux window vertically (top=generator, bottom=evaluator). The evaluator
# needs to read files and write eval-response, so it runs the INTERACTIVE executor.
tmux split-window -v -c "$(pwd)" "${EXECUTOR:-claude --dangerously-skip-permissions}"
tmux select-pane -t :.0 # return focus to the generator pane
sleep 10
tmux send-keys -t :.1 -l -- "$EVAL_PROMPT" # evaluator prompt from agent-contracts.md
tmux send-keys -t :.1 Enter
while [ ! -f .dev-workflow/signals/eval-response-<N>.md ]; do sleep 15; done
tmux kill-pane -t :.1Usetmux split-window, notcmux split— the generator runs inside tmux
but was not spawned by cmux, so it cannot use cmux socket commands. Under
cmux the attached surface displays both panes automatically.
---
teardown(ws)
tmux kill-session -t <ws> 2>/dev/null || true
# then the common worktree removal from backends.md#!/usr/bin/env bash
# Post-spawn liveness probe for AEP executor spawns.
#
# A spawn call returning, a flag being set, or a roster/state entry saying
# "active" is NOT evidence a worker started. (The removed `claude-team` mode
# failed exactly here: the launch command was truncated in a detached tmux pane
# and never submitted, yet the team roster still showed the member "active".)
#
# A worker is LIVE only if BOTH hold within the timeout:
# (1) the worker process/agent EXISTS — this is host-specific and must be
# checked by the CALLER via the host tool, because an in-process
# background subagent has no OS process to grep:
# native-bg-subagent : TaskList shows <agent_id> (bare-hex id) running
# claude-bg : claude agents --json shows the session running
# codex-subagent : list_agents shows <agent_id>
# codex-exec : the codex exec PID is alive
# legacy : tmux pane_current_command == claude (NOT zsh)
# (2) the worktree shows ACTIVITY — this script verifies the host-agnostic
# half below.
#
# Usage: spawn-liveness-probe.sh <ws> <agent_id> [timeout_secs]
# Exit 0 = worktree active within timeout; 1 = dead spawn (tear down + fall back
# to native-bg-subagent). The caller still confirms (1) above.
set -uo pipefail
WS="${1:?usage: spawn-liveness-probe.sh <ws> <agent_id> [timeout_secs]}"
AGENT_ID="${2:?missing agent_id}"
TIMEOUT="${3:-90}"
WT=".feature-workspaces/$WS"
SIG="$WT/.dev-workflow/signals/status.json"
worktree_active() {
# status.json written by the worker, OR uncommitted edits in the worktree.
[ -f "$SIG" ] && return 0
[ -d "$WT" ] && [ -n "$(git -C "$WT" diff --stat 2>/dev/null)" ] && return 0
return 1
}
deadline=$(( SECONDS + TIMEOUT ))
while [ "$SECONDS" -lt "$deadline" ]; do
if worktree_active; then
echo "LIVE: worktree '$WS' shows activity (agent_id=$AGENT_ID). Caller must still confirm the process/agent exists via the host tool."
exit 0
fi
sleep 5
done
echo "DEAD: no worktree activity for '$WS' within ${TIMEOUT}s (agent_id=$AGENT_ID)." >&2
echo " → Treat as a failed spawn: tear down the dead remnant (TeamDelete any team that got created)," >&2
echo " then auto-fall-back to native-bg-subagent into the SAME worktree and probe again." >&2
echo " → NEVER accept 'roster/state says active' as liveness." >&2
exit 1