gsd-pi 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +1 -0
  3. package/dist/loader.js +50 -6
  4. package/dist/resource-loader.d.ts +7 -6
  5. package/dist/resource-loader.js +15 -8
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +29 -183
  7. package/dist/resources/extensions/gsd/auto.ts +252 -370
  8. package/dist/resources/extensions/gsd/commands.ts +118 -34
  9. package/dist/resources/extensions/gsd/doctor.ts +29 -4
  10. package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
  11. package/dist/resources/extensions/gsd/git-service.ts +8 -431
  12. package/dist/resources/extensions/gsd/gitignore.ts +11 -4
  13. package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
  14. package/dist/resources/extensions/gsd/preferences.ts +18 -17
  15. package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
  16. package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
  17. package/dist/resources/extensions/gsd/state.ts +26 -8
  18. package/dist/resources/extensions/gsd/templates/state.md +0 -1
  19. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
  20. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  21. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
  22. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
  23. package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  24. package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
  25. package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  26. package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  27. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  28. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  29. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
  30. package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  31. package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  32. package/dist/resources/extensions/gsd/types.ts +0 -1
  33. package/dist/resources/extensions/gsd/worktree-manager.ts +7 -3
  34. package/dist/resources/extensions/gsd/worktree.ts +7 -65
  35. package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  36. package/package.json +1 -1
  37. package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
  38. package/packages/pi-ai/dist/providers/google.js +12 -4
  39. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  40. package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
  41. package/packages/pi-ai/dist/providers/mistral.js +10 -2
  42. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  43. package/packages/pi-ai/src/providers/google.ts +20 -8
  44. package/packages/pi-ai/src/providers/mistral.ts +14 -2
  45. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
  46. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
  48. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
  52. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +12 -3
  55. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
  57. package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
  58. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
  59. package/packages/pi-tui/dist/components/input.d.ts +1 -0
  60. package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
  61. package/packages/pi-tui/dist/components/input.js +10 -0
  62. package/packages/pi-tui/dist/components/input.js.map +1 -1
  63. package/packages/pi-tui/src/components/input.ts +11 -0
  64. package/src/resources/extensions/gsd/auto-worktree.ts +29 -183
  65. package/src/resources/extensions/gsd/auto.ts +252 -370
  66. package/src/resources/extensions/gsd/commands.ts +118 -34
  67. package/src/resources/extensions/gsd/doctor.ts +29 -4
  68. package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
  69. package/src/resources/extensions/gsd/git-service.ts +8 -431
  70. package/src/resources/extensions/gsd/gitignore.ts +11 -4
  71. package/src/resources/extensions/gsd/guided-flow.ts +141 -5
  72. package/src/resources/extensions/gsd/preferences.ts +18 -17
  73. package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
  74. package/src/resources/extensions/gsd/prompts/queue.md +7 -1
  75. package/src/resources/extensions/gsd/state.ts +26 -8
  76. package/src/resources/extensions/gsd/templates/state.md +0 -1
  77. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
  78. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  79. package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
  80. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
  81. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  82. package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
  83. package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  84. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  85. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  86. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  87. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
  88. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  89. package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  90. package/src/resources/extensions/gsd/types.ts +0 -1
  91. package/src/resources/extensions/gsd/worktree-manager.ts +7 -3
  92. package/src/resources/extensions/gsd/worktree.ts +7 -65
  93. package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  94. package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  95. package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  96. package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
  97. package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  98. package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  99. package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
package/README.md CHANGED
@@ -38,7 +38,7 @@ GSD v2 solves all of these because it's not a prompt framework anymore — it's
38
38
  | Context management | Hope the LLM doesn't fill up | Fresh session per task, programmatic |
39
39
  | Auto mode | LLM self-loop | State machine reading `.gsd/` files |
40
40
  | Crash recovery | None | Lock files + session forensics |
41
- | Git strategy | LLM writes git commands | Programmatic branch-per-slice, squash merge |
41
+ | Git strategy | LLM writes git commands | Worktree isolation, sequential commits, squash merge |
42
42
  | Cost tracking | None | Per-unit token/cost ledger with dashboard |
43
43
  | Stuck detection | None | Retry once, then stop with diagnostics |
44
44
  | Timeout supervision | None | Soft/idle/hard timeouts with recovery steering |
@@ -111,7 +111,7 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
111
111
 
112
112
  2. **Context pre-loading** — The dispatch prompt includes inlined task plans, slice plans, prior task summaries, dependency summaries, roadmap excerpts, and decisions register. The LLM starts with everything it needs instead of spending tool calls reading files.
113
113
 
114
- 3. **Git branch-per-slice** — Each slice gets its own branch (`gsd/M001/S01`). Tasks commit atomically on the branch. When the slice completes, it's squash-merged to main (or whichever branch you started from) as one clean commit.
114
+ 3. **Git worktree isolation** — Each milestone runs in its own git worktree with a `milestone/<MID>` branch. All slice work commits sequentially — no branch switching, no merge conflicts. When the milestone completes, it's squash-merged to main as one clean commit.
115
115
 
116
116
  4. **Crash recovery** — A lock file tracks the current unit. If the session dies, the next `/gsd auto` reads the surviving session file, synthesizes a recovery briefing from every tool call that made it to disk, and resumes with full context.
117
117
 
@@ -268,7 +268,7 @@ gsd/M001/S01 (deleted after merge):
268
268
  feat(S01/T01): core types and interfaces
269
269
  ```
270
270
 
271
- One commit per slice on main (or whichever branch you started from). Squash commits are the permanent record — branches are deleted after merge. Git bisect works. Individual slices are revertable.
271
+ One squash commit per milestone on main (or whichever branch you started from). The worktree is torn down after merge. Git bisect works. Individual milestones are revertable.
272
272
 
273
273
  ### Verification
274
274
 
package/dist/cli.js CHANGED
@@ -91,6 +91,7 @@ const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
91
91
  // `gsd config` — replay the setup wizard and exit
92
92
  if (cliFlags.messages[0] === 'config') {
93
93
  const authStorage = AuthStorage.create(authFilePath);
94
+ loadStoredEnvKeys(authStorage);
94
95
  await runOnboarding(authStorage);
95
96
  process.exit(0);
96
97
  }
package/dist/loader.js CHANGED
@@ -1,7 +1,50 @@
1
1
  #!/usr/bin/env node
2
+ // GSD Startup Loader
3
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
2
4
  import { fileURLToPath } from 'url';
3
5
  import { dirname, resolve, join, delimiter } from 'path';
4
6
  import { existsSync, readFileSync, readdirSync, mkdirSync, symlinkSync } from 'fs';
7
+ // Fast-path: handle --version/-v and --help/-h before importing any heavy
8
+ // dependencies. This avoids loading the entire pi-coding-agent barrel import
9
+ // (~1s) just to print a version string.
10
+ const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
11
+ const args = process.argv.slice(2);
12
+ const firstArg = args[0];
13
+ if (firstArg === '--version' || firstArg === '-v') {
14
+ try {
15
+ const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'));
16
+ process.stdout.write((pkg.version || '0.0.0') + '\n');
17
+ }
18
+ catch {
19
+ process.stdout.write('0.0.0\n');
20
+ }
21
+ process.exit(0);
22
+ }
23
+ if (firstArg === '--help' || firstArg === '-h') {
24
+ let version = '0.0.0';
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'));
27
+ version = pkg.version || version;
28
+ }
29
+ catch { /* ignore */ }
30
+ process.stdout.write(`GSD v${version} — Get Shit Done\n\n`);
31
+ process.stdout.write('Usage: gsd [options] [message...]\n\n');
32
+ process.stdout.write('Options:\n');
33
+ process.stdout.write(' --mode <text|json|rpc> Output mode (default: interactive)\n');
34
+ process.stdout.write(' --print, -p Single-shot print mode\n');
35
+ process.stdout.write(' --continue, -c Resume the most recent session\n');
36
+ process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n');
37
+ process.stdout.write(' --no-session Disable session persistence\n');
38
+ process.stdout.write(' --extension <path> Load additional extension\n');
39
+ process.stdout.write(' --tools <a,b,c> Restrict available tools\n');
40
+ process.stdout.write(' --list-models [search] List available models and exit\n');
41
+ process.stdout.write(' --version, -v Print version and exit\n');
42
+ process.stdout.write(' --help, -h Print this help and exit\n');
43
+ process.stdout.write('\nSubcommands:\n');
44
+ process.stdout.write(' config Re-run the setup wizard\n');
45
+ process.stdout.write(' update Update GSD to the latest version\n');
46
+ process.exit(0);
47
+ }
5
48
  import { agentDir, appRoot } from './app-paths.js';
6
49
  import { serializeBundledExtensionPaths } from './bundled-extension-paths.js';
7
50
  import { renderLogo } from './logo.js';
@@ -40,7 +83,6 @@ process.env.GSD_CODING_AGENT_DIR = agentDir;
40
83
  // Without this, extensions (e.g. browser-tools) can't resolve dependencies like
41
84
  // `playwright` because jiti resolves modules from pi-coding-agent's location, not gsd's.
42
85
  // Prepending gsd's node_modules to NODE_PATH fixes this for all extensions.
43
- const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
44
86
  const gsdNodeModules = join(gsdRoot, 'node_modules');
45
87
  process.env.NODE_PATH = [gsdNodeModules, process.env.NODE_PATH]
46
88
  .filter(Boolean)
@@ -64,9 +106,8 @@ process.env.GSD_BIN_PATH = process.argv[1];
64
106
  // GSD_WORKFLOW_PATH — absolute path to bundled GSD-WORKFLOW.md, used by patched gsd extension
65
107
  // when dispatching workflow prompts. Prefers dist/resources/ (stable, set at build time)
66
108
  // over src/resources/ (live working tree) — see resource-loader.ts for rationale.
67
- const loaderPackageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
68
- const distRes = join(loaderPackageRoot, 'dist', 'resources');
69
- const srcRes = join(loaderPackageRoot, 'src', 'resources');
109
+ const distRes = join(gsdRoot, 'dist', 'resources');
110
+ const srcRes = join(gsdRoot, 'src', 'resources');
70
111
  const resourcesDir = existsSync(distRes) ? distRes : srcRes;
71
112
  process.env.GSD_WORKFLOW_PATH = join(resourcesDir, 'GSD-WORKFLOW.md');
72
113
  // GSD_BUNDLED_EXTENSION_PATHS — dynamically discovered bundled extension entry points.
@@ -105,8 +146,11 @@ process.env.GSD_BUNDLED_EXTENSION_PATHS = serializeBundledExtensionPaths(discove
105
146
  // Respect HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars for all outbound requests.
106
147
  // pi-coding-agent's cli.ts sets this, but GSD bypasses that entry point — so we
107
148
  // must set it here before any SDK clients are created.
108
- import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
109
- setGlobalDispatcher(new EnvHttpProxyAgent());
149
+ // Lazy-load undici (~200ms) only when proxy env vars are actually set.
150
+ if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy) {
151
+ const { EnvHttpProxyAgent, setGlobalDispatcher } = await import('undici');
152
+ setGlobalDispatcher(new EnvHttpProxyAgent());
153
+ }
110
154
  // Ensure workspace packages are linked before importing cli.js (which imports @gsd/*).
111
155
  // npm postinstall handles this normally, but npx --ignore-scripts skips postinstall.
112
156
  const gsdScopeDir = join(gsdNodeModules, '@gsd');
@@ -5,14 +5,15 @@ export declare function getNewerManagedResourceVersion(agentDir: string, current
5
5
  /**
6
6
  * Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
7
7
  *
8
- * - extensions/ → ~/.gsd/agent/extensions/ (always overwrite ensures updates ship on next launch)
9
- * - agents/ → ~/.gsd/agent/agents/ (always overwrite)
10
- * - skills/ → ~/.gsd/agent/skills/ (always overwrite)
8
+ * - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
9
+ * - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
10
+ * - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
11
11
  * - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
12
12
  *
13
- * Always-overwrite ensures `npm update -g @glittercowboy/gsd` takes effect immediately.
14
- * User customizations should go in ~/.gsd/agent/extensions/ subdirs with unique names,
15
- * not by editing the gsd-managed files.
13
+ * Skips the copy when the managed-resources.json version matches the current
14
+ * GSD version, avoiding ~128ms of synchronous cpSync on every startup.
15
+ * After `npm update -g @glittercowboy/gsd`, versions will differ and the
16
+ * copy runs once to land the new resources.
16
17
  *
17
18
  * Inspectable: `ls ~/.gsd/agent/extensions/`
18
19
  */
@@ -107,20 +107,27 @@ export function getNewerManagedResourceVersion(agentDir, currentVersion) {
107
107
  /**
108
108
  * Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
109
109
  *
110
- * - extensions/ → ~/.gsd/agent/extensions/ (always overwrite ensures updates ship on next launch)
111
- * - agents/ → ~/.gsd/agent/agents/ (always overwrite)
112
- * - skills/ → ~/.gsd/agent/skills/ (always overwrite)
110
+ * - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
111
+ * - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
112
+ * - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
113
113
  * - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
114
114
  *
115
- * Always-overwrite ensures `npm update -g @glittercowboy/gsd` takes effect immediately.
116
- * User customizations should go in ~/.gsd/agent/extensions/ subdirs with unique names,
117
- * not by editing the gsd-managed files.
115
+ * Skips the copy when the managed-resources.json version matches the current
116
+ * GSD version, avoiding ~128ms of synchronous cpSync on every startup.
117
+ * After `npm update -g @glittercowboy/gsd`, versions will differ and the
118
+ * copy runs once to land the new resources.
118
119
  *
119
120
  * Inspectable: `ls ~/.gsd/agent/extensions/`
120
121
  */
121
122
  export function initResources(agentDir) {
122
123
  mkdirSync(agentDir, { recursive: true });
123
- // Sync extensions always overwrite so updates land on next launch
124
+ // Skip resource sync when versions match saves ~128ms of cpSync per launch
125
+ const currentVersion = getBundledGsdVersion();
126
+ const managedVersion = readManagedResourceVersion(agentDir);
127
+ if (managedVersion && managedVersion === currentVersion) {
128
+ return;
129
+ }
130
+ // Sync extensions — overwrite so updates land on next launch
124
131
  const destExtensions = join(agentDir, 'extensions');
125
132
  cpSync(bundledExtensionsDir, destExtensions, { recursive: true, force: true });
126
133
  // Sync agents
@@ -129,7 +136,7 @@ export function initResources(agentDir) {
129
136
  if (existsSync(srcAgents)) {
130
137
  cpSync(srcAgents, destAgents, { recursive: true, force: true });
131
138
  }
132
- // Sync skills — always overwrite so updates land on next launch
139
+ // Sync skills — overwrite so updates land on next launch
133
140
  const destSkills = join(agentDir, 'skills');
134
141
  const srcSkills = join(resourcesDir, 'skills');
135
142
  if (existsSync(srcSkills)) {
@@ -8,26 +8,15 @@
8
8
 
9
9
  import { existsSync, readFileSync, realpathSync, utimesSync } from "node:fs";
10
10
  import { join, resolve } from "node:path";
11
- import { execSync } from "node:child_process";
11
+ import { execSync, execFileSync } from "node:child_process";
12
12
  import {
13
13
  createWorktree,
14
14
  removeWorktree,
15
15
  worktreePath,
16
16
  } from "./worktree-manager.js";
17
- import {
18
- detectWorktreeName,
19
- getSliceBranchName,
20
- } from "./worktree.js";
21
17
  import {
22
18
  MergeConflictError,
23
- inferCommitType,
24
19
  } from "./git-service.js";
25
- import type { MergeSliceResult } from "./git-service.js";
26
- import { recoverCheckout, withMergeHeal } from "./git-self-heal.js";
27
- import {
28
- nativeBranchExists,
29
- nativeCommitCountBetween,
30
- } from "./native-git-bridge.js";
31
20
  import { parseRoadmap } from "./files.js";
32
21
  import { loadEffectiveGSDPreferences } from "./preferences.js";
33
22
 
@@ -36,46 +25,6 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
36
25
  /** Original project root before chdir into auto-worktree. */
37
26
  let originalBase: string | null = null;
38
27
 
39
- // ─── Isolation Resolver ────────────────────────────────────────────────────
40
-
41
- /**
42
- * Determine whether auto-mode should use worktree isolation.
43
- *
44
- * Resolution order:
45
- * 1. Explicit git.isolation preference -> return (isolation === "worktree")
46
- * 2. Legacy detection: if gsd branches exist -> return false (branch mode)
47
- * 3. Default: return true (worktree mode for new projects)
48
- */
49
- export function shouldUseWorktreeIsolation(basePath: string, overridePrefs?: { isolation?: string }): boolean {
50
- const prefs = overridePrefs ?? loadEffectiveGSDPreferences()?.preferences?.git;
51
- if (prefs?.isolation) {
52
- return prefs.isolation === "worktree";
53
- }
54
-
55
- // Legacy detection: check for existing gsd/*/* branches (branch-per-slice pattern)
56
- try {
57
- const output = execSync("git branch --list 'gsd/*/*'", {
58
- cwd: basePath,
59
- stdio: ["ignore", "pipe", "pipe"],
60
- encoding: "utf-8",
61
- }).trim();
62
- if (output) return false; // Legacy branch-per-slice project
63
- } catch {
64
- // If git command fails, default to worktree
65
- }
66
-
67
- return true; // New project default
68
- }
69
-
70
- /**
71
- * Resolve the merge_to_main preference value.
72
- * Returns "milestone" (default) or "slice".
73
- */
74
- export function getMergeToMainMode(): "milestone" | "slice" {
75
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
76
- return prefs?.merge_to_main ?? "milestone";
77
- }
78
-
79
28
  // ─── Git Helpers (local, mirrors worktree-command.ts pattern) ──────────────
80
29
 
81
30
  function resolveGitHeadPath(dir: string): string | null {
@@ -236,117 +185,6 @@ export function getAutoWorktreeOriginalBase(): string | null {
236
185
  return originalBase;
237
186
  }
238
187
 
239
- // ─── Merge Slice -> Milestone ───────────────────────────────────────────────
240
-
241
- /**
242
- * Merge a completed slice branch into the milestone branch via `--no-ff`.
243
- *
244
- * Worktree-mode merge: `.gsd/` is local to the worktree (not tracked in
245
- * git), so there are zero `.gsd/` conflict resolution concerns. No runtime
246
- * exclusion untracking, no `--theirs` checkout, no snapshot creation.
247
- *
248
- * On conflict: throws MergeConflictError with conflicted file list.
249
- * On success: deletes the slice branch and returns MergeSliceResult.
250
- */
251
- export function mergeSliceToMilestone(
252
- basePath: string,
253
- milestoneId: string,
254
- sliceId: string,
255
- sliceTitle: string,
256
- ): MergeSliceResult {
257
- if (!isInAutoWorktree(basePath)) {
258
- throw new Error("mergeSliceToMilestone called outside auto-worktree");
259
- }
260
-
261
- const cwd = process.cwd();
262
- const milestoneBranch = autoWorktreeBranch(milestoneId);
263
- const worktreeName = detectWorktreeName(cwd);
264
- const sliceBranch = getSliceBranchName(milestoneId, sliceId, worktreeName);
265
-
266
- // Verify slice branch exists
267
- if (!nativeBranchExists(cwd, sliceBranch)) {
268
- throw new Error(`Slice branch "${sliceBranch}" does not exist`);
269
- }
270
-
271
- // Verify slice has commits ahead of milestone branch
272
- const commitCount = nativeCommitCountBetween(cwd, milestoneBranch, sliceBranch);
273
- if (commitCount === 0) {
274
- throw new Error(
275
- `Slice branch "${sliceBranch}" has no commits ahead of "${milestoneBranch}"`,
276
- );
277
- }
278
-
279
- // Checkout milestone branch (with self-healing reset)
280
- recoverCheckout(cwd, milestoneBranch);
281
-
282
- // Build rich commit message (replicates GitServiceImpl.buildRichCommitMessage format)
283
- const commitType = inferCommitType(sliceTitle);
284
- const subject = `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`;
285
-
286
- let message = subject;
287
- try {
288
- const logOutput = execSync(
289
- `git log --oneline --format=%s ${milestoneBranch}..${sliceBranch}`,
290
- { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
291
- ).trim();
292
-
293
- if (logOutput) {
294
- const subjects = logOutput.split("\n").filter(Boolean);
295
- const MAX_ENTRIES = 20;
296
- const truncated = subjects.length > MAX_ENTRIES;
297
- const displayed = truncated ? subjects.slice(0, MAX_ENTRIES) : subjects;
298
- const taskLines = displayed.map(s => `- ${s}`).join("\n");
299
- const truncationLine = truncated
300
- ? `\n- ... and ${subjects.length - MAX_ENTRIES} more`
301
- : "";
302
- message = `${subject}\n\nTasks:\n${taskLines}${truncationLine}\n\nBranch: ${sliceBranch}`;
303
- }
304
- } catch {
305
- // Fall back to subject-only message
306
- }
307
-
308
- // Merge --no-ff (with self-healing retry for transient failures)
309
- try {
310
- withMergeHeal(cwd, () => {
311
- execSync(`git merge --no-ff -m "${message.replace(/"/g, '\\"')}" ${sliceBranch}`, {
312
- cwd,
313
- stdio: ["ignore", "pipe", "pipe"],
314
- encoding: "utf-8",
315
- });
316
- });
317
- } catch (err) {
318
- if (err instanceof MergeConflictError) {
319
- // Re-throw with correct branch context
320
- throw new MergeConflictError(
321
- err.conflictedFiles,
322
- err.strategy,
323
- sliceBranch,
324
- milestoneBranch,
325
- );
326
- }
327
- throw err;
328
- }
329
-
330
- // Delete slice branch
331
- let deletedBranch = false;
332
- try {
333
- execSync(`git branch -d ${sliceBranch}`, {
334
- cwd,
335
- stdio: ["ignore", "pipe", "pipe"],
336
- encoding: "utf-8",
337
- });
338
- deletedBranch = true;
339
- } catch {
340
- // Branch deletion is best-effort
341
- }
342
-
343
- return {
344
- branch: sliceBranch,
345
- mergedCommitMessage: message,
346
- deletedBranch,
347
- };
348
- }
349
-
350
188
  // ─── Merge Milestone -> Main ───────────────────────────────────────────────
351
189
 
352
190
  /**
@@ -361,7 +199,8 @@ function autoCommitDirtyState(cwd: string): boolean {
361
199
  encoding: "utf-8",
362
200
  }).trim();
363
201
  if (!status) return false;
364
- execSync('git add -A && git commit -m "chore: auto-commit before milestone merge"', {
202
+ execFileSync("git", ["add", "-A"], { cwd, stdio: "pipe" });
203
+ execFileSync("git", ["commit", "-m", "chore: auto-commit before milestone merge"], {
365
204
  cwd,
366
205
  stdio: ["ignore", "pipe", "pipe"],
367
206
  encoding: "utf-8",
@@ -413,8 +252,12 @@ export function mergeMilestoneToMain(
413
252
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
414
253
  const mainBranch = prefs.main_branch || "main";
415
254
 
416
- // 5. Checkout main (with self-healing reset)
417
- recoverCheckout(originalBasePath_, mainBranch);
255
+ // 5. Checkout main
256
+ execSync(`git checkout ${mainBranch}`, {
257
+ cwd: originalBasePath_,
258
+ stdio: ["ignore", "pipe", "pipe"],
259
+ encoding: "utf-8",
260
+ });
418
261
 
419
262
  // 6. Build rich commit message
420
263
  const milestoneTitle = roadmap.title.replace(/^M\d+:\s*/, "").trim() || milestoneId;
@@ -426,32 +269,35 @@ export function mergeMilestoneToMain(
426
269
  }
427
270
  const commitMessage = subject + body;
428
271
 
429
- // 7. Squash merge (with self-healing retry for transient failures)
272
+ // 7. Squash merge
430
273
  try {
431
- withMergeHeal(originalBasePath_, () => {
432
- execSync(`git merge --squash ${milestoneBranch}`, {
274
+ execSync(`git merge --squash ${milestoneBranch}`, {
275
+ cwd: originalBasePath_,
276
+ stdio: ["ignore", "pipe", "pipe"],
277
+ encoding: "utf-8",
278
+ });
279
+ } catch (mergeErr) {
280
+ // Check for real conflicts
281
+ try {
282
+ const conflictOutput = execSync("git diff --name-only --diff-filter=U", {
433
283
  cwd: originalBasePath_,
434
- stdio: ["ignore", "pipe", "pipe"],
435
284
  encoding: "utf-8",
436
- });
437
- });
438
- } catch (err) {
439
- if (err instanceof MergeConflictError) {
440
- // Re-throw with correct branch context
441
- throw new MergeConflictError(
442
- err.conflictedFiles,
443
- err.strategy,
444
- milestoneBranch,
445
- mainBranch,
446
- );
285
+ stdio: ["pipe", "pipe", "pipe"],
286
+ }).trim();
287
+ if (conflictOutput) {
288
+ const conflictedFiles = conflictOutput.split("\n").filter(Boolean);
289
+ throw new MergeConflictError(conflictedFiles, "squash", milestoneBranch, mainBranch);
290
+ }
291
+ } catch (diffErr) {
292
+ if (diffErr instanceof MergeConflictError) throw diffErr;
447
293
  }
448
- // Possibly "already up to date" -- fall through to commit which will handle nothing-to-commit
294
+ // No conflicts detected — possibly "already up to date", fall through to commit
449
295
  }
450
296
 
451
297
  // 8. Commit (handle nothing-to-commit gracefully)
452
298
  let nothingToCommit = false;
453
299
  try {
454
- execSync(`git commit -m ${JSON.stringify(commitMessage)}`, {
300
+ execFileSync("git", ["commit", "-m", commitMessage], {
455
301
  cwd: originalBasePath_,
456
302
  stdio: ["ignore", "pipe", "pipe"],
457
303
  encoding: "utf-8",