gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27

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 (212) hide show
  1. package/README.md +5 -7
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +6 -1
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
  6. package/dist/resources/extensions/gsd/auto/loop.js +235 -36
  7. package/dist/resources/extensions/gsd/auto/phases.js +14 -7
  8. package/dist/resources/extensions/gsd/auto/session.js +36 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
  11. package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
  12. package/dist/resources/extensions/gsd/auto.js +139 -49
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  16. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  17. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  18. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  19. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  20. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  22. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  23. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  24. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  25. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  26. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  27. package/dist/resources/extensions/gsd/doctor.js +12 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +355 -3
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  30. package/dist/resources/extensions/gsd/guided-flow.js +116 -26
  31. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  32. package/dist/resources/extensions/gsd/metrics.js +287 -1
  33. package/dist/resources/extensions/gsd/paths.js +79 -8
  34. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  38. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  40. package/dist/resources/extensions/gsd/state.js +21 -6
  41. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  42. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  43. package/dist/resources/extensions/gsd/workspace.js +59 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
  45. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  78. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  79. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/dist/web/standalone/server.js +1 -1
  81. package/package.json +1 -1
  82. package/packages/mcp-server/README.md +2 -11
  83. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  85. package/packages/mcp-server/dist/remote-questions.js +28 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  87. package/packages/mcp-server/dist/server.d.ts +28 -0
  88. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/server.js +94 -4
  90. package/packages/mcp-server/dist/server.js.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  92. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  93. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  94. package/packages/mcp-server/src/remote-questions.ts +35 -0
  95. package/packages/mcp-server/src/server.ts +129 -6
  96. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  97. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  98. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  99. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  100. package/src/resources/extensions/gsd/auto/phases.ts +15 -7
  101. package/src/resources/extensions/gsd/auto/session.ts +40 -0
  102. package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
  104. package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
  105. package/src/resources/extensions/gsd/auto.ts +166 -43
  106. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
  108. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  109. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  110. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  111. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  112. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  113. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  114. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  115. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  116. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  117. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  118. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  119. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  120. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  121. package/src/resources/extensions/gsd/doctor.ts +10 -2
  122. package/src/resources/extensions/gsd/gsd-db.ts +354 -3
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  124. package/src/resources/extensions/gsd/guided-flow.ts +152 -26
  125. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  126. package/src/resources/extensions/gsd/metrics.ts +321 -1
  127. package/src/resources/extensions/gsd/paths.ts +67 -8
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  129. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  130. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  131. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  134. package/src/resources/extensions/gsd/state.ts +44 -6
  135. package/src/resources/extensions/gsd/templates/project.md +10 -0
  136. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  137. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  138. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  139. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  140. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  141. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  142. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  143. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  144. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  145. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  146. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  147. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  148. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  149. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  150. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  151. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  152. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  153. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  154. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  155. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  156. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  157. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  158. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  159. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  160. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  161. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  162. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  163. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
  164. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  165. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  166. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  167. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  168. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  169. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  170. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  171. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  172. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  173. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  174. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  175. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  176. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  177. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  178. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  179. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  180. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  181. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  182. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  183. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  184. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
  185. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  186. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  187. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  188. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  189. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  190. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  192. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  194. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  196. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  197. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  198. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  199. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  200. package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
  201. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  202. package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
  203. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  204. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  205. package/src/resources/extensions/gsd/workspace.ts +95 -0
  206. package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
  207. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  208. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  209. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  210. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  211. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -21,9 +21,13 @@ Before your first action, print this banner verbatim in chat:
21
21
  ## Pre-flight
22
22
 
23
23
  1. Read `.gsd/PROJECT.md` end-to-end. If it does not exist, STOP and emit: `"PROJECT.md missing — run discuss-project first."`
24
- 2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence.
24
+ 2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence, and the project shape verdict — read the `## Project Shape` section and look for `**Complexity:**` (verdict is either `simple` or `complex`; default to `complex` if the section is missing or unclear).
25
25
  3. Check for existing `.gsd/REQUIREMENTS.md` — if present, this is a refinement pass, not a fresh write. Read existing requirements and treat them as the working set.
26
26
 
27
+ **Shape-dependent cadence:**
28
+ - **`simple`** — favor a single fast pass: extract requirements directly from PROJECT.md, ask 1–2 plain-text clarifying questions only if a class or status assignment is genuinely ambiguous, then write REQUIREMENTS.md.
29
+ - **`complex`** — full multi-round questioning with structured 3–4-option questions where alternatives matter.
30
+
27
31
  ---
28
32
 
29
33
  ## Interview Protocol
@@ -51,7 +55,7 @@ Ask **1–3 questions per round**. Each round targets one dimension:
51
55
 
52
56
  **Never fabricate or simulate user input.** Wait for actual responses.
53
57
 
54
- **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
58
+ **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. In **`complex`** mode, any free-form question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option grounded in the investigation above. The class-assignment and status questions are exempt — they have fixed enumerations. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
55
59
 
56
60
  **If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text. Keep each round to 1–3 questions.
57
61
 
@@ -8,6 +8,13 @@ Your goal is **not** to center the discussion on tech stack trivia, naming conve
8
8
 
9
9
  ## Interview Protocol
10
10
 
11
+ ### Read project shape
12
+
13
+ Before your first question round, read `.gsd/PROJECT.md` and look for `## Project Shape` → `**Complexity:**`. The verdict is either **`simple`** or **`complex`** (default to `complex` if the section is missing or unclear).
14
+
15
+ - **`simple`** — favor 1–2 plain-text rounds, write the slice context fast. Skip parallel-research investigation.
16
+ - **`complex`** — full investigation with structured 3–4-option questions.
17
+
11
18
  ### Before your first question round
12
19
 
13
20
  Do a lightweight targeted investigation so your questions are grounded in reality:
@@ -24,7 +31,7 @@ Do **not** go deep — just enough that your questions reflect what's actually t
24
31
 
25
32
  **Never fabricate or simulate user input.** Never generate fake transcript markers like `[User]`, `[Human]`, or `User:`. Ask one question round, then wait for the user's actual response before continuing.
26
33
 
27
- **If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
34
+ **If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. In **`complex`** mode, each multi-choice question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option; options must be grounded in the investigation above (codebase signals, library docs, prior `.gsd/` artifacts), not generic placeholders. In **`simple`** mode, 2 options is fine. Binary wrap-up gates are exempt from the 3-or-4 rule. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
28
35
  **If `{{structuredQuestionsAvailable}}` is `false`:** Ask **1–3 questions per round** in plain text. Number them and wait for the user's response before asking the next round.
29
36
  Keep each question focused on one of:
30
37
  - **UX and user-facing behaviour** — what does the user see, click, trigger, or experience?
@@ -270,6 +270,21 @@ export async function getActiveMilestoneId(basePath: string): Promise<string | n
270
270
  return null;
271
271
  }
272
272
 
273
+ /**
274
+ * Options for deriveState read-path routing.
275
+ *
276
+ * `projectRootForReads`: canonical project root (e.g. from
277
+ * `s.canonicalProjectRoot`) used for both the cache key and the artifact-read
278
+ * root in `_deriveStateImpl`. When omitted, behavior is identical to the
279
+ * single-arg signature (back-compat for all existing callers).
280
+ *
281
+ * Typed as an object literal (not `string | DeriveStateOptions`) so accidental
282
+ * `deriveState(path, "string")` is rejected at compile time.
283
+ */
284
+ export interface DeriveStateOptions {
285
+ projectRootForReads?: string;
286
+ }
287
+
273
288
  /**
274
289
  * Reconstruct GSD state from the authoritative DB.
275
290
  * STATE.md is a rendered cache of this output.
@@ -278,11 +293,22 @@ export async function getActiveMilestoneId(basePath: string): Promise<string | n
278
293
  * Legacy filesystem parsing is available only through an explicit opt-in for
279
294
  * tests/recovery flows; runtime must not silently infer state from markdown.
280
295
  */
281
- export async function deriveState(basePath: string): Promise<GSDState> {
282
- // Return cached result if within the TTL window for the same basePath
296
+ export async function deriveState(
297
+ basePath: string,
298
+ opts?: DeriveStateOptions,
299
+ ): Promise<GSDState> {
300
+ // Use the canonical project root (when provided) as the cache key so that
301
+ // two calls with different basePath strings (e.g. worktree path vs project
302
+ // root) but the same canonical .gsd/ share a single cache entry. The same
303
+ // key is used for both the lookup AND the write below — keying lookup on
304
+ // canonical-root while writing on basePath would silently return stale
305
+ // results across path-form alternation.
306
+ const cacheKey = opts?.projectRootForReads ?? basePath;
307
+
308
+ // Return cached result if within the TTL window for the same cacheKey
283
309
  if (
284
310
  _stateCache &&
285
- _stateCache.basePath === basePath &&
311
+ _stateCache.basePath === cacheKey &&
286
312
  Date.now() - _stateCache.timestamp < CACHE_TTL_MS
287
313
  ) {
288
314
  return _stateCache.result;
@@ -303,7 +329,7 @@ export async function deriveState(basePath: string): Promise<GSDState> {
303
329
  if (wasDbOpenAttempted()) {
304
330
  logWarning("state", "DB unavailable — using explicit legacy filesystem state derivation");
305
331
  }
306
- result = await _deriveStateImpl(basePath);
332
+ result = await _deriveStateImpl(basePath, opts);
307
333
  _telemetry.markdownDeriveCount++;
308
334
  } else {
309
335
  if (wasDbOpenAttempted()) {
@@ -325,7 +351,7 @@ export async function deriveState(basePath: string): Promise<GSDState> {
325
351
 
326
352
  stopTimer({ phase: result.phase, milestone: result.activeMilestone?.id });
327
353
  debugCount("deriveStateCalls");
328
- _stateCache = { basePath, result, timestamp: Date.now() };
354
+ _stateCache = { basePath: cacheKey, result, timestamp: Date.now() };
329
355
  return result;
330
356
  }
331
357
 
@@ -838,7 +864,19 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
838
864
  // LEGACY: Filesystem-based state derivation for unmigrated projects.
839
865
  // DB-backed projects use deriveStateFromDb() above. Target: extract to
840
866
  // state-legacy.ts when all projects are DB-backed.
841
- export async function _deriveStateImpl(basePath: string): Promise<GSDState> {
867
+ export async function _deriveStateImpl(
868
+ basePath: string,
869
+ opts?: DeriveStateOptions,
870
+ ): Promise<GSDState> {
871
+ // When the caller supplies a canonical project root for reads (e.g.
872
+ // s.canonicalProjectRoot from auto-mode), route all artifact reads through
873
+ // it. This prevents the worktree-local empty `.gsd/` from being consulted
874
+ // when the canonical state lives at the project root (or via a `.gsd`
875
+ // symlink into the external state dir).
876
+ if (opts?.projectRootForReads) {
877
+ basePath = opts.projectRootForReads;
878
+ }
879
+
842
880
  const diskIds = findMilestoneIds(basePath);
843
881
  const customOrder = loadQueueOrder(basePath);
844
882
  const milestoneIds = sortByQueueOrder(diskIds, customOrder);
@@ -11,6 +11,16 @@
11
11
 
12
12
  {{theOneThingThatMustWorkEvenIfEverythingElseIsCut}}
13
13
 
14
+ ## Project Shape
15
+
16
+ <!-- Drives questioning depth in downstream stages. `simple` → short plain-text
17
+ rounds, fast PROJECT/CONTEXT/REQUIREMENTS writes. `complex` → researched
18
+ 3–4-option questions with an "Other — let me discuss" hatch.
19
+ Default to `complex` when uncertain. -->
20
+
21
+ - **Complexity:** {{simple | complex}}
22
+ - **Why:** {{one-line rationale citing the signals that decided it}}
23
+
14
24
  ## Current State
15
25
 
16
26
  {{whatHasBeenBuiltSoFar — what works, what exists, what's deployed}}
@@ -35,7 +35,7 @@ import { _setAutoActiveForTest } from '../auto.ts';
35
35
  // Reset all relevant state before and after each test.
36
36
  function resetState(): void {
37
37
  _setAutoActiveForTest(false);
38
- clearDiscussionFlowState();
38
+ clearDiscussionFlowState(process.cwd());
39
39
  }
40
40
 
41
41
  describe('auto-discuss-milestone-deadlock-4973', () => {
@@ -51,7 +51,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
51
51
  _setAutoActiveForTest(true);
52
52
 
53
53
  // Before mark: blocked
54
- const snapshotBefore = loadWriteGateSnapshot();
54
+ const snapshotBefore = loadWriteGateSnapshot(process.cwd());
55
55
  const beforeResult = shouldBlockContextArtifactSaveInSnapshot(
56
56
  snapshotBefore,
57
57
  'CONTEXT',
@@ -61,10 +61,10 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
61
61
  assert.strictEqual(beforeResult.block, true, 'should block before markDepthVerified');
62
62
 
63
63
  // Simulate what the dispatch rule now does in auto-mode
64
- markDepthVerified('M001');
64
+ markDepthVerified('M001', process.cwd());
65
65
 
66
66
  // After mark: unblocked
67
- const snapshotAfter = loadWriteGateSnapshot();
67
+ const snapshotAfter = loadWriteGateSnapshot(process.cwd());
68
68
  const afterResult = shouldBlockContextArtifactSaveInSnapshot(
69
69
  snapshotAfter,
70
70
  'CONTEXT',
@@ -87,7 +87,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
87
87
  assert.strictEqual(beforeResult.block, true, 'write should be blocked before markDepthVerified');
88
88
 
89
89
  // Simulate dispatch rule auto-mark
90
- markDepthVerified('M001');
90
+ markDepthVerified('M001', process.cwd());
91
91
 
92
92
  // After mark: unblocked
93
93
  const afterResult = shouldBlockContextWrite('write', contextPath, 'M001');
@@ -109,8 +109,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
109
109
  // the dispatch-site call site is safe regardless of prior session state.
110
110
  test('Test 3: session_switch ordering — clearDiscussionFlowState clears mark; dispatch-site call re-establishes it', () => {
111
111
  // Simulate a mark from a prior session
112
- markDepthVerified('M001');
113
- let snapshot = loadWriteGateSnapshot();
112
+ markDepthVerified('M001', process.cwd());
113
+ let snapshot = loadWriteGateSnapshot(process.cwd());
114
114
  assert.strictEqual(
115
115
  isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
116
116
  true,
@@ -119,8 +119,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
119
119
 
120
120
  // session_switch fires clearDiscussionFlowState() — this is exactly what
121
121
  // register-hooks.ts:106 does
122
- clearDiscussionFlowState();
123
- snapshot = loadWriteGateSnapshot();
122
+ clearDiscussionFlowState(process.cwd());
123
+ snapshot = loadWriteGateSnapshot(process.cwd());
124
124
  assert.strictEqual(
125
125
  isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
126
126
  false,
@@ -130,9 +130,9 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
130
130
  // Now the dispatch rule fires (after session_switch cleared state)
131
131
  // and re-establishes the mark for the new session
132
132
  _setAutoActiveForTest(true);
133
- markDepthVerified('M001'); // this is what the dispatch rule does
133
+ markDepthVerified('M001', process.cwd()); // this is what the dispatch rule does
134
134
 
135
- snapshot = loadWriteGateSnapshot();
135
+ snapshot = loadWriteGateSnapshot(process.cwd());
136
136
  assert.strictEqual(
137
137
  isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
138
138
  true,
@@ -158,7 +158,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
158
158
 
159
159
  // CONTEXT artifact save is still blocked
160
160
  const snapshotResult = shouldBlockContextArtifactSaveInSnapshot(
161
- loadWriteGateSnapshot(),
161
+ loadWriteGateSnapshot(process.cwd()),
162
162
  'CONTEXT',
163
163
  'M002',
164
164
  null,
@@ -238,7 +238,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
238
238
  );
239
239
 
240
240
  // ── Deep auto-mode case: the user-facing approval gate must stay closed ──
241
- clearDiscussionFlowState();
241
+ clearDiscussionFlowState(process.cwd());
242
242
  if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
243
243
  _setAutoActiveForTest(true);
244
244
  const deepCtx = {
@@ -264,7 +264,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
264
264
  // ── Interactive case: the rule must NOT call markDepthVerified ──
265
265
  // clearDiscussionFlowState() only deletes the snapshot at process.cwd(),
266
266
  // so we must explicitly remove the snapshot under our tempBase too.
267
- clearDiscussionFlowState();
267
+ clearDiscussionFlowState(process.cwd());
268
268
  if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
269
269
  _setAutoActiveForTest(false);
270
270
  snap = loadWriteGateSnapshot(tempBase);
@@ -0,0 +1,72 @@
1
+ // gsd-2 + Phase C deletion regression: createAutoWorktree no longer copies .gsd/
2
+ //
3
+ // Verifies that createAutoWorktree on a project with a real (non-symlinked)
4
+ // .gsd/ does NOT populate .gsd/milestones/ inside the worktree. Pre-Phase-C,
5
+ // copyPlanningArtifacts would mirror the project-root .gsd/ into the
6
+ // worktree-local .gsd/. Phase C deleted that helper because writers in
7
+ // auto-mode now route through s.canonicalProjectRoot, so the worktree never
8
+ // needs a parallel .gsd/ projection.
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, mkdirSync, writeFileSync, existsSync, realpathSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import { execFileSync } from "node:child_process";
16
+
17
+ import { createAutoWorktree, teardownAutoWorktree } from "../auto-worktree.ts";
18
+
19
+ function git(args: string[], cwd: string): void {
20
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
21
+ }
22
+
23
+ test("createAutoWorktree does NOT copy project-root .gsd/milestones into the worktree", (t) => {
24
+ const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-no-copy-")));
25
+
26
+ // Initialize a real git repo with a real .gsd/ directory containing some
27
+ // planning artifacts that the deleted copyPlanningArtifacts would have
28
+ // mirrored.
29
+ git(["init", "-b", "main"], base);
30
+ git(["config", "user.name", "Pi Test"], base);
31
+ git(["config", "user.email", "pi@example.com"], base);
32
+ writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
33
+ git(["add", "README.md"], base);
34
+ git(["commit", "-m", "chore: init"], base);
35
+
36
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
37
+ writeFileSync(
38
+ join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
39
+ "# M001 Context\n",
40
+ "utf-8",
41
+ );
42
+ writeFileSync(
43
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
44
+ "# M001 Roadmap\n",
45
+ "utf-8",
46
+ );
47
+
48
+ const wtPath = createAutoWorktree(base, "M001");
49
+ t.after(() => {
50
+ try { teardownAutoWorktree(base, "M001"); } catch { /* noop */ }
51
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
52
+ });
53
+
54
+ // Phase C invariant: the worktree's .gsd/milestones/M001 must NOT exist
55
+ // (no copyPlanningArtifacts), and the project-root version must still be
56
+ // intact (it was the source, not destination).
57
+ assert.equal(
58
+ existsSync(join(wtPath, ".gsd", "milestones", "M001", "M001-CONTEXT.md")),
59
+ false,
60
+ "worktree should NOT have a copy of M001-CONTEXT.md (copyPlanningArtifacts deleted)",
61
+ );
62
+ assert.equal(
63
+ existsSync(join(wtPath, ".gsd", "milestones", "M001", "M001-ROADMAP.md")),
64
+ false,
65
+ "worktree should NOT have a copy of M001-ROADMAP.md",
66
+ );
67
+ assert.equal(
68
+ existsSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md")),
69
+ true,
70
+ "project-root .gsd/ retains the canonical CONTEXT.md",
71
+ );
72
+ });
@@ -0,0 +1,190 @@
1
+ // gsd-2 + Symlinked .gsd worktree-loop reproduction (Phase A pt 2 follow-up to PR #5236)
2
+ //
3
+ // Regression coverage for the auto-mode loop bug observed on projects whose
4
+ // .gsd/ is a symlink into ~/.gsd/projects/<hash>/ (the external-state layout).
5
+ //
6
+ // Two assertions:
7
+ // 1. deriveState's cache key is the canonical project root when callers
8
+ // opt into projectRootForReads — so two derive calls that should refer
9
+ // to the same canonical state share a single cache entry, regardless of
10
+ // whether the caller passed the worktree path or the project-root path.
11
+ // 2. _deriveStateImpl's projectRootForReads option routes legacy markdown
12
+ // reads through the canonical project root, finding files that live in
13
+ // the symlink target rather than the worktree-local empty .gsd/.
14
+ //
15
+ // Per project rule #11: regression test using node:test + node:assert/strict,
16
+ // no source-grep assertions. The first test would fail on main without the
17
+ // cache-key fix in state.ts (lookup vs write keys would diverge across
18
+ // path-form alternation, producing cache misses). The second test would
19
+ // fail on main because _deriveStateImpl doesn't accept the option at all.
20
+
21
+ import test from "node:test";
22
+ import assert from "node:assert/strict";
23
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, realpathSync, symlinkSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import { tmpdir } from "node:os";
26
+
27
+ import {
28
+ deriveState,
29
+ _deriveStateImpl,
30
+ invalidateStateCache,
31
+ type DeriveStateOptions,
32
+ } from "../state.ts";
33
+ import {
34
+ openDatabase,
35
+ closeDatabase,
36
+ insertMilestone,
37
+ insertSlice,
38
+ insertTask,
39
+ } from "../gsd-db.ts";
40
+
41
+ // ─── Fixture helpers ──────────────────────────────────────────────────────
42
+
43
+ interface SymlinkedFixture {
44
+ /** Project root containing .gsd as a symlink. */
45
+ projectRoot: string;
46
+ /** External state dir that .gsd points at (acts as the canonical .gsd/). */
47
+ externalState: string;
48
+ /** Worktree path under the external state's worktrees/ dir. */
49
+ worktreePath: string;
50
+ }
51
+
52
+ function makeSymlinkedFixture(prefix: string): SymlinkedFixture {
53
+ // Use realpathSync on tmpdir so that subsequent realpath comparisons are stable
54
+ // — macOS /var symlinks to /private/var, which would otherwise pollute the
55
+ // canonical-root assertions below.
56
+ const root = realpathSync(mkdtempSync(join(tmpdir(), `gsd-${prefix}-`)));
57
+ const projectRoot = join(root, "project");
58
+ const externalState = join(root, "external-state", "projects", "abc123");
59
+
60
+ mkdirSync(projectRoot, { recursive: true });
61
+ mkdirSync(externalState, { recursive: true });
62
+
63
+ // .gsd → externalState (the layout that triggered the original bug)
64
+ symlinkSync(externalState, join(projectRoot, ".gsd"), "junction");
65
+
66
+ // Worktree path lives under the external state's worktrees/ dir, mirroring
67
+ // the canonicalProjectRoot resolution that resolveGsdPathContract performs
68
+ // for the external-state layout.
69
+ const worktreePath = join(externalState, "worktrees", "M001");
70
+ mkdirSync(worktreePath, { recursive: true });
71
+
72
+ return { projectRoot, externalState, worktreePath };
73
+ }
74
+
75
+ function cleanupFixture(fx: SymlinkedFixture): void {
76
+ try { closeDatabase(); } catch { /* noop */ }
77
+ // The mkdtemp root is two levels above projectRoot.
78
+ try {
79
+ const root = join(fx.projectRoot, "..");
80
+ rmSync(root, { recursive: true, force: true });
81
+ } catch { /* noop */ }
82
+ }
83
+
84
+ // ═══════════════════════════════════════════════════════════════════════════
85
+ // Test 1: cache-key invariance under projectRootForReads
86
+ // ═══════════════════════════════════════════════════════════════════════════
87
+
88
+ test("deriveState: cache key is canonical when projectRootForReads is supplied", async (t) => {
89
+ const fx = makeSymlinkedFixture("symlink-cache");
90
+ t.after(() => cleanupFixture(fx));
91
+
92
+ // Open the DB at the canonical .gsd location (externalState).
93
+ openDatabase(join(fx.externalState, "gsd.db"));
94
+ insertMilestone({ id: "M001", title: "Symlinked", status: "active" });
95
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice" });
96
+ // No tasks → DB-derived state is "planning".
97
+
98
+ invalidateStateCache();
99
+
100
+ const optsCanonical: DeriveStateOptions = { projectRootForReads: fx.projectRoot };
101
+
102
+ // First call: seed the cache through the worktree-path form.
103
+ const stateA = await deriveState(fx.worktreePath, optsCanonical);
104
+ assert.equal(stateA.activeMilestone?.id, "M001");
105
+ assert.equal(stateA.activeSlice?.id, "S01");
106
+ assert.equal(stateA.phase, "planning");
107
+
108
+ // Second call: canonical project-root form must hit the same cache entry.
109
+ const stateB = await deriveState(fx.projectRoot);
110
+ assert.equal(stateB, stateA, "second call with same canonical key must return the cached object");
111
+
112
+ // Third call: worktree-path form with projectRootForReads must also hit the
113
+ // same cache entry, proving the cache key is symmetric across both call
114
+ // orders.
115
+ const stateC = await deriveState(fx.worktreePath, optsCanonical);
116
+ assert.equal(stateC, stateA, "third call with worktree path plus canonical reads must hit the same cache entry");
117
+
118
+ // Mutation invalidates: insert a task, clear cache, re-derive — must
119
+ // observe the new state via the canonical key path.
120
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Task", status: "active" });
121
+ invalidateStateCache();
122
+
123
+ const stateD = await deriveState(fx.worktreePath, optsCanonical);
124
+ assert.notEqual(stateD, stateA, "post-mutation derive must re-compute, not reuse the prior cached object");
125
+ assert.equal(stateD.activeTask?.id, "T01", "mutation must surface in the re-derived state");
126
+ assert.equal(stateD.phase, "executing");
127
+ });
128
+
129
+ // ═══════════════════════════════════════════════════════════════════════════
130
+ // Test 2: _deriveStateImpl reads from canonical root via projectRootForReads
131
+ // ═══════════════════════════════════════════════════════════════════════════
132
+
133
+ test("_deriveStateImpl: projectRootForReads routes legacy markdown reads to the canonical .gsd/", async (t) => {
134
+ const fx = makeSymlinkedFixture("symlink-md");
135
+ t.after(() => cleanupFixture(fx));
136
+ // No DB opened — exercise the markdown fallback.
137
+
138
+ // Seed the external state dir (the symlink target) with a roadmap so the
139
+ // legacy filesystem state derivation has a milestone to find.
140
+ const m1Dir = join(fx.externalState, "milestones", "M001");
141
+ mkdirSync(m1Dir, { recursive: true });
142
+ writeFileSync(
143
+ join(m1Dir, "M001-CONTEXT.md"),
144
+ "# M001: Symlinked legacy md test\n\nTest project.\n",
145
+ "utf-8",
146
+ );
147
+ writeFileSync(
148
+ join(m1Dir, "M001-ROADMAP.md"),
149
+ [
150
+ "# M001 Roadmap",
151
+ "",
152
+ "## Slices",
153
+ "",
154
+ "- [ ] **S01: First slice** — depends:",
155
+ "",
156
+ ].join("\n"),
157
+ "utf-8",
158
+ );
159
+
160
+ invalidateStateCache();
161
+
162
+ // Calling _deriveStateImpl with the worktree path AND projectRootForReads
163
+ // pointing at the project root must consult the canonical .gsd/ (via the
164
+ // symlink target externalState), find M001/S01, and report planning phase
165
+ // because no slice plan file exists yet.
166
+ const state = await _deriveStateImpl(fx.worktreePath, { projectRootForReads: fx.projectRoot });
167
+ assert.equal(state.activeMilestone?.id, "M001", "must find M001 via canonical .gsd/ reads");
168
+ assert.equal(state.activeSlice?.id, "S01", "must find S01 from the roadmap");
169
+ assert.equal(state.phase, "planning", "no slice PLAN.md yet → planning phase");
170
+ });
171
+
172
+ // ═══════════════════════════════════════════════════════════════════════════
173
+ // Test 3: type-safety guard for the deriveState opts overload (compile-time)
174
+ // ═══════════════════════════════════════════════════════════════════════════
175
+ //
176
+ // The DeriveStateOptions parameter is typed as an object literal so accidental
177
+ // `deriveState(path, "string")` is a TypeScript compile error. The
178
+ // expect-error directive verifies that this guard is in place — if the
179
+ // overload were widened to `string | DeriveStateOptions`, the directive would
180
+ // trigger TS2578 ("Unused '@ts-expect-error' directive") at build time.
181
+
182
+ test("deriveState: opts param rejects non-object values at compile time", () => {
183
+ // The actual assertion is the TypeScript compile-time check below; the
184
+ // runtime body just confirms the test ran.
185
+ if (false) {
186
+ // @ts-expect-error — projectRootForReads must be a string
187
+ void deriveState("/nonexistent", { projectRootForReads: 123 });
188
+ }
189
+ assert.ok(true);
190
+ });