MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-2097

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

Done
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

  1. A 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.)
  2. A LEAF with a description shows Re-plan, not Plan. A leaf with no description shows Plan.
  3. 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.

SurfaceRule 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 Expanda 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.
  • The row ⋯ menu was missed entirely by MOTIR-2084. Its blast-radius analysis grepped 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:
    • 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 (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.

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, 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).
  • The row ⋯ menu asks the same function, mapping the face onto its own two menu items: 'plan'Expand, 'replan'Re-plan, null → neither. IssueRowData already carries kind, hasChildren and statusCategory — 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)