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.
- package/README.md +17 -7
- package/agents/ms-compounder.md +19 -7
- package/agents/ms-consolidator.md +11 -1
- package/agents/ms-designer.md +25 -47
- package/agents/ms-executor.md +1 -1
- package/agents/ms-mockup-designer.md +7 -4
- package/agents/ms-plan-checker.md +32 -27
- package/agents/ms-plan-writer.md +12 -8
- package/commands/ms/adhoc.md +11 -1
- package/commands/ms/config.md +47 -9
- package/commands/ms/design-phase.md +83 -63
- package/commands/ms/discuss-phase.md +1 -0
- package/commands/ms/doctor.md +7 -3
- package/commands/ms/execute-phase.md +1 -5
- package/commands/ms/help.md +6 -5
- package/commands/ms/remove-phase.md +7 -25
- package/commands/ms/research-phase.md +13 -0
- package/commands/ms/review-design.md +1 -1
- package/commands/ms/verify-work.md +1 -3
- package/mindsystem/references/design-directions.md +2 -2
- package/mindsystem/references/knowledge-quality.md +89 -0
- package/mindsystem/references/plan-format.md +2 -13
- package/mindsystem/references/prework-status.md +6 -32
- package/mindsystem/references/routing/next-phase-routing.md +7 -41
- package/mindsystem/references/scope-estimation.md +8 -4
- package/mindsystem/templates/config.json +6 -0
- package/mindsystem/templates/design.md +1 -1
- package/mindsystem/templates/knowledge.md +18 -3
- package/mindsystem/workflows/adhoc.md +63 -0
- package/mindsystem/workflows/discuss-phase.md +12 -0
- package/mindsystem/workflows/doctor-fixes.md +71 -0
- package/mindsystem/workflows/execute-phase.md +19 -6
- package/mindsystem/workflows/execute-plan.md +1 -7
- package/mindsystem/workflows/mockup-generation.md +1 -1
- package/mindsystem/workflows/plan-phase.md +41 -77
- package/mindsystem/workflows/verify-work.md +8 -77
- package/package.json +1 -1
- package/scripts/ms-tools.py +481 -0
- package/agents/ms-verify-fixer.md +0 -125
- 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
|
-
|
|
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
|
-
-
|
|
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.
|
|
519
|
-
3.
|
|
449
|
+
2. Skip as assumption — Log and move on
|
|
450
|
+
3. Manual investigation — You'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
package/scripts/ms-tools.py
CHANGED
|
@@ -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")
|