| name | agile:epic:assign |
| description | Dispatch the next wave of epic tasks into the team inbox without re-running the full implement flow. Use this when the team-lead session's inbox has drained mid-epic and you need to push the next round of tasks into team members. The skill reads state.json + tasks.md, categorizes the inbox, advances the wave pointer if the current feature is done, then either assigns existing pending tasks (fast path) or creates the next feature's tasks via TaskCreate + TaskUpdate(addBlockedBy) + SendMessage assignment (re-populate path) using the same contracts as agile:epic:implement. Requires an active team-lead session with an existing team and an in-flight epic (state.json present, epicStatus=PENDING or IMPLEMENTING). Triggers on "/agile:epic:assign", "assign tasks to team", "assign tasks to team members", "assign next wave", "push tasks to inbox", "dispatch next round", "team inbox empty assign more", "next wave of tasks", "populate inbox". Do NOT use for fresh epic kickoff (use /agile:epic:implement) or for marking the epic complete (use /agile:epic:validate). |
| user-invocable | true |
| disable-model-invocation | false |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, Skill, TaskCreate, TaskList, TaskGet, TaskUpdate, SendMessage, AskUserQuestion |
Epic Assign
Focused wave dispatcher. Pushes the next round of tasks into the team inbox so team members keep
working without you having to re-prompt. This skill exists because, mid-/agile:epic:implement, the
inbox naturally drains between waves and you shouldn't have to type "assign tasks to team members"
every time.
This skill is a sniper, not a Swiss army knife. It does only the dispatch step. It does not spawn
teams, run validation cycles, mutate epic.md status, or commit phase checkpoints. The parent skill
agile:epic:implement remains the system of record for everything else.
Invocation
/agile:epic:assign — Auto-detect epic from cwd, branch, or active team
/agile:epic:assign 136 — Target by epic number
/agile:epic:assign agile/epics/136-feature-name — Target specific epic path
Identification Format
| Level | Format | Example |
|---|
| Epic | NNN | 136 |
| Feature | NNN-XX | 136-01 |
| User Story | NNN-XX-U{n} | 136-01-U1 |
| Task | NNN-XX-T{nn} | 136-01-T03 |
Role
The agent running this skill acts as the team lead for one focused dispatch pass. It coordinates,
delegates, and persists state — it never writes code, compiles, or runs tests.
Gate 1: Detect Epic
Read modules/preflight.md and execute Step 1.
Resolve the target epic using the same 6-step priority chain documented in
.claude/skills/agile-team/references/team-detection.md:
$ARGUMENTS is agile-epic-NNN → use directly
$ARGUMENTS is bare number NNN → resolve to agile/epics/{NNN}-*
$ARGUMENTS is a path → resolve directly
- cwd contains an epic directory → walk up to find it
- git branch matches
epic-NNN or feature/NNN-* → derive
AskUserQuestion listing teams from ~/.claude/teams/*/config.json
Fail-fast with "Cannot resolve target epic. Pass an epic number or run from inside an epic directory."
if all six steps fail.
Gate 2: Verify Pre-conditions
Read modules/preflight.md and execute Step 2.
ALL of the following must hold or the skill aborts with a specific remediation message:
| Check | If false, remediation |
|---|
agile/epics/{NNN-name}/state.json exists | "No state.json — run /agile:epic:implement first" |
state.json.epicStatus ∈ {PENDING, IMPLEMENTING} | If COMPLETED: clean exit "Epic already complete" |
~/.claude/teams/{team-name}/config.json exists | "No team config — run /agile:team create then add first" |
Team config members[] is non-empty | "Team has no members — run /agile:team add first" |
epic.md exists | "Epic spec missing — re-run /agile:epic:plan" |
TaskList() callable (i.e., team-lead session) | "Not a team-lead session — switch sessions or recreate team" |
At least one feature's spec.md and tasks.md exists | "No features ready — run /agile:epic:plan" |
The team name is derived from the epic number: agile-epic-NNN.
Gate 3: Read Inbox
Read modules/inbox-inspector.md and execute.
Call TaskList(). Categorize every task into one of:
| Category | Heuristic | Action |
|---|
assignable | status=pending, blockedBy=[] (empty), owner is empty or unset | Will be assigned in Gate 6 |
working | status=in_progress | Skip — teammate already on it |
blocked | status=pending, blockedBy non-empty | Skip — wait for blocker to clear |
completed | status=completed | Used for status promotion in Gate 4 |
stale | Exists in TaskList but no entry in state.json.taskInboxMapping | Report and skip (do not assign) |
Path Decision
The bucket counts decide which downstream gates run:
| Inbox state | Path | Next gate after drift fix |
|---|
assignable non-empty | Fast path | Gate 6 (assign) |
assignable empty AND working empty | Empty-or-completed | Gate 4 (promote, advance) |
assignable empty AND working non-empty AND wave still active | All-in-flight no-op | Exit clean |
assignable empty AND working non-empty AND those tasks' features done | Mixed | Gate 4 (promote, advance) |
"Wave still active" means at least one working task belongs to a feature still in
state.json.currentFeatures. "Wave is done" means every working task belongs to a feature that
has already been moved to completedFeatures (this is the crash-recovery case where stale
in-progress tasks survive a previous run).
Drift Fix Always Runs
Regardless of path, always run Gate 4a (auto-fix tasks.md drift) before any other action.
The fast path skips Gate 4b/4c/4d (status promotion + wave advance), but it must NOT skip 4a — a
drifted [ ]/[W] marker for an already-completed task would otherwise stay wrong forever
because the fast path never re-reads completed tasks otherwise.
All-in-Flight No-Op
In the no-op case, exit clean with:
All tasks in flight — team is busy.
Run /agile:epic:progress for status, or run /agile:epic:assign again later.
Do NOT mutate state.json. Do NOT update currentPhase. The skill is a no-op pass-through.
Gate 4: Promote Status & Advance Wave Pointer
Read modules/wave-advancer.md and execute.
Step 4a: Auto-Fix tasks.md Drift (narrow case)
For every task in the completed category from Gate 3, look up its taskInboxMapping entry to find
the source tasks.md line. If the marker there is [ ] or [W] but the inbox shows completed,
rewrite the marker to [X]. This corrects drift left over from a previous run that died mid-wave.
If the marker is anything else ([B], [S], [-], or already [X]), leave it alone and emit a
one-line warning per drift case. Do not auto-resolve ambiguous states.
Step 4b: Promote Feature Status
For each feature in state.json.currentFeatures:
- Read its
tasks.md. If every task marker is [X] or [S]:
- Set
state.json.features[id].status = "IMPLEMENTED".
- Move the feature ID from
currentFeatures into completedFeatures.
- Otherwise, leave it in
currentFeatures.
This skill does NOT update spec.md/epic.md status fields. Those updates are owned by
agile:epic:implement Phase 3 Step 5 — running them here would race with that skill's git checkpoints.
Step 4c: Compute Next Ready Features
Walk state.json.parallelGroups in order. The next wave is the first group whose features are NOT
all in completedFeatures ∪ skippedFeatures. Within that group, "ready" = dependencies all in
completedFeatures ∪ skippedFeatures.
Set state.json.currentFeatures = [ready feature IDs].
Step 4d: Boundary — Epic Done at Dispatch Layer
If after recomputation currentFeatures is empty AND parallelGroups is fully exhausted (all
features in completedFeatures ∪ skippedFeatures):
- Persist
state.json with currentFeatures = [] and currentPhase = "all-waves-dispatched" so
a future resume of /agile:epic:implement reads the correct phase. (Step 4b's promotions also
land in this write.)
- Do NOT mutate
epicStatus — that promotion belongs to /agile:epic:validate.
- Exit clean with:
All waves dispatched and complete. Run /agile:epic:validate next.
If currentFeatures is empty but parallelGroups is NOT exhausted (every feature in the next group
has unmet dependencies):
- Do NOT mutate
state.json further. Step 4b's promotions are reverted in memory; the on-disk
state.json is left exactly as it was on entry. (The intent is that the user investigates the
blocking features rather than seeing a phantom state advance.)
- Exit with the error message:
Next wave blocked: features {ids} all have unmet dependencies on
{blocking feature ids}. Cannot advance.
Investigate via /agile:epic:progress, then resume with
/agile:epic:implement once the blockers are resolved.
Gate 5: Decide What to Dispatch
Read modules/dispatch-router.md and execute.
For each feature in currentFeatures, check state.json.taskInboxMapping:
- Has entries for this feature → tasks already exist in the inbox. Do NOT recreate. Move to
Gate 6 and assign whichever ones now appear in
assignable.
- No entries for this feature → tasks must be created. Follow the procedures referenced from
agile:epic:implement's modules:
5a: Parse tasks.md into a Task DAG
Use the rules in .claude/skills/agile-epic-implement/modules/task-decomposer.md:
- Parse
agile/epics/{NNN-name}/features/{featureId}/tasks.md
- Extract task lines with explicit
:[dep] markers, parallel [P] flags, user story labels
- Build the task DAG with predecessors per task
- Apply the four dependency tiers (explicit > story-gate > sequential > parallel)
5b: Build Per-Task Context
Use .claude/skills/agile-epic-implement/modules/build-context.md to derive Maven commands and
service paths, and .claude/skills/agile-epic-implement/modules/file-finder.md to identify the
exact files each task touches.
5c: TaskCreate Every Task First
Per .claude/skills/agile-epic-implement/modules/task-delegator.md:
- Iterate the task DAG in topological order
- For each task,
TaskCreate with subject {taskId} (#{inboxId}): {task subject} and the full
description template (Feature, User Story, Context, Acceptance Criteria, Task, Expected
Deliverable, Build Context, Files, Technical Context)
- Capture each new inbox ID and write it back to
state.json.taskInboxMapping[taskId] = inboxId
5d: Wire Dependencies via TaskUpdate(addBlockedBy)
After ALL tasks for the feature are created (so all inbox IDs are known):
- For every task with predecessors, call
TaskUpdate with addBlockedBy referencing the
predecessor's inbox ID (NOT the task ID — they are different)
- Story-gate edges: the first task in U{n+1} blocks on ALL tasks in U{n}
- Cross-feature dependencies: do NOT use
addBlockedBy (different feature scope) — instead include
a CROSS-FEATURE DEPENDENCY: ... line in the task description; the team lead will send a
DEPENDENCY CLEARED SendMessage when the upstream task completes
5e: Mark Feature IMPLEMENTING in state.json
Set state.json.features[id].status = "IMPLEMENTING". Do not touch spec.md or epic.md.
Gate 6: Assign Assignable Tasks
Read modules/dispatch-router.md Step 6.
Re-call TaskList() to refresh the assignable set (some tasks may have just been created and are
now eligible). For each task in the refreshed assignable:
6a: Pick a Teammate
Use the specialty rules in .claude/skills/agile-epic-implement/modules/teammate-context.md and
.claude/skills/agile-epic-implement/modules/team-lead.md:
| Teammate Type | Preferred Tasks |
|---|
kotlin-expert | Kotlin source files, coroutines |
java-expert | Entity classes, service logic, Spring config |
maven-expert | POM files, dependency management, builds |
test-expert | Unit tests, integration tests, test data |
database-expert | Repository classes, SQL, schema changes |
code-reviewer | Validation, code review |
general-purpose | Any unmatched task |
Specialty match is best-effort. Round-robin across available teammates if multiple match. Balance
load — never pile on one teammate while others idle.
6b: SendMessage with Full Context
Build the assignment message using the template from
.claude/skills/agile-epic-implement/modules/teammate-context.md (Feature, User Story, Context,
Task Description, Expected Deliverable, Technical Context).
Call SendMessage with:
to: "{teammateName}"
message: "TASK ASSIGNMENT: {taskId} — {subject}\n\n{full template body}"
summary: "Assigned {taskId} to {teammateName}"
The SendMessage tool's parameters are to, message, and summary. Do NOT use recipient,
content, or type — the SendMessage tool does not accept those fields and the assignment will
fail.
NEVER use TaskUpdate(owner=...) to assign work. The owner parameter triggers hook errors
documented in agile-epic-implement/modules/team-lead.md. The only correct dispatch path is
SendMessage.
6c: Update tasks.md Marker
For each task just dispatched, edit its tasks.md line: change [ ] to [W]. Use the line
matching - [marker] {taskId} in agile/epics/{NNN-name}/features/{featureId}/tasks.md.
Gate 7: Persist & Report
Read modules/wave-advancer.md Step 7.
7a: Update state.json
Write back to agile/epics/{NNN-name}/state.json with:
| Field | New value |
|---|
currentPhase | wave-{n}-dispatching (where n = index in parallelGroups + 1) |
currentFeatures | The list set in Gate 4c |
completedFeatures | Updated per Gate 4b |
taskInboxMapping | Updated per Gate 5c (additions only — never remove entries) |
features[id].status | Updated per Gate 4b and 5e |
updatedAt | Current ISO timestamp |
Do NOT touch (these are owned by other skills):
| Field | Owner |
|---|
epicId, epicPath, epicName, startedAt | /agile:epic:implement Phase 0 |
executionOrder, parallelGroups | /agile:epic:implement Phase 1 |
skippedFeatures | /agile:epic:implement (user decision) |
epicStatus | /agile:epic:validate |
validationRetries | /agile:epic:implement Phase 3 |
checkpoints | /agile:epic:implement (git checkpoints) |
The skill reads skippedFeatures for the wave-advancement calculation but never writes it.
A user marking a feature as SKIPPED is an intentional decision that flows in via
/agile:epic:implement's escalation path; the dispatcher must not silently mutate it.
7b: Print Dispatch Report
Use the template at templates/dispatch-report.md. Sample output:
Epic: 136-feature-name
Wave: 2/4 (parallelGroups[1])
Promoted: 136-01 → IMPLEMENTED
Now in flight: 136-02
Created: 14 tasks (T01–T14)
Assigned: 6 tasks → {alice, bob, carol, dave}
Blocked: 8 tasks (waiting on intra-feature deps)
State: wave-2-dispatching written to state.json
Next: monitor TaskList; run /agile:epic:assign again when inbox drains.
7c: No Git Commit
This skill never commits. Phase checkpoints are owned by agile:epic:implement. Mixing in commits
here would fragment the epic's git history.
Modules
| Module | File | Used In |
|---|
| Preflight | modules/preflight.md | Gates 1, 2 |
| Inbox Inspector | modules/inbox-inspector.md | Gate 3 |
| Wave Advancer | modules/wave-advancer.md | Gates 4, 7 |
| Dispatch Router | modules/dispatch-router.md | Gates 5, 6 |
The new skill references these modules from agile:epic:implement rather than duplicating them
(single source of truth — when the dispatch contract evolves, only one file changes):
.claude/skills/agile-epic-implement/modules/task-decomposer.md (Gate 5a)
.claude/skills/agile-epic-implement/modules/build-context.md (Gate 5b)
.claude/skills/agile-epic-implement/modules/file-finder.md (Gate 5b)
.claude/skills/agile-epic-implement/modules/task-delegator.md (Gates 5c, 5d)
.claude/skills/agile-epic-implement/modules/teammate-context.md (Gate 6a, 6b)
.claude/skills/agile-epic-implement/modules/team-lead.md (Gate 6a, 6b)
.claude/skills/agile-epic-implement/modules/status-tracker.md (Gates 4, 7)
Guardrails
- Dispatch-only scope — does NOT spawn teams, validate completed work, mutate
epic.md,
maintain checklist.md, or commit. Those belong to other skills.
- Existing team required — refuses to run if
~/.claude/teams/{team-name}/config.json is
missing or empty.
- Inline execution — never spawns subagents. Runs all steps in the team-lead session.
- state.json is authoritative — no other source of wave/feature truth. Reads it first, writes
it last, never holds the file open.
- Inbox-IDs ≠ task-IDs —
addBlockedBy references inbox IDs from taskInboxMapping, not the
NNN-XX-T{nn} IDs. Confusing them silently breaks dependency chains.
- No duplicate TaskCreate — if
taskInboxMapping already has an entry for a task, never
create another. Recreating produces duplicate inbox IDs and broken addBlockedBy chains.
- SendMessage uses
to/message/summary — never recipient/content/type. The tool
schema rejects those, even if older skill docs use them.
- No
TaskUpdate(owner=...) — that path triggers hook errors. Always use SendMessage.
- tasks.md is authoritative for markers — auto-fix only the narrow
completed-in-inbox-but-
not-yet-[X]-in-tasks.md drift case. Anything ambiguous: warn, don't write.
- Cross-feature deps go in description, not addBlockedBy —
addBlockedBy cannot span
features; cross-feature ordering is enforced via SendMessage DEPENDENCY CLEARED notices
sent at runtime by the parent skill.
- Hard pre-conditions — if any pre-condition fails, exit with the specific remediation
message and a nonzero status. Do not try to repair state.
Error Handling
| Scenario | Behavior |
|---|
| Cannot resolve epic from cwd/branch/args | Stop. "Cannot resolve target epic." |
state.json missing | Stop. "No state.json — run /agile:epic:implement first" |
Team config missing or members[] empty | Stop. "No team — run /agile:team create + add first" |
epicStatus == COMPLETED | Clean exit. "Epic already complete — nothing to dispatch" |
All tasks working, none assignable | Clean exit. "All tasks in flight — team is busy" |
| Inbox empty AND no remaining waves | Clean exit. "All waves dispatched. Run /agile:epic:validate next" |
| Inbox empty AND next wave fully blocked by unmet deps | Stop. "Next wave blocked: features {ids} have unmet deps" |
taskInboxMapping references inbox IDs not in TaskList | Treat as stale; report and skip |
tasks.md cannot be parsed | Skip that feature; report; continue with siblings |
SendMessage rejects parameters | Stop and report. Do NOT retry with alternate field names |
TaskList returns error | Stop. Likely not a team-lead session |
Synchronization with AgileKit
| Skill | Relationship |
|---|
/agile:epic:brainstorm | Independent — runs before any of this exists |
/agile:epic:plan | Independent — produces the artifacts this skill reads |
/agile:epic:implement | Parent. Sets up state.json, owns Phase 1 and Phase 5 |
/agile:team (create/add) | Hard prerequisite |
/agile:epic:progress | Sibling. Run after assign to view dashboard |
/agile:epic:validate | Successor. Run after the last wave is fully complete |