gsd-pi 2.13.1 → 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 (93) 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 +25 -182
  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 +24 -1
  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/derive-state.test.ts +35 -0
  21. package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  22. package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +1 -105
  23. package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  24. package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  25. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  26. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  27. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +10 -90
  28. package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  29. package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  30. package/dist/resources/extensions/gsd/types.ts +0 -1
  31. package/dist/resources/extensions/gsd/worktree.ts +7 -65
  32. package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  33. package/package.json +1 -1
  34. package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
  35. package/packages/pi-ai/dist/providers/google.js +12 -4
  36. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  37. package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
  38. package/packages/pi-ai/dist/providers/mistral.js +10 -2
  39. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  40. package/packages/pi-ai/src/providers/google.ts +20 -8
  41. package/packages/pi-ai/src/providers/mistral.ts +14 -2
  42. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
  43. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
  45. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
  47. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +12 -3
  52. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
  54. package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
  55. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
  56. package/packages/pi-tui/dist/components/input.d.ts +1 -0
  57. package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
  58. package/packages/pi-tui/dist/components/input.js +10 -0
  59. package/packages/pi-tui/dist/components/input.js.map +1 -1
  60. package/packages/pi-tui/src/components/input.ts +11 -0
  61. package/src/resources/extensions/gsd/auto-worktree.ts +25 -182
  62. package/src/resources/extensions/gsd/auto.ts +252 -370
  63. package/src/resources/extensions/gsd/commands.ts +118 -34
  64. package/src/resources/extensions/gsd/doctor.ts +24 -1
  65. package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
  66. package/src/resources/extensions/gsd/git-service.ts +8 -431
  67. package/src/resources/extensions/gsd/gitignore.ts +11 -4
  68. package/src/resources/extensions/gsd/guided-flow.ts +141 -5
  69. package/src/resources/extensions/gsd/preferences.ts +18 -17
  70. package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
  71. package/src/resources/extensions/gsd/prompts/queue.md +7 -1
  72. package/src/resources/extensions/gsd/state.ts +26 -8
  73. package/src/resources/extensions/gsd/templates/state.md +0 -1
  74. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
  75. package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
  76. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  77. package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +1 -105
  78. package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  79. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  80. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  81. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  82. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +10 -90
  83. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  84. package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  85. package/src/resources/extensions/gsd/types.ts +0 -1
  86. package/src/resources/extensions/gsd/worktree.ts +7 -65
  87. package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  88. package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  89. package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  90. package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
  91. package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  92. package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  93. 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)) {
@@ -14,20 +14,9 @@ import {
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,48 +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
- // Use unquoted glob pattern — single quotes are not interpreted by cmd.exe on Windows,
58
- // causing the pattern to match literally instead of as a glob.
59
- const output = execSync("git branch --list gsd/*/*", {
60
- cwd: basePath,
61
- stdio: ["ignore", "pipe", "pipe"],
62
- encoding: "utf-8",
63
- }).trim();
64
- if (output) return false; // Legacy branch-per-slice project
65
- } catch {
66
- // If git command fails, default to worktree
67
- }
68
-
69
- return true; // New project default
70
- }
71
-
72
- /**
73
- * Resolve the merge_to_main preference value.
74
- * Returns "milestone" (default) or "slice".
75
- */
76
- export function getMergeToMainMode(): "milestone" | "slice" {
77
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
78
- return prefs?.merge_to_main ?? "milestone";
79
- }
80
-
81
28
  // ─── Git Helpers (local, mirrors worktree-command.ts pattern) ──────────────
82
29
 
83
30
  function resolveGitHeadPath(dir: string): string | null {
@@ -238,117 +185,6 @@ export function getAutoWorktreeOriginalBase(): string | null {
238
185
  return originalBase;
239
186
  }
240
187
 
241
- // ─── Merge Slice -> Milestone ───────────────────────────────────────────────
242
-
243
- /**
244
- * Merge a completed slice branch into the milestone branch via `--no-ff`.
245
- *
246
- * Worktree-mode merge: `.gsd/` is local to the worktree (not tracked in
247
- * git), so there are zero `.gsd/` conflict resolution concerns. No runtime
248
- * exclusion untracking, no `--theirs` checkout, no snapshot creation.
249
- *
250
- * On conflict: throws MergeConflictError with conflicted file list.
251
- * On success: deletes the slice branch and returns MergeSliceResult.
252
- */
253
- export function mergeSliceToMilestone(
254
- basePath: string,
255
- milestoneId: string,
256
- sliceId: string,
257
- sliceTitle: string,
258
- ): MergeSliceResult {
259
- if (!isInAutoWorktree(basePath)) {
260
- throw new Error("mergeSliceToMilestone called outside auto-worktree");
261
- }
262
-
263
- const cwd = process.cwd();
264
- const milestoneBranch = autoWorktreeBranch(milestoneId);
265
- const worktreeName = detectWorktreeName(cwd);
266
- const sliceBranch = getSliceBranchName(milestoneId, sliceId, worktreeName);
267
-
268
- // Verify slice branch exists
269
- if (!nativeBranchExists(cwd, sliceBranch)) {
270
- throw new Error(`Slice branch "${sliceBranch}" does not exist`);
271
- }
272
-
273
- // Verify slice has commits ahead of milestone branch
274
- const commitCount = nativeCommitCountBetween(cwd, milestoneBranch, sliceBranch);
275
- if (commitCount === 0) {
276
- throw new Error(
277
- `Slice branch "${sliceBranch}" has no commits ahead of "${milestoneBranch}"`,
278
- );
279
- }
280
-
281
- // Checkout milestone branch (with self-healing reset)
282
- recoverCheckout(cwd, milestoneBranch);
283
-
284
- // Build rich commit message (replicates GitServiceImpl.buildRichCommitMessage format)
285
- const commitType = inferCommitType(sliceTitle);
286
- const subject = `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`;
287
-
288
- let message = subject;
289
- try {
290
- const logOutput = execSync(
291
- `git log --oneline --format=%s ${milestoneBranch}..${sliceBranch}`,
292
- { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
293
- ).trim();
294
-
295
- if (logOutput) {
296
- const subjects = logOutput.split("\n").filter(Boolean);
297
- const MAX_ENTRIES = 20;
298
- const truncated = subjects.length > MAX_ENTRIES;
299
- const displayed = truncated ? subjects.slice(0, MAX_ENTRIES) : subjects;
300
- const taskLines = displayed.map(s => `- ${s}`).join("\n");
301
- const truncationLine = truncated
302
- ? `\n- ... and ${subjects.length - MAX_ENTRIES} more`
303
- : "";
304
- message = `${subject}\n\nTasks:\n${taskLines}${truncationLine}\n\nBranch: ${sliceBranch}`;
305
- }
306
- } catch {
307
- // Fall back to subject-only message
308
- }
309
-
310
- // Merge --no-ff (with self-healing retry for transient failures)
311
- try {
312
- withMergeHeal(cwd, () => {
313
- execFileSync("git", ["merge", "--no-ff", "-m", message, sliceBranch], {
314
- cwd,
315
- stdio: ["ignore", "pipe", "pipe"],
316
- encoding: "utf-8",
317
- });
318
- });
319
- } catch (err) {
320
- if (err instanceof MergeConflictError) {
321
- // Re-throw with correct branch context
322
- throw new MergeConflictError(
323
- err.conflictedFiles,
324
- err.strategy,
325
- sliceBranch,
326
- milestoneBranch,
327
- );
328
- }
329
- throw err;
330
- }
331
-
332
- // Delete slice branch
333
- let deletedBranch = false;
334
- try {
335
- execSync(`git branch -d ${sliceBranch}`, {
336
- cwd,
337
- stdio: ["ignore", "pipe", "pipe"],
338
- encoding: "utf-8",
339
- });
340
- deletedBranch = true;
341
- } catch {
342
- // Branch deletion is best-effort
343
- }
344
-
345
- return {
346
- branch: sliceBranch,
347
- mergedCommitMessage: message,
348
- deletedBranch,
349
- };
350
- }
351
-
352
188
  // ─── Merge Milestone -> Main ───────────────────────────────────────────────
353
189
 
354
190
  /**
@@ -416,8 +252,12 @@ export function mergeMilestoneToMain(
416
252
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
417
253
  const mainBranch = prefs.main_branch || "main";
418
254
 
419
- // 5. Checkout main (with self-healing reset)
420
- 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
+ });
421
261
 
422
262
  // 6. Build rich commit message
423
263
  const milestoneTitle = roadmap.title.replace(/^M\d+:\s*/, "").trim() || milestoneId;
@@ -429,26 +269,29 @@ export function mergeMilestoneToMain(
429
269
  }
430
270
  const commitMessage = subject + body;
431
271
 
432
- // 7. Squash merge (with self-healing retry for transient failures)
272
+ // 7. Squash merge
433
273
  try {
434
- withMergeHeal(originalBasePath_, () => {
435
- 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", {
436
283
  cwd: originalBasePath_,
437
- stdio: ["ignore", "pipe", "pipe"],
438
284
  encoding: "utf-8",
439
- });
440
- });
441
- } catch (err) {
442
- if (err instanceof MergeConflictError) {
443
- // Re-throw with correct branch context
444
- throw new MergeConflictError(
445
- err.conflictedFiles,
446
- err.strategy,
447
- milestoneBranch,
448
- mainBranch,
449
- );
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;
450
293
  }
451
- // 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
452
295
  }
453
296
 
454
297
  // 8. Commit (handle nothing-to-commit gracefully)