| name | review-epic |
| description | Use when asked to review epic health, audit stalled children, check scope drift, find missing coverage, or assess closure readiness. Produces a structured health report and actionable recommendations. Read-only. |
| argument-hint | <EPIC-ID> [--skip-drift] |
| model | sonnet |
| allowed-tools | ["Read","Bash(ll-issues:*)","Bash(git:*)","Bash(ll-history-context:*)"] |
| arguments | [{"name":"epic_id","description":"EPIC ID to review (e.g., EPIC-42)","required":true},{"name":"flags","description":"--skip-drift to skip scope-drift and missing-coverage LLM passes (structural-only, fast mode)","required":false}] |
| metadata | {"short-description":"Audit EPIC health: stalled children, scope drift, missing coverage."} |
Review Epic
Audits an EPIC's children for stalled status, scope drift, missing coverage, and closure
readiness. Outputs a structured health report and actionable recommendations. Never
writes to any file — all suggestions require the user to invoke a follow-up command.
Step 1: Parse Arguments
Extract from $ARGUMENTS:
- Set
EPIC_ID to the first token (e.g., EPIC-42). Uppercase it.
- Set
SKIP_DRIFT=true if --skip-drift is present in flags.
- Read
STALE_DAYS from {{config.commands.review_epic.stale_days}} (default 14).
- Read
ENABLE_SCOPE_DRIFT from {{config.commands.review_epic.enable_scope_drift_check}}
(default true). Force-disable if SKIP_DRIFT=true.
Step 2: Load EPIC and Resolve Children
2a. Verify EPIC exists
ll-issues list --status open,in_progress,blocked,done,cancelled,deferred --type EPIC --json
Parse the JSON output. Find the record where issue_id == EPIC_ID.
If not found, report and stop:
✗ EPIC not found: EPIC_ID
Run `ll-issues list --type EPIC` to see available EPICs.
Record the EPIC's path, title, status, and relates_to list.
2b. Load all issues for child resolution
ll-issues list --status open,in_progress,blocked,done,cancelled,deferred --json
Parse the JSON list as all_issues.
2c. Resolve children
Use parent: backrefs only — mirrors compute_epic_progress() in
scripts/little_loops/issue_progress.py:120:
relates_to_ids = set of IDs from the EPIC's `relates_to` list (recorded in Step 2a)
child_ids = {issue_id for issue in all_issues if issue.parent == EPIC_ID}
children = [issue for issue in all_issues if issue.issue_id in child_ids]
related_not_children = [issue for issue in all_issues
if issue.issue_id in relates_to_ids
and issue.issue_id not in child_ids]
If no children, report and stop:
================================================================================
EPIC HEALTH REPORT: EPIC_ID — EPIC title
================================================================================
This EPIC has no linked children.
Next steps:
• /ll:capture-issue -- with "parent: EPIC_ID" to create a child issue
• /ll:link-epics to assign orphaned issues to this EPIC
================================================================================
Step 3: Compute Progress Aggregates
ll-issues epic-progress EPIC_ID
Parse the JSON output for by_status, percent_done, total, and oldest_open.
Compute:
done_count = sum of by_status values for done and cancelled
active_count = sum for open, in_progress, blocked
deferred_count = by_status.deferred or 0
oldest_open_age = oldest_open.age_days from the JSON (or null if none)
Step 4: Stall Detection (Non-LLM)
For each child issue, fetch effort context to surface in the health report:
EFFORT=$(ll-history-context --for-skill review-epic --effort CHILD_ISSUE_ID 2>/dev/null || true)
If $EFFORT is non-empty, include session count and cycle time in the health report
alongside the child issue's status.
For each child with status open, in_progress, or blocked:
- Read the child issue file content.
- Extract the last-activity date using the session-log pattern (same logic as
_parse_updated_date() in scripts/little_loops/cli/issues/search.py):
- Compute
days_since_activity = today - last_activity_date.
- Flag as stalled if
days_since_activity > STALE_DAYS.
Collect all stalled children as stalled_children list with their
{id, title, status, days_since_activity}.
Step 5: Scope Drift Classification (LLM Pass)
Skip this step if ENABLE_SCOPE_DRIFT=false.
Read the EPIC file's ## Summary section (text after ## Summary heading, before the
next ## heading).
For each active child (open, in_progress, blocked), read its ## Summary section.
Classify each child using the following scoring table. Read the EPIC summary and each
child summary carefully, then assign one classification per child:
Classification criteria:
| Classification | Definition |
|---|
on-theme | Child directly implements, fixes, or improves something explicitly named in the EPIC summary, or clearly supports the EPIC's stated goal |
tangential | Child touches related technology or systems but its stated goal does not map to any area named in the EPIC summary; could belong under a different EPIC |
off-theme | Child goal contradicts or is entirely unrelated to the EPIC summary; likely belongs under a sibling EPIC or no EPIC |
For each child, produce:
{id}: <classification> — <one sentence rationale>
Collect only tangential and off-theme results as scope_drift_findings.
Step 6: Missing Coverage Analysis (LLM Pass)
Skip this step if ENABLE_SCOPE_DRIFT=false.
Parse the EPIC summary for named sub-areas, goals, or deliverable phrases. Look for:
- Quoted feature names, component names, or action phrases
- Numbered or bulleted sub-goals
- Sentences beginning with "will", "should", "must", or "covers"
For each identified sub-area, check whether any child's ## Summary section covers it.
A child "covers" a sub-area when its summary mentions the same component, action, or
deliverable — exact wording not required, but semantic overlap must be clear.
Collect uncovered sub-areas as missing_coverage_findings:
{sub-area description}: no child issue covers this area
Step 7: Closure Check (Pure)
Check all child statuses from by_status:
- Ready to close if
done_count == len(children) (all children are done or
cancelled) and active_count == 0.
- Not ready otherwise — note the count of still-active children.
Step 8: Render Health Report
Output the following Markdown report. Use N/A for any section where data is empty.
================================================================================
EPIC HEALTH REPORT: EPIC_ID — EPIC title
================================================================================
**Progress**: done_count/total done (percent_done%) · active_count active · deferred_count deferred
### Stalled children (> STALE_DAYS days without activity)
[For each stalled child]:
- CHILD_ID — CHILD_TITLE
Status: child_status · Last activity: DATE (days_since_activity days ago)
Recommendation: `ll-issues set-status CHILD_ID deferred` to park, or `/ll:manage-issue ... CHILD_ID` to resume. For bulk deferral of all stalled children, prefer `ll-issues set-status EPIC_ID done --cascade` to close everything at once.
[If none]: No stalled children detected.
### Scope drift
[For each scope_drift_finding]:
- CHILD_ID [classification] — rationale
Recommendation: reparent with `ll-issues` edit, or detach by removing `parent: EPIC_ID` from frontmatter
[If none / ENABLE_SCOPE_DRIFT=false]: No scope drift detected. [or: Skipped (--skip-drift).
### Missing coverage
[For each missing_coverage_finding]:
- "sub-area description" — no child issue covers this area
Recommendation: `/ll:capture-issue` to create a child issue targeting this area
[If none / ENABLE_SCOPE_DRIFT=false]: No missing coverage detected. [or: Skipped (--skip-drift).]
### Closure recommendation
[If ready]: All children done or cancelled — recommend: `ll-issues set-status EPIC_ID done`. Use `--cascade` to close any remaining open children in the same call.
[If not ready]: Not ready (active_count active children)
### Related (not children)
[For each issue in related_not_children]:
- ISSUE_ID — ISSUE_TITLE (status: STATUS)
[If empty]: No cross-references (relates_to list is empty or all entries are also children).
================================================================================
After the banner, add a ## Recommendations section that lists each runnable command
implied by the findings above, formatted as a numbered checklist:
## Recommendations
[N findings — or "No action needed." if all sections are clean]
1. (if stalled children exist) Defer or resume stalled children:
`ll-issues set-status CHILD_ID deferred` — park ENH-NNN (stalled N days). Bulk option: `ll-issues set-status EPIC_ID cancelled --cascade`
2. (if scope drift) Reparent or detach drifted children:
Review CHILD_ID frontmatter to update or remove `parent:` field
3. (if missing coverage) Capture new child issues for uncovered sub-areas:
`/ll:capture-issue` — describe the missing sub-area and set `parent: EPIC_ID`
4. (if closure-ready) Mark EPIC done:
`ll-issues set-status EPIC_ID done --cascade`
Step 9: Guard Rails
- Never write to any issue file. This skill is read-only audit. All mutations are
user-initiated follow-up commands shown in the Recommendations section.
--skip-drift mode: Steps 5 and 6 are skipped; the scope-drift and missing-coverage
sections display Skipped (--skip-drift). instead of findings.
- Empty EPIC (Step 2c exit): emit the early-exit message and stop — do not proceed to
Steps 3–8.
- EPIC not found (Step 2a exit): emit the not-found error and stop.
- Config defaults: if
{{config.commands.review_epic.stale_days}} is unavailable,
use 14. If {{config.commands.review_epic.enable_scope_drift_check}} is unavailable,
default to enabled.
Usage Examples
/ll:review-epic EPIC-42
/ll:review-epic EPIC-42 --skip-drift
Output Evidence Contract (verbatim-output rule)
When this skill emits an audit finding, verdict, or scorecard, cite evidence
verbatim rather than re-summarizing — quoting is cheaper than paraphrasing and
keeps the audit auditable:
IMPORTANT: For each condition you evaluate:
- State your verdict: Yes / No / Partial
- Provide a VERBATIM quote from the output that supports your verdict (exact text, in quotes)
- If you cannot quote specific text, your verdict is automatically No (or Partial if context suggests partial progress)
Do not assert a verdict without evidence. "The task appears complete" is not evidence.