The Plan / Re-plan RULE is not applied everywhere — the /items row ⋯ menu has no terminal gate and offers Re-plan on a childless epic, and no surface picks the leaf face from the description
Type: Bug (code) · UI / state-correctness. The follow-up to MOTIR-2084, which gated ONE of the three planning affordances. Yue stated the governing rule in full (2026-08-04) and it is not applied anywhere in its entirety.
Parent: MOTIR-1464 — the quality-bugs epic, the same home MOTIR-2050 and MOTIR-2084 took.
THE RULE (Yue, 2026-08-04) — it applies EVERYWHERE
- A
donecard offers NO Plan and NO Re-plan — parent or child, every kind, no exception. (Gate on the status CATEGORY, socancelledcounts; see MOTIR-2084.) - A LEAF with a description shows Re-plan, not Plan. A leaf with no description shows Plan.
- An epic or story WITHOUT children is always Plan; WITH children it is Re-plan. (Rule 3 wins for containers — a described but childless epic is still Plan.)
Container vs leaf is the split isTypeableKind (lib/issues/executorDefaults.ts) already encodes: epic / story are containers, task / bug / subtask are leaves.
What is wrong today (VERIFIED against origin/main @ d58a7c0a)
Three surfaces carry a Plan / Re-plan affordance. MOTIR-2084 fixed rule 1 on two of them and no surface implements rule 2.
| Surface | Rule 1 (done) | Rule 2 (leaf ← description) | Rule 3 (container ← children) |
|---|---|---|---|
Detail page header (WorkItemPlanEntrance) | ✅ MOTIR-2084 | ❌ leaf always reads Plan | ✅ |
| Quick-view peek (same component) | ✅ MOTIR-2084 | ❌ same | ✅ |
/items row ⋯ menu (WorkItemActionsMenu) | ❌ no status gate at all | ❌ a leaf shows Expand | ❌ a childless epic shows Re-plan |
WorkItemPlanEntrancepicks its face fromhasChildrenalone. A leaf never has children, so a described subtask/task/bug reads Plan where the rule says Re-plan.hasChildrenis the right input for containers (rule 3) and the wrong one for leaves (rule 2) — the component needs the KIND to know which rule it is under.- The row ⋯ menu was missed entirely by MOTIR-2084. Its blast-radius analysis grepped
planningWorkspaceHref/kind: 'work-item'; this path goes throughPlanEditsTrigger(MOTIR-903, a one-shot job + review dock) instead, so the grep never saw it. Its gates are:- Expand —
canEdit && !archived && hasChildren === false && onExpand(no kind check, so a LEAF gets "Expand", and no status check) - Re-plan —
canEdit && !archived && (kind === 'epic' || kind === 'story') && onReplan(nohasChildren, so a childless epic offers Re-plan against rule 3; no status, so a done epic offers it against rule 1) Onlyapp/(authed)/items/_components/WorkItemRowActions.tsxpasses these callbacks, so/itemsis the only host.
- Expand —
Fix direction
- ONE function decides all three rules, and every surface asks it. Extend
lib/planning/planEntranceVisibility.ts(the module MOTIR-2084 created) from a boolean to a face:planEntranceFace({ canPlan, archived, statusCategory, kind, hasChildren, hasDescription }) → 'plan' | 'replan' | null, wherenullmeans "draw nothing". KeepshowsPlanEntranceas the boolean derived from it, or fold it in — but there must be exactly ONE place the rule lives. MOTIR-2050 and MOTIR-2084 both had to retro-fit a gate onto inlined booleans; this is the third and last time that shape should exist. WorkItemPlanEntrancetakeskind+hasDescriptionand asks the function for its face, instead ofhasChildren ? replan : plan. Both call sites already hold both values (item.kind/item.descriptionMd;data.kind/data.descriptionMd).- The row ⋯ menu asks the same function, mapping the face onto its own two menu items:
'plan'→ Expand,'replan'→ Re-plan,null→ neither.IssueRowDataalready carrieskind,hasChildrenandstatusCategory— so rules 1 and 3 land with NO data change. - Rule 2 on the row menu needs data that is not there — see the follow-up card below. Until it lands, the row menu's leaf face is decided without the description; make that a NAMED, commented degrade, not a silent one.
- Gate on the category, never the
'done'key (MOTIR-2084's lesson — the default workflow has two done-category statuses).
Out of scope, but flag it in the PR
PlanEditsLauncher's own docblock records that MOTIR-1731 retired the one-shot AugmentPromptButton because "changing a plan is a CONVERSATION, so the entrance is the universal Plan-with-AI workspace … never a per-surface button with no way to refine." The row ⋯ menu's Expand / Re-plan IS a per-surface one-shot job with a review dock — the same shape that decision retired, and a different mechanism from the entrance the other two surfaces use. Whether the row menu should be re-pointed at the workspace (making the rule trivially uniform) is a PRODUCT question for Yue, not this card's to decide. Report it; do not act on it.
Tests
Per-rule unit tests on the extracted function (the full kind × children × description × category matrix, including a cancelled case proving the gate is category-based), plus component tests: the entrance's leaf-with-description → Re-plan face, the row menu's done-epic → no items and childless-epic → Expand-not-Re-plan.
Resolution: (open)