feat(M003/S04): worktree-aware merge + isolation preferences
This commit is contained in:
parent
84b6f80399
commit
4d60b49f25
16 changed files with 792 additions and 34 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]`
|
||||
|
|
|
|||
68
.gsd/milestones/M003/slices/S04/S04-PLAN.md
Normal file
68
.gsd/milestones/M003/slices/S04/S04-PLAN.md
Normal 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`
|
||||
66
.gsd/milestones/M003/slices/S04/S04-RESEARCH.md
Normal file
66
.gsd/milestones/M003/slices/S04/S04-RESEARCH.md
Normal 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
|
||||
117
.gsd/milestones/M003/slices/S04/S04-SUMMARY.md
Normal file
117
.gsd/milestones/M003/slices/S04/S04-SUMMARY.md
Normal 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.
|
||||
109
.gsd/milestones/M003/slices/S04/S04-UAT.md
Normal file
109
.gsd/milestones/M003/slices/S04/S04-UAT.md
Normal 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.
|
||||
58
.gsd/milestones/M003/slices/S04/tasks/T01-PLAN.md
Normal file
58
.gsd/milestones/M003/slices/S04/tasks/T01-PLAN.md
Normal 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
|
||||
92
.gsd/milestones/M003/slices/S04/tasks/T01-SUMMARY.md
Normal file
92
.gsd/milestones/M003/slices/S04/tasks/T01-SUMMARY.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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_\-\/.]+$/;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
107
src/resources/extensions/gsd/tests/isolation-resolver.test.ts
Normal file
107
src/resources/extensions/gsd/tests/isolation-resolver.test.ts
Normal 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();
|
||||
88
src/resources/extensions/gsd/tests/preferences-git.test.ts
Normal file
88
src/resources/extensions/gsd/tests/preferences-git.test.ts
Normal 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();
|
||||
Loading…
Add table
Reference in a new issue