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.
done card offers NO Plan and NO Re-plan — parent or child, every kind, no exception. (Gate on the status CATEGORY, so cancelled counts; see MOTIR-2084.)Container vs leaf is the split isTypeableKind (lib/issues/executorDefaults.ts) already encodes: epic / story are containers, task / bug / subtask are leaves.
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 |
WorkItemPlanEntrance picks its face from hasChildren alone. A leaf never has children, so a described subtask/task/bug reads Plan where the rule says Re-plan. hasChildren is 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.planningWorkspaceHref / kind: 'work-item'; this path goes through PlanEditsTrigger (MOTIR-903, a one-shot job + review dock) instead, so the grep never saw it. Its gates are:
canEdit && !archived && hasChildren === false && onExpand (no kind check, so a LEAF gets "Expand", and no status check)canEdit && !archived && (kind === 'epic' || kind === 'story') && onReplan (no hasChildren, so a childless epic offers Re-plan against rule 3; no status, so a done epic offers it against rule 1)
Only app/(authed)/items/_components/WorkItemRowActions.tsx passes these callbacks, so /items is the only host.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, where null means "draw nothing". Keep showsPlanEntrance as 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.WorkItemPlanEntrance takes kind + hasDescription and asks the function for its face, instead of hasChildren ? replan : plan. Both call sites already hold both values (item.kind / item.descriptionMd; data.kind / data.descriptionMd).'plan' → Expand, 'replan' → Re-plan, null → neither. IssueRowData already carries kind, hasChildren and statusCategory — so rules 1 and 3 land with NO data change.'done' key (MOTIR-2084's lesson — the default workflow has two done-category statuses).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.
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)