mindsystem-cc 3.22.1 → 4.0.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 (43) hide show
  1. package/README.md +3 -4
  2. package/agents/ms-adhoc-planner.md +133 -0
  3. package/agents/ms-code-reviewer.md +186 -0
  4. package/agents/ms-compounder.md +144 -0
  5. package/agents/ms-roadmapper.md +4 -0
  6. package/commands/ms/add-todo.md +46 -131
  7. package/commands/ms/adhoc.md +42 -57
  8. package/commands/ms/audit-milestone.md +66 -89
  9. package/commands/ms/complete-milestone.md +6 -4
  10. package/commands/ms/compound.md +64 -0
  11. package/commands/ms/config.md +66 -49
  12. package/commands/ms/create-roadmap.md +8 -7
  13. package/commands/ms/doctor.md +29 -4
  14. package/commands/ms/help.md +52 -52
  15. package/commands/ms/new-milestone.md +4 -3
  16. package/commands/ms/progress.md +23 -3
  17. package/commands/ms/update.md +102 -0
  18. package/mindsystem/references/linear-cli.md +71 -0
  19. package/mindsystem/references/routing/audit-result-routing.md +9 -4
  20. package/mindsystem/references/todo-file.md +63 -0
  21. package/mindsystem/templates/adhoc-summary.md +4 -5
  22. package/mindsystem/templates/knowledge.md +1 -1
  23. package/mindsystem/templates/project.md +15 -4
  24. package/mindsystem/templates/state.md +3 -14
  25. package/mindsystem/templates/tech-debt.md +2 -2
  26. package/mindsystem/workflows/adhoc.md +128 -316
  27. package/mindsystem/workflows/complete-milestone.md +20 -0
  28. package/mindsystem/workflows/compound.md +121 -0
  29. package/mindsystem/workflows/doctor-fixes.md +1 -1
  30. package/mindsystem/workflows/plan-phase.md +1 -1
  31. package/package.json +1 -1
  32. package/scripts/__pycache__/ms-tools.cpython-314.pyc +0 -0
  33. package/scripts/__pycache__/test_ms_tools.cpython-314-pytest-9.0.2.pyc +0 -0
  34. package/scripts/fixtures/scan-context/.planning/adhoc/20260225-refactor-api/adhoc-01-SUMMARY.md +39 -0
  35. package/scripts/fixtures/scan-context/.planning/todos/{pending/add-logout.md → add-logout.md} +2 -2
  36. package/scripts/fixtures/scan-context/.planning/todos/done/setup-db.md +2 -2
  37. package/scripts/fixtures/scan-context/expected-output.json +21 -7
  38. package/scripts/ms-tools.py +42 -23
  39. package/scripts/test_ms_tools.py +84 -5
  40. package/skills/senior-review/SKILL.md +0 -3
  41. package/commands/ms/check-todos.md +0 -240
  42. package/commands/ms/plan-milestone-gaps.md +0 -288
  43. package/skills/senior-review/AGENTS.md +0 -531
@@ -0,0 +1,121 @@
1
+ <purpose>
2
+ Compound code changes into per-subsystem knowledge files on demand. Handles work done outside the Mindsystem pipeline — direct Claude Code sessions, manual edits, merged branches.
3
+ </purpose>
4
+
5
+ <process>
6
+
7
+ <step name="parse_input" priority="first">
8
+ Parse `$ARGUMENTS` to determine input mode:
9
+
10
+ ```bash
11
+ # Validate active project
12
+ if [ ! -f .planning/config.json ]; then
13
+ echo "ERROR: No active Mindsystem project found (.planning/config.json missing)"
14
+ echo ""
15
+ echo "Options:"
16
+ echo "- Initialize project: /ms:new-project"
17
+ exit 1
18
+ fi
19
+ ```
20
+
21
+ **Mode detection:**
22
+ - If `$ARGUMENTS` empty: **description mode** — deduce from conversation context. Summarize what was discussed/changed in the current session.
23
+ - If matches git SHA pattern (7-40 hex chars), contains `..`, or starts with `HEAD`: **git mode**
24
+ - If matches existing file path (`test -e "$ARGUMENTS"`): **file mode**
25
+ - Otherwise: **description mode** — treat `$ARGUMENTS` as free-text description
26
+ </step>
27
+
28
+ <step name="resolve_change_context">
29
+ Gather lightweight change context based on input mode. Keep main context lean — only stats and summaries.
30
+
31
+ **Git mode:**
32
+ ```bash
33
+ # Stats only — full diff read by compounder
34
+ git show --stat <ref> # single commit
35
+ git diff --stat <range> # range
36
+ ```
37
+ Capture the ref/range string for passing to compounder.
38
+
39
+ **File mode:**
40
+ ```bash
41
+ git log --oneline -5 -- <path>
42
+ ```
43
+ Capture file path for passing to compounder.
44
+
45
+ **Description mode (including no-args):**
46
+ Spawn 1 Explore agent to find relevant code changes. If changes span multiple unrelated areas, spawn a second agent for the additional area. They return:
47
+ - Which files changed or are relevant
48
+ - Which subsystems are likely affected
49
+ - Concise summary of changes
50
+
51
+ Thoroughness: "medium".
52
+ </step>
53
+
54
+ <step name="determine_subsystems">
55
+ Read config.json subsystems and match changes:
56
+
57
+ ```bash
58
+ jq -r '.subsystems[]' .planning/config.json 2>/dev/null
59
+ ```
60
+
61
+ **Git/file mode:** Match file paths from diff stats against subsystem names via keyword matching.
62
+
63
+ **Description mode:** Use Explore agent findings for subsystem matching.
64
+
65
+ **Detect potential new subsystems:** Changes in file areas that don't match any existing subsystem.
66
+
67
+ **First-run handling:** If no subsystems in config.json and no knowledge files exist, propose a subsystem name derived from the project domain.
68
+ </step>
69
+
70
+ <step name="confirm_with_user">
71
+ Present findings and confirm before spawning compounder:
72
+
73
+ - Affected subsystems list
74
+ - Proposed new subsystems (if any)
75
+ - Change summary (1-3 lines)
76
+
77
+ AskUserQuestion: "Compound knowledge for these subsystems?" with options:
78
+ - Confirm
79
+ - Adjust (let me modify the list)
80
+ - Cancel
81
+ </step>
82
+
83
+ <step name="spawn_compounder">
84
+ Spawn ms-compounder via Task tool with:
85
+ - Input mode (`git`, `file`, or `description`)
86
+ - Change reference (git ref/range, file path, or description + exploration findings)
87
+ - Confirmed affected subsystems list
88
+ - Config.json subsystem vocabulary
89
+
90
+ Agent reads changes, reads affected knowledge files, writes updates, returns report.
91
+ </step>
92
+
93
+ <step name="finalize">
94
+ **Update config.json** (if new subsystems were confirmed in step 4):
95
+ ```bash
96
+ # Add new subsystem to config.json
97
+ jq '.subsystems += ["new-subsystem"]' .planning/config.json > tmp.$$.json && mv tmp.$$.json .planning/config.json
98
+ ```
99
+
100
+ **Commit changes:**
101
+ ```bash
102
+ git add .planning/knowledge/*.md
103
+ # Only add config.json if modified
104
+ git add .planning/config.json 2>/dev/null
105
+ git commit -m "$(cat <<'EOF'
106
+ docs: compound knowledge from <description>
107
+ EOF
108
+ )"
109
+ ```
110
+
111
+ **Set last command:**
112
+ ```bash
113
+ ms-tools set-last-command "ms:compound $ARGUMENTS"
114
+ ```
115
+
116
+ **Report:** Subsystems updated, entries added/changed/removed, new subsystems created (if any).
117
+ </step>
118
+
119
+ All three finalize actions (commit, set-last-command, report) must execute — do not stop after the compounder returns.
120
+
121
+ </process>
@@ -56,7 +56,7 @@ git add .planning/phases/*/*-SUMMARY.md 2>/dev/null
56
56
  git add .planning/adhoc/*-SUMMARY.md 2>/dev/null
57
57
  git add .planning/debug/*.md 2>/dev/null
58
58
  git add .planning/debug/resolved/*.md 2>/dev/null
59
- git add .planning/todos/pending/*.md 2>/dev/null
59
+ git add .planning/todos/*.md 2>/dev/null
60
60
  git add .planning/todos/done/*.md 2>/dev/null
61
61
  ```
62
62
 
@@ -51,7 +51,7 @@ PLAN.md IS the prompt that Claude executes. Plans are grouped into execution wav
51
51
  Read `.planning/STATE.md` and parse:
52
52
  - Current position (which phase we're planning)
53
53
  - Accumulated decisions (constraints on this phase)
54
- - Pending todos (candidates for inclusion)
54
+ - Pending todos from `.planning/todos/` (candidates for inclusion)
55
55
  - Blockers/concerns (things this phase may address)
56
56
  - Brief alignment status
57
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindsystem-cc",
3
- "version": "3.22.1",
3
+ "version": "4.0.0",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.",
5
5
  "bin": {
6
6
  "mindsystem-cc": "bin/install.js"
@@ -0,0 +1,39 @@
1
+ ---
2
+ phase: adhoc
3
+ plan: "01"
4
+ subsystem: api
5
+ tags: [rest, endpoints, refactor]
6
+
7
+ key-decisions:
8
+ - "Consolidated duplicate route handlers into shared utility"
9
+ - "Adopted consistent error response format across endpoints"
10
+
11
+ patterns-established:
12
+ - "Shared route handler pattern for CRUD operations"
13
+
14
+ key-files:
15
+ created:
16
+ - src/lib/route-utils.ts
17
+ modified:
18
+ - src/api/users/route.ts
19
+ - src/api/products/route.ts
20
+
21
+ duration: 15min
22
+ completed: 2026-02-25
23
+ ---
24
+
25
+ # Adhoc Plan 01: Refactor API Route Handlers Summary
26
+
27
+ **Consolidated duplicate CRUD logic into shared route utilities**
28
+
29
+ ## Performance
30
+ - **Duration:** 15min
31
+ - **Tasks:** 2
32
+ - **Files modified:** 3
33
+
34
+ ## Accomplishments
35
+ - Extracted shared CRUD handler from duplicate code in user and product routes
36
+ - Standardized error response format
37
+
38
+ ## Decisions Made
39
+ - Used generic handler factory pattern over middleware approach for type safety
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  title: Add logout endpoint
3
3
  subsystem: auth
4
- priority: medium
5
- phase_origin: "05-auth"
4
+ priority: 3
5
+ estimate: S
6
6
  ---
7
7
 
8
8
  # Add Logout Endpoint
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  title: Set up database migrations
3
3
  subsystem: database
4
- priority: high
5
- phase_origin: "02-infra"
4
+ priority: 2
5
+ estimate: M
6
6
  ---
7
7
 
8
8
  # Set Up Database Migrations
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "adhoc_summaries": {
26
26
  "dir": "<FIXTURE>/.planning/adhoc",
27
- "scanned": 1,
27
+ "scanned": 2,
28
28
  "skipped": null
29
29
  },
30
30
  "completed_todos": {
@@ -33,7 +33,7 @@
33
33
  "skipped": null
34
34
  },
35
35
  "pending_todos": {
36
- "dir": "<FIXTURE>/.planning/todos/pending",
36
+ "dir": "<FIXTURE>/.planning/todos",
37
37
  "scanned": 1,
38
38
  "skipped": null
39
39
  },
@@ -199,6 +199,20 @@
199
199
  "jwt",
200
200
  "tokens"
201
201
  ]
202
+ },
203
+ {
204
+ "path": "<FIXTURE>/.planning/adhoc/20260225-refactor-api/adhoc-01-SUMMARY.md",
205
+ "subsystem": "api",
206
+ "learnings": [
207
+ "Consolidated duplicate route handlers into shared utility",
208
+ "Adopted consistent error response format across endpoints"
209
+ ],
210
+ "related_phase": "",
211
+ "tags": [
212
+ "rest",
213
+ "endpoints",
214
+ "refactor"
215
+ ]
202
216
  }
203
217
  ],
204
218
  "completed_todos": [
@@ -206,17 +220,17 @@
206
220
  "path": "<FIXTURE>/.planning/todos/done/setup-db.md",
207
221
  "title": "Set up database migrations",
208
222
  "subsystem": "database",
209
- "priority": "high",
210
- "phase_origin": "02-infra"
223
+ "priority": 2,
224
+ "estimate": "M"
211
225
  }
212
226
  ],
213
227
  "pending_todos": [
214
228
  {
215
- "path": "<FIXTURE>/.planning/todos/pending/add-logout.md",
229
+ "path": "<FIXTURE>/.planning/todos/add-logout.md",
216
230
  "title": "Add logout endpoint",
217
231
  "subsystem": "auth",
218
- "priority": "medium",
219
- "phase_origin": "05-auth"
232
+ "priority": 3,
233
+ "estimate": "S"
220
234
  }
221
235
  ],
222
236
  "knowledge_files": [
@@ -798,16 +798,23 @@ def cmd_gather_milestone_stats(args: argparse.Namespace) -> None:
798
798
  phase_plans = 0
799
799
  phase_complete = 0
800
800
 
801
- for plan in sorted(d.glob("*-PLAN.md")):
801
+ # Discover plans from both PLAN.md and SUMMARY.md files
802
+ # (PLAN.md may be cleaned up after execution)
803
+ plan_bases: set[str] = set()
804
+ for plan in d.glob("*-PLAN.md"):
805
+ plan_bases.add(plan.name.replace("-PLAN.md", ""))
806
+ for summary in d.glob("*-SUMMARY.md"):
807
+ plan_bases.add(summary.name.replace("-SUMMARY.md", ""))
808
+
809
+ for plan_base in sorted(plan_bases):
802
810
  plan_count += 1
803
811
  phase_plans += 1
804
- plan_base = plan.name.replace("-PLAN.md", "")
805
812
  summary = d / f"{plan_base}-SUMMARY.md"
806
813
  if summary.is_file():
807
814
  complete += 1
808
815
  phase_complete += 1
809
816
  else:
810
- incomplete_list.append(f" {dirname}/{plan.name}")
817
+ incomplete_list.append(f" {dirname}/{plan_base}-PLAN.md")
811
818
 
812
819
  phase_details.append(f"- Phase {phase_num}: {phase_name} ({phase_complete}/{phase_plans} plans)")
813
820
 
@@ -1021,30 +1028,33 @@ def cmd_generate_phase_patch(args: argparse.Namespace) -> None:
1021
1028
 
1022
1029
 
1023
1030
  def cmd_generate_adhoc_patch(args: argparse.Namespace) -> None:
1024
- """Generate a patch file from an adhoc commit.
1031
+ """Generate a patch file from an adhoc commit or commit range.
1025
1032
 
1026
1033
  Contract:
1027
- Args: commit (str) — commit hash, output (str) — output file path
1034
+ Args: commit (str) — start commit hash, output (str) — output file path,
1035
+ end (str, optional) — end commit hash for range diffs
1028
1036
  Output: text — patch generation status and file path
1029
1037
  Exit codes: 0 = success (or no changes), 1 = commit not found
1030
1038
  Side effects: writes .patch file to output path
1031
1039
  """
1032
1040
  commit_hash = args.commit
1041
+ end_commit = getattr(args, "end", None) or commit_hash
1033
1042
  output_path = args.output
1034
1043
 
1035
1044
  git_root = find_git_root()
1036
1045
  import os
1037
1046
  os.chdir(git_root)
1038
1047
 
1039
- # Verify commit exists
1040
- try:
1041
- run_git("rev-parse", commit_hash)
1042
- except subprocess.CalledProcessError:
1043
- print(f"Error: Commit {commit_hash} not found", file=sys.stderr)
1044
- sys.exit(1)
1048
+ # Verify commits exist
1049
+ for ref in {commit_hash, end_commit}:
1050
+ try:
1051
+ run_git("rev-parse", ref)
1052
+ except subprocess.CalledProcessError:
1053
+ print(f"Error: Commit {ref} not found", file=sys.stderr)
1054
+ sys.exit(1)
1045
1055
 
1046
1056
  exclude_args = build_exclude_pathspecs()
1047
- diff_args = ["diff", f"{commit_hash}^", commit_hash, "--", "."] + exclude_args
1057
+ diff_args = ["diff", f"{commit_hash}^", end_commit, "--", "."] + exclude_args
1048
1058
 
1049
1059
  result = subprocess.run(
1050
1060
  ["git"] + diff_args,
@@ -1230,7 +1240,7 @@ def _scan_artifact_subsystem_values(planning: Path) -> list[str]:
1230
1240
  ("adhoc", "*-SUMMARY.md"),
1231
1241
  ("debug", "*.md"),
1232
1242
  ("debug/resolved", "*.md"),
1233
- ("todos/pending", "*.md"),
1243
+ ("todos", "*.md"),
1234
1244
  ("todos/done", "*.md"),
1235
1245
  ]
1236
1246
  for subdir, pattern in scan_globs:
@@ -1352,7 +1362,7 @@ def cmd_scan_artifact_subsystems(args: argparse.Namespace) -> None:
1352
1362
  ("Adhoc SUMMARYs", "adhoc", "*-SUMMARY.md"),
1353
1363
  ("Debug docs", "debug", "*.md"),
1354
1364
  ("Debug resolved", "debug/resolved", "*.md"),
1355
- ("Pending Todos", "todos/pending", "*.md"),
1365
+ ("Pending Todos", "todos", "*.md"),
1356
1366
  ("Done Todos", "todos/done", "*.md"),
1357
1367
  ]
1358
1368
 
@@ -1769,7 +1779,7 @@ def _scan_adhoc_summaries(
1769
1779
  source_info["skipped"] = "directory not found"
1770
1780
  return [], source_info
1771
1781
 
1772
- summary_files = sorted(adhoc_dir.glob("*-SUMMARY.md"))
1782
+ summary_files = sorted(adhoc_dir.glob("**/*-SUMMARY.md"))
1773
1783
  if not summary_files:
1774
1784
  source_info["skipped"] = "no adhoc SUMMARY.md files found"
1775
1785
  return [], source_info
@@ -1785,6 +1795,12 @@ def _scan_adhoc_summaries(
1785
1795
  learnings = fm.get("learnings", []) or []
1786
1796
  if isinstance(learnings, str):
1787
1797
  learnings = [learnings]
1798
+ # Fallback: extract from key-decisions if learnings absent (phase-style SUMMARY)
1799
+ if not learnings:
1800
+ key_decisions = fm.get("key-decisions", []) or []
1801
+ if isinstance(key_decisions, str):
1802
+ key_decisions = [key_decisions]
1803
+ learnings = key_decisions
1788
1804
 
1789
1805
  results.append({
1790
1806
  "path": str(path),
@@ -1802,8 +1818,8 @@ def _scan_todos(
1802
1818
  subdir: str,
1803
1819
  parse_errors: list[dict[str, str]],
1804
1820
  ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
1805
- """Scan todo files (done/ or pending/) for metadata."""
1806
- todo_dir = planning / "todos" / subdir
1821
+ """Scan todo files (done/ or root todos/) for metadata."""
1822
+ todo_dir = planning / "todos" / subdir if subdir else planning / "todos"
1807
1823
  source_info: dict[str, Any] = {"dir": str(todo_dir), "scanned": 0, "skipped": None}
1808
1824
 
1809
1825
  if not todo_dir.is_dir():
@@ -1812,7 +1828,8 @@ def _scan_todos(
1812
1828
 
1813
1829
  md_files = sorted(todo_dir.glob("*.md"))
1814
1830
  if not md_files:
1815
- source_info["skipped"] = f"no .md files in {subdir}/"
1831
+ label = f"{subdir}/" if subdir else "todos/"
1832
+ source_info["skipped"] = f"no .md files in {label}"
1816
1833
  return [], source_info
1817
1834
 
1818
1835
  results: list[dict[str, Any]] = []
@@ -1828,7 +1845,7 @@ def _scan_todos(
1828
1845
  "title": fm.get("title", path.stem),
1829
1846
  "subsystem": fm.get("subsystem", ""),
1830
1847
  "priority": fm.get("priority", ""),
1831
- "phase_origin": fm.get("phase_origin", ""),
1848
+ "estimate": fm.get("estimate", ""),
1832
1849
  })
1833
1850
 
1834
1851
  return results, source_info
@@ -2003,9 +2020,10 @@ def _format_markdown(output: dict[str, Any]) -> str:
2003
2020
  for t in todos:
2004
2021
  title = t.get("title", "untitled")
2005
2022
  priority = t.get("priority", "")
2023
+ estimate = t.get("estimate", "")
2006
2024
  sub = t.get("subsystem", "")
2007
2025
  path = t.get("path", "")
2008
- lines.append(f"- **{title}** [{priority}] ({sub}) — `{path}`")
2026
+ lines.append(f"- **{title}** [P{priority}|{estimate}] ({sub}) — `{path}`")
2009
2027
  sections.append("\n".join(lines))
2010
2028
 
2011
2029
  sources = output.get("sources", {})
@@ -2080,7 +2098,7 @@ def cmd_scan_planning_context(args: argparse.Namespace) -> None:
2080
2098
  debug_learnings, debug_src = _scan_debug_docs(planning, parse_errors)
2081
2099
  adhoc_learnings, adhoc_src = _scan_adhoc_summaries(planning, parse_errors)
2082
2100
  completed_todos, completed_src = _scan_todos(planning, "done", parse_errors)
2083
- pending_todos, pending_src = _scan_todos(planning, "pending", parse_errors)
2101
+ pending_todos, pending_src = _scan_todos(planning, "", parse_errors)
2084
2102
  knowledge_files, knowledge_src = _scan_knowledge_files(planning, subsystems)
2085
2103
 
2086
2104
  aggregated = _aggregate_from_summaries(summaries)
@@ -2798,9 +2816,10 @@ def build_parser() -> argparse.ArgumentParser:
2798
2816
  p.set_defaults(func=cmd_generate_phase_patch)
2799
2817
 
2800
2818
  # --- generate-adhoc-patch ---
2801
- p = subparsers.add_parser("generate-adhoc-patch", help="Generate patch from an adhoc commit")
2802
- p.add_argument("commit", help="Commit hash")
2819
+ p = subparsers.add_parser("generate-adhoc-patch", help="Generate patch from an adhoc commit or range")
2820
+ p.add_argument("commit", help="Start commit hash")
2803
2821
  p.add_argument("output", help="Output path for the patch file")
2822
+ p.add_argument("--end", default=None, help="End commit hash for range diffs (default: same as commit)")
2804
2823
  p.set_defaults(func=cmd_generate_adhoc_patch)
2805
2824
 
2806
2825
  # --- archive-milestone-phases ---
@@ -41,6 +41,7 @@ _detect_versioned_milestone_dirs = _mod._detect_versioned_milestone_dirs
41
41
  _parse_milestone_name_mapping = _mod._parse_milestone_name_mapping
42
42
  _SafeEncoder = _mod._SafeEncoder
43
43
  cmd_set_last_command = _mod.cmd_set_last_command
44
+ cmd_gather_milestone_stats = _mod.cmd_gather_milestone_stats
44
45
 
45
46
  # ---------------------------------------------------------------------------
46
47
  # Fixtures
@@ -480,7 +481,7 @@ def _build_scan_output(planning: Path) -> dict:
480
481
  debug_learnings, debug_src = _scan_debug_docs(planning, parse_errors)
481
482
  adhoc_learnings, adhoc_src = _scan_adhoc_summaries(planning, parse_errors)
482
483
  completed_todos, completed_src = _scan_todos(planning, "done", parse_errors)
483
- pending_todos, pending_src = _scan_todos(planning, "pending", parse_errors)
484
+ pending_todos, pending_src = _scan_todos(planning, "", parse_errors)
484
485
  knowledge_files, knowledge_src = _scan_knowledge_files(planning, subsystems)
485
486
 
486
487
  aggregated = _aggregate_from_summaries(summaries)
@@ -594,10 +595,14 @@ class TestScanIntegrationTargeted:
594
595
 
595
596
  def test_adhoc_learnings_collected(self):
596
597
  output = _build_scan_output(FIXTURE_PLANNING)
597
- assert len(output["adhoc_learnings"]) == 1
598
- adhoc = output["adhoc_learnings"][0]
599
- assert adhoc["subsystem"] == "auth"
600
- assert len(adhoc["learnings"]) == 2
598
+ assert len(output["adhoc_learnings"]) == 2
599
+ # Flat file (old format with learnings field)
600
+ auth_adhoc = next(a for a in output["adhoc_learnings"] if a["subsystem"] == "auth")
601
+ assert len(auth_adhoc["learnings"]) == 2
602
+ # Subdirectory file (phase-style with key-decisions fallback)
603
+ api_adhoc = next(a for a in output["adhoc_learnings"] if a["subsystem"] == "api")
604
+ assert len(api_adhoc["learnings"]) == 2
605
+ assert "duplicate route handlers" in api_adhoc["learnings"][0].lower()
601
606
 
602
607
  def test_pending_todos_collected(self):
603
608
  output = _build_scan_output(FIXTURE_PLANNING)
@@ -932,6 +937,80 @@ class TestSetLastCommand:
932
937
  assert "Last Command: ms:verify-work 10 | 2026-02-24 14:30" in text
933
938
 
934
939
 
940
+ class TestCmdGatherMilestoneStats:
941
+ """Tests for gather-milestone-stats command."""
942
+
943
+ def _patch_git_root(self, tmp_path):
944
+ return mock.patch.object(_mod, "find_git_root", return_value=tmp_path)
945
+
946
+ def _patch_run_git(self):
947
+ return mock.patch.object(_mod, "run_git", return_value="")
948
+
949
+ def _make_phase(self, tmp_path, name, plans=None, summaries=None):
950
+ """Create a phase dir with optional PLAN.md and SUMMARY.md files."""
951
+ phase_dir = tmp_path / ".planning" / "phases" / name
952
+ phase_dir.mkdir(parents=True, exist_ok=True)
953
+ for p in (plans or []):
954
+ (phase_dir / p).write_text("# Plan")
955
+ for s in (summaries or []):
956
+ (phase_dir / s).write_text("# Summary")
957
+ return phase_dir
958
+
959
+ def test_both_plan_and_summary(self, tmp_path, capsys):
960
+ self._make_phase(tmp_path, "01-auth",
961
+ plans=["01-01-PLAN.md"], summaries=["01-01-SUMMARY.md"])
962
+ args = argparse.Namespace(start_phase=1, end_phase=1)
963
+ with self._patch_git_root(tmp_path), self._patch_run_git():
964
+ cmd_gather_milestone_stats(args)
965
+ out = capsys.readouterr().out
966
+ assert "Plans: 1 total, 1 complete" in out
967
+ assert "Status: READY" in out
968
+
969
+ def test_summary_only_no_plan(self, tmp_path, capsys):
970
+ """PLAN.md cleaned up after execution — SUMMARY.md alone counts."""
971
+ self._make_phase(tmp_path, "09-persistence",
972
+ summaries=["09-01-SUMMARY.md", "09-02-SUMMARY.md"])
973
+ args = argparse.Namespace(start_phase=9, end_phase=9)
974
+ with self._patch_git_root(tmp_path), self._patch_run_git():
975
+ cmd_gather_milestone_stats(args)
976
+ out = capsys.readouterr().out
977
+ assert "Plans: 2 total, 2 complete" in out
978
+ assert "Status: READY" in out
979
+
980
+ def test_plan_only_no_summary_is_incomplete(self, tmp_path, capsys):
981
+ self._make_phase(tmp_path, "03-setup",
982
+ plans=["03-01-PLAN.md"])
983
+ args = argparse.Namespace(start_phase=3, end_phase=3)
984
+ with self._patch_git_root(tmp_path), self._patch_run_git():
985
+ cmd_gather_milestone_stats(args)
986
+ out = capsys.readouterr().out
987
+ assert "Plans: 1 total, 0 complete" in out
988
+ assert "Status: NOT READY" in out
989
+
990
+ def test_multi_phase_mixed(self, tmp_path, capsys):
991
+ """Multiple phases: some with PLANs, some with only SUMMARYs."""
992
+ self._make_phase(tmp_path, "09-persistence",
993
+ summaries=["09-01-SUMMARY.md", "09-02-SUMMARY.md"])
994
+ self._make_phase(tmp_path, "10-transactions",
995
+ plans=["10-01-PLAN.md"],
996
+ summaries=["10-01-SUMMARY.md"])
997
+ args = argparse.Namespace(start_phase=9, end_phase=10)
998
+ with self._patch_git_root(tmp_path), self._patch_run_git():
999
+ cmd_gather_milestone_stats(args)
1000
+ out = capsys.readouterr().out
1001
+ assert "Plans: 3 total, 3 complete" in out
1002
+ assert "Status: READY" in out
1003
+
1004
+ def test_no_plans_or_summaries(self, tmp_path, capsys):
1005
+ self._make_phase(tmp_path, "01-auth")
1006
+ args = argparse.Namespace(start_phase=1, end_phase=1)
1007
+ with self._patch_git_root(tmp_path), self._patch_run_git():
1008
+ cmd_gather_milestone_stats(args)
1009
+ out = capsys.readouterr().out
1010
+ assert "Plans: 0 total, 0 complete" in out
1011
+ assert "Status: NOT READY" in out
1012
+
1013
+
935
1014
  # ===================================================================
936
1015
  # Part 4: UAT File Management Tests
937
1016
  # ===================================================================
@@ -211,6 +211,3 @@ verdict: "clean | minor_issues | needs_refactoring | structural_concerns"
211
211
  - User has opportunity to provide context before changes
212
212
  - YAML summary block included for orchestrator parsing
213
213
 
214
- ## Full Compiled Document
215
-
216
- For the complete guide with all principles expanded: `AGENTS.md`