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
@@ -15,61 +15,73 @@ import type {
15
15
  } from "@gsd/pi-coding-agent";
16
16
  import { deriveState } from "./state.js";
17
17
  import { loadFile, getManifestStatus } from "./files.js";
18
- import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode } from "./preferences.js";
19
- import { isInsideWorktree, ensureGsdSymlink } from "./repo-identity.js";
20
- import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
21
- import { sendDesktopNotification } from "./notifications.js";
22
- import { sendRemoteNotification } from "../remote-questions/notify.js";
23
18
  import {
24
- gsdRoot,
25
- resolveMilestoneFile,
26
- milestonesDir,
27
- } from "./paths.js";
19
+ loadEffectiveGSDPreferences,
20
+ resolveSkillDiscoveryMode,
21
+ getIsolationMode,
22
+ } from "./preferences.js";
23
+ import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
24
+ import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
28
25
  import { invalidateAllCaches } from "./cache.js";
29
26
  import { synthesizeCrashRecovery } from "./session-forensics.js";
30
- import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
27
+ import {
28
+ writeLock,
29
+ clearLock,
30
+ readCrashLock,
31
+ formatCrashInfo,
32
+ isLockProcessAlive,
33
+ } from "./crash-recovery.js";
31
34
  import {
32
35
  acquireSessionLock,
33
- updateSessionLock,
34
36
  releaseSessionLock,
35
- readSessionLockData,
36
- isSessionLockProcessAlive,
37
+ updateSessionLock,
37
38
  } from "./session-lock.js";
38
- import { selfHealRuntimeRecords } from "./auto-recovery.js";
39
39
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
40
- import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
41
- import { createGitService } from "./git-service.js";
40
+ import {
41
+ nativeIsRepo,
42
+ nativeInit,
43
+ nativeAddAll,
44
+ nativeCommit,
45
+ } from "./native-git-bridge.js";
46
+ import { GitServiceImpl } from "./git-service.js";
42
47
  import {
43
48
  captureIntegrationBranch,
44
49
  detectWorktreeName,
45
50
  setActiveMilestoneId,
46
51
  } from "./worktree.js";
47
- import {
48
- createAutoWorktree,
49
- enterAutoWorktree,
50
- getAutoWorktreePath,
51
- isInAutoWorktree,
52
- } from "./auto-worktree.js";
53
- import { readResourceVersion } from "./resource-version.js";
54
- import { initMetrics, getLedger } from "./metrics.js";
52
+ import { getAutoWorktreePath, isInAutoWorktree } from "./auto-worktree.js";
53
+ import { readResourceVersion } from "./auto-worktree-sync.js";
54
+ import { initMetrics } from "./metrics.js";
55
55
  import { initRoutingHistory } from "./routing-history.js";
56
- import { restoreHookState, resetHookState, clearPersistedHookState } from "./post-unit-hooks.js";
56
+ import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
57
57
  import { resetProactiveHealing } from "./doctor-proactive.js";
58
58
  import { snapshotSkills } from "./skill-discovery.js";
59
59
  import { isDbAvailable } from "./gsd-db.js";
60
- import { loadPersistedKeys } from "./auto-recovery.js";
61
60
  import { hideFooter } from "./auto-dashboard.js";
62
- import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath } from "./debug-logger.js";
61
+ import {
62
+ debugLog,
63
+ enableDebug,
64
+ isDebugEnabled,
65
+ getDebugLogPath,
66
+ } from "./debug-logger.js";
63
67
  import type { AutoSession } from "./auto/session.js";
64
- import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
68
+ import {
69
+ existsSync,
70
+ mkdirSync,
71
+ readdirSync,
72
+ statSync,
73
+ unlinkSync,
74
+ } from "node:fs";
65
75
  import { join } from "node:path";
66
- import { getErrorMessage } from "./error-utils.js";
67
- import { parseUnitId } from "./unit-id.js";
76
+ import { sep as pathSep } from "node:path";
77
+
78
+ import type { WorktreeResolver } from "./worktree-resolver.js";
68
79
 
69
80
  export interface BootstrapDeps {
70
81
  shouldUseWorktreeIsolation: () => boolean;
71
82
  registerSigtermHandler: (basePath: string) => void;
72
83
  lockBase: () => string;
84
+ buildResolver: () => WorktreeResolver;
73
85
  }
74
86
 
75
87
  /**
@@ -89,395 +101,469 @@ export async function bootstrapAutoSession(
89
101
  requestedStepMode: boolean,
90
102
  deps: BootstrapDeps,
91
103
  ): Promise<boolean> {
92
- const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase } = deps;
104
+ const {
105
+ shouldUseWorktreeIsolation,
106
+ registerSigtermHandler,
107
+ lockBase,
108
+ buildResolver,
109
+ } = deps;
93
110
 
94
- // ── Session lock: acquire FIRST, before any state mutation ──────────────
95
- // This is the primary guard against concurrent sessions on the same project.
96
- // Uses OS-level file locking (proper-lockfile) to prevent TOCTOU races.
97
111
  const lockResult = acquireSessionLock(base);
98
112
  if (!lockResult.acquired) {
99
- ctx.ui.notify(
100
- `${lockResult.reason}\nStop it with \`kill ${lockResult.existingPid ?? "the other process"}\` before starting a new session.`,
101
- "error",
102
- );
113
+ ctx.ui.notify(lockResult.reason, "error");
103
114
  return false;
104
115
  }
105
116
 
106
- // Ensure git repo exists
107
- if (!nativeIsRepo(base)) {
108
- const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
109
- nativeInit(base, mainBranch);
117
+ function releaseLockAndReturn(): false {
118
+ releaseSessionLock(base);
119
+ clearLock(base);
120
+ return false;
110
121
  }
111
122
 
112
- // Ensure .gitignore has baseline patterns
113
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
114
- const manageGitignore = gitPrefs?.manage_gitignore;
115
- ensureGitignore(base, { manageGitignore });
116
- if (manageGitignore !== false) untrackRuntimeFiles(base);
117
-
118
- // Migrate legacy in-project .gsd/ to external state directory
119
- recoverFailedMigration(base);
120
- const migration = migrateToExternalState(base);
121
- if (migration.error) {
122
- ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
123
- }
124
- // Ensure symlink exists (handles fresh projects and post-migration)
125
- ensureGsdSymlink(base);
123
+ try {
124
+ // Ensure git repo exists
125
+ if (!nativeIsRepo(base)) {
126
+ const mainBranch =
127
+ loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
128
+ nativeInit(base, mainBranch);
129
+ }
126
130
 
127
- // Bootstrap .gsd/ if it doesn't exist
128
- const gsdDir = gsdRoot(base);
129
- if (!existsSync(gsdDir)) {
130
- mkdirSync(join(gsdDir, "milestones"), { recursive: true });
131
- }
131
+ // Ensure .gitignore has baseline patterns
132
+ const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
133
+ const commitDocs = gitPrefs?.commit_docs;
134
+ const manageGitignore = gitPrefs?.manage_gitignore;
135
+ ensureGitignore(base, { commitDocs, manageGitignore });
136
+ if (manageGitignore !== false) untrackRuntimeFiles(base);
137
+
138
+ // Bootstrap .gsd/ if it doesn't exist
139
+ const gsdDir = join(base, ".gsd");
140
+ if (!existsSync(gsdDir)) {
141
+ mkdirSync(join(gsdDir, "milestones"), { recursive: true });
142
+ if (commitDocs !== false) {
143
+ try {
144
+ nativeAddAll(base);
145
+ nativeCommit(base, "chore: init gsd");
146
+ } catch {
147
+ /* nothing to commit */
148
+ }
149
+ }
150
+ }
132
151
 
133
- // Initialize GitServiceImpl
134
- s.gitService = createGitService(s.basePath);
135
-
136
- // Check for crash from previous session (use both old and new lock data).
137
- // Skip if the lock PID matches this process — acquireSessionLock() writes
138
- // to the same auto.lock file before this check, so we'd always false-positive.
139
- const crashLock = readCrashLock(base);
140
- if (crashLock && crashLock.pid !== process.pid) {
141
- // We already hold the session lock, so no concurrent session is running.
142
- // The crash lock is from a dead process — recover context from it.
143
- const recoveredMid = parseUnitId(crashLock.unitId).milestone;
144
- const milestoneAlreadyComplete = recoveredMid
145
- ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
146
- : false;
147
-
148
- if (milestoneAlreadyComplete) {
149
- ctx.ui.notify(
150
- `Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`,
151
- "info",
152
- );
153
- } else {
154
- const activityDir = join(gsdRoot(base), "activity");
155
- const recovery = synthesizeCrashRecovery(
156
- base, crashLock.unitType, crashLock.unitId,
157
- crashLock.sessionFile, activityDir,
158
- );
159
- if (recovery && recovery.trace.toolCallCount > 0) {
160
- s.pendingCrashRecovery = recovery.prompt;
152
+ // Initialize GitServiceImpl
153
+ s.gitService = new GitServiceImpl(
154
+ s.basePath,
155
+ loadEffectiveGSDPreferences()?.preferences?.git ?? {},
156
+ );
157
+
158
+ // Check for crash from previous session. Skip our own fresh bootstrap lock.
159
+ const crashLock = readCrashLock(base);
160
+ if (crashLock && crashLock.pid !== process.pid) {
161
+ if (isLockProcessAlive(crashLock)) {
161
162
  ctx.ui.notify(
162
- `${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
163
- "warning",
163
+ `Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`,
164
+ "error",
164
165
  );
165
- } else {
166
+ return releaseLockAndReturn();
167
+ }
168
+ const recoveredMid = crashLock.unitId.split("/")[0];
169
+ const milestoneAlreadyComplete = recoveredMid
170
+ ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
171
+ : false;
172
+
173
+ if (milestoneAlreadyComplete) {
166
174
  ctx.ui.notify(
167
- `${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
168
- "warning",
175
+ `Crash recovery: discarding stale context for ${crashLock.unitId} milestone ${recoveredMid} is already complete.`,
176
+ "info",
177
+ );
178
+ } else {
179
+ const activityDir = join(gsdRoot(base), "activity");
180
+ const recovery = synthesizeCrashRecovery(
181
+ base,
182
+ crashLock.unitType,
183
+ crashLock.unitId,
184
+ crashLock.sessionFile,
185
+ activityDir,
169
186
  );
187
+ if (recovery && recovery.trace.toolCallCount > 0) {
188
+ s.pendingCrashRecovery = recovery.prompt;
189
+ ctx.ui.notify(
190
+ `${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
191
+ "warning",
192
+ );
193
+ } else {
194
+ ctx.ui.notify(
195
+ `${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
196
+ "warning",
197
+ );
198
+ }
170
199
  }
200
+ clearLock(base);
171
201
  }
172
- clearLock(base);
173
- }
174
202
 
175
- // ── Debug mode ──
176
- if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
177
- enableDebug(base);
178
- }
179
- if (isDebugEnabled()) {
180
- const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
181
- debugLog("debug-start", {
182
- platform: process.platform,
183
- arch: process.arch,
184
- node: process.version,
185
- model: ctx.model?.id ?? "unknown",
186
- provider: ctx.model?.provider ?? "unknown",
187
- nativeParser: isNativeParserAvailable(),
188
- cwd: base,
189
- });
190
- ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
191
- }
203
+ // ── Debug mode ──
204
+ if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
205
+ enableDebug(base);
206
+ }
207
+ if (isDebugEnabled()) {
208
+ const { isNativeParserAvailable } =
209
+ await import("./native-parser-bridge.js");
210
+ debugLog("debug-start", {
211
+ platform: process.platform,
212
+ arch: process.arch,
213
+ node: process.version,
214
+ model: ctx.model?.id ?? "unknown",
215
+ provider: ctx.model?.provider ?? "unknown",
216
+ nativeParser: isNativeParserAvailable(),
217
+ cwd: base,
218
+ });
219
+ ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
220
+ }
192
221
 
193
- // Invalidate caches before initial state derivation
194
- invalidateAllCaches();
222
+ // Invalidate caches before initial state derivation
223
+ invalidateAllCaches();
195
224
 
196
- // Clean stale runtime unit files for completed milestones (#887)
197
- try {
198
- const runtimeUnitsDir = join(gsdRoot(base), "runtime", "units");
199
- if (existsSync(runtimeUnitsDir)) {
200
- for (const file of readdirSync(runtimeUnitsDir)) {
201
- if (!file.endsWith(".json")) continue;
202
- const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
203
- if (!midMatch) continue;
204
- const mid = midMatch[1];
205
- if (resolveMilestoneFile(base, mid, "SUMMARY")) {
206
- try { unlinkSync(join(runtimeUnitsDir, file)); } catch (e) { debugLog("stale-unit-cleanup-failed", { file, error: getErrorMessage(e) }); }
225
+ // Clean stale runtime unit files for completed milestones (#887)
226
+ try {
227
+ const runtimeUnitsDir = join(gsdRoot(base), "runtime", "units");
228
+ if (existsSync(runtimeUnitsDir)) {
229
+ for (const file of readdirSync(runtimeUnitsDir)) {
230
+ if (!file.endsWith(".json")) continue;
231
+ const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
232
+ if (!midMatch) continue;
233
+ const mid = midMatch[1];
234
+ if (resolveMilestoneFile(base, mid, "SUMMARY")) {
235
+ try {
236
+ unlinkSync(join(runtimeUnitsDir, file));
237
+ } catch (e) {
238
+ debugLog("stale-unit-cleanup-failed", {
239
+ file,
240
+ error: e instanceof Error ? e.message : String(e),
241
+ });
242
+ }
243
+ }
207
244
  }
208
245
  }
246
+ } catch (e) {
247
+ debugLog("stale-unit-dir-cleanup-failed", {
248
+ error: e instanceof Error ? e.message : String(e),
249
+ });
209
250
  }
210
- } catch (e) { debugLog("stale-unit-dir-cleanup-failed", { error: getErrorMessage(e) }); }
211
-
212
- let state = await deriveState(base);
213
-
214
- // Milestone branch recovery (#601)
215
- let hasSurvivorBranch = false;
216
- if (
217
- state.activeMilestone &&
218
- (state.phase === "pre-planning" || state.phase === "needs-discussion") &&
219
- shouldUseWorktreeIsolation() &&
220
- !detectWorktreeName(base) &&
221
- !isInsideWorktree(base)
222
- ) {
223
- const milestoneBranch = `milestone/${state.activeMilestone.id}`;
224
- const { nativeBranchExists } = await import("./native-git-bridge.js");
225
- hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
226
- if (hasSurvivorBranch) {
227
- ctx.ui.notify(
228
- `Found prior session branch ${milestoneBranch}. Resuming.`,
229
- "info",
230
- );
231
- }
232
- }
233
251
 
234
- if (!hasSurvivorBranch) {
235
- // No active work — start a new milestone via discuss flow
236
- if (!state.activeMilestone || state.phase === "complete") {
237
- const { showSmartEntry } = await import("./guided-flow.js");
238
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
252
+ let state = await deriveState(base);
253
+
254
+ // Stale worktree state recovery (#654)
255
+ if (
256
+ state.activeMilestone &&
257
+ shouldUseWorktreeIsolation() &&
258
+ !detectWorktreeName(base)
259
+ ) {
260
+ const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
261
+ if (wtPath) {
262
+ state = await deriveState(wtPath);
263
+ }
264
+ }
239
265
 
240
- invalidateAllCaches();
241
- const postState = await deriveState(base);
242
- if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
243
- state = postState;
244
- } else if (postState.activeMilestone && postState.phase === "pre-planning") {
245
- const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
246
- const hasContext = !!(contextFile && await loadFile(contextFile));
247
- if (hasContext) {
248
- state = postState;
249
- } else {
250
- ctx.ui.notify(
251
- "Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.",
252
- "warning",
253
- );
254
- return false;
255
- }
256
- } else {
257
- return false;
266
+ // Milestone branch recovery (#601)
267
+ let hasSurvivorBranch = false;
268
+ if (
269
+ state.activeMilestone &&
270
+ (state.phase === "pre-planning" || state.phase === "needs-discussion") &&
271
+ shouldUseWorktreeIsolation() &&
272
+ !detectWorktreeName(base) &&
273
+ !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
274
+ ) {
275
+ const milestoneBranch = `milestone/${state.activeMilestone.id}`;
276
+ const { nativeBranchExists } = await import("./native-git-bridge.js");
277
+ hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
278
+ if (hasSurvivorBranch) {
279
+ ctx.ui.notify(
280
+ `Found prior session branch ${milestoneBranch}. Resuming.`,
281
+ "info",
282
+ );
258
283
  }
259
284
  }
260
285
 
261
- // Active milestone exists but has no roadmap
262
- if (state.phase === "pre-planning") {
263
- const mid = state.activeMilestone!.id;
264
- const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
265
- const hasContext = !!(contextFile && await loadFile(contextFile));
266
- if (!hasContext) {
286
+ if (!hasSurvivorBranch) {
287
+ // No active work — start a new milestone via discuss flow
288
+ if (!state.activeMilestone || state.phase === "complete") {
267
289
  const { showSmartEntry } = await import("./guided-flow.js");
268
290
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
269
291
 
270
292
  invalidateAllCaches();
271
293
  const postState = await deriveState(base);
272
- if (postState.activeMilestone && postState.phase !== "pre-planning") {
294
+ if (
295
+ postState.activeMilestone &&
296
+ postState.phase !== "complete" &&
297
+ postState.phase !== "pre-planning"
298
+ ) {
273
299
  state = postState;
274
- } else {
275
- ctx.ui.notify(
276
- "Discussion completed but milestone context is still missing. Run /gsd to try again.",
277
- "warning",
300
+ } else if (
301
+ postState.activeMilestone &&
302
+ postState.phase === "pre-planning"
303
+ ) {
304
+ const contextFile = resolveMilestoneFile(
305
+ base,
306
+ postState.activeMilestone.id,
307
+ "CONTEXT",
278
308
  );
279
- return false;
309
+ const hasContext = !!(contextFile && (await loadFile(contextFile)));
310
+ if (hasContext) {
311
+ state = postState;
312
+ } else {
313
+ ctx.ui.notify(
314
+ "Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.",
315
+ "warning",
316
+ );
317
+ return releaseLockAndReturn();
318
+ }
319
+ } else {
320
+ return releaseLockAndReturn();
280
321
  }
281
322
  }
282
- }
283
- }
284
323
 
285
- // Unreachable safety check
286
- if (!state.activeMilestone) {
287
- const { showSmartEntry } = await import("./guided-flow.js");
288
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
289
- return false;
290
- }
324
+ // Active milestone exists but has no roadmap
325
+ if (state.phase === "pre-planning") {
326
+ const mid = state.activeMilestone!.id;
327
+ const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
328
+ const hasContext = !!(contextFile && (await loadFile(contextFile)));
329
+ if (!hasContext) {
330
+ const { showSmartEntry } = await import("./guided-flow.js");
331
+ await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
332
+
333
+ invalidateAllCaches();
334
+ const postState = await deriveState(base);
335
+ if (postState.activeMilestone && postState.phase !== "pre-planning") {
336
+ state = postState;
337
+ } else {
338
+ ctx.ui.notify(
339
+ "Discussion completed but milestone context is still missing. Run /gsd to try again.",
340
+ "warning",
341
+ );
342
+ return releaseLockAndReturn();
343
+ }
344
+ }
345
+ }
346
+ }
291
347
 
292
- // ── Initialize session state ──
293
- s.active = true;
294
- s.stepMode = requestedStepMode;
295
- s.verbose = verboseMode;
296
- s.cmdCtx = ctx;
297
- s.basePath = base;
298
- s.unitDispatchCount.clear();
299
- s.unitRecoveryCount.clear();
300
- s.unitConsecutiveSkips.clear();
301
- s.lastBudgetAlertLevel = 0;
302
- s.unitLifetimeDispatches.clear();
303
- s.completedKeySet.clear();
304
- loadPersistedKeys(base, s.completedKeySet);
305
- resetHookState();
306
- restoreHookState(base);
307
- resetProactiveHealing();
308
- s.autoStartTime = Date.now();
309
- s.resourceVersionOnStart = readResourceVersion();
310
- s.completedUnits = [];
311
- s.pendingQuickTasks = [];
312
- s.currentUnit = null;
313
- s.currentMilestoneId = state.activeMilestone?.id ?? null;
314
- s.originalModelId = ctx.model?.id ?? null;
315
- s.originalModelProvider = ctx.model?.provider ?? null;
316
-
317
- // Register SIGTERM handler
318
- registerSigtermHandler(base);
319
-
320
- // Capture integration branch
321
- if (s.currentMilestoneId) {
322
- if (getIsolationMode() !== "none") {
323
- captureIntegrationBranch(base, s.currentMilestoneId);
348
+ // Unreachable safety check
349
+ if (!state.activeMilestone) {
350
+ const { showSmartEntry } = await import("./guided-flow.js");
351
+ await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
352
+ return releaseLockAndReturn();
324
353
  }
325
- setActiveMilestoneId(base, s.currentMilestoneId);
326
- }
327
354
 
328
- // ── Auto-worktree setup ──
329
- s.originalBasePath = base;
355
+ // ── Initialize session state ──
356
+ s.active = true;
357
+ s.stepMode = requestedStepMode;
358
+ s.verbose = verboseMode;
359
+ s.cmdCtx = ctx;
360
+ s.basePath = base;
361
+ s.unitDispatchCount.clear();
362
+ s.unitRecoveryCount.clear();
363
+ s.lastBudgetAlertLevel = 0;
364
+ s.unitLifetimeDispatches.clear();
365
+ resetHookState();
366
+ restoreHookState(base);
367
+ resetProactiveHealing();
368
+ s.autoStartTime = Date.now();
369
+ s.resourceVersionOnStart = readResourceVersion();
370
+ s.completedUnits = [];
371
+ s.pendingQuickTasks = [];
372
+ s.currentUnit = null;
373
+ s.currentMilestoneId = state.activeMilestone?.id ?? null;
374
+ s.originalModelId = ctx.model?.id ?? null;
375
+ s.originalModelProvider = ctx.model?.provider ?? null;
376
+
377
+ // Register SIGTERM handler
378
+ registerSigtermHandler(base);
379
+
380
+ // Capture integration branch
381
+ if (s.currentMilestoneId) {
382
+ if (getIsolationMode() !== "none") {
383
+ captureIntegrationBranch(base, s.currentMilestoneId, { commitDocs });
384
+ }
385
+ setActiveMilestoneId(base, s.currentMilestoneId);
386
+ }
330
387
 
331
- if (s.currentMilestoneId && shouldUseWorktreeIsolation() && !detectWorktreeName(base) && !isInsideWorktree(base)) {
332
- try {
333
- const existingWtPath = getAutoWorktreePath(base, s.currentMilestoneId);
334
- if (existingWtPath) {
335
- const wtPath = enterAutoWorktree(base, s.currentMilestoneId);
336
- s.basePath = wtPath;
337
- s.gitService = createGitService(s.basePath);
338
- ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
339
- } else {
340
- const wtPath = createAutoWorktree(base, s.currentMilestoneId);
341
- s.basePath = wtPath;
342
- s.gitService = createGitService(s.basePath);
343
- ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
388
+ // ── Auto-worktree setup ──
389
+ s.originalBasePath = base;
390
+
391
+ const isUnderGsdWorktrees = (p: string): boolean => {
392
+ const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
393
+ if (p.includes(marker)) return true;
394
+ const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
395
+ return p.endsWith(worktreesSuffix);
396
+ };
397
+
398
+ if (
399
+ s.currentMilestoneId &&
400
+ shouldUseWorktreeIsolation() &&
401
+ !detectWorktreeName(base) &&
402
+ !isUnderGsdWorktrees(base)
403
+ ) {
404
+ buildResolver().enterMilestone(s.currentMilestoneId, {
405
+ notify: ctx.ui.notify.bind(ctx.ui),
406
+ });
407
+ if (s.basePath !== base) {
408
+ // Successfully entered worktree — re-register SIGTERM handler at original base
409
+ registerSigtermHandler(s.originalBasePath);
344
410
  }
345
- registerSigtermHandler(s.originalBasePath);
346
- } catch (err) {
347
- ctx.ui.notify(
348
- `Auto-worktree setup failed: ${getErrorMessage(err)}. Continuing in project root.`,
349
- "warning",
350
- );
351
411
  }
352
- }
353
412
 
354
- // ── DB lifecycle ──
355
- const gsdDbPath = join(gsdRoot(s.basePath), "gsd.db");
356
- const gsdDirPath = gsdRoot(s.basePath);
357
- if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
358
- const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
359
- const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
360
- const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
361
- if (hasDecisions || hasRequirements || hasMilestones) {
413
+ // ── DB lifecycle ──
414
+ const gsdDbPath = join(s.basePath, ".gsd", "gsd.db");
415
+ const gsdDirPath = join(s.basePath, ".gsd");
416
+ if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
417
+ const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
418
+ const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
419
+ const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
420
+ if (hasDecisions || hasRequirements || hasMilestones) {
421
+ try {
422
+ const { openDatabase: openDb } = await import("./gsd-db.js");
423
+ const { migrateFromMarkdown } = await import("./md-importer.js");
424
+ openDb(gsdDbPath);
425
+ migrateFromMarkdown(s.basePath);
426
+ } catch (err) {
427
+ process.stderr.write(
428
+ `gsd-migrate: auto-migration failed: ${(err as Error).message}\n`,
429
+ );
430
+ }
431
+ }
432
+ }
433
+ if (existsSync(gsdDbPath) && !isDbAvailable()) {
362
434
  try {
363
435
  const { openDatabase: openDb } = await import("./gsd-db.js");
364
- const { migrateFromMarkdown } = await import("./md-importer.js");
365
436
  openDb(gsdDbPath);
366
- migrateFromMarkdown(s.basePath);
367
437
  } catch (err) {
368
- process.stderr.write(`gsd-migrate: auto-migration failed: ${(err as Error).message}\n`);
438
+ process.stderr.write(
439
+ `gsd-db: failed to open existing database: ${(err as Error).message}\n`,
440
+ );
369
441
  }
370
442
  }
371
- }
372
- if (existsSync(gsdDbPath) && !isDbAvailable()) {
373
- try {
374
- const { openDatabase: openDb } = await import("./gsd-db.js");
375
- openDb(gsdDbPath);
376
- } catch (err) {
377
- process.stderr.write(`gsd-db: failed to open existing database: ${(err as Error).message}\n`);
378
- }
379
- }
380
443
 
381
- // Initialize metrics
382
- initMetrics(s.basePath);
444
+ // Initialize metrics
445
+ initMetrics(s.basePath);
383
446
 
384
- // Initialize routing history
385
- initRoutingHistory(s.basePath);
447
+ // Initialize routing history
448
+ initRoutingHistory(s.basePath);
386
449
 
387
- // Capture session's model at auto-mode start (#650)
388
- const currentModel = ctx.model;
389
- if (currentModel) {
390
- s.autoModeStartModel = { provider: currentModel.provider, id: currentModel.id };
391
- }
450
+ // Capture session's model at auto-mode start (#650)
451
+ const currentModel = ctx.model;
452
+ if (currentModel) {
453
+ s.autoModeStartModel = {
454
+ provider: currentModel.provider,
455
+ id: currentModel.id,
456
+ };
457
+ }
392
458
 
393
- // Snapshot installed skills
394
- if (resolveSkillDiscoveryMode() !== "off") {
395
- snapshotSkills();
396
- }
459
+ // Snapshot installed skills
460
+ if (resolveSkillDiscoveryMode() !== "off") {
461
+ snapshotSkills();
462
+ }
397
463
 
398
- ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
399
- ctx.ui.setFooter(hideFooter);
400
- const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
401
- const pendingCount = (state.registry ?? []).filter(m => m.status !== 'complete' && m.status !== 'parked').length;
402
- const scopeMsg = pendingCount > 1
403
- ? `Will loop through ${pendingCount} milestones.`
404
- : "Will loop until milestone complete.";
405
- ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
406
-
407
- // Update lock file with milestone info (OS lock already acquired at bootstrap start)
408
- updateSessionLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
409
- writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
410
-
411
- // Secrets collection gate — pause instead of blocking (#1146)
412
- const mid = state.activeMilestone!.id;
413
- try {
414
- const manifestStatus = await getManifestStatus(base, mid);
415
- if (manifestStatus && manifestStatus.pending.length > 0) {
416
- const pendingKeys = manifestStatus.pending;
417
- const keyList = pendingKeys.map((k: string) => ` • ${k}`).join("\n");
418
- s.paused = true;
419
- s.pausedForSecrets = true;
464
+ ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
465
+ ctx.ui.setFooter(hideFooter);
466
+ const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
467
+ const pendingCount = (state.registry ?? []).filter(
468
+ (m) => m.status !== "complete" && m.status !== "parked",
469
+ ).length;
470
+ const scopeMsg =
471
+ pendingCount > 1
472
+ ? `Will loop through ${pendingCount} milestones.`
473
+ : "Will loop until milestone complete.";
474
+ ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
475
+
476
+ updateSessionLock(
477
+ lockBase(),
478
+ "starting",
479
+ s.currentMilestoneId ?? "unknown",
480
+ 0,
481
+ );
482
+ writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
483
+
484
+ // Secrets collection gate
485
+ const mid = state.activeMilestone!.id;
486
+ try {
487
+ const manifestStatus = await getManifestStatus(base, mid);
488
+ if (manifestStatus && manifestStatus.pending.length > 0) {
489
+ const result = await collectSecretsFromManifest(base, mid, ctx);
490
+ if (
491
+ result &&
492
+ result.applied &&
493
+ result.skipped &&
494
+ result.existingSkipped
495
+ ) {
496
+ ctx.ui.notify(
497
+ `Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
498
+ "info",
499
+ );
500
+ } else {
501
+ ctx.ui.notify("Secrets collection skipped.", "info");
502
+ }
503
+ }
504
+ } catch (err) {
420
505
  ctx.ui.notify(
421
- `Auto-mode paused: ${pendingKeys.length} env variable${pendingKeys.length > 1 ? "s" : ""} needed for ${mid}.\n${keyList}\n\nCollect them with /gsd secrets, then resume with /gsd auto.`,
422
- "warning",
423
- );
424
- ctx.ui.setStatus("gsd-auto", "paused");
425
- sendDesktopNotification(
426
- "GSD — Secrets Required",
427
- `${pendingKeys.length} env variable(s) needed for ${mid}. Run /gsd secrets to provide them.`,
506
+ `Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`,
428
507
  "warning",
429
- "attention",
430
508
  );
431
- // Notify remote channel if configured (one-way — never collect secrets via remote)
432
- sendRemoteNotification(
433
- "GSD — Secrets Required",
434
- `Auto-mode paused: ${pendingKeys.length} env variable(s) needed for ${mid}.\n${keyList}\n\nReturn to the terminal and run /gsd secrets to provide them securely.`,
435
- ).catch(() => {}); // fire-and-forget
436
- return false;
437
509
  }
438
- } catch (err) {
439
- ctx.ui.notify(
440
- `Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`,
441
- "warning",
442
- );
443
- }
444
510
 
445
- // Self-heal: clear stale runtime records
446
- await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
447
-
448
- // Self-heal: remove stale .git/index.lock
449
- try {
450
- const gitLockFile = join(base, ".git", "index.lock");
451
- if (existsSync(gitLockFile)) {
452
- const lockAge = Date.now() - statSync(gitLockFile).mtimeMs;
453
- if (lockAge > 60_000) {
454
- unlinkSync(gitLockFile);
455
- ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
511
+ // Self-heal: remove stale .git/index.lock
512
+ try {
513
+ const gitLockFile = join(base, ".git", "index.lock");
514
+ if (existsSync(gitLockFile)) {
515
+ const lockAge = Date.now() - statSync(gitLockFile).mtimeMs;
516
+ if (lockAge > 60_000) {
517
+ unlinkSync(gitLockFile);
518
+ ctx.ui.notify(
519
+ "Removed stale .git/index.lock from prior crash.",
520
+ "info",
521
+ );
522
+ }
456
523
  }
524
+ } catch (e) {
525
+ debugLog("git-lock-cleanup-failed", {
526
+ error: e instanceof Error ? e.message : String(e),
527
+ });
457
528
  }
458
- } catch (e) { debugLog("git-lock-cleanup-failed", { error: getErrorMessage(e) }); }
459
529
 
460
- // Pre-flight: validate milestone queue
461
- try {
462
- const msDir = join(gsdRoot(base), "milestones");
463
- if (existsSync(msDir)) {
464
- const milestoneIds = readdirSync(msDir, { withFileTypes: true })
465
- .filter(d => d.isDirectory() && /^M\d{3}/.test(d.name))
466
- .map(d => d.name.match(/^(M\d{3})/)?.[1] ?? d.name);
467
- if (milestoneIds.length > 1) {
468
- const issues: string[] = [];
469
- for (const id of milestoneIds) {
470
- const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
471
- if (draft) issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
472
- }
473
- if (issues.length > 0) {
474
- ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued.\n${issues.map(i => ` ⚠ ${i}`).join("\n")}`, "warning");
475
- } else {
476
- ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued. All have full context.`, "info");
530
+ // Pre-flight: validate milestone queue
531
+ try {
532
+ const msDir = join(base, ".gsd", "milestones");
533
+ if (existsSync(msDir)) {
534
+ const milestoneIds = readdirSync(msDir, { withFileTypes: true })
535
+ .filter((d) => d.isDirectory() && /^M\d{3}/.test(d.name))
536
+ .map((d) => d.name.match(/^(M\d{3})/)?.[1] ?? d.name);
537
+ if (milestoneIds.length > 1) {
538
+ const issues: string[] = [];
539
+ for (const id of milestoneIds) {
540
+ const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
541
+ if (draft)
542
+ issues.push(
543
+ `${id}: has CONTEXT-DRAFT.md (will pause for discussion)`,
544
+ );
545
+ }
546
+ if (issues.length > 0) {
547
+ ctx.ui.notify(
548
+ `Pre-flight: ${milestoneIds.length} milestones queued.\n${issues.map((i) => ` ⚠ ${i}`).join("\n")}`,
549
+ "warning",
550
+ );
551
+ } else {
552
+ ctx.ui.notify(
553
+ `Pre-flight: ${milestoneIds.length} milestones queued. All have full context.`,
554
+ "info",
555
+ );
556
+ }
477
557
  }
478
558
  }
559
+ } catch {
560
+ /* non-fatal */
479
561
  }
480
- } catch { /* non-fatal */ }
481
562
 
482
- return true;
563
+ return true;
564
+ } catch (err) {
565
+ releaseSessionLock(base);
566
+ clearLock(base);
567
+ throw err;
568
+ }
483
569
  }