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,485 @@
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
+
16
+ import type { AutoSession } from "./auto/session.js";
17
+ import { debugLog } from "./debug-logger.js";
18
+
19
+ // ─── Dependency Interface ──────────────────────────────────────────────────
20
+
21
+ export interface WorktreeResolverDeps {
22
+ isInAutoWorktree: (basePath: string) => boolean;
23
+ shouldUseWorktreeIsolation: () => boolean;
24
+ getIsolationMode: () => "worktree" | "branch" | "none";
25
+ mergeMilestoneToMain: (
26
+ basePath: string,
27
+ milestoneId: string,
28
+ roadmapContent: string,
29
+ ) => { pushed: boolean };
30
+ syncWorktreeStateBack: (
31
+ mainBasePath: string,
32
+ worktreePath: string,
33
+ milestoneId: string,
34
+ ) => { synced: string[] };
35
+ teardownAutoWorktree: (
36
+ basePath: string,
37
+ milestoneId: string,
38
+ opts?: { preserveBranch?: boolean },
39
+ ) => void;
40
+ createAutoWorktree: (basePath: string, milestoneId: string) => string;
41
+ enterAutoWorktree: (basePath: string, milestoneId: string) => string;
42
+ getAutoWorktreePath: (basePath: string, milestoneId: string) => string | null;
43
+ autoCommitCurrentBranch: (
44
+ basePath: string,
45
+ reason: string,
46
+ milestoneId: string,
47
+ ) => void;
48
+ getCurrentBranch: (basePath: string) => string;
49
+ autoWorktreeBranch: (milestoneId: string) => string;
50
+ resolveMilestoneFile: (
51
+ basePath: string,
52
+ milestoneId: string,
53
+ fileType: string,
54
+ ) => string | null;
55
+ readFileSync: (path: string, encoding: string) => string;
56
+ GitServiceImpl: new (basePath: string, gitConfig: unknown) => unknown;
57
+ loadEffectiveGSDPreferences: () =>
58
+ | { preferences?: { git?: Record<string, unknown> } }
59
+ | undefined;
60
+ invalidateAllCaches: () => void;
61
+ captureIntegrationBranch: (
62
+ basePath: string,
63
+ mid: string,
64
+ opts?: { commitDocs?: boolean },
65
+ ) => void;
66
+ }
67
+
68
+ // ─── Notify Context ────────────────────────────────────────────────────────
69
+
70
+ export interface NotifyCtx {
71
+ notify: (
72
+ msg: string,
73
+ level?: "info" | "warning" | "error" | "success",
74
+ ) => void;
75
+ }
76
+
77
+ // ─── WorktreeResolver ──────────────────────────────────────────────────────
78
+
79
+ export class WorktreeResolver {
80
+ private readonly s: AutoSession;
81
+ private readonly deps: WorktreeResolverDeps;
82
+
83
+ constructor(session: AutoSession, deps: WorktreeResolverDeps) {
84
+ this.s = session;
85
+ this.deps = deps;
86
+ }
87
+
88
+ // ── Getters ────────────────────────────────────────────────────────────
89
+
90
+ /** Current working path — may be worktree or project root. */
91
+ get workPath(): string {
92
+ return this.s.basePath;
93
+ }
94
+
95
+ /** Original project root — always the non-worktree path. */
96
+ get projectRoot(): string {
97
+ return this.s.originalBasePath || this.s.basePath;
98
+ }
99
+
100
+ /** Path for auto.lock file — same as the old lockBase(). */
101
+ get lockPath(): string {
102
+ return this.s.originalBasePath || this.s.basePath;
103
+ }
104
+
105
+ // ── Private Helpers ────────────────────────────────────────────────────
106
+
107
+ private rebuildGitService(): void {
108
+ const gitConfig =
109
+ this.deps.loadEffectiveGSDPreferences()?.preferences?.git ?? {};
110
+ this.s.gitService = new this.deps.GitServiceImpl(
111
+ this.s.basePath,
112
+ gitConfig,
113
+ ) as AutoSession["gitService"];
114
+ }
115
+
116
+ /** Restore basePath to originalBasePath and rebuild GitService. */
117
+ private restoreToProjectRoot(): void {
118
+ if (!this.s.originalBasePath) return;
119
+ this.s.basePath = this.s.originalBasePath;
120
+ this.rebuildGitService();
121
+ this.deps.invalidateAllCaches();
122
+ }
123
+
124
+ // ── Validation ──────────────────────────────────────────────────────────
125
+
126
+ /** Validate milestoneId to prevent path traversal. */
127
+ private validateMilestoneId(milestoneId: string): void {
128
+ if (/[\/\\]|\.\./.test(milestoneId)) {
129
+ throw new Error(
130
+ `Invalid milestoneId: ${milestoneId} — contains path separators or traversal`,
131
+ );
132
+ }
133
+ }
134
+
135
+ // ── Enter Milestone ────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Enter or create a worktree for the given milestone.
139
+ *
140
+ * Only acts if `shouldUseWorktreeIsolation()` returns true.
141
+ * Delegates to `enterAutoWorktree` (existing) or `createAutoWorktree` (new).
142
+ * Those functions call `process.chdir()` internally — we do NOT double-chdir.
143
+ *
144
+ * Updates `s.basePath` and rebuilds GitService on success.
145
+ * On failure: notifies a warning and does NOT update `s.basePath`.
146
+ */
147
+ enterMilestone(milestoneId: string, ctx: NotifyCtx): void {
148
+ this.validateMilestoneId(milestoneId);
149
+ if (!this.deps.shouldUseWorktreeIsolation()) {
150
+ debugLog("WorktreeResolver", {
151
+ action: "enterMilestone",
152
+ milestoneId,
153
+ skipped: true,
154
+ reason: "isolation-disabled",
155
+ });
156
+ return;
157
+ }
158
+
159
+ const basePath = this.s.originalBasePath || this.s.basePath;
160
+ debugLog("WorktreeResolver", {
161
+ action: "enterMilestone",
162
+ milestoneId,
163
+ basePath,
164
+ });
165
+
166
+ try {
167
+ const existingPath = this.deps.getAutoWorktreePath(basePath, milestoneId);
168
+ let wtPath: string;
169
+
170
+ if (existingPath) {
171
+ wtPath = this.deps.enterAutoWorktree(basePath, milestoneId);
172
+ } else {
173
+ wtPath = this.deps.createAutoWorktree(basePath, milestoneId);
174
+ }
175
+
176
+ this.s.basePath = wtPath;
177
+ this.rebuildGitService();
178
+
179
+ debugLog("WorktreeResolver", {
180
+ action: "enterMilestone",
181
+ milestoneId,
182
+ result: "success",
183
+ wtPath,
184
+ });
185
+ ctx.notify(`Entered worktree for ${milestoneId} at ${wtPath}`, "info");
186
+ } catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ debugLog("WorktreeResolver", {
189
+ action: "enterMilestone",
190
+ milestoneId,
191
+ result: "error",
192
+ error: msg,
193
+ });
194
+ ctx.notify(
195
+ `Auto-worktree creation for ${milestoneId} failed: ${msg}. Continuing in project root.`,
196
+ "warning",
197
+ );
198
+ // Do NOT update s.basePath — stay in project root
199
+ }
200
+ }
201
+
202
+ // ── Exit Milestone ─────────────────────────────────────────────────────
203
+
204
+ /**
205
+ * Exit the current worktree: auto-commit, teardown, reset basePath.
206
+ *
207
+ * Only acts if currently in an auto-worktree (checked via `isInAutoWorktree`).
208
+ * Resets `s.basePath` to `s.originalBasePath` and rebuilds GitService.
209
+ */
210
+ exitMilestone(
211
+ milestoneId: string,
212
+ ctx: NotifyCtx,
213
+ opts?: { preserveBranch?: boolean },
214
+ ): void {
215
+ this.validateMilestoneId(milestoneId);
216
+ if (!this.deps.isInAutoWorktree(this.s.basePath)) {
217
+ debugLog("WorktreeResolver", {
218
+ action: "exitMilestone",
219
+ milestoneId,
220
+ skipped: true,
221
+ reason: "not-in-worktree",
222
+ });
223
+ return;
224
+ }
225
+
226
+ debugLog("WorktreeResolver", {
227
+ action: "exitMilestone",
228
+ milestoneId,
229
+ basePath: this.s.basePath,
230
+ });
231
+
232
+ try {
233
+ this.deps.autoCommitCurrentBranch(this.s.basePath, "stop", milestoneId);
234
+ } catch (err) {
235
+ debugLog("WorktreeResolver", {
236
+ action: "exitMilestone",
237
+ milestoneId,
238
+ phase: "auto-commit-failed",
239
+ error: err instanceof Error ? err.message : String(err),
240
+ });
241
+ }
242
+
243
+ try {
244
+ this.deps.teardownAutoWorktree(this.s.originalBasePath, milestoneId, {
245
+ preserveBranch: opts?.preserveBranch ?? false,
246
+ });
247
+ } catch (err) {
248
+ debugLog("WorktreeResolver", {
249
+ action: "exitMilestone",
250
+ milestoneId,
251
+ phase: "teardown-failed",
252
+ error: err instanceof Error ? err.message : String(err),
253
+ });
254
+ }
255
+
256
+ this.restoreToProjectRoot();
257
+ debugLog("WorktreeResolver", {
258
+ action: "exitMilestone",
259
+ milestoneId,
260
+ result: "done",
261
+ basePath: this.s.basePath,
262
+ });
263
+ ctx.notify(`Exited worktree for ${milestoneId}`, "info");
264
+ }
265
+
266
+ // ── Merge and Exit ─────────────────────────────────────────────────────
267
+
268
+ /**
269
+ * Merge the completed milestone branch back to main and exit the worktree.
270
+ *
271
+ * Handles all three isolation modes:
272
+ * - **worktree**: Read roadmap, merge, teardown worktree, reset paths.
273
+ * Falls back to bare teardown if no roadmap exists.
274
+ * - **branch**: Check if on milestone branch, merge if so (no chdir/teardown).
275
+ * - **none**: No-op.
276
+ *
277
+ * Error recovery: on merge failure, always restore `s.basePath` to
278
+ * `s.originalBasePath` and `process.chdir(s.originalBasePath)`.
279
+ */
280
+ mergeAndExit(milestoneId: string, ctx: NotifyCtx): void {
281
+ this.validateMilestoneId(milestoneId);
282
+ const mode = this.deps.getIsolationMode();
283
+ debugLog("WorktreeResolver", {
284
+ action: "mergeAndExit",
285
+ milestoneId,
286
+ mode,
287
+ basePath: this.s.basePath,
288
+ });
289
+
290
+ if (mode === "none") {
291
+ debugLog("WorktreeResolver", {
292
+ action: "mergeAndExit",
293
+ milestoneId,
294
+ skipped: true,
295
+ reason: "mode-none",
296
+ });
297
+ return;
298
+ }
299
+
300
+ if (
301
+ mode === "worktree" ||
302
+ (this.deps.isInAutoWorktree(this.s.basePath) && this.s.originalBasePath)
303
+ ) {
304
+ this._mergeWorktreeMode(milestoneId, ctx);
305
+ } else if (mode === "branch") {
306
+ this._mergeBranchMode(milestoneId, ctx);
307
+ }
308
+ }
309
+
310
+ /** Worktree-mode merge: read roadmap, merge, teardown, reset paths. */
311
+ private _mergeWorktreeMode(milestoneId: string, ctx: NotifyCtx): void {
312
+ const originalBase = this.s.originalBasePath;
313
+ if (!originalBase) {
314
+ debugLog("WorktreeResolver", {
315
+ action: "mergeAndExit",
316
+ milestoneId,
317
+ mode: "worktree",
318
+ skipped: true,
319
+ reason: "missing-original-base",
320
+ });
321
+ return;
322
+ }
323
+
324
+ try {
325
+ const { synced } = this.deps.syncWorktreeStateBack(
326
+ originalBase,
327
+ this.s.basePath,
328
+ milestoneId,
329
+ );
330
+ if (synced.length > 0) {
331
+ debugLog("WorktreeResolver", {
332
+ action: "mergeAndExit",
333
+ milestoneId,
334
+ phase: "reverse-sync",
335
+ synced: synced.length,
336
+ });
337
+ }
338
+
339
+ const roadmapPath = this.deps.resolveMilestoneFile(
340
+ originalBase,
341
+ milestoneId,
342
+ "ROADMAP",
343
+ );
344
+
345
+ if (roadmapPath) {
346
+ const roadmapContent = this.deps.readFileSync(roadmapPath, "utf-8");
347
+ const mergeResult = this.deps.mergeMilestoneToMain(
348
+ originalBase,
349
+ milestoneId,
350
+ roadmapContent,
351
+ );
352
+ ctx.notify(
353
+ `Milestone ${milestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
354
+ "info",
355
+ );
356
+ } else {
357
+ // No roadmap — fall back to bare teardown
358
+ this.deps.teardownAutoWorktree(originalBase, milestoneId);
359
+ ctx.notify(
360
+ `Exited worktree for ${milestoneId} (no roadmap for merge).`,
361
+ "info",
362
+ );
363
+ }
364
+ } catch (err) {
365
+ const msg = err instanceof Error ? err.message : String(err);
366
+ debugLog("WorktreeResolver", {
367
+ action: "mergeAndExit",
368
+ milestoneId,
369
+ result: "error",
370
+ error: msg,
371
+ fallback: "chdir-to-project-root",
372
+ });
373
+ ctx.notify(`Milestone merge failed: ${msg}`, "warning");
374
+
375
+ // Error recovery: always restore to project root
376
+ if (originalBase) {
377
+ try {
378
+ process.chdir(originalBase);
379
+ } catch {
380
+ /* best-effort */
381
+ }
382
+ }
383
+ }
384
+
385
+ // Always restore basePath and rebuild — whether merge succeeded or failed
386
+ this.restoreToProjectRoot();
387
+ debugLog("WorktreeResolver", {
388
+ action: "mergeAndExit",
389
+ milestoneId,
390
+ result: "done",
391
+ basePath: this.s.basePath,
392
+ });
393
+ }
394
+
395
+ /** Branch-mode merge: check current branch, merge if on milestone branch. */
396
+ private _mergeBranchMode(milestoneId: string, ctx: NotifyCtx): void {
397
+ try {
398
+ const currentBranch = this.deps.getCurrentBranch(this.s.basePath);
399
+ const milestoneBranch = this.deps.autoWorktreeBranch(milestoneId);
400
+
401
+ if (currentBranch !== milestoneBranch) {
402
+ debugLog("WorktreeResolver", {
403
+ action: "mergeAndExit",
404
+ milestoneId,
405
+ mode: "branch",
406
+ skipped: true,
407
+ reason: "not-on-milestone-branch",
408
+ currentBranch,
409
+ milestoneBranch,
410
+ });
411
+ return;
412
+ }
413
+
414
+ const roadmapPath = this.deps.resolveMilestoneFile(
415
+ this.s.basePath,
416
+ milestoneId,
417
+ "ROADMAP",
418
+ );
419
+ if (!roadmapPath) {
420
+ debugLog("WorktreeResolver", {
421
+ action: "mergeAndExit",
422
+ milestoneId,
423
+ mode: "branch",
424
+ skipped: true,
425
+ reason: "no-roadmap",
426
+ });
427
+ return;
428
+ }
429
+
430
+ const roadmapContent = this.deps.readFileSync(roadmapPath, "utf-8");
431
+ const mergeResult = this.deps.mergeMilestoneToMain(
432
+ this.s.basePath,
433
+ milestoneId,
434
+ roadmapContent,
435
+ );
436
+
437
+ // Rebuild GitService after merge (branch HEAD changed)
438
+ this.rebuildGitService();
439
+
440
+ ctx.notify(
441
+ `Milestone ${milestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
442
+ "info",
443
+ );
444
+ debugLog("WorktreeResolver", {
445
+ action: "mergeAndExit",
446
+ milestoneId,
447
+ mode: "branch",
448
+ result: "success",
449
+ });
450
+ } catch (err) {
451
+ const msg = err instanceof Error ? err.message : String(err);
452
+ debugLog("WorktreeResolver", {
453
+ action: "mergeAndExit",
454
+ milestoneId,
455
+ mode: "branch",
456
+ result: "error",
457
+ error: msg,
458
+ });
459
+ ctx.notify(`Milestone merge failed (branch mode): ${msg}`, "warning");
460
+ }
461
+ }
462
+
463
+ // ── Merge and Enter Next ───────────────────────────────────────────────
464
+
465
+ /**
466
+ * Milestone transition: merge the current milestone, then enter the next one.
467
+ *
468
+ * This is the pattern used when the loop detects that the active milestone
469
+ * has changed (e.g., current completed, next one is now active). The caller
470
+ * is responsible for re-deriving state between the merge and the enter.
471
+ */
472
+ mergeAndEnterNext(
473
+ currentMilestoneId: string,
474
+ nextMilestoneId: string,
475
+ ctx: NotifyCtx,
476
+ ): void {
477
+ debugLog("WorktreeResolver", {
478
+ action: "mergeAndEnterNext",
479
+ currentMilestoneId,
480
+ nextMilestoneId,
481
+ });
482
+ this.mergeAndExit(currentMilestoneId, ctx);
483
+ this.enterMilestone(nextMilestoneId, ctx);
484
+ }
485
+ }
@@ -12,7 +12,7 @@
12
12
  * SLICE_BRANCH_RE) remain for backwards compatibility with legacy branches.
13
13
  */
14
14
 
15
- import { existsSync, lstatSync, readFileSync, utimesSync } from "node:fs";
15
+ import { existsSync, readFileSync, utimesSync } from "node:fs";
16
16
  import { join, resolve, sep } from "node:path";
17
17
 
18
18
  import { GitServiceImpl, writeIntegrationBranch, type TaskCommitContext } from "./git-service.js";
@@ -56,13 +56,13 @@ export function setActiveMilestoneId(basePath: string, milestoneId: string | nul
56
56
  * record when the user starts from a different branch (#300). Always a no-op
57
57
  * if on a GSD slice branch.
58
58
  */
59
- export function captureIntegrationBranch(basePath: string, milestoneId: string): void {
59
+ export function captureIntegrationBranch(basePath: string, milestoneId: string, options?: { commitDocs?: boolean }): void {
60
60
  // In a worktree, the base branch is implicit (worktree/<name>).
61
61
  // Writing it to META.json would leave stale metadata after merge back to main.
62
62
  if (detectWorktreeName(basePath)) return;
63
63
  const svc = getService(basePath);
64
64
  const current = svc.getCurrentBranch();
65
- writeIntegrationBranch(basePath, milestoneId, current);
65
+ writeIntegrationBranch(basePath, milestoneId, current, options);
66
66
  }
67
67
 
68
68
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────
@@ -72,25 +72,6 @@ export function captureIntegrationBranch(basePath: string, milestoneId: string):
72
72
  * Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
73
73
  */
74
74
  export function detectWorktreeName(basePath: string): string | null {
75
- // Primary: use git metadata — .git file with gitdir: pointer
76
- const gitPath = join(basePath, ".git");
77
- try {
78
- const stat = lstatSync(gitPath);
79
- if (stat.isFile()) {
80
- const content = readFileSync(gitPath, "utf-8").trim();
81
- if (content.startsWith("gitdir:")) {
82
- const gitdir = content.slice(7).trim();
83
- // Git worktree gitdir format: <repo>/.git/worktrees/<name>
84
- const parts = gitdir.replace(/\\/g, "/").split("/");
85
- const wtIdx = parts.lastIndexOf("worktrees");
86
- if (wtIdx !== -1 && wtIdx < parts.length - 1) {
87
- return parts[wtIdx + 1] || null;
88
- }
89
- }
90
- }
91
- } catch { /* fall through */ }
92
-
93
- // Fallback: path-based detection for legacy setups
94
75
  const normalizedPath = basePath.replaceAll("\\", "/");
95
76
  const marker = "/.gsd/worktrees/";
96
77
  const idx = normalizedPath.indexOf(marker);
@@ -109,32 +90,14 @@ export function detectWorktreeName(basePath: string): string | null {
109
90
  * operate against the real project root, not a worktree subdirectory.
110
91
  */
111
92
  export function resolveProjectRoot(basePath: string): string {
112
- // Primary: use git metadata to resolve the main worktree root
113
- const gitPath = join(basePath, ".git");
114
- try {
115
- const stat = lstatSync(gitPath);
116
- if (stat.isFile()) {
117
- const content = readFileSync(gitPath, "utf-8").trim();
118
- if (content.startsWith("gitdir:")) {
119
- const gitdir = resolve(basePath, content.slice(7).trim());
120
- // Git worktree gitdir: <repo>/.git/worktrees/<name>
121
- // Walk up to <repo>
122
- const parts = gitdir.replace(/\\/g, "/").split("/");
123
- const wtIdx = parts.lastIndexOf("worktrees");
124
- if (wtIdx >= 2 && parts[wtIdx - 1] === ".git") {
125
- return parts.slice(0, wtIdx - 1).join("/");
126
- }
127
- }
128
- }
129
- } catch { /* fall through */ }
130
-
131
- // Fallback: legacy path-based detection
132
93
  const normalizedPath = basePath.replaceAll("\\", "/");
133
94
  const marker = "/.gsd/worktrees/";
134
95
  const idx = normalizedPath.indexOf(marker);
135
96
  if (idx === -1) return basePath;
136
- const osSep = basePath.includes("\\") ? "\\" : "/";
137
- const markerOs = `${osSep}.gsd${osSep}worktrees${osSep}`;
97
+ // Return the original path up to the .gsd/ marker (un-normalized)
98
+ // Account for potential OS-specific separators
99
+ const sep = basePath.includes("\\") ? "\\" : "/";
100
+ const markerOs = `${sep}.gsd${sep}worktrees${sep}`;
138
101
  const idxOs = basePath.indexOf(markerOs);
139
102
  if (idxOs !== -1) return basePath.slice(0, idxOs);
140
103
  return basePath.slice(0, idx);
@@ -1,5 +0,0 @@
1
- /**
2
- * Shared constants for auto-mode modules (auto.ts, auto-post-unit.ts, etc.).
3
- */
4
- /** Throttle STATE.md rebuilds — at most once per 30 seconds. */
5
- export const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
@@ -1,106 +0,0 @@
1
- /**
2
- * Idempotency checks for auto-mode unit dispatch.
3
- *
4
- * Handles completed-key membership, artifact cross-validation,
5
- * consecutive skip counting, phantom skip loop detection, key eviction,
6
- * and fallback persistence.
7
- *
8
- * Extracted from dispatchNextUnit() in auto.ts. Pure decision logic
9
- * with set mutations — does NOT call dispatchNextUnit or stopAuto.
10
- */
11
- import { invalidateAllCaches } from "./cache.js";
12
- import { verifyExpectedArtifact, persistCompletedKey, removePersistedKey, } from "./auto-recovery.js";
13
- import { resolveMilestoneFile } from "./paths.js";
14
- import { MAX_CONSECUTIVE_SKIPS, MAX_LIFETIME_DISPATCHES } from "./auto/session.js";
15
- import { parseUnitId } from "./unit-id.js";
16
- /**
17
- * Check whether a unit should be skipped (already completed), rerun
18
- * (stale completion record), or dispatched normally.
19
- *
20
- * Mutates s.completedKeySet, s.unitConsecutiveSkips, s.unitLifetimeDispatches,
21
- * and s.recentlyEvictedKeys as needed.
22
- */
23
- export function checkIdempotency(ictx) {
24
- const { s, unitType, unitId, basePath, notify } = ictx;
25
- const idempotencyKey = `${unitType}/${unitId}`;
26
- // ── Primary path: key exists in completed set ──
27
- if (s.completedKeySet.has(idempotencyKey)) {
28
- const artifactExists = verifyExpectedArtifact(unitType, unitId, basePath);
29
- if (artifactExists) {
30
- // Guard against infinite skip loops
31
- const skipCount = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
32
- s.unitConsecutiveSkips.set(idempotencyKey, skipCount);
33
- if (skipCount > MAX_CONSECUTIVE_SKIPS) {
34
- // Cross-check: verify the unit's milestone is still active (#790)
35
- const skippedMid = parseUnitId(unitId).milestone;
36
- const skippedMilestoneComplete = skippedMid
37
- ? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
38
- : false;
39
- if (skippedMilestoneComplete) {
40
- s.unitConsecutiveSkips.delete(idempotencyKey);
41
- invalidateAllCaches();
42
- notify(`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`, "info");
43
- return { action: "skip", reason: "phantom-loop-cleared" };
44
- }
45
- s.unitConsecutiveSkips.delete(idempotencyKey);
46
- s.completedKeySet.delete(idempotencyKey);
47
- s.recentlyEvictedKeys.add(idempotencyKey);
48
- removePersistedKey(basePath, idempotencyKey);
49
- invalidateAllCaches();
50
- notify(`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`, "warning");
51
- return { action: "skip", reason: "evicted" };
52
- }
53
- // Count toward lifetime cap
54
- const lifeSkip = (s.unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
55
- s.unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
56
- if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
57
- return { action: "stop", reason: `Hard loop: ${unitType} ${unitId} (skip cycle)` };
58
- }
59
- notify(`Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`, "info");
60
- return { action: "skip", reason: "completed" };
61
- }
62
- else {
63
- // Stale completion record — artifact missing. Remove and re-run.
64
- s.completedKeySet.delete(idempotencyKey);
65
- removePersistedKey(basePath, idempotencyKey);
66
- notify(`Re-running ${unitType} ${unitId} — marked complete but expected artifact missing.`, "warning");
67
- return { action: "rerun", reason: "stale-key" };
68
- }
69
- }
70
- // ── Fallback: key missing but artifact exists ──
71
- if (verifyExpectedArtifact(unitType, unitId, basePath) && !s.recentlyEvictedKeys.has(idempotencyKey)) {
72
- persistCompletedKey(basePath, idempotencyKey);
73
- s.completedKeySet.add(idempotencyKey);
74
- invalidateAllCaches();
75
- // Same consecutive-skip guard as the primary path
76
- const skipCount2 = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
77
- s.unitConsecutiveSkips.set(idempotencyKey, skipCount2);
78
- if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
79
- const skippedMid2 = parseUnitId(unitId).milestone;
80
- const skippedMilestoneComplete2 = skippedMid2
81
- ? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
82
- : false;
83
- if (skippedMilestoneComplete2) {
84
- s.unitConsecutiveSkips.delete(idempotencyKey);
85
- invalidateAllCaches();
86
- notify(`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`, "info");
87
- return { action: "skip", reason: "phantom-loop-cleared" };
88
- }
89
- s.unitConsecutiveSkips.delete(idempotencyKey);
90
- s.completedKeySet.delete(idempotencyKey);
91
- removePersistedKey(basePath, idempotencyKey);
92
- invalidateAllCaches();
93
- notify(`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`, "warning");
94
- return { action: "skip", reason: "evicted" };
95
- }
96
- // Count toward lifetime cap
97
- const lifeSkip2 = (s.unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
98
- s.unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
99
- if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
100
- return { action: "stop", reason: `Hard loop: ${unitType} ${unitId} (skip cycle)` };
101
- }
102
- notify(`Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`, "info");
103
- return { action: "skip", reason: "fallback-persisted" };
104
- }
105
- return { action: "proceed" };
106
- }