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
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@g
10
10
  import type { GSDState } from "./types.js";
11
11
  import { showNextAction } from "../shared/tui.js";
12
12
  import { loadFile, saveFile } from "./files.js";
13
- import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
13
+ import { isDbAvailable, getMilestone, getMilestoneSlices } from "./gsd-db.js";
14
14
  import { parseRoadmapSlices } from "./roadmap-slices.js";
15
15
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
16
16
  import {
@@ -21,7 +21,7 @@ import {
21
21
  buildPlanSlicePrompt,
22
22
  buildSkillActivationBlock,
23
23
  } from "./auto-prompts.js";
24
- import { deriveState } from "./state.js";
24
+ import { deriveState, isGhostMilestone } from "./state.js";
25
25
  import { invalidateAllCaches } from "./cache.js";
26
26
  import { startAutoDetached } from "./auto.js";
27
27
  import { clearLock } from "./crash-recovery.js";
@@ -36,7 +36,7 @@ import { gsdHome } from "./gsd-home.js";
36
36
  import {
37
37
  gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath,
38
38
  resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile,
39
- relMilestoneFile, relSliceFile,
39
+ relMilestoneFile, relSliceFile, clearPathCache,
40
40
  } from "./paths.js";
41
41
  import { join } from "node:path";
42
42
  import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
@@ -69,6 +69,7 @@ import {
69
69
  formatPriorContextBrief,
70
70
  } from "./preparation.js";
71
71
  import { verifyExpectedArtifact } from "./auto-recovery.js";
72
+ import { createWorkspace, scopeMilestone, type MilestoneScope } from "./workspace.js";
72
73
 
73
74
  // ─── Re-exports (preserve public API for existing importers) ────────────────
74
75
  export {
@@ -83,6 +84,46 @@ export {
83
84
  } from "./guided-flow-queue.js";
84
85
  import { logWarning } from "./workflow-logger.js";
85
86
 
87
+ // ─── Scope-based validator wrappers ──────────────────────────────────────────
88
+ // These thin wrappers accept a MilestoneScope so callers that already hold a
89
+ // pinned scope never have to re-derive (basePath, milestoneId) separately.
90
+ // The underlying implementations in auto-recovery.ts / auto-artifact-paths.ts /
91
+ // state.ts are unchanged — only the call surface in guided-flow.ts is migrated.
92
+
93
+ /**
94
+ * Scope-based overload of verifyExpectedArtifact.
95
+ * Uses scope.workspace.projectRoot as the authoritative base path, making
96
+ * the check immune to cwd-drift and worktree-path divergence.
97
+ */
98
+ export function verifyExpectedArtifactForScope(
99
+ scope: MilestoneScope,
100
+ unitType: string,
101
+ unitId: string,
102
+ ): boolean {
103
+ return verifyExpectedArtifact(unitType, unitId, scope.workspace.projectRoot);
104
+ }
105
+
106
+ /**
107
+ * Scope-based overload of resolveExpectedArtifactPath.
108
+ * Returns the canonical absolute path (or null) using the scope's projectRoot.
109
+ */
110
+ export function resolveExpectedArtifactPathForScope(
111
+ scope: MilestoneScope,
112
+ unitType: string,
113
+ unitId: string,
114
+ ): string | null {
115
+ return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
116
+ }
117
+
118
+ /**
119
+ * Scope-based overload of isGhostMilestone.
120
+ * Binds basePath and milestoneId from the scope, ensuring path resolution
121
+ * uses the canonical project root regardless of the cwd at call time.
122
+ */
123
+ export function isGhostMilestoneByScope(scope: MilestoneScope): boolean {
124
+ return isGhostMilestone(scope.workspace.projectRoot, scope.milestoneId);
125
+ }
126
+
86
127
  function needsPlanV2Gate(state: GSDState): boolean {
87
128
  return state.phase === "executing"
88
129
  || state.phase === "summarizing"
@@ -135,6 +176,13 @@ interface PendingAutoStartEntry {
135
176
  // #4573: counter for how many times the LLM emitted the ready phrase
136
177
  // without writing the required artifacts. Cleared on entry delete/recreate.
137
178
  readyRejectCount?: number;
179
+ // C1: scope is pinned at reservation time so path resolution is immune to
180
+ // cwd-drift between discuss and checkAutoStartAfterDiscuss.
181
+ // TODO(C3): basePath becomes redundant once all consumers migrate to scope.
182
+ scope: MilestoneScope;
183
+ // H1: retry counter for Gate 1b plan-blocked recovery. Capped at
184
+ // MAX_PLAN_BLOCKED_RECOVERIES to prevent infinite recovery loops (#5012).
185
+ planBlockedRecoveryCount: number;
138
186
  }
139
187
 
140
188
  interface PendingDeepProjectSetupEntry {
@@ -152,6 +200,11 @@ interface PendingDeepProjectSetupEntry {
152
200
  // phrase before giving up and asking the user to re-run /gsd.
153
201
  const MAX_READY_REJECTS = 2;
154
202
 
203
+ // H1 (#5012): cap for Gate 1b plan-blocked recovery hints. After this many
204
+ // consecutive recovery attempts the loop is stopped and the user is directed
205
+ // to investigate manually.
206
+ const MAX_PLAN_BLOCKED_RECOVERIES = 3;
207
+
155
208
  // #4573: matches the canonical ready phrase the discuss prompt asks the LLM
156
209
  // to emit. Accepts any M-prefixed milestone ID (three digits + optional
157
210
  // suffix) with optional trailing punctuation.
@@ -187,9 +240,9 @@ This stage is running inside the foreground \`/gsd new-project --deep\` intervie
187
240
  /**
188
241
  * Backward-compat bridge: returns a mutable reference to the entry matching
189
242
  * basePath, or the sole entry when only one session exists.
190
- * Internal use onlyexternal code should use the Map directly.
243
+ * Exported for testinginternal use only in production code.
191
244
  */
192
- function _getPendingAutoStart(basePath?: string): PendingAutoStartEntry | null {
245
+ export function _getPendingAutoStart(basePath?: string): PendingAutoStartEntry | null {
193
246
  if (basePath) return pendingAutoStartMap.get(basePath) ?? null;
194
247
  if (pendingAutoStartMap.size === 1) return pendingAutoStartMap.values().next().value!;
195
248
  return null;
@@ -233,7 +286,9 @@ function clearEmptyLegacyDeepSetupPseudoMilestones(basePath: string, entries: st
233
286
  * Exported for testing (#2985).
234
287
  */
235
288
  export function setPendingAutoStart(basePath: string, entry: { basePath: string; milestoneId: string; ctx?: ExtensionCommandContext; pi?: ExtensionAPI; step?: boolean; createdAt?: number }): void {
236
- pendingAutoStartMap.set(basePath, { createdAt: Date.now(), ...entry } as PendingAutoStartEntry);
289
+ const ws = createWorkspace(entry.basePath);
290
+ const scope = scopeMilestone(ws, entry.milestoneId);
291
+ pendingAutoStartMap.set(basePath, { createdAt: Date.now(), planBlockedRecoveryCount: 0, ...entry, scope } as PendingAutoStartEntry);
237
292
  }
238
293
 
239
294
  /**
@@ -343,6 +398,10 @@ export async function checkDeepProjectSetupAfterTurn(
343
398
  if (!entry) return false;
344
399
 
345
400
  if (entry.currentUnitType && entry.currentUnitId) {
401
+ // TODO(C-future): PendingDeepProjectSetupEntry does not carry a MilestoneScope
402
+ // because deep-project-setup units span non-milestone unit types (discuss-project,
403
+ // discuss-requirements, etc.). Migrate to verifyExpectedArtifactForScope once
404
+ // PendingDeepProjectSetupEntry is extended with a scope field.
346
405
  const artifactReady = verifyExpectedArtifact(entry.currentUnitType, entry.currentUnitId, entry.basePath);
347
406
  if (!artifactReady) {
348
407
  return false;
@@ -426,17 +485,77 @@ export function checkAutoStartAfterDiscuss(): boolean {
426
485
 
427
486
  // Gate 1: Primary milestone must have CONTEXT.md or ROADMAP.md
428
487
  // The "discuss" path creates CONTEXT.md; the "plan" path creates ROADMAP.md.
429
- const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
430
- const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
488
+ // Use pinned scope (immune to cwd-drift) for existence checks.
489
+ const contextFilePath = entry.scope.contextFile();
490
+ const roadmapFilePath = entry.scope.roadmapFile();
491
+ const contextFile = existsSync(contextFilePath) ? contextFilePath : null;
492
+ const roadmapFile = existsSync(roadmapFilePath) ? roadmapFilePath : null;
431
493
  if (!contextFile && !roadmapFile) return false; // neither artifact yet — keep waiting
432
494
 
495
+ // Gate 1b: Discriminate plan-blocked from discuss-incomplete when the DB row is queued.
496
+ // If the DB is available and the row is still "queued" but CONTEXT.md already exists on
497
+ // disk, the discuss phase completed but gsd_plan_milestone was hard-blocked by the
498
+ // depth-verification gate. Emit a recovery hint so the next agent turn can retry
499
+ // gsd_plan_milestone, then return false (keep blocking auto-start).
500
+ // If CONTEXT.md does not exist (discuss-incomplete), Gate 1 already blocked above.
501
+ if (isDbAvailable()) {
502
+ const dbRow = getMilestone(milestoneId);
503
+ if (dbRow?.status === "queued" && contextFile) {
504
+ if (entry.planBlockedRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
505
+ // H1: recovery loop cap reached — stop triggering new turns, escalate to user.
506
+ logWarning(
507
+ "guided",
508
+ `Gate 1b: milestone ${milestoneId} plan-blocked recovery limit reached ` +
509
+ `(${entry.planBlockedRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`,
510
+ );
511
+ ctx.ui.notify(
512
+ `Milestone ${milestoneId} plan_milestone has been blocked ${entry.planBlockedRecoveryCount} times. ` +
513
+ `Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`,
514
+ "error",
515
+ );
516
+ return false;
517
+ }
518
+ logWarning(
519
+ "guided",
520
+ `Gate 1b: milestone ${milestoneId} queued with CONTEXT.md present — ` +
521
+ `plan_milestone was blocked; emitting recovery hint ` +
522
+ `(attempt ${entry.planBlockedRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`,
523
+ );
524
+ ctx.ui.notify(
525
+ `Milestone ${milestoneId}: context file exists but milestone is still queued. ` +
526
+ `Retrying gsd_plan_milestone to complete the blocked planning step.`,
527
+ "warning",
528
+ );
529
+ try {
530
+ pi.sendMessage(
531
+ {
532
+ customType: "gsd-plan-milestone-blocked-recovery",
533
+ content:
534
+ `Milestone ${milestoneId} has ${contextFile} on disk but its DB row is still ` +
535
+ `"queued". The gsd_plan_milestone tool was previously blocked by the ` +
536
+ `depth-verification gate. Call gsd_plan_milestone now to complete the ` +
537
+ `planning phase.`,
538
+ display: false,
539
+ },
540
+ { triggerTurn: true },
541
+ );
542
+ // Increment only after a successful dispatch so transient sendMessage
543
+ // failures do not consume recovery budget.
544
+ entry.planBlockedRecoveryCount += 1;
545
+ } catch (e) {
546
+ logWarning("guided", `Gate 1b recovery sendMessage failed: ${(e as Error).message}`);
547
+ }
548
+ return false;
549
+ }
550
+ }
551
+
433
552
  // Gate 2: STATE.md must exist — written as the last step in the discuss
434
553
  // output phase. This prevents auto-start from firing during Phase 3
435
554
  // (sequential readiness gates for remaining milestones) in multi-milestone
436
555
  // discussions, where M001-CONTEXT.md exists but M002/M003 haven't been
437
556
  // processed yet.
438
- const stateFile = resolveGsdRootFile(basePath, "STATE");
439
- if (!stateFile) return false; // discussion not finalized yet
557
+ const stateFilePath = entry.scope.stateFile();
558
+ if (!existsSync(stateFilePath)) return false; // discussion not finalized yet
440
559
 
441
560
  // Gate 3: Multi-milestone completeness warning
442
561
  // Parse PROJECT.md for milestone sequence, warn if any are missing context.
@@ -469,7 +588,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
469
588
  // The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
470
589
  // When it exists, validate it before auto-starting. Project history alone is
471
590
  // not a reliable signal for the current discussion mode.
472
- const manifestPath = join(gsdRoot(basePath), "DISCUSSION-MANIFEST.json");
591
+ const manifestPath = join(entry.scope.workspace.contract.projectGsd, "DISCUSSION-MANIFEST.json");
473
592
  if (existsSync(manifestPath)) {
474
593
  try {
475
594
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -588,6 +707,13 @@ export function maybeHandleReadyPhraseWithoutFiles(event: { messages: any[] }):
588
707
  const text = extractAssistantText(lastMsg);
589
708
  if (!READY_PHRASE_RE.test(text)) return false;
590
709
 
710
+ // Bust paths.ts cached dir listings before checking for fresh writes. The
711
+ // LLM's Write tool calls do not invalidate paths.ts caches, so a stale
712
+ // listing taken before the milestone dir or its CONTEXT/ROADMAP files
713
+ // existed would falsely report the artifacts as missing and trigger the
714
+ // 3-strike "ready without files" abort even though the writes succeeded.
715
+ clearPathCache();
716
+
591
717
  // Gate: artifacts must still be missing — if they exist, the happy path
592
718
  // already fired and we have nothing to do.
593
719
  const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
@@ -1097,7 +1223,7 @@ export async function showHeadlessMilestoneCreation(
1097
1223
  const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
1098
1224
 
1099
1225
  // Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
1100
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, createdAt: Date.now() });
1226
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId });
1101
1227
 
1102
1228
  // Dispatch as discuss-milestone. The LLM writes PROJECT.md, REQUIREMENTS.md,
1103
1229
  // and CONTEXT.md, then calls gsd_plan_milestone — this is semantically the
@@ -1294,12 +1420,12 @@ export async function showDiscuss(
1294
1420
  const seed = draftContent
1295
1421
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
1296
1422
  : basePrompt;
1297
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: mid, step: false, createdAt: Date.now() });
1423
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: mid, step: false });
1298
1424
  await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
1299
1425
  } else if (choice === "discuss_fresh") {
1300
1426
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
1301
1427
  const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
1302
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: mid, step: false, createdAt: Date.now() });
1428
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: mid, step: false });
1303
1429
  await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1304
1430
  milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
1305
1431
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
@@ -1311,7 +1437,7 @@ export async function showDiscuss(
1311
1437
  const milestoneIds = findMilestoneIds(basePath);
1312
1438
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1313
1439
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
1314
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: false, createdAt: Date.now() });
1440
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: false });
1315
1441
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1316
1442
  }
1317
1443
  return;
@@ -1602,6 +1728,9 @@ function selfHealRuntimeRecords(basePath: string, ctx: ExtensionContext): { clea
1602
1728
  for (const record of records) {
1603
1729
  const { unitType, unitId, phase } = record;
1604
1730
  // Clear records whose expected artifact already exists (completed but not cleaned up)
1731
+ // TODO(C-future): selfHealRuntimeRecords iterates across all unit types (not just milestone
1732
+ // units), so it cannot be converted to resolveExpectedArtifactPathForScope without
1733
+ // first establishing a per-record scope. Migrate once unit runtime records carry scope info.
1605
1734
  const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
1606
1735
  if (artifactPath && existsSync(artifactPath)) {
1607
1736
  clearUnitRuntimeRecord(basePath, unitType, unitId);
@@ -1716,7 +1845,7 @@ async function handleMilestoneActions(
1716
1845
  const milestoneIds = findMilestoneIds(basePath);
1717
1846
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1718
1847
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
1719
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1848
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1720
1849
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
1721
1850
  `New milestone ${nextId}.`,
1722
1851
  basePath
@@ -1944,7 +2073,7 @@ export async function showSmartEntry(
1944
2073
 
1945
2074
  if (isFirst) {
1946
2075
  // First ever — skip wizard, just ask directly
1947
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
2076
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1948
2077
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
1949
2078
  `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
1950
2079
  basePath
@@ -1965,7 +2094,7 @@ export async function showSmartEntry(
1965
2094
  });
1966
2095
 
1967
2096
  if (choice === "new_milestone") {
1968
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
2097
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
1969
2098
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
1970
2099
  `New milestone ${nextId}.`,
1971
2100
  basePath
@@ -1979,7 +2108,7 @@ export async function showSmartEntry(
1979
2108
  const milestoneTitle = state.activeMilestone.title;
1980
2109
 
1981
2110
  if (planV2GateDecision === "recover-missing-context") {
1982
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
2111
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
1983
2112
  await dispatchWorkflow(
1984
2113
  pi,
1985
2114
  await buildDiscussMilestonePrompt(
@@ -2021,7 +2150,7 @@ export async function showSmartEntry(
2021
2150
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
2022
2151
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
2023
2152
 
2024
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
2153
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
2025
2154
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
2026
2155
  `New milestone ${nextId}.`,
2027
2156
  basePath
@@ -2073,12 +2202,12 @@ export async function showSmartEntry(
2073
2202
  const seed = draftContent
2074
2203
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
2075
2204
  : basePrompt;
2076
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
2205
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
2077
2206
  await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
2078
2207
  } else if (choice === "discuss_fresh") {
2079
2208
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
2080
2209
  const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
2081
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
2210
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
2082
2211
  await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
2083
2212
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
2084
2213
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
@@ -2088,7 +2217,7 @@ export async function showSmartEntry(
2088
2217
  const milestoneIds = findMilestoneIds(basePath);
2089
2218
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
2090
2219
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
2091
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
2220
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
2092
2221
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
2093
2222
  `New milestone ${nextId}.`,
2094
2223
  basePath
@@ -2153,7 +2282,7 @@ export async function showSmartEntry(
2153
2282
  });
2154
2283
 
2155
2284
  if (choice === "plan") {
2156
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
2285
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
2157
2286
  await dispatchWorkflow(
2158
2287
  pi,
2159
2288
  await buildPlanMilestonePrompt(milestoneId, milestoneTitle, basePath),
@@ -2173,7 +2302,7 @@ export async function showSmartEntry(
2173
2302
  const milestoneIds = findMilestoneIds(basePath);
2174
2303
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
2175
2304
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
2176
- pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
2305
+ setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
2177
2306
  await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
2178
2307
  `New milestone ${nextId}.`,
2179
2308
  basePath