
Aep Dispatch
- 50 installs
- 14 repo stars
- Updated July 31, 2026
- memorysaver/agentic-engineering-patterns
Helps with ai & agent building tasks.
About
aep-dispatch is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- aep-dispatch
- AI & Agent Building
- AI-coding skill
Aep Dispatch by the numbers
- 50 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #7,245 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-dispatchAdd 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
Dispatch
Bridge between the product context (control plane) and the feature lifecycle (execution plane). Syncs workspace state, scores stories, picks what to build next, assembles context, and routes into /aep-design or /aep-launch.
Where this fits:
/aep-envision → /aep-map → /aep-model (UI-facing) → /aep-scaffold
→ [ /aep-dispatch → /aep-design → /aep-launch → /aep-build → /aep-wrap ]
▲ you are here
→ /aep-reflect → loopSession: Main, interactive Input: Product definition from product/index.yaml (split mode) or product-context.yaml (v1 mode); operational state from product-context.yaml Output: OpenSpec change with pre-assembled context, story status updated, handoff to /aep-design or /aep-launch
For autonomous orchestration: Use/aep-autopilotinstead. Autopilot runs the full dispatch-launch-monitor-review-wrap-dispatch cycle as a tick-based state machine via/loop. Dispatch remains a single-pass interactive tool.
>
For hands-free batch under Claude Code: dispatch a wave "with workflow" to build the whole wave as a single dynamic workflow (executor workflow mode) instead of N monitorable workers. See Step 5 → _Dynamic Workflow_ mode.
---
Before Starting
File Resolution:
ls product/index.yaml 2>/dev/null && echo "SPLIT MODE" || echo "V1 MODE"
cat product-context.yaml- Split mode (
product/index.yamlexists): Read product definition fromproduct/index.yamlfor context assembly. Read stories, topology, architecture, cost fromproduct-context.yaml. - V1 mode: Read everything from
product-context.yaml.
If product-context.yaml doesn't exist, run /aep-envision then /aep-map first. If the stories section is empty, run /aep-map to decompose the product.
---
The Dispatch Protocol
Every dispatch run follows the same 7-step protocol. Each step is idempotent — running /aep-dispatch twice with no state changes produces the same result.
① SYNC signals → bring YAML up to date with workspace reality
② CASCADE states → compute all pending→ready, pending→blocked transitions
③ SCORE stories → rank the ready queue by dispatch_score
④ PRESENT queue → show user the scored dispatch queue
⑤ DISPATCH → lock stories (status→in_progress), create OpenSpec changes
⑥ MONITOR → agents work, main session watches signals
⑦ COMPLETE → /aep-wrap updates YAML, atomic cascade, re-invoke dispatch---
Step 1: Signal Sync
Before calculating anything, sync workspace signals into the YAML to reflect reality:
For each story with status: in_progress or in_review:
workspace = story.assigned_to
signal_path = .feature-workspaces/<workspace>/.dev-workflow/signals/status.json
Read signal file (if exists):
If signal.story_status == "completed":
Update YAML: story.status → completed
Update: story.completed_at = signal.completed_at
Update: story.pr_url = signal.pr_url
Update: story.cost_usd = signal.cost_usd
If signal.story_status == "in_review":
Update YAML: story.status → in_review
Update: story.pr_url = signal.pr_url
If signal.story_status == "failed":
Update YAML: story.status → failed
Append signal.failure_log to story.failure_logsWhy: Without signal sync, the YAML shows in_progress for stories already done. Downstream stories stay pending even though they're actually ready.
---
Step 2: Cascade State Transitions
After syncing all signals, compute all state transitions in one pass:
For each story with status: pending:
If any story in dependencies[] has status: failed:
Transition → blocked
Elif any story in dependencies[] has status: deferred:
Transition → blocked (dependency deferred)
Elif all stories in dependencies[] have status: completed:
Transition → ready
For each story with status: blocked:
If the blocking dependency is now completed:
Transition → pending (will be re-evaluated in this same pass)Recovery transitions (user-initiated, handle if requested):
failed → pending— user resets after fixing the specdeferred → pending— user un-defers a story
Validate the YAML after all updates (see references/yaml-guardrails.md):
npx js-yaml product-context.yaml > /dev/null && echo "YAML OK"If this fails, fix the YAML before proceeding. Common fixes: quote list items containing colons, flatten nested sub-lists, escape embedded double quotes.
Commit the synced + cascaded state to YAML before computing scores. Increment dispatch_epoch.
---
Step 3: Score Stories
Determine Active Layer
For each layer (0, 1, 2, ...):
If any story in this layer has status not in [completed, deferred]:
This is the active layer. Stop.Layer gate check: If active layer > 0, verify layer_gates[active_layer - 1].status == passed. If not, block and suggest running the layer gate test.
Filter Ready Queue
From ready stories in the active layer, remove stories with file-level conflicts:
For each ready story:
For each in_progress story:
If files_affected intersection is non-empty:
Mark as conflicted — cannot dispatch until the in_progress story completesCompute Readiness Score
Before scoring, compute each story's readiness (spec completeness):
readiness_score = (
min(3, acceptance_criteria_count) # 0-3
+ (interface_obligations_defined ? 2 : 0) # 0 or 2
+ (files_affected_identified ? 1 : 0) # 0 or 1
+ (verification_defined ? 2 : 0) # 0 or 2
+ (no_relevant_open_questions ? 2 : 0) # 0 or 2
) / 10Write readiness_score to the story in YAML. This is used for routing in Step 7.
Compute Dispatch Score
Each remaining ready story gets a score:
dispatch_score = (business_value + unblock_potential + critical_path_urgency + reuse_leverage) / (complexity_cost + ambiguity_penalty + interface_risk)Business Value (1-10)
Use story.business_value if explicitly set. Otherwise derive from priority:
critical = 10
high = 7
medium = 4
low = 1Unblock Potential (0-10)
unblock_potential = min(10, count of stories that directly depend on this one * 2)A story that unblocks 5 others scores 10. A leaf story scores 0.
Critical Path Urgency (0-10)
Compute the critical path through the dependency DAG (longest chain from any root to any leaf within the active layer). Stories on the critical path get maximum urgency:
If story is on critical path:
critical_path_urgency = 10
Else:
slack = latest_possible_start - earliest_possible_start
critical_path_urgency = max(0, 10 - slack)Reuse Leverage (0-10)
Stories that produce shared enablers (auth middleware, base components, shared utilities) score higher:
reuse_leverage = min(10, count_of_modules_depending_on_output * 3)Only applies to stories with compile_mode: shared_enabler or whose module appears in 2+ other modules' depends_on.
Complexity Cost (denominator term)
S = 1 (fast feedback)
M = 2
L = 4 (slow, expensive)Ambiguity Penalty (0-5, denominator term)
ambiguity_penalty = 0
If acceptance_criteria count < 3: +2
If interface_obligations empty: +1
If relevant open_questions exist: +1
If files_affected empty: +1Stories with high ambiguity get lower scores, biasing dispatch toward well-specified work.
Interface Risk (0-3, denominator term)
interface_risk = min(3, count of interface contracts this story creates or modifies)Cross-module interface changes carry integration risk in parallel execution.
Example Scores
| Story | Value | Unblock | CP | Reuse | Cost | Ambig | IFace | Score |
|---|---|---|---|---|---|---|---|---|
| Auth middleware (critical path, high, unblocks 3, enabler) | 7 | 6 | 10 | 6 | S=1 | 0 | 1 | 14.5 |
| User model (not critical, medium, unblocks 2) | 4 | 4 | 4 | 0 | S=1 | 0 | 0 | 12.0 |
| Dashboard layout (not critical, low, leaf, ambiguous) | 1 | 0 | 2 | 0 | L=4 | 3 | 0 | 0.43 |
Grouped Change Dispatch
For stories with compile_mode: grouped_change sharing the same change_group:
- Readiness gate: Use min readiness_score of any story in the group — if any story is under-specified, the group isn't ready
- Dispatch score: Sum
business_valueandunblock_potentialacross the group; use maxcritical_path_urgencyand maxreuse_leverage; divide by sum ofcomplexity_cost+ maxambiguity_penalty+ maxinterface_risk - Dispatch the entire group as one unit — one OpenSpec change, one workspace, one PR
- Max 3 stories per group. Failure of any story fails the group.
---
Step 4: Present Dispatch Queue
Show the sorted queue with context:
Dispatch Queue (Layer 0 — 4 ready, 2 in_progress, WIP 3/5)
1. ★ PROJ-003 "Setup auth middleware" score: 14.5
[high] S | Module: auth | Wave 1 | Critical path | Shared enabler
Unblocks: PROJ-005, PROJ-007, PROJ-008
→ Readiness: 0.9 — skip to /aep-launch
2. PROJ-004 "Create user model" score: 12.0
[medium] S | Module: db | Wave 1 | 4h slack
Unblocks: PROJ-006, PROJ-009
→ Readiness: 0.8 — skip to /aep-launch
3. PROJ-010 "Add settings page" score: 0.43
[low] L | Module: web | Wave 3 | Leaf
→ Readiness: 0.3 — go through /aep-design (ambiguous)
Conflicted (waiting):
• PROJ-006 — files overlap with in_progress PROJ-002
Blocked (dependencies not met):
• PROJ-008 — waiting on PROJ-003 (auth middleware)
In progress:
• PROJ-001 (tab: feat-api-scaffold) — Phase 4, 60% complete
• PROJ-002 (tab: feat-db-schema) — Phase 5, code reviewRecommendation: Always highlight the top story and explain why (highest score = critical path + high value + unblocks the most).
---
Step 5: Dispatch
Dispatch Modes
Interactive (default)
User picks stories one at a time. Best for early layers or learning the system.
Wave Batch (--batch wave)
Dispatch all ready stories in the current wave (execution slice) at once:
Dispatches all ready stories in Wave N (up to WIP limit)
Creates N workspaces via /aep-launchDynamic Workflow (--batch wave + "…with workflow")
When the user explicitly asks to dispatch a wave "with workflow" AND the host is Claude Code with the dynamic-workflow (Workflow) tool, route the batch through the workflow mode instead of creating N workers. The dispatch front-end is identical — sync, cascade, score, lock, assemble context — only the execution plane changes: instead of N /aep-launch workers, author one dynamic workflow that fans out pipeline(stories, build, verify) with one agent per story (recipe: aep-executor/references/backends.md → "Mode: workflow").
Locks + creates OpenSpec changes for the ready stories in Wave N — up to the WIP limit (as usual)
Creates the .feature-workspaces/<name> worktrees (launch guardrails apply)
Then: one dynamic workflow, one agent per locked story (build → verify), each bound to its worktree
After the run: collect `gated` results → ask the human → resume gated stories with the answersRespect the WIP limit. Workflow mode does not exempt the wave from the WIP cap below: each workflow agent still opens a PR, so the integration/merge bottleneck is the same as Wave Batch. Lock at most available_slots stories into the workflow (available_slots = concurrency_limit − current in_progress); the workflow's own per-agent concurrency cap is a separate, lower-level limit and does not replace this one.
Announce the mode (this path bypasses `/aep-launch`). Because dispatch authors the workflow directly instead of handing to /aep-launch, dispatch owns the announcement that /aep-launch normally makes: state "workflow mode (dynamic workflow) — autonomous, billed, background; no mid-stage steering; human gates park and return here for confirmation, then gated stories resume" before authoring the workflow.
This is the hands-free batch path: autonomous, billed, background. Steering is at stage boundaries only — but human decisions are NOT lost: a worker that hits one returns a gated result (gate-and-park), this session asks you, and the story resumes in its worktree with your answer. Use it when you want a wave built autonomously without watching individual workers. Requires Claude Code + Workflow tool (see .claude/skills/aep-executor/references/backends.md, "Mode: workflow"). If the host can't support it, fall back to Wave Batch and say so.
WIP Limits
max_wip = topology.routing.concurrency_limit (default: 5)
current_wip = count of stories with status: in_progress
available_slots = max_wip - current_wip
Never dispatch more than available_slots stories.Why (Little's Law): Lead Time = WIP / Throughput. If you merge 3 stories/day, WIP 3 = 1 day lead time. WIP 15 = 5 days. The bottleneck is usually human PR review, not agent speed.
The Dispatch Lock
For each selected story, dispatch atomically:
1. Re-read story.status from YAML (not from cache)
2. If status != ready → SKIP (already dispatched by another run)
3. Write to YAML:
status: in_progress
assigned_to: <workspace-name>
openspec_change: <story-id>
started_at: <ISO 8601 now>
dispatched_at_epoch: <current dispatch_epoch>
4. Commit YAML immediately (this IS the lock)
5. THEN create OpenSpec change and workspaceThe commit happens BEFORE the workspace is created. Two consecutive /aep-dispatch runs: Run 1 writes in_progress and commits. Run 2 reads in_progress, skips. No double dispatch.
---
Step 6: Create OpenSpec Change with Context Package
For each dispatched story, create the OpenSpec change with pre-assembled context:
openspec/changes/<story-id>/
├── proposal.md ← story description + why + business value
├── design.md ← module definition + interface contracts + dependency APIs
├── specs/<module>.md ← acceptance criteria + interface obligations + verification
├── tasks.md ← story decomposed into implementation tasks
└── .context/ ← pre-assembled context package
├── stable-prefix.md ← shared product/architecture context (cacheable)
├── dependencies.md ← public APIs from completed dependency stories
└── retrieval.md ← what to explore at runtimeContext Assembly
Part 1: Stable Prefix (~10K tokens, shared across agents in same layer)
Extracted from product definition (product/index.yaml in split mode, product-context.yaml in v1 mode):
product.problem— what we're solvingproduct.constraints— tech stack, infrastructureproduct.layers[active_layer]— what the user can do at this layerarchitecture.overview— high-level structurearchitecture.technical_spec— if set, include the technical specification document (or relevant sections for the story's module). This provides Symphony-style precision for protocol-heavy systems.- Coding conventions (conventional commits, git + worktree workflow, trunk-based)
Part 2: Story-Specific Payload (~20K tokens, unique per agent)
- Full story spec from the
storiessection - Module definition from
architecture.modulesmatchingstory.module - Adjacent interfaces from
architecture.interfaceswherefromorto= story module - Dependency outputs — for each completed dependency: public API surface (types, exports, endpoints). NOT internal implementation.
Part 3: Retrieval Instructions (~500 tokens)
## Files to read first
- <files_affected from story spec>
## Patterns to explore
- Check existing patterns in <module> directory
- Read interface contract tests for consumed interfaces
## Do not read
- Other module internals — use dependency_outputs aboveCalibration Context (for .5 alignment layers and calibrated stories)
For stories with calibration_type set, or stories in .5 alignment layers:
Heavy calibrations (visual-design, ux-flow, copy-tone):
1. Include the calibration artifact — calibration/<type>.yaml (e.g., calibration/visual-design.yaml) 2. For visual-design: Also include reference design files from docs/design-references/ matching the story's page (by story activity or title) 3. Include calibration constraint directive:
This story has calibrated <dimension> decisions.
Follow the calibration artifact in calibration/<type>.yaml strictly.
Do not introduce new [visual tokens / flow patterns / voice patterns]
not defined in the calibration artifact.If the required calibration/<type>.yaml does not exist, do not dispatch — instruct the user to run /aep-calibrate <type> first.
Light calibrations (api-surface, data-model, scope-direction, performance-quality):
No additional context needed — decisions are already in the architecture section of product-context.yaml and the product section of product/index.yaml (split mode), which flow through the stable prefix (Part 1) and story-specific payload (Part 2).
Backward compatibility: For .5 layer stories without calibration_type set, default to visual-design. Check both calibration/visual-design.yaml and design-context.yaml (legacy path).
Object Map Context (UI-facing stories)
A story is UI-facing when it has object_model_refs set, calibration_type in {visual-design, ux-flow}, or a non-null activity whose module has kind: ui (architecture.modules[].kind). For these, inject the Object Map slice, not the whole model:
1. Resolve the capability: use story.capability if set; otherwise (v1 / single-journey) the default capability is the project slug. The Object Map is product/maps/<capability>/object-map.yaml. 2. Gate (must pass to dispatch): the resolved object-map must exist, have status: approved, and its coverage[] must list this story id. If it is missing, status is draft or stale, or it does not cover the story → do not dispatch; instruct the user to run /aep-model first (same posture as the calibration gate). If story.capability is unset, fall back to scanning product/maps/*/object-map.yaml for a coverage[].story match — an approved match resolves the capability. 3. From the map's coverage index, select only the objects this story realizes and include just those entries: their attributes (core/secondary/metadata), the relationships among them, the CTAs (object × role) on them, and the screen(s) the story builds. Skip unrelated objects — keep the slice minimal. 4. Include the object-first directive:
This story realizes objects from an approved Object Map.
Build object-first (noun→verb): the listed screens, object cards/detail,
attributes, and CTA placements come from product/maps/<capability>/object-map.yaml.
Do not introduce objects or screen structures not in this slice, and do not
collapse the flow into a step-by-step wizard unless the map marks it task_oriented.Visual look, copy voice, and journey/transition still come from calibration/{visual-design,copy-tone,ux-flow}.yaml — the Object Map governs object structure and CTA grammar, not taste.
Assembly Rules
1. Prune aggressively — irrelevant context degrades agent performance 2. Dependency outputs = public API only — types, exports, endpoint signatures, never internals 3. Measure the package — if it exceeds the role's token budget from topology, prune harder or split the story 4. Stable prefix is cacheable — when dispatching multiple stories in the same layer, write it once
---
Commit and Push Before Handoff
CRITICAL: Commit and push ALL dispatch artifacts (YAML updates, OpenSpec changes, changelog) to remote BEFORE handing off to /aep-launch. If the dispatch commit stays local, it will be lost when workspace PRs merge to the integration branch and you rebase. The push ensures OpenSpec changes survive on the remote.Append to the changelog section:
- date: <today>
type: dispatch
author: human
summary: "Dispatched PROJ-003 (auth middleware), PROJ-004 (user model) — Layer 0, Wave 1"
sections_changed: [stories]Commit and push:
# 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}
git pull --ff-only origin "$BASE"
git add product-context.yaml openspec/changes/
git commit -m "feat: dispatch PROJ-003, PROJ-004 — Layer 0 Wave 1"
git push origin "$BASE"Verify the push succeeded before proceeding to handoff. If push fails (e.g., remote conflict), resolve before launching workspaces.
---
Step 7: Hand Off
Launch mode is normally resolved at `/aep-launch`, not here. For the default
path dispatch stays executor-agnostic — it hands a well-specified change to
/aep-launch, which detects the host and selects a mode (native-bg-subagent /claude-bg / codex-subagent / codex-exec / legacy) via aep-executor. Nativemodes outrank tmux on every host; dispatch does not need to know. **The one
exception is the _Dynamic Workflow_ opt-in (Step 5):** that path runs the
workflow mode _from dispatch_, bypassing /aep-launch, so dispatch itselfowns mode selection and the announcement for that case.
Determine the handoff based on story completeness:
Readiness-based routing
Use the readiness_score computed in Step 3:
- readiness_score >= 0.7 → skip to
/aep-launch(spec is dispatch-ready) - readiness_score 0.5–0.7 → present to user for decision (
/aep-launchor/aep-design) - readiness_score < 0.5 → route to
/aep-design(spec needs refinement)
Full-auto / auto-design routing (medium/low readiness)
Routing of an under-ready story depends on two topology.routing flags — the full_auto master switch (default false) and the finer-grained auto_design (default false). full_auto sits above auto_design: full_auto: true implies auto_design: true.
- `full_auto: true` OR `auto_design: true` → a medium/low-readiness story
(readiness < 0.7) is resolved by a non-interactive gen/eval design resolver — a design agent that refines the spec without a human, then routes to /aep-launch — instead of escalating to interactive /aep-design (the G3 human gate). No strategic pause.
- `full_auto: false` AND `auto_design: false` (default) → keep escalating: a
medium/low-readiness story routes to interactive /aep-design for human design refinement before launch. The strategic "what to build" gate stays with the human.
Well-specified (readiness >= 0.7) → skip to /aep-launch
- 3+ specific, testable acceptance criteria
- Interface obligations defined
- Verification strategy complete
- Files affected identified
Ambiguous (readiness < 0.5) → go through /aep-design
- Vague or fewer than 3 acceptance criteria
- Missing interface details
- Open questions relevant to this story
Story PROJ-003 dispatched (score: 14.5, critical path, shared enabler).
OpenSpec change: openspec/changes/PROJ-003/
Context package: openspec/changes/PROJ-003/.context/
Recommendation: Readiness 0.9 — well-specified
→ Skip to /aep-launch
/aep-launch ← start building immediately
/aep-design ← refine the spec firstBatch Handoff
For batch dispatch, create all workspaces via /aep-launch:
Batch dispatched: PROJ-003 (score 23.0), PROJ-004 (score 12.0)
/aep-launch PROJ-003 → tab: auth-middleware
/aep-launch PROJ-004 → tab: user-model---
Edge Cases
- No stories ready: All pending stories have unmet dependencies. Check if any
in_progressstories are stuck (high attempt_count, old started_at). Suggest checking workspace progress or running/aep-reflect. - All stories completed in active layer: Trigger layer gate test. If passed, advance to next layer and re-run dispatch.
- All stories completed in all layers: Product is done. Suggest
/aep-reflectfor final review. - Layer gate failed: Do not advance. Create fix stories based on gate failure, add to current layer, re-dispatch.
- WIP limit reached: No available slots. Show what's in progress and suggest waiting or reviewing PRs to unblock slots.
---
Guardrails
- Never dispatch a story with unmet dependencies — even if the user insists.
- Never dispatch conflicting stories in parallel — file-level conflicts cause merge chaos.
- Always sync signals before computing — stale YAML produces wrong dispatch decisions.
- Always commit YAML before creating workspaces — the commit IS the dispatch lock.
- Always create the OpenSpec change — even for well-specified stories. The
.context/directory is what the agent reads. - Respect WIP limits — dispatching beyond integration capacity creates traffic jams, not speed.
Generated by scripts/build-skills.sh from skills/product-context/_shared/. Do not edit; edit _shared/ and rebuild.
Orchestration Patterns
Detailed patterns for the control plane's orchestrator — state management, context assembly, layer gating, and failure handling. Read this when setting up or debugging the execution pipeline.
---
Work Graph as State Machine
The work graph is a live state machine. Every story node holds a status and transitions based on events.
State Transitions
pending → ready (all dependency stories reach 'completed')
ready → in_progress (orchestrator dispatches to agent)
in_progress → in_review (agent submits PR)
in_review → completed (verification passes)
in_review → in_progress (verification fails, retry initiated)
in_progress → failed (retry limit exceeded, escalated)
pending → blocked (a dependency story enters 'failed')
any → deferred (user explicitly postpones)Orchestrator Loop
The orchestrator is event-driven, not polling-based:
1. Event received (story completed, PR submitted, verification result, failure). 2. Update state of the affected story in the work graph. 3. Cascade check: Does this transition unlock new stories? (completed → check dependents). Does it block stories? (failed → mark dependents as blocked). 4. Dispatch: For each newly ready story, run conflict detection, assemble context, dispatch to agent per routing rules. 5. Layer check: Are all stories in the current layer completed? If yes, trigger Integration Gate. 6. Alert check: Any cost anomalies? Any critical path blockages? Notify user if needed.
Concurrency Control
- Maximum parallel agents is configurable. Start with 5–10.
- Two stories with overlapping "Files Likely Affected" must not run in parallel — serialize them.
- If two parallel stories produce merge conflicts, the later PR rebases on the merged one and re-verifies.
---
Context Assembly
The Problem Context Assembly Solves
An agent's output quality is directly proportional to the relevance and precision of its input context. Too little context → the agent guesses. Too much context → the agent gets confused or hits token limits. Context assembly is the art of giving each agent exactly what it needs and nothing more.
Assembly Rules
For each agent role, the Agent Topology document defines a context window composition — the ordered list of what goes in. The orchestrator follows this list mechanically:
1. Read the composition spec for the target agent role. 2. Prune the Context Document to the sections listed in the spec. 3. Extract the relevant System Map slice — the story's module and its adjacent interfaces only. Do not include unrelated modules. 4. Collect dependency artifacts — for each completed dependency, extract the public interface (types, exports, API surface). Do not include internal implementation unless the composition spec explicitly requires it. 5. Validate the package — all required fields present, no references to missing artifacts. 6. Measure the package — if it exceeds the target token budget for the role, escalate for manual pruning or split the story.
Common Assembly Failures
- Missing dependency artifact: A dependency is marked
completedbut its output artifact is not found. This usually means the previous agent's output contract was not enforced. Fix: add post-completion validation in the handoff contract. - Stale interface contract: The System Map was amended but the context package still references the old version. Fix: always read interface contracts from the latest System Map, not from cached copies.
- Context overflow: The assembled package exceeds the agent's token budget. Fix: either prune more aggressively (summarize dependency artifacts instead of including full source) or split the story into smaller units.
---
Layer Gating
Gate Design
Each layer has an Integration Gate — tests that verify stories work together. The gate is NOT the sum of individual story tests. It tests emergent behavior at integration boundaries.
Layer 0 gate is the most important test in the pipeline. It executes the exact user journey from the Context Document's Layer 0 MVP Contract. If the walking skeleton doesn't work end-to-end, something is architecturally wrong.
Subsequent layer gates test:
1. All previous layer journeys still work (regression). 2. New capabilities added in this layer work end-to-end. 3. Interface contracts honored under realistic conditions (not just mocks).
Gate Failure Protocol
Gate fails
→ Identify failure boundary (which module interface)
→ Check: implementation vs contract mismatch?
→ Implementation wrong: create fix story → Phase 4
→ Contract wrong: trigger Architecture Review → Phase 2
→ Assess impact on completed stories
→ May require re-execution of affected storiesGate failure on a contract issue is the most expensive failure in the pipeline because it can invalidate already-completed work. This is why Phase 2 (System Map approval) is a human-reviewed gate — catching contract errors early prevents cascading rework.
---
Failure Handling
Why Fresh-Agent Retry Works
When an agent fails and retries, it carries the full reasoning trajectory from its first attempt. If that trajectory led to a dead end, the retry often follows the same path — the agent is stuck in its own logic. A fresh agent receives only the structured failure log, not the reasoning. It approaches the problem without the stuck trajectory.
The failure log's "what was NOT tried" field is the highest-value signal for the fresh agent. It provides starting points the previous agent considered but did not explore.
Failure Log Schema
{
story_id: string,
attempt_number: number,
agent_role: string,
approach_summary: string, // What the agent tried to do
failure_point: string, // Which verification step failed
error_output: string, // Exact error messages or test failures
hypothesis: string, // Agent's best guess about root cause
not_tried: string[], // Alternative approaches considered but not attempted
context_issues?: string, // Any problems with the context package
time_spent_seconds: number,
tokens_used: number
}Cascade Prevention
When a story fails:
1. Mark direct dependents as blocked. 2. Continue executing non-blocked stories in the same layer. 3. If the failed story is on the critical path → alert user immediately (entire layer is blocked). 4. If NOT on critical path → other work continues. User addresses failure asynchronously. 5. When the failed story is eventually resolved (fixed or deferred), unblock dependents and resume normal dispatch.
Escalation Format
When a story reaches human escalation, present:
1. The story spec (what was being attempted). 2. All failure logs from all attempts (what happened). 3. The fresh agent's failure log specifically (the most informed analysis). 4. Current impact: which stories are blocked, is this on the critical path? 5. Suggested options: fix the story, simplify the story, defer it, or modify the architecture.
---
State Persistence
The orchestrator's state must survive crashes.
Storage Options
- File-based (JSON in repo): Simple, version-controlled. Sufficient for most MVP projects. Limitation: does not support concurrent orchestrators.
- SQLite: Supports querying ("show all failed stories") and concurrent access. Better for larger projects.
- External store (Redis, Postgres): For production-grade orchestration with multiple concurrent sessions.
For MVP-stage projects, start with JSON in the repo. Upgrade when the limitation matters.
State Snapshot Schema
{
project_id: string,
current_layer: number,
stories: {
[story_id]: {
status: "pending" | "ready" | "in_progress" | "in_review" | "completed" | "failed" | "blocked" | "deferred",
assigned_agent?: string,
attempt_count: number,
last_updated: ISO8601,
failure_logs?: FailureLog[],
pr_url?: string,
completed_at?: ISO8601
}
},
layer_gates: {
[layer_number]: {
status: "not_started" | "running" | "passed" | "failed",
test_results?: TestResult[],
completed_at?: ISO8601
}
},
cost_summary: {
total_cost_usd: number,
cost_by_layer: { [layer]: number },
cost_by_role: { [role]: number },
cost_by_story: { [story_id]: number }
},
last_updated: ISO8601
}State Inspection
The user should be able to query the current state at any time:
- Progress per layer: completed / in_progress / pending / failed / blocked
- Critical path status: what is the next bottleneck?
- Cost breakdown: where is the money going?
- Blocked stories: what is waiting on what?
Provide a simple CLI command or dashboard that reads the state file and renders this overview.
Telemetry Ingestion & Outcome Auto-Evaluation
How /aep-reflect (and /aep-watch) pull real-world signals automatically, and how a layer's quantitative outcome contract is evaluated without a human. This augments the interactive reflect flow — it never replaces human review by default. (Gap G5.)
Authoring note: this file is canonical in
skills/product-context/_shared/references/;scripts/build-skills.sh
materializes it into each consuming skill's references/.---
1. Automated source ingestion
Pull from read-only sources with bash/curl/jq and reduce each to the normalized observation record the reflect Step 2 classifier consumes:
{
"source": "error_stream | analytics | monitoring | bug_tracker | dogfood",
"signal": "one-line description of what was observed",
"evidence": "url | query | sample (no secrets)",
"story_ref": "<story-id if attributable, else null>",
"suggested_class": "bug | refinement | discovery | opportunity_shift | process | null"
}suggested_class is a hint only — the reflect Step 2 classifier (and the human, unless full_auto) makes the final call. Ingested records are merged with interactive input before classification; automation augments, never replaces.
Source config
Endpoints live under topology.routing.telemetry_sources (a list). Each entry:
telemetry_sources:
- kind: error_stream # error_stream | analytics | monitoring | bug_tracker
endpoint: "https://…/api/…?since={since}" # {since} = last-ingest high-water mark
token_env: SENTRY_TOKEN # NAME of an env var / secret — never the secret itself
metric_map: # for analytics/monitoring: outcome-metric name → query
activation_rate: "SELECT … "Safety: access is read-only; reference credentials by env-var / secret-store name only — never embed secrets in the repo or in `product-context.yaml`.
Dogfood-report adapter (dogfood_report source)
Dogfood runs — local (/aep-build Phase 6), post-deploy (autopilot post-merge guard), or a standalone / ad-hoc live exercise — emit the unified markdown report (## <title> / **Severity:** / **Category:** / **Repro:** / **Observed:** / **Expected:** / **Evidence:**) to .dev-workflow/dogfood-*.md (see patterns/executor/references/dogfood-validation.md → Unified report format). This adapter parses each ## finding into the `/aep-watch` Step 1 finding record (the operative shape Step 3 dedupes and Step 4 turns into a story — _not_ the 5-field telemetry record above, which is the classifier's conceptual input) so the same Step 2 classifier consumes it — closing the G6 self-feeding loop for every dogfood trigger, not just the guard path. It is a file glob, not a network source: self-describing, so coverage_check (§1.5) does not gate it.
Source config (the discriminator key matches the container: type: under watch.sources[], kind: under telemetry_sources[]):
watch:
sources:
- type: dogfood_report
glob: ".dev-workflow/dogfood-*.md" # default; add post-deploy report paths as neededPer-finding mapping (markdown field → finding field):
| Dogfood field | Finding field |
|---|---|
## <title> | title (the story title /aep-watch Step 4 reads) |
**Repro / Observed / Expected / Evidence** | detail (repro steps + observed-vs-expected; no secrets) — also the classifier's evidence |
**Severity:** | priority — blocker/major → high (critical if it blocks a core flow); minor → normal. Dogfood findings have no `count`, so priority comes from Severity, not the count-based escalation other sources use |
**Category:** | suggested_class hint — UX/logic/edge-case/accessibility → bug; visual/performance → bug when Severity ∈ {blocker,major}, else refinement |
| — | signal: dogfood, story_ref: null, external_id: (below); count/first_seen/last_seen unset (the report carries no occurrence count or timestamp) |
suggested_class is a hint only — the Step 2 classifier (and the human, unless full_auto) makes the final call, exactly as for every other source. In particular a finding that reads as calibration / discovery / opportunity-shift / process is not auto-filed; it surfaces to a human (see /aep-watch Step 2).
No high-water mark — dedupe-only. The unified report has no per-finding timestamp, so a dogfood_report source does not advance watch.since (that cursor applies only to time-ordered sources); re-scanning the glob each tick is harmless because idempotency rests entirely on the stable dedupe key. Each finding gets a deterministic external_id = "dogfood:" + slug(report-basename) + ":" + shorthash(slug(title) + "|" + category), so /aep-watch Step 3 dedupes on watch_origin.{source,external_id}: already-filed findings no-op, and a genuinely new finding (new title/category) yields a new id and a new story. The autopilot post-merge guard Path 1 stamps the same external_id on the story it files, so whichever path ingests a given report first wins and the other no-ops — no double-filing.
---
1.5 Deciding which sources to wire (the coverage rule)
You don't list telemetry for its own sake — a source is needed _iff_ some declared signal requires it. The decision is hybrid:
1. Metric-driven (what signals do we need?) — enumerate every quantitative success_metric across product.layers[].outcome_contract plus every topology.routing.post_merge_guard.health_signals entry. That set _is_ the demand for telemetry. 2. Inventory (which tool provides each?) — /aep-scaffold's audit detects the project's observability stack (Sentry, Datadog, PostHog, OpenTelemetry, log drains, /healthz-style endpoints) and records candidate telemetry_sources (kind + endpoint + token_env, no metric_map yet); you can also add candidates by hand. 3. Bind (`/aep-map`) — for each needed signal, attach it to a candidate source by adding a metric_map: { <metric-or-signal>: "<query>" } entry. A needed signal with no measurable source is flagged, not ignored: make the metric qualitative, or record it unmeasured — never leave a quantitative metric silently un-sourced.
coverage_check(needed) — the guard helper
Consumers that rely on telemetry (/aep-watch, /aep-reflect Step 2.75, /aep-autopilot) call this before trusting auto behavior. It is pure config inspection — no network:
coverage_check(needed_signals):
missing = []
for sig in needed_signals: # quantitative success_metric names + health_signals
if no telemetry_sources[*].metric_map has key == sig
(and, for a health_signal, no source/endpoint provides it):
missing.append(sig)
return { covered: missing == [], missing }On `covered == false`: surface "telemetry binding incomplete for <missing> — run /aep-map (observability step)" and block the auto path (watch refuses to claim auto-coverage; reflect falls back to the human pause; autopilot pauses). Missing wiring must block auto, never silently no-op — that's the v2 human-in-the-loop default.
---
2. Outcome-contract auto-evaluation
A layer's outcome_contract carries a success_metric (type + target) and a decision_rule (keep_if / otherwise). Precondition: run coverage_check([success_metric]) (§1.5) first — if the metric isn't bound to a source, take the human-pause path (the binding is incomplete; do not auto-eval). When covered, evaluate per topology.routing.auto_outcome_eval:
Metric type | auto_outcome_eval: quantitative | default (none) |
|---|---|---|
| quantitative (numeric, measurable from a source) | fetch actual value via the matching telemetry_sources query, apply keep_if/otherwise mechanically, record result — no pause | human pause (current behavior) |
| qualitative | human pause — unless full_auto: true (then agent-judgment auto-eval) | human pause |
On a fetch failure or ambiguity, fall back to the human pause (fail safe, not fail open). Record every auto-evaluation in the changelog:
- date: YYYY-MM-DD
type: outcome_evaluation
summary: "Layer N: <metric> = <actual> vs target <target> → passed|failed (auto)"---
3. full_auto interaction (A1)
topology.routing.full_auto (default false) is the master switch. It only changes the qualitative path:
full_auto | auto_outcome_eval | quantitative outcome | qualitative outcome |
|---|---|---|---|
| false (default) | none | human pause | human pause |
| false | quantitative | auto-eval | human pause |
| true | (implied quantitative) | auto-eval | agent-judgment auto-eval |
Default keeps humans in the loop; only an explicit full_auto: true removes the qualitative pause.
---
Cross-references
/aep-reflectStep 1 (Gather Feedback) and Step 2.75 (Evaluate Outcome Contracts)/aep-watch(reuses the normalized observation record for its ingest step)aep-autopilotreferences/tick-protocol.md— Step ⑥ Layer Completion (what the
auto-eval lets advance without a pause)
YAML Guardrails for product-context.yaml
Every skill that writes to product-context.yaml must validate the file before committing. Invalid YAML silently breaks the dashboard and blocks all downstream consumers.
Validation Command
Run this after every edit to product-context.yaml:
npx js-yaml product-context.yaml > /dev/null && echo "YAML OK"If the project has the @agentic-engineering-patterns/api package, use the actual loader for deeper validation (Zod schema + preprocessing):
npx tsx -e "
const { loadProductContext } = require('@agentic-engineering-patterns/api/lib/product-context-loader');
loadProductContext(process.env.PRODUCT_CONTEXT_PATH || './product-context.yaml');
console.log('YAML + schema OK');
"If validation fails, fix the YAML before committing. Do not commit broken YAML under any circumstances.
Common YAML Pitfalls in product-context.yaml
These are the patterns that most frequently break the parser when agents write to the file.
1. List items ending with a colon
A trailing colon makes YAML interpret the item as a mapping key. If the next lines are indented, YAML expects a value — and fails.
# BROKEN — YAML treats this as a mapping key
acceptance_criteria:
- Generate page redesigned for multi-step video workflow:
- Intent prompt input
- Multi-step progress display
# FIXED — quote the entire item, flatten sub-items
acceptance_criteria:
- "Generate page redesigned for multi-step video workflow: intent prompt input, multi-step progress display"Rule: Never end a list item with : followed by indented sub-items. Either quote the item or flatten the sub-list.
2. Embedded double quotes inside list items
YAML interprets "text" as a quoted string boundary. Content after the closing quote is invalid.
# BROKEN — YAML sees "Complete Your Profile" as the full string, then chokes on the rest
- "Complete Your Profile" guard includes link to /profile
# FIXED — wrap in double quotes, use single quotes inside
- "'Complete Your Profile' guard includes link to /profile"
# ALSO FIXED — escape inner quotes
- "\"Complete Your Profile\" guard includes link to /profile"Rule: If a list item contains embedded double quotes, wrap the entire value in double quotes and use single quotes (or escaped quotes) inside.
3. Colons in the middle of list items
A colon followed by a space (: ) triggers YAML key-value parsing.
# BROKEN — YAML tries to parse "Dashboard" as a key
- Dashboard: creator dashboard showing recent generations
# WORKS (preprocessor handles this) — but quoting is safer
- "Dashboard: creator dashboard showing recent generations"Rule: The preprocessYaml function in the loader auto-quotes most of these, but when writing new content, prefer explicit quoting for items containing : .
4. Special characters: @, {, }
# BROKEN — @ is a YAML tag indicator, { starts a flow mapping
- @mention the user
- Use {variable} interpolation
# FIXED
- "@mention the user"
- "Use {variable} interpolation"Rule: Quote list items containing @, {, or }.
5. Nested sub-lists under string items
YAML list items are scalar values — they cannot have children unless the item is a mapping key.
# BROKEN — a string item cannot have sub-items
- Main feature description
- Sub-feature A
- Sub-feature B
# FIXED — flatten into one item or use a mapping structure
- "Main feature description: Sub-feature A, Sub-feature B"Pre-commit Checklist
Before committing any change to product-context.yaml:
1. Run the validation command above 2. If adding acceptance_criteria, description, or any free-text list: scan for colons, quotes, and special characters 3. If the validation command is not available (e.g., no Node.js), at minimum review list items for the patterns above