gsd-pi 2.33.1-dev.9bafd68 → 2.33.1-dev.b1c6556

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 (26) hide show
  1. package/dist/resource-loader.js +3 -1
  2. package/dist/resources/extensions/gsd/auto-start.js +300 -288
  3. package/dist/resources/extensions/gsd/auto-worktree.js +71 -0
  4. package/dist/resources/extensions/gsd/auto.js +11 -1
  5. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  6. package/package.json +1 -1
  7. package/packages/pi-ai/dist/models.generated.d.ts +5498 -3645
  8. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  9. package/packages/pi-ai/dist/models.generated.js +2595 -742
  10. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  11. package/packages/pi-ai/src/models.generated.ts +2852 -999
  12. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  13. package/packages/pi-coding-agent/dist/core/agent-session.js +9 -0
  14. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  15. package/packages/pi-coding-agent/src/core/agent-session.ts +9 -0
  16. package/packages/pi-tui/dist/utils.d.ts.map +1 -1
  17. package/packages/pi-tui/dist/utils.js +32 -2
  18. package/packages/pi-tui/dist/utils.js.map +1 -1
  19. package/packages/pi-tui/src/utils.ts +28 -2
  20. package/src/resources/extensions/gsd/auto-start.ts +22 -10
  21. package/src/resources/extensions/gsd/auto-worktree.ts +71 -0
  22. package/src/resources/extensions/gsd/auto.ts +11 -0
  23. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  24. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  25. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +4 -4
  26. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
@@ -19,7 +19,7 @@ import { gsdRoot, resolveMilestoneFile, } from "./paths.js";
19
19
  import { invalidateAllCaches } from "./cache.js";
20
20
  import { synthesizeCrashRecovery } from "./session-forensics.js";
21
21
  import { writeLock, clearLock, readCrashLock, formatCrashInfo } from "./crash-recovery.js";
22
- import { acquireSessionLock, updateSessionLock, } from "./session-lock.js";
22
+ import { acquireSessionLock, updateSessionLock, releaseSessionLock, } from "./session-lock.js";
23
23
  import { selfHealRuntimeRecords } from "./auto-recovery.js";
24
24
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
25
25
  import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
@@ -58,336 +58,348 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
58
58
  ctx.ui.notify(`${lockResult.reason}\nStop it with \`kill ${lockResult.existingPid ?? "the other process"}\` before starting a new session.`, "error");
59
59
  return false;
60
60
  }
61
- // Ensure git repo exists
62
- if (!nativeIsRepo(base)) {
63
- const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
64
- nativeInit(base, mainBranch);
65
- }
66
- // Ensure .gitignore has baseline patterns
67
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
68
- const manageGitignore = gitPrefs?.manage_gitignore;
69
- ensureGitignore(base, { manageGitignore });
70
- if (manageGitignore !== false)
71
- untrackRuntimeFiles(base);
72
- // Migrate legacy in-project .gsd/ to external state directory
73
- recoverFailedMigration(base);
74
- const migration = migrateToExternalState(base);
75
- if (migration.error) {
76
- ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
77
- }
78
- // Ensure symlink exists (handles fresh projects and post-migration)
79
- ensureGsdSymlink(base);
80
- // Bootstrap .gsd/ if it doesn't exist
81
- const gsdDir = gsdRoot(base);
82
- if (!existsSync(gsdDir)) {
83
- mkdirSync(join(gsdDir, "milestones"), { recursive: true });
61
+ function releaseLockAndReturn() {
62
+ releaseSessionLock(base);
63
+ clearLock(base);
64
+ return false;
84
65
  }
85
- // Initialize GitServiceImpl
86
- s.gitService = createGitService(s.basePath);
87
- // Check for crash from previous session (use both old and new lock data).
88
- // Skip if the lock PID matches this process — acquireSessionLock() writes
89
- // to the same auto.lock file before this check, so we'd always false-positive.
90
- const crashLock = readCrashLock(base);
91
- if (crashLock && crashLock.pid !== process.pid) {
92
- // We already hold the session lock, so no concurrent session is running.
93
- // The crash lock is from a dead process — recover context from it.
94
- const recoveredMid = parseUnitId(crashLock.unitId).milestone;
95
- const milestoneAlreadyComplete = recoveredMid
96
- ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
97
- : false;
98
- if (milestoneAlreadyComplete) {
99
- ctx.ui.notify(`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`, "info");
66
+ try {
67
+ // Ensure git repo exists
68
+ if (!nativeIsRepo(base)) {
69
+ const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
70
+ nativeInit(base, mainBranch);
71
+ }
72
+ // Ensure .gitignore has baseline patterns
73
+ const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
74
+ const manageGitignore = gitPrefs?.manage_gitignore;
75
+ ensureGitignore(base, { manageGitignore });
76
+ if (manageGitignore !== false)
77
+ untrackRuntimeFiles(base);
78
+ // Migrate legacy in-project .gsd/ to external state directory
79
+ recoverFailedMigration(base);
80
+ const migration = migrateToExternalState(base);
81
+ if (migration.error) {
82
+ ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
100
83
  }
101
- else {
102
- const activityDir = join(gsdRoot(base), "activity");
103
- const recovery = synthesizeCrashRecovery(base, crashLock.unitType, crashLock.unitId, crashLock.sessionFile, activityDir);
104
- if (recovery && recovery.trace.toolCallCount > 0) {
105
- s.pendingCrashRecovery = recovery.prompt;
106
- ctx.ui.notify(`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`, "warning");
84
+ // Ensure symlink exists (handles fresh projects and post-migration)
85
+ ensureGsdSymlink(base);
86
+ // Bootstrap .gsd/ if it doesn't exist
87
+ const gsdDir = gsdRoot(base);
88
+ if (!existsSync(gsdDir)) {
89
+ mkdirSync(join(gsdDir, "milestones"), { recursive: true });
90
+ }
91
+ // Initialize GitServiceImpl
92
+ s.gitService = createGitService(s.basePath);
93
+ // Check for crash from previous session (use both old and new lock data).
94
+ // Skip if the lock PID matches this process — acquireSessionLock() writes
95
+ // to the same auto.lock file before this check, so we'd always false-positive.
96
+ const crashLock = readCrashLock(base);
97
+ if (crashLock && crashLock.pid !== process.pid) {
98
+ // We already hold the session lock, so no concurrent session is running.
99
+ // The crash lock is from a dead process — recover context from it.
100
+ const recoveredMid = parseUnitId(crashLock.unitId).milestone;
101
+ const milestoneAlreadyComplete = recoveredMid
102
+ ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
103
+ : false;
104
+ if (milestoneAlreadyComplete) {
105
+ ctx.ui.notify(`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`, "info");
107
106
  }
108
107
  else {
109
- ctx.ui.notify(`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`, "warning");
108
+ const activityDir = join(gsdRoot(base), "activity");
109
+ const recovery = synthesizeCrashRecovery(base, crashLock.unitType, crashLock.unitId, crashLock.sessionFile, activityDir);
110
+ if (recovery && recovery.trace.toolCallCount > 0) {
111
+ s.pendingCrashRecovery = recovery.prompt;
112
+ ctx.ui.notify(`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`, "warning");
113
+ }
114
+ else {
115
+ ctx.ui.notify(`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`, "warning");
116
+ }
110
117
  }
118
+ clearLock(base);
111
119
  }
112
- clearLock(base);
113
- }
114
- // ── Debug mode ──
115
- if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
116
- enableDebug(base);
117
- }
118
- if (isDebugEnabled()) {
119
- const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
120
- debugLog("debug-start", {
121
- platform: process.platform,
122
- arch: process.arch,
123
- node: process.version,
124
- model: ctx.model?.id ?? "unknown",
125
- provider: ctx.model?.provider ?? "unknown",
126
- nativeParser: isNativeParserAvailable(),
127
- cwd: base,
128
- });
129
- ctx.ui.notify(`Debug logging enabled ${getDebugLogPath()}`, "info");
130
- }
131
- // Invalidate caches before initial state derivation
132
- invalidateAllCaches();
133
- // Clean stale runtime unit files for completed milestones (#887)
134
- try {
135
- const runtimeUnitsDir = join(gsdRoot(base), "runtime", "units");
136
- if (existsSync(runtimeUnitsDir)) {
137
- for (const file of readdirSync(runtimeUnitsDir)) {
138
- if (!file.endsWith(".json"))
139
- continue;
140
- const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
141
- if (!midMatch)
142
- continue;
143
- const mid = midMatch[1];
144
- if (resolveMilestoneFile(base, mid, "SUMMARY")) {
145
- try {
146
- unlinkSync(join(runtimeUnitsDir, file));
147
- }
148
- catch (e) {
149
- debugLog("stale-unit-cleanup-failed", { file, error: getErrorMessage(e) });
120
+ // ── Debug mode ──
121
+ if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
122
+ enableDebug(base);
123
+ }
124
+ if (isDebugEnabled()) {
125
+ const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
126
+ debugLog("debug-start", {
127
+ platform: process.platform,
128
+ arch: process.arch,
129
+ node: process.version,
130
+ model: ctx.model?.id ?? "unknown",
131
+ provider: ctx.model?.provider ?? "unknown",
132
+ nativeParser: isNativeParserAvailable(),
133
+ cwd: base,
134
+ });
135
+ ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
136
+ }
137
+ // Invalidate caches before initial state derivation
138
+ invalidateAllCaches();
139
+ // Clean stale runtime unit files for completed milestones (#887)
140
+ try {
141
+ const runtimeUnitsDir = join(gsdRoot(base), "runtime", "units");
142
+ if (existsSync(runtimeUnitsDir)) {
143
+ for (const file of readdirSync(runtimeUnitsDir)) {
144
+ if (!file.endsWith(".json"))
145
+ continue;
146
+ const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
147
+ if (!midMatch)
148
+ continue;
149
+ const mid = midMatch[1];
150
+ if (resolveMilestoneFile(base, mid, "SUMMARY")) {
151
+ try {
152
+ unlinkSync(join(runtimeUnitsDir, file));
153
+ }
154
+ catch (e) {
155
+ debugLog("stale-unit-cleanup-failed", { file, error: getErrorMessage(e) });
156
+ }
150
157
  }
151
158
  }
152
159
  }
153
160
  }
154
- }
155
- catch (e) {
156
- debugLog("stale-unit-dir-cleanup-failed", { error: getErrorMessage(e) });
157
- }
158
- let state = await deriveState(base);
159
- // Milestone branch recovery (#601)
160
- let hasSurvivorBranch = false;
161
- if (state.activeMilestone &&
162
- (state.phase === "pre-planning" || state.phase === "needs-discussion") &&
163
- shouldUseWorktreeIsolation() &&
164
- !detectWorktreeName(base) &&
165
- !isInsideWorktree(base)) {
166
- const milestoneBranch = `milestone/${state.activeMilestone.id}`;
167
- const { nativeBranchExists } = await import("./native-git-bridge.js");
168
- hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
169
- if (hasSurvivorBranch) {
170
- ctx.ui.notify(`Found prior session branch ${milestoneBranch}. Resuming.`, "info");
161
+ catch (e) {
162
+ debugLog("stale-unit-dir-cleanup-failed", { error: getErrorMessage(e) });
171
163
  }
172
- }
173
- if (!hasSurvivorBranch) {
174
- // No active work — start a new milestone via discuss flow
175
- if (!state.activeMilestone || state.phase === "complete") {
176
- const { showSmartEntry } = await import("./guided-flow.js");
177
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
178
- invalidateAllCaches();
179
- const postState = await deriveState(base);
180
- if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
181
- state = postState;
182
- }
183
- else if (postState.activeMilestone && postState.phase === "pre-planning") {
184
- const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
185
- const hasContext = !!(contextFile && await loadFile(contextFile));
186
- if (hasContext) {
187
- state = postState;
188
- }
189
- else {
190
- ctx.ui.notify("Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.", "warning");
191
- return false;
192
- }
193
- }
194
- else {
195
- return false;
164
+ let state = await deriveState(base);
165
+ // Milestone branch recovery (#601)
166
+ let hasSurvivorBranch = false;
167
+ if (state.activeMilestone &&
168
+ (state.phase === "pre-planning" || state.phase === "needs-discussion") &&
169
+ shouldUseWorktreeIsolation() &&
170
+ !detectWorktreeName(base) &&
171
+ !isInsideWorktree(base)) {
172
+ const milestoneBranch = `milestone/${state.activeMilestone.id}`;
173
+ const { nativeBranchExists } = await import("./native-git-bridge.js");
174
+ hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
175
+ if (hasSurvivorBranch) {
176
+ ctx.ui.notify(`Found prior session branch ${milestoneBranch}. Resuming.`, "info");
196
177
  }
197
178
  }
198
- // Active milestone exists but has no roadmap
199
- if (state.phase === "pre-planning") {
200
- const mid = state.activeMilestone.id;
201
- const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
202
- const hasContext = !!(contextFile && await loadFile(contextFile));
203
- if (!hasContext) {
179
+ if (!hasSurvivorBranch) {
180
+ // No active work — start a new milestone via discuss flow
181
+ if (!state.activeMilestone || state.phase === "complete") {
204
182
  const { showSmartEntry } = await import("./guided-flow.js");
205
183
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
206
184
  invalidateAllCaches();
207
185
  const postState = await deriveState(base);
208
- if (postState.activeMilestone && postState.phase !== "pre-planning") {
186
+ if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
209
187
  state = postState;
210
188
  }
189
+ else if (postState.activeMilestone && postState.phase === "pre-planning") {
190
+ const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
191
+ const hasContext = !!(contextFile && await loadFile(contextFile));
192
+ if (hasContext) {
193
+ state = postState;
194
+ }
195
+ else {
196
+ ctx.ui.notify("Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.", "warning");
197
+ return releaseLockAndReturn();
198
+ }
199
+ }
211
200
  else {
212
- ctx.ui.notify("Discussion completed but milestone context is still missing. Run /gsd to try again.", "warning");
213
- return false;
201
+ return releaseLockAndReturn();
202
+ }
203
+ }
204
+ // Active milestone exists but has no roadmap
205
+ if (state.phase === "pre-planning") {
206
+ const mid = state.activeMilestone.id;
207
+ const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
208
+ const hasContext = !!(contextFile && await loadFile(contextFile));
209
+ if (!hasContext) {
210
+ const { showSmartEntry } = await import("./guided-flow.js");
211
+ await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
212
+ invalidateAllCaches();
213
+ const postState = await deriveState(base);
214
+ if (postState.activeMilestone && postState.phase !== "pre-planning") {
215
+ state = postState;
216
+ }
217
+ else {
218
+ ctx.ui.notify("Discussion completed but milestone context is still missing. Run /gsd to try again.", "warning");
219
+ return releaseLockAndReturn();
220
+ }
214
221
  }
215
222
  }
216
223
  }
217
- }
218
- // Unreachable safety check
219
- if (!state.activeMilestone) {
220
- const { showSmartEntry } = await import("./guided-flow.js");
221
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
222
- return false;
223
- }
224
- // ── Initialize session state ──
225
- s.active = true;
226
- s.stepMode = requestedStepMode;
227
- s.verbose = verboseMode;
228
- s.cmdCtx = ctx;
229
- s.basePath = base;
230
- s.unitDispatchCount.clear();
231
- s.unitRecoveryCount.clear();
232
- s.unitConsecutiveSkips.clear();
233
- s.lastBudgetAlertLevel = 0;
234
- s.unitLifetimeDispatches.clear();
235
- s.completedKeySet.clear();
236
- loadPersistedKeys(base, s.completedKeySet);
237
- resetHookState();
238
- restoreHookState(base);
239
- resetProactiveHealing();
240
- s.autoStartTime = Date.now();
241
- s.resourceVersionOnStart = readResourceVersion();
242
- s.completedUnits = [];
243
- s.pendingQuickTasks = [];
244
- s.currentUnit = null;
245
- s.currentMilestoneId = state.activeMilestone?.id ?? null;
246
- s.originalModelId = ctx.model?.id ?? null;
247
- s.originalModelProvider = ctx.model?.provider ?? null;
248
- // Register SIGTERM handler
249
- registerSigtermHandler(base);
250
- // Capture integration branch
251
- if (s.currentMilestoneId) {
252
- if (getIsolationMode() !== "none") {
253
- captureIntegrationBranch(base, s.currentMilestoneId);
224
+ // Unreachable safety check
225
+ if (!state.activeMilestone) {
226
+ const { showSmartEntry } = await import("./guided-flow.js");
227
+ await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
228
+ return releaseLockAndReturn();
254
229
  }
255
- setActiveMilestoneId(base, s.currentMilestoneId);
256
- }
257
- // ── Auto-worktree setup ──
258
- s.originalBasePath = base;
259
- if (s.currentMilestoneId && shouldUseWorktreeIsolation() && !detectWorktreeName(base) && !isInsideWorktree(base)) {
260
- try {
261
- const existingWtPath = getAutoWorktreePath(base, s.currentMilestoneId);
262
- if (existingWtPath) {
263
- const wtPath = enterAutoWorktree(base, s.currentMilestoneId);
264
- s.basePath = wtPath;
265
- s.gitService = createGitService(s.basePath);
266
- ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
230
+ // ── Initialize session state ──
231
+ s.active = true;
232
+ s.stepMode = requestedStepMode;
233
+ s.verbose = verboseMode;
234
+ s.cmdCtx = ctx;
235
+ s.basePath = base;
236
+ s.unitDispatchCount.clear();
237
+ s.unitRecoveryCount.clear();
238
+ s.unitConsecutiveSkips.clear();
239
+ s.lastBudgetAlertLevel = 0;
240
+ s.unitLifetimeDispatches.clear();
241
+ s.completedKeySet.clear();
242
+ loadPersistedKeys(base, s.completedKeySet);
243
+ resetHookState();
244
+ restoreHookState(base);
245
+ resetProactiveHealing();
246
+ s.autoStartTime = Date.now();
247
+ s.resourceVersionOnStart = readResourceVersion();
248
+ s.completedUnits = [];
249
+ s.pendingQuickTasks = [];
250
+ s.currentUnit = null;
251
+ s.currentMilestoneId = state.activeMilestone?.id ?? null;
252
+ s.originalModelId = ctx.model?.id ?? null;
253
+ s.originalModelProvider = ctx.model?.provider ?? null;
254
+ // Register SIGTERM handler
255
+ registerSigtermHandler(base);
256
+ // Capture integration branch
257
+ if (s.currentMilestoneId) {
258
+ if (getIsolationMode() !== "none") {
259
+ captureIntegrationBranch(base, s.currentMilestoneId);
267
260
  }
268
- else {
269
- const wtPath = createAutoWorktree(base, s.currentMilestoneId);
270
- s.basePath = wtPath;
271
- s.gitService = createGitService(s.basePath);
272
- ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
261
+ setActiveMilestoneId(base, s.currentMilestoneId);
262
+ }
263
+ // ── Auto-worktree setup ──
264
+ s.originalBasePath = base;
265
+ if (s.currentMilestoneId && shouldUseWorktreeIsolation() && !detectWorktreeName(base) && !isInsideWorktree(base)) {
266
+ try {
267
+ const existingWtPath = getAutoWorktreePath(base, s.currentMilestoneId);
268
+ if (existingWtPath) {
269
+ const wtPath = enterAutoWorktree(base, s.currentMilestoneId);
270
+ s.basePath = wtPath;
271
+ s.gitService = createGitService(s.basePath);
272
+ ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
273
+ }
274
+ else {
275
+ const wtPath = createAutoWorktree(base, s.currentMilestoneId);
276
+ s.basePath = wtPath;
277
+ s.gitService = createGitService(s.basePath);
278
+ ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
279
+ }
280
+ registerSigtermHandler(s.originalBasePath);
281
+ }
282
+ catch (err) {
283
+ ctx.ui.notify(`Auto-worktree setup failed: ${getErrorMessage(err)}. Continuing in project root.`, "warning");
273
284
  }
274
- registerSigtermHandler(s.originalBasePath);
275
285
  }
276
- catch (err) {
277
- ctx.ui.notify(`Auto-worktree setup failed: ${getErrorMessage(err)}. Continuing in project root.`, "warning");
286
+ // ── DB lifecycle ──
287
+ const gsdDbPath = join(gsdRoot(s.basePath), "gsd.db");
288
+ const gsdDirPath = gsdRoot(s.basePath);
289
+ if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
290
+ const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
291
+ const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
292
+ const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
293
+ if (hasDecisions || hasRequirements || hasMilestones) {
294
+ try {
295
+ const { openDatabase: openDb } = await import("./gsd-db.js");
296
+ const { migrateFromMarkdown } = await import("./md-importer.js");
297
+ openDb(gsdDbPath);
298
+ migrateFromMarkdown(s.basePath);
299
+ }
300
+ catch (err) {
301
+ process.stderr.write(`gsd-migrate: auto-migration failed: ${err.message}\n`);
302
+ }
303
+ }
278
304
  }
279
- }
280
- // ── DB lifecycle ──
281
- const gsdDbPath = join(gsdRoot(s.basePath), "gsd.db");
282
- const gsdDirPath = gsdRoot(s.basePath);
283
- if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
284
- const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
285
- const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
286
- const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
287
- if (hasDecisions || hasRequirements || hasMilestones) {
305
+ if (existsSync(gsdDbPath) && !isDbAvailable()) {
288
306
  try {
289
307
  const { openDatabase: openDb } = await import("./gsd-db.js");
290
- const { migrateFromMarkdown } = await import("./md-importer.js");
291
308
  openDb(gsdDbPath);
292
- migrateFromMarkdown(s.basePath);
293
309
  }
294
310
  catch (err) {
295
- process.stderr.write(`gsd-migrate: auto-migration failed: ${err.message}\n`);
311
+ process.stderr.write(`gsd-db: failed to open existing database: ${err.message}\n`);
296
312
  }
297
313
  }
298
- }
299
- if (existsSync(gsdDbPath) && !isDbAvailable()) {
314
+ // Initialize metrics
315
+ initMetrics(s.basePath);
316
+ // Initialize routing history
317
+ initRoutingHistory(s.basePath);
318
+ // Capture session's model at auto-mode start (#650)
319
+ const currentModel = ctx.model;
320
+ if (currentModel) {
321
+ s.autoModeStartModel = { provider: currentModel.provider, id: currentModel.id };
322
+ }
323
+ // Snapshot installed skills
324
+ if (resolveSkillDiscoveryMode() !== "off") {
325
+ snapshotSkills();
326
+ }
327
+ ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
328
+ ctx.ui.setFooter(hideFooter);
329
+ const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
330
+ const pendingCount = (state.registry ?? []).filter(m => m.status !== 'complete' && m.status !== 'parked').length;
331
+ const scopeMsg = pendingCount > 1
332
+ ? `Will loop through ${pendingCount} milestones.`
333
+ : "Will loop until milestone complete.";
334
+ ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
335
+ // Update lock file with milestone info (OS lock already acquired at bootstrap start)
336
+ updateSessionLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
337
+ writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
338
+ // Secrets collection gate — pause instead of blocking (#1146)
339
+ const mid = state.activeMilestone.id;
300
340
  try {
301
- const { openDatabase: openDb } = await import("./gsd-db.js");
302
- openDb(gsdDbPath);
341
+ const manifestStatus = await getManifestStatus(base, mid);
342
+ if (manifestStatus && manifestStatus.pending.length > 0) {
343
+ const pendingKeys = manifestStatus.pending;
344
+ const keyList = pendingKeys.map((k) => ` • ${k}`).join("\n");
345
+ s.paused = true;
346
+ s.pausedForSecrets = true;
347
+ ctx.ui.notify(`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.`, "warning");
348
+ ctx.ui.setStatus("gsd-auto", "paused");
349
+ sendDesktopNotification("GSD — Secrets Required", `${pendingKeys.length} env variable(s) needed for ${mid}. Run /gsd secrets to provide them.`, "warning", "attention");
350
+ // Notify remote channel if configured (one-way — never collect secrets via remote)
351
+ sendRemoteNotification("GSD — Secrets Required", `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.`).catch(() => { }); // fire-and-forget
352
+ return false;
353
+ }
303
354
  }
304
355
  catch (err) {
305
- process.stderr.write(`gsd-db: failed to open existing database: ${err.message}\n`);
356
+ ctx.ui.notify(`Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`, "warning");
306
357
  }
307
- }
308
- // Initialize metrics
309
- initMetrics(s.basePath);
310
- // Initialize routing history
311
- initRoutingHistory(s.basePath);
312
- // Capture session's model at auto-mode start (#650)
313
- const currentModel = ctx.model;
314
- if (currentModel) {
315
- s.autoModeStartModel = { provider: currentModel.provider, id: currentModel.id };
316
- }
317
- // Snapshot installed skills
318
- if (resolveSkillDiscoveryMode() !== "off") {
319
- snapshotSkills();
320
- }
321
- ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
322
- ctx.ui.setFooter(hideFooter);
323
- const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
324
- const pendingCount = (state.registry ?? []).filter(m => m.status !== 'complete' && m.status !== 'parked').length;
325
- const scopeMsg = pendingCount > 1
326
- ? `Will loop through ${pendingCount} milestones.`
327
- : "Will loop until milestone complete.";
328
- ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
329
- // Update lock file with milestone info (OS lock already acquired at bootstrap start)
330
- updateSessionLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
331
- writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
332
- // Secrets collection gate — pause instead of blocking (#1146)
333
- const mid = state.activeMilestone.id;
334
- try {
335
- const manifestStatus = await getManifestStatus(base, mid);
336
- if (manifestStatus && manifestStatus.pending.length > 0) {
337
- const pendingKeys = manifestStatus.pending;
338
- const keyList = pendingKeys.map((k) => ` • ${k}`).join("\n");
339
- s.paused = true;
340
- s.pausedForSecrets = true;
341
- ctx.ui.notify(`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.`, "warning");
342
- ctx.ui.setStatus("gsd-auto", "paused");
343
- sendDesktopNotification("GSD — Secrets Required", `${pendingKeys.length} env variable(s) needed for ${mid}. Run /gsd secrets to provide them.`, "warning", "attention");
344
- // Notify remote channel if configured (one-way — never collect secrets via remote)
345
- sendRemoteNotification("GSD — Secrets Required", `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.`).catch(() => { }); // fire-and-forget
346
- return false;
347
- }
348
- }
349
- catch (err) {
350
- ctx.ui.notify(`Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`, "warning");
351
- }
352
- // Self-heal: clear stale runtime records
353
- await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
354
- // Self-heal: remove stale .git/index.lock
355
- try {
356
- const gitLockFile = join(base, ".git", "index.lock");
357
- if (existsSync(gitLockFile)) {
358
- const lockAge = Date.now() - statSync(gitLockFile).mtimeMs;
359
- if (lockAge > 60_000) {
360
- unlinkSync(gitLockFile);
361
- ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
358
+ // Self-heal: clear stale runtime records
359
+ await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
360
+ // Self-heal: remove stale .git/index.lock
361
+ try {
362
+ const gitLockFile = join(base, ".git", "index.lock");
363
+ if (existsSync(gitLockFile)) {
364
+ const lockAge = Date.now() - statSync(gitLockFile).mtimeMs;
365
+ if (lockAge > 60_000) {
366
+ unlinkSync(gitLockFile);
367
+ ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
368
+ }
362
369
  }
363
370
  }
364
- }
365
- catch (e) {
366
- debugLog("git-lock-cleanup-failed", { error: getErrorMessage(e) });
367
- }
368
- // Pre-flight: validate milestone queue
369
- try {
370
- const msDir = join(gsdRoot(base), "milestones");
371
- if (existsSync(msDir)) {
372
- const milestoneIds = readdirSync(msDir, { withFileTypes: true })
373
- .filter(d => d.isDirectory() && /^M\d{3}/.test(d.name))
374
- .map(d => d.name.match(/^(M\d{3})/)?.[1] ?? d.name);
375
- if (milestoneIds.length > 1) {
376
- const issues = [];
377
- for (const id of milestoneIds) {
378
- const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
379
- if (draft)
380
- issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
381
- }
382
- if (issues.length > 0) {
383
- ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued.\n${issues.map(i => ` ⚠ ${i}`).join("\n")}`, "warning");
384
- }
385
- else {
386
- ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued. All have full context.`, "info");
371
+ catch (e) {
372
+ debugLog("git-lock-cleanup-failed", { error: getErrorMessage(e) });
373
+ }
374
+ // Pre-flight: validate milestone queue
375
+ try {
376
+ const msDir = join(gsdRoot(base), "milestones");
377
+ if (existsSync(msDir)) {
378
+ const milestoneIds = readdirSync(msDir, { withFileTypes: true })
379
+ .filter(d => d.isDirectory() && /^M\d{3}/.test(d.name))
380
+ .map(d => d.name.match(/^(M\d{3})/)?.[1] ?? d.name);
381
+ if (milestoneIds.length > 1) {
382
+ const issues = [];
383
+ for (const id of milestoneIds) {
384
+ const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
385
+ if (draft)
386
+ issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
387
+ }
388
+ if (issues.length > 0) {
389
+ ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued.\n${issues.map(i => ` ⚠ ${i}`).join("\n")}`, "warning");
390
+ }
391
+ else {
392
+ ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued. All have full context.`, "info");
393
+ }
387
394
  }
388
395
  }
389
396
  }
397
+ catch { /* non-fatal */ }
398
+ return true;
399
+ }
400
+ catch (err) {
401
+ releaseSessionLock(base);
402
+ clearLock(base);
403
+ throw err;
390
404
  }
391
- catch { /* non-fatal */ }
392
- return true;
393
405
  }