mindsystem-cc 4.0.3 → 4.1.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 (32) hide show
  1. package/README.md +39 -22
  2. package/agents/ms-researcher.md +3 -3
  3. package/agents/ms-roadmapper.md +11 -6
  4. package/bin/install.js +29 -8
  5. package/commands/ms/audit-milestone.md +35 -42
  6. package/commands/ms/create-roadmap.md +5 -7
  7. package/commands/ms/doctor.md +3 -3
  8. package/commands/ms/help.md +14 -14
  9. package/commands/ms/new-milestone.md +3 -3
  10. package/commands/ms/research-milestone.md +339 -0
  11. package/mindsystem/references/questioning.md +1 -1
  12. package/mindsystem/references/routing/between-milestones-routing.md +1 -1
  13. package/mindsystem/templates/milestone-context.md +1 -1
  14. package/mindsystem/templates/milestone-research.md +89 -0
  15. package/mindsystem/templates/requirements.md +1 -1
  16. package/mindsystem/templates/roadmap.md +20 -0
  17. package/mindsystem/templates/tech-debt.md +4 -4
  18. package/mindsystem/workflows/complete-milestone.md +4 -4
  19. package/mindsystem/workflows/define-requirements.md +12 -14
  20. package/mindsystem/workflows/verify-work.md +14 -49
  21. package/package.json +1 -1
  22. package/scripts/ms-tools.py +290 -14
  23. package/agents/ms-integration-checker.md +0 -424
  24. package/agents/ms-research-synthesizer.md +0 -248
  25. package/commands/ms/research-project.md +0 -353
  26. package/mindsystem/templates/research-project/ARCHITECTURE.md +0 -204
  27. package/mindsystem/templates/research-project/FEATURES.md +0 -147
  28. package/mindsystem/templates/research-project/PITFALLS.md +0 -200
  29. package/mindsystem/templates/research-project/STACK.md +0 -120
  30. package/mindsystem/templates/research-project/SUMMARY.md +0 -170
  31. package/mindsystem/templates/research-project-output.md +0 -81
  32. package/mindsystem/workflows/research-project.md +0 -23
@@ -538,7 +538,7 @@ Archived to milestones/{slug}/:
538
538
  - phases/ (phase directories moved from .planning/phases/)
539
539
  - MILESTONE-AUDIT.md (if audit was run)
540
540
  - CONTEXT.md (if milestone context existed)
541
- - research/ (if research existed)
541
+ - MILESTONE-RESEARCH.md (if existed)
542
542
 
543
543
  Cleaned:
544
544
  - Raw phase artifacts deleted (CONTEXT, DESIGN, RESEARCH, SUMMARY, UAT, VERIFICATION, EXECUTION-ORDER)
@@ -548,7 +548,7 @@ Cleaned:
548
548
  Deleted (fresh for next milestone):
549
549
  - ROADMAP.md
550
550
  - REQUIREMENTS.md
551
- - .planning/research/ (archived to milestone)
551
+ - MILESTONE-RESEARCH.md (archived to milestone)
552
552
 
553
553
  Updated:
554
554
  - MILESTONES.md (new entry)
@@ -574,7 +574,7 @@ Shipped:
574
574
  Archived to milestones/{slug}/:
575
575
  - ROADMAP.md
576
576
  - REQUIREMENTS.md
577
- - research/ (if existed)
577
+ - MILESTONE-RESEARCH.md (if existed)
578
578
 
579
579
  Summary: .planning/MILESTONES.md
580
580
 
@@ -590,7 +590,7 @@ Summary: .planning/MILESTONES.md
590
590
 
591
591
  **Next milestone flow:**
592
592
  1. `/ms:new-milestone` — discover what to build, update PROJECT.md with goals
593
- 2. `/ms:research-project` — (optional) research ecosystem
593
+ 2. `/ms:research-milestone` — (optional) research ecosystem
594
594
  3. `/ms:create-roadmap` — define requirements and plan how to build it
595
595
 
596
596
  ---
@@ -2,7 +2,7 @@
2
2
  Define concrete, checkable requirements for v1.
3
3
 
4
4
  Two modes:
5
- 1. **With research** — Transform FEATURES.md into scoped requirements
5
+ 1. **With research** — Transform MILESTONE-RESEARCH.md Product Landscape into scoped requirements
6
6
  2. **Without research** — Gather requirements through questioning
7
7
  </purpose>
8
8
 
@@ -11,8 +11,7 @@ Two modes:
11
11
 
12
12
  1. ~/.claude/mindsystem/templates/requirements.md
13
13
  2. .planning/PROJECT.md
14
- 3. .planning/research/FEATURES.md (if exists)
15
- 4. .planning/research/SUMMARY.md (if exists)
14
+ 3. .planning/MILESTONE-RESEARCH.md (if exists)
16
15
  </required_reading>
17
16
 
18
17
  <process>
@@ -20,7 +19,7 @@ Two modes:
20
19
  <step name="detect_mode">
21
20
  Check for research:
22
21
  ```bash
23
- [ -f .planning/research/FEATURES.md ] && echo "HAS_RESEARCH" || echo "NO_RESEARCH"
22
+ [ -f .planning/MILESTONE-RESEARCH.md ] && echo "HAS_RESEARCH" || echo "NO_RESEARCH"
24
23
  ```
25
24
 
26
25
  **If HAS_RESEARCH:** Follow steps load_context → present_features → scope_categories
@@ -33,17 +32,16 @@ Read PROJECT.md and extract:
33
32
  - Stated constraints (budget, timeline, tech limitations)
34
33
  - Any explicit scope boundaries from project definition
35
34
 
36
- Read research/FEATURES.md and extract:
35
+ Read MILESTONE-RESEARCH.md and extract from Product Landscape:
37
36
  - Table stakes (users expect these)
38
37
  - Differentiators (competitive advantage)
39
38
  - Anti-features (commonly requested, often problematic)
40
- - Feature dependencies
41
- - MVP vs full product recommendations
42
39
 
43
- Read research/SUMMARY.md for:
44
- - Overall confidence level
45
- - Key architectural constraints
46
- - Suggested phase structure (informational only)
40
+ Extract from other sections:
41
+ - Technology constraints and decisions
42
+ - Architecture dependencies
43
+ - Feasibility constraints
44
+ - Key risks and pitfalls
47
45
  </step>
48
46
 
49
47
  <step name="load_project" mode="without_research">
@@ -105,7 +103,7 @@ Here are the features for [domain]:
105
103
  - OAuth (Google, GitHub)
106
104
  - 2FA
107
105
 
108
- **Research notes:** [any relevant notes from FEATURES.md]
106
+ **Research notes:** [any relevant notes from MILESTONE-RESEARCH.md]
109
107
 
110
108
  ---
111
109
 
@@ -114,8 +112,8 @@ Here are the features for [domain]:
114
112
  ```
115
113
 
116
114
  For each category, include:
117
- - Table stakes from FEATURES.md
118
- - Differentiators from FEATURES.md
115
+ - Table stakes from MILESTONE-RESEARCH.md
116
+ - Differentiators from MILESTONE-RESEARCH.md
119
117
  - Any anti-features flagged (with warnings)
120
118
  - Complexity notes where relevant
121
119
  </step>
@@ -200,11 +200,7 @@ Read current batch from UAT.md (test descriptions needed for presenting to user)
200
200
  **1. Handle mock generation (if needed):**
201
201
 
202
202
  If `mock_type` is not null AND different from previous batch:
203
- - Revert old mocks if any (from `mocked_files` in UAT.md frontmatter):
204
- ```bash
205
- git checkout -- <mocked_files>
206
- ```
207
- - Clear mocked_files: `ms-tools uat-update $PHASE_NUMBER --session mocked_files=`
203
+ - Revert old mocks: `ms-tools uat-revert-mocks $PHASE_NUMBER`
208
204
  - Go to `generate_mocks`
209
205
 
210
206
  If `mock_type` is null or same as previous:
@@ -382,53 +378,28 @@ Progress auto-recalculates on every `uat-update` call. No manual progress recalc
382
378
  **Apply fix inline:**
383
379
 
384
380
  **1. Stash mocks (if active):**
385
- ```bash
386
- git stash push -m "mocks-batch-{N}" -- <mocked_files>
387
- ```
388
- Use `mocked_files` list from UAT.md frontmatter.
381
+ `ms-tools uat-stash-mocks $PHASE_NUMBER`
389
382
 
390
383
  **2. Make the fix:**
391
384
  - Edit the file(s)
392
385
  - Test that fix compiles/runs
393
386
 
394
- **3. Commit (amend on retry when safe):**
387
+ **3. Commit and record fix:**
395
388
  ```bash
396
- git add [specific files]
397
-
398
- # Check if this is a retry AND HEAD matches the test's previous fix_commit:
399
- PREV_FIX=$(ms-tools uat-status $PHASE_NUMBER | python3 -c "import sys,json; d=json.load(sys.stdin); t=[x for x in d['fixing_tests'] if x['num']==N]; print(t[0].get('fix_commit','') if t else '')" 2>/dev/null)
400
- HEAD_SHORT=$(git rev-parse --short HEAD)
401
-
402
- if [ "$PREV_FIX" = "$HEAD_SHORT" ] && [ -n "$PREV_FIX" ]; then
403
- git commit --amend --no-edit
404
- else
405
- git commit -m "fix({phase}-uat): {description}"
406
- fi
389
+ ms-tools uat-fix-commit $PHASE_NUMBER --test N --message "fix({phase}-uat): {description}" <files>
407
390
  ```
391
+ Handles amend-on-retry automatically (amends if HEAD matches previous fix_commit for this test).
408
392
 
409
- Safety: only amend if HEAD matches recorded fix_commit. If HEAD has moved (other fixes in between), create new commit.
410
-
411
- **4. Record in UAT.md via ms-tools:**
393
+ **4. Record fix details in UAT.md:**
394
+ Use the `hash` from fix-commit JSON output:
412
395
  ```bash
413
- FIX_HASH=$(git rev-parse --short HEAD)
414
- ms-tools uat-update $PHASE_NUMBER --test N fix_status=applied fix_commit=$FIX_HASH
415
- echo '{"commit":"'$FIX_HASH'","test":N,"description":"what was fixed","files":["changed.ts"]}' | ms-tools uat-update $PHASE_NUMBER --append-fix
396
+ echo '{"commit":"HASH","test":N,"description":"what was fixed","files":["changed.ts"]}' | ms-tools uat-update $PHASE_NUMBER --append-fix
416
397
  ```
417
398
 
418
- `append-fix` updates in-place if a fix for the same test already exists (amend support).
419
-
420
399
  **5. Restore mocks:**
421
- ```bash
422
- git stash pop
423
- ```
400
+ `ms-tools uat-pop-mocks $PHASE_NUMBER`
424
401
 
425
- **Handle stash conflict:**
426
- ```bash
427
- # Conflict means fix touched a mocked file — take the fix version
428
- git checkout --theirs <conflicted-file>
429
- git add <conflicted-file>
430
- ```
431
- Remove conflicted file from `mocked_files` list in UAT.md (mock no longer needed for that file).
402
+ Conflicts are resolved automatically (fix version wins, conflicted files removed from mocked_files).
432
403
 
433
404
  **6. Request re-test:**
434
405
  ```
@@ -444,9 +415,7 @@ Go to `handle_retest`.
444
415
  **Spawn fixer subagent for complex issue:**
445
416
 
446
417
  **1. Stash mocks (if active):**
447
- ```bash
448
- git stash push -m "mocks-batch-{N}" -- <mocked_files>
449
- ```
418
+ `ms-tools uat-stash-mocks $PHASE_NUMBER`
450
419
 
451
420
  **2. Spawn ms-verify-fixer:**
452
421
  ```
@@ -489,12 +458,11 @@ Mocks are stashed — working tree is clean.
489
458
 
490
459
  **If FIX COMPLETE:**
491
460
  - Record fix via ms-tools (same as `apply_fix` step 4: `uat-update --test N` + `--append-fix`)
492
- - Restore mocks: `git stash pop`
493
- - Handle conflicts as in `apply_fix`
461
+ - Restore mocks: `ms-tools uat-pop-mocks $PHASE_NUMBER`
494
462
  - Request re-test
495
463
 
496
464
  **If INVESTIGATION INCONCLUSIVE:**
497
- - Restore mocks: `git stash pop`
465
+ - Restore mocks: `ms-tools uat-pop-mocks $PHASE_NUMBER`
498
466
  - Present options:
499
467
  ```
500
468
  Investigation didn't find root cause.
@@ -607,10 +575,7 @@ The `--session current_batch=N` call auto-syncs the Current Batch section with t
607
575
  **Complete UAT session:**
608
576
 
609
577
  **1. Revert mocks:**
610
- ```bash
611
- git checkout -- <mocked_files>
612
- ```
613
- Use `mocked_files` list from UAT.md frontmatter. Clear the list after reverting.
578
+ `ms-tools uat-revert-mocks $PHASE_NUMBER`
614
579
 
615
580
  **2. Generate UAT fixes patch (if fixes were made):**
616
581
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindsystem-cc",
3
- "version": "4.0.3",
3
+ "version": "4.1.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"
@@ -761,19 +761,67 @@ def cmd_doctor_scan(args: argparse.Namespace) -> None:
761
761
  record("PASS", "PLAN Cleanup")
762
762
  print()
763
763
 
764
- # ---- CHECK 7: CLI Wrappers ----
764
+ # ---- CHECK 7: CLI Wrappers & Environment ----
765
765
  print("=== CLI Wrappers ===")
766
766
  wrapper_names = ["ms-tools", "ms-lookup", "ms-compare-mockups"]
767
- missing_wrappers = [w for w in wrapper_names if shutil.which(w) is None]
768
- if missing_wrappers:
767
+
768
+ # 7a: Check bin directory exists
769
+ global_bin = Path.home() / ".claude" / "bin"
770
+ local_bin = Path(".claude") / "bin"
771
+ bin_dir = global_bin if global_bin.is_dir() else (local_bin if local_bin.is_dir() else None)
772
+
773
+ if bin_dir is None:
769
774
  print("Status: FAIL")
770
- print(f"Not on PATH: {', '.join(missing_wrappers)}")
771
- print("Fix: re-run `npx mindsystem-cc` to regenerate wrappers and PATH hook")
775
+ print("Bin directory not found (~/.claude/bin/ or .claude/bin/)")
776
+ print("Fix: re-run `npx mindsystem-cc` to generate wrappers")
772
777
  record("FAIL", "CLI Wrappers")
773
778
  else:
774
- print("Status: PASS")
775
- print(f"All {len(wrapper_names)} CLI wrappers found on PATH")
776
- record("PASS", "CLI Wrappers")
779
+ # 7b: Check wrapper files present
780
+ missing_files = [w for w in wrapper_names if not (bin_dir / w).exists()]
781
+ if missing_files:
782
+ print("Status: FAIL")
783
+ print(f"Wrapper files missing from {bin_dir}: {', '.join(missing_files)}")
784
+ print("Fix: re-run `npx mindsystem-cc` to regenerate wrappers")
785
+ record("FAIL", "CLI Wrappers")
786
+ else:
787
+ # 7c: Check bin dir in PATH
788
+ path_dirs = os.environ.get("PATH", "").split(os.pathsep)
789
+ bin_in_path = str(bin_dir.resolve()) in [os.path.realpath(p) for p in path_dirs]
790
+
791
+ # 7d: Check wrappers resolvable
792
+ missing_wrappers = [w for w in wrapper_names if shutil.which(w) is None]
793
+
794
+ if missing_wrappers:
795
+ print("Status: FAIL")
796
+ print(f"Not resolvable: {', '.join(missing_wrappers)}")
797
+ if not bin_in_path:
798
+ print(f"Cause: {bin_dir} not in PATH")
799
+ print("Fix: restart Claude Code session (PATH hook fires on SessionStart)")
800
+ else:
801
+ print("Fix: re-run `npx mindsystem-cc` to regenerate wrappers and PATH hook")
802
+ record("FAIL", "CLI Wrappers")
803
+ else:
804
+ print(f"All {len(wrapper_names)} CLI wrappers found on PATH")
805
+
806
+ # 7e: Check uv available
807
+ uv_ok = shutil.which("uv") is not None
808
+ # 7f: Check Python available
809
+ py_ok = shutil.which("python3") is not None or shutil.which("python") is not None
810
+
811
+ issues = []
812
+ if not uv_ok:
813
+ issues.append("uv not found — install: `curl -LsSf https://astral.sh/uv/install.sh | sh`")
814
+ if not py_ok:
815
+ issues.append("Python not found — install Python 3.10+")
816
+
817
+ if issues:
818
+ print("Status: WARN")
819
+ for issue in issues:
820
+ print(f" {issue}")
821
+ record("WARN", "CLI Wrappers")
822
+ else:
823
+ print("Status: PASS")
824
+ record("PASS", "CLI Wrappers")
777
825
  print()
778
826
 
779
827
  # ---- CHECK 8: Milestone Naming Convention ----
@@ -1306,15 +1354,15 @@ def cmd_archive_milestone_files(args: argparse.Namespace) -> None:
1306
1354
  print("Archived: MILESTONE-CONTEXT.md → CONTEXT.md")
1307
1355
  archived += 1
1308
1356
 
1309
- # Research directory
1310
- research = planning_dir / "research"
1311
- if research.is_dir():
1312
- shutil.move(str(research), str(milestone_dir / "research"))
1313
- print("Archived: research/ → research/")
1357
+ # MILESTONE-RESEARCH.md
1358
+ research_file = planning_dir / "MILESTONE-RESEARCH.md"
1359
+ if research_file.is_file():
1360
+ shutil.move(str(research_file), str(milestone_dir / "MILESTONE-RESEARCH.md"))
1361
+ print("Archived: MILESTONE-RESEARCH.md")
1314
1362
  archived += 1
1315
1363
 
1316
1364
  if archived == 0:
1317
- print("No optional files to archive (audit, context, research all absent)")
1365
+ print("No optional files to archive (audit, context, milestone research all absent)")
1318
1366
  else:
1319
1367
  print()
1320
1368
  print(f"Archived {archived} item(s) to milestones/{milestone}/")
@@ -2415,6 +2463,7 @@ class UATFile:
2415
2463
  "current_batch": 1,
2416
2464
  "mocked_files": [],
2417
2465
  "pre_work_stash": None,
2466
+ "stash_ref": None,
2418
2467
  }
2419
2468
 
2420
2469
  # Build tests
@@ -2685,6 +2734,22 @@ class UATFile:
2685
2734
  return "\n".join(lines)
2686
2735
 
2687
2736
 
2737
+ def _load_uat(args_phase: str) -> tuple[Path, "UATFile"]:
2738
+ """Load UAT file for a phase. Returns (uat_path, uat). Exits 1 if missing."""
2739
+ phase = normalize_phase(args_phase)
2740
+ planning = find_planning_dir()
2741
+ phase_dir = find_phase_dir(planning, phase)
2742
+ if phase_dir is None:
2743
+ print(f"Error: Phase directory not found for {phase}", file=sys.stderr)
2744
+ sys.exit(1)
2745
+ uat_path = phase_dir / f"{phase_dir.name}-UAT.md"
2746
+ if not uat_path.is_file():
2747
+ print(f"Error: UAT file not found: {uat_path}", file=sys.stderr)
2748
+ sys.exit(1)
2749
+ uat = UATFile.parse(uat_path.read_text(encoding="utf-8"))
2750
+ return uat_path, uat
2751
+
2752
+
2688
2753
  # ===================================================================
2689
2754
  # Subcommand: uat-init
2690
2755
  # ===================================================================
@@ -2857,6 +2922,7 @@ def cmd_uat_status(args: argparse.Namespace) -> None:
2857
2922
  "pending_tests": pending_tests,
2858
2923
  "blocked_tests": blocked_tests,
2859
2924
  "pre_work_stash": uat.frontmatter.get("pre_work_stash"),
2925
+ "stash_ref": uat.frontmatter.get("stash_ref"),
2860
2926
  "path": str(uat_path),
2861
2927
  }
2862
2928
 
@@ -2864,6 +2930,193 @@ def cmd_uat_status(args: argparse.Namespace) -> None:
2864
2930
  sys.stdout.write("\n")
2865
2931
 
2866
2932
 
2933
+ # ===================================================================
2934
+ # Subcommand: uat-stash-mocks
2935
+ # ===================================================================
2936
+
2937
+
2938
+ def cmd_uat_stash_mocks(args: argparse.Namespace) -> None:
2939
+ """Stash mocked files before applying a fix.
2940
+
2941
+ Contract:
2942
+ Args: phase (str) — phase number
2943
+ Output: JSON — stash_ref and files list
2944
+ Exit codes: 0 = success (or no-op), 1 = git failure
2945
+ Side effects: git stash push, updates UAT.md stash_ref
2946
+ """
2947
+ uat_path, uat = _load_uat(args.phase)
2948
+
2949
+ mocked_files = uat.frontmatter.get("mocked_files", [])
2950
+ if not mocked_files:
2951
+ print("No mocked files to stash", file=sys.stderr)
2952
+ return
2953
+
2954
+ current_batch = uat.frontmatter.get("current_batch", 1)
2955
+
2956
+ try:
2957
+ run_git("stash", "push", "-m", f"mocks-batch-{current_batch}", "--", *mocked_files)
2958
+ except subprocess.CalledProcessError as e:
2959
+ print(f"Error: git stash push failed: {e.stderr}", file=sys.stderr)
2960
+ sys.exit(1)
2961
+
2962
+ uat.update_session({"stash_ref": "stash@{0}"})
2963
+ uat_path.write_text(uat.serialize(), encoding="utf-8")
2964
+
2965
+ output = {"stash_ref": "stash@{0}", "files": mocked_files}
2966
+ json.dump(output, sys.stdout, cls=_SafeEncoder)
2967
+ sys.stdout.write("\n")
2968
+
2969
+
2970
+ # ===================================================================
2971
+ # Subcommand: uat-pop-mocks
2972
+ # ===================================================================
2973
+
2974
+
2975
+ def cmd_uat_pop_mocks(args: argparse.Namespace) -> None:
2976
+ """Restore stashed mocks after fix is committed.
2977
+
2978
+ Contract:
2979
+ Args: phase (str) — phase number
2980
+ Output: JSON — status and conflicts list
2981
+ Exit codes: 0 = success (or no-op), 1 = unexpected failure
2982
+ Side effects: git stash pop, updates UAT.md stash_ref/mocked_files
2983
+ """
2984
+ uat_path, uat = _load_uat(args.phase)
2985
+
2986
+ stash_ref = uat.frontmatter.get("stash_ref")
2987
+ if not stash_ref:
2988
+ print("No stash to pop", file=sys.stderr)
2989
+ return
2990
+
2991
+ try:
2992
+ run_git("stash", "pop", stash_ref)
2993
+ uat.update_session({"stash_ref": ""})
2994
+ uat_path.write_text(uat.serialize(), encoding="utf-8")
2995
+ output = {"status": "restored", "conflicts": []}
2996
+ json.dump(output, sys.stdout, cls=_SafeEncoder)
2997
+ sys.stdout.write("\n")
2998
+ except subprocess.CalledProcessError:
2999
+ # Check for merge conflicts
3000
+ try:
3001
+ conflict_output = run_git("diff", "--name-only", "--diff-filter=U")
3002
+ except subprocess.CalledProcessError:
3003
+ conflict_output = ""
3004
+
3005
+ conflicts = [f.strip() for f in conflict_output.splitlines() if f.strip()]
3006
+ if not conflicts:
3007
+ print("Error: stash pop failed with no merge conflicts — unexpected failure", file=sys.stderr)
3008
+ sys.exit(1)
3009
+
3010
+ # Resolve conflicts by taking the fix version (theirs)
3011
+ for f in conflicts:
3012
+ run_git("checkout", "--theirs", f)
3013
+ run_git("add", f)
3014
+
3015
+ # Drop the stash (failed pop doesn't auto-drop)
3016
+ try:
3017
+ run_git("stash", "drop", stash_ref)
3018
+ except subprocess.CalledProcessError:
3019
+ pass # Best effort
3020
+
3021
+ # Remove conflicted files from mocked_files
3022
+ mocked = uat.frontmatter.get("mocked_files", [])
3023
+ removed = [f for f in conflicts if f in mocked]
3024
+ uat.frontmatter["mocked_files"] = [f for f in mocked if f not in conflicts]
3025
+ uat.update_session({"stash_ref": ""})
3026
+ uat_path.write_text(uat.serialize(), encoding="utf-8")
3027
+
3028
+ output = {"status": "restored_with_conflicts", "conflicts": conflicts, "removed_from_mocks": removed}
3029
+ json.dump(output, sys.stdout, cls=_SafeEncoder)
3030
+ sys.stdout.write("\n")
3031
+
3032
+
3033
+ # ===================================================================
3034
+ # Subcommand: uat-fix-commit
3035
+ # ===================================================================
3036
+
3037
+
3038
+ def cmd_uat_fix_commit(args: argparse.Namespace) -> None:
3039
+ """Stage files, commit (or amend), and record fix in UAT.md.
3040
+
3041
+ Contract:
3042
+ Args: phase (str), --test (int), --message (str), files (list)
3043
+ Output: JSON — hash and amend flag
3044
+ Exit codes: 0 = success, 1 = no files or git failure
3045
+ Side effects: git add/commit, updates UAT.md fix_status/fix_commit
3046
+ """
3047
+ uat_path, uat = _load_uat(args.phase)
3048
+
3049
+ if not args.files:
3050
+ print("Error: No files to commit", file=sys.stderr)
3051
+ sys.exit(1)
3052
+
3053
+ test_num = args.test
3054
+
3055
+ # Find previous fix_commit for this test
3056
+ prev_fix_commit = ""
3057
+ for t in uat.tests:
3058
+ if t["num"] == str(test_num):
3059
+ prev_fix_commit = t.get("fix_commit", "")
3060
+ break
3061
+
3062
+ run_git("add", *args.files)
3063
+
3064
+ amend = False
3065
+ if prev_fix_commit:
3066
+ head_short = run_git("rev-parse", "--short", "HEAD")
3067
+ if head_short == prev_fix_commit:
3068
+ amend = True
3069
+
3070
+ if amend:
3071
+ run_git("commit", "--amend", "--no-edit")
3072
+ else:
3073
+ run_git("commit", "-m", args.message)
3074
+
3075
+ new_hash = run_git("rev-parse", "--short", "HEAD")
3076
+ uat.update_test(test_num, {"fix_status": "applied", "fix_commit": new_hash})
3077
+ uat_path.write_text(uat.serialize(), encoding="utf-8")
3078
+
3079
+ output = {"hash": new_hash, "amend": amend}
3080
+ json.dump(output, sys.stdout, cls=_SafeEncoder)
3081
+ sys.stdout.write("\n")
3082
+
3083
+
3084
+ # ===================================================================
3085
+ # Subcommand: uat-revert-mocks
3086
+ # ===================================================================
3087
+
3088
+
3089
+ def cmd_uat_revert_mocks(args: argparse.Namespace) -> None:
3090
+ """Revert mocked files and clear the mocked_files list.
3091
+
3092
+ Contract:
3093
+ Args: phase (str) — phase number
3094
+ Output: JSON — reverted files list
3095
+ Exit codes: 0 = success (or no-op), 1 = git failure
3096
+ Side effects: git checkout, clears mocked_files in UAT.md
3097
+ """
3098
+ uat_path, uat = _load_uat(args.phase)
3099
+
3100
+ mocked_files = uat.frontmatter.get("mocked_files", [])
3101
+ if not mocked_files:
3102
+ print("No mocked files to revert", file=sys.stderr)
3103
+ return
3104
+
3105
+ try:
3106
+ run_git("checkout", "--", *mocked_files)
3107
+ except subprocess.CalledProcessError as e:
3108
+ print(f"Error: git checkout failed: {e.stderr}", file=sys.stderr)
3109
+ sys.exit(1)
3110
+
3111
+ reverted = list(mocked_files)
3112
+ uat.frontmatter["mocked_files"] = []
3113
+ uat_path.write_text(uat.serialize(), encoding="utf-8")
3114
+
3115
+ output = {"reverted": reverted}
3116
+ json.dump(output, sys.stdout, cls=_SafeEncoder)
3117
+ sys.stdout.write("\n")
3118
+
3119
+
2867
3120
  # ===================================================================
2868
3121
  # config-get / config-set / config-delete
2869
3122
  # ===================================================================
@@ -3072,6 +3325,29 @@ def build_parser() -> argparse.ArgumentParser:
3072
3325
  p.add_argument("phase", help="Phase number")
3073
3326
  p.set_defaults(func=cmd_uat_status)
3074
3327
 
3328
+ # --- uat-stash-mocks ---
3329
+ p = subparsers.add_parser("uat-stash-mocks", help="Stash mocked files before fix")
3330
+ p.add_argument("phase", help="Phase number")
3331
+ p.set_defaults(func=cmd_uat_stash_mocks)
3332
+
3333
+ # --- uat-pop-mocks ---
3334
+ p = subparsers.add_parser("uat-pop-mocks", help="Restore stashed mocks after fix")
3335
+ p.add_argument("phase", help="Phase number")
3336
+ p.set_defaults(func=cmd_uat_pop_mocks)
3337
+
3338
+ # --- uat-fix-commit ---
3339
+ p = subparsers.add_parser("uat-fix-commit", help="Commit fix and record in UAT.md")
3340
+ p.add_argument("phase", help="Phase number")
3341
+ p.add_argument("--test", type=int, required=True, help="Test number")
3342
+ p.add_argument("--message", required=True, help="Commit message")
3343
+ p.add_argument("files", nargs="*", help="Files to stage and commit")
3344
+ p.set_defaults(func=cmd_uat_fix_commit)
3345
+
3346
+ # --- uat-revert-mocks ---
3347
+ p = subparsers.add_parser("uat-revert-mocks", help="Revert mocked files to clean state")
3348
+ p.add_argument("phase", help="Phase number")
3349
+ p.set_defaults(func=cmd_uat_revert_mocks)
3350
+
3075
3351
  # --- config-get ---
3076
3352
  p = subparsers.add_parser("config-get", help="Read a value from config.json by dot-path")
3077
3353
  p.add_argument("key", help="Dot-notation key (e.g. subsystems, code_review.phase, subsystems.0)")