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.
- package/dist/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -10,36 +10,30 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { deriveState } from "./state.js";
|
|
12
12
|
import { loadFile, getManifestStatus } from "./files.js";
|
|
13
|
-
import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode } from "./preferences.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { sendDesktopNotification } from "./notifications.js";
|
|
17
|
-
import { sendRemoteNotification } from "../remote-questions/notify.js";
|
|
18
|
-
import { gsdRoot, resolveMilestoneFile, } from "./paths.js";
|
|
13
|
+
import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, } from "./preferences.js";
|
|
14
|
+
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
15
|
+
import { gsdRoot, resolveMilestoneFile } from "./paths.js";
|
|
19
16
|
import { invalidateAllCaches } from "./cache.js";
|
|
20
17
|
import { synthesizeCrashRecovery } from "./session-forensics.js";
|
|
21
|
-
import { writeLock, clearLock, readCrashLock, formatCrashInfo } from "./crash-recovery.js";
|
|
22
|
-
import { acquireSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
23
|
-
import { selfHealRuntimeRecords } from "./auto-recovery.js";
|
|
18
|
+
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive, } from "./crash-recovery.js";
|
|
19
|
+
import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
24
20
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
25
|
-
import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
|
|
26
|
-
import {
|
|
21
|
+
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, } from "./native-git-bridge.js";
|
|
22
|
+
import { GitServiceImpl } from "./git-service.js";
|
|
27
23
|
import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
|
|
28
|
-
import {
|
|
29
|
-
import { readResourceVersion } from "./
|
|
24
|
+
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
25
|
+
import { readResourceVersion } from "./auto-worktree-sync.js";
|
|
30
26
|
import { initMetrics } from "./metrics.js";
|
|
31
27
|
import { initRoutingHistory } from "./routing-history.js";
|
|
32
28
|
import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
|
|
33
29
|
import { resetProactiveHealing } from "./doctor-proactive.js";
|
|
34
30
|
import { snapshotSkills } from "./skill-discovery.js";
|
|
35
31
|
import { isDbAvailable } from "./gsd-db.js";
|
|
36
|
-
import { loadPersistedKeys } from "./auto-recovery.js";
|
|
37
32
|
import { hideFooter } from "./auto-dashboard.js";
|
|
38
|
-
import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath } from "./debug-logger.js";
|
|
39
|
-
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
33
|
+
import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
|
|
34
|
+
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:fs";
|
|
40
35
|
import { join } from "node:path";
|
|
41
|
-
import {
|
|
42
|
-
import { parseUnitId } from "./unit-id.js";
|
|
36
|
+
import { sep as pathSep } from "node:path";
|
|
43
37
|
/**
|
|
44
38
|
* Bootstrap a fresh auto-mode session. Handles everything from git init
|
|
45
39
|
* through secrets collection, returning when ready for the first
|
|
@@ -49,345 +43,372 @@ import { parseUnitId } from "./unit-id.js";
|
|
|
49
43
|
* concurrent session detected). Returns true when ready to dispatch.
|
|
50
44
|
*/
|
|
51
45
|
export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
|
|
52
|
-
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase } = deps;
|
|
53
|
-
// ── Session lock: acquire FIRST, before any state mutation ──────────────
|
|
54
|
-
// This is the primary guard against concurrent sessions on the same project.
|
|
55
|
-
// Uses OS-level file locking (proper-lockfile) to prevent TOCTOU races.
|
|
46
|
+
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
|
|
56
47
|
const lockResult = acquireSessionLock(base);
|
|
57
48
|
if (!lockResult.acquired) {
|
|
58
|
-
ctx.ui.notify(
|
|
49
|
+
ctx.ui.notify(lockResult.reason, "error");
|
|
59
50
|
return false;
|
|
60
51
|
}
|
|
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 });
|
|
52
|
+
function releaseLockAndReturn() {
|
|
53
|
+
releaseSessionLock(base);
|
|
54
|
+
clearLock(base);
|
|
55
|
+
return false;
|
|
84
56
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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");
|
|
57
|
+
try {
|
|
58
|
+
// Ensure git repo exists
|
|
59
|
+
if (!nativeIsRepo(base)) {
|
|
60
|
+
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
61
|
+
nativeInit(base, mainBranch);
|
|
100
62
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
63
|
+
// Ensure .gitignore has baseline patterns
|
|
64
|
+
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
65
|
+
const commitDocs = gitPrefs?.commit_docs;
|
|
66
|
+
const manageGitignore = gitPrefs?.manage_gitignore;
|
|
67
|
+
ensureGitignore(base, { commitDocs, manageGitignore });
|
|
68
|
+
if (manageGitignore !== false)
|
|
69
|
+
untrackRuntimeFiles(base);
|
|
70
|
+
// Bootstrap .gsd/ if it doesn't exist
|
|
71
|
+
const gsdDir = join(base, ".gsd");
|
|
72
|
+
if (!existsSync(gsdDir)) {
|
|
73
|
+
mkdirSync(join(gsdDir, "milestones"), { recursive: true });
|
|
74
|
+
if (commitDocs !== false) {
|
|
75
|
+
try {
|
|
76
|
+
nativeAddAll(base);
|
|
77
|
+
nativeCommit(base, "chore: init gsd");
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* nothing to commit */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Initialize GitServiceImpl
|
|
85
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
86
|
+
// Check for crash from previous session. Skip our own fresh bootstrap lock.
|
|
87
|
+
const crashLock = readCrashLock(base);
|
|
88
|
+
if (crashLock && crashLock.pid !== process.pid) {
|
|
89
|
+
if (isLockProcessAlive(crashLock)) {
|
|
90
|
+
ctx.ui.notify(`Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`, "error");
|
|
91
|
+
return releaseLockAndReturn();
|
|
92
|
+
}
|
|
93
|
+
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
94
|
+
const milestoneAlreadyComplete = recoveredMid
|
|
95
|
+
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
96
|
+
: false;
|
|
97
|
+
if (milestoneAlreadyComplete) {
|
|
98
|
+
ctx.ui.notify(`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`, "info");
|
|
107
99
|
}
|
|
108
100
|
else {
|
|
109
|
-
|
|
101
|
+
const activityDir = join(gsdRoot(base), "activity");
|
|
102
|
+
const recovery = synthesizeCrashRecovery(base, crashLock.unitType, crashLock.unitId, crashLock.sessionFile, activityDir);
|
|
103
|
+
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
104
|
+
s.pendingCrashRecovery = recovery.prompt;
|
|
105
|
+
ctx.ui.notify(`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`, "warning");
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
ctx.ui.notify(`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`, "warning");
|
|
109
|
+
}
|
|
110
110
|
}
|
|
111
|
+
clearLock(base);
|
|
111
112
|
}
|
|
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
|
-
|
|
113
|
+
// ── Debug mode ──
|
|
114
|
+
if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
|
|
115
|
+
enableDebug(base);
|
|
116
|
+
}
|
|
117
|
+
if (isDebugEnabled()) {
|
|
118
|
+
const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
|
|
119
|
+
debugLog("debug-start", {
|
|
120
|
+
platform: process.platform,
|
|
121
|
+
arch: process.arch,
|
|
122
|
+
node: process.version,
|
|
123
|
+
model: ctx.model?.id ?? "unknown",
|
|
124
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
125
|
+
nativeParser: isNativeParserAvailable(),
|
|
126
|
+
cwd: base,
|
|
127
|
+
});
|
|
128
|
+
ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
|
|
129
|
+
}
|
|
130
|
+
// Invalidate caches before initial state derivation
|
|
131
|
+
invalidateAllCaches();
|
|
132
|
+
// Clean stale runtime unit files for completed milestones (#887)
|
|
133
|
+
try {
|
|
134
|
+
const runtimeUnitsDir = join(gsdRoot(base), "runtime", "units");
|
|
135
|
+
if (existsSync(runtimeUnitsDir)) {
|
|
136
|
+
for (const file of readdirSync(runtimeUnitsDir)) {
|
|
137
|
+
if (!file.endsWith(".json"))
|
|
138
|
+
continue;
|
|
139
|
+
const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
|
|
140
|
+
if (!midMatch)
|
|
141
|
+
continue;
|
|
142
|
+
const mid = midMatch[1];
|
|
143
|
+
if (resolveMilestoneFile(base, mid, "SUMMARY")) {
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(join(runtimeUnitsDir, file));
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
debugLog("stale-unit-cleanup-failed", {
|
|
149
|
+
file,
|
|
150
|
+
error: e instanceof Error ? e.message : String(e),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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");
|
|
157
|
+
catch (e) {
|
|
158
|
+
debugLog("stale-unit-dir-cleanup-failed", {
|
|
159
|
+
error: e instanceof Error ? e.message : String(e),
|
|
160
|
+
});
|
|
171
161
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
|
|
181
|
-
state = postState;
|
|
162
|
+
let state = await deriveState(base);
|
|
163
|
+
// Stale worktree state recovery (#654)
|
|
164
|
+
if (state.activeMilestone &&
|
|
165
|
+
shouldUseWorktreeIsolation() &&
|
|
166
|
+
!detectWorktreeName(base)) {
|
|
167
|
+
const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
|
|
168
|
+
if (wtPath) {
|
|
169
|
+
state = await deriveState(wtPath);
|
|
182
170
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
171
|
+
}
|
|
172
|
+
// Milestone branch recovery (#601)
|
|
173
|
+
let hasSurvivorBranch = false;
|
|
174
|
+
if (state.activeMilestone &&
|
|
175
|
+
(state.phase === "pre-planning" || state.phase === "needs-discussion") &&
|
|
176
|
+
shouldUseWorktreeIsolation() &&
|
|
177
|
+
!detectWorktreeName(base) &&
|
|
178
|
+
!base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)) {
|
|
179
|
+
const milestoneBranch = `milestone/${state.activeMilestone.id}`;
|
|
180
|
+
const { nativeBranchExists } = await import("./native-git-bridge.js");
|
|
181
|
+
hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
|
|
182
|
+
if (hasSurvivorBranch) {
|
|
183
|
+
ctx.ui.notify(`Found prior session branch ${milestoneBranch}. Resuming.`, "info");
|
|
196
184
|
}
|
|
197
185
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
202
|
-
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
203
|
-
if (!hasContext) {
|
|
186
|
+
if (!hasSurvivorBranch) {
|
|
187
|
+
// No active work — start a new milestone via discuss flow
|
|
188
|
+
if (!state.activeMilestone || state.phase === "complete") {
|
|
204
189
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
205
190
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
206
191
|
invalidateAllCaches();
|
|
207
192
|
const postState = await deriveState(base);
|
|
208
|
-
if (postState.activeMilestone &&
|
|
193
|
+
if (postState.activeMilestone &&
|
|
194
|
+
postState.phase !== "complete" &&
|
|
195
|
+
postState.phase !== "pre-planning") {
|
|
209
196
|
state = postState;
|
|
210
197
|
}
|
|
198
|
+
else if (postState.activeMilestone &&
|
|
199
|
+
postState.phase === "pre-planning") {
|
|
200
|
+
const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
|
|
201
|
+
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
202
|
+
if (hasContext) {
|
|
203
|
+
state = postState;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
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");
|
|
207
|
+
return releaseLockAndReturn();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
211
210
|
else {
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
return releaseLockAndReturn();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Active milestone exists but has no roadmap
|
|
215
|
+
if (state.phase === "pre-planning") {
|
|
216
|
+
const mid = state.activeMilestone.id;
|
|
217
|
+
const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
218
|
+
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
219
|
+
if (!hasContext) {
|
|
220
|
+
const { showSmartEntry } = await import("./guided-flow.js");
|
|
221
|
+
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
222
|
+
invalidateAllCaches();
|
|
223
|
+
const postState = await deriveState(base);
|
|
224
|
+
if (postState.activeMilestone && postState.phase !== "pre-planning") {
|
|
225
|
+
state = postState;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
ctx.ui.notify("Discussion completed but milestone context is still missing. Run /gsd to try again.", "warning");
|
|
229
|
+
return releaseLockAndReturn();
|
|
230
|
+
}
|
|
214
231
|
}
|
|
215
232
|
}
|
|
216
233
|
}
|
|
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);
|
|
234
|
+
// Unreachable safety check
|
|
235
|
+
if (!state.activeMilestone) {
|
|
236
|
+
const { showSmartEntry } = await import("./guided-flow.js");
|
|
237
|
+
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
238
|
+
return releaseLockAndReturn();
|
|
254
239
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
240
|
+
// ── Initialize session state ──
|
|
241
|
+
s.active = true;
|
|
242
|
+
s.stepMode = requestedStepMode;
|
|
243
|
+
s.verbose = verboseMode;
|
|
244
|
+
s.cmdCtx = ctx;
|
|
245
|
+
s.basePath = base;
|
|
246
|
+
s.unitDispatchCount.clear();
|
|
247
|
+
s.unitRecoveryCount.clear();
|
|
248
|
+
s.lastBudgetAlertLevel = 0;
|
|
249
|
+
s.unitLifetimeDispatches.clear();
|
|
250
|
+
resetHookState();
|
|
251
|
+
restoreHookState(base);
|
|
252
|
+
resetProactiveHealing();
|
|
253
|
+
s.autoStartTime = Date.now();
|
|
254
|
+
s.resourceVersionOnStart = readResourceVersion();
|
|
255
|
+
s.completedUnits = [];
|
|
256
|
+
s.pendingQuickTasks = [];
|
|
257
|
+
s.currentUnit = null;
|
|
258
|
+
s.currentMilestoneId = state.activeMilestone?.id ?? null;
|
|
259
|
+
s.originalModelId = ctx.model?.id ?? null;
|
|
260
|
+
s.originalModelProvider = ctx.model?.provider ?? null;
|
|
261
|
+
// Register SIGTERM handler
|
|
262
|
+
registerSigtermHandler(base);
|
|
263
|
+
// Capture integration branch
|
|
264
|
+
if (s.currentMilestoneId) {
|
|
265
|
+
if (getIsolationMode() !== "none") {
|
|
266
|
+
captureIntegrationBranch(base, s.currentMilestoneId, { commitDocs });
|
|
267
267
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
setActiveMilestoneId(base, s.currentMilestoneId);
|
|
269
|
+
}
|
|
270
|
+
// ── Auto-worktree setup ──
|
|
271
|
+
s.originalBasePath = base;
|
|
272
|
+
const isUnderGsdWorktrees = (p) => {
|
|
273
|
+
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
274
|
+
if (p.includes(marker))
|
|
275
|
+
return true;
|
|
276
|
+
const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
|
|
277
|
+
return p.endsWith(worktreesSuffix);
|
|
278
|
+
};
|
|
279
|
+
if (s.currentMilestoneId &&
|
|
280
|
+
shouldUseWorktreeIsolation() &&
|
|
281
|
+
!detectWorktreeName(base) &&
|
|
282
|
+
!isUnderGsdWorktrees(base)) {
|
|
283
|
+
buildResolver().enterMilestone(s.currentMilestoneId, {
|
|
284
|
+
notify: ctx.ui.notify.bind(ctx.ui),
|
|
285
|
+
});
|
|
286
|
+
if (s.basePath !== base) {
|
|
287
|
+
// Successfully entered worktree — re-register SIGTERM handler at original base
|
|
288
|
+
registerSigtermHandler(s.originalBasePath);
|
|
273
289
|
}
|
|
274
|
-
registerSigtermHandler(s.originalBasePath);
|
|
275
290
|
}
|
|
276
|
-
|
|
277
|
-
|
|
291
|
+
// ── DB lifecycle ──
|
|
292
|
+
const gsdDbPath = join(s.basePath, ".gsd", "gsd.db");
|
|
293
|
+
const gsdDirPath = join(s.basePath, ".gsd");
|
|
294
|
+
if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
|
|
295
|
+
const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
|
|
296
|
+
const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
|
|
297
|
+
const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
|
|
298
|
+
if (hasDecisions || hasRequirements || hasMilestones) {
|
|
299
|
+
try {
|
|
300
|
+
const { openDatabase: openDb } = await import("./gsd-db.js");
|
|
301
|
+
const { migrateFromMarkdown } = await import("./md-importer.js");
|
|
302
|
+
openDb(gsdDbPath);
|
|
303
|
+
migrateFromMarkdown(s.basePath);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
process.stderr.write(`gsd-migrate: auto-migration failed: ${err.message}\n`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
278
309
|
}
|
|
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) {
|
|
310
|
+
if (existsSync(gsdDbPath) && !isDbAvailable()) {
|
|
288
311
|
try {
|
|
289
312
|
const { openDatabase: openDb } = await import("./gsd-db.js");
|
|
290
|
-
const { migrateFromMarkdown } = await import("./md-importer.js");
|
|
291
313
|
openDb(gsdDbPath);
|
|
292
|
-
migrateFromMarkdown(s.basePath);
|
|
293
314
|
}
|
|
294
315
|
catch (err) {
|
|
295
|
-
process.stderr.write(`gsd-
|
|
316
|
+
process.stderr.write(`gsd-db: failed to open existing database: ${err.message}\n`);
|
|
296
317
|
}
|
|
297
318
|
}
|
|
298
|
-
|
|
299
|
-
|
|
319
|
+
// Initialize metrics
|
|
320
|
+
initMetrics(s.basePath);
|
|
321
|
+
// Initialize routing history
|
|
322
|
+
initRoutingHistory(s.basePath);
|
|
323
|
+
// Capture session's model at auto-mode start (#650)
|
|
324
|
+
const currentModel = ctx.model;
|
|
325
|
+
if (currentModel) {
|
|
326
|
+
s.autoModeStartModel = {
|
|
327
|
+
provider: currentModel.provider,
|
|
328
|
+
id: currentModel.id,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// Snapshot installed skills
|
|
332
|
+
if (resolveSkillDiscoveryMode() !== "off") {
|
|
333
|
+
snapshotSkills();
|
|
334
|
+
}
|
|
335
|
+
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
336
|
+
ctx.ui.setFooter(hideFooter);
|
|
337
|
+
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
|
338
|
+
const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
|
|
339
|
+
const scopeMsg = pendingCount > 1
|
|
340
|
+
? `Will loop through ${pendingCount} milestones.`
|
|
341
|
+
: "Will loop until milestone complete.";
|
|
342
|
+
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
343
|
+
updateSessionLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
|
|
344
|
+
writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
|
|
345
|
+
// Secrets collection gate
|
|
346
|
+
const mid = state.activeMilestone.id;
|
|
300
347
|
try {
|
|
301
|
-
const
|
|
302
|
-
|
|
348
|
+
const manifestStatus = await getManifestStatus(base, mid);
|
|
349
|
+
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
350
|
+
const result = await collectSecretsFromManifest(base, mid, ctx);
|
|
351
|
+
if (result &&
|
|
352
|
+
result.applied &&
|
|
353
|
+
result.skipped &&
|
|
354
|
+
result.existingSkipped) {
|
|
355
|
+
ctx.ui.notify(`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, "info");
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
303
361
|
}
|
|
304
362
|
catch (err) {
|
|
305
|
-
|
|
306
|
-
}
|
|
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;
|
|
363
|
+
ctx.ui.notify(`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`, "warning");
|
|
347
364
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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");
|
|
365
|
+
// Self-heal: remove stale .git/index.lock
|
|
366
|
+
try {
|
|
367
|
+
const gitLockFile = join(base, ".git", "index.lock");
|
|
368
|
+
if (existsSync(gitLockFile)) {
|
|
369
|
+
const lockAge = Date.now() - statSync(gitLockFile).mtimeMs;
|
|
370
|
+
if (lockAge > 60_000) {
|
|
371
|
+
unlinkSync(gitLockFile);
|
|
372
|
+
ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
|
|
373
|
+
}
|
|
362
374
|
}
|
|
363
375
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
376
|
+
catch (e) {
|
|
377
|
+
debugLog("git-lock-cleanup-failed", {
|
|
378
|
+
error: e instanceof Error ? e.message : String(e),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
// Pre-flight: validate milestone queue
|
|
382
|
+
try {
|
|
383
|
+
const msDir = join(base, ".gsd", "milestones");
|
|
384
|
+
if (existsSync(msDir)) {
|
|
385
|
+
const milestoneIds = readdirSync(msDir, { withFileTypes: true })
|
|
386
|
+
.filter((d) => d.isDirectory() && /^M\d{3}/.test(d.name))
|
|
387
|
+
.map((d) => d.name.match(/^(M\d{3})/)?.[1] ?? d.name);
|
|
388
|
+
if (milestoneIds.length > 1) {
|
|
389
|
+
const issues = [];
|
|
390
|
+
for (const id of milestoneIds) {
|
|
391
|
+
const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
|
|
392
|
+
if (draft)
|
|
393
|
+
issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
|
|
394
|
+
}
|
|
395
|
+
if (issues.length > 0) {
|
|
396
|
+
ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued.\n${issues.map((i) => ` ⚠ ${i}`).join("\n")}`, "warning");
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued. All have full context.`, "info");
|
|
400
|
+
}
|
|
387
401
|
}
|
|
388
402
|
}
|
|
389
403
|
}
|
|
404
|
+
catch {
|
|
405
|
+
/* non-fatal */
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
releaseSessionLock(base);
|
|
411
|
+
clearLock(base);
|
|
412
|
+
throw err;
|
|
390
413
|
}
|
|
391
|
-
catch { /* non-fatal */ }
|
|
392
|
-
return true;
|
|
393
414
|
}
|