gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
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/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -6,24 +6,40 @@
|
|
|
6
6
|
* manages create, enter, detect, and teardown for auto-mode worktrees.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
cpSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
lstatSync as lstatSyncFn,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { isAbsolute, join } from "node:path";
|
|
11
20
|
import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
|
|
21
|
+
import {
|
|
22
|
+
copyWorktreeDb,
|
|
23
|
+
reconcileWorktreeDb,
|
|
24
|
+
isDbAvailable,
|
|
25
|
+
} from "./gsd-db.js";
|
|
26
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
12
27
|
import { execSync, execFileSync } from "node:child_process";
|
|
28
|
+
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
29
|
+
import { gsdRoot } from "./paths.js";
|
|
13
30
|
import {
|
|
14
31
|
createWorktree,
|
|
15
32
|
removeWorktree,
|
|
16
33
|
worktreePath,
|
|
17
34
|
} from "./worktree-manager.js";
|
|
18
|
-
import { detectWorktreeName, resolveGitHeadPath, nudgeGitBranchCache } from "./worktree.js";
|
|
19
|
-
import { ensureGsdSymlink } from "./repo-identity.js";
|
|
20
35
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
detectWorktreeName,
|
|
37
|
+
resolveGitHeadPath,
|
|
38
|
+
nudgeGitBranchCache,
|
|
39
|
+
} from "./worktree.js";
|
|
40
|
+
import { MergeConflictError, readIntegrationBranch } from "./git-service.js";
|
|
24
41
|
import { parseRoadmap } from "./files.js";
|
|
25
42
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
26
|
-
import { gsdRoot } from "./paths.js";
|
|
27
43
|
import {
|
|
28
44
|
nativeGetCurrentBranch,
|
|
29
45
|
nativeWorkingTreeStatus,
|
|
@@ -38,13 +54,28 @@ import {
|
|
|
38
54
|
nativeBranchDelete,
|
|
39
55
|
nativeBranchExists,
|
|
40
56
|
} from "./native-git-bridge.js";
|
|
41
|
-
import { getErrorMessage } from "./error-utils.js";
|
|
42
57
|
|
|
43
58
|
// ─── Module State ──────────────────────────────────────────────────────────
|
|
44
59
|
|
|
45
60
|
/** Original project root before chdir into auto-worktree. */
|
|
46
61
|
let originalBase: string | null = null;
|
|
47
62
|
|
|
63
|
+
function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
|
|
64
|
+
const gsdDir = gsdRoot(basePath);
|
|
65
|
+
const transientFiles = [
|
|
66
|
+
join(gsdDir, "STATE.md"),
|
|
67
|
+
join(gsdDir, "auto.lock"),
|
|
68
|
+
join(gsdDir, "milestones", milestoneId, `${milestoneId}-META.json`),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
for (const file of transientFiles) {
|
|
72
|
+
try {
|
|
73
|
+
unlinkSync(file);
|
|
74
|
+
} catch {
|
|
75
|
+
/* non-fatal — file may not exist */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
48
79
|
// ─── Worktree ↔ Main Repo Sync (#1311) ──────────────────────────────────────
|
|
49
80
|
|
|
50
81
|
/**
|
|
@@ -61,7 +92,10 @@ let originalBase: string | null = null;
|
|
|
61
92
|
* Only adds missing content — never overwrites existing files in the worktree
|
|
62
93
|
* (the worktree's execution state is authoritative for in-progress work).
|
|
63
94
|
*/
|
|
64
|
-
export function syncGsdStateToWorktree(
|
|
95
|
+
export function syncGsdStateToWorktree(
|
|
96
|
+
mainBasePath: string,
|
|
97
|
+
worktreePath_: string,
|
|
98
|
+
): { synced: string[] } {
|
|
65
99
|
const mainGsd = gsdRoot(mainBasePath);
|
|
66
100
|
const wtGsd = gsdRoot(worktreePath_);
|
|
67
101
|
const synced: string[] = [];
|
|
@@ -78,7 +112,13 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
78
112
|
if (!existsSync(mainGsd) || !existsSync(wtGsd)) return { synced };
|
|
79
113
|
|
|
80
114
|
// Sync root-level .gsd/ files (DECISIONS, REQUIREMENTS, PROJECT, KNOWLEDGE)
|
|
81
|
-
const rootFiles = [
|
|
115
|
+
const rootFiles = [
|
|
116
|
+
"DECISIONS.md",
|
|
117
|
+
"REQUIREMENTS.md",
|
|
118
|
+
"PROJECT.md",
|
|
119
|
+
"KNOWLEDGE.md",
|
|
120
|
+
"OVERRIDES.md",
|
|
121
|
+
];
|
|
82
122
|
for (const f of rootFiles) {
|
|
83
123
|
const src = join(mainGsd, f);
|
|
84
124
|
const dst = join(wtGsd, f);
|
|
@@ -86,7 +126,9 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
86
126
|
try {
|
|
87
127
|
cpSync(src, dst);
|
|
88
128
|
synced.push(f);
|
|
89
|
-
} catch {
|
|
129
|
+
} catch {
|
|
130
|
+
/* non-fatal */
|
|
131
|
+
}
|
|
90
132
|
}
|
|
91
133
|
}
|
|
92
134
|
|
|
@@ -96,9 +138,11 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
96
138
|
if (existsSync(mainMilestonesDir)) {
|
|
97
139
|
try {
|
|
98
140
|
mkdirSync(wtMilestonesDir, { recursive: true });
|
|
99
|
-
const mainMilestones = readdirSync(mainMilestonesDir, {
|
|
100
|
-
|
|
101
|
-
|
|
141
|
+
const mainMilestones = readdirSync(mainMilestonesDir, {
|
|
142
|
+
withFileTypes: true,
|
|
143
|
+
})
|
|
144
|
+
.filter((d) => d.isDirectory() && /^M\d{3}/.test(d.name))
|
|
145
|
+
.map((d) => d.name);
|
|
102
146
|
|
|
103
147
|
for (const mid of mainMilestones) {
|
|
104
148
|
const srcDir = join(mainMilestonesDir, mid);
|
|
@@ -109,12 +153,16 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
109
153
|
try {
|
|
110
154
|
cpSync(srcDir, dstDir, { recursive: true });
|
|
111
155
|
synced.push(`milestones/${mid}/`);
|
|
112
|
-
} catch {
|
|
156
|
+
} catch {
|
|
157
|
+
/* non-fatal */
|
|
158
|
+
}
|
|
113
159
|
} else {
|
|
114
160
|
// Milestone directory exists but may be missing files (stale snapshot).
|
|
115
161
|
// Sync individual top-level milestone files (CONTEXT, ROADMAP, RESEARCH, etc.)
|
|
116
162
|
try {
|
|
117
|
-
const srcFiles = readdirSync(srcDir).filter(
|
|
163
|
+
const srcFiles = readdirSync(srcDir).filter(
|
|
164
|
+
(f) => f.endsWith(".md") || f.endsWith(".json"),
|
|
165
|
+
);
|
|
118
166
|
for (const f of srcFiles) {
|
|
119
167
|
const srcFile = join(srcDir, f);
|
|
120
168
|
const dstFile = join(dstDir, f);
|
|
@@ -125,7 +173,9 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
125
173
|
cpSync(srcFile, dstFile);
|
|
126
174
|
synced.push(`milestones/${mid}/${f}`);
|
|
127
175
|
}
|
|
128
|
-
} catch {
|
|
176
|
+
} catch {
|
|
177
|
+
/* non-fatal */
|
|
178
|
+
}
|
|
129
179
|
}
|
|
130
180
|
}
|
|
131
181
|
|
|
@@ -136,12 +186,16 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
136
186
|
try {
|
|
137
187
|
cpSync(srcSlicesDir, dstSlicesDir, { recursive: true });
|
|
138
188
|
synced.push(`milestones/${mid}/slices/`);
|
|
139
|
-
} catch {
|
|
189
|
+
} catch {
|
|
190
|
+
/* non-fatal */
|
|
191
|
+
}
|
|
140
192
|
} else if (existsSync(srcSlicesDir) && existsSync(dstSlicesDir)) {
|
|
141
193
|
// Both exist — sync missing slice directories
|
|
142
|
-
const srcSlices = readdirSync(srcSlicesDir, {
|
|
143
|
-
|
|
144
|
-
|
|
194
|
+
const srcSlices = readdirSync(srcSlicesDir, {
|
|
195
|
+
withFileTypes: true,
|
|
196
|
+
})
|
|
197
|
+
.filter((d) => d.isDirectory())
|
|
198
|
+
.map((d) => d.name);
|
|
145
199
|
for (const sid of srcSlices) {
|
|
146
200
|
const srcSlice = join(srcSlicesDir, sid);
|
|
147
201
|
const dstSlice = join(dstSlicesDir, sid);
|
|
@@ -149,14 +203,20 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
149
203
|
try {
|
|
150
204
|
cpSync(srcSlice, dstSlice, { recursive: true });
|
|
151
205
|
synced.push(`milestones/${mid}/slices/${sid}/`);
|
|
152
|
-
} catch {
|
|
206
|
+
} catch {
|
|
207
|
+
/* non-fatal */
|
|
208
|
+
}
|
|
153
209
|
}
|
|
154
210
|
}
|
|
155
211
|
}
|
|
156
|
-
} catch {
|
|
212
|
+
} catch {
|
|
213
|
+
/* non-fatal */
|
|
214
|
+
}
|
|
157
215
|
}
|
|
158
216
|
}
|
|
159
|
-
} catch {
|
|
217
|
+
} catch {
|
|
218
|
+
/* non-fatal */
|
|
219
|
+
}
|
|
160
220
|
}
|
|
161
221
|
|
|
162
222
|
return { synced };
|
|
@@ -170,7 +230,11 @@ export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: stri
|
|
|
170
230
|
* Only syncs .gsd/milestones/ content — root-level files (DECISIONS, REQUIREMENTS, etc.)
|
|
171
231
|
* are handled by the merge itself.
|
|
172
232
|
*/
|
|
173
|
-
export function syncWorktreeStateBack(
|
|
233
|
+
export function syncWorktreeStateBack(
|
|
234
|
+
mainBasePath: string,
|
|
235
|
+
worktreePath: string,
|
|
236
|
+
milestoneId: string,
|
|
237
|
+
): { synced: string[] } {
|
|
174
238
|
const mainGsd = gsdRoot(mainBasePath);
|
|
175
239
|
const wtGsd = gsdRoot(worktreePath);
|
|
176
240
|
const synced: string[] = [];
|
|
@@ -199,40 +263,53 @@ export function syncWorktreeStateBack(mainBasePath: string, worktreePath: string
|
|
|
199
263
|
try {
|
|
200
264
|
cpSync(src, dst, { force: true });
|
|
201
265
|
synced.push(`milestones/${milestoneId}/${entry.name}`);
|
|
202
|
-
} catch {
|
|
266
|
+
} catch {
|
|
267
|
+
/* non-fatal */
|
|
268
|
+
}
|
|
203
269
|
}
|
|
204
270
|
}
|
|
205
|
-
} catch {
|
|
271
|
+
} catch {
|
|
272
|
+
/* non-fatal */
|
|
273
|
+
}
|
|
206
274
|
|
|
207
275
|
// Sync slice-level files (summaries, UATs)
|
|
208
276
|
const wtSlicesDir = join(wtMilestoneDir, "slices");
|
|
209
277
|
const mainSlicesDir = join(mainMilestoneDir, "slices");
|
|
210
278
|
if (existsSync(wtSlicesDir)) {
|
|
211
279
|
try {
|
|
212
|
-
for (const sliceEntry of readdirSync(wtSlicesDir, {
|
|
280
|
+
for (const sliceEntry of readdirSync(wtSlicesDir, {
|
|
281
|
+
withFileTypes: true,
|
|
282
|
+
})) {
|
|
213
283
|
if (!sliceEntry.isDirectory()) continue;
|
|
214
284
|
const sid = sliceEntry.name;
|
|
215
285
|
const wtSliceDir = join(wtSlicesDir, sid);
|
|
216
286
|
const mainSliceDir = join(mainSlicesDir, sid);
|
|
217
287
|
mkdirSync(mainSliceDir, { recursive: true });
|
|
218
288
|
|
|
219
|
-
for (const fileEntry of readdirSync(wtSliceDir, {
|
|
289
|
+
for (const fileEntry of readdirSync(wtSliceDir, {
|
|
290
|
+
withFileTypes: true,
|
|
291
|
+
})) {
|
|
220
292
|
if (fileEntry.isFile() && fileEntry.name.endsWith(".md")) {
|
|
221
293
|
const src = join(wtSliceDir, fileEntry.name);
|
|
222
294
|
const dst = join(mainSliceDir, fileEntry.name);
|
|
223
295
|
try {
|
|
224
296
|
cpSync(src, dst, { force: true });
|
|
225
|
-
synced.push(
|
|
226
|
-
|
|
297
|
+
synced.push(
|
|
298
|
+
`milestones/${milestoneId}/slices/${sid}/${fileEntry.name}`,
|
|
299
|
+
);
|
|
300
|
+
} catch {
|
|
301
|
+
/* non-fatal */
|
|
302
|
+
}
|
|
227
303
|
}
|
|
228
304
|
}
|
|
229
305
|
}
|
|
230
|
-
} catch {
|
|
306
|
+
} catch {
|
|
307
|
+
/* non-fatal */
|
|
308
|
+
}
|
|
231
309
|
}
|
|
232
310
|
|
|
233
311
|
return { synced };
|
|
234
312
|
}
|
|
235
|
-
|
|
236
313
|
// ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
|
|
237
314
|
|
|
238
315
|
/**
|
|
@@ -243,7 +320,11 @@ export function syncWorktreeStateBack(mainBasePath: string, worktreePath: string
|
|
|
243
320
|
* Reads the hook path from git.worktree_post_create in preferences.
|
|
244
321
|
* Pass hookPath directly to bypass preference loading (useful for testing).
|
|
245
322
|
*/
|
|
246
|
-
export function runWorktreePostCreateHook(
|
|
323
|
+
export function runWorktreePostCreateHook(
|
|
324
|
+
sourceDir: string,
|
|
325
|
+
worktreeDir: string,
|
|
326
|
+
hookPath?: string,
|
|
327
|
+
): string | null {
|
|
247
328
|
if (hookPath === undefined) {
|
|
248
329
|
const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
249
330
|
hookPath = prefs?.worktree_post_create;
|
|
@@ -270,7 +351,7 @@ export function runWorktreePostCreateHook(sourceDir: string, worktreeDir: string
|
|
|
270
351
|
});
|
|
271
352
|
return null;
|
|
272
353
|
} catch (err) {
|
|
273
|
-
const msg =
|
|
354
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
274
355
|
return `Worktree post-create hook failed: ${msg}`;
|
|
275
356
|
}
|
|
276
357
|
}
|
|
@@ -291,7 +372,110 @@ export function autoWorktreeBranch(milestoneId: string): string {
|
|
|
291
372
|
* to prevent split-brain.
|
|
292
373
|
*/
|
|
293
374
|
|
|
294
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Forward-merge plan checkbox state from the project root into a freshly
|
|
377
|
+
* re-attached worktree (#778).
|
|
378
|
+
*
|
|
379
|
+
* When auto-mode stops via crash (not graceful stop), the milestone branch
|
|
380
|
+
* HEAD may be behind the filesystem state at the project root because
|
|
381
|
+
* syncStateToProjectRoot() runs after every task completion but the final
|
|
382
|
+
* git commit may not have happened before the crash. On restart the worktree
|
|
383
|
+
* is re-attached to the branch HEAD, which has [ ] for the crashed task,
|
|
384
|
+
* causing verifyExpectedArtifact() to fail and triggering an infinite
|
|
385
|
+
* dispatch/skip loop.
|
|
386
|
+
*
|
|
387
|
+
* Fix: after re-attaching, read every *.md plan file in the milestone
|
|
388
|
+
* directory at the project root and apply any [x] checkbox states that are
|
|
389
|
+
* ahead of the worktree version (forward-only: never downgrade [x] → [ ]).
|
|
390
|
+
*
|
|
391
|
+
* This is safe because syncStateToProjectRoot() is the authoritative source
|
|
392
|
+
* of post-task state at the project root — it writes the same [x] the LLM
|
|
393
|
+
* produced, then the auto-commit follows. If the commit never happened, the
|
|
394
|
+
* filesystem copy is still valid and correct.
|
|
395
|
+
*/
|
|
396
|
+
function reconcilePlanCheckboxes(
|
|
397
|
+
projectRoot: string,
|
|
398
|
+
wtPath: string,
|
|
399
|
+
milestoneId: string,
|
|
400
|
+
): void {
|
|
401
|
+
const srcMilestone = join(projectRoot, ".gsd", "milestones", milestoneId);
|
|
402
|
+
const dstMilestone = join(wtPath, ".gsd", "milestones", milestoneId);
|
|
403
|
+
if (!existsSync(srcMilestone) || !existsSync(dstMilestone)) return;
|
|
404
|
+
|
|
405
|
+
// Walk all markdown files in the milestone directory (plans, summaries, etc.)
|
|
406
|
+
function walkMd(dir: string): string[] {
|
|
407
|
+
const results: string[] = [];
|
|
408
|
+
try {
|
|
409
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
410
|
+
const full = join(dir, entry.name);
|
|
411
|
+
if (entry.isDirectory()) {
|
|
412
|
+
results.push(...walkMd(full));
|
|
413
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
414
|
+
results.push(full);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
/* non-fatal */
|
|
419
|
+
}
|
|
420
|
+
return results;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const srcFile of walkMd(srcMilestone)) {
|
|
424
|
+
const rel = srcFile.slice(srcMilestone.length);
|
|
425
|
+
const dstFile = dstMilestone + rel;
|
|
426
|
+
if (!existsSync(dstFile)) continue; // only reconcile existing files
|
|
427
|
+
|
|
428
|
+
let srcContent: string;
|
|
429
|
+
let dstContent: string;
|
|
430
|
+
try {
|
|
431
|
+
srcContent = readFileSync(srcFile, "utf-8");
|
|
432
|
+
dstContent = readFileSync(dstFile, "utf-8");
|
|
433
|
+
} catch {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (srcContent === dstContent) continue;
|
|
438
|
+
|
|
439
|
+
// Extract all checked task IDs from the source (project root)
|
|
440
|
+
// Pattern: - [x] **T<id>: or - [x] **S<id>: (case-insensitive x)
|
|
441
|
+
const checkedRe = /^- \[[xX]\] \*\*([TS]\d+):/gm;
|
|
442
|
+
const srcChecked = new Set<string>();
|
|
443
|
+
for (const m of srcContent.matchAll(checkedRe)) srcChecked.add(m[1]);
|
|
444
|
+
|
|
445
|
+
if (srcChecked.size === 0) continue;
|
|
446
|
+
|
|
447
|
+
// Forward-apply: replace [ ] → [x] for any IDs that are checked in src
|
|
448
|
+
let updated = dstContent;
|
|
449
|
+
let changed = false;
|
|
450
|
+
for (const id of srcChecked) {
|
|
451
|
+
const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
452
|
+
const uncheckedRe = new RegExp(
|
|
453
|
+
`^(- )\\[ \\]( \\*\\*${escapedId}:)`,
|
|
454
|
+
"gm",
|
|
455
|
+
);
|
|
456
|
+
if (uncheckedRe.test(updated)) {
|
|
457
|
+
updated = updated.replace(
|
|
458
|
+
new RegExp(`^(- )\\[ \\]( \\*\\*${escapedId}:)`, "gm"),
|
|
459
|
+
"$1[x]$2",
|
|
460
|
+
);
|
|
461
|
+
changed = true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (changed) {
|
|
466
|
+
try {
|
|
467
|
+
atomicWriteSync(dstFile, updated, "utf-8");
|
|
468
|
+
} catch {
|
|
469
|
+
/* non-fatal */
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function createAutoWorktree(
|
|
476
|
+
basePath: string,
|
|
477
|
+
milestoneId: string,
|
|
478
|
+
): string {
|
|
295
479
|
const branch = autoWorktreeBranch(milestoneId);
|
|
296
480
|
|
|
297
481
|
// Check if the milestone branch already exists — it survives auto-mode
|
|
@@ -303,21 +487,46 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|
|
303
487
|
let info: { name: string; path: string; branch: string; exists: boolean };
|
|
304
488
|
if (branchExists) {
|
|
305
489
|
// Re-attach worktree to the existing milestone branch (preserving commits)
|
|
306
|
-
info = createWorktree(basePath, milestoneId, {
|
|
490
|
+
info = createWorktree(basePath, milestoneId, {
|
|
491
|
+
branch,
|
|
492
|
+
reuseExistingBranch: true,
|
|
493
|
+
});
|
|
307
494
|
} else {
|
|
308
495
|
// Fresh start — create branch from integration branch
|
|
309
|
-
const integrationBranch =
|
|
310
|
-
|
|
496
|
+
const integrationBranch =
|
|
497
|
+
readIntegrationBranch(basePath, milestoneId) ?? undefined;
|
|
498
|
+
info = createWorktree(basePath, milestoneId, {
|
|
499
|
+
branch,
|
|
500
|
+
startPoint: integrationBranch,
|
|
501
|
+
});
|
|
311
502
|
}
|
|
312
503
|
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
504
|
+
// Copy .gsd/ planning artifacts from the source repo into the new worktree.
|
|
505
|
+
// Worktrees are fresh git checkouts — untracked files don't carry over.
|
|
506
|
+
// Planning artifacts may be untracked if the project's .gitignore had a
|
|
507
|
+
// blanket .gsd/ rule (pre-v2.14.0). Without this copy, auto-mode loops
|
|
508
|
+
// on plan-slice because the plan file doesn't exist in the worktree.
|
|
509
|
+
//
|
|
510
|
+
// IMPORTANT: Skip when re-attaching to an existing branch (#759).
|
|
511
|
+
// The branch checkout already has committed artifacts with correct state
|
|
512
|
+
// (e.g. [x] for completed slices). Copying from the project root would
|
|
513
|
+
// overwrite them with stale data ([ ] checkboxes) because the root is
|
|
514
|
+
// not always fully synced.
|
|
515
|
+
if (!branchExists) {
|
|
516
|
+
copyPlanningArtifacts(basePath, info.path);
|
|
517
|
+
} else {
|
|
518
|
+
// Re-attaching to an existing branch: forward-merge any plan checkpoint
|
|
519
|
+
// state from the project root into the worktree (#778).
|
|
520
|
+
//
|
|
521
|
+
// If auto-mode stopped via crash, the milestone branch HEAD may lag behind
|
|
522
|
+
// the project root filesystem because syncStateToProjectRoot() ran after
|
|
523
|
+
// task completion but the auto-commit never fired. On restart the worktree
|
|
524
|
+
// is re-created from the branch HEAD (which has [ ] for the crashed task),
|
|
525
|
+
// causing verifyExpectedArtifact() to return false → stale-key eviction →
|
|
526
|
+
// infinite dispatch/skip loop. Reconciling here ensures the worktree sees
|
|
527
|
+
// the same [x] state that syncStateToProjectRoot() wrote to the root.
|
|
528
|
+
reconcilePlanCheckboxes(basePath, info.path, milestoneId);
|
|
529
|
+
}
|
|
321
530
|
|
|
322
531
|
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
|
323
532
|
const hookError = runWorktreePostCreateHook(basePath, info.path);
|
|
@@ -336,7 +545,7 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|
|
336
545
|
// Don't store originalBase -- caller can retry or clean up.
|
|
337
546
|
throw new GSDError(
|
|
338
547
|
GSD_IO_ERROR,
|
|
339
|
-
`Auto-worktree created at ${info.path} but chdir failed: ${
|
|
548
|
+
`Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
340
549
|
);
|
|
341
550
|
}
|
|
342
551
|
|
|
@@ -344,6 +553,49 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|
|
344
553
|
return info.path;
|
|
345
554
|
}
|
|
346
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Copy .gsd/ planning artifacts from source repo to a new worktree.
|
|
558
|
+
* Copies milestones/, DECISIONS.md, REQUIREMENTS.md, PROJECT.md, QUEUE.md,
|
|
559
|
+
* STATE.md, KNOWLEDGE.md, and OVERRIDES.md.
|
|
560
|
+
* Skips runtime files (auto.lock, metrics.json, etc.) and the worktrees/ dir.
|
|
561
|
+
* Best-effort — failures are non-fatal since auto-mode can recreate artifacts.
|
|
562
|
+
*/
|
|
563
|
+
function copyPlanningArtifacts(srcBase: string, wtPath: string): void {
|
|
564
|
+
const srcGsd = join(srcBase, ".gsd");
|
|
565
|
+
const dstGsd = join(wtPath, ".gsd");
|
|
566
|
+
if (!existsSync(srcGsd)) return;
|
|
567
|
+
|
|
568
|
+
// Copy milestones/ directory (planning files, roadmaps, plans, research)
|
|
569
|
+
safeCopyRecursive(join(srcGsd, "milestones"), join(dstGsd, "milestones"), {
|
|
570
|
+
force: true,
|
|
571
|
+
filter: (src) => !src.endsWith("-META.json"),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Copy top-level planning files
|
|
575
|
+
for (const file of [
|
|
576
|
+
"DECISIONS.md",
|
|
577
|
+
"REQUIREMENTS.md",
|
|
578
|
+
"PROJECT.md",
|
|
579
|
+
"QUEUE.md",
|
|
580
|
+
"STATE.md",
|
|
581
|
+
"KNOWLEDGE.md",
|
|
582
|
+
"OVERRIDES.md",
|
|
583
|
+
]) {
|
|
584
|
+
safeCopy(join(srcGsd, file), join(dstGsd, file), { force: true });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Copy gsd.db if present in source
|
|
588
|
+
const srcDb = join(srcGsd, "gsd.db");
|
|
589
|
+
const destDb = join(dstGsd, "gsd.db");
|
|
590
|
+
if (existsSync(srcDb)) {
|
|
591
|
+
try {
|
|
592
|
+
copyWorktreeDb(srcDb, destDb);
|
|
593
|
+
} catch {
|
|
594
|
+
/* non-fatal */
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
347
599
|
/**
|
|
348
600
|
* Teardown an auto-worktree: chdir back to original base, then remove
|
|
349
601
|
* the worktree and its branch.
|
|
@@ -363,12 +615,15 @@ export function teardownAutoWorktree(
|
|
|
363
615
|
} catch (err) {
|
|
364
616
|
throw new GSDError(
|
|
365
617
|
GSD_IO_ERROR,
|
|
366
|
-
`Failed to chdir back to ${originalBasePath} during teardown: ${
|
|
618
|
+
`Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
|
|
367
619
|
);
|
|
368
620
|
}
|
|
369
621
|
|
|
370
622
|
nudgeGitBranchCache(previousCwd);
|
|
371
|
-
removeWorktree(originalBasePath, milestoneId, {
|
|
623
|
+
removeWorktree(originalBasePath, milestoneId, {
|
|
624
|
+
branch,
|
|
625
|
+
deleteBranch: !preserveBranch,
|
|
626
|
+
});
|
|
372
627
|
}
|
|
373
628
|
|
|
374
629
|
/**
|
|
@@ -376,36 +631,13 @@ export function teardownAutoWorktree(
|
|
|
376
631
|
* Checks both module state and git branch prefix.
|
|
377
632
|
*/
|
|
378
633
|
export function isInAutoWorktree(basePath: string): boolean {
|
|
634
|
+
if (!originalBase) return false;
|
|
379
635
|
const cwd = process.cwd();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (!cwd.startsWith(wtDir)) return false;
|
|
386
|
-
const branch = nativeGetCurrentBranch(cwd);
|
|
387
|
-
return branch.startsWith("milestone/");
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Fallback: infer worktree status structurally when originalBase is null
|
|
391
|
-
// (happens after session restart where module-level state is lost, #1120).
|
|
392
|
-
// Check if cwd is inside a .gsd/worktrees/ directory and has a .git file
|
|
393
|
-
// (worktree marker) pointing to the main repo.
|
|
394
|
-
const worktreeMarker = join(cwd, ".git");
|
|
395
|
-
if (!existsSync(worktreeMarker)) return false;
|
|
396
|
-
try {
|
|
397
|
-
const stat = statSync(worktreeMarker);
|
|
398
|
-
if (stat.isDirectory()) return false; // Main repo has .git dir, not file
|
|
399
|
-
// Worktrees have a .git file with "gitdir: ..." pointing to the main repo
|
|
400
|
-
const gitContent = readFileSync(worktreeMarker, "utf-8").trim();
|
|
401
|
-
if (!gitContent.startsWith("gitdir:")) return false;
|
|
402
|
-
// Verify we're inside a GSD-managed worktree
|
|
403
|
-
if (!detectWorktreeName(cwd)) return false;
|
|
404
|
-
const branch = nativeGetCurrentBranch(cwd);
|
|
405
|
-
return branch.startsWith("milestone/");
|
|
406
|
-
} catch {
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
636
|
+
const resolvedBase = existsSync(basePath) ? realpathSync(basePath) : basePath;
|
|
637
|
+
const wtDir = join(resolvedBase, ".gsd", "worktrees");
|
|
638
|
+
if (!cwd.startsWith(wtDir)) return false;
|
|
639
|
+
const branch = nativeGetCurrentBranch(cwd);
|
|
640
|
+
return branch.startsWith("milestone/");
|
|
409
641
|
}
|
|
410
642
|
|
|
411
643
|
/**
|
|
@@ -416,7 +648,10 @@ export function isInAutoWorktree(basePath: string): boolean {
|
|
|
416
648
|
* gitdir: pointer) rather than just a stray directory. This prevents
|
|
417
649
|
* mis-detection of leftover directories as active worktrees (#695).
|
|
418
650
|
*/
|
|
419
|
-
export function getAutoWorktreePath(
|
|
651
|
+
export function getAutoWorktreePath(
|
|
652
|
+
basePath: string,
|
|
653
|
+
milestoneId: string,
|
|
654
|
+
): string | null {
|
|
420
655
|
const p = worktreePath(basePath, milestoneId);
|
|
421
656
|
if (!existsSync(p)) return null;
|
|
422
657
|
|
|
@@ -440,39 +675,42 @@ export function getAutoWorktreePath(basePath: string, milestoneId: string): stri
|
|
|
440
675
|
*
|
|
441
676
|
* Atomic: chdir + originalBase update in same try block.
|
|
442
677
|
*/
|
|
443
|
-
export function enterAutoWorktree(
|
|
678
|
+
export function enterAutoWorktree(
|
|
679
|
+
basePath: string,
|
|
680
|
+
milestoneId: string,
|
|
681
|
+
): string {
|
|
444
682
|
const p = worktreePath(basePath, milestoneId);
|
|
445
683
|
if (!existsSync(p)) {
|
|
446
|
-
throw new GSDError(
|
|
684
|
+
throw new GSDError(
|
|
685
|
+
GSD_IO_ERROR,
|
|
686
|
+
`Auto-worktree for ${milestoneId} does not exist at ${p}`,
|
|
687
|
+
);
|
|
447
688
|
}
|
|
448
689
|
|
|
449
690
|
// Validate this is a real git worktree, not a stray directory (#695)
|
|
450
691
|
const gitPath = join(p, ".git");
|
|
451
692
|
if (!existsSync(gitPath)) {
|
|
452
|
-
throw new GSDError(
|
|
693
|
+
throw new GSDError(
|
|
694
|
+
GSD_GIT_ERROR,
|
|
695
|
+
`Auto-worktree path ${p} exists but is not a git worktree (no .git)`,
|
|
696
|
+
);
|
|
453
697
|
}
|
|
454
698
|
try {
|
|
455
699
|
const content = readFileSync(gitPath, "utf8").trim();
|
|
456
700
|
if (!content.startsWith("gitdir: ")) {
|
|
457
|
-
throw new GSDError(
|
|
701
|
+
throw new GSDError(
|
|
702
|
+
GSD_GIT_ERROR,
|
|
703
|
+
`Auto-worktree path ${p} has a .git but it is not a worktree gitdir pointer`,
|
|
704
|
+
);
|
|
458
705
|
}
|
|
459
706
|
} catch (err) {
|
|
460
707
|
if (err instanceof Error && err.message.includes("worktree")) throw err;
|
|
461
|
-
throw new GSDError(
|
|
708
|
+
throw new GSDError(
|
|
709
|
+
GSD_IO_ERROR,
|
|
710
|
+
`Auto-worktree path ${p} exists but .git is unreadable`,
|
|
711
|
+
);
|
|
462
712
|
}
|
|
463
713
|
|
|
464
|
-
// Ensure worktree shares external state via symlink (#1311).
|
|
465
|
-
// On resume (enterAutoWorktree), the symlink may be missing if it was
|
|
466
|
-
// created before ensureGsdSymlink existed, or the .gsd/ directory may be
|
|
467
|
-
// a stale git-tracked copy instead of a symlink. Refreshing here ensures
|
|
468
|
-
// the worktree sees the same milestone state as the main repo.
|
|
469
|
-
ensureGsdSymlink(p);
|
|
470
|
-
|
|
471
|
-
// Sync .gsd/ state from main repo into worktree (#1311).
|
|
472
|
-
// Covers the case where .gsd/ is a real directory (not symlinked) and
|
|
473
|
-
// milestones were created on main after the worktree was last used.
|
|
474
|
-
syncGsdStateToWorktree(basePath, p);
|
|
475
|
-
|
|
476
714
|
const previousCwd = process.cwd();
|
|
477
715
|
|
|
478
716
|
try {
|
|
@@ -481,7 +719,7 @@ export function enterAutoWorktree(basePath: string, milestoneId: string): string
|
|
|
481
719
|
} catch (err) {
|
|
482
720
|
throw new GSDError(
|
|
483
721
|
GSD_IO_ERROR,
|
|
484
|
-
`Failed to enter auto-worktree at ${p}: ${
|
|
722
|
+
`Failed to enter auto-worktree at ${p}: ${err instanceof Error ? err.message : String(err)}`,
|
|
485
723
|
);
|
|
486
724
|
}
|
|
487
725
|
|
|
@@ -504,8 +742,10 @@ export function getActiveAutoWorktreeContext(): {
|
|
|
504
742
|
} | null {
|
|
505
743
|
if (!originalBase) return null;
|
|
506
744
|
const cwd = process.cwd();
|
|
507
|
-
const resolvedBase = existsSync(originalBase)
|
|
508
|
-
|
|
745
|
+
const resolvedBase = existsSync(originalBase)
|
|
746
|
+
? realpathSync(originalBase)
|
|
747
|
+
: originalBase;
|
|
748
|
+
const wtDir = join(resolvedBase, ".gsd", "worktrees");
|
|
509
749
|
if (!cwd.startsWith(wtDir)) return null;
|
|
510
750
|
const worktreeName = detectWorktreeName(cwd);
|
|
511
751
|
if (!worktreeName) return null;
|
|
@@ -529,7 +769,10 @@ function autoCommitDirtyState(cwd: string): boolean {
|
|
|
529
769
|
const status = nativeWorkingTreeStatus(cwd);
|
|
530
770
|
if (!status) return false;
|
|
531
771
|
nativeAddAll(cwd);
|
|
532
|
-
const result = nativeCommit(
|
|
772
|
+
const result = nativeCommit(
|
|
773
|
+
cwd,
|
|
774
|
+
"chore: auto-commit before milestone merge",
|
|
775
|
+
);
|
|
533
776
|
return result !== null;
|
|
534
777
|
} catch {
|
|
535
778
|
return false;
|
|
@@ -565,59 +808,53 @@ export function mergeMilestoneToMain(
|
|
|
565
808
|
// 1. Auto-commit dirty state in worktree before leaving
|
|
566
809
|
autoCommitDirtyState(worktreeCwd);
|
|
567
810
|
|
|
811
|
+
// Reconcile worktree DB into main DB before leaving worktree context
|
|
812
|
+
if (isDbAvailable()) {
|
|
813
|
+
try {
|
|
814
|
+
const worktreeDbPath = join(worktreeCwd, ".gsd", "gsd.db");
|
|
815
|
+
const mainDbPath = join(originalBasePath_, ".gsd", "gsd.db");
|
|
816
|
+
reconcileWorktreeDb(mainDbPath, worktreeDbPath);
|
|
817
|
+
} catch {
|
|
818
|
+
/* non-fatal */
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
568
822
|
// 2. Parse roadmap for slice listing
|
|
569
823
|
const roadmap = parseRoadmap(roadmapContent);
|
|
570
|
-
const completedSlices = roadmap.slices.filter(s => s.done);
|
|
824
|
+
const completedSlices = roadmap.slices.filter((s) => s.done);
|
|
571
825
|
|
|
572
826
|
// 3. chdir to original base
|
|
573
827
|
const previousCwd = process.cwd();
|
|
574
828
|
process.chdir(originalBasePath_);
|
|
575
829
|
|
|
576
|
-
// 3a. Auto-commit any dirty state in the project root. Without this, the
|
|
577
|
-
// squash merge can fail with "Your local changes would be overwritten" (#1127).
|
|
578
|
-
autoCommitDirtyState(originalBasePath_);
|
|
579
|
-
|
|
580
|
-
// 3b. Remove untracked .gsd/ runtime files that syncStateToProjectRoot copied.
|
|
581
|
-
// Only clean specific runtime files — NEVER touch milestones/, decisions, or
|
|
582
|
-
// other planning artifacts that represent user work (#1250).
|
|
583
|
-
const runtimeFilesToClean = ["STATE.md", "completed-units.json", "auto.lock", "gsd.db"];
|
|
584
|
-
for (const f of runtimeFilesToClean) {
|
|
585
|
-
const p = join(originalBasePath_, ".gsd", f);
|
|
586
|
-
try { if (existsSync(p)) unlinkSync(p); } catch { /* non-fatal */ }
|
|
587
|
-
}
|
|
588
|
-
try {
|
|
589
|
-
const runtimeDir = join(originalBasePath_, ".gsd", "runtime");
|
|
590
|
-
if (existsSync(runtimeDir)) rmSync(runtimeDir, { recursive: true, force: true });
|
|
591
|
-
} catch { /* non-fatal */ }
|
|
592
|
-
|
|
593
830
|
// 4. Resolve integration branch — prefer milestone metadata, fall back to preferences / "main"
|
|
594
831
|
const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
595
|
-
const integrationBranch = readIntegrationBranch(
|
|
832
|
+
const integrationBranch = readIntegrationBranch(
|
|
833
|
+
originalBasePath_,
|
|
834
|
+
milestoneId,
|
|
835
|
+
);
|
|
596
836
|
const mainBranch = integrationBranch ?? prefs.main_branch ?? "main";
|
|
597
837
|
|
|
838
|
+
// Remove transient project-root state files before any branch or merge
|
|
839
|
+
// operation. Untracked milestone metadata can otherwise block squash merges.
|
|
840
|
+
clearProjectRootStateFiles(originalBasePath_, milestoneId);
|
|
841
|
+
|
|
598
842
|
// 5. Checkout integration branch (skip if already current — avoids git error
|
|
599
843
|
// when main is already checked out in the project-root worktree, #757)
|
|
600
844
|
const currentBranchAtBase = nativeGetCurrentBranch(originalBasePath_);
|
|
601
845
|
if (currentBranchAtBase !== mainBranch) {
|
|
602
|
-
// Remove untracked .gsd/ state files that may conflict with the branch
|
|
603
|
-
// being checked out. These are regenerated by doctor/rebuildState and
|
|
604
|
-
// are not meaningful in the main working tree — the worktree had the
|
|
605
|
-
// real state. Without this, `git checkout main` fails with
|
|
606
|
-
// "Your local changes would be overwritten" (#827).
|
|
607
|
-
const gsdStateFiles = ["STATE.md", "completed-units.json", "auto.lock"];
|
|
608
|
-
for (const f of gsdStateFiles) {
|
|
609
|
-
const p = join(gsdRoot(originalBasePath_), f);
|
|
610
|
-
try { unlinkSync(p); } catch { /* non-fatal — file may not exist */ }
|
|
611
|
-
}
|
|
612
846
|
nativeCheckoutBranch(originalBasePath_, mainBranch);
|
|
613
847
|
}
|
|
614
848
|
|
|
615
849
|
// 6. Build rich commit message
|
|
616
|
-
const milestoneTitle =
|
|
850
|
+
const milestoneTitle =
|
|
851
|
+
roadmap.title.replace(/^M\d+:\s*/, "").trim() || milestoneId;
|
|
617
852
|
const subject = `feat(${milestoneId}): ${milestoneTitle}`;
|
|
618
853
|
let body = "";
|
|
619
854
|
if (completedSlices.length > 0) {
|
|
620
|
-
const sliceLines = completedSlices
|
|
855
|
+
const sliceLines = completedSlices
|
|
856
|
+
.map((s) => `- ${s.id}: ${s.title}`)
|
|
857
|
+
.join("\n");
|
|
621
858
|
body = `\n\nCompleted slices:\n${sliceLines}\n\nBranch: ${milestoneBranch}`;
|
|
622
859
|
}
|
|
623
860
|
const commitMessage = subject + body;
|
|
@@ -627,17 +864,20 @@ export function mergeMilestoneToMain(
|
|
|
627
864
|
|
|
628
865
|
if (!mergeResult.success) {
|
|
629
866
|
// Check for conflicts — use merge result first, fall back to nativeConflictFiles
|
|
630
|
-
const conflictedFiles =
|
|
631
|
-
|
|
632
|
-
|
|
867
|
+
const conflictedFiles =
|
|
868
|
+
mergeResult.conflicts.length > 0
|
|
869
|
+
? mergeResult.conflicts
|
|
870
|
+
: nativeConflictFiles(originalBasePath_);
|
|
633
871
|
|
|
634
872
|
if (conflictedFiles.length > 0) {
|
|
635
873
|
// Separate .gsd/ state file conflicts from real code conflicts.
|
|
636
|
-
// GSD state files (STATE.md,
|
|
874
|
+
// GSD state files (STATE.md, auto.lock, etc.)
|
|
637
875
|
// diverge between branches during normal operation — always prefer the
|
|
638
876
|
// milestone branch version since it has the latest execution state.
|
|
639
|
-
const gsdConflicts = conflictedFiles.filter(f => f.startsWith(".gsd/"));
|
|
640
|
-
const codeConflicts = conflictedFiles.filter(
|
|
877
|
+
const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/"));
|
|
878
|
+
const codeConflicts = conflictedFiles.filter(
|
|
879
|
+
(f) => !f.startsWith(".gsd/"),
|
|
880
|
+
);
|
|
641
881
|
|
|
642
882
|
// Auto-resolve .gsd/ conflicts by accepting the milestone branch version
|
|
643
883
|
if (gsdConflicts.length > 0) {
|
|
@@ -655,7 +895,12 @@ export function mergeMilestoneToMain(
|
|
|
655
895
|
|
|
656
896
|
// If there are still non-.gsd conflicts, escalate
|
|
657
897
|
if (codeConflicts.length > 0) {
|
|
658
|
-
throw new MergeConflictError(
|
|
898
|
+
throw new MergeConflictError(
|
|
899
|
+
codeConflicts,
|
|
900
|
+
"squash",
|
|
901
|
+
milestoneBranch,
|
|
902
|
+
mainBranch,
|
|
903
|
+
);
|
|
659
904
|
}
|
|
660
905
|
}
|
|
661
906
|
// No conflicts detected — possibly "already up to date", fall through to commit
|
|
@@ -710,7 +955,10 @@ export function mergeMilestoneToMain(
|
|
|
710
955
|
|
|
711
956
|
// 10. Remove worktree directory first (must happen before branch deletion)
|
|
712
957
|
try {
|
|
713
|
-
removeWorktree(originalBasePath_, milestoneId, {
|
|
958
|
+
removeWorktree(originalBasePath_, milestoneId, {
|
|
959
|
+
branch: null as unknown as string,
|
|
960
|
+
deleteBranch: false,
|
|
961
|
+
});
|
|
714
962
|
} catch {
|
|
715
963
|
// Best-effort -- worktree dir may already be gone
|
|
716
964
|
}
|