chore(M003/S06): Doctor + cleanup + code simplification
Tasks: - chore(M003/S06): auto-commit after complete-slice - chore(M003/S06/T02): auto-commit after execute-task - chore(M003/S06/T01): auto-commit after execute-task - chore(M003/S06): auto-commit after plan-slice - docs(S06): add slice plan Branch: gsd/M003/S06
This commit is contained in:
parent
34cd1056ea
commit
f9b9f6bf32
14 changed files with 1052 additions and 8 deletions
|
|
@ -127,13 +127,13 @@ This file is the explicit capability and coverage contract for the project.
|
|||
|
||||
### R040 — Doctor git health checks
|
||||
- Class: operability
|
||||
- Status: active
|
||||
- Status: validated
|
||||
- Description: `/gsd doctor` detects and optionally fixes git-related issues: orphaned auto-worktrees, stale milestone branches, corrupt merge state (MERGE_HEAD/SQUASH_MSG), tracked runtime files, missing gitignore patterns.
|
||||
- Why it matters: When things do go wrong, users need a one-command fix. Doctor is the safety net.
|
||||
- Source: inferred
|
||||
- Primary owning slice: M003/S06
|
||||
- Supporting slices: M003/S05
|
||||
- Validation: unmapped
|
||||
- Validation: 4 DoctorIssueCode values with detection and fix logic in checkGitHealth. 6 integration tests (17 assertions) in doctor-git.test.ts covering detect/fix/verify cycle for all codes plus safety guards.
|
||||
- Notes: Doctor already handles planning artifact issues. This extends it to git health.
|
||||
|
||||
### R041 — Test coverage for worktree-isolated flow
|
||||
|
|
@ -537,7 +537,7 @@ This file is the explicit capability and coverage contract for the project.
|
|||
| R037 | primary-user-loop | active | M003/S05 | all M003 | unmapped |
|
||||
| R038 | continuity | active | M003/S04 | none | unmapped |
|
||||
| R039 | integration | active | M003/S01 | none | unmapped |
|
||||
| R040 | operability | active | M003/S06 | M003/S05 | unmapped |
|
||||
| R040 | operability | validated | M003/S06 | M003/S05 | 4 DoctorIssueCode values, 6 integration tests (17 assertions) in doctor-git.test.ts |
|
||||
| R041 | quality-attribute | active | M003/S07 | all M003 | unmapped |
|
||||
| R042 | core-capability | deferred | none | none | unmapped |
|
||||
| R043 | quality-attribute | deferred | none | none | unmapped |
|
||||
|
|
@ -545,9 +545,9 @@ This file is the explicit capability and coverage contract for the project.
|
|||
|
||||
## Coverage Summary
|
||||
|
||||
- Active requirements: 11
|
||||
- Mapped to slices: 11
|
||||
- Validated: 24
|
||||
- Active requirements: 10
|
||||
- Mapped to slices: 10
|
||||
- Validated: 25
|
||||
- Deferred: 5
|
||||
- Out of scope: 4
|
||||
- Unmapped active requirements: 0
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ This milestone is complete only when all are true:
|
|||
- [x] **S05: Self-healing git repair** `risk:medium` `depends:[S01,S02,S03]`
|
||||
> After this: when a merge fails or checkout breaks during auto-mode, the system aborts the failed operation, resets working tree state, and retries. Only truly unresolvable conflicts (real code conflicts between human-edited files) pause auto-mode. Users see non-technical messages, not raw git errors. Verified by deliberately introducing failures and confirming auto-recovery.
|
||||
|
||||
- [ ] **S06: Doctor + cleanup + code simplification** `risk:low` `depends:[S01,S02,S03,S05]`
|
||||
- [x] **S06: Doctor + cleanup + code simplification** `risk:low` `depends:[S01,S02,S03,S05]`
|
||||
> After this: `/gsd doctor` detects orphaned auto-worktrees, stale milestone branches, corrupt merge state (MERGE_HEAD/SQUASH_MSG), and tracked runtime files — and fixes them. Dead `.gsd/` conflict resolution code removed from worktree-mode paths in git-service.ts. Verified via doctor test cases.
|
||||
|
||||
- [ ] **S07: Test suite for worktree-isolated flow** `risk:low` `depends:[S01,S02,S03,S04,S05,S06]`
|
||||
|
|
|
|||
23
.gsd/milestones/M003/slices/S05/S05-ASSESSMENT.md
Normal file
23
.gsd/milestones/M003/slices/S05/S05-ASSESSMENT.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# S05 Assessment — Roadmap Reassessment
|
||||
|
||||
**Verdict: Roadmap unchanged.**
|
||||
|
||||
S05 delivered exactly what was planned — self-heal module with abort/reset/retry for transient failures, immediate escalation for real conflicts, user-friendly error messages pointing to `/gsd doctor`. No new risks surfaced. No assumptions changed that affect remaining slices.
|
||||
|
||||
## Success Criteria Coverage
|
||||
|
||||
All six success criteria have remaining owning slices (S06 for doctor, S07 for full test coverage and end-to-end verification). No gaps.
|
||||
|
||||
## Requirement Coverage
|
||||
|
||||
- R035 (self-healing) and R037 (zero git errors) advanced by S05 but remain active — full validation requires S06 (doctor exists for error messages to reference) and S07 (test coverage).
|
||||
- R040 (doctor) still owned by S06. S05's `formatGitError` references `/gsd doctor` which S06 must implement.
|
||||
- All other active requirements retain their slice ownership unchanged.
|
||||
|
||||
## Boundary Map
|
||||
|
||||
S05 → S06 boundary holds: S05 produced the structured error handling patterns and `formatGitError` that S06 will use for doctor fix operations. No interface changes needed.
|
||||
|
||||
## Next Slice
|
||||
|
||||
S06: Doctor + cleanup + code simplification. Ready to start — all dependencies (S01, S02, S03, S05) complete.
|
||||
50
.gsd/milestones/M003/slices/S06/S06-PLAN.md
Normal file
50
.gsd/milestones/M003/slices/S06/S06-PLAN.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# S06: Doctor + cleanup + code simplification
|
||||
|
||||
**Goal:** `/gsd doctor` detects and fixes git health issues (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files). Branch-mode-only `.gsd/` conflict resolution code annotated for clarity.
|
||||
**Demo:** Run `/gsd doctor` on a repo with an orphaned worktree and stale milestone branch → both detected and fixed.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- 4 new DoctorIssueCode values: `orphaned_auto_worktree`, `stale_milestone_branch`, `corrupt_merge_state`, `tracked_runtime_files`
|
||||
- Detection logic for each using existing `listWorktrees`, `abortAndReset`, `RUNTIME_EXCLUSION_PATHS`
|
||||
- Fix logic for each (remove worktree, delete branch, abort merge, untrack files) gated behind `shouldFix`
|
||||
- Doctor runs from main project root, never crashes if not a git repo
|
||||
- Never removes a worktree matching `process.cwd()`
|
||||
- `.gsd/` conflict resolution code in `git-service.ts` annotated as branch-mode-only
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` — all pass
|
||||
- `npx tsc --noEmit` — zero errors
|
||||
- Existing `doctor.test.ts` and `doctor-fixlevel.test.ts` still pass
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] **T01: Add git health checks to doctor.ts** `est:30m`
|
||||
- Why: R040 — doctor needs git-aware checks. The existing pattern (DoctorIssueCode + detection + fix) is well-established; this extends it with 4 new codes.
|
||||
- Files: `src/resources/extensions/gsd/doctor.ts`, `src/resources/extensions/gsd/git-service.ts`
|
||||
- Do: Add 4 new codes to `DoctorIssueCode` union. Add `checkGitHealth` async function that: (1) lists worktrees via `listWorktrees`, filters to `milestone/` branches, cross-references against roadmap completion status — orphaned if milestone complete or branch gone; (2) lists branches matching `milestone/*`, flags stale if milestone complete; (3) checks for MERGE_HEAD/SQUASH_MSG/rebase dirs via `abortAndReset` detection logic; (4) runs `git ls-files` against `RUNTIME_EXCLUSION_PATHS` entries. Each pushes to `issues[]`. Fixes: removeWorktree (skip if cwd match), branch -D, abortAndReset, git rm --cached. Wrap entire block in try/catch for non-git repos. Add `checkGitHealth` call in `runGSDDoctor` after preferences check. Also annotate the `.gsd/` conflict resolution block in `git-service.ts` (lines ~768-863) with a comment block explaining it's branch-mode-only.
|
||||
- Verify: `npx tsc --noEmit` — zero errors
|
||||
- Done when: DoctorIssueCode has 4 new values, `runGSDDoctor` calls git health checks, `git-service.ts` conflict block annotated
|
||||
|
||||
- [x] **T02: Integration tests for doctor git health checks** `est:25m`
|
||||
- Why: Prove detection and fixes work against real git repos with deliberate broken state. Without tests, the doctor checks are unverified.
|
||||
- Files: `src/resources/extensions/gsd/tests/doctor-git.test.ts`
|
||||
- Do: Create test file with temp git repos. Tests: (1) orphaned worktree detected and fixed (create worktree, mark milestone complete in roadmap, run doctor); (2) stale milestone branch detected and fixed (create branch, complete milestone, run doctor); (3) corrupt merge state detected and fixed (create MERGE_HEAD, run doctor); (4) tracked runtime files detected and fixed (git add .gsd/activity/foo, run doctor); (5) non-git directory doesn't crash (run doctor in /tmp); (6) active worktree NOT flagged as orphaned (worktree exists, milestone in-progress). Use `node:test` runner consistent with other test files.
|
||||
- Verify: `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` — all pass, existing `doctor.test.ts` still passes
|
||||
- Done when: 6+ test cases pass, covering detection and fix for all 4 issue codes plus safety guards
|
||||
|
||||
## Files Likely Touched
|
||||
|
||||
## Observability / Diagnostics
|
||||
|
||||
- **Doctor report output:** 4 new issue codes (`orphaned_auto_worktree`, `stale_milestone_branch`, `corrupt_merge_state`, `tracked_runtime_files`) appear in `/gsd doctor` output with severity, scope, and fix status.
|
||||
- **Fix audit trail:** All auto-fixes log to `fixesApplied[]`, visible in doctor report "Fixes applied" section.
|
||||
- **Graceful degradation:** Non-git directories produce no git-related issues (silent skip). Git failures within checks are caught and don't block other checks.
|
||||
- **Inspection:** Run `/gsd doctor --fix` to see detection + remediation. Run without `--fix` for detection-only mode.
|
||||
|
||||
## Files Likely Touched
|
||||
|
||||
- `src/resources/extensions/gsd/doctor.ts`
|
||||
- `src/resources/extensions/gsd/git-service.ts`
|
||||
- `src/resources/extensions/gsd/tests/doctor-git.test.ts`
|
||||
70
.gsd/milestones/M003/slices/S06/S06-RESEARCH.md
Normal file
70
.gsd/milestones/M003/slices/S06/S06-RESEARCH.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# S06: Doctor + cleanup + code simplification — Research
|
||||
|
||||
**Date:** 2026-03-14
|
||||
|
||||
## Summary
|
||||
|
||||
S06 has two jobs: (1) extend the existing `doctor.ts` with git health checks, and (2) remove dead `.gsd/` conflict resolution code from worktree-mode paths. Both are straightforward additions to well-established patterns.
|
||||
|
||||
The doctor system (`doctor.ts`, 766 lines) already has a mature architecture: `DoctorIssueCode` union type, `DoctorIssue` interface with severity/fixable flags, `runGSDDoctor` function that collects issues and optionally fixes them. Adding git health checks means extending this pattern with new issue codes and detection logic. The self-heal module (`git-self-heal.ts`) provides `abortAndReset` which already detects MERGE_HEAD/SQUASH_MSG/rebase state — doctor can reuse this for detection and fix.
|
||||
|
||||
For dead code removal: `git-service.ts` lines ~768-863 contain ~95 lines of `.gsd/` conflict auto-resolution in `mergeSliceToMain` (runtime conflict resolution via `--theirs`, `.gsd/` planning conflict resolution, post-merge runtime file stripping). In worktree mode, `mergeSliceToMilestone` in `auto-worktree.ts` handles merges instead — this code is only needed for branch-per-slice mode. The code should stay but could be annotated/commented for clarity. Per D038, worktree merges skip `.gsd/` conflict resolution entirely.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Extend `doctor.ts` with git-specific issue codes and checks.** Add detection for: orphaned auto-worktrees (worktree on disk but no matching milestone/branch), stale milestone branches (branch exists but milestone completed), corrupt merge state (MERGE_HEAD/SQUASH_MSG present), and tracked runtime files. Reuse `listWorktrees` from `worktree-manager.ts` and `abortAndReset` from `git-self-heal.ts`. Keep fixes non-destructive (remove worktrees, delete branches, abort merges — never lose data).
|
||||
|
||||
**Do NOT remove the `.gsd/` conflict resolution code from `mergeSliceToMain`.** It's still needed for `git.isolation: "branch"` users. Instead, add a code comment clarifying it's branch-mode-only. The "dead code removal" in the slice description refers to worktree-mode paths — and those paths (`mergeSliceToMilestone`) already have zero conflict resolution code (D038 confirmed).
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Existing Solution | Why Use It |
|
||||
|---------|------------------|------------|
|
||||
| Worktree listing | `listWorktrees()` in worktree-manager.ts | Already parses `git worktree list --porcelain`, returns structured data |
|
||||
| Merge state detection | `abortAndReset()` in git-self-heal.ts | Already checks MERGE_HEAD, SQUASH_MSG, rebase-apply/merge dirs |
|
||||
| Doctor issue reporting | `DoctorIssue` / `DoctorIssueCode` types in doctor.ts | Established pattern with severity, fixable flags, scope, and formatting |
|
||||
| Git command execution | `runGit()` in git-service.ts | Consistent error handling, SVN noise filtering |
|
||||
| Runtime path list | `RUNTIME_EXCLUSION_PATHS` in git-service.ts | Canonical list of paths that shouldn't be tracked |
|
||||
|
||||
## Existing Code and Patterns
|
||||
|
||||
- `src/resources/extensions/gsd/doctor.ts` — Issue detection + fix pattern: detect issue → push to `issues[]` → if `shouldFix(code)` → apply fix → push to `fixesApplied[]`. New git checks follow this exact pattern.
|
||||
- `src/resources/extensions/gsd/git-self-heal.ts` — `abortAndReset(cwd)` detects and cleans MERGE_HEAD/SQUASH_MSG/rebase state. Doctor fix for corrupt merge state can call this directly.
|
||||
- `src/resources/extensions/gsd/worktree-manager.ts` — `listWorktrees(basePath)` returns `WorktreeInfo[]` with path, branch, head, bare, main fields. `removeWorktree(basePath, name, opts)` handles cleanup.
|
||||
- `src/resources/extensions/gsd/git-service.ts:705-870` — `mergeSliceToMain` contains the `.gsd/` conflict resolution code. This is branch-mode-only code and should NOT be removed — just annotated.
|
||||
- `src/resources/extensions/gsd/git-service.ts:101-108` — `RUNTIME_EXCLUSION_PATHS` array lists paths that should never be committed. Doctor can check if any are tracked.
|
||||
- `src/resources/extensions/gsd/auto-worktree.ts` — `autoWorktreeBranch(milestoneId)` returns `milestone/<MID>` — the branch naming convention for detecting auto-worktree branches vs manual `worktree/<name>` branches.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Doctor must work from the main project root, not from within a worktree. Git commands for worktree detection run against the main `.git` dir.
|
||||
- `DoctorIssueCode` is a string union type — adding new codes requires extending the union (type-checked at compile time).
|
||||
- `listWorktrees` returns all worktrees including the main one (marked with `main: true`). Must filter to auto-worktrees only (branch matches `milestone/`).
|
||||
- The `fixLevel` mechanism (`"task"` vs `"all"`) in `runGSDDoctor` controls which fixes are auto-applied. Git fixes should probably be in the `"all"` level since they're infrastructure repair, not completion transitions.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Deleting a worktree that's in use** — If auto-mode is running in a worktree, doctor must not remove it. Check if the worktree path matches `process.cwd()` before removal.
|
||||
- **Branch deletion of checked-out branch** — git refuses to delete a branch checked out in any worktree. Must remove worktree first, then delete branch (D040).
|
||||
- **False positive "stale" branches** — A `milestone/<MID>` branch is only stale if the milestone is marked complete in the roadmap. An in-progress milestone's branch is expected.
|
||||
- **Runtime file tracking detection** — `git ls-files` against `RUNTIME_EXCLUSION_PATHS` may produce false positives if paths use glob patterns. The current list uses directory prefixes, so `git ls-files --error-unmatch .gsd/activity/` will work.
|
||||
|
||||
## Open Risks
|
||||
|
||||
- Doctor currently has no git-aware checks at all — this is entirely new territory. The first implementation should be conservative (detect + report) with fixes gated behind `fix: true`.
|
||||
- If `listWorktrees` fails (not a git repo, git not installed), doctor should degrade gracefully rather than crash. Wrap in try/catch.
|
||||
|
||||
## Skills Discovered
|
||||
|
||||
| Technology | Skill | Status |
|
||||
|------------|-------|--------|
|
||||
| Git | N/A — standard git CLI operations | none needed |
|
||||
|
||||
## Sources
|
||||
|
||||
- S01-SUMMARY: Auto-worktree lifecycle and naming conventions
|
||||
- S02-SUMMARY: mergeSliceToMilestone location and .gsd/ conflict elimination (D037, D038)
|
||||
- S03-SUMMARY: Milestone merge and worktree teardown ordering (D040)
|
||||
- S05-SUMMARY: Self-heal patterns (abortAndReset, formatGitError)
|
||||
- doctor.ts source: Existing issue detection and fix patterns
|
||||
- git-service.ts source: .gsd/ conflict resolution code location (lines 768-863)
|
||||
108
.gsd/milestones/M003/slices/S06/S06-SUMMARY.md
Normal file
108
.gsd/milestones/M003/slices/S06/S06-SUMMARY.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
---
|
||||
id: S06
|
||||
parent: M003
|
||||
milestone: M003
|
||||
provides:
|
||||
- 4 git health check issue codes in doctor (orphaned_auto_worktree, stale_milestone_branch, corrupt_merge_state, tracked_runtime_files)
|
||||
- checkGitHealth function with detection and fix logic for all 4 codes
|
||||
- branch-mode-only annotation on .gsd/ conflict resolution code in git-service.ts
|
||||
- Integration test suite (6 tests, 17 assertions) for git health checks
|
||||
requires:
|
||||
- slice: S01
|
||||
provides: listWorktrees, worktree infrastructure
|
||||
- slice: S05
|
||||
provides: abortAndReset error handling patterns
|
||||
affects:
|
||||
- S07
|
||||
key_files:
|
||||
- src/resources/extensions/gsd/doctor.ts
|
||||
- src/resources/extensions/gsd/git-service.ts
|
||||
- src/resources/extensions/gsd/tests/doctor-git.test.ts
|
||||
key_decisions:
|
||||
- D038 — branch-mode-only annotation on .gsd/ conflict resolution code (annotate rather than delete, preserving branch-mode path)
|
||||
- checkGitHealth is a standalone async function called from runGSDDoctor, not inlined
|
||||
- autoWorktreeBranch import skipped — milestone branch pattern extracted inline via string replace
|
||||
- Worktrees must be under .gsd/worktrees/ to match listWorktrees filter
|
||||
- Roadmap must use ## Slices with checkbox format to match parseRoadmapSlices parser
|
||||
patterns_established:
|
||||
- Git health check test pattern: createRepoWithCompletedMilestone helper, detect → fix → verify cycle
|
||||
- git health checks wrap all operations in try/catch for graceful degradation in non-git repos
|
||||
- fix actions record descriptive strings in fixesApplied for audit trail
|
||||
observability_surfaces:
|
||||
- 4 new issue codes in /gsd doctor output with severity, scope, and fix status
|
||||
- fixesApplied strings for each remediation action
|
||||
- Non-git directories produce no git-related issues (silent skip)
|
||||
drill_down_paths:
|
||||
- .gsd/milestones/M003/slices/S06/tasks/T01-SUMMARY.md
|
||||
- .gsd/milestones/M003/slices/S06/tasks/T02-SUMMARY.md
|
||||
duration: 37m
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-14
|
||||
---
|
||||
|
||||
# S06: Doctor + cleanup + code simplification
|
||||
|
||||
**Added 4 git health checks to `/gsd doctor` with detection, fix, and integration tests covering orphaned worktrees, stale branches, corrupt merge state, and tracked runtime files.**
|
||||
|
||||
## What Happened
|
||||
|
||||
T01 extended the doctor system with `checkGitHealth`, a standalone async function that runs 4 checks: (1) orphaned auto-worktrees — cross-references `listWorktrees` against roadmap completion status, with safety guard against removing the current working directory; (2) stale milestone branches — flags `milestone/*` branches for completed milestones with no associated worktree; (3) corrupt merge state — detects MERGE_HEAD, SQUASH_MSG, and rebase directories, fixes via `abortAndReset`; (4) tracked runtime files — runs `git ls-files` against `RUNTIME_EXCLUSION_PATHS`, fixes via `git rm --cached`. All checks are wrapped in try/catch for non-git repo safety. The `.gsd/` conflict resolution block in git-service.ts was annotated as branch-mode-only per D038.
|
||||
|
||||
T02 built 6 integration tests (17 assertions) using real temp git repos with deliberately broken state. Tests cover the full detect → fix → verify cycle for all 4 issue codes plus safety guards (non-git directory doesn't crash, active worktree not flagged as orphaned).
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsc --noEmit` — zero errors
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` — 17 passed, 0 failed
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor.test.ts` — 59 passed, 0 failed
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts` — 3 passed, 0 failed
|
||||
|
||||
## Requirements Advanced
|
||||
|
||||
- R040 — `/gsd doctor` now detects and fixes 4 git health issue types with full test coverage
|
||||
|
||||
## Requirements Validated
|
||||
|
||||
- R040 — 6 integration tests prove detection and fix for all 4 issue codes, plus safety guards for non-git repos and active worktrees
|
||||
|
||||
## New Requirements Surfaced
|
||||
|
||||
- none
|
||||
|
||||
## Requirements Invalidated or Re-scoped
|
||||
|
||||
- none
|
||||
|
||||
## Deviations
|
||||
|
||||
- Worktree path in tests changed from `.gsd-worktrees/` to `.gsd/worktrees/` to match `listWorktrees` filter
|
||||
- Roadmap format in tests changed from table to checkbox format to match `parseRoadmapSlices` parser
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- `.gsd/` conflict resolution code is annotated but not removed — preserved for `git.isolation: "branch"` users per R036/R038
|
||||
- Doctor git checks require the `git` CLI to be available; no fallback to native module
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- none
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/resources/extensions/gsd/doctor.ts` — 4 new DoctorIssueCode values, checkGitHealth function
|
||||
- `src/resources/extensions/gsd/git-service.ts` — branch-mode-only annotation on conflict resolution code
|
||||
- `src/resources/extensions/gsd/tests/doctor-git.test.ts` — 6 integration tests for git health checks
|
||||
|
||||
## Forward Intelligence
|
||||
|
||||
### What the next slice should know
|
||||
- All 4 doctor git checks work and have tests. S07 can build on these test patterns for broader coverage.
|
||||
|
||||
### What's fragile
|
||||
- `parseRoadmapSlices` is strict about format — tests must use `## Slices` with `- [x] **S01: Title**` format, not tables.
|
||||
|
||||
### Authoritative diagnostics
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` — canonical test for git health checks
|
||||
|
||||
### What assumptions changed
|
||||
- Assumed `.gsd-worktrees/` path — actual path is `.gsd/worktrees/` per listWorktrees filter
|
||||
111
.gsd/milestones/M003/slices/S06/S06-UAT.md
Normal file
111
.gsd/milestones/M003/slices/S06/S06-UAT.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# S06: Doctor + cleanup + code simplification — UAT
|
||||
|
||||
**Milestone:** M003
|
||||
**Written:** 2026-03-14
|
||||
|
||||
## UAT Type
|
||||
|
||||
- UAT mode: artifact-driven
|
||||
- Why this mode is sufficient: Doctor checks are CLI commands with deterministic output — detection and fix results are fully verifiable via command output and git state inspection.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Must be in the gsd-2 project root
|
||||
- Git CLI available
|
||||
- Project builds cleanly (`npx tsc --noEmit`)
|
||||
|
||||
## Smoke Test
|
||||
|
||||
Run `/gsd doctor` in a clean repo. Confirm no git-related issues are reported (no orphaned worktrees, no stale branches, no corrupt merge state, no tracked runtime files). Output should show only non-git checks.
|
||||
|
||||
## Test Cases
|
||||
|
||||
### 1. Orphaned worktree detection and fix
|
||||
|
||||
1. Create a temp git repo with a completed milestone in the roadmap
|
||||
2. Create a worktree under `.gsd/worktrees/M099/` on branch `milestone/M099`
|
||||
3. Run `/gsd doctor` (detection only)
|
||||
4. **Expected:** Issue `orphaned_auto_worktree` reported with severity and worktree path
|
||||
5. Run `/gsd doctor --fix`
|
||||
6. **Expected:** Worktree removed, fix recorded in fixesApplied
|
||||
7. Run `/gsd doctor` again
|
||||
8. **Expected:** No orphaned worktree issues
|
||||
|
||||
### 2. Stale milestone branch detection and fix
|
||||
|
||||
1. Create a temp git repo with a completed milestone in the roadmap
|
||||
2. Create branch `milestone/M099` (no worktree)
|
||||
3. Run `/gsd doctor`
|
||||
4. **Expected:** Issue `stale_milestone_branch` reported
|
||||
5. Run `/gsd doctor --fix`
|
||||
6. **Expected:** Branch deleted, fix recorded
|
||||
7. Verify branch gone: `git branch --list milestone/M099` returns empty
|
||||
|
||||
### 3. Corrupt merge state detection and fix
|
||||
|
||||
1. Create a temp git repo
|
||||
2. Create `.git/MERGE_HEAD` file with dummy content
|
||||
3. Run `/gsd doctor`
|
||||
4. **Expected:** Issue `corrupt_merge_state` reported
|
||||
5. Run `/gsd doctor --fix`
|
||||
6. **Expected:** MERGE_HEAD removed via abortAndReset, fix recorded
|
||||
|
||||
### 4. Tracked runtime files detection and fix
|
||||
|
||||
1. Create a temp git repo
|
||||
2. `git add` a file matching RUNTIME_EXCLUSION_PATHS (e.g., `.gsd/activity/foo.md`)
|
||||
3. Run `/gsd doctor`
|
||||
4. **Expected:** Issue `tracked_runtime_files` reported
|
||||
5. Run `/gsd doctor --fix`
|
||||
6. **Expected:** File untracked via `git rm --cached`, fix recorded
|
||||
|
||||
### 5. Non-git directory safety
|
||||
|
||||
1. Run `/gsd doctor` from a non-git directory (e.g., `/tmp/nonrepo`)
|
||||
2. **Expected:** No crash, no git-related issues reported, other checks still run
|
||||
|
||||
### 6. Active worktree not flagged
|
||||
|
||||
1. Create a temp git repo with an in-progress milestone
|
||||
2. Create a worktree under `.gsd/worktrees/M099/` on branch `milestone/M099`
|
||||
3. Run `/gsd doctor`
|
||||
4. **Expected:** Worktree NOT flagged as orphaned (milestone is in-progress)
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### cwd matches orphaned worktree
|
||||
|
||||
1. Create a worktree for a completed milestone
|
||||
2. `cd` into the worktree directory
|
||||
3. Run doctor
|
||||
4. **Expected:** Worktree detected as orphaned but NOT removed (safety guard against removing cwd)
|
||||
|
||||
### Multiple issue types simultaneously
|
||||
|
||||
1. Create a repo with an orphaned worktree AND a MERGE_HEAD file AND tracked runtime files
|
||||
2. Run `/gsd doctor`
|
||||
3. **Expected:** All 3 issues detected independently
|
||||
4. Run `/gsd doctor --fix`
|
||||
5. **Expected:** All 3 fixed independently
|
||||
|
||||
## Failure Signals
|
||||
|
||||
- Any test in `doctor-git.test.ts` failing
|
||||
- `npx tsc --noEmit` producing errors
|
||||
- Existing `doctor.test.ts` or `doctor-fixlevel.test.ts` tests regressing
|
||||
- `/gsd doctor` crashing in a non-git directory
|
||||
|
||||
## Requirements Proved By This UAT
|
||||
|
||||
- R040 — Doctor detects and fixes orphaned auto-worktrees, stale milestone branches, corrupt merge state, and tracked runtime files
|
||||
|
||||
## Not Proven By This UAT
|
||||
|
||||
- R041 — Full test suite coverage (deferred to S07)
|
||||
- R036 — Dead code removal (annotated only, not removed, per backwards compatibility)
|
||||
- Live auto-mode interaction with doctor (operational verification)
|
||||
|
||||
## Notes for Tester
|
||||
|
||||
- All test cases are already covered by automated tests in `doctor-git.test.ts`. Run `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` to verify all 17 assertions pass.
|
||||
- The `.gsd/` conflict resolution code was annotated, not removed — this is intentional per R038 (backwards compatibility with branch-per-slice model).
|
||||
59
.gsd/milestones/M003/slices/S06/tasks/T01-PLAN.md
Normal file
59
.gsd/milestones/M003/slices/S06/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
estimated_steps: 6
|
||||
estimated_files: 3
|
||||
---
|
||||
|
||||
# T01: Add git health checks to doctor.ts
|
||||
|
||||
**Slice:** S06 — Doctor + cleanup + code simplification
|
||||
**Milestone:** M003
|
||||
|
||||
## Description
|
||||
|
||||
Extend `runGSDDoctor` with 4 new git health checks: orphaned auto-worktrees, stale milestone branches, corrupt merge state, and tracked runtime files. Add code annotation to branch-mode-only `.gsd/` conflict resolution in `git-service.ts`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add 4 new values to `DoctorIssueCode` union type: `orphaned_auto_worktree`, `stale_milestone_branch`, `corrupt_merge_state`, `tracked_runtime_files`
|
||||
2. Import `listWorktrees` from `worktree-manager.ts`, `autoWorktreeBranch` from `auto-worktree.ts`, `abortAndReset` from `git-self-heal.ts`, `RUNTIME_EXCLUSION_PATHS` from `git-service.ts`, and `execSync` for direct git commands
|
||||
3. Create `checkGitHealth(basePath, issues, fixesApplied, shouldFix)` async function:
|
||||
- Wrap all git operations in try/catch (degrade gracefully if not a git repo)
|
||||
- **Orphaned worktrees:** Call `listWorktrees(basePath)`, filter to branches starting with `milestone/`. For each, extract milestone ID, load roadmap, check if milestone is complete via `isMilestoneComplete`. If complete → orphaned. Skip fix if worktree path === `process.cwd()`.
|
||||
- **Stale branches:** Run `git branch --list 'milestone/*'`, cross-reference against completed milestones. A branch is stale if its milestone is complete AND no worktree points to it (worktree check already handles the overlap case).
|
||||
- **Corrupt merge state:** Check for MERGE_HEAD, SQUASH_MSG, rebase-apply/, rebase-merge/ in `.git/` dir. If found, report. Fix via `abortAndReset(basePath)`.
|
||||
- **Tracked runtime files:** Run `git ls-files` for each `RUNTIME_EXCLUSION_PATHS` entry. If any returned, report. Fix via `git rm --cached -r --ignore-unmatch`.
|
||||
4. Call `checkGitHealth` from `runGSDDoctor` after the preferences validation block
|
||||
5. Add a block comment above the `.gsd/` conflict resolution code in `git-service.ts` (~line 768) explaining it's branch-mode-only and not used in worktree isolation mode (D038)
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] 4 new DoctorIssueCode values compile
|
||||
- [ ] Git health checks run inside `runGSDDoctor`
|
||||
- [ ] Non-git repos don't crash doctor
|
||||
- [ ] Active worktrees (cwd match) are never removed
|
||||
- [ ] `.gsd/` conflict code annotated
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsc --noEmit` — zero errors
|
||||
- Existing `npx tsx tests/doctor.test.ts` and `doctor-fixlevel.test.ts` still pass
|
||||
|
||||
## Inputs
|
||||
|
||||
- `src/resources/extensions/gsd/doctor.ts` — existing doctor pattern
|
||||
- `src/resources/extensions/gsd/git-self-heal.ts` — `abortAndReset` for corrupt merge state detection/fix
|
||||
- `src/resources/extensions/gsd/worktree-manager.ts` — `listWorktrees` for orphaned worktree detection
|
||||
- `src/resources/extensions/gsd/auto-worktree.ts` — `autoWorktreeBranch` for milestone branch naming
|
||||
- `src/resources/extensions/gsd/git-service.ts` — `RUNTIME_EXCLUSION_PATHS` for tracked file detection
|
||||
- S05-SUMMARY: abortAndReset patterns, formatGitError
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `src/resources/extensions/gsd/doctor.ts` — 4 new issue codes, `checkGitHealth` function, called from `runGSDDoctor`
|
||||
- `src/resources/extensions/gsd/git-service.ts` — block comment on `.gsd/` conflict resolution code
|
||||
|
||||
## Observability Impact
|
||||
|
||||
- **New issue codes visible in doctor report:** `orphaned_auto_worktree`, `stale_milestone_branch`, `corrupt_merge_state`, `tracked_runtime_files` — all appear in `formatDoctorReport` output and `summarizeDoctorIssues` byCode breakdown.
|
||||
- **Fix actions logged:** Each fix records a human-readable string in `fixesApplied[]`, surfaced in doctor report under "Fixes applied".
|
||||
- **Failure degradation:** All git checks wrap in try/catch — failures are silent (no issue emitted) rather than crashing doctor. This means a broken git repo won't block non-git doctor checks.
|
||||
65
.gsd/milestones/M003/slices/S06/tasks/T01-SUMMARY.md
Normal file
65
.gsd/milestones/M003/slices/S06/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S06
|
||||
milestone: M003
|
||||
provides:
|
||||
- 4 git health check issue codes in DoctorIssueCode union
|
||||
- checkGitHealth function detecting orphaned worktrees, stale branches, corrupt merge state, tracked runtime files
|
||||
- branch-mode-only annotation on .gsd/ conflict resolution code
|
||||
key_files:
|
||||
- src/resources/extensions/gsd/doctor.ts
|
||||
- src/resources/extensions/gsd/git-service.ts
|
||||
key_decisions:
|
||||
- checkGitHealth is a standalone async function called from runGSDDoctor, not inlined
|
||||
- autoWorktreeBranch import skipped — milestone branch pattern extracted inline via string replace
|
||||
patterns_established:
|
||||
- git health checks wrap all operations in try/catch for graceful degradation in non-git repos
|
||||
- fix actions record descriptive strings in fixesApplied for audit trail
|
||||
observability_surfaces:
|
||||
- 4 new issue codes in doctor report output (orphaned_auto_worktree, stale_milestone_branch, corrupt_merge_state, tracked_runtime_files)
|
||||
- fixesApplied strings for each remediation action
|
||||
duration: 12m
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-14T22:16:00-06:00
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Add git health checks to doctor.ts
|
||||
|
||||
**Added 4 git health checks (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files) to runGSDDoctor with detection and fix logic.**
|
||||
|
||||
## What Happened
|
||||
|
||||
Added `checkGitHealth` async function to doctor.ts with 4 checks:
|
||||
1. **Orphaned auto-worktrees:** Uses `listWorktrees` to find `milestone/*` branches, cross-references against roadmap completion via `isMilestoneComplete`. Skips removal if worktree path matches `process.cwd()`.
|
||||
2. **Stale milestone branches:** Lists `milestone/*` branches, flags those for completed milestones that have no associated worktree. Fix deletes with `git branch -D`.
|
||||
3. **Corrupt merge state:** Checks for MERGE_HEAD, SQUASH_MSG, rebase-apply/, rebase-merge/ in .git/. Fix calls `abortAndReset`.
|
||||
4. **Tracked runtime files:** Runs `git ls-files` against each `RUNTIME_EXCLUSION_PATHS` entry. Fix runs `git rm --cached -r --ignore-unmatch`.
|
||||
|
||||
Added 4 new values to `DoctorIssueCode` union type. Imported `listWorktrees`, `abortAndReset`, `RUNTIME_EXCLUSION_PATHS`, and `execSync`. Called `checkGitHealth` from `runGSDDoctor` after preferences validation.
|
||||
|
||||
Annotated the `.gsd/` conflict resolution block in git-service.ts (~line 770) with a block comment explaining it's branch-mode-only (D038).
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsc --noEmit` — zero errors
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor.test.ts` — 59 passed, 0 failed
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts` — all passed
|
||||
- Slice-level `doctor-git.test.ts` does not exist yet (T02 will create it)
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Run `/gsd doctor` to see git health issues. Run `/gsd doctor --fix` to auto-remediate. Issue codes appear in `summarizeDoctorIssues` byCode breakdown. Non-git repos produce no git-related output.
|
||||
|
||||
## Deviations
|
||||
|
||||
- Skipped importing `autoWorktreeBranch` — the branch naming pattern (`milestone/${milestoneId}`) is trivial enough to inline as a string replace, avoiding an unnecessary import.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/resources/extensions/gsd/doctor.ts` — 4 new DoctorIssueCode values, checkGitHealth function, called from runGSDDoctor
|
||||
- `src/resources/extensions/gsd/git-service.ts` — block comment on .gsd/ conflict resolution code (branch-mode-only, D038)
|
||||
55
.gsd/milestones/M003/slices/S06/tasks/T02-PLAN.md
Normal file
55
.gsd/milestones/M003/slices/S06/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
estimated_steps: 4
|
||||
estimated_files: 1
|
||||
---
|
||||
|
||||
# T02: Integration tests for doctor git health checks
|
||||
|
||||
**Slice:** S06 — Doctor + cleanup + code simplification
|
||||
**Milestone:** M003
|
||||
|
||||
## Description
|
||||
|
||||
Build integration tests that create real temp git repos with deliberate broken state, run `runGSDDoctor`, and assert correct detection and fixing of all 4 git issue codes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `doctor-git.test.ts` using `node:test` with temp dir helpers (consistent with `auto-worktree.test.ts` pattern — `mkdtempSync`, `realpathSync`, `execSync` for git init)
|
||||
2. Write helper to create a minimal GSD project with roadmap containing a milestone (reuse pattern from auto-worktree tests)
|
||||
3. Implement test cases:
|
||||
- Orphaned worktree: create worktree with `milestone/M001` branch, mark M001 complete in roadmap → doctor detects `orphaned_auto_worktree`, fix removes it
|
||||
- Stale branch: create `milestone/M001` branch (no worktree), mark M001 complete → doctor detects `stale_milestone_branch`, fix deletes branch
|
||||
- Corrupt merge state: write MERGE_HEAD file in `.git/` → doctor detects `corrupt_merge_state`, fix cleans it
|
||||
- Tracked runtime files: `git add -f .gsd/activity/test.log` → doctor detects `tracked_runtime_files`, fix untracks
|
||||
- Non-git dir: run doctor in a plain temp dir → no crash, no git issues reported
|
||||
- Active worktree safety: create worktree, milestone in-progress → NOT flagged as orphaned
|
||||
4. Each test: run `runGSDDoctor(basePath)` for detection assertions, then `runGSDDoctor(basePath, { fix: true })` for fix assertions, then verify git state after fix
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] All 4 issue codes tested for detection
|
||||
- [ ] All 4 issue codes tested for fix
|
||||
- [ ] Non-git directory graceful degradation tested
|
||||
- [ ] Active worktree not flagged (false positive prevention)
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` — all pass
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor.test.ts` — still passes
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts` — still passes
|
||||
|
||||
## Inputs
|
||||
|
||||
- `src/resources/extensions/gsd/doctor.ts` — T01's new `checkGitHealth` function and issue codes
|
||||
- `src/resources/extensions/gsd/tests/auto-worktree.test.ts` — temp repo setup patterns
|
||||
- `src/resources/extensions/gsd/tests/git-self-heal.test.ts` — corrupt state injection patterns
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `src/resources/extensions/gsd/tests/doctor-git.test.ts` — 6+ test cases with real git repos
|
||||
|
||||
## Observability Impact
|
||||
|
||||
- **Test output:** Running `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` prints pass/fail for all 6 test cases (17 assertions) covering detection and fix of all 4 git issue codes plus graceful degradation and false positive prevention.
|
||||
- **Failure diagnostics:** Each failed assertion prints the expected vs actual value with a descriptive label.
|
||||
- **No runtime signals changed** — this task adds tests only, no production behavior changes.
|
||||
54
.gsd/milestones/M003/slices/S06/tasks/T02-SUMMARY.md
Normal file
54
.gsd/milestones/M003/slices/S06/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
id: T02
|
||||
parent: S06
|
||||
milestone: M003
|
||||
provides:
|
||||
- Integration tests for all 4 git health check issue codes in doctor
|
||||
key_files:
|
||||
- src/resources/extensions/gsd/tests/doctor-git.test.ts
|
||||
key_decisions:
|
||||
- Worktrees must be under .gsd/worktrees/ to match listWorktrees filter (not .gsd-worktrees/)
|
||||
- Roadmap must use `## Slices` with checkbox format to match parseRoadmapSlices parser
|
||||
patterns_established:
|
||||
- Git health check test pattern: createRepoWithCompletedMilestone helper, detect → fix → verify cycle
|
||||
observability_surfaces:
|
||||
- none
|
||||
duration: 25m
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-14
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Integration tests for doctor git health checks
|
||||
|
||||
**Built 6 integration tests (17 assertions) covering detection, fix, and false-positive prevention for all 4 git health check issue codes.**
|
||||
|
||||
## What Happened
|
||||
|
||||
Created `doctor-git.test.ts` with real temp git repos. Each test injects deliberate broken state, runs `runGSDDoctor` for detection, then `runGSDDoctor({fix:true})` for remediation, then verifies git state post-fix. Key discovery: worktrees must be under `.gsd/worktrees/` (not `.gsd-worktrees/`) and roadmaps must use the `## Slices` checkbox format (not table format) to match the actual parsers.
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` — 17 passed, 0 failed ✓
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor.test.ts` — 59 passed, 0 failed ✓
|
||||
- `npx tsx src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts` — all pass ✓
|
||||
- `npx tsc --noEmit` — zero errors ✓
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Run `npx tsx src/resources/extensions/gsd/tests/doctor-git.test.ts` to see all git health check test results.
|
||||
|
||||
## Deviations
|
||||
|
||||
- Roadmap format in tests changed from table (`## Slice Inventory` with `| |` rows) to checkbox format (`## Slices` with `- [x] **S01: ...**`) to match `parseRoadmapSlices` parser expectations.
|
||||
- Worktree path changed from `.gsd-worktrees/` to `.gsd/worktrees/` to match `listWorktrees` filter.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/resources/extensions/gsd/tests/doctor-git.test.ts` — 6 integration tests for git health checks
|
||||
- `.gsd/milestones/M003/slices/S06/S06-PLAN.md` — marked T02 done
|
||||
- `.gsd/milestones/M003/slices/S06/tasks/T02-PLAN.md` — added Observability Impact section
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { execSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
|
|
@ -5,6 +6,9 @@ import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPla
|
|||
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
||||
import { deriveState, isMilestoneComplete } from "./state.js";
|
||||
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
||||
import { listWorktrees } from "./worktree-manager.js";
|
||||
import { abortAndReset } from "./git-self-heal.js";
|
||||
import { RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
|
||||
|
||||
export type DoctorSeverity = "info" | "warning" | "error";
|
||||
export type DoctorIssueCode =
|
||||
|
|
@ -23,7 +27,11 @@ export type DoctorIssueCode =
|
|||
| "active_requirement_missing_owner"
|
||||
| "blocked_requirement_missing_reason"
|
||||
| "blocker_discovered_no_replan"
|
||||
| "delimiter_in_title";
|
||||
| "delimiter_in_title"
|
||||
| "orphaned_auto_worktree"
|
||||
| "stale_milestone_branch"
|
||||
| "corrupt_merge_state"
|
||||
| "tracked_runtime_files";
|
||||
|
||||
export interface DoctorIssue {
|
||||
severity: DoctorSeverity;
|
||||
|
|
@ -451,6 +459,189 @@ export function formatDoctorIssuesForPrompt(issues: DoctorIssue[]): string {
|
|||
}).join("\n");
|
||||
}
|
||||
|
||||
async function checkGitHealth(
|
||||
basePath: string,
|
||||
issues: DoctorIssue[],
|
||||
fixesApplied: string[],
|
||||
shouldFix: (code: DoctorIssueCode) => boolean,
|
||||
): Promise<void> {
|
||||
// Degrade gracefully if not a git repo
|
||||
try {
|
||||
execSync("git rev-parse --git-dir", { cwd: basePath, stdio: "pipe" });
|
||||
} catch {
|
||||
return; // Not a git repo — skip all git health checks
|
||||
}
|
||||
|
||||
const gitDir = join(basePath, ".git");
|
||||
|
||||
// ── Orphaned auto-worktrees ──────────────────────────────────────────
|
||||
try {
|
||||
const worktrees = listWorktrees(basePath);
|
||||
const milestoneWorktrees = worktrees.filter(wt => wt.branch.startsWith("milestone/"));
|
||||
|
||||
// Load roadmap state once for cross-referencing
|
||||
const state = await deriveState(basePath);
|
||||
|
||||
for (const wt of milestoneWorktrees) {
|
||||
// Extract milestone ID from branch name "milestone/M001" → "M001"
|
||||
const milestoneId = wt.branch.replace(/^milestone\//, "");
|
||||
const milestoneEntry = state.registry.find(m => m.id === milestoneId);
|
||||
|
||||
// Check if milestone is complete via roadmap
|
||||
let isComplete = false;
|
||||
if (milestoneEntry) {
|
||||
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
||||
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
||||
if (roadmapContent) {
|
||||
const roadmap = parseRoadmap(roadmapContent);
|
||||
isComplete = isMilestoneComplete(roadmap);
|
||||
}
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "orphaned_auto_worktree",
|
||||
scope: "milestone",
|
||||
unitId: milestoneId,
|
||||
message: `Worktree for completed milestone ${milestoneId} still exists at ${wt.path}`,
|
||||
fixable: true,
|
||||
});
|
||||
|
||||
if (shouldFix("orphaned_auto_worktree")) {
|
||||
// Never remove a worktree matching current working directory
|
||||
const cwd = process.cwd();
|
||||
if (wt.path === cwd || cwd.startsWith(wt.path + "/")) {
|
||||
fixesApplied.push(`skipped removing worktree at ${wt.path} (is cwd)`);
|
||||
} else {
|
||||
try {
|
||||
execSync(`git worktree remove --force "${wt.path}"`, { cwd: basePath, stdio: "pipe" });
|
||||
fixesApplied.push(`removed orphaned worktree ${wt.path}`);
|
||||
} catch {
|
||||
fixesApplied.push(`failed to remove worktree ${wt.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stale milestone branches ─────────────────────────────────────────
|
||||
try {
|
||||
const branchOutput = execSync("git branch --list 'milestone/*'", { cwd: basePath, stdio: "pipe" }).toString().trim();
|
||||
if (branchOutput) {
|
||||
const branches = branchOutput.split("\n").map(b => b.trim().replace(/^\*\s*/, "")).filter(Boolean);
|
||||
const worktreeBranches = new Set(milestoneWorktrees.map(wt => wt.branch));
|
||||
|
||||
for (const branch of branches) {
|
||||
// Skip branches that have a worktree (handled above)
|
||||
if (worktreeBranches.has(branch)) continue;
|
||||
|
||||
const milestoneId = branch.replace(/^milestone\//, "");
|
||||
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
||||
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
||||
if (!roadmapContent) continue;
|
||||
|
||||
const roadmap = parseRoadmap(roadmapContent);
|
||||
if (isMilestoneComplete(roadmap)) {
|
||||
issues.push({
|
||||
severity: "info",
|
||||
code: "stale_milestone_branch",
|
||||
scope: "milestone",
|
||||
unitId: milestoneId,
|
||||
message: `Branch ${branch} exists for completed milestone ${milestoneId}`,
|
||||
fixable: true,
|
||||
});
|
||||
|
||||
if (shouldFix("stale_milestone_branch")) {
|
||||
try {
|
||||
execSync(`git branch -D "${branch}"`, { cwd: basePath, stdio: "pipe" });
|
||||
fixesApplied.push(`deleted stale branch ${branch}`);
|
||||
} catch {
|
||||
fixesApplied.push(`failed to delete branch ${branch}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// git branch list failed — skip stale branch check
|
||||
}
|
||||
} catch {
|
||||
// listWorktrees or deriveState failed — skip worktree/branch checks
|
||||
}
|
||||
|
||||
// ── Corrupt merge state ────────────────────────────────────────────────
|
||||
try {
|
||||
const mergeStateFiles = ["MERGE_HEAD", "SQUASH_MSG"];
|
||||
const mergeStateDirs = ["rebase-apply", "rebase-merge"];
|
||||
const found: string[] = [];
|
||||
|
||||
for (const f of mergeStateFiles) {
|
||||
if (existsSync(join(gitDir, f))) found.push(f);
|
||||
}
|
||||
for (const d of mergeStateDirs) {
|
||||
if (existsSync(join(gitDir, d))) found.push(d);
|
||||
}
|
||||
|
||||
if (found.length > 0) {
|
||||
issues.push({
|
||||
severity: "error",
|
||||
code: "corrupt_merge_state",
|
||||
scope: "project",
|
||||
unitId: "project",
|
||||
message: `Corrupt merge/rebase state detected: ${found.join(", ")}`,
|
||||
fixable: true,
|
||||
});
|
||||
|
||||
if (shouldFix("corrupt_merge_state")) {
|
||||
const result = abortAndReset(basePath);
|
||||
fixesApplied.push(`cleaned merge state: ${result.cleaned.join(", ")}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Can't check .git dir — skip
|
||||
}
|
||||
|
||||
// ── Tracked runtime files ──────────────────────────────────────────────
|
||||
try {
|
||||
const trackedPaths: string[] = [];
|
||||
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
||||
try {
|
||||
const output = execSync(`git ls-files "${exclusion}"`, { cwd: basePath, stdio: "pipe" }).toString().trim();
|
||||
if (output) {
|
||||
trackedPaths.push(...output.split("\n").filter(Boolean));
|
||||
}
|
||||
} catch {
|
||||
// Individual ls-files can fail — continue
|
||||
}
|
||||
}
|
||||
|
||||
if (trackedPaths.length > 0) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "tracked_runtime_files",
|
||||
scope: "project",
|
||||
unitId: "project",
|
||||
message: `${trackedPaths.length} runtime file(s) are tracked by git: ${trackedPaths.slice(0, 5).join(", ")}${trackedPaths.length > 5 ? "..." : ""}`,
|
||||
fixable: true,
|
||||
});
|
||||
|
||||
if (shouldFix("tracked_runtime_files")) {
|
||||
try {
|
||||
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
||||
execSync(`git rm --cached -r --ignore-unmatch "${exclusion}"`, { cwd: basePath, stdio: "pipe" });
|
||||
}
|
||||
fixesApplied.push(`untracked ${trackedPaths.length} runtime file(s)`);
|
||||
} catch {
|
||||
fixesApplied.push("failed to untrack runtime files");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// git ls-files failed — skip
|
||||
}
|
||||
}
|
||||
|
||||
export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; scope?: string; fixLevel?: "task" | "all" }): Promise<DoctorReport> {
|
||||
const issues: DoctorIssue[] = [];
|
||||
const fixesApplied: string[] = [];
|
||||
|
|
@ -491,6 +682,9 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|||
}
|
||||
}
|
||||
|
||||
// Git health checks (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files)
|
||||
await checkGitHealth(basePath, issues, fixesApplied, shouldFix);
|
||||
|
||||
const milestonesPath = milestonesDir(basePath);
|
||||
if (!existsSync(milestonesPath)) {
|
||||
return { ok: issues.every(issue => issue.severity !== "error"), basePath, issues, fixesApplied };
|
||||
|
|
|
|||
|
|
@ -766,6 +766,15 @@ export class GitServiceImpl {
|
|||
this.git(mergeArgs);
|
||||
} catch (mergeError) {
|
||||
// Check if conflicts can be auto-resolved (#189, #218)
|
||||
//
|
||||
// ─── BRANCH-MODE ONLY (D038) ────────────────────────────────────────
|
||||
// The conflict resolution logic below applies ONLY when git.isolation = "branch".
|
||||
// In worktree isolation mode, each milestone works in its own worktree directory
|
||||
// so merge conflicts between slice branches and main are handled differently
|
||||
// (worktree teardown merges via worktree-manager). This block is never reached
|
||||
// in worktree mode because mergeSliceToMain is only called from the branch-mode
|
||||
// code path. If you're modifying this logic, verify the isolation mode first.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
const conflicted = this.git(["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
|
||||
if (conflicted) {
|
||||
const conflictedFiles = conflicted.split("\n").filter(Boolean);
|
||||
|
|
|
|||
246
src/resources/extensions/gsd/tests/doctor-git.test.ts
Normal file
246
src/resources/extensions/gsd/tests/doctor-git.test.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
* doctor-git.test.ts — Integration tests for doctor git health checks.
|
||||
*
|
||||
* Creates real temp git repos with deliberate broken state, runs runGSDDoctor,
|
||||
* and asserts correct detection and fixing of all 4 git issue codes:
|
||||
* orphaned_auto_worktree, stale_milestone_branch,
|
||||
* corrupt_merge_state, tracked_runtime_files
|
||||
*/
|
||||
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
import { runGSDDoctor } from "../doctor.ts";
|
||||
import { createTestContext } from "./test-helpers.ts";
|
||||
|
||||
const { assertEq, assertTrue, report } = createTestContext();
|
||||
|
||||
function run(cmd: string, cwd: string): string {
|
||||
return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
||||
}
|
||||
|
||||
/** Create a temp git repo with a completed milestone M001 in roadmap. */
|
||||
function createRepoWithCompletedMilestone(): string {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-git-test-")));
|
||||
run("git init", dir);
|
||||
run("git config user.email test@test.com", dir);
|
||||
run("git config user.name Test", dir);
|
||||
|
||||
// Initial commit
|
||||
writeFileSync(join(dir, "README.md"), "# test\n");
|
||||
run("git add .", dir);
|
||||
run("git commit -m init", dir);
|
||||
run("git branch -M main", dir);
|
||||
|
||||
// Create .gsd structure with milestone M001 — all slices done → complete
|
||||
const msDir = join(dir, ".gsd", "milestones", "M001");
|
||||
mkdirSync(msDir, { recursive: true });
|
||||
writeFileSync(join(msDir, "ROADMAP.md"), `---
|
||||
id: M001
|
||||
title: "Test Milestone"
|
||||
---
|
||||
|
||||
# M001: Test Milestone
|
||||
|
||||
## Vision
|
||||
Test
|
||||
|
||||
## Success Criteria
|
||||
- Done
|
||||
|
||||
## Slices
|
||||
- [x] **S01: Test slice** \`risk:low\` \`depends:[]\`
|
||||
> After this: done
|
||||
|
||||
## Boundary Map
|
||||
_None_
|
||||
`);
|
||||
|
||||
// Commit .gsd files
|
||||
run("git add -A", dir);
|
||||
run("git commit -m 'add milestone'", dir);
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
/** Create a repo with an in-progress milestone. */
|
||||
function createRepoWithActiveMilestone(): string {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-git-test-")));
|
||||
run("git init", dir);
|
||||
run("git config user.email test@test.com", dir);
|
||||
run("git config user.name Test", dir);
|
||||
|
||||
writeFileSync(join(dir, "README.md"), "# test\n");
|
||||
run("git add .", dir);
|
||||
run("git commit -m init", dir);
|
||||
run("git branch -M main", dir);
|
||||
|
||||
const msDir = join(dir, ".gsd", "milestones", "M001");
|
||||
mkdirSync(msDir, { recursive: true });
|
||||
writeFileSync(join(msDir, "ROADMAP.md"), `---
|
||||
id: M001
|
||||
title: "Active Milestone"
|
||||
---
|
||||
|
||||
# M001: Active Milestone
|
||||
|
||||
## Vision
|
||||
Test
|
||||
|
||||
## Success Criteria
|
||||
- Done
|
||||
|
||||
## Slices
|
||||
- [ ] **S01: Test slice** \`risk:low\` \`depends:[]\`
|
||||
> After this: done
|
||||
|
||||
## Boundary Map
|
||||
_None_
|
||||
`);
|
||||
|
||||
run("git add -A", dir);
|
||||
run("git commit -m 'add milestone'", dir);
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const cleanups: string[] = [];
|
||||
|
||||
try {
|
||||
// ─── Test 1: Orphaned worktree detection & fix ─────────────────────
|
||||
console.log("\n=== orphaned_auto_worktree ===");
|
||||
{
|
||||
const dir = createRepoWithCompletedMilestone();
|
||||
cleanups.push(dir);
|
||||
|
||||
// Create worktree with milestone/M001 branch under .gsd/worktrees/
|
||||
mkdirSync(join(dir, ".gsd", "worktrees"), { recursive: true });
|
||||
run("git worktree add -b milestone/M001 .gsd/worktrees/M001", dir);
|
||||
|
||||
const detect = await runGSDDoctor(dir);
|
||||
const orphanIssues = detect.issues.filter(i => i.code === "orphaned_auto_worktree");
|
||||
assertTrue(orphanIssues.length > 0, "detects orphaned worktree");
|
||||
assertEq(orphanIssues[0]?.unitId, "M001", "orphaned worktree unitId is M001");
|
||||
|
||||
const fixed = await runGSDDoctor(dir, { fix: true });
|
||||
assertTrue(fixed.fixesApplied.some(f => f.includes("removed orphaned worktree")), "fix removes orphaned worktree");
|
||||
|
||||
// Verify worktree is gone
|
||||
const wtList = run("git worktree list", dir);
|
||||
assertTrue(!wtList.includes("milestone/M001"), "worktree no longer listed after fix");
|
||||
}
|
||||
|
||||
// ─── Test 2: Stale milestone branch detection & fix ────────────────
|
||||
console.log("\n=== stale_milestone_branch ===");
|
||||
{
|
||||
const dir = createRepoWithCompletedMilestone();
|
||||
cleanups.push(dir);
|
||||
|
||||
// Create a milestone/M001 branch (no worktree)
|
||||
run("git branch milestone/M001", dir);
|
||||
|
||||
const detect = await runGSDDoctor(dir);
|
||||
const staleIssues = detect.issues.filter(i => i.code === "stale_milestone_branch");
|
||||
assertTrue(staleIssues.length > 0, "detects stale milestone branch");
|
||||
assertEq(staleIssues[0]?.unitId, "M001", "stale branch unitId is M001");
|
||||
|
||||
const fixed = await runGSDDoctor(dir, { fix: true });
|
||||
assertTrue(fixed.fixesApplied.some(f => f.includes("deleted stale branch")), "fix deletes stale branch");
|
||||
|
||||
// Verify branch is gone
|
||||
const branches = run("git branch --list 'milestone/*'", dir);
|
||||
assertTrue(!branches.includes("milestone/M001"), "branch gone after fix");
|
||||
}
|
||||
|
||||
// ─── Test 3: Corrupt merge state detection & fix ───────────────────
|
||||
console.log("\n=== corrupt_merge_state ===");
|
||||
{
|
||||
const dir = createRepoWithCompletedMilestone();
|
||||
cleanups.push(dir);
|
||||
|
||||
// Inject MERGE_HEAD into .git
|
||||
const headHash = run("git rev-parse HEAD", dir);
|
||||
writeFileSync(join(dir, ".git", "MERGE_HEAD"), headHash + "\n");
|
||||
|
||||
const detect = await runGSDDoctor(dir);
|
||||
const mergeIssues = detect.issues.filter(i => i.code === "corrupt_merge_state");
|
||||
assertTrue(mergeIssues.length > 0, "detects corrupt merge state");
|
||||
|
||||
const fixed = await runGSDDoctor(dir, { fix: true });
|
||||
assertTrue(fixed.fixesApplied.some(f => f.includes("cleaned merge state")), "fix cleans merge state");
|
||||
|
||||
// Verify MERGE_HEAD is gone
|
||||
assertTrue(!existsSync(join(dir, ".git", "MERGE_HEAD")), "MERGE_HEAD removed after fix");
|
||||
}
|
||||
|
||||
// ─── Test 4: Tracked runtime files detection & fix ─────────────────
|
||||
console.log("\n=== tracked_runtime_files ===");
|
||||
{
|
||||
const dir = createRepoWithCompletedMilestone();
|
||||
cleanups.push(dir);
|
||||
|
||||
// Force-add a runtime file
|
||||
const activityDir = join(dir, ".gsd", "activity");
|
||||
mkdirSync(activityDir, { recursive: true });
|
||||
writeFileSync(join(activityDir, "test.log"), "log data\n");
|
||||
run("git add -f .gsd/activity/test.log", dir);
|
||||
run("git commit -m 'track runtime file'", dir);
|
||||
|
||||
const detect = await runGSDDoctor(dir);
|
||||
const trackedIssues = detect.issues.filter(i => i.code === "tracked_runtime_files");
|
||||
assertTrue(trackedIssues.length > 0, "detects tracked runtime files");
|
||||
|
||||
const fixed = await runGSDDoctor(dir, { fix: true });
|
||||
assertTrue(fixed.fixesApplied.some(f => f.includes("untracked")), "fix untracks runtime files");
|
||||
|
||||
// Verify file is no longer tracked
|
||||
const tracked = run("git ls-files .gsd/activity/", dir);
|
||||
assertEq(tracked, "", "runtime file untracked after fix");
|
||||
}
|
||||
|
||||
// ─── Test 5: Non-git directory — graceful degradation ──────────────
|
||||
console.log("\n=== non-git directory ===");
|
||||
{
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-git-test-")));
|
||||
cleanups.push(dir);
|
||||
|
||||
// Create minimal .gsd structure (no git)
|
||||
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
||||
|
||||
const result = await runGSDDoctor(dir);
|
||||
const gitIssues = result.issues.filter(i =>
|
||||
["orphaned_auto_worktree", "stale_milestone_branch", "corrupt_merge_state", "tracked_runtime_files"].includes(i.code)
|
||||
);
|
||||
assertEq(gitIssues.length, 0, "no git issues in non-git directory");
|
||||
// Should not throw — reaching here means no crash
|
||||
assertTrue(true, "non-git directory does not crash");
|
||||
}
|
||||
|
||||
// ─── Test 6: Active worktree NOT flagged (false positive prevention) ─
|
||||
console.log("\n=== active worktree safety ===");
|
||||
{
|
||||
const dir = createRepoWithActiveMilestone();
|
||||
cleanups.push(dir);
|
||||
|
||||
// Create worktree for in-progress milestone under .gsd/worktrees/
|
||||
mkdirSync(join(dir, ".gsd", "worktrees"), { recursive: true });
|
||||
run("git worktree add -b milestone/M001 .gsd/worktrees/M001", dir);
|
||||
|
||||
const detect = await runGSDDoctor(dir);
|
||||
const orphanIssues = detect.issues.filter(i => i.code === "orphaned_auto_worktree");
|
||||
assertEq(orphanIssues.length, 0, "active worktree NOT flagged as orphaned");
|
||||
}
|
||||
|
||||
} finally {
|
||||
for (const dir of cleanups) {
|
||||
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
report();
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Reference in a new issue