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:
Lex Christopherson 2026-03-14 23:54:13 -06:00
parent 34cd1056ea
commit f9b9f6bf32
14 changed files with 1052 additions and 8 deletions

View file

@ -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

View file

@ -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]`

View 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.

View 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`

View 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)

View 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

View 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).

View 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.

View 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)

View 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.

View 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

View file

@ -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 };

View file

@ -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);

View 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();