feat(M003/S04): worktree-aware merge + isolation preferences

This commit is contained in:
Lex Christopherson 2026-03-14 23:20:30 -06:00
parent 84b6f80399
commit 4d60b49f25
16 changed files with 792 additions and 34 deletions

View file

@ -47,3 +47,5 @@
| D039 | M003/S03 | bugfix | Nothing-to-commit detection in mergeMilestoneToMain | Check err.stdout/stderr properties, not just err.message | Node's execSync wraps the error; err.message contains Node's wrapper text, not git's output. The actual "nothing to commit" text is in err.stdout. | No |
| D040 | M003/S03 | bugfix | Worktree removal before branch deletion in mergeMilestoneToMain | Swap ordering: removeWorktree first, then git branch -D | Git refuses to delete a branch checked out in a worktree. Must remove worktree first to unlock the ref. | No |
| D041 | M003/S03 | pattern | JSON.stringify for git commit message escaping | Use JSON.stringify to wrap commit message in git commit -m | Handles special characters (quotes, newlines) safely without shell escaping bugs. | No |
| D042 | M003/S04 | pattern | shouldUseWorktreeIsolation override parameter | Accept optional overridePrefs for testability | loadEffectiveGSDPreferences computes PROJECT_PREFERENCES_PATH at module load time from process.cwd(). chdir-based test fixtures cannot influence it. Override parameter enables reliable testing. | Yes — if preference loading becomes dynamic |
| D043 | M003/S04 | pattern | validatePreferences exported | Export from preferences.ts for direct test access | Was module-private. Tests need to call it directly without full file-loading pipeline. No downstream consumers affected. | No |

View file

@ -41,4 +41,4 @@ See `.gsd/REQUIREMENTS.md` for the explicit capability contract, requirement sta
- [x] M001: Proactive Secret Management — Front-loaded API key collection into planning so auto-mode runs uninterrupted (10 requirements validated)
- [x] M002: Browser Tools Performance & Intelligence — Module decomposition, action pipeline optimization, sharp-based screenshots, form intelligence, intent-ranked retrieval, semantic actions, 108-test suite (12 requirements validated)
- [ ] M003: Worktree-Isolated Git Architecture — S01-S03 complete (worktree lifecycle, --no-ff slice merges, milestone squash-merge to main). S04-S07 remaining.
- [ ] M003: Worktree-Isolated Git Architecture — S01-S04 complete (worktree lifecycle, --no-ff slice merges, milestone squash-merge, preferences + backwards compat). S05-S07 remaining.

View file

@ -50,24 +50,24 @@ This file is the explicit capability and coverage contract for the project.
### R033 — `git.isolation` preference
- Class: core-capability
- Status: active
- Status: validated
- Description: A `git.isolation` preference with values `"worktree"` (default for new projects) and `"branch"` (legacy model). New projects that have never run GSD default to worktree isolation. Existing projects with an established branch-per-slice history default to branch mode.
- Why it matters: Backwards compatibility — existing projects must not break. New projects get the better model by default.
- Source: user
- Primary owning slice: M003/S04
- Supporting slices: none
- Validation: unmapped
- Validation: Set-based validation in validatePreferences, shouldUseWorktreeIsolation resolver with three-tier resolution (explicit pref > legacy detection > default). 25 test assertions in preferences-git.test.ts and isolation-resolver.test.ts.
- Notes: Detection heuristic: if the project has existing `gsd/*` branches or milestone metadata with integration branch records, it's a legacy project → default to "branch". Otherwise → default to "worktree".
### R034 — `git.merge_to_main` preference
- Class: core-capability
- Status: active
- Status: validated
- Description: A `git.merge_to_main` preference with values `"milestone"` (default) and `"slice"`. In milestone mode, main only receives commits when milestones complete. In slice mode, each completed slice squash-merges to main immediately (current behavior).
- Why it matters: Senior engineers who want frequent integration can opt into slice-level merges. Vibe coders get the cleaner milestone-level default.
- Source: user
- Primary owning slice: M003/S04
- Supporting slices: M003/S03
- Validation: unmapped
- Validation: Set-based validation in validatePreferences, getMergeToMainMode helper, auto.ts merge routing gated behind preference. Tested in preferences-git.test.ts.
- Notes: `merge_to_main: "slice"` with `isolation: "worktree"` is valid — slices squash-merge to main from within the worktree, but the worktree still provides `.gsd/` isolation.
### R035 — Self-healing git repair on failure
@ -530,8 +530,8 @@ This file is the explicit capability and coverage contract for the project.
| R030 | core-capability | active | M003/S03 | M003/S01 | unmapped |
| R031 | core-capability | active | M003/S02 | M003/S01 | unmapped |
| R032 | core-capability | active | M003/S03 | none | unmapped |
| R033 | core-capability | active | M003/S04 | none | unmapped |
| R034 | core-capability | active | M003/S04 | M003/S03 | unmapped |
| R033 | core-capability | validated | M003/S04 | none | Set-based validation, shouldUseWorktreeIsolation resolver, 25 test assertions |
| R034 | core-capability | validated | M003/S04 | M003/S03 | Set-based validation, getMergeToMainMode, auto.ts merge routing gated |
| R035 | core-capability | active | M003/S05 | M003/S01, M003/S02, M003/S03 | unmapped |
| R036 | quality-attribute | active | M003/S02 | M003/S06 | unmapped |
| R037 | primary-user-loop | active | M003/S05 | all M003 | unmapped |
@ -545,9 +545,9 @@ This file is the explicit capability and coverage contract for the project.
## Coverage Summary
- Active requirements: 13
- Mapped to slices: 13
- Validated: 22
- Active requirements: 11
- Mapped to slices: 11
- Validated: 24
- Deferred: 5
- Out of scope: 4
- Unmapped active requirements: 0

View file

@ -62,7 +62,7 @@ This milestone is complete only when all are true:
- [x] **S03: Milestone-to-main squash merge + worktree teardown** `risk:high` `depends:[S01,S02]`
> After this: `complete-milestone` squash-merges the milestone branch to main with a rich commit message listing all slices, removes the worktree, `chdir`s back to the main project root. `git log main` shows one clean commit. Auto-push works if enabled. Verified in temp repo with remote.
- [ ] **S04: Preferences + backwards compatibility** `risk:medium` `depends:[S01]`
- [x] **S04: Preferences + backwards compatibility** `risk:medium` `depends:[S01]`
> After this: `git.isolation: "worktree"` (default for new projects) / `"branch"` (existing projects) and `git.merge_to_main: "milestone"` / `"slice"` preferences are validated and respected. An existing project with `gsd/*` branches defaults to branch mode and works identically to today. Verified by running tests in both modes.
- [ ] **S05: Self-healing git repair** `risk:medium` `depends:[S01,S02,S03]`

View file

@ -0,0 +1,68 @@
# S04: Preferences + backwards compatibility
**Goal:** `git.isolation` and `git.merge_to_main` preferences are validated and respected. Existing branch-per-slice projects auto-detect as `"branch"` mode and work identically. New projects default to `"worktree"`.
**Demo:** Set `git.isolation: "branch"` in preferences → auto-mode skips worktree creation and uses legacy branch-per-slice. Remove the preference on a project with no `gsd/*` branches → auto-mode creates worktrees. Set `git.merge_to_main: "slice"` → slices merge directly to main even in worktree mode.
## Must-Haves
- `git.isolation: "worktree" | "branch"` preference with validation
- `git.merge_to_main: "milestone" | "slice"` preference with validation
- `shouldUseWorktreeIsolation(basePath)` resolver that checks preference then falls back to legacy detection heuristic
- All 3 worktree creation/entry sites in auto.ts gated behind the resolver
- Milestone-to-main merge gated behind `merge_to_main` preference
- `merge_to_main: "slice"` + `isolation: "worktree"` combo works (slices merge to main, not milestone branch)
- Resolve merge conflict markers in auto-worktree.ts inherited from S03 branch merge
## Proof Level
- This slice proves: contract + integration
- Real runtime required: no (preference logic is testable without a real git repo for most paths; legacy detection needs git commands but can use test repos)
- Human/UAT required: no
## Observability / Diagnostics
- `shouldUseWorktreeIsolation()` logs nothing by default -- its resolution is observable through the auto-mode notify messages ("Created auto-worktree" vs normal branch flow).
- When `isolation` or `merge_to_main` preferences are invalid, `validatePreferences()` returns clear error strings in the `errors` array; these surface in the UI during preference loading.
- Legacy detection result (branch-per-slice vs worktree) is implicit in auto-mode behavior: worktree creation messages appear only when resolver returns true.
- Failure path: invalid preference values produce structured error messages matching the pattern `"git.<field> must be one of: <values>"`.
## Verification
- `npx tsc --noEmit` — clean build
- `node --test src/resources/extensions/gsd/tests/preferences-git.test.ts` — validates new preference fields
- `node --test src/resources/extensions/gsd/tests/isolation-resolver.test.ts` — validates shouldUseWorktreeIsolation with preference override, legacy detection, and default
- Grep for `<<<<` in auto-worktree.ts returns 0 matches (conflict markers resolved)
- Verify `validatePreferences({ git: { isolation: "bad" } })` returns error containing "git.isolation" (failure-path check)
## Integration Closure
- Upstream surfaces consumed: `auto-worktree.ts` (S01 lifecycle functions), `auto.ts` (S01/S02/S03 worktree wiring), `git-service.ts` (GitPreferences interface), `preferences.ts` (validatePreferences)
- New wiring introduced: `shouldUseWorktreeIsolation()` call at 3 sites in auto.ts, `merge_to_main` check at milestone merge site
- What remains: S05 (self-healing), S06 (doctor/cleanup), S07 (full test suite)
## Tasks
- [x] **T01: Resolve auto-worktree.ts merge conflicts + add preference fields + validation + resolver + gate auto.ts** `est:45m`
- Why: This is a single coherent unit — the interface change, validation, resolver function, and gating are all tightly coupled and small. The merge conflicts must be resolved first since we're editing the same file.
- Files: `src/resources/extensions/gsd/auto-worktree.ts`, `src/resources/extensions/gsd/git-service.ts`, `src/resources/extensions/gsd/preferences.ts`, `src/resources/extensions/gsd/auto.ts`, `src/resources/extensions/gsd/tests/preferences-git.test.ts`, `src/resources/extensions/gsd/tests/isolation-resolver.test.ts`
- Do:
1. Resolve merge conflict markers in `auto-worktree.ts` — keep both sides (HEAD imports + S03's `mergeMilestoneToMain` function and its helpers)
2. Add `isolation?: "worktree" | "branch"` and `merge_to_main?: "milestone" | "slice"` to `GitPreferences` in `git-service.ts`
3. Add validation for both fields in `validatePreferences()` in `preferences.ts` following the `merge_strategy` Set pattern
4. Add `shouldUseWorktreeIsolation(basePath: string): boolean` in `auto-worktree.ts` — checks `loadEffectiveGSDPreferences().preferences.git.isolation`, falls back to legacy detection (`git branch --list 'gsd/*/*'` returns branches → `false`, otherwise → `true`)
5. Gate the 3 worktree sites in `auto.ts` (fresh start ~785, resume ~620, milestone merge ~1735) behind `shouldUseWorktreeIsolation()`
6. For `merge_to_main: "slice"` + worktree mode: override `isInAutoWorktree()` merge routing at lines ~558 and ~1603 to use `mergeSliceToMain` instead of `mergeSliceToMilestone`
7. Write test file `preferences-git.test.ts` — validates new fields accept valid values, reject invalid, and pass through undefined
8. Write test file `isolation-resolver.test.ts` — tests shouldUseWorktreeIsolation with explicit preference, legacy detection, and default behavior
- Verify: `npx tsc --noEmit && node --test src/resources/extensions/gsd/tests/preferences-git.test.ts && node --test src/resources/extensions/gsd/tests/isolation-resolver.test.ts && ! grep -l '<<<<<<' src/resources/extensions/gsd/auto-worktree.ts`
- Done when: Both new preferences validated, resolver returns correct mode for all 3 cases (explicit pref, legacy project, new project), auto.ts gates worktree code behind preference, merge routing respects merge_to_main, all tests pass, no conflict markers remain
## Files Likely Touched
- `src/resources/extensions/gsd/auto-worktree.ts`
- `src/resources/extensions/gsd/git-service.ts`
- `src/resources/extensions/gsd/preferences.ts`
- `src/resources/extensions/gsd/auto.ts`
- `src/resources/extensions/gsd/tests/preferences-git.test.ts`
- `src/resources/extensions/gsd/tests/isolation-resolver.test.ts`

View file

@ -0,0 +1,66 @@
# S04: Preferences + backwards compatibility — Research
**Date:** 2026-03-14
## Summary
This slice adds two new git preferences (`git.isolation` and `git.merge_to_main`) and gates all worktree-mode code behind them. The codebase is well-structured for this: `GitPreferences` interface in `git-service.ts` already has 9 fields, `validatePreferences()` in `preferences.ts` already validates each field with error messages, and `auto.ts` already uses `isInAutoWorktree()` to branch between worktree and legacy merge paths. The main work is: (1) extend the interface, (2) add validation, (3) add a `shouldUseWorktreeIsolation()` resolver with legacy detection heuristic, (4) gate worktree creation/entry in auto.ts behind the preference, (5) gate milestone-to-main merge behind `merge_to_main`.
The legacy detection heuristic is straightforward: if the repo has `gsd/*/*` branches (checked via `git branch --list 'gsd/*/*'`), it's a legacy project → default to `"branch"`. Otherwise → default to `"worktree"`. This aligns with D033.
## Recommendation
Implement in this order:
1. Add `isolation` and `merge_to_main` to `GitPreferences` interface
2. Add validation in `validatePreferences()` following the existing pattern (Set of valid values, string check, cast)
3. Add `shouldUseWorktreeIsolation(basePath)` function in `auto-worktree.ts` — resolves effective mode from preference + legacy detection
4. Gate the 3 worktree creation/entry sites in `auto.ts` (lines ~785-800, ~620-637, ~794) behind `shouldUseWorktreeIsolation()`
5. Gate `mergeMilestoneToMain` call (line ~1739) behind `merge_to_main` preference
6. Ensure `isInAutoWorktree()` branch checks in merge paths (lines ~558, ~1603) continue working — they already handle both modes correctly since they check runtime state, not preference
## Don't Hand-Roll
| Problem | Existing Solution | Why Use It |
|---------|------------------|------------|
| Preference validation | `validatePreferences()` in preferences.ts | Established pattern with error accumulation, type narrowing, and Set-based enum validation |
| Preference loading | `loadEffectiveGSDPreferences()` | Already merges global + project prefs with override semantics |
| Legacy branch detection | `git branch --list 'gsd/*/*'` | Already used in `mergeOrphanedSliceBranches()` at auto.ts:506 |
| Worktree state detection | `isInAutoWorktree()` | Already gates merge strategy selection at runtime |
## Existing Code and Patterns
- `src/resources/extensions/gsd/git-service.ts:31-39``GitPreferences` interface. Add `isolation?: "worktree" | "branch"` and `merge_to_main?: "milestone" | "slice"` here.
- `src/resources/extensions/gsd/preferences.ts:860-912` — git preference validation block. Follow the `merge_strategy` pattern (Set + string check + cast) for new fields.
- `src/resources/extensions/gsd/auto.ts:558,1603``isInAutoWorktree(base)` already gates merge strategy at runtime. These don't need preference changes — they check actual worktree state.
- `src/resources/extensions/gsd/auto.ts:785-800` — worktree creation/entry on fresh milestone start. Gate with `shouldUseWorktreeIsolation()`.
- `src/resources/extensions/gsd/auto.ts:620-637` — worktree re-entry on resume. Gate with same check.
- `src/resources/extensions/gsd/auto.ts:1739``mergeMilestoneToMain()` call. Gate with `merge_to_main` preference.
- `src/resources/extensions/gsd/auto.ts:506``git branch --list 'gsd/*/*'` already used for orphan detection. Reuse same pattern for legacy detection.
## Constraints
- `GitPreferences` is exported from `git-service.ts` and imported by `preferences.ts` — the interface lives in git-service, validation lives in preferences. Follow this split.
- `shouldUseWorktreeIsolation()` needs both the preference value AND a basePath for legacy detection. It should live in `auto-worktree.ts` since that module owns worktree lifecycle.
- The `merge_to_main: "slice"` + `isolation: "worktree"` combination is valid per R034 — slices squash-merge to main from within worktree. The existing `mergeSliceToMain()` path handles this.
- Existing `merge_strategy` preference ("squash" | "merge") is per-slice merge strategy, separate from the new `merge_to_main` preference. Don't confuse them.
## Common Pitfalls
- **Gating resume path but not fresh-start path** — Both auto.ts:785-800 (fresh start) AND auto.ts:620-637 (resume) must be gated. Missing either causes inconsistent behavior.
- **Legacy detection on worktree basePath** — Legacy branch detection (`git branch --list 'gsd/*/*'`) must run against the main repo, not a worktree path. Use `originalBasePath` if available, fall back to `basePath`.
- **merge_to_main: "slice" in worktree mode** — When `isolation: "worktree"` + `merge_to_main: "slice"`, the slice merge path at auto.ts:1603 should use `mergeSliceToMain` (not `mergeSliceToMilestone`). Currently `isInAutoWorktree()` gates this — need to override when `merge_to_main: "slice"`.
- **Preference loading timing**`loadEffectiveGSDPreferences()` reads from disk. In worktree mode, `.gsd/preferences.md` might not exist in the worktree. Preference loading should happen BEFORE entering the worktree, or fall back to the main tree's preferences.
## Open Risks
- The `merge_to_main: "slice"` + `isolation: "worktree"` combination needs the slice merge to go to main, not the milestone branch. This means `isInAutoWorktree()` alone is no longer sufficient to decide merge target — the preference must also be consulted. This is a behavioral change in the merge decision logic.
## Skills Discovered
| Technology | Skill | Status |
|------------|-------|--------|
| Git worktrees | N/A | No relevant skill — this is internal architecture |
## Sources
- All findings from direct codebase exploration of preferences.ts, git-service.ts, auto.ts, and auto-worktree.ts

View file

@ -0,0 +1,117 @@
---
id: S04
parent: M003
milestone: M003
provides:
- git.isolation preference ("worktree" | "branch") with validation
- git.merge_to_main preference ("milestone" | "slice") with validation
- shouldUseWorktreeIsolation resolver with legacy detection heuristic
- getMergeToMainMode helper
- All worktree/merge sites in auto.ts gated behind preferences
requires:
- slice: S01
provides: auto-worktree lifecycle functions (createAutoWorktree, enterAutoWorktree, isInAutoWorktree)
affects:
- S05
- S06
- S07
key_files:
- src/resources/extensions/gsd/git-service.ts
- src/resources/extensions/gsd/preferences.ts
- src/resources/extensions/gsd/auto-worktree.ts
- src/resources/extensions/gsd/auto.ts
- src/resources/extensions/gsd/tests/preferences-git.test.ts
- src/resources/extensions/gsd/tests/isolation-resolver.test.ts
key_decisions:
- D042: shouldUseWorktreeIsolation accepts optional overridePrefs for testability
- D043: validatePreferences exported for direct test access
patterns_established:
- Set-based validation pattern extended for isolation and merge_to_main fields
- Preference override parameter pattern for functions that load preferences internally
observability_surfaces:
- Preference validation errors as structured strings in errors array
- Worktree vs branch mode observable through auto-mode notify messages
drill_down_paths:
- .gsd/milestones/M003/slices/S04/tasks/T01-SUMMARY.md
duration: 30m
verification_result: passed
completed_at: 2026-03-14
---
# S04: Preferences + backwards compatibility
**Added git.isolation and git.merge_to_main preferences with validation, resolver, and auto.ts gating for full backwards compatibility**
## What Happened
Extended GitPreferences with `isolation` ("worktree" | "branch") and `merge_to_main` ("milestone" | "slice") fields. Added Set-based validation for both in validatePreferences(). Implemented `shouldUseWorktreeIsolation(basePath)` with three-tier resolution: explicit preference → legacy branch detection (gsd/*/* branches) → default to worktree. Added `getMergeToMainMode()` helper.
Gated 5 sites in auto.ts: fresh-start worktree creation, resume worktree re-entry, milestone merge, and two slice merge routing sites. When `merge_to_main: "slice"`, slices merge to main via mergeSliceToMain instead of mergeSliceToMilestone, even in worktree mode.
Resolved 3 merge conflict regions in auto-worktree.ts and 1 in auto.ts from S03 merge. Fixed Unicode characters in JSDoc comments that broke Node's --experimental-strip-types parser.
## Verification
- `npx tsc --noEmit` — zero errors
- `preferences-git.test.ts` — 21 assertions pass (valid/invalid/undefined for both fields, combined)
- `isolation-resolver.test.ts` — 4 assertions pass (default/legacy/explicit worktree/explicit branch)
- `grep '<<<<<<' auto-worktree.ts` — 0 matches (all conflicts resolved)
## Requirements Advanced
- R033 — git.isolation preference implemented with validation and three-tier resolver
- R034 — git.merge_to_main preference implemented with validation and auto.ts merge routing
- R038 — Backwards compatibility ensured: legacy detection defaults existing projects to branch mode
## Requirements Validated
- R033 — git.isolation preference validated: Set-based validation rejects invalid values, resolver correctly handles explicit pref, legacy detection, and default. 25 test assertions cover all paths.
- R034 — git.merge_to_main preference validated: validation rejects invalid values, auto.ts routes slice merges to main or milestone branch based on preference. Tested alongside isolation.
## New Requirements Surfaced
- none
## Requirements Invalidated or Re-scoped
- none
## Deviations
- Exported `validatePreferences` (was module-private) for direct test access — no downstream impact.
- Added `overridePrefs` parameter to `shouldUseWorktreeIsolation` — loadEffectiveGSDPreferences uses module-level path constant, making chdir-based test fixtures unreliable.
- Fixed Unicode characters in JSDoc comments — Node's strip-types parser misinterprets `/*` inside backtick-quoted strings within `/** */` comments.
## Known Limitations
- `auto-worktree.test.ts` (pre-existing) may still have Unicode issues from S03 merge content — not in scope for this slice.
- The legacy detection heuristic (`git branch --list 'gsd/*/*'`) requires git CLI — won't work in environments without git.
## Follow-ups
- S07 should add integration tests verifying the full preference → behavior flow (set isolation: "branch" → confirm no worktree created).
- Other test files may need the same Unicode fix applied in auto-worktree.ts.
## Files Created/Modified
- `src/resources/extensions/gsd/git-service.ts` — added isolation and merge_to_main to GitPreferences
- `src/resources/extensions/gsd/preferences.ts` — added validation for both fields, exported validatePreferences
- `src/resources/extensions/gsd/auto-worktree.ts` — resolved conflicts, added shouldUseWorktreeIsolation + getMergeToMainMode, fixed Unicode
- `src/resources/extensions/gsd/auto.ts` — resolved import conflict, gated 5 worktree/merge sites
- `src/resources/extensions/gsd/tests/preferences-git.test.ts` — new: 21 assertions for git preference validation
- `src/resources/extensions/gsd/tests/isolation-resolver.test.ts` — new: 4 assertions for resolver logic
## Forward Intelligence
### What the next slice should know
- The preference system is fully wired. `shouldUseWorktreeIsolation()` and `getMergeToMainMode()` are the two entry points all downstream code should use.
### What's fragile
- Node's `--experimental-strip-types` chokes on Unicode in JSDoc comments — any new functions with fancy chars in comments will break tests.
### Authoritative diagnostics
- `validatePreferences({ git: { isolation: "bad" } }).errors` — structured error messages for invalid prefs
- Auto-mode notify messages ("Created auto-worktree" vs absence) indicate which mode is active
### What assumptions changed
- None — the plan was accurate.

View file

@ -0,0 +1,109 @@
# S04: Preferences + backwards compatibility — UAT
**Milestone:** M003
**Written:** 2026-03-14
## UAT Type
- UAT mode: artifact-driven
- Why this mode is sufficient: Preferences are configuration logic — validation and routing are fully testable through automated tests and CLI inspection. No live runtime or visual verification needed.
## Preconditions
- Project checked out with current S04 changes
- Node.js available with `--experimental-strip-types` support
- The resolve-ts.mjs loader is present at `src/resources/extensions/gsd/tests/resolve-ts.mjs`
## Smoke Test
Run `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/preferences-git.test.ts` — should show 21 assertions passing.
## Test Cases
### 1. git.isolation accepts valid values
1. Run: `node -e "const {validatePreferences} = require('./dist/preferences.js'); console.log(JSON.stringify(validatePreferences({git:{isolation:'worktree'}})))"`
(Or use the test: `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/preferences-git.test.ts`)
2. **Expected:** errors array is empty, `preferences.git.isolation` is `"worktree"`. Same for `"branch"`.
### 2. git.isolation rejects invalid values
1. Call `validatePreferences({ git: { isolation: "invalid" } })`
2. **Expected:** errors array contains a string mentioning `"isolation"` and listing valid values.
### 3. git.merge_to_main accepts valid values
1. Call `validatePreferences({ git: { merge_to_main: "milestone" } })`
2. **Expected:** errors array is empty, value preserved. Same for `"slice"`.
### 4. git.merge_to_main rejects invalid values
1. Call `validatePreferences({ git: { merge_to_main: "invalid" } })`
2. **Expected:** errors array contains a string mentioning `"merge_to_main"`.
### 5. shouldUseWorktreeIsolation with explicit preference
1. Call `shouldUseWorktreeIsolation("/tmp/test", { git: { isolation: "branch" } })`
2. **Expected:** returns `false`
3. Call with `{ git: { isolation: "worktree" } }`
4. **Expected:** returns `true`
### 6. shouldUseWorktreeIsolation with legacy detection
1. In a git repo with `gsd/M001/S01` branch, call `shouldUseWorktreeIsolation(repoPath)`
2. **Expected:** returns `false` (legacy project detected)
### 7. shouldUseWorktreeIsolation default (new project)
1. In a git repo with no `gsd/*` branches, call `shouldUseWorktreeIsolation(repoPath)`
2. **Expected:** returns `true` (new project defaults to worktree)
### 8. No merge conflict markers remain
1. Run: `grep -c '<<<<<<' src/resources/extensions/gsd/auto-worktree.ts`
2. **Expected:** returns 0
### 9. TypeScript compiles clean
1. Run: `npx tsc --noEmit`
2. **Expected:** zero errors
## Edge Cases
### Both fields invalid simultaneously
1. Call `validatePreferences({ git: { isolation: "bad", merge_to_main: "bad" } })`
2. **Expected:** errors array contains two entries, one for each field.
### Undefined fields pass through
1. Call `validatePreferences({ git: { auto_push: true } })` (no isolation or merge_to_main)
2. **Expected:** errors array is empty, isolation and merge_to_main are undefined.
### Non-string type for preference values
1. Call `validatePreferences({ git: { isolation: 42 } })`
2. **Expected:** errors array is non-empty (rejects non-string types).
## Failure Signals
- Any test assertion failure in preferences-git.test.ts or isolation-resolver.test.ts
- TypeScript compilation errors
- Merge conflict markers (`<<<<<<`) found in auto-worktree.ts
- Auto-mode creating worktrees when `git.isolation: "branch"` is set
## Requirements Proved By This UAT
- R033 — git.isolation preference validated and respected
- R034 — git.merge_to_main preference validated and respected
- R038 — Backwards compatibility: legacy detection defaults existing projects to branch mode
## Not Proven By This UAT
- R038 full integration — running a complete auto-mode session in branch mode vs worktree mode (deferred to S07)
- merge_to_main: "slice" + isolation: "worktree" end-to-end merge behavior (logic is wired but not integration-tested)
## Notes for Tester
- The automated tests are the primary verification. Run them with the resolve-ts loader as shown in the smoke test.
- The shouldUseWorktreeIsolation tests use the `overridePrefs` parameter to avoid filesystem setup for preference loading.

View file

@ -0,0 +1,58 @@
---
estimated_steps: 8
estimated_files: 6
---
# T01: Resolve merge conflicts + add git preferences + resolver + gate auto.ts
**Slice:** S04 — Preferences + backwards compatibility
**Milestone:** M003
## Description
Single task covering the full S04 scope: resolve inherited merge conflicts in auto-worktree.ts, add `git.isolation` and `git.merge_to_main` preferences with validation, implement `shouldUseWorktreeIsolation()` resolver with legacy detection heuristic, gate all worktree creation/entry/merge sites in auto.ts behind the preferences, and write tests proving the contract. This is one task because all pieces are tightly coupled — the interface change, validation, resolver, and gating form a single logical unit with ~8 steps across 6 files.
## Steps
1. Resolve merge conflict markers in `auto-worktree.ts` — accept both HEAD (no new imports) and S03 (mergeMilestoneToMain function + helpers). Verify no `<<<<` markers remain.
2. Add `isolation?: "worktree" | "branch"` and `merge_to_main?: "milestone" | "slice"` to `GitPreferences` interface in `git-service.ts`.
3. Add validation blocks for both new fields in `validatePreferences()` in `preferences.ts`, following the existing `merge_strategy` Set-based pattern.
4. Add `shouldUseWorktreeIsolation(basePath: string): boolean` to `auto-worktree.ts`. Logic: load preferences → if `git.isolation` is set, return it === "worktree" → else run `git branch --list 'gsd/*/*'` → if branches exist, return false (legacy) → else return true (new project default).
5. In `auto.ts` fresh-start (~785) and resume (~620): wrap worktree creation/entry blocks with `if (shouldUseWorktreeIsolation(originalBasePath || base))`.
6. In `auto.ts` milestone merge (~1735): wrap `mergeMilestoneToMain` call with check for `merge_to_main !== "slice"` (skip milestone merge when user wants slice-level merging).
7. In `auto.ts` slice merge routing (~558 and ~1603): when `merge_to_main === "slice"`, force `mergeSliceToMain` path even when `isInAutoWorktree()` is true.
8. Write test files: `preferences-git.test.ts` (validation of new fields) and `isolation-resolver.test.ts` (resolver logic with mocked preferences and git state).
## Must-Haves
- [ ] `GitPreferences` interface extended with both new fields
- [ ] Validation rejects invalid values with clear error messages
- [ ] `shouldUseWorktreeIsolation` checks preference first, then legacy heuristic, then defaults to worktree
- [ ] All 3 worktree sites in auto.ts gated
- [ ] `merge_to_main: "slice"` overrides merge routing even in worktree mode
- [ ] Merge conflicts in auto-worktree.ts fully resolved
- [ ] Tests pass for preference validation and resolver logic
## Verification
- `npx tsc --noEmit` — zero errors
- `node --test src/resources/extensions/gsd/tests/preferences-git.test.ts` — all pass
- `node --test src/resources/extensions/gsd/tests/isolation-resolver.test.ts` — all pass
- `grep -c '<<<<<<' src/resources/extensions/gsd/auto-worktree.ts` returns 0
## Inputs
- `src/resources/extensions/gsd/git-service.ts` — existing `GitPreferences` interface (lines 31-39)
- `src/resources/extensions/gsd/preferences.ts` — existing `validatePreferences()` with Set-based pattern (lines 860-912)
- `src/resources/extensions/gsd/auto-worktree.ts` — S01 lifecycle functions + S03 merge functions (with conflict markers)
- `src/resources/extensions/gsd/auto.ts` — worktree creation/entry at ~785/~620, merge routing at ~558/~1603, milestone merge at ~1735
- S01 summary — `shouldUseWorktreeIsolation` must use `originalBasePath` for legacy detection
## Expected Output
- `src/resources/extensions/gsd/git-service.ts``GitPreferences` with 2 new optional fields
- `src/resources/extensions/gsd/preferences.ts` — 2 new validation blocks
- `src/resources/extensions/gsd/auto-worktree.ts` — conflict-free, with `shouldUseWorktreeIsolation()` exported
- `src/resources/extensions/gsd/auto.ts` — 5 sites gated behind preferences
- `src/resources/extensions/gsd/tests/preferences-git.test.ts` — preference validation tests
- `src/resources/extensions/gsd/tests/isolation-resolver.test.ts` — resolver logic tests

View file

@ -0,0 +1,92 @@
---
id: T01
parent: S04
milestone: M003
provides:
- git.isolation and git.merge_to_main preference validation
- shouldUseWorktreeIsolation resolver with legacy detection
- getMergeToMainMode helper
- All worktree sites in auto.ts gated behind preferences
key_files:
- src/resources/extensions/gsd/git-service.ts
- src/resources/extensions/gsd/preferences.ts
- src/resources/extensions/gsd/auto-worktree.ts
- src/resources/extensions/gsd/auto.ts
- src/resources/extensions/gsd/tests/preferences-git.test.ts
- src/resources/extensions/gsd/tests/isolation-resolver.test.ts
key_decisions:
- shouldUseWorktreeIsolation accepts optional overridePrefs parameter for testability (loadEffectiveGSDPreferences uses module-level cwd constant)
- validatePreferences exported (was private) so tests can call it directly
- Replaced Unicode arrows/dashes in auto-worktree.ts JSDoc comments to fix Node --experimental-strip-types parser
patterns_established:
- Set-based validation pattern extended for isolation and merge_to_main fields
- Preference override parameter pattern for functions that load preferences internally
observability_surfaces:
- Preference validation errors surface as structured strings in errors array
- Worktree creation/skip observable through auto-mode notify messages
duration: 30m
verification_result: passed
completed_at: 2026-03-14
blocker_discovered: false
---
# T01: Resolve merge conflicts + add git preferences + resolver + gate auto.ts
**Added git.isolation and git.merge_to_main preferences with validation, resolver, and gating across all worktree sites in auto.ts**
## What Happened
1. Resolved all merge conflict markers in `auto-worktree.ts` (3 conflict regions from S03 merge) and `auto.ts` (1 conflict in imports). Kept both HEAD and S03 content: imports for `parseRoadmap`/`loadEffectiveGSDPreferences` and the full `mergeMilestoneToMain` function with helpers.
2. Extended `GitPreferences` interface with `isolation?: "worktree" | "branch"` and `merge_to_main?: "milestone" | "slice"`.
3. Added Set-based validation blocks for both new fields in `validatePreferences()`, following the existing `merge_strategy` pattern. Also exported `validatePreferences` (was private) for direct test access.
4. Implemented `shouldUseWorktreeIsolation(basePath, overridePrefs?)` in `auto-worktree.ts` with three-tier resolution: explicit preference > legacy branch detection (`gsd/*/*` branches) > default to worktree. Added `getMergeToMainMode()` helper.
5. Gated 5 sites in `auto.ts`:
- Fresh-start worktree creation (~785): `shouldUseWorktreeIsolation(base)`
- Resume worktree re-entry (~620): `shouldUseWorktreeIsolation(originalBasePath)`
- Milestone merge (~1735): `getMergeToMainMode() === "milestone"`
- Two slice merge routing sites (~558, ~1603): `getMergeToMainMode() !== "slice"` controls whether `mergeSliceToMilestone` or `mergeSliceToMain` is called
6. Fixed Unicode characters (`→`, `—`, backtick-quoted `gsd/*/*`) in JSDoc comments that caused Node's `--experimental-strip-types` parser to fail.
## Verification
- `npx tsc --noEmit` — zero errors
- `node --test preferences-git.test.ts` — 21 assertions, all pass (valid/invalid/undefined for both fields)
- `node --test isolation-resolver.test.ts` — 4 assertions, all pass (default/legacy/explicit worktree/explicit branch)
- `grep -c '<<<<<<' auto-worktree.ts` — returns 0
Slice-level verification status (this is the only task):
- [x] `npx tsc --noEmit` — clean build
- [x] `node --test preferences-git.test.ts` — pass
- [x] `node --test isolation-resolver.test.ts` — pass
- [x] Grep for `<<<<` in auto-worktree.ts — 0 matches
## Diagnostics
- Invalid preference values produce errors matching `"git.<field> must be one of: <values>"` pattern
- Worktree vs branch mode observable through auto-mode notify messages (presence/absence of "Created auto-worktree" or "Entered auto-worktree")
- `shouldUseWorktreeIsolation` can be tested with `overridePrefs` parameter without filesystem setup
## Deviations
- Made `validatePreferences` exported (was module-private) — needed for direct test access without going through the full file-loading pipeline.
- Added `overridePrefs` parameter to `shouldUseWorktreeIsolation``loadEffectiveGSDPreferences` computes paths at module load time from `process.cwd()`, making chdir-based test fixtures unreliable.
- Replaced Unicode box-drawing and arrow characters in auto-worktree.ts JSDoc comments — Node's `--experimental-strip-types` parser incorrectly interprets `/*` inside backtick-quoted strings within `/** */` comments.
## Known Issues
- `auto-worktree.test.ts` (pre-existing, not part of this task) was already broken by S03's merge adding content that triggers the same strip-types parser bug. The Unicode fix in this task only covers auto-worktree.ts; other files may have similar issues.
## Files Created/Modified
- `src/resources/extensions/gsd/git-service.ts` — added isolation and merge_to_main fields to GitPreferences
- `src/resources/extensions/gsd/preferences.ts` — added validation for both new fields, exported validatePreferences
- `src/resources/extensions/gsd/auto-worktree.ts` — resolved conflicts, added shouldUseWorktreeIsolation + getMergeToMainMode, fixed Unicode chars
- `src/resources/extensions/gsd/auto.ts` — resolved import conflict, gated 5 worktree/merge sites behind preferences
- `src/resources/extensions/gsd/tests/preferences-git.test.ts` — new: validates git.isolation and git.merge_to_main preference fields
- `src/resources/extensions/gsd/tests/isolation-resolver.test.ts` — new: validates shouldUseWorktreeIsolation resolver logic
- `.gsd/milestones/M003/slices/S04/S04-PLAN.md` — added observability section, marked T01 done

View file

@ -1,5 +1,5 @@
/**
* GSD Auto-Worktree lifecycle management for auto-mode worktrees.
* GSD Auto-Worktree -- lifecycle management for auto-mode worktrees.
*
* Auto-mode creates worktrees with `milestone/<MID>` branches (distinct from
* manual `/worktree` which uses `worktree/<name>` branches). This module
@ -27,17 +27,54 @@ import {
nativeBranchExists,
nativeCommitCountBetween,
} from "./native-git-bridge.js";
<<<<<<< HEAD
=======
import { parseRoadmap } from "./files.js";
import { loadEffectiveGSDPreferences } from "./preferences.js";
>>>>>>> gsd/M003/S03
// ─── Module State ──────────────────────────────────────────────────────────
/** Original project root before chdir into auto-worktree. */
let originalBase: string | null = null;
// ─── Isolation Resolver ────────────────────────────────────────────────────
/**
* Determine whether auto-mode should use worktree isolation.
*
* Resolution order:
* 1. Explicit git.isolation preference -> return (isolation === "worktree")
* 2. Legacy detection: if gsd branches exist -> return false (branch mode)
* 3. Default: return true (worktree mode for new projects)
*/
export function shouldUseWorktreeIsolation(basePath: string, overridePrefs?: { isolation?: string }): boolean {
const prefs = overridePrefs ?? loadEffectiveGSDPreferences()?.preferences?.git;
if (prefs?.isolation) {
return prefs.isolation === "worktree";
}
// Legacy detection: check for existing gsd/*/* branches (branch-per-slice pattern)
try {
const output = execSync("git branch --list 'gsd/*/*'", {
cwd: basePath,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8",
}).trim();
if (output) return false; // Legacy branch-per-slice project
} catch {
// If git command fails, default to worktree
}
return true; // New project default
}
/**
* Resolve the merge_to_main preference value.
* Returns "milestone" (default) or "slice".
*/
export function getMergeToMainMode(): "milestone" | "slice" {
const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
return prefs?.merge_to_main ?? "milestone";
}
// ─── Git Helpers (local, mirrors worktree-command.ts pattern) ──────────────
function resolveGitHeadPath(dir: string): string | null {
@ -110,7 +147,7 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
originalBase = basePath;
} catch (err) {
// If chdir fails, the worktree was created but we couldn't enter it.
// Don't store originalBase caller can retry or clean up.
// Don't store originalBase -- caller can retry or clean up.
throw new Error(
`Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
);
@ -165,7 +202,7 @@ export function getAutoWorktreePath(basePath: string, milestoneId: string): stri
/**
* Enter an existing auto-worktree (chdir into it, store originalBase).
* Use for resume the worktree already exists from a prior create.
* Use for resume -- the worktree already exists from a prior create.
*
* Atomic: chdir + originalBase update in same try block.
*/
@ -198,7 +235,7 @@ export function getAutoWorktreeOriginalBase(): string | null {
return originalBase;
}
// ─── Merge Slice Milestone ───────────────────────────────────────────────
// ─── Merge Slice -> Milestone ───────────────────────────────────────────────
/**
* Merge a completed slice branch into the milestone branch via `--no-ff`.
@ -322,10 +359,8 @@ export function mergeSliceToMilestone(
deletedBranch,
};
}
<<<<<<< HEAD
=======
// ─── Merge Milestone Main ───────────────────────────────────────────────
// ─── Merge Milestone -> Main ───────────────────────────────────────────────
/**
* Auto-commit any dirty (uncommitted) state in the given directory.
@ -435,7 +470,7 @@ export function mergeMilestoneToMain(
} catch (innerErr) {
if (innerErr instanceof MergeConflictError) throw innerErr;
}
// Possibly "already up to date" fall through to commit which will handle nothing-to-commit
// Possibly "already up to date" -- fall through to commit which will handle nothing-to-commit
}
// 8. Commit (handle nothing-to-commit gracefully)
@ -447,7 +482,7 @@ export function mergeMilestoneToMain(
encoding: "utf-8",
});
} catch (err: unknown) {
// execSync errors have stdout/stderr as properties check those for git's message
// execSync errors have stdout/stderr as properties -- check those for git's message
const errObj = err as { stdout?: string; stderr?: string; message?: string };
const combined = [errObj.stdout, errObj.stderr, errObj.message].filter(Boolean).join(" ");
if (combined.includes("nothing to commit") || combined.includes("nothing added to commit") || combined.includes("no changes added")) {
@ -477,7 +512,7 @@ export function mergeMilestoneToMain(
try {
removeWorktree(originalBasePath_, milestoneId, { branch: null as unknown as string, deleteBranch: false });
} catch {
// Best-effort worktree dir may already be gone
// Best-effort -- worktree dir may already be gone
}
// 11. Delete milestone branch (after worktree removal so ref is unlocked)
@ -497,4 +532,3 @@ export function mergeMilestoneToMain(
return { commitMessage, pushed };
}
>>>>>>> gsd/M003/S03

View file

@ -94,10 +94,9 @@ import {
getAutoWorktreePath,
getAutoWorktreeOriginalBase,
mergeSliceToMilestone,
<<<<<<< HEAD
=======
mergeMilestoneToMain,
>>>>>>> gsd/M003/S03
shouldUseWorktreeIsolation,
getMergeToMainMode,
} from "./auto-worktree.js";
import type { GitPreferences } from "./git-service.js";
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
@ -562,7 +561,7 @@ async function mergeOrphanedSliceBranches(
);
try {
let mergeResult;
if (isInAutoWorktree(base)) {
if (isInAutoWorktree(base) && getMergeToMainMode() !== "slice") {
mergeResult = mergeSliceToMilestone(
base, milestoneId, sliceId, sliceEntry.title || sliceId,
);
@ -625,7 +624,7 @@ export async function startAuto(
if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId);
// ── Auto-worktree: re-enter worktree on resume if not already inside ──
if (currentMilestoneId && originalBasePath && !isInAutoWorktree(basePath)) {
if (currentMilestoneId && originalBasePath && !isInAutoWorktree(basePath) && shouldUseWorktreeIsolation(originalBasePath)) {
try {
const existingWtPath = getAutoWorktreePath(originalBasePath, currentMilestoneId);
if (existingWtPath) {
@ -789,7 +788,7 @@ export async function startAuto(
// ── Auto-worktree: create or enter worktree for the active milestone ──
// Store the original project root before any chdir so we can restore on stop.
originalBasePath = base;
if (currentMilestoneId) {
if (currentMilestoneId && shouldUseWorktreeIsolation(base)) {
try {
const existingWtPath = getAutoWorktreePath(base, currentMilestoneId);
if (existingWtPath) {
@ -1631,7 +1630,7 @@ async function dispatchNextUnit(
try {
const sliceTitleForMerge = sliceEntry.title || branchSid;
let mergeResult;
if (isInAutoWorktree(basePath)) {
if (isInAutoWorktree(basePath) && getMergeToMainMode() !== "slice") {
mergeResult = mergeSliceToMilestone(
basePath, branchMid, branchSid, sliceTitleForMerge,
);
@ -1763,7 +1762,7 @@ async function dispatchNextUnit(
} catch { /* non-fatal */ }
// ── Milestone merge: squash-merge milestone branch to main before stopping ──
if (currentMilestoneId && isInAutoWorktree(basePath) && originalBasePath) {
if (currentMilestoneId && isInAutoWorktree(basePath) && originalBasePath && getMergeToMainMode() === "milestone") {
try {
const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
const roadmapContent = readFileSync(roadmapPath, "utf-8");

View file

@ -37,6 +37,8 @@ export interface GitPreferences {
commit_type?: string;
main_branch?: string;
merge_strategy?: "squash" | "merge";
isolation?: "worktree" | "branch";
merge_to_main?: "milestone" | "slice";
}
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;

View file

@ -634,7 +634,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
};
}
function validatePreferences(preferences: GSDPreferences): {
export function validatePreferences(preferences: GSDPreferences): {
preferences: GSDPreferences;
errors: string[];
} {
@ -905,6 +905,22 @@ function validatePreferences(preferences: GSDPreferences): {
errors.push("git.main_branch must be a valid branch name (alphanumeric, _, -, /, .)");
}
}
if (g.isolation !== undefined) {
const validIsolation = new Set(["worktree", "branch"]);
if (typeof g.isolation === "string" && validIsolation.has(g.isolation)) {
git.isolation = g.isolation as "worktree" | "branch";
} else {
errors.push("git.isolation must be one of: worktree, branch");
}
}
if (g.merge_to_main !== undefined) {
const validMergeToMain = new Set(["milestone", "slice"]);
if (typeof g.merge_to_main === "string" && validMergeToMain.has(g.merge_to_main)) {
git.merge_to_main = g.merge_to_main as "milestone" | "slice";
} else {
errors.push("git.merge_to_main must be one of: milestone, slice");
}
}
if (Object.keys(git).length > 0) {
validated.git = git as GitPreferences;

View file

@ -0,0 +1,107 @@
/**
* isolation-resolver.test.ts -- Tests for shouldUseWorktreeIsolation resolver.
*
* Tests three resolution paths:
* 1. Explicit git.isolation preference overrides everything
* 2. Legacy detection: existing gsd/*\/* branches = branch mode
* 3. Default: new project = worktree mode
*/
import { mkdtempSync, writeFileSync, rmSync, realpathSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { shouldUseWorktreeIsolation } from "../auto-worktree.ts";
import { createTestContext } from "./test-helpers.ts";
const { assertEq, report } = createTestContext();
function run(command: string, cwd: string): string {
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
}
function createTempRepo(): string {
const dir = realpathSync(mkdtempSync(join(tmpdir(), "iso-resolver-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);
return dir;
}
async function main(): Promise<void> {
const savedCwd = process.cwd();
console.log("\n=== shouldUseWorktreeIsolation ===");
// Test 1: New project with no gsd branches → defaults to worktree (true)
{
const dir = createTempRepo();
try {
const result = shouldUseWorktreeIsolation(dir);
assertEq(result, true, "new project defaults to worktree isolation");
} finally {
process.chdir(savedCwd);
rmSync(dir, { recursive: true, force: true });
}
}
// Test 2: Legacy project with gsd/*/* branches → returns false (branch mode)
{
const dir = createTempRepo();
try {
// Create a legacy gsd/*/* branch
run("git checkout -b gsd/M001/S01", dir);
writeFileSync(join(dir, "slice.md"), "# S01\n");
run("git add .", dir);
run("git commit -m 'slice work'", dir);
run("git checkout main", dir);
const result = shouldUseWorktreeIsolation(dir);
assertEq(result, false, "legacy project with gsd branches → branch mode");
} finally {
process.chdir(savedCwd);
rmSync(dir, { recursive: true, force: true });
}
}
// Test 3: Explicit preference override -- isolation: "worktree"
{
const dir = createTempRepo();
try {
// Create legacy branches that would normally trigger branch mode
run("git checkout -b gsd/M001/S01", dir);
writeFileSync(join(dir, "slice.md"), "# S01\n");
run("git add .", dir);
run("git commit -m 'slice work'", dir);
run("git checkout main", dir);
const result = shouldUseWorktreeIsolation(dir, { isolation: "worktree" });
assertEq(result, true, "explicit isolation: worktree overrides legacy detection");
} finally {
process.chdir(savedCwd);
rmSync(dir, { recursive: true, force: true });
}
}
// Test 4: Explicit preference override -- isolation: "branch"
{
const dir = createTempRepo();
try {
// No legacy branches -- would normally default to worktree
const result = shouldUseWorktreeIsolation(dir, { isolation: "branch" });
assertEq(result, false, "explicit isolation: branch overrides default");
} finally {
process.chdir(savedCwd);
rmSync(dir, { recursive: true, force: true });
}
}
report();
}
main();

View file

@ -0,0 +1,88 @@
/**
* preferences-git.test.ts Validates git.isolation and git.merge_to_main preference fields.
*/
import { createTestContext } from "./test-helpers.ts";
import { validatePreferences } from "../preferences.ts";
const { assertEq, assertTrue, report } = createTestContext();
async function main(): Promise<void> {
console.log("\n=== git.isolation validation ===");
// Valid values
{
const { preferences, errors } = validatePreferences({ git: { isolation: "worktree" } });
assertEq(errors.length, 0, "isolation: worktree — no errors");
assertEq(preferences.git?.isolation, "worktree", "isolation: worktree — value preserved");
}
{
const { preferences, errors } = validatePreferences({ git: { isolation: "branch" } });
assertEq(errors.length, 0, "isolation: branch — no errors");
assertEq(preferences.git?.isolation, "branch", "isolation: branch — value preserved");
}
// Invalid values
{
const { errors } = validatePreferences({ git: { isolation: "invalid" } });
assertTrue(errors.length > 0, "isolation: invalid — produces error");
assertTrue(errors[0].includes("isolation"), "isolation: invalid — error mentions isolation");
}
{
const { errors } = validatePreferences({ git: { isolation: 42 } });
assertTrue(errors.length > 0, "isolation: number — produces error");
}
// Undefined passes through
{
const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
assertEq(errors.length, 0, "isolation: undefined — no errors");
assertEq(preferences.git?.isolation, undefined, "isolation: undefined — not set");
}
console.log("\n=== git.merge_to_main validation ===");
// Valid values
{
const { preferences, errors } = validatePreferences({ git: { merge_to_main: "milestone" } });
assertEq(errors.length, 0, "merge_to_main: milestone — no errors");
assertEq(preferences.git?.merge_to_main, "milestone", "merge_to_main: milestone — value preserved");
}
{
const { preferences, errors } = validatePreferences({ git: { merge_to_main: "slice" } });
assertEq(errors.length, 0, "merge_to_main: slice — no errors");
assertEq(preferences.git?.merge_to_main, "slice", "merge_to_main: slice — value preserved");
}
// Invalid values
{
const { errors } = validatePreferences({ git: { merge_to_main: "invalid" } });
assertTrue(errors.length > 0, "merge_to_main: invalid — produces error");
assertTrue(errors[0].includes("merge_to_main"), "merge_to_main: invalid — error mentions merge_to_main");
}
{
const { errors } = validatePreferences({ git: { merge_to_main: false } });
assertTrue(errors.length > 0, "merge_to_main: boolean — produces error");
}
// Undefined passes through
{
const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
assertEq(errors.length, 0, "merge_to_main: undefined — no errors");
assertEq(preferences.git?.merge_to_main, undefined, "merge_to_main: undefined — not set");
}
console.log("\n=== both fields together ===");
{
const { preferences, errors } = validatePreferences({
git: { isolation: "worktree", merge_to_main: "slice" },
});
assertEq(errors.length, 0, "both fields valid — no errors");
assertEq(preferences.git?.isolation, "worktree", "isolation preserved");
assertEq(preferences.git?.merge_to_main, "slice", "merge_to_main preserved");
}
report();
}
main();