gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445

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 (155) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +7 -2
  3. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
  5. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
  7. package/dist/resources/extensions/gsd/auto.js +62 -1
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  9. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
  10. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  11. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  12. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  13. package/dist/resources/extensions/gsd/gsd-db.js +194 -0
  14. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  15. package/dist/resources/extensions/gsd/guided-flow.js +117 -25
  16. package/dist/resources/extensions/gsd/metrics.js +287 -1
  17. package/dist/resources/extensions/gsd/paths.js +79 -8
  18. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  20. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  23. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  24. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  25. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  26. package/dist/resources/extensions/gsd/workspace.js +59 -0
  27. package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
  28. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  29. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  30. package/dist/web/standalone/.next/BUILD_ID +1 -1
  31. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  32. package/dist/web/standalone/.next/build-manifest.json +2 -2
  33. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  34. package/dist/web/standalone/.next/required-server-files.json +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.html +1 -1
  52. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/README.md +2 -11
  66. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  67. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  68. package/packages/mcp-server/dist/remote-questions.js +28 -0
  69. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  70. package/packages/mcp-server/dist/server.d.ts +28 -0
  71. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  72. package/packages/mcp-server/dist/server.js +94 -4
  73. package/packages/mcp-server/dist/server.js.map +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  76. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  77. package/packages/mcp-server/src/remote-questions.ts +35 -0
  78. package/packages/mcp-server/src/server.ts +129 -6
  79. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  81. package/src/resources/extensions/gsd/auto/phases.ts +8 -2
  82. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  83. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
  84. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
  85. package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
  86. package/src/resources/extensions/gsd/auto.ts +79 -1
  87. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  88. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
  89. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  90. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  91. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  92. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  93. package/src/resources/extensions/gsd/gsd-db.ts +184 -0
  94. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  95. package/src/resources/extensions/gsd/guided-flow.ts +154 -25
  96. package/src/resources/extensions/gsd/metrics.ts +321 -1
  97. package/src/resources/extensions/gsd/paths.ts +67 -8
  98. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  99. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  100. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  101. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  102. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  103. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  104. package/src/resources/extensions/gsd/templates/project.md +10 -0
  105. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  106. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  107. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  108. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  109. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  110. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  111. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  112. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  113. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  114. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  115. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  116. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  117. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  118. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  119. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  120. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  121. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
  123. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  124. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  125. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  126. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  127. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  128. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  129. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  130. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  131. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  132. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  133. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  134. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  135. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
  136. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
  137. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  138. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
  139. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  140. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
  141. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  142. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  143. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  144. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  145. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  146. package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
  147. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  148. package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
  149. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  150. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  151. package/src/resources/extensions/gsd/workspace.ts +95 -0
  152. package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
  153. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  154. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
  155. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
@@ -1156,6 +1156,193 @@ let _exitHandlerRegistered = false;
1156
1156
  let _dbOpenAttempted = false;
1157
1157
  let _lastDbError = null;
1158
1158
  let _lastDbPhase = null;
1159
+ /**
1160
+ * Identity key of the workspace whose connection is currently active
1161
+ * (currentDb). Set by openDatabaseByWorkspace(); null when the active
1162
+ * connection was opened via the legacy openDatabase(path) path.
1163
+ */
1164
+ let _currentIdentityKey = null;
1165
+ /**
1166
+ * Workspace-scoped connection cache.
1167
+ * Key: GsdWorkspace.identityKey (realpath-normalized project root).
1168
+ * Value: the DB path and open adapter for that workspace.
1169
+ *
1170
+ * Sibling worktrees of the same project share the same identityKey (set by
1171
+ * createWorkspace) and therefore reuse the same cached connection, preserving
1172
+ * shared-WAL semantics. Different projects get distinct cache entries.
1173
+ *
1174
+ * NOTE: Only one connection is "active" at a time (currentDb/currentPath).
1175
+ * The cache allows fast re-activation of a previously opened connection when
1176
+ * callers switch between known workspaces via openDatabaseByWorkspace().
1177
+ */
1178
+ const _dbCache = new Map();
1179
+ /** Test helper: expose the internal cache for inspection. Not for production use. */
1180
+ export function _getDbCache() {
1181
+ return _dbCache;
1182
+ }
1183
+ /**
1184
+ * Close and evict every entry in the workspace connection cache, then call
1185
+ * closeDatabase() to close the active connection.
1186
+ *
1187
+ * Use this for test teardown or process-shutdown paths where every open
1188
+ * connection must be flushed. Normal callers should use closeDatabase() or
1189
+ * closeDatabaseByWorkspace() instead.
1190
+ */
1191
+ export function closeAllDatabases() {
1192
+ // Close all non-active cached connections first.
1193
+ for (const [key, entry] of _dbCache) {
1194
+ if (entry.db === currentDb)
1195
+ continue; // handled by closeDatabase() below
1196
+ _dbCache.delete(key);
1197
+ try {
1198
+ entry.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
1199
+ }
1200
+ catch { /* best-effort */ }
1201
+ try {
1202
+ entry.db.exec("PRAGMA incremental_vacuum(64)");
1203
+ }
1204
+ catch { /* best-effort */ }
1205
+ try {
1206
+ entry.db.close();
1207
+ }
1208
+ catch { /* best-effort */ }
1209
+ }
1210
+ closeDatabase();
1211
+ }
1212
+ /**
1213
+ * Open (or reuse) the database connection scoped to the given workspace.
1214
+ *
1215
+ * Uses workspace.identityKey as the cache key, so sibling worktrees of the
1216
+ * same project resolve to the same connection. On a cache hit the existing
1217
+ * adapter is reactivated as the current connection without re-opening the
1218
+ * file. On a cache miss, delegates to openDatabase() for the full
1219
+ * open + schema-init + migration flow, then caches the result.
1220
+ *
1221
+ * When switching to a different workspace, the previously active connection
1222
+ * is preserved in the cache (not closed), so callers can switch back to it
1223
+ * cheaply via a subsequent openDatabaseByWorkspace() call.
1224
+ *
1225
+ * @param workspace A GsdWorkspace created by createWorkspace().
1226
+ * @returns true if the connection is open and ready, false otherwise.
1227
+ */
1228
+ export function openDatabaseByWorkspace(workspace) {
1229
+ const key = workspace.identityKey;
1230
+ const dbPath = workspace.contract.projectDb;
1231
+ const cached = _dbCache.get(key);
1232
+ if (cached) {
1233
+ // Reactivate the cached connection as the current singleton.
1234
+ currentDb = cached.db;
1235
+ currentPath = cached.dbPath;
1236
+ currentPid = process.pid;
1237
+ _dbOpenAttempted = true;
1238
+ _currentIdentityKey = key;
1239
+ return true;
1240
+ }
1241
+ // Cache miss — need to open a new connection.
1242
+ //
1243
+ // If there is a currently active workspace connection, stash it in the
1244
+ // cache under its identity key before calling openDatabase(), because
1245
+ // openDatabase() will call closeDatabase() when the path changes (which
1246
+ // would destroy the existing adapter). By nulling out currentDb first,
1247
+ // we prevent openDatabase() from closing the live adapter.
1248
+ let oldDb = null;
1249
+ let oldPath = null;
1250
+ let oldPid = 0;
1251
+ let oldKey = null;
1252
+ if (currentDb !== null && _currentIdentityKey !== null) {
1253
+ // Snapshot the old globals so we can restore them on failure.
1254
+ oldDb = currentDb;
1255
+ oldPath = currentPath;
1256
+ oldPid = currentPid;
1257
+ oldKey = _currentIdentityKey;
1258
+ // Save the current connection so it stays alive in the cache.
1259
+ _dbCache.set(_currentIdentityKey, {
1260
+ dbPath: currentPath,
1261
+ db: currentDb,
1262
+ });
1263
+ // Detach from globals so openDatabase() opens fresh without closing it.
1264
+ currentDb = null;
1265
+ currentPath = null;
1266
+ currentPid = 0;
1267
+ _currentIdentityKey = null;
1268
+ }
1269
+ // Run the full open/schema/migration flow for the new workspace.
1270
+ // openDatabase() can throw on corrupt DB or permission error — catch so we
1271
+ // can restore the previous connection rather than leaving globals null.
1272
+ let opened;
1273
+ try {
1274
+ opened = openDatabase(dbPath);
1275
+ }
1276
+ catch (err) {
1277
+ // Failed to open the new DB. Restore the previous workspace connection so
1278
+ // the caller's workspace remains active (it is still safe in _dbCache).
1279
+ if (oldDb !== null) {
1280
+ currentDb = oldDb;
1281
+ currentPath = oldPath;
1282
+ currentPid = oldPid;
1283
+ _currentIdentityKey = oldKey;
1284
+ }
1285
+ throw err;
1286
+ }
1287
+ if (opened && currentDb) {
1288
+ _dbCache.set(key, { dbPath, db: currentDb });
1289
+ _currentIdentityKey = key;
1290
+ }
1291
+ else if (!opened && oldDb !== null) {
1292
+ // Restore the previous connection so the caller's workspace remains active.
1293
+ // The failed attempt left no live adapter, so the globals stayed null.
1294
+ currentDb = oldDb;
1295
+ currentPath = oldPath;
1296
+ currentPid = oldPid;
1297
+ _currentIdentityKey = oldKey;
1298
+ }
1299
+ return opened;
1300
+ }
1301
+ /**
1302
+ * Open (or reuse) the database connection scoped to the workspace in a
1303
+ * MilestoneScope. Thin delegation to openDatabaseByWorkspace().
1304
+ */
1305
+ export function openDatabaseByScope(scope) {
1306
+ return openDatabaseByWorkspace(scope.workspace);
1307
+ }
1308
+ /**
1309
+ * Close the database connection for the given workspace and remove it from
1310
+ * the cache. If the workspace's connection is currently active (currentDb),
1311
+ * performs a full closeDatabase() including WAL checkpoint. Otherwise only
1312
+ * removes the cache entry (the adapter was already replaced by a later open).
1313
+ */
1314
+ export function closeDatabaseByWorkspace(workspace) {
1315
+ const key = workspace.identityKey;
1316
+ const cached = _dbCache.get(key);
1317
+ if (!cached)
1318
+ return;
1319
+ _dbCache.delete(key);
1320
+ if (currentDb === cached.db) {
1321
+ // This workspace's connection is the active one — full close.
1322
+ closeDatabase();
1323
+ }
1324
+ else {
1325
+ // Connection was displaced by a later open; close the adapter directly.
1326
+ try {
1327
+ cached.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
1328
+ }
1329
+ catch (e) {
1330
+ logWarning("db", `WAL checkpoint (byWorkspace) failed: ${e.message}`);
1331
+ }
1332
+ try {
1333
+ cached.db.exec("PRAGMA incremental_vacuum(64)");
1334
+ }
1335
+ catch (e) {
1336
+ logWarning("db", `incremental vacuum (byWorkspace) failed: ${e.message}`);
1337
+ }
1338
+ try {
1339
+ cached.db.close();
1340
+ }
1341
+ catch (e) {
1342
+ logWarning("db", `database close (byWorkspace) failed: ${e.message}`);
1343
+ }
1344
+ }
1345
+ }
1159
1346
  export function getDbProvider() {
1160
1347
  loadProvider();
1161
1348
  return providerName;
@@ -1300,6 +1487,13 @@ export function closeDatabase() {
1300
1487
  catch (e) {
1301
1488
  logWarning("db", `database close failed: ${e.message}`);
1302
1489
  }
1490
+ // If this connection was workspace-tracked, evict it from the cache so
1491
+ // subsequent openDatabaseByWorkspace() calls re-open rather than reactivate
1492
+ // a closed adapter.
1493
+ if (_currentIdentityKey !== null) {
1494
+ _dbCache.delete(_currentIdentityKey);
1495
+ _currentIdentityKey = null;
1496
+ }
1303
1497
  currentDb = null;
1304
1498
  currentPath = null;
1305
1499
  currentPid = 0;
@@ -150,7 +150,7 @@ export async function showQueueAdd(ctx, pi, basePath, state) {
150
150
  ].join(" ");
151
151
  // ── Dispatch the queue prompt ───────────────────────────────────────
152
152
  // Activate the queue phase so the write-gate applies to CONTEXT.md writes
153
- setQueuePhaseActive(true);
153
+ setQueuePhaseActive(true, basePath);
154
154
  const queueInlinedTemplates = inlineTemplate("context", "Context");
155
155
  const prompt = loadPrompt("queue", {
156
156
  preamble,
@@ -7,11 +7,11 @@
7
7
  */
8
8
  import { showNextAction } from "../shared/tui.js";
9
9
  import { loadFile, saveFile } from "./files.js";
10
- import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
10
+ import { isDbAvailable, getMilestone, getMilestoneSlices } from "./gsd-db.js";
11
11
  import { parseRoadmapSlices } from "./roadmap-slices.js";
12
12
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
13
13
  import { buildCompleteSlicePrompt, buildDiscussMilestonePrompt, buildExecuteTaskPrompt, buildPlanMilestonePrompt, buildPlanSlicePrompt, buildSkillActivationBlock, } from "./auto-prompts.js";
14
- import { deriveState } from "./state.js";
14
+ import { deriveState, isGhostMilestone } from "./state.js";
15
15
  import { invalidateAllCaches } from "./cache.js";
16
16
  import { startAutoDetached } from "./auto.js";
17
17
  import { clearLock } from "./crash-recovery.js";
@@ -19,7 +19,7 @@ import { assessInterruptedSession, formatInterruptedSessionRunningMessage, forma
19
19
  import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
20
20
  import { resolveExpectedArtifactPath } from "./auto.js";
21
21
  import { gsdHome } from "./gsd-home.js";
22
- import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, } from "./paths.js";
22
+ import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, clearPathCache, } from "./paths.js";
23
23
  import { join } from "node:path";
24
24
  import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
25
25
  import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
@@ -43,10 +43,39 @@ import { DISCUSS_TOOLS_ALLOWLIST } from "./constants.js";
43
43
  import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForGuidedUnit, supportsStructuredQuestions, } from "./workflow-mcp.js";
44
44
  import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from "./preparation.js";
45
45
  import { verifyExpectedArtifact } from "./auto-recovery.js";
46
+ import { createWorkspace, scopeMilestone } from "./workspace.js";
46
47
  // ─── Re-exports (preserve public API for existing importers) ────────────────
47
48
  export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, reserveMilestoneId, claimReservedId, getReservedMilestoneIds, clearReservedMilestoneIds, } from "./milestone-ids.js";
48
49
  export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
49
50
  import { logWarning } from "./workflow-logger.js";
51
+ // ─── Scope-based validator wrappers ──────────────────────────────────────────
52
+ // These thin wrappers accept a MilestoneScope so callers that already hold a
53
+ // pinned scope never have to re-derive (basePath, milestoneId) separately.
54
+ // The underlying implementations in auto-recovery.ts / auto-artifact-paths.ts /
55
+ // state.ts are unchanged — only the call surface in guided-flow.ts is migrated.
56
+ /**
57
+ * Scope-based overload of verifyExpectedArtifact.
58
+ * Uses scope.workspace.projectRoot as the authoritative base path, making
59
+ * the check immune to cwd-drift and worktree-path divergence.
60
+ */
61
+ export function verifyExpectedArtifactForScope(scope, unitType, unitId) {
62
+ return verifyExpectedArtifact(unitType, unitId, scope.workspace.projectRoot);
63
+ }
64
+ /**
65
+ * Scope-based overload of resolveExpectedArtifactPath.
66
+ * Returns the canonical absolute path (or null) using the scope's projectRoot.
67
+ */
68
+ export function resolveExpectedArtifactPathForScope(scope, unitType, unitId) {
69
+ return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
70
+ }
71
+ /**
72
+ * Scope-based overload of isGhostMilestone.
73
+ * Binds basePath and milestoneId from the scope, ensuring path resolution
74
+ * uses the canonical project root regardless of the cwd at call time.
75
+ */
76
+ export function isGhostMilestoneByScope(scope) {
77
+ return isGhostMilestone(scope.workspace.projectRoot, scope.milestoneId);
78
+ }
50
79
  function needsPlanV2Gate(state) {
51
80
  return state.phase === "executing"
52
81
  || state.phase === "summarizing"
@@ -77,6 +106,10 @@ function buildDocsCommitInstruction(_message) {
77
106
  // #4573: cap for how many times we nudge the LLM after a premature ready
78
107
  // phrase before giving up and asking the user to re-run /gsd.
79
108
  const MAX_READY_REJECTS = 2;
109
+ // H1 (#5012): cap for Gate 1b plan-blocked recovery hints. After this many
110
+ // consecutive recovery attempts the loop is stopped and the user is directed
111
+ // to investigate manually.
112
+ const MAX_PLAN_BLOCKED_RECOVERIES = 3;
80
113
  // #4573: matches the canonical ready phrase the discuss prompt asks the LLM
81
114
  // to emit. Accepts any M-prefixed milestone ID (three digits + optional
82
115
  // suffix) with optional trailing punctuation.
@@ -110,9 +143,9 @@ This stage is running inside the foreground \`/gsd new-project --deep\` intervie
110
143
  /**
111
144
  * Backward-compat bridge: returns a mutable reference to the entry matching
112
145
  * basePath, or the sole entry when only one session exists.
113
- * Internal use onlyexternal code should use the Map directly.
146
+ * Exported for testinginternal use only in production code.
114
147
  */
115
- function _getPendingAutoStart(basePath) {
148
+ export function _getPendingAutoStart(basePath) {
116
149
  if (basePath)
117
150
  return pendingAutoStartMap.get(basePath) ?? null;
118
151
  if (pendingAutoStartMap.size === 1)
@@ -157,7 +190,9 @@ function clearEmptyLegacyDeepSetupPseudoMilestones(basePath, entries) {
157
190
  * Exported for testing (#2985).
158
191
  */
159
192
  export function setPendingAutoStart(basePath, entry) {
160
- pendingAutoStartMap.set(basePath, { createdAt: Date.now(), ...entry });
193
+ const ws = createWorkspace(entry.basePath);
194
+ const scope = scopeMilestone(ws, entry.milestoneId);
195
+ pendingAutoStartMap.set(basePath, { createdAt: Date.now(), planBlockedRecoveryCount: 0, ...entry, scope });
161
196
  }
162
197
  /**
163
198
  * Clear pending auto-start state.
@@ -249,6 +284,10 @@ export async function checkDeepProjectSetupAfterTurn(_event, ctx, basePath) {
249
284
  if (!entry)
250
285
  return false;
251
286
  if (entry.currentUnitType && entry.currentUnitId) {
287
+ // TODO(C-future): PendingDeepProjectSetupEntry does not carry a MilestoneScope
288
+ // because deep-project-setup units span non-milestone unit types (discuss-project,
289
+ // discuss-requirements, etc.). Migrate to verifyExpectedArtifactForScope once
290
+ // PendingDeepProjectSetupEntry is extended with a scope field.
252
291
  const artifactReady = verifyExpectedArtifact(entry.currentUnitType, entry.currentUnitId, entry.basePath);
253
292
  if (!artifactReady) {
254
293
  return false;
@@ -320,17 +359,61 @@ export function checkAutoStartAfterDiscuss() {
320
359
  const { ctx, pi, basePath, milestoneId, step } = entry;
321
360
  // Gate 1: Primary milestone must have CONTEXT.md or ROADMAP.md
322
361
  // The "discuss" path creates CONTEXT.md; the "plan" path creates ROADMAP.md.
323
- const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
324
- const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
362
+ // Use pinned scope (immune to cwd-drift) for existence checks.
363
+ const contextFilePath = entry.scope.contextFile();
364
+ const roadmapFilePath = entry.scope.roadmapFile();
365
+ const contextFile = existsSync(contextFilePath) ? contextFilePath : null;
366
+ const roadmapFile = existsSync(roadmapFilePath) ? roadmapFilePath : null;
325
367
  if (!contextFile && !roadmapFile)
326
368
  return false; // neither artifact yet — keep waiting
369
+ // Gate 1b: Discriminate plan-blocked from discuss-incomplete when the DB row is queued.
370
+ // If the DB is available and the row is still "queued" but CONTEXT.md already exists on
371
+ // disk, the discuss phase completed but gsd_plan_milestone was hard-blocked by the
372
+ // depth-verification gate. Emit a recovery hint so the next agent turn can retry
373
+ // gsd_plan_milestone, then return false (keep blocking auto-start).
374
+ // If CONTEXT.md does not exist (discuss-incomplete), Gate 1 already blocked above.
375
+ if (isDbAvailable()) {
376
+ const dbRow = getMilestone(milestoneId);
377
+ if (dbRow?.status === "queued" && contextFile) {
378
+ if (entry.planBlockedRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
379
+ // H1: recovery loop cap reached — stop triggering new turns, escalate to user.
380
+ logWarning("guided", `Gate 1b: milestone ${milestoneId} plan-blocked recovery limit reached ` +
381
+ `(${entry.planBlockedRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`);
382
+ ctx.ui.notify(`Milestone ${milestoneId} plan_milestone has been blocked ${entry.planBlockedRecoveryCount} times. ` +
383
+ `Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`, "error");
384
+ return false;
385
+ }
386
+ logWarning("guided", `Gate 1b: milestone ${milestoneId} queued with CONTEXT.md present — ` +
387
+ `plan_milestone was blocked; emitting recovery hint ` +
388
+ `(attempt ${entry.planBlockedRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`);
389
+ ctx.ui.notify(`Milestone ${milestoneId}: context file exists but milestone is still queued. ` +
390
+ `Retrying gsd_plan_milestone to complete the blocked planning step.`, "warning");
391
+ try {
392
+ pi.sendMessage({
393
+ customType: "gsd-plan-milestone-blocked-recovery",
394
+ content: `Milestone ${milestoneId} has ${contextFile} on disk but its DB row is still ` +
395
+ `"queued". The gsd_plan_milestone tool was previously blocked by the ` +
396
+ `depth-verification gate. Call gsd_plan_milestone now to complete the ` +
397
+ `planning phase.`,
398
+ display: false,
399
+ }, { triggerTurn: true });
400
+ // Increment only after a successful dispatch so transient sendMessage
401
+ // failures do not consume recovery budget.
402
+ entry.planBlockedRecoveryCount += 1;
403
+ }
404
+ catch (e) {
405
+ logWarning("guided", `Gate 1b recovery sendMessage failed: ${e.message}`);
406
+ }
407
+ return false;
408
+ }
409
+ }
327
410
  // Gate 2: STATE.md must exist — written as the last step in the discuss
328
411
  // output phase. This prevents auto-start from firing during Phase 3
329
412
  // (sequential readiness gates for remaining milestones) in multi-milestone
330
413
  // discussions, where M001-CONTEXT.md exists but M002/M003 haven't been
331
414
  // processed yet.
332
- const stateFile = resolveGsdRootFile(basePath, "STATE");
333
- if (!stateFile)
415
+ const stateFilePath = entry.scope.stateFile();
416
+ if (!existsSync(stateFilePath))
334
417
  return false; // discussion not finalized yet
335
418
  // Gate 3: Multi-milestone completeness warning
336
419
  // Parse PROJECT.md for milestone sequence, warn if any are missing context.
@@ -362,7 +445,7 @@ export function checkAutoStartAfterDiscuss() {
362
445
  // The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
363
446
  // When it exists, validate it before auto-starting. Project history alone is
364
447
  // not a reliable signal for the current discussion mode.
365
- const manifestPath = join(gsdRoot(basePath), "DISCUSSION-MANIFEST.json");
448
+ const manifestPath = join(entry.scope.workspace.contract.projectGsd, "DISCUSSION-MANIFEST.json");
366
449
  if (existsSync(manifestPath)) {
367
450
  try {
368
451
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -486,6 +569,12 @@ export function maybeHandleReadyPhraseWithoutFiles(event) {
486
569
  const text = extractAssistantText(lastMsg);
487
570
  if (!READY_PHRASE_RE.test(text))
488
571
  return false;
572
+ // Bust paths.ts cached dir listings before checking for fresh writes. The
573
+ // LLM's Write tool calls do not invalidate paths.ts caches, so a stale
574
+ // listing taken before the milestone dir or its CONTEXT/ROADMAP files
575
+ // existed would falsely report the artifacts as missing and trigger the
576
+ // 3-strike "ready without files" abort even though the writes succeeded.
577
+ clearPathCache();
489
578
  // Gate: artifacts must still be missing — if they exist, the happy path
490
579
  // already fired and we have nothing to do.
491
580
  const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
@@ -890,7 +979,7 @@ export async function showHeadlessMilestoneCreation(ctx, pi, basePath, seedConte
890
979
  // Build and dispatch the headless discuss prompt
891
980
  const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
892
981
  // Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
893
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, createdAt: Date.now() });
982
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId });
894
983
  // Dispatch as discuss-milestone. The LLM writes PROJECT.md, REQUIREMENTS.md,
895
984
  // and CONTEXT.md, then calls gsd_plan_milestone — this is semantically the
896
985
  // discuss path, just non-interactive. Using "plan-milestone" here caused
@@ -1056,13 +1145,13 @@ export async function showDiscuss(ctx, pi, basePath) {
1056
1145
  const seed = draftContent
1057
1146
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
1058
1147
  : basePrompt;
1059
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: mid, step: false, createdAt: Date.now() });
1148
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: mid, step: false });
1060
1149
  await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
1061
1150
  }
1062
1151
  else if (choice === "discuss_fresh") {
1063
1152
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
1064
1153
  const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
1065
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: mid, step: false, createdAt: Date.now() });
1154
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: mid, step: false });
1066
1155
  await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1067
1156
  milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
1068
1157
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
@@ -1075,7 +1164,7 @@ export async function showDiscuss(ctx, pi, basePath) {
1075
1164
  const milestoneIds = findMilestoneIds(basePath);
1076
1165
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1077
1166
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
1078
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: false, createdAt: Date.now() });
1167
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: false });
1079
1168
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1080
1169
  }
1081
1170
  return;
@@ -1329,6 +1418,9 @@ function selfHealRuntimeRecords(basePath, ctx) {
1329
1418
  for (const record of records) {
1330
1419
  const { unitType, unitId, phase } = record;
1331
1420
  // Clear records whose expected artifact already exists (completed but not cleaned up)
1421
+ // TODO(C-future): selfHealRuntimeRecords iterates across all unit types (not just milestone
1422
+ // units), so it cannot be converted to resolveExpectedArtifactPathForScope without
1423
+ // first establishing a per-record scope. Migrate once unit runtime records carry scope info.
1332
1424
  const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
1333
1425
  if (artifactPath && existsSync(artifactPath)) {
1334
1426
  clearUnitRuntimeRecord(basePath, unitType, unitId);
@@ -1431,7 +1523,7 @@ async function handleMilestoneActions(ctx, pi, basePath, milestoneId, milestoneT
1431
1523
  const milestoneIds = findMilestoneIds(basePath);
1432
1524
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1433
1525
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
1434
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1526
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1435
1527
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1436
1528
  return true;
1437
1529
  }
@@ -1636,7 +1728,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1636
1728
  const isFirst = milestoneIds.length === 0;
1637
1729
  if (isFirst) {
1638
1730
  // First ever — skip wizard, just ask directly
1639
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1731
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1640
1732
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath), "gsd-run", ctx, "discuss-milestone");
1641
1733
  }
1642
1734
  else {
@@ -1654,7 +1746,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1654
1746
  notYetMessage: "Run /gsd when ready.",
1655
1747
  });
1656
1748
  if (choice === "new_milestone") {
1657
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1749
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1658
1750
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1659
1751
  }
1660
1752
  }
@@ -1663,7 +1755,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1663
1755
  const milestoneId = state.activeMilestone.id;
1664
1756
  const milestoneTitle = state.activeMilestone.title;
1665
1757
  if (planV2GateDecision === "recover-missing-context") {
1666
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
1758
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
1667
1759
  await dispatchWorkflow(pi, await buildDiscussMilestonePrompt(milestoneId, milestoneTitle, basePath, getStructuredQuestionsAvailability(pi, ctx)), "gsd-discuss", ctx, "discuss-milestone");
1668
1760
  return;
1669
1761
  }
@@ -1691,7 +1783,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1691
1783
  const milestoneIds = findMilestoneIds(basePath);
1692
1784
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1693
1785
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
1694
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1786
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1695
1787
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1696
1788
  }
1697
1789
  else if (choice === "status") {
@@ -1738,13 +1830,13 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1738
1830
  const seed = draftContent
1739
1831
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
1740
1832
  : basePrompt;
1741
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
1833
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
1742
1834
  await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
1743
1835
  }
1744
1836
  else if (choice === "discuss_fresh") {
1745
1837
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
1746
1838
  const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
1747
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
1839
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
1748
1840
  await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1749
1841
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
1750
1842
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
@@ -1755,7 +1847,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1755
1847
  const milestoneIds = findMilestoneIds(basePath);
1756
1848
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1757
1849
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
1758
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1850
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1759
1851
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1760
1852
  }
1761
1853
  return;
@@ -1811,7 +1903,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1811
1903
  notYetMessage: "Run /gsd when ready.",
1812
1904
  });
1813
1905
  if (choice === "plan") {
1814
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
1906
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
1815
1907
  await dispatchWorkflow(pi, await buildPlanMilestonePrompt(milestoneId, milestoneTitle, basePath), "gsd-run", ctx, "plan-milestone");
1816
1908
  }
1817
1909
  else if (choice === "discuss") {
@@ -1827,7 +1919,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1827
1919
  const milestoneIds = findMilestoneIds(basePath);
1828
1920
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1829
1921
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
1830
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1922
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1831
1923
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1832
1924
  }
1833
1925
  else if (choice === "discard_milestone") {