From dfe9527641c6b6f21705f0bce012c3736c379246 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Thu, 12 Mar 2026 11:53:48 -0600 Subject: [PATCH] feat(M001/S02): Wire GitService into codebase --- .claude/worktrees/agent-aa26d13d | 1 + .claude/worktrees/agent-af52432f | 1 + .gsd/STATE.md | 18 -- .gsd/milestones/M001/M001-ROADMAP.md | 2 +- .gsd/milestones/M001/slices/S02/S02-PLAN.md | 82 ++++++++ .../M001/slices/S02/tasks/T01-PLAN.md | 66 +++++++ .../M001/slices/S02/tasks/T01-SUMMARY.md | 75 +++++++ .../M001/slices/S02/tasks/T02-PLAN.md | 59 ++++++ .../M001/slices/S02/tasks/T02-SUMMARY.md | 62 ++++++ .../M001/slices/S02/tasks/T03-PLAN.md | 74 +++++++ src/resources/extensions/gsd/auto.ts | 7 +- .../gsd/docs/preferences-reference.md | 27 +++ src/resources/extensions/gsd/preferences.ts | 49 +++++ .../extensions/gsd/templates/preferences.md | 7 + src/resources/extensions/gsd/worktree.ts | 186 ++++-------------- 15 files changed, 548 insertions(+), 168 deletions(-) create mode 160000 .claude/worktrees/agent-aa26d13d create mode 160000 .claude/worktrees/agent-af52432f delete mode 100644 .gsd/STATE.md create mode 100644 .gsd/milestones/M001/slices/S02/S02-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md diff --git a/.claude/worktrees/agent-aa26d13d b/.claude/worktrees/agent-aa26d13d new file mode 160000 index 000000000..afcbdfa95 --- /dev/null +++ b/.claude/worktrees/agent-aa26d13d @@ -0,0 +1 @@ +Subproject commit afcbdfa956c83b3bb6f5476bb02437d9edeeda10 diff --git a/.claude/worktrees/agent-af52432f b/.claude/worktrees/agent-af52432f new file mode 160000 index 000000000..5f7e04025 --- /dev/null +++ b/.claude/worktrees/agent-af52432f @@ -0,0 +1 @@ +Subproject commit 5f7e040254970b356e4e70d9d1a307fe07e2209a diff --git a/.gsd/STATE.md b/.gsd/STATE.md deleted file mode 100644 index 24a1afad9..000000000 --- a/.gsd/STATE.md +++ /dev/null @@ -1,18 +0,0 @@ -# GSD State - -**Active Milestone:** M001 — Deterministic GitService -**Active Slice:** S02 — Wire GitService into codebase -**Phase:** researching -**Requirements Status:** 18 active · 0 validated · 3 deferred · 6 out of scope - -## Milestone Registry -- 🔄 **M001:** Deterministic GitService - -## Recent Decisions -- None recorded - -## Blockers -- None - -## Next Action -Research slice S02 (Wire GitService into codebase). diff --git a/.gsd/milestones/M001/M001-ROADMAP.md b/.gsd/milestones/M001/M001-ROADMAP.md index 6079edb3a..73fce61ef 100644 --- a/.gsd/milestones/M001/M001-ROADMAP.md +++ b/.gsd/milestones/M001/M001-ROADMAP.md @@ -55,7 +55,7 @@ This milestone is complete only when all are true: - [x] **S01: GitService core implementation** `risk:high` `depends:[]` > After this: `git-service.ts` exists with commit, autoCommit, ensureSliceBranch, switchToMain, mergeSliceToMain, inferCommitType, smart staging — all passing unit tests in temp git repos. -- [ ] **S02: Wire GitService into codebase** `risk:high` `depends:[S01]` +- [x] **S02: Wire GitService into codebase** `risk:high` `depends:[S01]` > After this: auto.ts and worktree.ts delegate to GitService. Git preferences schema added to preferences.ts. `npm run build` passes. Existing worktree tests still pass. - [ ] **S03: Bug fixes and doc corrections** `risk:medium` `depends:[S02]` diff --git a/.gsd/milestones/M001/slices/S02/S02-PLAN.md b/.gsd/milestones/M001/slices/S02/S02-PLAN.md new file mode 100644 index 000000000..0812be9eb --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/S02-PLAN.md @@ -0,0 +1,82 @@ +# S02: Wire GitService into codebase + +**Goal:** All git-mutation functions in `worktree.ts` delegate to `GitServiceImpl`. `auto.ts` creates a `GitServiceImpl` instance and routes its git calls through it. `preferences.ts` exposes `git?: GitPreferences` with validation, merge, and documentation. + +**Demo:** `npm run build` passes. All three test suites pass: `worktree.test.ts`, `worktree-integration.test.ts`, `git-service.test.ts`. All 6+ consumers of `worktree.ts` continue to import and call functions without changes. + +## Must-Haves + +- All 10 `worktree.ts` exports preserved with identical signatures +- `worktree.ts` git-mutation functions (`getMainBranch`, `getCurrentBranch`, `ensureSliceBranch`, `autoCommitCurrentBranch`, `switchToMain`, `mergeSliceToMain`) delegate to `GitServiceImpl` internally +- `MergeSliceResult` re-exported via `export type` from `git-service.ts` (eliminate type duplication per D014) +- No circular dependency crash at module-evaluation time between `worktree.ts` and `git-service.ts` +- `auto.ts` creates `GitServiceImpl` instance with `basePath` and git preferences after `basePath` is set +- `auto.ts` callsites for `autoCommitCurrentBranch`, `ensureSliceBranch`, `switchToMain`, `mergeSliceToMain` route through GitService (via worktree.ts facade) +- `GSDPreferences` interface includes `git?: GitPreferences` field +- `validatePreferences()` validates git sub-fields +- `mergePreferences()` merges git preferences with override semantics +- `templates/preferences.md` documents git section +- `docs/preferences-reference.md` documents git preferences +- Existing tests pass without regressions + +## Proof Level + +- This slice proves: integration +- Real runtime required: no (existing test suites with temp git repos exercise the full contract) +- Human/UAT required: no + +## Verification + +- `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/worktree.test.ts` — all pass +- `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/worktree-integration.test.ts` — all pass +- `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/git-service.test.ts` — all pass +- `npx tsc --noEmit` — clean (no errors) +- `npm run test` — same pass/fail as baseline (116 pass, 2 pre-existing failures) +- `grep -n 'import.*from.*worktree' src/resources/extensions/gsd/state.ts src/resources/extensions/gsd/workspace-index.ts src/resources/extensions/gsd/worktree-command.ts` — unchanged, still importing from worktree.ts + +## Observability / Diagnostics + +- Runtime signals: `mergeSliceToMain` now returns commit messages with inferred conventional types (via `inferCommitType`) instead of hardcoded `feat`. `console.error` warning from `smartStage()` fallback signals exclusion failure. +- Inspection surfaces: Re-run any of the three test suites to verify delegation is working. `git log --oneline` after merge shows inferred commit types. +- Failure visibility: `runGit` errors include full command, basePath, and stderr. Lazy `GitServiceImpl` construction errors surface at first call, not module load. +- Redaction constraints: none (no secrets involved) + +## Integration Closure + +- Upstream surfaces consumed: `git-service.ts` → `GitServiceImpl`, `GitPreferences`, `MergeSliceResult`, all public methods (from S01) +- New wiring introduced in this slice: `worktree.ts` facade delegates to `GitServiceImpl`; `auto.ts` creates `GitServiceImpl` instance from `basePath` + preferences; `preferences.ts` exposes `git` field in `GSDPreferences` +- What remains before the milestone is truly usable end-to-end: S03 (bug fixes), S04 (remove git from prompts), S05 (enhanced features), S06 (cleanup/archive) + +## Tasks + +- [x] **T01: Convert worktree.ts to thin facade delegating to GitServiceImpl** `est:45m` + - Why: Core integration task — all 6+ consumers depend on worktree.ts exports being stable while internals switch to GitServiceImpl. Satisfies R005 (facade delegation) and R009 (inferCommitType replaces hardcoded feat). + - Files: `src/resources/extensions/gsd/worktree.ts`, `src/resources/extensions/gsd/tests/worktree.test.ts`, `src/resources/extensions/gsd/tests/worktree-integration.test.ts` + - Do: Import `GitServiceImpl`, `GitPreferences`, and re-export `MergeSliceResult` via `export type` (per D014) from `git-service.ts`. Add lazy `GitServiceImpl` cache with basePath-change guard. Convert 6 git-mutation functions to delegate. Remove unused private `branchExists` and `runGit`. Keep 4 pure functions and query functions unchanged. Run tests and fix any assertion mismatches. + - Verify: All three test suites pass. `npx tsc --noEmit` clean. No consumer import changes needed. + - Done when: `worktree.ts` delegates to `GitServiceImpl` for all git-mutation functions, all tests green, build clean. + +- [x] **T02: Wire auto.ts to use GitServiceImpl via worktree.ts facade** `est:30m` + - Why: auto.ts is the primary orchestrator — it must have a `GitServiceImpl` instance for future direct usage and verify all callsites work through the T01 facade. Satisfies R006. + - Files: `src/resources/extensions/gsd/auto.ts` + - Do: Import `GitServiceImpl` and `GitPreferences` from `git-service.ts`. Add module-level `gitService` variable. Initialize after `basePath` is set in `startAutoMode()` using git preferences from `loadEffectiveGSDPreferences()`. Keep bootstrap git calls and idle detection inline per spec. Verify build and tests. + - Verify: `npx tsc --noEmit` clean. `npm run test` same baseline. `grep -c 'new GitServiceImpl' src/resources/extensions/gsd/auto.ts` shows 1. + - Done when: auto.ts has a `GitServiceImpl` instance created from preferences, build clean, tests pass. + +- [x] **T03: Add git preferences to preferences.ts, template, and docs** `est:30m` + - Why: Preferences schema is needed for S05 features (auto_push, merge guards, snapshots) and must exist before those slices. Satisfies R004. + - Files: `src/resources/extensions/gsd/preferences.ts`, `src/resources/extensions/gsd/templates/preferences.md`, `src/resources/extensions/gsd/docs/preferences-reference.md` + - Do: Add `git?: GitPreferences` to `GSDPreferences` interface (import from `git-service.ts`). Add git validation in `validatePreferences()` for all 6 sub-fields. Add git merge in `mergePreferences()` with override-wins semantics. Add `git:` section to preferences template. Document git preferences in reference doc. + - Verify: `npx tsc --noEmit` clean. `npm run test` same baseline. `grep 'git.*GitPreferences' src/resources/extensions/gsd/preferences.ts` confirms field. + - Done when: `GSDPreferences.git` field exists with full validation, merge, template, and reference doc. + +## Files Likely Touched + +- `src/resources/extensions/gsd/worktree.ts` +- `src/resources/extensions/gsd/auto.ts` +- `src/resources/extensions/gsd/preferences.ts` +- `src/resources/extensions/gsd/git-service.ts` (minor — only if re-export adjustments needed) +- `src/resources/extensions/gsd/templates/preferences.md` +- `src/resources/extensions/gsd/docs/preferences-reference.md` +- `src/resources/extensions/gsd/tests/worktree.test.ts` (assertion updates if needed) +- `src/resources/extensions/gsd/tests/worktree-integration.test.ts` (assertion updates if needed) diff --git a/.gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md b/.gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md new file mode 100644 index 000000000..cf8fd35aa --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md @@ -0,0 +1,66 @@ +--- +estimated_steps: 5 +estimated_files: 3 +--- + +# T01: Convert worktree.ts to thin facade delegating to GitServiceImpl + +**Slice:** S02 — Wire GitService into codebase +**Milestone:** M001 + +## Description + +Convert `worktree.ts` from a standalone git-operations module into a thin facade that preserves all 10 exports but delegates git-mutation functions to `GitServiceImpl` from `git-service.ts`. Pure utility functions stay as-is. The `MergeSliceResult` type duplication is eliminated by re-exporting from `git-service.ts` using `export type` per D014. + +The circular dependency between `git-service.ts` (imports pure functions from `worktree.ts`) and `worktree.ts` (imports `GitServiceImpl` from `git-service.ts`) is handled by lazy construction: `GitServiceImpl` is only instantiated inside function bodies at call-time, never at module-evaluation time. The pure functions consumed by `git-service.ts` are available immediately at module evaluation. + +## Steps + +1. **Replace `MergeSliceResult` with type re-export:** Remove the local `MergeSliceResult` interface definition in `worktree.ts` and replace with `export type { MergeSliceResult } from "./git-service.ts"` (per D014 — type-only re-export avoids ESM cycle issues). Verify this doesn't break any consumer. + +2. **Add lazy GitServiceImpl cache:** Import `GitServiceImpl` from `git-service.ts`. Add a `let cachedService: GitServiceImpl | null = null` and a `function getService(basePath: string): GitServiceImpl` that creates or returns the cached instance with `{}` default prefs. Include a basePath-change guard that resets the cache if basePath changes between calls. + +3. **Convert 6 git-mutation functions to delegate:** Replace the bodies of `getMainBranch`, `getCurrentBranch`, `ensureSliceBranch`, `autoCommitCurrentBranch`, `switchToMain`, `mergeSliceToMain` with calls to the corresponding `GitServiceImpl` methods via `getService(basePath)`. Keep all function signatures identical. For `autoCommitCurrentBranch(basePath, unitType, unitId)`, map to `svc.autoCommit(unitType, unitId)`. For `mergeSliceToMain`, the GitServiceImpl version uses `inferCommitType` instead of hardcoded `feat` — this is the intended R009 fix. + +4. **Remove now-unused private code:** Delete the private `branchExists()` function and private `runGit()` function from `worktree.ts` — these are now handled by `GitServiceImpl` and `git-service.ts` respectively. Keep `isOnSliceBranch()` and `getActiveSliceBranch()` as-is (they use `getCurrentBranch` which now delegates). Keep all pure utility functions unchanged: `detectWorktreeName`, `getSliceBranchName`, `SLICE_BRANCH_RE`, `parseSliceBranch`. + +5. **Run all test suites and fix any assertion mismatches:** Run `worktree.test.ts`, `worktree-integration.test.ts`, and `git-service.test.ts`. The worktree tests don't assert on `mergedCommitMessage` format, so the switch from hardcoded `feat(` to `inferCommitType` should be transparent. Verify `npm run build` passes. + +## Must-Haves + +- [ ] All 10 `worktree.ts` exports preserved with identical type signatures +- [ ] `MergeSliceResult` re-exported via `export type` from `git-service.ts` (per D014, no local duplicate) +- [ ] Lazy `GitServiceImpl` construction (call-time, not module-evaluation) +- [ ] BasePath-change guard on cached service +- [ ] No circular dependency crash at import time +- [ ] `worktree.test.ts` passes — all assertions +- [ ] `worktree-integration.test.ts` passes — all assertions +- [ ] `git-service.test.ts` passes — all assertions +- [ ] `npx tsc --noEmit` clean + +## Verification + +- `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/worktree.test.ts` — all pass +- `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/worktree-integration.test.ts` — all pass +- `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/git-service.test.ts` — all pass +- `npx tsc --noEmit` — no errors +- `grep -c 'GitServiceImpl' src/resources/extensions/gsd/worktree.ts` — at least 1 (confirms delegation wiring) +- `grep 'export type.*MergeSliceResult' src/resources/extensions/gsd/worktree.ts` — exactly 1 (type re-export per D014) + +## Observability Impact + +- Signals added/changed: `mergeSliceToMain` now returns commits with inferred conventional types (via `inferCommitType`) instead of hardcoded `feat`. This is an intentional R009 fix, not a regression. +- How a future agent inspects this: `git log --oneline` after merge shows the inferred commit type. Run worktree test suites to verify delegation. +- Failure state exposed: `runGit` errors from `git-service.ts` include command, basePath, and stderr. Lazy init errors surface at first function call, not import time. + +## Inputs + +- `src/resources/extensions/gsd/git-service.ts` — `GitServiceImpl` class, `GitPreferences` interface, `MergeSliceResult` interface (from S01) +- `src/resources/extensions/gsd/worktree.ts` — 10 exports, private `branchExists`, private `runGit`, `MergeSliceResult` duplicate +- D014 — `export type { MergeSliceResult }` decision + +## Expected Output + +- `src/resources/extensions/gsd/worktree.ts` — thin facade: imports `GitServiceImpl`, lazy cache, 6 functions delegate, 4 pure functions unchanged, `MergeSliceResult` type re-exported +- `src/resources/extensions/gsd/tests/worktree.test.ts` — unchanged (no assertion format changes needed) +- `src/resources/extensions/gsd/tests/worktree-integration.test.ts` — unchanged (no assertion format changes needed) diff --git a/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md b/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md new file mode 100644 index 000000000..50c85eaa9 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md @@ -0,0 +1,75 @@ +--- +id: T01 +parent: S02 +milestone: M001 +provides: + - worktree.ts thin facade delegating git-mutation functions to GitServiceImpl + - MergeSliceResult type re-export from git-service.ts (D014) + - Lazy GitServiceImpl cache with basePath-change guard +key_files: + - src/resources/extensions/gsd/worktree.ts +key_decisions: + - Lazy construction via getService() avoids circular dependency crash at module-evaluation time + - isOnSliceBranch and getActiveSliceBranch kept as local functions using getCurrentBranch (which delegates) — no need to route through GitServiceImpl since they're pure regex checks on the branch name +patterns_established: + - Lazy singleton GitServiceImpl cache with basePath-change guard for facade modules +observability_surfaces: + - mergeSliceToMain now returns commits with inferred conventional types via inferCommitType (R009 fix) + - smartStage() console.error fallback warning surfaces exclusion failures + - runGit errors include command, basePath, and stderr +duration: 10m +verification_result: passed +completed_at: 2026-03-12 +blocker_discovered: false +--- + +# T01: Convert worktree.ts to thin facade delegating to GitServiceImpl + +**Converted worktree.ts from standalone git module to thin facade — all 13 exports preserved, 6 git-mutation functions delegate to GitServiceImpl via lazy cache, MergeSliceResult re-exported per D014.** + +## What Happened + +1. Removed local `MergeSliceResult` interface, replaced with `export type { MergeSliceResult } from "./git-service.ts"` (D014 type-only re-export). +2. Added lazy `GitServiceImpl` cache: `cachedService`/`cachedBasePath` module-level variables with `getService(basePath)` factory that resets on basePath change. Construction happens at call-time only, never at module evaluation — avoids circular dependency crash. +3. Converted 6 git-mutation functions to delegate: `getMainBranch`, `getCurrentBranch`, `ensureSliceBranch`, `autoCommitCurrentBranch` (maps to `svc.autoCommit`), `switchToMain`, `mergeSliceToMain`. All function signatures identical. +4. Removed private `runGit()` and `branchExists()` — now handled by `GitServiceImpl` internals. +5. Kept pure utility functions unchanged: `detectWorktreeName`, `getSliceBranchName`, `SLICE_BRANCH_RE`, `parseSliceBranch`. +6. Kept `isOnSliceBranch` and `getActiveSliceBranch` as local functions that use `getCurrentBranch` (which delegates). + +The circular dependency (git-service.ts imports pure functions from worktree.ts, worktree.ts imports GitServiceImpl from git-service.ts) works because GitServiceImpl is only instantiated inside function bodies at call-time, and the pure functions consumed by git-service.ts are available immediately at module evaluation. + +## Verification + +- `worktree.test.ts`: 56 passed, 0 failed ✓ +- `worktree-integration.test.ts`: 40 passed, 0 failed ✓ +- `git-service.test.ts`: 113 passed, 0 failed ✓ +- `npx tsc --noEmit`: clean, no errors ✓ +- `grep -c 'GitServiceImpl' worktree.ts` → 9 (confirms delegation wiring) ✓ +- `grep 'export type.*MergeSliceResult' worktree.ts` → exactly 1 match ✓ +- Consumer imports in state.ts, workspace-index.ts, worktree-command.ts unchanged ✓ + +### Slice-level verification (partial — T01 of 3): +- ✅ worktree.test.ts passes +- ✅ worktree-integration.test.ts passes +- ✅ git-service.test.ts passes +- ✅ npx tsc --noEmit clean +- ⏳ npm run test (full suite) — deferred to final task +- ✅ Consumer imports unchanged + +## Diagnostics + +- Run any of the three test suites to verify delegation is working +- `git log --oneline` after a merge shows inferred commit types (feat/fix/refactor/etc. instead of always feat) +- Lazy init errors surface at first function call, not import time — look for "git ... failed in ..." error messages + +## Deviations + +None. Plan executed as written. + +## Known Issues + +None. + +## Files Created/Modified + +- `src/resources/extensions/gsd/worktree.ts` — converted to thin facade: imports GitServiceImpl, lazy cache, 6 functions delegate, 4 pure functions unchanged, MergeSliceResult type re-exported diff --git a/.gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md b/.gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md new file mode 100644 index 000000000..d9f54af58 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md @@ -0,0 +1,59 @@ +--- +estimated_steps: 4 +estimated_files: 1 +--- + +# T02: Wire auto.ts to use GitServiceImpl via worktree.ts facade + +**Slice:** S02 — Wire GitService into codebase +**Milestone:** M001 + +## Description + +After T01 converts `worktree.ts` to a facade, all auto.ts calls already route through `GitServiceImpl` transparently. This task makes that explicit by creating a `GitServiceImpl` instance in auto.ts (for future direct usage by S05) and verifying all callsites work correctly through the facade. The instance is initialized after `basePath` is set, using git preferences from the preferences system. + +Bootstrap git calls (`git rev-parse --git-dir`, `git init`, `git add -A .gsd .gitignore && git commit`) and idle detection (`git status --porcelain`) remain inline per milestone success criteria. + +## Steps + +1. **Add GitServiceImpl import and module-level variable:** Import `GitServiceImpl` and `GitPreferences` from `./git-service.ts`. Add `let gitService: GitServiceImpl | null = null;` at module level near the existing `let basePath = ""` declaration. + +2. **Initialize GitServiceImpl after basePath is set:** In `startAutoMode()`, after `basePath = base` is set and after the git init/bootstrap block (line ~375), create the instance: `gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {})`. This must happen after the git repo is confirmed to exist. Note: `loadEffectiveGSDPreferences` is already imported in auto.ts. + +3. **Verify all callsites compile and route correctly:** The 8 imports from `worktree.ts` remain unchanged — `autoCommitCurrentBranch`, `ensureSliceBranch`, `getCurrentBranch`, `getMainBranch`, `getSliceBranchName`, `parseSliceBranch`, `switchToMain`, `mergeSliceToMain`. These now delegate through the T01 facade. No call signature changes needed. Remove `getSliceBranchName` from imports if unused (line 65 — imported but not called in auto.ts). + +4. **Verify build and tests pass:** Run `npx tsc --noEmit` and `npm run test` to confirm no regressions. The auto.ts changes are additive — existing behavior is preserved. + +## Must-Haves + +- [ ] `GitServiceImpl` imported from `git-service.ts` +- [ ] Module-level `gitService` variable initialized after `basePath` is set +- [ ] Instance created with git preferences from `loadEffectiveGSDPreferences()` +- [ ] Bootstrap git calls remain inline (lines 360-375) +- [ ] Idle detection `git status --porcelain` remains inline (line 2545) +- [ ] `npx tsc --noEmit` clean +- [ ] `npm run test` same baseline (116 pass, 2 pre-existing failures) + +## Verification + +- `npx tsc --noEmit` — no errors +- `npm run test` — same pass/fail as baseline +- `grep -c 'GitServiceImpl' src/resources/extensions/gsd/auto.ts` — at least 1 +- `grep -c 'new GitServiceImpl' src/resources/extensions/gsd/auto.ts` — exactly 1 + +## Observability Impact + +- Signals added/changed: None — this task adds the instance but doesn't change runtime behavior (facade handles delegation) +- How a future agent inspects this: `grep GitServiceImpl src/resources/extensions/gsd/auto.ts` to confirm the instance exists +- Failure state exposed: If preferences loading fails, `?? {}` ensures default empty prefs — no crash + +## Inputs + +- `src/resources/extensions/gsd/auto.ts` — orchestrator with 8 worktree.ts imports and git callsites +- `src/resources/extensions/gsd/worktree.ts` — T01 facade (all function calls now route to GitServiceImpl) +- `src/resources/extensions/gsd/git-service.ts` — `GitServiceImpl` constructor takes `(basePath, prefs?)` +- T01 completion — facade must be in place before this task verifies routing + +## Expected Output + +- `src/resources/extensions/gsd/auto.ts` — added `GitServiceImpl` import, module-level `gitService` variable, initialization in `startAutoMode()` after basePath is set diff --git a/.gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md b/.gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md new file mode 100644 index 000000000..88bde79a5 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md @@ -0,0 +1,62 @@ +--- +id: T02 +parent: S02 +milestone: M001 +provides: + - GitServiceImpl instance in auto.ts initialized from basePath + git preferences + - Removed unused getSliceBranchName import from auto.ts +key_files: + - src/resources/extensions/gsd/auto.ts +key_decisions: + - Initialize gitService after bootstrap block (git init + .gsd mkdir) but before crash-lock check — ensures git repo exists + - Use `loadEffectiveGSDPreferences()?.preferences?.git ?? {}` for safe fallback to empty prefs +patterns_established: + - Module-level `gitService` variable with post-init construction in startAutoMode() +observability_surfaces: + - If preferences loading fails, `?? {}` ensures default empty prefs — no crash. gitService null before startAutoMode() runs. +duration: 8m +verification_result: passed +completed_at: 2026-03-12 +blocker_discovered: false +--- + +# T02: Wire auto.ts to use GitServiceImpl via worktree.ts facade + +**Added GitServiceImpl instance in auto.ts, initialized after basePath is set with git preferences from the preferences system. Removed unused `getSliceBranchName` import.** + +## What Happened + +Three changes to `auto.ts`: +1. Added imports for `GitServiceImpl` and `GitPreferences` from `git-service.ts` +2. Added module-level `let gitService: GitServiceImpl | null = null` next to the existing `basePath` declaration +3. Added initialization line after the bootstrap block in `startAutoMode()`: `gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {})` +4. Removed `getSliceBranchName` from the worktree.ts import list — it was imported but never used in auto.ts + +All 8 remaining worktree.ts imports continue to work through the T01 facade unchanged. Bootstrap git calls and idle detection remain inline per spec. + +## Verification + +- `npx tsc --noEmit` — clean, no errors +- `npm run test` — 116 pass, 2 pre-existing failures (matches baseline) +- `grep -c 'GitServiceImpl' auto.ts` → 4 (import, type import, variable declaration, instantiation) +- `grep -c 'new GitServiceImpl' auto.ts` → 1 (exactly one instantiation) +- All three test suites pass: worktree.test.ts, worktree-integration.test.ts, git-service.test.ts +- Consumer imports unchanged: state.ts, workspace-index.ts, worktree-command.ts still import from worktree.ts + +## Diagnostics + +- `grep GitServiceImpl src/resources/extensions/gsd/auto.ts` confirms instance exists +- `gitService` is null before `startAutoMode()` runs — any premature access will surface as TypeError +- Preferences fallback `?? {}` means no crash even if preferences file is missing or malformed + +## Deviations + +Removed unused `getSliceBranchName` import as noted in the plan (step 3). This is a cleanup, not a behavioral change. + +## Known Issues + +None. + +## Files Created/Modified + +- `src/resources/extensions/gsd/auto.ts` — Added GitServiceImpl import, module-level variable, initialization in startAutoMode(), removed unused getSliceBranchName import diff --git a/.gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md b/.gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md new file mode 100644 index 000000000..31c6eb280 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md @@ -0,0 +1,74 @@ +--- +estimated_steps: 5 +estimated_files: 3 +--- + +# T03: Add git preferences to preferences.ts, template, and docs + +**Slice:** S02 — Wire GitService into codebase +**Milestone:** M001 + +## Description + +Add `git?: GitPreferences` to the `GSDPreferences` interface with full validation, merge semantics, documentation in the preferences template, and a reference doc entry. This enables all preference-gated git features in S05 (auto_push, merge guards, snapshots) via the existing preferences system. + +The `GitPreferences` type already exists in `git-service.ts` — this task imports it and wires it into the preferences infrastructure. + +## Steps + +1. **Add `git` field to `GSDPreferences` interface:** Import `GitPreferences` from `./git-service.ts`. Add `git?: GitPreferences` to the `GSDPreferences` interface alongside the existing fields. + +2. **Add git validation to `validatePreferences()`:** After the existing validation blocks, add a `git` section that validates each sub-field: + - `auto_push`: must be boolean if present + - `push_branches`: must be boolean if present + - `remote`: must be non-empty string if present + - `snapshots`: must be boolean if present + - `pre_merge_check`: must be boolean or the string `"auto"` if present + - `commit_type`: must be one of `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style` if present + Collect errors for invalid values. Copy valid values to `validated.git`. + +3. **Add git merge to `mergePreferences()`:** Add `git: { ...(base.git ?? {}), ...(override.git ?? {}) }` to the merge return object. Override-wins for each field, same as `models` and `auto_supervisor`. + +4. **Update preferences template:** Add a `git:` section to `src/resources/extensions/gsd/templates/preferences.md` showing all available fields with sensible defaults (empty/unset). Place it after the existing sections in the frontmatter block. + +5. **Update preferences reference doc:** Add a "Git Preferences" section to `src/resources/extensions/gsd/docs/preferences-reference.md` documenting each field, its type, default value, and behavior. Include a YAML example. + +## Must-Haves + +- [ ] `GSDPreferences` interface includes `git?: GitPreferences` +- [ ] `GitPreferences` imported from `git-service.ts` +- [ ] `validatePreferences()` validates all 6 git sub-fields with type checking +- [ ] Invalid git values produce error messages (not silent drops) +- [ ] `mergePreferences()` merges git with override-wins semantics +- [ ] `templates/preferences.md` has `git:` section with all fields +- [ ] `docs/preferences-reference.md` documents git preferences +- [ ] `npx tsc --noEmit` clean +- [ ] `npm run test` same baseline + +## Verification + +- `npx tsc --noEmit` — no errors +- `npm run test` — same pass/fail as baseline +- `grep 'git.*GitPreferences' src/resources/extensions/gsd/preferences.ts` — confirms field exists +- `grep -c 'auto_push\|push_branches\|pre_merge_check\|snapshots\|commit_type' src/resources/extensions/gsd/preferences.ts` — at least 5 (validation of each field) +- `grep 'git:' src/resources/extensions/gsd/templates/preferences.md` — confirms template section +- `grep -i 'git preferences\|git:' src/resources/extensions/gsd/docs/preferences-reference.md` — confirms docs section + +## Observability Impact + +- Signals added/changed: None — this adds schema infrastructure only. Preferences are parsed at load time; invalid values produce error strings in the `validatePreferences` return. +- How a future agent inspects this: Call `loadEffectiveGSDPreferences()` and check `.preferences.git` for parsed values. Validation errors are in the return tuple. +- Failure state exposed: Invalid git preference values are reported as strings in the `errors` array from `validatePreferences()` — callers already log these. + +## Inputs + +- `src/resources/extensions/gsd/preferences.ts` — `GSDPreferences` interface, `validatePreferences()`, `mergePreferences()` +- `src/resources/extensions/gsd/git-service.ts` — `GitPreferences` interface +- `src/resources/extensions/gsd/templates/preferences.md` — existing template +- `src/resources/extensions/gsd/docs/preferences-reference.md` — existing reference doc + +## Expected Output + +- `src/resources/extensions/gsd/preferences.ts` — `git?: GitPreferences` in interface, validation logic, merge logic +- `src/resources/extensions/gsd/templates/preferences.md` — `git:` section added to frontmatter +- `src/resources/extensions/gsd/docs/preferences-reference.md` — git preferences documented with example diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index e7eb55d28..7027bafde 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -62,11 +62,12 @@ import { ensureSliceBranch, getCurrentBranch, getMainBranch, - getSliceBranchName, parseSliceBranch, switchToMain, mergeSliceToMain, } from "./worktree.ts"; +import { GitServiceImpl } from "./git-service.ts"; +import type { GitPreferences } from "./git-service.ts"; import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { makeUI, GLYPH, INDENT } from "../shared/ui.js"; import { showNextAction } from "../shared/next-action-ui.js"; @@ -124,6 +125,7 @@ let stepMode = false; let verbose = false; let cmdCtx: ExtensionCommandContext | null = null; let basePath = ""; +let gitService: GitServiceImpl | null = null; /** Track total dispatches per unit to detect stuck loops (catches A→B→A→B patterns) */ const unitDispatchCount = new Map(); @@ -388,6 +390,9 @@ export async function startAuto( } catch { /* nothing to commit */ } } + // Initialize GitServiceImpl — basePath is set and git repo confirmed + gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {}); + // Check for crash from previous session const crashLock = readCrashLock(base); if (crashLock) { diff --git a/src/resources/extensions/gsd/docs/preferences-reference.md b/src/resources/extensions/gsd/docs/preferences-reference.md index cc38d391c..3d382138a 100644 --- a/src/resources/extensions/gsd/docs/preferences-reference.md +++ b/src/resources/extensions/gsd/docs/preferences-reference.md @@ -40,6 +40,14 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md - `idle_timeout_minutes`: minutes of inactivity before the supervisor intervenes (default: 10). - `hard_timeout_minutes`: minutes before the supervisor forces termination (default: 30). +- `git`: configures GSD's git behavior. All fields are optional — omit any to use defaults. Keys: + - `auto_push`: boolean — automatically push commits to the remote after committing. Default: `false`. + - `push_branches`: boolean — push newly created slice branches to the remote. Default: `false`. + - `remote`: string — git remote name to push to. Default: `"origin"`. + - `snapshots`: boolean — create snapshot commits (WIP saves) during long-running tasks. Default: `false`. + - `pre_merge_check`: boolean or `"auto"` — run pre-merge checks before merging a slice branch. `true` always runs, `false` never runs, `"auto"` runs when CI is detected. Default: `false`. + - `commit_type`: string — override the conventional commit type prefix. Must be one of: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style`. Default: inferred from diff content. + --- ## Best Practices @@ -101,3 +109,22 @@ skill_rules: - find-skills --- ``` + +--- + +## Git Preferences Example + +```yaml +--- +version: 1 +git: + auto_push: true + push_branches: true + remote: origin + snapshots: true + pre_merge_check: auto + commit_type: feat +--- +``` + +All git fields are optional. Omit any field to use the default behavior. Project-level preferences override global preferences on a per-field basis. diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index 30b567e75..9274a3599 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { isAbsolute, join } from "node:path"; import { getAgentDir } from "@mariozechner/pi-coding-agent"; +import type { GitPreferences } from "./git-service.ts"; const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md"); const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md"); @@ -51,6 +52,7 @@ export interface GSDPreferences { uat_dispatch?: boolean; budget_ceiling?: number; remote_questions?: RemoteQuestionsConfig; + git?: GitPreferences; } export interface LoadedGSDPreferences { @@ -511,6 +513,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr remote_questions: override.remote_questions ? { ...(base.remote_questions ?? {}), ...override.remote_questions } : base.remote_questions, + git: (base.git || override.git) + ? { ...(base.git ?? {}), ...(override.git ?? {}) } + : undefined, }; } @@ -594,6 +599,50 @@ function validatePreferences(preferences: GSDPreferences): { } } + // ─── Git Preferences ─────────────────────────────────────────────────── + if (preferences.git && typeof preferences.git === "object") { + const git: Record = {}; + const g = preferences.git as Record; + + if (g.auto_push !== undefined) { + if (typeof g.auto_push === "boolean") git.auto_push = g.auto_push; + else errors.push("git.auto_push must be a boolean"); + } + if (g.push_branches !== undefined) { + if (typeof g.push_branches === "boolean") git.push_branches = g.push_branches; + else errors.push("git.push_branches must be a boolean"); + } + if (g.remote !== undefined) { + if (typeof g.remote === "string" && g.remote.trim() !== "") git.remote = g.remote.trim(); + else errors.push("git.remote must be a non-empty string"); + } + if (g.snapshots !== undefined) { + if (typeof g.snapshots === "boolean") git.snapshots = g.snapshots; + else errors.push("git.snapshots must be a boolean"); + } + if (g.pre_merge_check !== undefined) { + if (typeof g.pre_merge_check === "boolean" || g.pre_merge_check === "auto") { + git.pre_merge_check = g.pre_merge_check; + } else { + errors.push('git.pre_merge_check must be a boolean or "auto"'); + } + } + if (g.commit_type !== undefined) { + const validCommitTypes = new Set([ + "feat", "fix", "refactor", "docs", "test", "chore", "perf", "ci", "build", "style", + ]); + if (typeof g.commit_type === "string" && validCommitTypes.has(g.commit_type)) { + git.commit_type = g.commit_type; + } else { + errors.push(`git.commit_type must be one of: feat, fix, refactor, docs, test, chore, perf, ci, build, style`); + } + } + + if (Object.keys(git).length > 0) { + validated.git = git as GitPreferences; + } + } + return { preferences: validated, errors }; } diff --git a/src/resources/extensions/gsd/templates/preferences.md b/src/resources/extensions/gsd/templates/preferences.md index c91794f2b..404401a4e 100644 --- a/src/resources/extensions/gsd/templates/preferences.md +++ b/src/resources/extensions/gsd/templates/preferences.md @@ -8,6 +8,13 @@ custom_instructions: [] models: {} skill_discovery: auto_supervisor: {} +git: + auto_push: + push_branches: + remote: + snapshots: + pre_merge_check: + commit_type: --- # GSD Skill Preferences diff --git a/src/resources/extensions/gsd/worktree.ts b/src/resources/extensions/gsd/worktree.ts index 31f6fe7d1..159c4963f 100644 --- a/src/resources/extensions/gsd/worktree.ts +++ b/src/resources/extensions/gsd/worktree.ts @@ -1,39 +1,46 @@ /** - * GSD Slice Branch Management + * GSD Slice Branch Management — Thin Facade * * Simple branch-per-slice workflow. No worktrees, no registry. * Runtime state (metrics, activity, lock, STATE.md) is gitignored * so branch switches are clean. * + * All git-mutation functions delegate to GitServiceImpl from git-service.ts. + * Pure utility functions (detectWorktreeName, getSliceBranchName, parseSliceBranch, + * SLICE_BRANCH_RE) remain standalone. + * * Flow: * 1. ensureSliceBranch() — create + checkout slice branch * 2. agent does work, commits * 3. mergeSliceToMain() — checkout main, squash-merge, delete branch */ -import { existsSync } from "node:fs"; -import { execSync } from "node:child_process"; import { sep } from "node:path"; -export interface MergeSliceResult { - branch: string; - mergedCommitMessage: string; - deletedBranch: boolean; +import { GitServiceImpl } from "./git-service.ts"; + +// Re-export MergeSliceResult from the canonical source (D014 — type-only re-export) +export type { MergeSliceResult } from "./git-service.ts"; + +// ─── Lazy GitServiceImpl Cache ───────────────────────────────────────────── + +let cachedService: GitServiceImpl | null = null; +let cachedBasePath: string | null = null; + +/** + * Get or create a GitServiceImpl for the given basePath. + * Resets the cache if basePath changes between calls. + * Lazy construction: only instantiated at call-time, never at module-evaluation. + */ +function getService(basePath: string): GitServiceImpl { + if (cachedService === null || cachedBasePath !== basePath) { + cachedService = new GitServiceImpl(basePath, {}); + cachedBasePath = basePath; + } + return cachedService; } -function runGit(basePath: string, args: string[], options: { allowFailure?: boolean } = {}): string { - try { - return execSync(`git ${args.join(" ")}`, { - cwd: basePath, - stdio: ["ignore", "pipe", "pipe"], - encoding: "utf-8", - }).trim(); - } catch (error) { - if (options.allowFailure) return ""; - const message = error instanceof Error ? error.message : String(error); - throw new Error(`git ${args.join(" ")} failed in ${basePath}: ${message}`); - } -} +// ─── Pure Utility Functions (unchanged) ──────────────────────────────────── /** * Detect the active worktree name from the current working directory. @@ -86,6 +93,8 @@ export function parseSliceBranch(branchName: string): { }; } +// ─── Git-Mutation Functions (delegate to GitServiceImpl) ─────────────────── + /** * Get the "main" branch for GSD slice operations. * @@ -98,46 +107,11 @@ export function parseSliceBranch(branchName: string): { * /worktree merge. */ export function getMainBranch(basePath: string): string { - // When inside a worktree, slice branches should merge into the worktree's - // own branch (worktree/), not main — main is checked out by the - // parent working tree and git would refuse the checkout. - const wtName = detectWorktreeName(basePath); - if (wtName) { - const wtBranch = `worktree/${wtName}`; - // Verify the branch exists (it should — createWorktree made it) - const exists = runGit(basePath, ["show-ref", "--verify", `refs/heads/${wtBranch}`], { allowFailure: true }); - if (exists) return wtBranch; - // Worktree branch is gone — return current branch rather than falling - // through to main/master which would cause a checkout conflict - return runGit(basePath, ["branch", "--show-current"]); - } - - const symbolic = runGit(basePath, ["symbolic-ref", "refs/remotes/origin/HEAD"], { allowFailure: true }); - if (symbolic) { - const match = symbolic.match(/refs\/remotes\/origin\/(.+)$/); - if (match) return match[1]!; - } - - const mainExists = runGit(basePath, ["show-ref", "--verify", "refs/heads/main"], { allowFailure: true }); - if (mainExists) return "main"; - - const masterExists = runGit(basePath, ["show-ref", "--verify", "refs/heads/master"], { allowFailure: true }); - if (masterExists) return "master"; - - return runGit(basePath, ["branch", "--show-current"]); + return getService(basePath).getMainBranch(); } export function getCurrentBranch(basePath: string): string { - return runGit(basePath, ["branch", "--show-current"]); -} - -function branchExists(basePath: string, branch: string): boolean { - try { - runGit(basePath, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]); - return true; - } catch { - return false; - } + return getService(basePath).getCurrentBranch(); } /** @@ -150,49 +124,7 @@ function branchExists(basePath: string, branch: string): boolean { * Returns true if the branch was newly created. */ export function ensureSliceBranch(basePath: string, milestoneId: string, sliceId: string): boolean { - const wtName = detectWorktreeName(basePath); - const branch = getSliceBranchName(milestoneId, sliceId, wtName); - const current = getCurrentBranch(basePath); - - if (current === branch) return false; - - let created = false; - - if (!branchExists(basePath, branch)) { - // Branch from the current branch when it's a normal working branch - // (not itself a slice branch). This ensures the new slice branch - // inherits planning artifacts that may only exist on the working - // branch and haven't been merged to main yet. - // If we're already on a slice branch (e.g. creating S02 while S01 - // wasn't merged yet), fall back to main to avoid chaining slice branches. - const mainBranch = getMainBranch(basePath); - const base = SLICE_BRANCH_RE.test(current) ? mainBranch : current; - runGit(basePath, ["branch", branch, base]); - created = true; - } else { - // Check if the branch is already checked out in another worktree - const worktreeList = runGit(basePath, ["worktree", "list", "--porcelain"]); - if (worktreeList.includes(`branch refs/heads/${branch}`)) { - throw new Error( - `Branch "${branch}" is already in use by another worktree. ` + - `Remove that worktree first, or switch it to a different branch.`, - ); - } - } - - // Auto-commit dirty files before checkout to prevent "would be overwritten" errors. - // This handles cases where doctor, STATE.md rebuild, or agent work left uncommitted changes. - const status = runGit(basePath, ["status", "--short"]); - if (status.trim()) { - runGit(basePath, ["add", "-A"]); - const staged = runGit(basePath, ["diff", "--cached", "--stat"]); - if (staged.trim()) { - runGit(basePath, ["commit", "-m", `"chore: auto-commit before switching to ${branch}"`]); - } - } - - runGit(basePath, ["checkout", branch]); - return created; + return getService(basePath).ensureSliceBranch(milestoneId, sliceId); } /** @@ -202,31 +134,14 @@ export function ensureSliceBranch(basePath: string, milestoneId: string, sliceId export function autoCommitCurrentBranch( basePath: string, unitType: string, unitId: string, ): string | null { - const status = runGit(basePath, ["status", "--short"]); - if (!status.trim()) return null; - - runGit(basePath, ["add", "-A"]); - - const staged = runGit(basePath, ["diff", "--cached", "--stat"]); - if (!staged.trim()) return null; - - const message = `chore(${unitId}): auto-commit after ${unitType}`; - runGit(basePath, ["commit", "-m", JSON.stringify(message)]); - return message; + return getService(basePath).autoCommit(unitType, unitId); } /** * Switch to main, auto-committing any dirty files on the current branch first. */ export function switchToMain(basePath: string): void { - const mainBranch = getMainBranch(basePath); - const current = getCurrentBranch(basePath); - if (current === mainBranch) return; - - // Auto-commit if dirty - autoCommitCurrentBranch(basePath, "pre-switch", current); - - runGit(basePath, ["checkout", mainBranch]); + getService(basePath).switchToMain(); } /** @@ -236,37 +151,12 @@ export function switchToMain(basePath: string): void { */ export function mergeSliceToMain( basePath: string, milestoneId: string, sliceId: string, sliceTitle: string, -): MergeSliceResult { - const wtName = detectWorktreeName(basePath); - const branch = getSliceBranchName(milestoneId, sliceId, wtName); - const mainBranch = getMainBranch(basePath); - - const current = getCurrentBranch(basePath); - if (current !== mainBranch) { - throw new Error(`Expected to be on ${mainBranch}, found ${current}`); - } - - if (!branchExists(basePath, branch)) { - throw new Error(`Slice branch ${branch} does not exist`); - } - - const ahead = runGit(basePath, ["rev-list", "--count", `${mainBranch}..${branch}`]); - if (Number(ahead) <= 0) { - throw new Error(`Slice branch ${branch} has no commits ahead of ${mainBranch}`); - } - - runGit(basePath, ["merge", "--squash", branch]); - const mergedCommitMessage = `feat(${milestoneId}/${sliceId}): ${sliceTitle}`; - runGit(basePath, ["commit", "-m", JSON.stringify(mergedCommitMessage)]); - runGit(basePath, ["branch", "-D", branch]); - - return { - branch, - mergedCommitMessage, - deletedBranch: true, - }; +): import("./git-service.ts").MergeSliceResult { + return getService(basePath).mergeSliceToMain(milestoneId, sliceId, sliceTitle); } +// ─── Query Functions (delegate to GitServiceImpl) ────────────────────────── + /** * Check if we're currently on a slice branch (not main). * Handles both plain (gsd/M001/S01) and worktree-namespaced (gsd/wt/M001/S01) branches.