gsd-pi 2.11.0 → 2.13.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.
- package/dist/cli.js +18 -1
- package/dist/onboarding.js +3 -0
- package/dist/resource-loader.d.ts +2 -0
- package/dist/resource-loader.js +36 -1
- package/dist/resources/extensions/bg-shell/index.ts +51 -7
- package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/dist/resources/extensions/gsd/auto.ts +381 -13
- package/dist/resources/extensions/gsd/commands.ts +9 -3
- package/dist/resources/extensions/gsd/doctor.ts +254 -3
- package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/dist/resources/extensions/gsd/git-service.ts +11 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +81 -9
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/dist/resources/extensions/gsd/preferences.ts +209 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/system.md +32 -29
- package/dist/resources/extensions/gsd/templates/context.md +1 -1
- package/dist/resources/extensions/gsd/templates/state.md +3 -3
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/dist/resources/extensions/gsd/types.ts +109 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
- package/dist/resources/extensions/search-the-web/provider.ts +19 -2
- package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/dist/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/dist/resources/extensions/search-the-web/tool-search.ts +62 -3
- package/dist/wizard.js +1 -0
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +169 -55
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +13 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +16 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +91 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +273 -63
- package/packages/pi-agent-core/src/agent.ts +24 -0
- package/packages/pi-agent-core/src/types.ts +98 -0
- package/packages/pi-ai/dist/env-api-keys.js +1 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +314 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +236 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/env-api-keys.ts +1 -0
- package/packages/pi-ai/src/models.generated.ts +236 -0
- package/packages/pi-ai/src/types.ts +2 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +2 -1
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +69 -8
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +2 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +76 -7
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
- package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
- package/packages/pi-tui/dist/components/editor.d.ts +11 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +64 -6
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +71 -6
- package/src/resources/extensions/bg-shell/index.ts +51 -7
- package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/src/resources/extensions/gsd/auto.ts +381 -13
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/doctor.ts +254 -3
- package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/src/resources/extensions/gsd/git-service.ts +11 -0
- package/src/resources/extensions/gsd/guided-flow.ts +81 -9
- package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/src/resources/extensions/gsd/preferences.ts +209 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/queue.md +3 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +32 -29
- package/src/resources/extensions/gsd/templates/context.md +1 -1
- package/src/resources/extensions/gsd/templates/state.md +3 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/src/resources/extensions/gsd/types.ts +109 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/src/resources/extensions/search-the-web/native-search.ts +15 -10
- package/src/resources/extensions/search-the-web/provider.ts +19 -2
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Auto-Worktree -- lifecycle management for auto-mode worktrees.
|
|
3
|
+
*
|
|
4
|
+
* Auto-mode creates worktrees with `milestone/<MID>` branches (distinct from
|
|
5
|
+
* manual `/worktree` which uses `worktree/<name>` branches). This module
|
|
6
|
+
* manages create, enter, detect, and teardown for auto-mode worktrees.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, realpathSync, utimesSync } from "node:fs";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import {
|
|
13
|
+
createWorktree,
|
|
14
|
+
removeWorktree,
|
|
15
|
+
worktreePath,
|
|
16
|
+
} from "./worktree-manager.js";
|
|
17
|
+
import {
|
|
18
|
+
detectWorktreeName,
|
|
19
|
+
getSliceBranchName,
|
|
20
|
+
} from "./worktree.js";
|
|
21
|
+
import {
|
|
22
|
+
MergeConflictError,
|
|
23
|
+
inferCommitType,
|
|
24
|
+
} 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
|
+
import { parseRoadmap } from "./files.js";
|
|
32
|
+
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
33
|
+
|
|
34
|
+
// ─── Module State ──────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/** Original project root before chdir into auto-worktree. */
|
|
37
|
+
let originalBase: string | null = null;
|
|
38
|
+
|
|
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
|
+
// ─── Git Helpers (local, mirrors worktree-command.ts pattern) ──────────────
|
|
80
|
+
|
|
81
|
+
function resolveGitHeadPath(dir: string): string | null {
|
|
82
|
+
const gitPath = join(dir, ".git");
|
|
83
|
+
if (!existsSync(gitPath)) return null;
|
|
84
|
+
try {
|
|
85
|
+
const content = readFileSync(gitPath, "utf8").trim();
|
|
86
|
+
if (content.startsWith("gitdir: ")) {
|
|
87
|
+
const gitDir = resolve(dir, content.slice(8));
|
|
88
|
+
const headPath = join(gitDir, "HEAD");
|
|
89
|
+
return existsSync(headPath) ? headPath : null;
|
|
90
|
+
}
|
|
91
|
+
const headPath = join(dir, ".git", "HEAD");
|
|
92
|
+
return existsSync(headPath) ? headPath : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Nudge pi's FooterDataProvider to re-read the git branch after chdir.
|
|
100
|
+
* Touches HEAD in both old and new cwd to fire the fs watcher.
|
|
101
|
+
*/
|
|
102
|
+
function nudgeGitBranchCache(previousCwd: string): void {
|
|
103
|
+
const now = new Date();
|
|
104
|
+
for (const dir of [previousCwd, process.cwd()]) {
|
|
105
|
+
try {
|
|
106
|
+
const headPath = resolveGitHeadPath(dir);
|
|
107
|
+
if (headPath) utimesSync(headPath, now, now);
|
|
108
|
+
} catch {
|
|
109
|
+
// Best-effort
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getCurrentBranch(cwd: string): string {
|
|
115
|
+
try {
|
|
116
|
+
return execSync("git branch --show-current", {
|
|
117
|
+
cwd,
|
|
118
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
119
|
+
encoding: "utf-8",
|
|
120
|
+
}).trim();
|
|
121
|
+
} catch {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Auto-Worktree Branch Naming ───────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export function autoWorktreeBranch(milestoneId: string): string {
|
|
129
|
+
return `milestone/${milestoneId}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a new auto-worktree for a milestone, chdir into it, and store
|
|
136
|
+
* the original base path for later teardown.
|
|
137
|
+
*
|
|
138
|
+
* Atomic: chdir + originalBase update happen in the same try block
|
|
139
|
+
* to prevent split-brain.
|
|
140
|
+
*/
|
|
141
|
+
export function createAutoWorktree(basePath: string, milestoneId: string): string {
|
|
142
|
+
const branch = autoWorktreeBranch(milestoneId);
|
|
143
|
+
const info = createWorktree(basePath, milestoneId, { branch });
|
|
144
|
+
const previousCwd = process.cwd();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
process.chdir(info.path);
|
|
148
|
+
originalBase = basePath;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// If chdir fails, the worktree was created but we couldn't enter it.
|
|
151
|
+
// Don't store originalBase -- caller can retry or clean up.
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
nudgeGitBranchCache(previousCwd);
|
|
158
|
+
return info.path;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Teardown an auto-worktree: chdir back to original base, then remove
|
|
163
|
+
* the worktree and its branch.
|
|
164
|
+
*/
|
|
165
|
+
export function teardownAutoWorktree(originalBasePath: string, milestoneId: string): void {
|
|
166
|
+
const branch = autoWorktreeBranch(milestoneId);
|
|
167
|
+
const previousCwd = process.cwd();
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
process.chdir(originalBasePath);
|
|
171
|
+
originalBase = null;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
nudgeGitBranchCache(previousCwd);
|
|
179
|
+
removeWorktree(originalBasePath, milestoneId, { branch });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Detect if the process is currently inside an auto-worktree.
|
|
184
|
+
* Checks both module state and git branch prefix.
|
|
185
|
+
*/
|
|
186
|
+
export function isInAutoWorktree(basePath: string): boolean {
|
|
187
|
+
if (!originalBase) return false;
|
|
188
|
+
const cwd = process.cwd();
|
|
189
|
+
const resolvedBase = existsSync(basePath) ? realpathSync(basePath) : basePath;
|
|
190
|
+
const wtDir = join(resolvedBase, ".gsd", "worktrees");
|
|
191
|
+
if (!cwd.startsWith(wtDir)) return false;
|
|
192
|
+
const branch = getCurrentBranch(cwd);
|
|
193
|
+
return branch.startsWith("milestone/");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the filesystem path for an auto-worktree, or null if it doesn't exist.
|
|
198
|
+
*/
|
|
199
|
+
export function getAutoWorktreePath(basePath: string, milestoneId: string): string | null {
|
|
200
|
+
const p = worktreePath(basePath, milestoneId);
|
|
201
|
+
return existsSync(p) ? p : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Enter an existing auto-worktree (chdir into it, store originalBase).
|
|
206
|
+
* Use for resume -- the worktree already exists from a prior create.
|
|
207
|
+
*
|
|
208
|
+
* Atomic: chdir + originalBase update in same try block.
|
|
209
|
+
*/
|
|
210
|
+
export function enterAutoWorktree(basePath: string, milestoneId: string): string {
|
|
211
|
+
const p = worktreePath(basePath, milestoneId);
|
|
212
|
+
if (!existsSync(p)) {
|
|
213
|
+
throw new Error(`Auto-worktree for ${milestoneId} does not exist at ${p}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const previousCwd = process.cwd();
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
process.chdir(p);
|
|
220
|
+
originalBase = basePath;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Failed to enter auto-worktree at ${p}: ${err instanceof Error ? err.message : String(err)}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
nudgeGitBranchCache(previousCwd);
|
|
228
|
+
return p;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the original project root stored when entering an auto-worktree.
|
|
233
|
+
* Returns null if not currently in an auto-worktree.
|
|
234
|
+
*/
|
|
235
|
+
export function getAutoWorktreeOriginalBase(): string | null {
|
|
236
|
+
return originalBase;
|
|
237
|
+
}
|
|
238
|
+
|
|
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
|
+
// ─── Merge Milestone -> Main ───────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Auto-commit any dirty (uncommitted) state in the given directory.
|
|
354
|
+
* Returns true if a commit was made, false if working tree was clean.
|
|
355
|
+
*/
|
|
356
|
+
function autoCommitDirtyState(cwd: string): boolean {
|
|
357
|
+
try {
|
|
358
|
+
const status = execSync("git status --porcelain", {
|
|
359
|
+
cwd,
|
|
360
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
361
|
+
encoding: "utf-8",
|
|
362
|
+
}).trim();
|
|
363
|
+
if (!status) return false;
|
|
364
|
+
execSync('git add -A && git commit -m "chore: auto-commit before milestone merge"', {
|
|
365
|
+
cwd,
|
|
366
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
367
|
+
encoding: "utf-8",
|
|
368
|
+
});
|
|
369
|
+
return true;
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Squash-merge the milestone branch into main with a rich commit message
|
|
377
|
+
* listing all completed slices, then tear down the worktree.
|
|
378
|
+
*
|
|
379
|
+
* Sequence:
|
|
380
|
+
* 1. Auto-commit dirty worktree state
|
|
381
|
+
* 2. chdir to originalBasePath
|
|
382
|
+
* 3. git checkout main
|
|
383
|
+
* 4. git merge --squash milestone/<MID>
|
|
384
|
+
* 5. git commit with rich message
|
|
385
|
+
* 6. Auto-push if enabled
|
|
386
|
+
* 7. Delete milestone branch
|
|
387
|
+
* 8. Remove worktree directory
|
|
388
|
+
* 9. Clear originalBase
|
|
389
|
+
*
|
|
390
|
+
* On merge conflict: throws MergeConflictError.
|
|
391
|
+
* On "nothing to commit" after squash: handles gracefully (no error).
|
|
392
|
+
*/
|
|
393
|
+
export function mergeMilestoneToMain(
|
|
394
|
+
originalBasePath_: string,
|
|
395
|
+
milestoneId: string,
|
|
396
|
+
roadmapContent: string,
|
|
397
|
+
): { commitMessage: string; pushed: boolean } {
|
|
398
|
+
const worktreeCwd = process.cwd();
|
|
399
|
+
const milestoneBranch = autoWorktreeBranch(milestoneId);
|
|
400
|
+
|
|
401
|
+
// 1. Auto-commit dirty state in worktree before leaving
|
|
402
|
+
autoCommitDirtyState(worktreeCwd);
|
|
403
|
+
|
|
404
|
+
// 2. Parse roadmap for slice listing
|
|
405
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
406
|
+
const completedSlices = roadmap.slices.filter(s => s.done);
|
|
407
|
+
|
|
408
|
+
// 3. chdir to original base
|
|
409
|
+
const previousCwd = process.cwd();
|
|
410
|
+
process.chdir(originalBasePath_);
|
|
411
|
+
|
|
412
|
+
// 4. Resolve main branch from preferences
|
|
413
|
+
const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
414
|
+
const mainBranch = prefs.main_branch || "main";
|
|
415
|
+
|
|
416
|
+
// 5. Checkout main (with self-healing reset)
|
|
417
|
+
recoverCheckout(originalBasePath_, mainBranch);
|
|
418
|
+
|
|
419
|
+
// 6. Build rich commit message
|
|
420
|
+
const milestoneTitle = roadmap.title.replace(/^M\d+:\s*/, "").trim() || milestoneId;
|
|
421
|
+
const subject = `feat(${milestoneId}): ${milestoneTitle}`;
|
|
422
|
+
let body = "";
|
|
423
|
+
if (completedSlices.length > 0) {
|
|
424
|
+
const sliceLines = completedSlices.map(s => `- ${s.id}: ${s.title}`).join("\n");
|
|
425
|
+
body = `\n\nCompleted slices:\n${sliceLines}\n\nBranch: ${milestoneBranch}`;
|
|
426
|
+
}
|
|
427
|
+
const commitMessage = subject + body;
|
|
428
|
+
|
|
429
|
+
// 7. Squash merge (with self-healing retry for transient failures)
|
|
430
|
+
try {
|
|
431
|
+
withMergeHeal(originalBasePath_, () => {
|
|
432
|
+
execSync(`git merge --squash ${milestoneBranch}`, {
|
|
433
|
+
cwd: originalBasePath_,
|
|
434
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
435
|
+
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
|
+
);
|
|
447
|
+
}
|
|
448
|
+
// Possibly "already up to date" -- fall through to commit which will handle nothing-to-commit
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 8. Commit (handle nothing-to-commit gracefully)
|
|
452
|
+
let nothingToCommit = false;
|
|
453
|
+
try {
|
|
454
|
+
execSync(`git commit -m ${JSON.stringify(commitMessage)}`, {
|
|
455
|
+
cwd: originalBasePath_,
|
|
456
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
457
|
+
encoding: "utf-8",
|
|
458
|
+
});
|
|
459
|
+
} catch (err: unknown) {
|
|
460
|
+
// execSync errors have stdout/stderr as properties -- check those for git's message
|
|
461
|
+
const errObj = err as { stdout?: string; stderr?: string; message?: string };
|
|
462
|
+
const combined = [errObj.stdout, errObj.stderr, errObj.message].filter(Boolean).join(" ");
|
|
463
|
+
if (combined.includes("nothing to commit") || combined.includes("nothing added to commit") || combined.includes("no changes added")) {
|
|
464
|
+
nothingToCommit = true;
|
|
465
|
+
} else {
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 9. Auto-push if enabled
|
|
471
|
+
let pushed = false;
|
|
472
|
+
if (prefs.auto_push === true && !nothingToCommit) {
|
|
473
|
+
const remote = prefs.remote ?? "origin";
|
|
474
|
+
try {
|
|
475
|
+
execSync(`git push ${remote} ${mainBranch}`, {
|
|
476
|
+
cwd: originalBasePath_,
|
|
477
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
478
|
+
encoding: "utf-8",
|
|
479
|
+
});
|
|
480
|
+
pushed = true;
|
|
481
|
+
} catch {
|
|
482
|
+
// Push failure is non-fatal
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 10. Remove worktree directory first (must happen before branch deletion)
|
|
487
|
+
try {
|
|
488
|
+
removeWorktree(originalBasePath_, milestoneId, { branch: null as unknown as string, deleteBranch: false });
|
|
489
|
+
} catch {
|
|
490
|
+
// Best-effort -- worktree dir may already be gone
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 11. Delete milestone branch (after worktree removal so ref is unlocked)
|
|
494
|
+
try {
|
|
495
|
+
execSync(`git branch -D ${milestoneBranch}`, {
|
|
496
|
+
cwd: originalBasePath_,
|
|
497
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
498
|
+
encoding: "utf-8",
|
|
499
|
+
});
|
|
500
|
+
} catch {
|
|
501
|
+
// Best-effort
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 12. Clear module state
|
|
505
|
+
originalBase = null;
|
|
506
|
+
nudgeGitBranchCache(previousCwd);
|
|
507
|
+
|
|
508
|
+
return { commitMessage, pushed };
|
|
509
|
+
}
|