Problem
adv_epic_retire cannot retire an empty (zero-entry) Epic. It refuses with epic_incomplete even when no blockers exist:
{
"error": "Epic <id> has incomplete entries and cannot be retired",
"code": "epic_incomplete",
"blockers": []
}
The empty blockers array confirms the refusal is structural, not product-blocked. There is no operator-only Epic purge tool, and adv_archive_purge is changes-only.
Root cause
recomputeEpicProgress at plugin/src/temporal/epic-state.ts:899-904 derives Epic status after every mutation:
status:
state.status === "archived"
? EpicStatusSchema.enum.archived
: completed === total && total > 0 // ← line 902, the gap
? EpicStatusSchema.enum.completed
: EpicStatusSchema.enum.active,
For total=0, completed=0: completed === total is 0 === 0 → true, but && total > 0 is false, so the whole condition is false → status forced to active. An empty epic can never reach completed via this path. Because recomputeEpicProgress runs after every mutation, no subsequent tool can flip the status.
Both retirement guards then reject on progress.status !== "completed":
plugin/src/storage/store-temporal/epics.ts:467 (store-layer retire)
plugin/src/temporal/epic-state.ts:751-754 (applyEpicArchivedToState workflow-signal re-check)
Both: if (progress.status !== "completed" || blockers.length > 0) → throw epic_incomplete.
Evidence (live incident)
alignPokeedgeSkyTheme in the pokeedge-web project is a historical static-theme Epic (Taupe/Sky alignment target). The work has been superseded by the current Stone/Sky + premiumColorPresets direction. Its narrative explicitly says:
Retire this empty Epic once the Epic retirement path accepts zero-entry initiatives. Current adv_epic_retire dry run refuses it as epic_incomplete while returning no blockers, so retirement is tool-blocked rather than product-blocked.
State (via adv_epic_show view:compact):
{
"total_entries": 0,
"completed_entries": 0,
"active_entries": 0,
"next_entry_id": null,
"status": "active",
"version": 1
}
adv_epic_retire dry-run probe (2026-07-23):
adv_epic_retire(
epic_id: "alignPokeedgeSkyTheme",
expected_version: 1,
evidence: "Dry-run probe...",
retired_by: "agent",
dryRun: true
)
→ {"error":"Epic alignPokeedgeSkyTheme has incomplete entries and cannot be retired",
"code":"epic_incomplete","blockers":[]}
Test coverage gap
recomputeEpicProgress suite (epic-state.test.ts:814-873) only covers non-empty cases (3 mixed entries → active; 2 terminal → completed). The 0-entry case is entirely untested.
- All
epic_incomplete tests (epic-state.test.ts:969-996, store-temporal/epics.test.ts:745-778) use epics with ≥1 named active/shell entry. None exercise the 0-entry path.
Proposed fix
Option A (recommended, minimal): drop && total > 0 at epic-state.ts:902:
- : completed === total && total > 0
+ : completed === total
Empty epic → completed; eligible to retire naturally; both guards already accept blockers: []. Semantically correct (zero work = complete). No existing test breaks (verified: both recomputeEpicProgress cases use total ≥ 2). A brand-new empty epic becomes immediately retireable, but retirement is still an explicit tool call — no auto-retire risk.
Option B (conservative): leave status active for empty epics; add total_entries === 0 as an alternate pass path in both guards (epics.ts:467, epic-state.ts:751). Matches the "retirement-path-only" framing in the Epic narrative but touches 2 sites.
Either option should ship with a regression test in epic-state.test.ts (recomputeEpicProgress 0-entry case) and store-temporal/epics.test.ts (retire-on-empty).
No projection-write changes needed — RetiredEpicProjection snapshots whatever state.epic holds, including empty entries: [].
Acceptance sketch
- Given an empty Epic (
total_entries === 0, no entries), adv_epic_retire succeeds and the Epic moves to retired projection.
adv_epic_retire dryRun: true on an empty Epic returns the retirement preview without epic_incomplete.
recomputeEpicProgress on entries: [] produces status: completed (Option A) or stays active but remains retireable (Option B).
- Regression test in
epic-state.test.ts covers the 0-entry recomputeEpicProgress case.
- Regression test in
store-temporal/epics.test.ts covers retire-on-empty.
Related
- Discussed in
alignPokeedgeSkyTheme Epic narrative (pokeedge-web project, refreshed 2026-07-19).
- Adjacent logic:
recomputeEpicProgress callers at epic-state.ts:182,204,232,275,339,422,461,577,611,660,701,734,797 — no behavioral change for non-empty epics.
- No existing ADV change in
~/dev/advance covers this; this issue is the tracking entry.
Filed from the pokeedge-web session after adv-temporal-repair diagnosis (2026-07-23).
Problem
adv_epic_retirecannot retire an empty (zero-entry) Epic. It refuses withepic_incompleteeven when no blockers exist:{ "error": "Epic <id> has incomplete entries and cannot be retired", "code": "epic_incomplete", "blockers": [] }The empty
blockersarray confirms the refusal is structural, not product-blocked. There is no operator-only Epic purge tool, andadv_archive_purgeis changes-only.Root cause
recomputeEpicProgressatplugin/src/temporal/epic-state.ts:899-904derives Epicstatusafter every mutation:For
total=0, completed=0:completed === totalis0 === 0→true, but&& total > 0isfalse, so the whole condition isfalse→ status forced toactive. An empty epic can never reachcompletedvia this path. BecauserecomputeEpicProgressruns after every mutation, no subsequent tool can flip the status.Both retirement guards then reject on
progress.status !== "completed":plugin/src/storage/store-temporal/epics.ts:467(store-layerretire)plugin/src/temporal/epic-state.ts:751-754(applyEpicArchivedToStateworkflow-signal re-check)Both:
if (progress.status !== "completed" || blockers.length > 0)→ throwepic_incomplete.Evidence (live incident)
alignPokeedgeSkyThemein the pokeedge-web project is a historical static-theme Epic (Taupe/Sky alignment target). The work has been superseded by the current Stone/Sky +premiumColorPresetsdirection. Its narrative explicitly says:State (via
adv_epic_show view:compact):{ "total_entries": 0, "completed_entries": 0, "active_entries": 0, "next_entry_id": null, "status": "active", "version": 1 }adv_epic_retiredry-run probe (2026-07-23):Test coverage gap
recomputeEpicProgresssuite (epic-state.test.ts:814-873) only covers non-empty cases (3 mixed entries → active; 2 terminal → completed). The 0-entry case is entirely untested.epic_incompletetests (epic-state.test.ts:969-996,store-temporal/epics.test.ts:745-778) use epics with ≥1 named active/shell entry. None exercise the 0-entry path.Proposed fix
Option A (recommended, minimal): drop
&& total > 0atepic-state.ts:902:Empty epic →
completed; eligible to retire naturally; both guards already acceptblockers: []. Semantically correct (zero work = complete). No existing test breaks (verified: bothrecomputeEpicProgresscases usetotal ≥ 2). A brand-new empty epic becomes immediately retireable, but retirement is still an explicit tool call — no auto-retire risk.Option B (conservative): leave status
activefor empty epics; addtotal_entries === 0as an alternate pass path in both guards (epics.ts:467,epic-state.ts:751). Matches the "retirement-path-only" framing in the Epic narrative but touches 2 sites.Either option should ship with a regression test in
epic-state.test.ts(recomputeEpicProgress0-entry case) andstore-temporal/epics.test.ts(retire-on-empty).No projection-write changes needed —
RetiredEpicProjectionsnapshots whateverstate.epicholds, including emptyentries: [].Acceptance sketch
total_entries === 0, no entries),adv_epic_retiresucceeds and the Epic moves toretiredprojection.adv_epic_retire dryRun: trueon an empty Epic returns the retirement preview withoutepic_incomplete.recomputeEpicProgressonentries: []producesstatus: completed(Option A) or staysactivebut remains retireable (Option B).epic-state.test.tscovers the 0-entryrecomputeEpicProgresscase.store-temporal/epics.test.tscovers retire-on-empty.Related
alignPokeedgeSkyThemeEpic narrative (pokeedge-web project, refreshed 2026-07-19).recomputeEpicProgresscallers atepic-state.ts:182,204,232,275,339,422,461,577,611,660,701,734,797— no behavioral change for non-empty epics.~/dev/advancecovers this; this issue is the tracking entry.Filed from the pokeedge-web session after
adv-temporal-repairdiagnosis (2026-07-23).