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
@@ -1,3 +1,4 @@
1
+ // GSD-2 — WorktreeResolver: encapsulates worktree path state and merge/exit lifecycle.
1
2
  /**
2
3
  * WorktreeResolver — encapsulates worktree path state and merge/exit lifecycle.
3
4
  *
@@ -23,7 +24,21 @@ import { emitJournalEvent } from "./journal.js";
23
24
  import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js";
24
25
  import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
25
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
26
- import { resolveWorktreeProjectRoot } from "./worktree-root.js";
27
+ import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
28
+ import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
29
+
30
+ // ─── Path Comparison Helper ────────────────────────────────────────────────
31
+ /**
32
+ * Compare two paths for physical identity, tolerating trailing slashes,
33
+ * symlink differences, and case variations on case-insensitive volumes.
34
+ *
35
+ * Used in place of string `===` / `!==` wherever one operand may be
36
+ * realpath-normalised (e.g. from the workspace registry) and the other
37
+ * may not be (e.g. a raw caller-supplied basePath).
38
+ */
39
+ function isSamePath(a: string, b: string): boolean {
40
+ return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
41
+ }
27
42
 
28
43
  // ─── Dependency Interface ──────────────────────────────────────────────────
29
44
 
@@ -185,6 +200,67 @@ export class WorktreeResolver {
185
200
  return;
186
201
  }
187
202
 
203
+ // Phase B: claim a milestone lease before any worktree mutation. Two
204
+ // workers cannot enter the same milestone concurrently. Best-effort:
205
+ // skip if no worker registered (single-worker fallback) or DB
206
+ // unavailable; reuse existing lease if we already hold it on this
207
+ // milestone (re-entry within the same session).
208
+ if (this.s.workerId) {
209
+ if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
210
+ // Already held — no-op, the heartbeat in loop.ts refreshes TTL.
211
+ } else {
212
+ // If we held a different milestone, release it first so other
213
+ // workers don't have to wait for TTL.
214
+ if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
215
+ try {
216
+ releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
217
+ } catch (err) {
218
+ debugLog("WorktreeResolver", {
219
+ action: "enterMilestone",
220
+ milestoneId,
221
+ releasePriorLeaseError: err instanceof Error ? err.message : String(err),
222
+ });
223
+ }
224
+ this.s.milestoneLeaseToken = null;
225
+ }
226
+
227
+ try {
228
+ const claim = claimMilestoneLease(this.s.workerId, milestoneId);
229
+ if (claim.ok) {
230
+ this.s.milestoneLeaseToken = claim.token;
231
+ debugLog("WorktreeResolver", {
232
+ action: "enterMilestone",
233
+ milestoneId,
234
+ leaseAcquired: true,
235
+ fencingToken: claim.token,
236
+ expiresAt: claim.expiresAt,
237
+ });
238
+ } else {
239
+ // Lease held by another worker — fail loud so the user can
240
+ // see the conflict instead of silently double-running.
241
+ const msg = `Milestone ${milestoneId} is held by worker ${claim.byWorker} until ${claim.expiresAt}.`;
242
+ debugLog("WorktreeResolver", {
243
+ action: "enterMilestone",
244
+ milestoneId,
245
+ leaseHeldByOther: claim.byWorker,
246
+ expiresAt: claim.expiresAt,
247
+ });
248
+ ctx.notify(`${msg} Another auto-mode worker is active. Stop it before entering ${milestoneId}.`, "error");
249
+ return;
250
+ }
251
+ } catch (err) {
252
+ // DB unavailable or other error — log and fall through to the
253
+ // pre-Phase-B single-worker behavior so a fresh project before
254
+ // DB init still works.
255
+ debugLog("WorktreeResolver", {
256
+ action: "enterMilestone",
257
+ milestoneId,
258
+ leaseError: err instanceof Error ? err.message : String(err),
259
+ });
260
+ }
261
+ }
262
+ }
263
+
188
264
  // Resolve the project root for worktree operations via shared helper.
189
265
  // Handles the case where originalBasePath is falsy and basePath is itself
190
266
  // a worktree path — prevents double-nested worktree paths (#3729).
@@ -589,7 +665,7 @@ export class WorktreeResolver {
589
665
  milestoneId,
590
666
  "ROADMAP",
591
667
  );
592
- if (!roadmapPath && this.s.basePath !== originalBase) {
668
+ if (!roadmapPath && !isSamePath(this.s.basePath, originalBase)) {
593
669
  roadmapPath = this.deps.resolveMilestoneFile(
594
670
  this.s.basePath,
595
671
  milestoneId,
@@ -91,9 +91,9 @@ function matchesBlockedPattern(path: string): boolean {
91
91
  * Directs the agent to use engine tool calls instead.
92
92
  */
93
93
  export const BLOCKED_WRITE_ERROR = `Direct writes to .gsd/STATE.md and .gsd/gsd.db are blocked. Use engine tool calls instead:
94
- - To complete a task: call gsd_complete_task(milestone_id, slice_id, task_id, summary)
95
- - To complete a slice: call gsd_complete_slice(milestone_id, slice_id, summary, uat_result)
96
- - To save a decision: call gsd_save_decision(scope, decision, choice, rationale)
94
+ - To complete a task: call gsd_task_complete(milestone_id, slice_id, task_id, summary)
95
+ - To complete a slice: call gsd_slice_complete(milestone_id, slice_id, summary, uat_result)
96
+ - To save a decision: call gsd_decision_save(scope, decision, choice, rationale)
97
97
  - To start a task: call gsd_start_task(milestone_id, slice_id, task_id)
98
98
  - To record verification: call gsd_record_verification(milestone_id, slice_id, task_id, evidence)
99
99
  - To report a blocker: call gsd_report_blocker(milestone_id, slice_id, task_id, description)`;
@@ -1,213 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { mkdirSync, mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
4
- import { createRequire } from "node:module";
5
- import { join } from "node:path";
6
- import { tmpdir } from "node:os";
7
-
8
- import { writeLock, readCrashLock, clearLock, isLockProcessAlive } from "../crash-recovery.ts";
9
- import { acquireSessionLock, releaseSessionLock } from "../session-lock.ts";
10
-
11
- const require = createRequire(import.meta.url);
12
-
13
- function hasProperLockfile(): boolean {
14
- try {
15
- require("proper-lockfile");
16
- return true;
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- const properLockfileAvailable = hasProperLockfile();
23
-
24
- // ─── writeLock creates auto.lock in .gsd/ ────────────────────────────────
25
-
26
- test("writeLock creates auto.lock with correct structure", () => {
27
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
28
- mkdirSync(join(dir, ".gsd"), { recursive: true });
29
-
30
- writeLock(dir, "starting", "M001");
31
-
32
- const lockPath = join(dir, ".gsd", "auto.lock");
33
- assert.ok(existsSync(lockPath), "auto.lock should exist after writeLock");
34
-
35
- const data = JSON.parse(readFileSync(lockPath, "utf-8"));
36
- assert.equal(data.pid, process.pid, "lock should contain current PID");
37
- assert.equal(data.unitType, "starting", "lock should contain unit type");
38
- assert.equal(data.unitId, "M001", "lock should contain unit ID");
39
- assert.ok(data.startedAt, "lock should have startedAt timestamp");
40
-
41
- rmSync(dir, { recursive: true, force: true });
42
- });
43
-
44
- test("writeLock updates existing lock with new unit info", () => {
45
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
46
- mkdirSync(join(dir, ".gsd"), { recursive: true });
47
-
48
- writeLock(dir, "starting", "M001");
49
- writeLock(dir, "execute-task", "M001/S01/T01", "/tmp/session.jsonl");
50
-
51
- const data = JSON.parse(readFileSync(join(dir, ".gsd", "auto.lock"), "utf-8"));
52
- assert.equal(data.unitType, "execute-task", "lock should be updated to new unit type");
53
- assert.equal(data.unitId, "M001/S01/T01", "lock should be updated to new unit ID");
54
- assert.equal(data.sessionFile, "/tmp/session.jsonl", "session file should be recorded");
55
-
56
- rmSync(dir, { recursive: true, force: true });
57
- });
58
-
59
- // ─── readCrashLock reads auto.lock data ──────────────────────────────────
60
-
61
- test("readCrashLock returns null when no lock file exists", () => {
62
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
63
- mkdirSync(join(dir, ".gsd"), { recursive: true });
64
-
65
- const lock = readCrashLock(dir);
66
- assert.equal(lock, null, "should return null when no lock file");
67
-
68
- rmSync(dir, { recursive: true, force: true });
69
- });
70
-
71
- test("readCrashLock returns lock data when file exists", () => {
72
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
73
- mkdirSync(join(dir, ".gsd"), { recursive: true });
74
-
75
- writeLock(dir, "plan-milestone", "M002");
76
- const lock = readCrashLock(dir);
77
-
78
- assert.ok(lock, "should return lock data");
79
- assert.equal(lock!.unitType, "plan-milestone");
80
- assert.equal(lock!.unitId, "M002");
81
-
82
- rmSync(dir, { recursive: true, force: true });
83
- });
84
-
85
- // ─── clearLock removes auto.lock ─────────────────────────────────────────
86
-
87
- test("clearLock removes the lock file", () => {
88
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
89
- mkdirSync(join(dir, ".gsd"), { recursive: true });
90
-
91
- writeLock(dir, "starting", "M001");
92
- assert.ok(existsSync(join(dir, ".gsd", "auto.lock")), "lock should exist before clear");
93
-
94
- clearLock(dir);
95
- assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "lock should be removed after clear");
96
-
97
- rmSync(dir, { recursive: true, force: true });
98
- });
99
-
100
- test("clearLock is safe when no lock file exists", () => {
101
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
102
- mkdirSync(join(dir, ".gsd"), { recursive: true });
103
-
104
- // Should not throw
105
- clearLock(dir);
106
-
107
- rmSync(dir, { recursive: true, force: true });
108
- });
109
-
110
- test("bootstrap cleanup releases session lock artifacts", (t) => {
111
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
112
- mkdirSync(join(dir, ".gsd"), { recursive: true });
113
-
114
- t.after(() => rmSync(dir, { recursive: true, force: true }));
115
-
116
- const result = acquireSessionLock(dir);
117
- assert.equal(result.acquired, true, "session lock should be acquired");
118
- assert.ok(existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock should exist while lock is held");
119
- if (properLockfileAvailable) {
120
- assert.ok(existsSync(join(dir, ".gsd.lock")), ".gsd.lock should exist while lock is held");
121
- }
122
-
123
- releaseSessionLock(dir);
124
- clearLock(dir);
125
-
126
- assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock should be removed by bootstrap cleanup");
127
- assert.ok(!existsSync(join(dir, ".gsd.lock")), ".gsd.lock should be removed by bootstrap cleanup");
128
- });
129
-
130
- // ─── isLockProcessAlive detects live vs dead PIDs ────────────────────────
131
-
132
- test("isLockProcessAlive returns false for dead PID", () => {
133
- const lock = {
134
- pid: 9999999,
135
- startedAt: new Date().toISOString(),
136
- unitType: "execute-task",
137
- unitId: "M001/S01/T01",
138
- unitStartedAt: new Date().toISOString(),
139
- };
140
- assert.equal(isLockProcessAlive(lock), false, "dead PID should return false");
141
- });
142
-
143
- test("#2470: isLockProcessAlive returns true for own PID (we hold the lock)", () => {
144
- const lock = {
145
- pid: process.pid,
146
- startedAt: new Date().toISOString(),
147
- unitType: "execute-task",
148
- unitId: "M001/S01/T01",
149
- unitStartedAt: new Date().toISOString(),
150
- };
151
- assert.equal(isLockProcessAlive(lock), true, "own PID means we are alive — not stale (#2470)");
152
- });
153
-
154
- test("isLockProcessAlive returns false for invalid PID", () => {
155
- const lock = {
156
- pid: -1,
157
- startedAt: new Date().toISOString(),
158
- unitType: "execute-task",
159
- unitId: "M001/S01/T01",
160
- unitStartedAt: new Date().toISOString(),
161
- };
162
- assert.equal(isLockProcessAlive(lock), false, "negative PID should return false");
163
- });
164
-
165
- // ─── Cross-process detection via lock file ───────────────────────────────
166
-
167
- test("lock file enables cross-process auto-mode detection", () => {
168
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
169
- mkdirSync(join(dir, ".gsd"), { recursive: true });
170
-
171
- // Use the parent process PID — guaranteed alive on all platforms (Unix and Windows).
172
- // PID 1 (init) only works on Unix; on Windows it doesn't exist.
173
- const alivePid = process.ppid;
174
- const lockData = {
175
- pid: alivePid,
176
- startedAt: new Date().toISOString(),
177
- unitType: "execute-task",
178
- unitId: "M001/S01/T02",
179
- unitStartedAt: new Date().toISOString(),
180
- };
181
- writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
182
-
183
- const lock = readCrashLock(dir);
184
- assert.ok(lock, "should read the lock");
185
- assert.equal(lock!.pid, alivePid);
186
-
187
- // Parent PID is always alive — isLockProcessAlive should detect it
188
- const alive = isLockProcessAlive(lock!);
189
- assert.equal(alive, true, "parent PID should be detected as alive");
190
-
191
- rmSync(dir, { recursive: true, force: true });
192
- });
193
-
194
- test("stale lock from dead process is detected as not alive", () => {
195
- const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
196
- mkdirSync(join(dir, ".gsd"), { recursive: true });
197
-
198
- // Simulate a stale lock from a process that no longer exists
199
- const lockData = {
200
- pid: 9999999,
201
- startedAt: "2026-03-01T00:00:00Z",
202
- unitType: "plan-slice",
203
- unitId: "M001/S02",
204
- unitStartedAt: "2026-03-01T00:05:00Z",
205
- };
206
- writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
207
-
208
- const lock = readCrashLock(dir);
209
- assert.ok(lock, "should read the stale lock");
210
- assert.equal(isLockProcessAlive(lock!), false, "dead process should not be alive");
211
-
212
- rmSync(dir, { recursive: true, force: true });
213
- });
@@ -1,87 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { mkdirSync, mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { tmpdir } from "node:os";
6
-
7
- import { writeLock, readCrashLock, clearLock } from "../crash-recovery.ts";
8
- import { checkRemoteAutoSession, stopAutoRemote } from "../auto.ts";
9
-
10
- function makeTmpProject(): string {
11
- const dir = mkdtempSync(join(tmpdir(), "gsd-stale-lock-test-"));
12
- mkdirSync(join(dir, ".gsd"), { recursive: true });
13
- return dir;
14
- }
15
-
16
- // ─── checkRemoteAutoSession: own-PID filtering (#2730) ───────────────────
17
-
18
- test("#2730: checkRemoteAutoSession returns { running: false } when lock PID matches current process", (t) => {
19
- const dir = makeTmpProject();
20
- t.after(() => rmSync(dir, { recursive: true, force: true }));
21
-
22
- // Write a lock with the current process PID — simulates a stale lock
23
- // left behind after step-mode exit without full cleanup.
24
- writeLock(dir, "execute-task", "M001/S01/T01");
25
-
26
- const lock = readCrashLock(dir);
27
- assert.ok(lock, "lock file should exist");
28
- assert.equal(lock!.pid, process.pid, "lock should have our PID");
29
-
30
- const result = checkRemoteAutoSession(dir);
31
- assert.equal(result.running, false, "own PID must not be treated as a remote session");
32
- });
33
-
34
- test("#2730: checkRemoteAutoSession still detects a genuine remote session (different PID)", (t) => {
35
- const dir = makeTmpProject();
36
- t.after(() => rmSync(dir, { recursive: true, force: true }));
37
-
38
- // Use parent PID — guaranteed alive, guaranteed not our PID.
39
- const remotePid = process.ppid;
40
- const lockData = {
41
- pid: remotePid,
42
- startedAt: new Date().toISOString(),
43
- unitType: "execute-task",
44
- unitId: "M001/S01/T02",
45
- unitStartedAt: new Date().toISOString(),
46
- };
47
- writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
48
-
49
- const result = checkRemoteAutoSession(dir);
50
- assert.equal(result.running, true, "different live PID should be detected as running");
51
- assert.equal(result.pid, remotePid);
52
- });
53
-
54
- // ─── stopAutoRemote: self-kill prevention (#2730) ────────────────────────
55
-
56
- test("#2730: stopAutoRemote does not send SIGTERM when lock PID matches current process", (t) => {
57
- const dir = makeTmpProject();
58
- t.after(() => rmSync(dir, { recursive: true, force: true }));
59
-
60
- // Write a lock with our own PID
61
- writeLock(dir, "execute-task", "M001/S01/T01");
62
-
63
- const result = stopAutoRemote(dir);
64
- assert.equal(result.found, false, "own PID must not be signalled");
65
-
66
- // The lock should be cleared as part of the self-detection cleanup
67
- assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "stale self-lock should be cleared");
68
- });
69
-
70
- test("#2730: stopAutoRemote clears stale lock from dead remote process without error", (t) => {
71
- const dir = makeTmpProject();
72
- t.after(() => rmSync(dir, { recursive: true, force: true }));
73
-
74
- // Simulate a stale lock from a process that no longer exists
75
- const lockData = {
76
- pid: 9999999,
77
- startedAt: "2026-03-01T00:00:00Z",
78
- unitType: "plan-slice",
79
- unitId: "M001/S02",
80
- unitStartedAt: "2026-03-01T00:05:00Z",
81
- };
82
- writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
83
-
84
- const result = stopAutoRemote(dir);
85
- assert.equal(result.found, false, "dead remote PID should not be reported as found");
86
- assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "stale lock should be cleaned up");
87
- });
@@ -1,159 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { mkdirSync, rmSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { tmpdir } from "node:os";
6
- import { randomUUID } from "node:crypto";
7
- import { spawn, type ChildProcess } from "node:child_process";
8
-
9
- import { writeFileSync } from "node:fs";
10
- import {
11
- writeLock,
12
- readCrashLock,
13
- clearLock,
14
- isLockProcessAlive,
15
- } from "../crash-recovery.ts";
16
- import { stopAutoRemote } from "../auto.ts";
17
-
18
- function makeTmpBase(): string {
19
- const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
20
- mkdirSync(join(base, ".gsd"), { recursive: true });
21
- return base;
22
- }
23
-
24
- function cleanup(base: string): void {
25
- try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
26
- }
27
-
28
- function waitForChildExit(child: ChildProcess, timeoutMs = 10000): Promise<number | null> {
29
- return new Promise((resolve) => {
30
- if (child.exitCode !== null) {
31
- resolve(child.exitCode);
32
- return;
33
- }
34
-
35
- const timeout = setTimeout(() => {
36
- child.off("exit", onExit);
37
- resolve(child.exitCode);
38
- }, timeoutMs);
39
-
40
- const onExit = (code: number | null) => {
41
- clearTimeout(timeout);
42
- resolve(code);
43
- };
44
-
45
- child.once("exit", onExit);
46
- });
47
- }
48
-
49
- // ─── stopAutoRemote ──────────────────────────────────────────────────────
50
-
51
- test("stopAutoRemote returns found:false when no lock file exists", () => {
52
- const base = makeTmpBase();
53
- try {
54
- const result = stopAutoRemote(base);
55
- assert.equal(result.found, false);
56
- assert.equal(result.pid, undefined);
57
- assert.equal(result.error, undefined);
58
- } finally {
59
- cleanup(base);
60
- }
61
- });
62
-
63
- test("stopAutoRemote cleans up stale lock (dead PID) and returns found:false", () => {
64
- const base = makeTmpBase();
65
- try {
66
- // Write a lock with a PID that doesn't exist
67
- writeLock(base, "execute-task", "M001/S01/T01");
68
- // Overwrite PID to a dead one
69
- const lock = readCrashLock(base)!;
70
- const staleData = { ...lock, pid: 999999999 };
71
- writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(staleData, null, 2), "utf-8");
72
-
73
- const result = stopAutoRemote(base);
74
- assert.equal(result.found, false, "stale lock should not be found as running");
75
-
76
- // Lock should be cleaned up
77
- assert.equal(readCrashLock(base), null, "stale lock should be removed");
78
- } finally {
79
- cleanup(base);
80
- }
81
- });
82
-
83
- // KNOWN FLAKE: This test is timing-sensitive — it spawns a child, writes a lock file,
84
- // sends SIGTERM, and asserts the child exited. Under heavy CI load the child may
85
- // not be ready when SIGTERM is sent. Mitigations: 500ms startup delay, 10s exit timeout.
86
- test("stopAutoRemote sends SIGTERM to a live process and returns found:true", { timeout: 15000 }, async () => {
87
- const base = makeTmpBase();
88
-
89
- // Spawn a child process that prints "ready" then sleeps, acting as a fake auto-mode session
90
- const child = spawn(
91
- process.execPath,
92
- ["-e", "process.on('SIGTERM', () => process.exit(0)); process.stdout.write('ready'); setTimeout(() => process.exit(1), 30000);"],
93
- { stdio: ["ignore", "pipe", "ignore"], detached: false },
94
- );
95
-
96
- if (!child.pid) {
97
- throw new Error("failed to spawn child process for stopAutoRemote test");
98
- }
99
-
100
- try {
101
- // Wait for child to signal readiness via stdout
102
- await new Promise<void>((resolve) => {
103
- child.stdout!.once("data", () => resolve());
104
- setTimeout(resolve, 2000); // fallback timeout
105
- });
106
-
107
- // Write lock with child's PID
108
- const lockData = {
109
- pid: child.pid,
110
- startedAt: new Date().toISOString(),
111
- unitType: "execute-task",
112
- unitId: "M001/S01/T01",
113
- unitStartedAt: new Date().toISOString(),
114
- };
115
- writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2), "utf-8");
116
-
117
- const exitPromise = waitForChildExit(child);
118
- const result = stopAutoRemote(base);
119
- assert.equal(result.found, true, "should find running auto-mode");
120
- assert.equal(result.pid, child.pid, "should return the PID");
121
-
122
- // Wait for child to exit (it should receive SIGTERM)
123
- const exitCode = await exitPromise;
124
- // On Windows, SIGTERM is not interceptable — the process exits with code 1
125
- // rather than running the handler. Accept either clean exit (0) or forced (1).
126
- assert.ok(exitCode !== null, "child should have exited after SIGTERM");
127
- if (process.platform !== "win32") {
128
- assert.equal(exitCode, 0, "child should have exited cleanly via SIGTERM");
129
- }
130
- } finally {
131
- try { child.kill("SIGKILL"); } catch { /* already dead */ }
132
- cleanup(base);
133
- }
134
- });
135
-
136
- // ─── Lock path: original project root vs worktree ────────────────────────
137
-
138
- test("lock file should be discoverable from project root and worktree path", () => {
139
- const projectRoot = makeTmpBase();
140
- const worktreePath = join(projectRoot, ".gsd", "worktrees", "M001");
141
- mkdirSync(join(worktreePath, ".gsd"), { recursive: true });
142
-
143
- try {
144
- // Simulate: auto-mode writes lock to project root (the fix)
145
- writeLock(projectRoot, "execute-task", "M001/S01/T01");
146
-
147
- // Second terminal checks project root — should find the lock
148
- const lock = readCrashLock(projectRoot);
149
- assert.ok(lock, "lock should be found at project root");
150
- assert.equal(lock!.unitType, "execute-task");
151
-
152
- // Worktree path resolves to the same canonical project .gsd lock.
153
- const worktreeLock = readCrashLock(worktreePath);
154
- assert.ok(worktreeLock, "lock should be discoverable via worktree path");
155
- assert.equal(worktreeLock!.unitType, "execute-task");
156
- } finally {
157
- cleanup(projectRoot);
158
- }
159
- });