mindsystem-cc 4.4.1 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +17 -7
  2. package/agents/ms-compounder.md +19 -7
  3. package/agents/ms-consolidator.md +11 -1
  4. package/agents/ms-designer.md +25 -47
  5. package/agents/ms-executor.md +1 -1
  6. package/agents/ms-mockup-designer.md +7 -4
  7. package/agents/ms-plan-checker.md +32 -27
  8. package/agents/ms-plan-writer.md +12 -8
  9. package/commands/ms/adhoc.md +11 -1
  10. package/commands/ms/config.md +47 -9
  11. package/commands/ms/design-phase.md +83 -63
  12. package/commands/ms/discuss-phase.md +1 -0
  13. package/commands/ms/doctor.md +7 -3
  14. package/commands/ms/execute-phase.md +1 -5
  15. package/commands/ms/help.md +6 -5
  16. package/commands/ms/remove-phase.md +7 -25
  17. package/commands/ms/research-phase.md +13 -0
  18. package/commands/ms/review-design.md +1 -1
  19. package/commands/ms/verify-work.md +1 -3
  20. package/mindsystem/references/design-directions.md +2 -2
  21. package/mindsystem/references/knowledge-quality.md +89 -0
  22. package/mindsystem/references/plan-format.md +2 -13
  23. package/mindsystem/references/prework-status.md +6 -32
  24. package/mindsystem/references/routing/next-phase-routing.md +7 -41
  25. package/mindsystem/references/scope-estimation.md +8 -4
  26. package/mindsystem/templates/config.json +6 -0
  27. package/mindsystem/templates/design.md +1 -1
  28. package/mindsystem/templates/knowledge.md +18 -3
  29. package/mindsystem/workflows/adhoc.md +63 -0
  30. package/mindsystem/workflows/discuss-phase.md +12 -0
  31. package/mindsystem/workflows/doctor-fixes.md +71 -0
  32. package/mindsystem/workflows/execute-phase.md +19 -6
  33. package/mindsystem/workflows/execute-plan.md +1 -7
  34. package/mindsystem/workflows/mockup-generation.md +1 -1
  35. package/mindsystem/workflows/plan-phase.md +41 -77
  36. package/mindsystem/workflows/verify-work.md +8 -77
  37. package/package.json +1 -1
  38. package/scripts/ms-tools.py +481 -0
  39. package/agents/ms-verify-fixer.md +0 -125
  40. package/mindsystem/workflows/transition.md +0 -460
@@ -376,14 +376,13 @@ Format:
376
376
  Fix: {what the fix changes, 1-2 sentences}
377
377
  ```
378
378
 
379
- **If fix is simple (single file, straightforward change):**
380
- - Go to `apply_fix` immediately
381
-
382
- **If fix is complex (multiple files, architectural):**
383
- - Spawn ms-verify-fixer subagent (go to `escalate_to_fixer`)
379
+ Go to `apply_fix`.
384
380
 
385
381
  **If cause NOT found after 2-3 checks:**
386
- - Escalate to fixer subagent (go to `escalate_to_fixer`)
382
+ - Present options via AskUserQuestion:
383
+ 1. Try different approach — Investigate from another angle
384
+ 2. Skip as assumption — Log and move on
385
+ 3. Manual investigation — You'll look into this yourself
387
386
  </step>
388
387
 
389
388
  <step name="apply_fix">
@@ -423,74 +422,6 @@ Fix applied. Please re-test: {specific instruction}
423
422
  Go to `handle_retest`.
424
423
  </step>
425
424
 
426
- <step name="escalate_to_fixer">
427
- **Spawn fixer subagent for complex issue:**
428
-
429
- **1. Stash mocks (if active):**
430
- `ms-tools uat-stash-mocks $PHASE_NUMBER`
431
-
432
- **2. Spawn ms-verify-fixer:**
433
- ```
434
- Task(
435
- prompt="""
436
- You are a Mindsystem verify-fixer. Investigate this issue, find the root cause, implement a fix, and commit it.
437
-
438
- ## Issue
439
-
440
- **Test:** {test_name}
441
- **Expected:** {expected_behavior}
442
- **Actual:** {user_reported_behavior}
443
- **Severity:** {inferred_severity}
444
-
445
- ## Context
446
-
447
- **Phase:** {phase_name}
448
- **Mock state active:** {mock_type or "none"}
449
- **Relevant files (suspected):** {file_list}
450
-
451
- ## What was already checked
452
-
453
- {lightweight_investigation_results}
454
-
455
- ## Knowledge Context
456
-
457
- {loaded_knowledge_content or "No knowledge files loaded for this session."}
458
-
459
- ## Your task
460
-
461
- 1. Investigate to find root cause
462
- 2. Implement minimal fix
463
- 3. Commit with message: fix({phase}-uat): {description}
464
- 4. Return FIX COMPLETE or INVESTIGATION INCONCLUSIVE
465
-
466
- Mocks are stashed — working tree is clean.
467
- """,
468
- subagent_type="ms-verify-fixer",
469
- description="Fix: {test_name}"
470
- )
471
- ```
472
-
473
- **3. Handle fixer return:**
474
-
475
- **If FIX COMPLETE:**
476
- - Record fix via ms-tools (same as `apply_fix` step 4: `uat-update --test N` + `--append-fix`)
477
- - Restore mocks: `ms-tools uat-pop-mocks $PHASE_NUMBER`
478
- - Request re-test
479
-
480
- **If INVESTIGATION INCONCLUSIVE:**
481
- - Restore mocks: `ms-tools uat-pop-mocks $PHASE_NUMBER`
482
- - Present options:
483
- ```
484
- Investigation didn't find root cause.
485
-
486
- Options:
487
- 1. Try different approach — I'll investigate from another angle
488
- 2. Skip as assumption — Log and move on
489
- 3. Manual investigation — You'll look into this yourself
490
- ```
491
- - Handle response accordingly
492
- </step>
493
-
494
425
  <step name="handle_retest">
495
426
  **Handle re-test result:**
496
427
 
@@ -515,12 +446,12 @@ ms-tools uat-update $PHASE_NUMBER --test N retry_count=1
515
446
 
516
447
  Options:
517
448
  1. Try different approach — Investigate from scratch
518
- 2. Escalate to subagentFresh context might help
519
- 3. Skip as assumption Log and move on
449
+ 2. Skip as assumptionLog and move on
450
+ 3. Manual investigationYou'll look into this yourself
520
451
  ```
521
452
  - "Try different" → Reset investigation, try again
522
- - "Escalate" → Go to `escalate_to_fixer`
523
453
  - "Skip" → Mark as skipped assumption
454
+ - "Manual" → Mark as skipped, user will investigate
524
455
 
525
456
  **If New issue:**
526
457
  - Mark original as pass
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindsystem-cc",
3
- "version": "4.4.1",
3
+ "version": "4.5.0",
4
4
  "description": "The engineer's meta-prompting system for Claude Code.",
5
5
  "bin": {
6
6
  "mindsystem-cc": "bin/install.js"
@@ -168,6 +168,14 @@ def parse_roadmap_phases(roadmap_path: Path) -> list[tuple[str, str]]:
168
168
  return results
169
169
 
170
170
 
171
+ def _phase_sort_key(phase_str: str) -> float:
172
+ """Convert phase string to sortable float: '17' -> 17.0, '17.1' -> 17.1."""
173
+ try:
174
+ return float(phase_str)
175
+ except ValueError:
176
+ return float("inf")
177
+
178
+
171
179
  def run_git(*args: str) -> str:
172
180
  """Run a git command and return stdout. Raise on failure."""
173
181
  result = subprocess.run(
@@ -1413,6 +1421,121 @@ def cmd_doctor_scan(args: argparse.Namespace) -> None:
1413
1421
  record("WARN", "Screenshot Optimization")
1414
1422
  print()
1415
1423
 
1424
+ # ---- CHECK 13: Roadmap Format ----
1425
+ print("=== Roadmap Format ===")
1426
+ roadmap_path = planning / "ROADMAP.md"
1427
+ if not roadmap_path.is_file():
1428
+ print("Status: SKIP")
1429
+ print("No ROADMAP.md found")
1430
+ record("SKIP", "Roadmap Format")
1431
+ else:
1432
+ roadmap_text = roadmap_path.read_text(encoding="utf-8")
1433
+ all_phases = parse_roadmap_phases(roadmap_path)
1434
+
1435
+ if not all_phases:
1436
+ print("Status: SKIP")
1437
+ print("No phases found in ROADMAP.md")
1438
+ record("SKIP", "Roadmap Format")
1439
+ else:
1440
+ # Find incomplete phases — check overview checklist
1441
+ completed_phases: set[str] = set()
1442
+ for line in roadmap_text.splitlines():
1443
+ done_match = re.match(
1444
+ r"^-\s*\[x\]\s*\*\*Phase\s+(\d+(?:\.\d+)?)", line
1445
+ )
1446
+ if done_match:
1447
+ completed_phases.add(done_match.group(1))
1448
+
1449
+ phases_to_check = [
1450
+ (num, name)
1451
+ for num, name in all_phases
1452
+ if num not in completed_phases
1453
+ ]
1454
+
1455
+ if not phases_to_check:
1456
+ print("Status: PASS")
1457
+ print("All phases completed — no pre-work flags to validate")
1458
+ record("PASS", "Roadmap Format")
1459
+ else:
1460
+ issues: list[str] = []
1461
+ for num, name in phases_to_check:
1462
+ padded = normalize_phase(num)
1463
+ info = _parse_phase_section(roadmap_text, padded)
1464
+ if info is None:
1465
+ issues.append(f"Phase {num}: no detail section found")
1466
+ continue
1467
+ for flag in ("discuss", "design", "research"):
1468
+ pw = info["prework"][flag]
1469
+ if pw.get("status") == "parse_error":
1470
+ issues.append(
1471
+ f"Phase {num} ({name}): {flag.capitalize()} flag missing or malformed"
1472
+ )
1473
+
1474
+ if issues:
1475
+ print("Status: FAIL")
1476
+ print(f"{len(issues)} pre-work flag issue(s):")
1477
+ for issue in issues:
1478
+ print(f" - {issue}")
1479
+ record("FAIL", "Roadmap Format")
1480
+ else:
1481
+ print("Status: PASS")
1482
+ print(
1483
+ f"All {len(phases_to_check)} incomplete phase(s) have valid pre-work flags"
1484
+ )
1485
+ record("PASS", "Roadmap Format")
1486
+ print()
1487
+
1488
+ # ---- CHECK 14: Phase Skills ----
1489
+ print("=== Phase Skills ===")
1490
+ skills_config = config.get("skills", {})
1491
+ plan_skills = skills_config.get("plan", []) if isinstance(skills_config, dict) else []
1492
+ design_skills = skills_config.get("design", []) if isinstance(skills_config, dict) else []
1493
+ skill_warnings: list[str] = []
1494
+
1495
+ if not plan_skills:
1496
+ skill_warnings.append("plan")
1497
+ print("skills.plan: not configured")
1498
+ print(" Impact: Plan-phase code quality — the highest-leverage skill slot")
1499
+ print(" What to add: A code quality skill encoding your project's framework")
1500
+ print(" best practices. Include rules for common pitfalls, idiomatic patterns,")
1501
+ print(" performance gotchas, and structural conventions specific to your stack.")
1502
+ print(" The executor runs a multi-pass review after implementation, catching")
1503
+ print(" framework misuse and structural problems before they reach verification.")
1504
+ print(" Ideal structure: A SKILL.md with categorized rules (reactivity, typing,")
1505
+ print(" performance, composition) plus reference files with bad/good code examples")
1506
+ print(" for each category. The agent selectively reads only rules relevant to the")
1507
+ print(" changed code, keeping context usage efficient.")
1508
+ print(" Set up: Create a skill with framework-specific rules and reference files,")
1509
+ print(" then run /ms:config to add it to skills.plan")
1510
+ else:
1511
+ print(f"skills.plan: {', '.join(plan_skills)}")
1512
+
1513
+ if not design_skills:
1514
+ skill_warnings.append("design")
1515
+ print("skills.design: not configured")
1516
+ print(" Impact: Design-phase quality — ensures generated designs match your")
1517
+ print(" existing design system instead of generic AI output")
1518
+ print(" What to add: A skill describing your project's design system — color")
1519
+ print(" palette, typography, spacing scale, reusable components, and layout")
1520
+ print(" conventions. The designer agent uses this to produce designs that feel")
1521
+ print(" native to your product rather than starting from scratch.")
1522
+ print(" Ideal structure: Document your design tokens (colors, fonts, sizes),")
1523
+ print(" component inventory (buttons, cards, inputs with their variants), and")
1524
+ print(" brand guidelines (visual tone, density preference, platform conventions).")
1525
+ print(" Set up: Create a skill with your design tokens and component inventory,")
1526
+ print(" then run /ms:config to add it to skills.design")
1527
+ else:
1528
+ print(f"skills.design: {', '.join(design_skills)}")
1529
+
1530
+ if skill_warnings:
1531
+ print(f"Status: WARN")
1532
+ record("WARN", "Phase Skills")
1533
+ else:
1534
+ print("Status: PASS")
1535
+ print("Plan and design phase skills configured")
1536
+ record("PASS", "Phase Skills")
1537
+ print()
1538
+
1416
1539
  # ---- SUMMARY ----
1417
1540
  total = pass_count + warn_count + fail_count + skip_count
1418
1541
  print("=== Summary ===")
@@ -1464,6 +1587,140 @@ def cmd_create_phase_dirs(args: argparse.Namespace) -> None:
1464
1587
  print(f"\n{created} created, {skipped} skipped (already exist)")
1465
1588
 
1466
1589
 
1590
+ # ===================================================================
1591
+ # Subcommand: phase-renumber
1592
+ # ===================================================================
1593
+
1594
+
1595
+ def cmd_phase_renumber(args: argparse.Namespace) -> None:
1596
+ """Renumber phase directories and files after phase removal.
1597
+
1598
+ Contract:
1599
+ Args: phase (str, the removed phase number), --dry-run (bool)
1600
+ Output: JSON report of all renames
1601
+ Exit codes: 0 = success, 1 = error
1602
+ Side effects: renames directories and files (unless --dry-run)
1603
+ """
1604
+ removed = normalize_phase(args.phase)
1605
+ is_decimal = "." in removed
1606
+ dry_run = args.dry_run
1607
+
1608
+ git_root = find_git_root()
1609
+ planning = git_root / ".planning"
1610
+ phases_dir = planning / "phases"
1611
+
1612
+ if not phases_dir.is_dir():
1613
+ print("Error: .planning/phases/ directory not found", file=sys.stderr)
1614
+ sys.exit(1)
1615
+
1616
+ # Precondition: removed phase dir must not exist
1617
+ if find_phase_dir(planning, removed) is not None:
1618
+ print(
1619
+ f"Error: Directory for phase {removed} still exists. "
1620
+ "Delete it before renumbering.",
1621
+ file=sys.stderr,
1622
+ )
1623
+ sys.exit(1)
1624
+
1625
+ # Parse removed phase components
1626
+ removed_float = _phase_sort_key(removed)
1627
+ if is_decimal:
1628
+ removed_parent = int(removed_float) # e.g., 17.1 -> 17
1629
+ else:
1630
+ removed_int = int(removed_float) # e.g., 17
1631
+
1632
+ # Scan all phase dirs and compute renames
1633
+ renames: list[tuple[str, str, Path]] = [] # (old_phase, new_phase, dir_path)
1634
+ for d in sorted(phases_dir.iterdir()):
1635
+ if not d.is_dir():
1636
+ continue
1637
+ parts = d.name.split("-", 1)
1638
+ phase_prefix = parts[0]
1639
+ phase_float = _phase_sort_key(phase_prefix)
1640
+ if phase_float == float("inf"):
1641
+ continue # not a phase dir
1642
+
1643
+ is_phase_decimal = "." in phase_prefix
1644
+
1645
+ if is_decimal:
1646
+ # Decimal removal: only subsequent decimals in same series
1647
+ if not is_phase_decimal:
1648
+ continue
1649
+ phase_parent = int(phase_float)
1650
+ if phase_parent != removed_parent:
1651
+ continue
1652
+ if phase_float <= removed_float:
1653
+ continue
1654
+ # Decrement decimal: 17.2 -> 17.1, 17.3 -> 17.2
1655
+ old_decimal = int(phase_prefix.split(".")[1])
1656
+ new_decimal = old_decimal - 1
1657
+ new_phase = normalize_phase(f"{phase_parent}.{new_decimal}")
1658
+ renames.append((phase_prefix, new_phase, d))
1659
+ else:
1660
+ # Integer removal: all dirs with phase > removed decrement by 1
1661
+ if phase_float <= removed_float:
1662
+ continue
1663
+ if is_phase_decimal:
1664
+ # Decimal under higher integer: 18.1 -> 17.1
1665
+ parent = int(phase_float)
1666
+ dec = phase_prefix.split(".")[1]
1667
+ new_parent = parent - 1
1668
+ new_phase = normalize_phase(f"{new_parent}.{dec}")
1669
+ else:
1670
+ # Integer: 18 -> 17
1671
+ new_phase = normalize_phase(str(int(phase_float) - 1))
1672
+ renames.append((phase_prefix, new_phase, d))
1673
+
1674
+ # Sort ascending by phase key — ascending is correct because the removed
1675
+ # phase slot is free, so each rename fills the slot vacated by the previous
1676
+ renames.sort(key=lambda r: _phase_sort_key(r[0]))
1677
+
1678
+ # Collision detection: check every target path before renaming
1679
+ source_paths = {r[2] for r in renames}
1680
+ for old_phase, new_phase, dir_path in renames:
1681
+ suffix = dir_path.name.split("-", 1)[1] if "-" in dir_path.name else ""
1682
+ target_name = f"{new_phase}-{suffix}" if suffix else new_phase
1683
+ target_path = phases_dir / target_name
1684
+ if target_path.exists() and target_path not in source_paths:
1685
+ print(
1686
+ f"Error: Collision — renaming {dir_path.name} to {target_name} "
1687
+ f"would overwrite existing directory",
1688
+ file=sys.stderr,
1689
+ )
1690
+ sys.exit(1)
1691
+
1692
+ # Execute renames
1693
+ dir_renames = []
1694
+ file_renames = []
1695
+
1696
+ for old_phase, new_phase, dir_path in renames:
1697
+ suffix = dir_path.name.split("-", 1)[1] if "-" in dir_path.name else ""
1698
+ new_dir_name = f"{new_phase}-{suffix}" if suffix else new_phase
1699
+ new_dir_path = phases_dir / new_dir_name
1700
+
1701
+ dir_renames.append({"old": dir_path.name, "new": new_dir_name})
1702
+
1703
+ if not dry_run:
1704
+ dir_path.rename(new_dir_path)
1705
+
1706
+ # Rename files matching {old_phase}-* inside the directory
1707
+ scan_dir = new_dir_path if not dry_run else dir_path
1708
+ for f in sorted(scan_dir.iterdir()):
1709
+ if f.is_file() and f.name.startswith(f"{old_phase}-"):
1710
+ new_file_name = f"{new_phase}-{f.name[len(old_phase) + 1:]}"
1711
+ file_renames.append({"directory": new_dir_name, "old": f.name, "new": new_file_name})
1712
+ if not dry_run:
1713
+ f.rename(scan_dir / new_file_name)
1714
+
1715
+ report = {
1716
+ "removed_phase": removed,
1717
+ "dry_run": dry_run,
1718
+ "directory_renames": dir_renames,
1719
+ "file_renames": file_renames,
1720
+ }
1721
+ print(json.dumps(report, indent=2))
1722
+
1723
+
1467
1724
  # ===================================================================
1468
1725
  # Subcommand: gather-milestone-stats
1469
1726
  # ===================================================================
@@ -2217,6 +2474,219 @@ def cmd_list_artifacts(args: argparse.Namespace) -> None:
2217
2474
  sys.stdout.write("\n")
2218
2475
 
2219
2476
 
2477
+ # ===================================================================
2478
+ # Subcommand: prework-status
2479
+ # ===================================================================
2480
+
2481
+
2482
+ def _parse_phase_section(roadmap_text: str, phase: str) -> dict[str, Any] | None:
2483
+ """Parse a phase section from ROADMAP.md for pre-work flags.
2484
+
2485
+ Returns dict with name, goal, and prework flags, or None if phase not found.
2486
+ """
2487
+ # Try both padded ("08") and unpadded ("8") forms
2488
+ raw_match = re.match(r"^0*(\d.*)", phase)
2489
+ raw = raw_match.group(1) if raw_match else phase
2490
+ candidates = [phase] if raw == phase else [phase, raw]
2491
+
2492
+ match = None
2493
+ for candidate in candidates:
2494
+ pattern = rf"### Phase\s+{re.escape(candidate)}:\s*(.+)"
2495
+ match = re.search(pattern, roadmap_text)
2496
+ if match:
2497
+ break
2498
+
2499
+ if not match:
2500
+ return None
2501
+
2502
+ phase_name = match.group(1).strip()
2503
+
2504
+ # Extract section text until next "### " or end
2505
+ start = match.start()
2506
+ next_section = re.search(r"\n### ", roadmap_text[start + 1:])
2507
+ if next_section:
2508
+ section = roadmap_text[start : start + 1 + next_section.start()]
2509
+ else:
2510
+ section = roadmap_text[start:]
2511
+
2512
+ # Extract goal
2513
+ goal_match = re.search(r"\*\*Goal\*\*:\s*(.+)", section)
2514
+ goal = goal_match.group(1).strip() if goal_match else ""
2515
+
2516
+ # Extract pre-work flags with two-tier detection:
2517
+ # 1. Keyword check: does **Flag** appear at all?
2518
+ # 2. Full regex: does it match "Likely/Unlikely (reason)"?
2519
+ detail_keys = {"Discuss": "topics", "Design": "focus", "Research": "topics"}
2520
+ prework: dict[str, dict[str, str]] = {}
2521
+
2522
+ for flag_type, detail_key in detail_keys.items():
2523
+ # Tier 1: keyword presence check (case-insensitive)
2524
+ keyword_present = bool(
2525
+ re.search(rf"\*\*{flag_type}\*\*", section, re.IGNORECASE)
2526
+ )
2527
+
2528
+ # Tier 2: full regex match (case-insensitive, greedy reason capture)
2529
+ flag_match = re.search(
2530
+ rf"\*\*{flag_type}\*\*:\s*(Likely|Unlikely)(?:\s*\((.+)\))?",
2531
+ section,
2532
+ re.IGNORECASE,
2533
+ )
2534
+
2535
+ if flag_match:
2536
+ recommended = flag_match.group(1).capitalize()
2537
+ reason = (flag_match.group(2) or "").strip()
2538
+ status = "ok"
2539
+ elif keyword_present:
2540
+ # Keyword found but format doesn't match — non-standard
2541
+ recommended = ""
2542
+ reason = ""
2543
+ status = "parse_error"
2544
+ else:
2545
+ # Keyword absent — older roadmap format
2546
+ recommended = ""
2547
+ reason = ""
2548
+ status = "parse_error"
2549
+
2550
+ # Extract detail line (topics/focus)
2551
+ detail = ""
2552
+ if recommended == "Likely":
2553
+ detail_match = re.search(
2554
+ rf"\*\*{flag_type} {detail_key}\*\*:\s*(.+)", section, re.IGNORECASE
2555
+ )
2556
+ if detail_match:
2557
+ detail = detail_match.group(1).strip()
2558
+
2559
+ prework[flag_type.lower()] = {
2560
+ "recommended": recommended,
2561
+ "reason": reason,
2562
+ "detail": detail,
2563
+ "status": status,
2564
+ }
2565
+
2566
+ return {"name": phase_name, "goal": goal, "prework": prework}
2567
+
2568
+
2569
+ def _determine_prework_suggestion(
2570
+ prework: dict[str, dict[str, str]],
2571
+ has_context: bool,
2572
+ has_design: bool,
2573
+ has_research: bool,
2574
+ ) -> tuple[str, str]:
2575
+ """Apply routing logic to determine next suggested command.
2576
+
2577
+ Returns (command_name, reason) tuple. Flags with parse errors are skipped
2578
+ in the routing waterfall (never treated as Likely or Unlikely).
2579
+ """
2580
+ if prework["discuss"].get("status") != "parse_error":
2581
+ if prework["discuss"]["recommended"] == "Likely" and not has_context:
2582
+ return "discuss-phase", prework["discuss"]["reason"] or "clarify vision"
2583
+ if prework["design"].get("status") != "parse_error":
2584
+ if prework["design"]["recommended"] == "Likely" and not has_design:
2585
+ return "design-phase", prework["design"]["reason"] or "create UI/UX specs"
2586
+ if prework["research"].get("status") != "parse_error":
2587
+ if prework["research"]["recommended"] == "Likely" and not has_research:
2588
+ return "research-phase", prework["research"]["reason"] or "investigate approach"
2589
+
2590
+ # Check if any flags had parse errors with no other Likely flag triggering
2591
+ has_parse_error = any(
2592
+ prework[f].get("status") == "parse_error" for f in ("discuss", "design", "research")
2593
+ )
2594
+ if has_parse_error:
2595
+ return "plan-phase", "ready to plan (some flags unreadable — verify ROADMAP.md)"
2596
+ return "plan-phase", "ready to plan"
2597
+
2598
+
2599
+ def cmd_prework_status(args: argparse.Namespace) -> None:
2600
+ """Show pre-work status and routing suggestion for a phase.
2601
+
2602
+ Contract:
2603
+ Args: phase (str) — phase number (e.g., 5, 05, 2.1)
2604
+ Output: human-readable text — phase info, pre-work status, suggestion
2605
+ Exit codes: 0 = success, 1 = ROADMAP.md missing or phase not found
2606
+ Side effects: read-only
2607
+ """
2608
+ phase = normalize_phase(args.phase)
2609
+ planning = find_planning_dir()
2610
+
2611
+ # Parse ROADMAP.md
2612
+ roadmap = planning / "ROADMAP.md"
2613
+ if not roadmap.is_file():
2614
+ print("Error: No ROADMAP.md found", file=sys.stderr)
2615
+ sys.exit(1)
2616
+
2617
+ roadmap_text = roadmap.read_text(encoding="utf-8")
2618
+ phase_info = _parse_phase_section(roadmap_text, phase)
2619
+ if not phase_info:
2620
+ print(f"Error: Phase {phase} not found in ROADMAP.md", file=sys.stderr)
2621
+ sys.exit(1)
2622
+
2623
+ # Check artifacts
2624
+ phase_dir = find_phase_dir(planning, phase)
2625
+ has_context = False
2626
+ has_design = False
2627
+ has_research = False
2628
+ if phase_dir and phase_dir.is_dir():
2629
+ has_context = any(phase_dir.glob("*-CONTEXT.md"))
2630
+ has_design = any(phase_dir.glob("*-DESIGN.md"))
2631
+ has_research = any(phase_dir.glob("*-RESEARCH.md"))
2632
+
2633
+ # Routing
2634
+ suggested_cmd, reason = _determine_prework_suggestion(
2635
+ phase_info["prework"], has_context, has_design, has_research
2636
+ )
2637
+
2638
+ # Format output
2639
+ print(f"Phase {phase}: {phase_info['name']}")
2640
+ print(f"Goal: {phase_info['goal']}")
2641
+ print()
2642
+ print("Pre-work:")
2643
+
2644
+ artifact_done = {
2645
+ "discuss": has_context,
2646
+ "design": has_design,
2647
+ "research": has_research,
2648
+ }
2649
+ detail_labels = {"discuss": "Topics", "design": "Focus", "research": "Topics"}
2650
+
2651
+ has_any_parse_error = False
2652
+ for flag in ("discuss", "design", "research"):
2653
+ pw = phase_info["prework"][flag]
2654
+ if pw.get("status") == "parse_error":
2655
+ has_any_parse_error = True
2656
+ print(f" {flag.capitalize()}: [parse error] — read ROADMAP.md phase section for this flag")
2657
+ elif pw["recommended"] == "Likely":
2658
+ done = artifact_done[flag]
2659
+ status = "done" if done else "not started"
2660
+ line = f" {flag.capitalize()}: Likely ({status})"
2661
+ if pw["reason"]:
2662
+ line += f" — {pw['reason']}"
2663
+ print(line)
2664
+ if pw["detail"]:
2665
+ print(f" {detail_labels[flag]}: {pw['detail']}")
2666
+ else:
2667
+ print(f" {flag.capitalize()}: Unlikely")
2668
+
2669
+ # Existing artifacts
2670
+ existing = []
2671
+ if has_context:
2672
+ existing.append("CONTEXT.md")
2673
+ if has_design:
2674
+ existing.append("DESIGN.md")
2675
+ if has_research:
2676
+ existing.append("RESEARCH.md")
2677
+
2678
+ print()
2679
+ if existing:
2680
+ print(f"Existing: {', '.join(existing)}")
2681
+ else:
2682
+ print("Existing: (none)")
2683
+
2684
+ print(f"Suggested: /ms:{suggested_cmd} {phase} — {reason}")
2685
+
2686
+ if has_any_parse_error:
2687
+ print(f"\nNote: Some pre-work flags could not be parsed. Read ROADMAP.md phase section for Phase {phase} to determine accurate routing.")
2688
+
2689
+
2220
2690
  # ===================================================================
2221
2691
  # Subcommand: check-artifact
2222
2692
  # ===================================================================
@@ -3812,6 +4282,12 @@ def build_parser() -> argparse.ArgumentParser:
3812
4282
  p = subparsers.add_parser("create-phase-dirs", help="Create phase directories from ROADMAP.md")
3813
4283
  p.set_defaults(func=cmd_create_phase_dirs)
3814
4284
 
4285
+ # --- phase-renumber ---
4286
+ p = subparsers.add_parser("phase-renumber", help="Renumber phase dirs and files after phase removal")
4287
+ p.add_argument("phase", help="The removed phase number (e.g., 17 or 17.1)")
4288
+ p.add_argument("--dry-run", action="store_true", help="Preview renames without executing")
4289
+ p.set_defaults(func=cmd_phase_renumber)
4290
+
3815
4291
  # --- gather-milestone-stats ---
3816
4292
  p = subparsers.add_parser("gather-milestone-stats", help="Gather milestone readiness and git statistics")
3817
4293
  p.add_argument("start_phase", type=int, help="Start phase number")
@@ -3877,6 +4353,11 @@ def build_parser() -> argparse.ArgumentParser:
3877
4353
  p.add_argument("phase", help="Phase number")
3878
4354
  p.set_defaults(func=cmd_list_artifacts)
3879
4355
 
4356
+ # --- prework-status ---
4357
+ p = subparsers.add_parser("prework-status", help="Show pre-work status and routing suggestion")
4358
+ p.add_argument("phase", help="Phase number (e.g., 5, 05, 2.1)")
4359
+ p.set_defaults(func=cmd_prework_status)
4360
+
3880
4361
  # --- check-artifact ---
3881
4362
  p = subparsers.add_parser("check-artifact", help="Check if specific artifact exists")
3882
4363
  p.add_argument("phase", help="Phase number")