gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -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 { isInsideWorktree, ensureGsdSymlink } from "./repo-identity.js";
15
- import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
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 { createGitService } from "./git-service.js";
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 { createAutoWorktree, enterAutoWorktree, getAutoWorktreePath, } from "./auto-worktree.js";
29
- import { readResourceVersion } from "./resource-version.js";
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 { getErrorMessage } from "./error-utils.js";
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(`${lockResult.reason}\nStop it with \`kill ${lockResult.existingPid ?? "the other process"}\` before starting a new session.`, "error");
49
+ ctx.ui.notify(lockResult.reason, "error");
59
50
  return false;
60
51
  }
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 });
52
+ function releaseLockAndReturn() {
53
+ releaseSessionLock(base);
54
+ clearLock(base);
55
+ return false;
84
56
  }
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");
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
- 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");
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
- ctx.ui.notify(`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`, "warning");
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
- 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) });
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
- 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");
157
+ catch (e) {
158
+ debugLog("stale-unit-dir-cleanup-failed", {
159
+ error: e instanceof Error ? e.message : String(e),
160
+ });
171
161
  }
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;
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
- 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;
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
- // 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) {
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 && postState.phase !== "pre-planning") {
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
- ctx.ui.notify("Discussion completed but milestone context is still missing. Run /gsd to try again.", "warning");
213
- return false;
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
- // 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);
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
- 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");
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
- 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");
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
- catch (err) {
277
- ctx.ui.notify(`Auto-worktree setup failed: ${getErrorMessage(err)}. Continuing in project root.`, "warning");
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-migrate: auto-migration failed: ${err.message}\n`);
316
+ process.stderr.write(`gsd-db: failed to open existing database: ${err.message}\n`);
296
317
  }
297
318
  }
298
- }
299
- if (existsSync(gsdDbPath) && !isDbAvailable()) {
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 { openDatabase: openDb } = await import("./gsd-db.js");
302
- openDb(gsdDbPath);
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
- process.stderr.write(`gsd-db: failed to open existing database: ${err.message}\n`);
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
- 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");
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
- 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");
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
  }