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.
Files changed (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -0,0 +1,344 @@
1
+ /**
2
+ * WorktreeResolver — encapsulates worktree path state and merge/exit lifecycle.
3
+ *
4
+ * Replaces scattered `s.basePath`/`s.originalBasePath` mutation and 3 duplicated
5
+ * merge-or-teardown blocks in auto-loop.ts with single method calls. All
6
+ * `s.basePath` mutations (except session.reset() and initial setup) happen
7
+ * through this class.
8
+ *
9
+ * Design: Option A — mutates AutoSession fields directly so existing `s.basePath`
10
+ * reads continue to work everywhere without wiring changes.
11
+ *
12
+ * Key invariant: `createAutoWorktree()` and `enterAutoWorktree()` call
13
+ * `process.chdir()` internally — this class MUST NOT double-chdir.
14
+ */
15
+ import { debugLog } from "./debug-logger.js";
16
+ // ─── WorktreeResolver ──────────────────────────────────────────────────────
17
+ export class WorktreeResolver {
18
+ s;
19
+ deps;
20
+ constructor(session, deps) {
21
+ this.s = session;
22
+ this.deps = deps;
23
+ }
24
+ // ── Getters ────────────────────────────────────────────────────────────
25
+ /** Current working path — may be worktree or project root. */
26
+ get workPath() {
27
+ return this.s.basePath;
28
+ }
29
+ /** Original project root — always the non-worktree path. */
30
+ get projectRoot() {
31
+ return this.s.originalBasePath || this.s.basePath;
32
+ }
33
+ /** Path for auto.lock file — same as the old lockBase(). */
34
+ get lockPath() {
35
+ return this.s.originalBasePath || this.s.basePath;
36
+ }
37
+ // ── Private Helpers ────────────────────────────────────────────────────
38
+ rebuildGitService() {
39
+ const gitConfig = this.deps.loadEffectiveGSDPreferences()?.preferences?.git ?? {};
40
+ this.s.gitService = new this.deps.GitServiceImpl(this.s.basePath, gitConfig);
41
+ }
42
+ /** Restore basePath to originalBasePath and rebuild GitService. */
43
+ restoreToProjectRoot() {
44
+ if (!this.s.originalBasePath)
45
+ return;
46
+ this.s.basePath = this.s.originalBasePath;
47
+ this.rebuildGitService();
48
+ this.deps.invalidateAllCaches();
49
+ }
50
+ // ── Validation ──────────────────────────────────────────────────────────
51
+ /** Validate milestoneId to prevent path traversal. */
52
+ validateMilestoneId(milestoneId) {
53
+ if (/[\/\\]|\.\./.test(milestoneId)) {
54
+ throw new Error(`Invalid milestoneId: ${milestoneId} — contains path separators or traversal`);
55
+ }
56
+ }
57
+ // ── Enter Milestone ────────────────────────────────────────────────────
58
+ /**
59
+ * Enter or create a worktree for the given milestone.
60
+ *
61
+ * Only acts if `shouldUseWorktreeIsolation()` returns true.
62
+ * Delegates to `enterAutoWorktree` (existing) or `createAutoWorktree` (new).
63
+ * Those functions call `process.chdir()` internally — we do NOT double-chdir.
64
+ *
65
+ * Updates `s.basePath` and rebuilds GitService on success.
66
+ * On failure: notifies a warning and does NOT update `s.basePath`.
67
+ */
68
+ enterMilestone(milestoneId, ctx) {
69
+ this.validateMilestoneId(milestoneId);
70
+ if (!this.deps.shouldUseWorktreeIsolation()) {
71
+ debugLog("WorktreeResolver", {
72
+ action: "enterMilestone",
73
+ milestoneId,
74
+ skipped: true,
75
+ reason: "isolation-disabled",
76
+ });
77
+ return;
78
+ }
79
+ const basePath = this.s.originalBasePath || this.s.basePath;
80
+ debugLog("WorktreeResolver", {
81
+ action: "enterMilestone",
82
+ milestoneId,
83
+ basePath,
84
+ });
85
+ try {
86
+ const existingPath = this.deps.getAutoWorktreePath(basePath, milestoneId);
87
+ let wtPath;
88
+ if (existingPath) {
89
+ wtPath = this.deps.enterAutoWorktree(basePath, milestoneId);
90
+ }
91
+ else {
92
+ wtPath = this.deps.createAutoWorktree(basePath, milestoneId);
93
+ }
94
+ this.s.basePath = wtPath;
95
+ this.rebuildGitService();
96
+ debugLog("WorktreeResolver", {
97
+ action: "enterMilestone",
98
+ milestoneId,
99
+ result: "success",
100
+ wtPath,
101
+ });
102
+ ctx.notify(`Entered worktree for ${milestoneId} at ${wtPath}`, "info");
103
+ }
104
+ catch (err) {
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ debugLog("WorktreeResolver", {
107
+ action: "enterMilestone",
108
+ milestoneId,
109
+ result: "error",
110
+ error: msg,
111
+ });
112
+ ctx.notify(`Auto-worktree creation for ${milestoneId} failed: ${msg}. Continuing in project root.`, "warning");
113
+ // Do NOT update s.basePath — stay in project root
114
+ }
115
+ }
116
+ // ── Exit Milestone ─────────────────────────────────────────────────────
117
+ /**
118
+ * Exit the current worktree: auto-commit, teardown, reset basePath.
119
+ *
120
+ * Only acts if currently in an auto-worktree (checked via `isInAutoWorktree`).
121
+ * Resets `s.basePath` to `s.originalBasePath` and rebuilds GitService.
122
+ */
123
+ exitMilestone(milestoneId, ctx, opts) {
124
+ this.validateMilestoneId(milestoneId);
125
+ if (!this.deps.isInAutoWorktree(this.s.basePath)) {
126
+ debugLog("WorktreeResolver", {
127
+ action: "exitMilestone",
128
+ milestoneId,
129
+ skipped: true,
130
+ reason: "not-in-worktree",
131
+ });
132
+ return;
133
+ }
134
+ debugLog("WorktreeResolver", {
135
+ action: "exitMilestone",
136
+ milestoneId,
137
+ basePath: this.s.basePath,
138
+ });
139
+ try {
140
+ this.deps.autoCommitCurrentBranch(this.s.basePath, "stop", milestoneId);
141
+ }
142
+ catch (err) {
143
+ debugLog("WorktreeResolver", {
144
+ action: "exitMilestone",
145
+ milestoneId,
146
+ phase: "auto-commit-failed",
147
+ error: err instanceof Error ? err.message : String(err),
148
+ });
149
+ }
150
+ try {
151
+ this.deps.teardownAutoWorktree(this.s.originalBasePath, milestoneId, {
152
+ preserveBranch: opts?.preserveBranch ?? false,
153
+ });
154
+ }
155
+ catch (err) {
156
+ debugLog("WorktreeResolver", {
157
+ action: "exitMilestone",
158
+ milestoneId,
159
+ phase: "teardown-failed",
160
+ error: err instanceof Error ? err.message : String(err),
161
+ });
162
+ }
163
+ this.restoreToProjectRoot();
164
+ debugLog("WorktreeResolver", {
165
+ action: "exitMilestone",
166
+ milestoneId,
167
+ result: "done",
168
+ basePath: this.s.basePath,
169
+ });
170
+ ctx.notify(`Exited worktree for ${milestoneId}`, "info");
171
+ }
172
+ // ── Merge and Exit ─────────────────────────────────────────────────────
173
+ /**
174
+ * Merge the completed milestone branch back to main and exit the worktree.
175
+ *
176
+ * Handles all three isolation modes:
177
+ * - **worktree**: Read roadmap, merge, teardown worktree, reset paths.
178
+ * Falls back to bare teardown if no roadmap exists.
179
+ * - **branch**: Check if on milestone branch, merge if so (no chdir/teardown).
180
+ * - **none**: No-op.
181
+ *
182
+ * Error recovery: on merge failure, always restore `s.basePath` to
183
+ * `s.originalBasePath` and `process.chdir(s.originalBasePath)`.
184
+ */
185
+ mergeAndExit(milestoneId, ctx) {
186
+ this.validateMilestoneId(milestoneId);
187
+ const mode = this.deps.getIsolationMode();
188
+ debugLog("WorktreeResolver", {
189
+ action: "mergeAndExit",
190
+ milestoneId,
191
+ mode,
192
+ basePath: this.s.basePath,
193
+ });
194
+ if (mode === "none") {
195
+ debugLog("WorktreeResolver", {
196
+ action: "mergeAndExit",
197
+ milestoneId,
198
+ skipped: true,
199
+ reason: "mode-none",
200
+ });
201
+ return;
202
+ }
203
+ if (mode === "worktree" ||
204
+ (this.deps.isInAutoWorktree(this.s.basePath) && this.s.originalBasePath)) {
205
+ this._mergeWorktreeMode(milestoneId, ctx);
206
+ }
207
+ else if (mode === "branch") {
208
+ this._mergeBranchMode(milestoneId, ctx);
209
+ }
210
+ }
211
+ /** Worktree-mode merge: read roadmap, merge, teardown, reset paths. */
212
+ _mergeWorktreeMode(milestoneId, ctx) {
213
+ const originalBase = this.s.originalBasePath;
214
+ if (!originalBase) {
215
+ debugLog("WorktreeResolver", {
216
+ action: "mergeAndExit",
217
+ milestoneId,
218
+ mode: "worktree",
219
+ skipped: true,
220
+ reason: "missing-original-base",
221
+ });
222
+ return;
223
+ }
224
+ try {
225
+ const { synced } = this.deps.syncWorktreeStateBack(originalBase, this.s.basePath, milestoneId);
226
+ if (synced.length > 0) {
227
+ debugLog("WorktreeResolver", {
228
+ action: "mergeAndExit",
229
+ milestoneId,
230
+ phase: "reverse-sync",
231
+ synced: synced.length,
232
+ });
233
+ }
234
+ const roadmapPath = this.deps.resolveMilestoneFile(originalBase, milestoneId, "ROADMAP");
235
+ if (roadmapPath) {
236
+ const roadmapContent = this.deps.readFileSync(roadmapPath, "utf-8");
237
+ const mergeResult = this.deps.mergeMilestoneToMain(originalBase, milestoneId, roadmapContent);
238
+ ctx.notify(`Milestone ${milestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`, "info");
239
+ }
240
+ else {
241
+ // No roadmap — fall back to bare teardown
242
+ this.deps.teardownAutoWorktree(originalBase, milestoneId);
243
+ ctx.notify(`Exited worktree for ${milestoneId} (no roadmap for merge).`, "info");
244
+ }
245
+ }
246
+ catch (err) {
247
+ const msg = err instanceof Error ? err.message : String(err);
248
+ debugLog("WorktreeResolver", {
249
+ action: "mergeAndExit",
250
+ milestoneId,
251
+ result: "error",
252
+ error: msg,
253
+ fallback: "chdir-to-project-root",
254
+ });
255
+ ctx.notify(`Milestone merge failed: ${msg}`, "warning");
256
+ // Error recovery: always restore to project root
257
+ if (originalBase) {
258
+ try {
259
+ process.chdir(originalBase);
260
+ }
261
+ catch {
262
+ /* best-effort */
263
+ }
264
+ }
265
+ }
266
+ // Always restore basePath and rebuild — whether merge succeeded or failed
267
+ this.restoreToProjectRoot();
268
+ debugLog("WorktreeResolver", {
269
+ action: "mergeAndExit",
270
+ milestoneId,
271
+ result: "done",
272
+ basePath: this.s.basePath,
273
+ });
274
+ }
275
+ /** Branch-mode merge: check current branch, merge if on milestone branch. */
276
+ _mergeBranchMode(milestoneId, ctx) {
277
+ try {
278
+ const currentBranch = this.deps.getCurrentBranch(this.s.basePath);
279
+ const milestoneBranch = this.deps.autoWorktreeBranch(milestoneId);
280
+ if (currentBranch !== milestoneBranch) {
281
+ debugLog("WorktreeResolver", {
282
+ action: "mergeAndExit",
283
+ milestoneId,
284
+ mode: "branch",
285
+ skipped: true,
286
+ reason: "not-on-milestone-branch",
287
+ currentBranch,
288
+ milestoneBranch,
289
+ });
290
+ return;
291
+ }
292
+ const roadmapPath = this.deps.resolveMilestoneFile(this.s.basePath, milestoneId, "ROADMAP");
293
+ if (!roadmapPath) {
294
+ debugLog("WorktreeResolver", {
295
+ action: "mergeAndExit",
296
+ milestoneId,
297
+ mode: "branch",
298
+ skipped: true,
299
+ reason: "no-roadmap",
300
+ });
301
+ return;
302
+ }
303
+ const roadmapContent = this.deps.readFileSync(roadmapPath, "utf-8");
304
+ const mergeResult = this.deps.mergeMilestoneToMain(this.s.basePath, milestoneId, roadmapContent);
305
+ // Rebuild GitService after merge (branch HEAD changed)
306
+ this.rebuildGitService();
307
+ ctx.notify(`Milestone ${milestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`, "info");
308
+ debugLog("WorktreeResolver", {
309
+ action: "mergeAndExit",
310
+ milestoneId,
311
+ mode: "branch",
312
+ result: "success",
313
+ });
314
+ }
315
+ catch (err) {
316
+ const msg = err instanceof Error ? err.message : String(err);
317
+ debugLog("WorktreeResolver", {
318
+ action: "mergeAndExit",
319
+ milestoneId,
320
+ mode: "branch",
321
+ result: "error",
322
+ error: msg,
323
+ });
324
+ ctx.notify(`Milestone merge failed (branch mode): ${msg}`, "warning");
325
+ }
326
+ }
327
+ // ── Merge and Enter Next ───────────────────────────────────────────────
328
+ /**
329
+ * Milestone transition: merge the current milestone, then enter the next one.
330
+ *
331
+ * This is the pattern used when the loop detects that the active milestone
332
+ * has changed (e.g., current completed, next one is now active). The caller
333
+ * is responsible for re-deriving state between the merge and the enter.
334
+ */
335
+ mergeAndEnterNext(currentMilestoneId, nextMilestoneId, ctx) {
336
+ debugLog("WorktreeResolver", {
337
+ action: "mergeAndEnterNext",
338
+ currentMilestoneId,
339
+ nextMilestoneId,
340
+ });
341
+ this.mergeAndExit(currentMilestoneId, ctx);
342
+ this.enterMilestone(nextMilestoneId, ctx);
343
+ }
344
+ }
@@ -11,7 +11,7 @@
11
11
  * Pure utility functions (detectWorktreeName, getSliceBranchName, parseSliceBranch,
12
12
  * SLICE_BRANCH_RE) remain for backwards compatibility with legacy branches.
13
13
  */
14
- import { existsSync, lstatSync, readFileSync, utimesSync } from "node:fs";
14
+ import { existsSync, readFileSync, utimesSync } from "node:fs";
15
15
  import { join, resolve } from "node:path";
16
16
  import { GitServiceImpl, writeIntegrationBranch } from "./git-service.js";
17
17
  import { loadEffectiveGSDPreferences } from "./preferences.js";
@@ -47,14 +47,14 @@ export function setActiveMilestoneId(basePath, milestoneId) {
47
47
  * record when the user starts from a different branch (#300). Always a no-op
48
48
  * if on a GSD slice branch.
49
49
  */
50
- export function captureIntegrationBranch(basePath, milestoneId) {
50
+ export function captureIntegrationBranch(basePath, milestoneId, options) {
51
51
  // In a worktree, the base branch is implicit (worktree/<name>).
52
52
  // Writing it to META.json would leave stale metadata after merge back to main.
53
53
  if (detectWorktreeName(basePath))
54
54
  return;
55
55
  const svc = getService(basePath);
56
56
  const current = svc.getCurrentBranch();
57
- writeIntegrationBranch(basePath, milestoneId, current);
57
+ writeIntegrationBranch(basePath, milestoneId, current, options);
58
58
  }
59
59
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────
60
60
  /**
@@ -62,25 +62,6 @@ export function captureIntegrationBranch(basePath, milestoneId) {
62
62
  * Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
63
63
  */
64
64
  export function detectWorktreeName(basePath) {
65
- // Primary: use git metadata — .git file with gitdir: pointer
66
- const gitPath = join(basePath, ".git");
67
- try {
68
- const stat = lstatSync(gitPath);
69
- if (stat.isFile()) {
70
- const content = readFileSync(gitPath, "utf-8").trim();
71
- if (content.startsWith("gitdir:")) {
72
- const gitdir = content.slice(7).trim();
73
- // Git worktree gitdir format: <repo>/.git/worktrees/<name>
74
- const parts = gitdir.replace(/\\/g, "/").split("/");
75
- const wtIdx = parts.lastIndexOf("worktrees");
76
- if (wtIdx !== -1 && wtIdx < parts.length - 1) {
77
- return parts[wtIdx + 1] || null;
78
- }
79
- }
80
- }
81
- }
82
- catch { /* fall through */ }
83
- // Fallback: path-based detection for legacy setups
84
65
  const normalizedPath = basePath.replaceAll("\\", "/");
85
66
  const marker = "/.gsd/worktrees/";
86
67
  const idx = normalizedPath.indexOf(marker);
@@ -99,33 +80,15 @@ export function detectWorktreeName(basePath) {
99
80
  * operate against the real project root, not a worktree subdirectory.
100
81
  */
101
82
  export function resolveProjectRoot(basePath) {
102
- // Primary: use git metadata to resolve the main worktree root
103
- const gitPath = join(basePath, ".git");
104
- try {
105
- const stat = lstatSync(gitPath);
106
- if (stat.isFile()) {
107
- const content = readFileSync(gitPath, "utf-8").trim();
108
- if (content.startsWith("gitdir:")) {
109
- const gitdir = resolve(basePath, content.slice(7).trim());
110
- // Git worktree gitdir: <repo>/.git/worktrees/<name>
111
- // Walk up to <repo>
112
- const parts = gitdir.replace(/\\/g, "/").split("/");
113
- const wtIdx = parts.lastIndexOf("worktrees");
114
- if (wtIdx >= 2 && parts[wtIdx - 1] === ".git") {
115
- return parts.slice(0, wtIdx - 1).join("/");
116
- }
117
- }
118
- }
119
- }
120
- catch { /* fall through */ }
121
- // Fallback: legacy path-based detection
122
83
  const normalizedPath = basePath.replaceAll("\\", "/");
123
84
  const marker = "/.gsd/worktrees/";
124
85
  const idx = normalizedPath.indexOf(marker);
125
86
  if (idx === -1)
126
87
  return basePath;
127
- const osSep = basePath.includes("\\") ? "\\" : "/";
128
- const markerOs = `${osSep}.gsd${osSep}worktrees${osSep}`;
88
+ // Return the original path up to the .gsd/ marker (un-normalized)
89
+ // Account for potential OS-specific separators
90
+ const sep = basePath.includes("\\") ? "\\" : "/";
91
+ const markerOs = `${sep}.gsd${sep}worktrees${sep}`;
129
92
  const idxOs = basePath.indexOf(markerOs);
130
93
  if (idxOs !== -1)
131
94
  return basePath.slice(0, idxOs);
@@ -1,4 +1,4 @@
1
- import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
1
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, statSync, symlinkSync, unlinkSync } from "node:fs";
2
2
  import { delimiter, join } from "node:path";
3
3
  const TOOL_SPECS = {
4
4
  fd: {
@@ -32,6 +32,45 @@ function isRegularFile(path) {
32
32
  return false;
33
33
  }
34
34
  }
35
+ function pathExistsIncludingBrokenSymlink(path) {
36
+ try {
37
+ lstatSync(path);
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ function isBrokenSymlink(path) {
45
+ try {
46
+ const stat = lstatSync(path);
47
+ if (!stat.isSymbolicLink())
48
+ return false;
49
+ try {
50
+ statSync(path);
51
+ return false;
52
+ }
53
+ catch {
54
+ return true;
55
+ }
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ function removeTargetPath(path) {
62
+ try {
63
+ const stat = lstatSync(path);
64
+ if (stat.isSymbolicLink()) {
65
+ unlinkSync(path);
66
+ return;
67
+ }
68
+ rmSync(path, { force: true });
69
+ }
70
+ catch {
71
+ // Path already absent.
72
+ }
73
+ }
35
74
  export function resolveToolFromPath(tool, pathValue = process.env.PATH) {
36
75
  const spec = TOOL_SPECS[tool];
37
76
  for (const dir of splitPath(pathValue)) {
@@ -48,23 +87,32 @@ export function resolveToolFromPath(tool, pathValue = process.env.PATH) {
48
87
  }
49
88
  function provisionTool(targetDir, tool, sourcePath) {
50
89
  const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
51
- if (existsSync(targetPath))
52
- return targetPath;
53
- mkdirSync(targetDir, { recursive: true });
54
- try {
55
- symlinkSync(sourcePath, targetPath);
90
+ const brokenTarget = isBrokenSymlink(targetPath);
91
+ if (pathExistsIncludingBrokenSymlink(targetPath)) {
92
+ if (!brokenTarget)
93
+ return targetPath;
94
+ removeTargetPath(targetPath);
56
95
  }
57
- catch {
58
- rmSync(targetPath, { force: true });
59
- copyFileSync(sourcePath, targetPath);
60
- chmodSync(targetPath, 0o755);
96
+ mkdirSync(targetDir, { recursive: true });
97
+ if (!brokenTarget) {
98
+ try {
99
+ symlinkSync(sourcePath, targetPath);
100
+ return targetPath;
101
+ }
102
+ catch {
103
+ // Fall back to copying below.
104
+ }
61
105
  }
106
+ removeTargetPath(targetPath);
107
+ copyFileSync(sourcePath, targetPath);
108
+ chmodSync(targetPath, 0o755);
62
109
  return targetPath;
63
110
  }
64
111
  export function ensureManagedTools(targetDir, pathValue = process.env.PATH) {
65
112
  const provisioned = [];
66
113
  for (const tool of Object.keys(TOOL_SPECS)) {
67
- if (existsSync(join(targetDir, TOOL_SPECS[tool].targetName)))
114
+ const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
115
+ if (pathExistsIncludingBrokenSymlink(targetPath) && !isBrokenSymlink(targetPath))
68
116
  continue;
69
117
  const sourcePath = resolveToolFromPath(tool, pathValue);
70
118
  if (!sourcePath)
@@ -20,22 +20,22 @@
20
20
  import chalk from 'chalk';
21
21
  import { createJiti } from '@mariozechner/jiti';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { dirname, join } from 'node:path';
24
23
  import { generateWorktreeName } from './worktree-name-gen.js';
25
24
  import { existsSync } from 'node:fs';
26
- const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ import { resolveBundledSourceResource } from './bundled-resource-path.js';
27
26
  const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false });
27
+ const gsdExtensionPath = (...segments) => resolveBundledSourceResource(import.meta.url, 'extensions', 'gsd', ...segments);
28
28
  // Lazily-loaded extension modules (loaded once on first use via jiti)
29
29
  let _ext = null;
30
30
  async function loadExtensionModules() {
31
31
  if (_ext)
32
32
  return _ext;
33
33
  const [wtMgr, autoWt, gitBridge, gitSvc, wt] = await Promise.all([
34
- jiti.import(join(__dirname, 'resources/extensions/gsd/worktree-manager.ts'), {}),
35
- jiti.import(join(__dirname, 'resources/extensions/gsd/auto-worktree.ts'), {}),
36
- jiti.import(join(__dirname, 'resources/extensions/gsd/native-git-bridge.ts'), {}),
37
- jiti.import(join(__dirname, 'resources/extensions/gsd/git-service.ts'), {}),
38
- jiti.import(join(__dirname, 'resources/extensions/gsd/worktree.ts'), {}),
34
+ jiti.import(gsdExtensionPath('worktree-manager.ts'), {}),
35
+ jiti.import(gsdExtensionPath('auto-worktree.ts'), {}),
36
+ jiti.import(gsdExtensionPath('native-git-bridge.ts'), {}),
37
+ jiti.import(gsdExtensionPath('git-service.ts'), {}),
38
+ jiti.import(gsdExtensionPath('worktree.ts'), {}),
39
39
  ]);
40
40
  _ext = {
41
41
  createWorktree: wtMgr.createWorktree,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.33.1-dev.ee47f1b",
3
+ "version": "2.34.0-dev.bbb5216",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {