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.
- package/dist/resource-loader.js +3 -1
- package/dist/resources/extensions/gsd/auto-start.js +300 -288
- package/dist/resources/extensions/gsd/auto-worktree.js +71 -0
- package/dist/resources/extensions/gsd/auto.js +11 -1
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +5498 -3645
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +2595 -742
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +2852 -999
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +9 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +9 -0
- package/packages/pi-tui/dist/utils.d.ts.map +1 -1
- package/packages/pi-tui/dist/utils.js +32 -2
- package/packages/pi-tui/dist/utils.js.map +1 -1
- package/packages/pi-tui/src/utils.ts +28 -2
- package/src/resources/extensions/gsd/auto-start.ts +22 -10
- package/src/resources/extensions/gsd/auto-worktree.ts +71 -0
- package/src/resources/extensions/gsd/auto.ts +11 -0
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +4 -4
- 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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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-
|
|
311
|
+
process.stderr.write(`gsd-db: failed to open existing database: ${err.message}\n`);
|
|
296
312
|
}
|
|
297
313
|
}
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
302
|
-
|
|
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
|
-
|
|
356
|
+
ctx.ui.notify(`Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`, "warning");
|
|
306
357
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
}
|