gsd-pi 2.80.0-dev.c5c38454b → 2.80.0-dev.cf9433f56

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 (266) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +2 -2
  4. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +66 -45
  6. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  8. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
  9. package/dist/resources/extensions/gsd/auto-prompts.js +124 -2
  10. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  11. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  12. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  13. package/dist/resources/extensions/gsd/auto.js +77 -5
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  15. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  17. package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
  18. package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
  19. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  20. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  22. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  23. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  24. package/dist/resources/extensions/gsd/detection.js +106 -0
  25. package/dist/resources/extensions/gsd/git-service.js +36 -4
  26. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  27. package/dist/resources/extensions/gsd/guided-flow.js +80 -14
  28. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  29. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  30. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  31. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  32. package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
  33. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  34. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +19 -19
  35. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  39. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  40. package/dist/resources/extensions/gsd/quick.js +34 -2
  41. package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
  42. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  43. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  44. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  45. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  46. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  47. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  48. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  49. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  50. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  51. package/dist/resources/extensions/gsd/unit-runtime.js +22 -0
  52. package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
  53. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  54. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  55. package/dist/web/standalone/.next/BUILD_ID +1 -1
  56. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  57. package/dist/web/standalone/.next/build-manifest.json +2 -2
  58. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  59. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.html +1 -1
  76. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  83. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  85. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  86. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  87. package/package.json +3 -3
  88. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  90. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  91. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  92. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  93. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  94. package/packages/native/tsconfig.tsbuildinfo +1 -1
  95. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  96. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  99. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +32 -0
  101. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  103. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  105. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  109. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  111. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  113. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  117. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  118. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  119. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  121. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  123. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  125. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  128. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  130. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +15 -5
  137. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  139. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  144. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  145. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  146. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  147. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  149. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  150. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  151. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  152. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +38 -0
  153. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  154. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  155. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  156. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  157. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  158. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  159. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  160. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  161. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  162. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  163. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +24 -7
  164. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  165. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  166. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  167. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  168. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  169. package/packages/pi-tui/dist/tui.js +18 -8
  170. package/packages/pi-tui/dist/tui.js.map +1 -1
  171. package/packages/pi-tui/src/tui.ts +20 -8
  172. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  173. package/src/resources/GSD-WORKFLOW.md +2 -2
  174. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  175. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  176. package/src/resources/extensions/gsd/auto/phases.ts +77 -48
  177. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  178. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  179. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
  180. package/src/resources/extensions/gsd/auto-prompts.ts +133 -2
  181. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  182. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  183. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  184. package/src/resources/extensions/gsd/auto.ts +92 -4
  185. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  186. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  187. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  188. package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
  189. package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
  190. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  191. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  192. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  193. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  194. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  195. package/src/resources/extensions/gsd/detection.ts +128 -0
  196. package/src/resources/extensions/gsd/git-service.ts +46 -8
  197. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  198. package/src/resources/extensions/gsd/guided-flow.ts +96 -14
  199. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  200. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  201. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  202. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  203. package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
  204. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  205. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  206. package/src/resources/extensions/gsd/prompts/complete-milestone.md +19 -19
  207. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  208. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  209. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  210. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  211. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  212. package/src/resources/extensions/gsd/quick.ts +37 -2
  213. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
  214. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +72 -1
  215. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  216. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  217. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
  218. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  219. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  220. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  221. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  222. package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
  223. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  224. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  225. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  226. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  227. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  228. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  229. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  230. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  231. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  232. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  233. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
  234. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  235. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  236. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  237. package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
  238. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
  239. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  240. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  241. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
  242. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  243. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +101 -2
  244. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  245. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  246. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  247. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +37 -0
  248. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  249. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
  250. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
  251. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
  252. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  253. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  254. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  255. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  256. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  257. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  258. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  259. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  260. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  261. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  262. package/src/resources/extensions/gsd/unit-runtime.ts +25 -0
  263. package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
  264. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  265. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → -5nHJWzSdG-WkPMul_khA}/_buildManifest.js +0 -0
  266. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → -5nHJWzSdG-WkPMul_khA}/_ssgManifest.js +0 -0
@@ -3,6 +3,9 @@ import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
 
6
+ import { _withDetachedAutoKeepaliveForTest } from "../auto.ts";
7
+ import { _scheduleAutoStartAfterIdleForTest } from "../guided-flow.ts";
8
+
6
9
  const gsdDir = resolve(import.meta.dirname, "..");
7
10
 
8
11
  function readGsdFile(relativePath: string): string {
@@ -94,6 +97,23 @@ test("auto bootstrap validates blocked directories before touching .gsd migratio
94
97
  );
95
98
  });
96
99
 
100
+ test("fresh start registers the auto worker before bootstrap enters worktree flow (#5405)", () => {
101
+ const autoSrc = readGsdFile("auto.ts");
102
+ const startAutoIdx = autoSrc.indexOf("export async function startAuto(");
103
+ const startAutoBody = autoSrc.slice(startAutoIdx);
104
+
105
+ const preBootstrapRegisterIdx = startAutoBody.indexOf("registerAutoWorkerForSession(s, base);");
106
+ const bootstrapCallIdx = startAutoBody.indexOf("const ready = await bootstrapAutoSession(");
107
+
108
+ assert.ok(startAutoIdx > -1, "startAuto should exist");
109
+ assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
110
+ assert.ok(bootstrapCallIdx > -1, "startAuto should call bootstrapAutoSession");
111
+ assert.ok(
112
+ preBootstrapRegisterIdx < bootstrapCallIdx,
113
+ "worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
114
+ );
115
+ });
116
+
97
117
  test("startAutoDetached reports failures asynchronously (#3733)", () => {
98
118
  const autoSrc = readGsdFile("auto.ts");
99
119
 
@@ -102,8 +122,8 @@ test("startAutoDetached reports failures asynchronously (#3733)", () => {
102
122
  "auto.ts should export startAutoDetached",
103
123
  );
104
124
  assert.ok(
105
- autoSrc.includes("void startAuto(ctx, pi, base, verboseMode, options).catch"),
106
- "startAutoDetached should launch startAuto without awaiting it",
125
+ autoSrc.includes("void withDetachedAutoKeepalive(startAuto(ctx, pi, base, verboseMode, options)).catch"),
126
+ "startAutoDetached should launch startAuto without awaiting it and keep the process alive",
107
127
  );
108
128
  assert.ok(
109
129
  autoSrc.includes("ctx.ui.notify(`Auto-start failed: ${message}`, \"error\")"),
@@ -111,6 +131,48 @@ test("startAutoDetached reports failures asynchronously (#3733)", () => {
111
131
  );
112
132
  });
113
133
 
134
+ test("detached auto-start keeps a ref'ed handle until the run settles", async () => {
135
+ const originalSetInterval = globalThis.setInterval;
136
+ const originalClearInterval = globalThis.clearInterval;
137
+ let intervalCreated = false;
138
+ let intervalCleared = false;
139
+ let createdHandle: NodeJS.Timeout | undefined;
140
+ let resolveRun!: () => void;
141
+ const run = new Promise<void>((resolve) => {
142
+ resolveRun = resolve;
143
+ });
144
+
145
+ globalThis.setInterval = ((handler: TimerHandler, timeout?: number, ...args: unknown[]) => {
146
+ intervalCreated = true;
147
+ assert.equal(timeout, 30_000);
148
+ void handler;
149
+ void args;
150
+ createdHandle = originalSetInterval(() => {}, 1_000_000);
151
+ assert.equal(createdHandle.hasRef(), true, "detached auto keepalive must be ref'ed");
152
+ return createdHandle;
153
+ }) as unknown as typeof setInterval;
154
+
155
+ globalThis.clearInterval = ((handle?: NodeJS.Timeout | number | string) => {
156
+ if (handle === createdHandle) intervalCleared = true;
157
+ return originalClearInterval(handle);
158
+ }) as unknown as typeof clearInterval;
159
+
160
+ try {
161
+ const heldRun = _withDetachedAutoKeepaliveForTest(run);
162
+ assert.equal(intervalCreated, true, "keepalive interval should start immediately");
163
+ assert.equal(intervalCleared, false, "keepalive should remain active while auto-mode is running");
164
+
165
+ resolveRun();
166
+ await heldRun;
167
+
168
+ assert.equal(intervalCleared, true, "keepalive interval should clear when auto-mode settles");
169
+ } finally {
170
+ if (createdHandle) originalClearInterval(createdHandle);
171
+ globalThis.setInterval = originalSetInterval;
172
+ globalThis.clearInterval = originalClearInterval;
173
+ }
174
+ });
175
+
114
176
  test("detached auto-start preserves milestone lock across pause/stop cleanup (#3733)", () => {
115
177
  const autoSrc = readGsdFile("auto.ts");
116
178
  const sessionSrc = readGsdFile("auto/session.ts");
@@ -141,3 +203,40 @@ test("detached auto-start preserves milestone lock across pause/stop cleanup (#3
141
203
  "AutoSession should track the detached milestone lock explicitly",
142
204
  );
143
205
  });
206
+
207
+ test("discussion auto-start waits for the current command context to become idle", async () => {
208
+ let releaseIdle!: () => void;
209
+ const idle = new Promise<void>((resolveIdle) => {
210
+ releaseIdle = resolveIdle;
211
+ });
212
+ const launches: unknown[][] = [];
213
+ const ctx = {
214
+ waitForIdle: () => idle,
215
+ ui: {
216
+ notify: () => {},
217
+ },
218
+ } as any;
219
+
220
+ _scheduleAutoStartAfterIdleForTest(
221
+ ctx,
222
+ {} as any,
223
+ "/tmp/gsd-auto-start-idle-test",
224
+ false,
225
+ { step: true },
226
+ (...args: unknown[]) => {
227
+ launches.push(args);
228
+ },
229
+ );
230
+
231
+ await Promise.resolve();
232
+ assert.equal(launches.length, 0, "auto-start must not launch before waitForIdle resolves");
233
+
234
+ releaseIdle();
235
+ await Promise.resolve();
236
+ assert.equal(launches.length, 0, "auto-start should defer launch to the next timer turn");
237
+
238
+ await new Promise((resolveTimer) => setTimeout(resolveTimer, 0));
239
+ assert.equal(launches.length, 1);
240
+ assert.equal(launches[0][2], "/tmp/gsd-auto-start-idle-test");
241
+ assert.deepEqual(launches[0][4], { step: true });
242
+ });
@@ -44,4 +44,13 @@ describe('status opens DB before deriveState (#3691)', () => {
44
44
  assert.match(quickSrc, /getIsolationMode\(\)\s*!==\s*"none"/,
45
45
  'quick.ts should compare isolation mode against "none"');
46
46
  });
47
+
48
+ test('quick task prompt handles external .gsd without staging quick files', () => {
49
+ assert.match(quickSrc, /isExternalGsdRoot/,
50
+ 'quick.ts should detect whether .gsd resolves outside the project repo');
51
+ assert.match(quickSrc, /do not stage or commit `\.gsd\/quick\/\.\.\.`/,
52
+ 'external-state quick tasks must tell the agent not to stage .gsd/quick files');
53
+ assert.match(quickSrc, /nothing in the project repo to commit/,
54
+ 'external-state quick tasks should allow summary-only work without a git commit');
55
+ });
47
56
  });
@@ -21,12 +21,18 @@ import type {
21
21
  ComputedArtifactRegistry,
22
22
  UnitContextManifest,
23
23
  } from "../unit-context-manifest.ts";
24
- import { UNIT_MANIFESTS } from "../unit-context-manifest.ts";
25
- import { buildReassessRoadmapPrompt } from "../auto-prompts.ts";
24
+ import { KNOWN_UNIT_TYPES, UNIT_MANIFESTS } from "../unit-context-manifest.ts";
25
+ import {
26
+ buildExecuteTaskPrompt,
27
+ buildGateEvaluatePrompt,
28
+ buildReassessRoadmapPrompt,
29
+ buildWorkflowPreferencesPrompt,
30
+ } from "../auto-prompts.ts";
26
31
  import { invalidateAllCaches } from "../cache.ts";
27
32
  import {
28
33
  openDatabase,
29
34
  closeDatabase,
35
+ insertGateRow,
30
36
  insertMilestone,
31
37
  upsertMilestonePlanning,
32
38
  insertSlice,
@@ -121,7 +127,7 @@ test("Context Mode composer: standalone output starts with heading and includes
121
127
  assert.ok(out.startsWith("## Context Mode"));
122
128
  assert.match(out, /execution lane/i);
123
129
  assert.match(out, /`gsd_exec`/);
124
- assert.match(out, /noisy commands/);
130
+ assert.match(out, /builds, tests, and diagnostics/);
125
131
  assert.match(out, /`gsd_exec_search`/);
126
132
  assert.match(out, /before reruns/);
127
133
  assert.match(out, /`gsd_resume`/);
@@ -136,7 +142,45 @@ test("Context Mode composer: nested output is compact single sentence", () => {
136
142
  assert.match(out, /`gsd_exec`/);
137
143
  assert.match(out, /`gsd_exec_search`/);
138
144
  assert.match(out, /`gsd_resume`/);
139
- assert.ok(out.length < 180, `nested guidance should stay compact, got ${out.length} chars`);
145
+ assert.ok(out.length < 240, `nested guidance should stay compact, got ${out.length} chars`);
146
+ });
147
+
148
+ const laneLabelByMode: Record<string, string> = {
149
+ interview: "interview",
150
+ research: "research",
151
+ planning: "planning",
152
+ execution: "execution",
153
+ verification: "verification",
154
+ orchestration: "orchestration",
155
+ docs: "documentation",
156
+ };
157
+
158
+ test("Context Mode composer: every known eligible unit renders its configured lane and required tools", () => {
159
+ for (const unitType of KNOWN_UNIT_TYPES) {
160
+ const manifest = UNIT_MANIFESTS[unitType];
161
+ assert.ok(manifest, `missing manifest for ${unitType}`);
162
+ const out = composeContextModeInstructions(unitType, { enabled: true, renderMode: "standalone" });
163
+ if (manifest.contextMode === "none") {
164
+ assert.strictEqual(out, "", `${unitType} should not render Context Mode`);
165
+ continue;
166
+ }
167
+ assert.ok(out.startsWith("## Context Mode"), `${unitType} should render standalone Context Mode heading`);
168
+ assert.match(out, new RegExp(`Lane: \\*\\*${laneLabelByMode[manifest.contextMode]} lane\\*\\*\\.`, "i"));
169
+ assert.match(out, /`gsd_exec`/, `${unitType} should mention gsd_exec`);
170
+ assert.match(out, /`gsd_exec_search`/, `${unitType} should mention gsd_exec_search`);
171
+ assert.match(out, /`gsd_resume`/, `${unitType} should mention gsd_resume`);
172
+ }
173
+ });
174
+
175
+ test("Context Mode composer: workflow-preferences and research-decision render no Context Mode block", () => {
176
+ assert.strictEqual(
177
+ composeContextModeInstructions("workflow-preferences", { enabled: true, renderMode: "standalone" }),
178
+ "",
179
+ );
180
+ assert.strictEqual(
181
+ composeContextModeInstructions("research-decision", { enabled: true, renderMode: "standalone" }),
182
+ "",
183
+ );
140
184
  });
141
185
 
142
186
  // ─── Integration: migrated buildReassessRoadmapPrompt ─────────────────────
@@ -220,6 +264,94 @@ test("#4782 phase 2: buildReassessRoadmapPrompt emits composer-shaped context wi
220
264
  assert.ok(!prompt.includes("Slice Context (from discussion)"));
221
265
  });
222
266
 
267
+ test("Context Mode resume injection: eligible prompts include one bounded snapshot block above inlined context", async (t) => {
268
+ const base = makeFixtureBase();
269
+ t.after(() => cleanup(base));
270
+ invalidateAllCaches();
271
+
272
+ seed(base, "M001");
273
+ writeArtifacts(base);
274
+ writeFileSync(
275
+ join(base, ".gsd", "last-snapshot.md"),
276
+ "# GSD context snapshot\n\nResume evidence.\n",
277
+ "utf-8",
278
+ );
279
+
280
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
281
+
282
+ assert.equal(prompt.match(/## Context Snapshot/g)?.length, 1);
283
+ assert.match(prompt, /Source: `\.gsd\/last-snapshot\.md`/);
284
+ assert.match(prompt, /Resume evidence/);
285
+ assert.ok(prompt.indexOf("## Context Mode") < prompt.indexOf("## Context Snapshot"));
286
+ assert.ok(prompt.indexOf("## Context Snapshot") < prompt.indexOf("## Inlined Context"));
287
+ });
288
+
289
+ test("Context Mode resume injection: missing snapshot does not add an empty block", async (t) => {
290
+ const base = makeFixtureBase();
291
+ t.after(() => cleanup(base));
292
+ invalidateAllCaches();
293
+
294
+ seed(base, "M001");
295
+ writeArtifacts(base);
296
+
297
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
298
+
299
+ assert.match(prompt, /## Context Mode/);
300
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
301
+ });
302
+
303
+ test("Context Mode resume injection: disabled mode suppresses guidance and snapshot reads", async (t) => {
304
+ const base = makeFixtureBase();
305
+ t.after(() => cleanup(base));
306
+ invalidateAllCaches();
307
+
308
+ seed(base, "M001");
309
+ writeArtifacts(base);
310
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\ncontext_mode:\n enabled: false\n---\n", "utf-8");
311
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nDo not inject.\n", "utf-8");
312
+
313
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
314
+
315
+ assert.doesNotMatch(prompt, /## Context Mode/);
316
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
317
+ assert.doesNotMatch(prompt, /Do not inject/);
318
+ });
319
+
320
+ test("Context Mode resume injection: none-mode units do not inject snapshots", async () => {
321
+ const base = makeFixtureBase();
322
+ try {
323
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nNo lane.\n", "utf-8");
324
+ const prompt = await buildWorkflowPreferencesPrompt(base);
325
+ assert.doesNotMatch(prompt, /## Context Mode/);
326
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
327
+ assert.doesNotMatch(prompt, /No lane/);
328
+ } finally {
329
+ cleanup(base);
330
+ }
331
+ });
332
+
333
+ test("Context Mode prompt suppression: disabled inlined, phase-anchor, and nested prompts omit Context Mode", async (t) => {
334
+ const base = makeFixtureBase();
335
+ t.after(() => cleanup(base));
336
+ invalidateAllCaches();
337
+
338
+ seed(base, "M001");
339
+ writeArtifacts(base);
340
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
341
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\ncontext_mode:\n enabled: false\n---\n", "utf-8");
342
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nDo not inject.\n", "utf-8");
343
+
344
+ const inlinedPrompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
345
+ assert.doesNotMatch(inlinedPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
346
+
347
+ const phaseAnchorPrompt = await buildExecuteTaskPrompt("M001", "S01", "First", "T01", "Task", base);
348
+ assert.doesNotMatch(phaseAnchorPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
349
+
350
+ const nestedPrompt = await buildGateEvaluatePrompt("M001", "Test", "S01", "First", base);
351
+ assert.match(nestedPrompt, /Use this as the prompt for a `subagent` call/);
352
+ assert.doesNotMatch(nestedPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
353
+ });
354
+
223
355
  // ─── v2 surface (#4924) ───────────────────────────────────────────────────
224
356
 
225
357
  const fakeBase: BaseResolverContext = {
@@ -21,6 +21,7 @@ import {
21
21
  markFailed,
22
22
  markStuck,
23
23
  markCanceled,
24
+ markLatestActiveForWorkerCanceled,
24
25
  getRecentForUnit,
25
26
  getLatestForUnit,
26
27
  } from "../db/unit-dispatches.ts";
@@ -196,6 +197,35 @@ test("markStuck and markCanceled set their respective statuses", (t) => {
196
197
  assert.equal(getLatestForUnit("M001/S01/T01")!.status, "canceled");
197
198
  });
198
199
 
200
+ test("markLatestActiveForWorkerCanceled cancels only the latest active dispatch for a worker", (t) => {
201
+ const base = makeBase();
202
+ t.after(() => cleanup(base));
203
+ const { workerId, leaseToken } = setup(base);
204
+
205
+ const first = recordDispatchClaim({
206
+ traceId: "tc-1", workerId, milestoneLeaseToken: leaseToken,
207
+ milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
208
+ });
209
+ assert.equal(first.ok, true);
210
+ if (!first.ok) return;
211
+ markCompleted(first.dispatchId);
212
+
213
+ const second = recordDispatchClaim({
214
+ traceId: "tc-2", workerId, milestoneLeaseToken: leaseToken,
215
+ milestoneId: "M001", unitType: "run-task", unitId: "M001/S01/T01",
216
+ });
217
+ assert.equal(second.ok, true);
218
+ if (!second.ok) return;
219
+ markRunning(second.dispatchId);
220
+
221
+ assert.equal(markLatestActiveForWorkerCanceled(workerId, "signal-exit"), true);
222
+ assert.equal(getLatestForUnit("M001/S01")!.status, "completed");
223
+ const latest = getLatestForUnit("M001/S01/T01")!;
224
+ assert.equal(latest.status, "canceled");
225
+ assert.equal(latest.exit_reason, "signal-exit");
226
+ assert.equal(markLatestActiveForWorkerCanceled(workerId, "signal-exit"), false);
227
+ });
228
+
199
229
  test("terminal transitions do not overwrite an already terminal dispatch", (t) => {
200
230
  const base = makeBase();
201
231
  t.after(() => cleanup(base));
@@ -5,9 +5,11 @@ import {
5
5
  clearUnitRuntimeRecord,
6
6
  formatExecuteTaskRecoveryStatus,
7
7
  inspectExecuteTaskDurability,
8
+ isInFlightRuntimePhase,
8
9
  readUnitRuntimeRecord,
9
10
  writeUnitRuntimeRecord,
10
11
  } from "../unit-runtime.ts";
12
+ import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from "../gsd-db.ts";
11
13
  import { clearPathCache } from '../paths.ts';
12
14
  import { test } from 'node:test';
13
15
  import assert from 'node:assert/strict';
@@ -22,6 +24,12 @@ writeFileSync(
22
24
  "utf-8",
23
25
  );
24
26
 
27
+ console.log("\n=== in-flight runtime phases ===");
28
+ {
29
+ assert.equal(isInFlightRuntimePhase("crashed"), true, "crashed records remain recoverable");
30
+ assert.equal(isInFlightRuntimePhase("finalized"), false, "finalized records are terminal");
31
+ }
32
+
25
33
  console.log("\n=== runtime record write/read/update ===");
26
34
  {
27
35
  const first = writeUnitRuntimeRecord(base, "execute-task", "M100/S02/T09", 1000, { phase: "dispatched" });
@@ -65,6 +73,35 @@ console.log("\n=== runtime record cleanup ===");
65
73
  assert.deepStrictEqual(loaded, null, "record removed");
66
74
  }
67
75
 
76
+ console.log("\n=== execute-task durability trusts closed DB task status ===");
77
+ {
78
+ const dbBase = mkdtempSync(join(tmpdir(), "gsd-unit-runtime-db-test-"));
79
+ mkdirSync(join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "tasks"), { recursive: true });
80
+ try {
81
+ openDatabase(join(dbBase, ".gsd", "gsd.db"));
82
+ insertMilestone({ id: "M300", title: "DB Milestone", status: "active" });
83
+ insertSlice({ id: "S01", milestoneId: "M300", title: "DB Slice", status: "in_progress" });
84
+ insertTask({ id: "T01", milestoneId: "M300", sliceId: "S01", title: "DB Task", status: "complete" });
85
+ writeFileSync(
86
+ join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "S01-PLAN.md"),
87
+ "# S01\n\n## Tasks\n\n- [ ] **T01: DB Task** `est:10m`\n",
88
+ "utf-8",
89
+ );
90
+ writeFileSync(join(dbBase, ".gsd", "STATE.md"), "## Next Action\nExecute T01 for S01: DB task\n", "utf-8");
91
+
92
+ const status = await inspectExecuteTaskDurability(dbBase, "M300/S01/T01");
93
+ assert.ok(status !== null, "db-complete: status exists");
94
+ assert.equal(status!.dbComplete, true, "db-complete: closed DB status is captured");
95
+ assert.equal(status!.summaryExists, false, "db-complete: summary can still be missing");
96
+ assert.equal(status!.taskChecked, false, "db-complete: checkbox can still be unchecked");
97
+ assert.equal(status!.nextActionAdvanced, false, "db-complete: next action can still point at task");
98
+ assert.equal(formatExecuteTaskRecoveryStatus(status!), "DB task status is closed");
99
+ } finally {
100
+ closeDatabase();
101
+ rmSync(dbBase, { recursive: true, force: true });
102
+ }
103
+ }
104
+
68
105
  console.log("\n=== hook unit type sanitization (slash in unitType) ===");
69
106
  {
70
107
  // Hook units have unitType like "hook/code-review" with a slash
@@ -165,6 +165,9 @@ test("executeMilestoneStatus returns milestone metadata and slice counts", async
165
165
  assert.equal(parsed.sliceCount, 1);
166
166
  assert.equal(parsed.slices[0].id, "S01");
167
167
  assert.equal(parsed.slices[0].taskCounts.pending, 1);
168
+ assert.equal(result.details.status, "active");
169
+ assert.equal(result.details.title, "Milestone One");
170
+ assert.deepEqual(result.details.slices, parsed.slices);
168
171
  } finally {
169
172
  closeDatabase();
170
173
  cleanup(base);
@@ -9,12 +9,12 @@
9
9
 
10
10
  import { describe, test, beforeEach, afterEach } from "node:test";
11
11
  import assert from "node:assert/strict";
12
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readdirSync } from "node:fs";
12
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { tmpdir } from "node:os";
15
15
  import { execSync } from "node:child_process";
16
16
 
17
- import { PROJECT_FILES } from "../detection.js";
17
+ import { PROJECT_FILES, classifyProject } from "../detection.js";
18
18
 
19
19
  // ─── Helpers ─────────────────────────────────────────────────────────────────
20
20
 
@@ -30,6 +30,14 @@ function createGitRepo(): string {
30
30
  return dir;
31
31
  }
32
32
 
33
+ function createEmptyGitRepo(): string {
34
+ const dir = mkdtempSync(join(tmpdir(), "wt-dispatch-test-empty-"));
35
+ execSync("git init", { cwd: dir, stdio: "ignore" });
36
+ execSync("git config user.email test@test.com", { cwd: dir, stdio: "ignore" });
37
+ execSync("git config user.name Test", { cwd: dir, stdio: "ignore" });
38
+ return dir;
39
+ }
40
+
33
41
  /**
34
42
  * Simulate the health check logic from auto/phases.ts.
35
43
  *
@@ -64,8 +72,6 @@ function hasXcodeBundle(basePath: string): boolean {
64
72
  } catch { return false; }
65
73
  }
66
74
 
67
- import { existsSync } from "node:fs";
68
-
69
75
  // ─── Tests ───────────────────────────────────────────────────────────────────
70
76
 
71
77
  test("PROJECT_FILES is exported and contains expected multi-ecosystem entries", () => {
@@ -80,6 +86,21 @@ test("PROJECT_FILES is exported and contains expected multi-ecosystem entries",
80
86
  assert.ok(PROJECT_FILES.includes("Package.swift"), "includes Swift marker");
81
87
  });
82
88
 
89
+ test("runUnitPhase fails closed when classification returns invalid-repo", () => {
90
+ const source = readFileSync(join(process.cwd(), "src/resources/extensions/gsd/auto/phases.ts"), "utf-8");
91
+ const invalidRepoBranch = source.slice(
92
+ source.indexOf('projectClassification.kind === "invalid-repo"'),
93
+ source.indexOf('projectClassification.kind === "greenfield"'),
94
+ );
95
+
96
+ assert.match(invalidRepoBranch, /projectClassification\.reason === "missing \.git" && hasGit/);
97
+ assert.match(invalidRepoBranch, /project classification could not confirm \.git/);
98
+ assert.match(invalidRepoBranch, /ctx\.ui\.notify\(msg,\s*"error"\)/);
99
+ assert.match(invalidRepoBranch, /await deps\.stopAuto\(ctx,\s*pi,\s*msg\)/);
100
+ assert.match(invalidRepoBranch, /return \{ action: "break", reason: "worktree-invalid" \}/);
101
+ assert.match(invalidRepoBranch, /classified as invalid-repo/);
102
+ });
103
+
83
104
  describe("health check with git repo", () => {
84
105
  let dir: string;
85
106
  beforeEach(() => { dir = createGitRepo(); });
@@ -132,8 +153,18 @@ describe("health check with git repo", () => {
132
153
  });
133
154
 
134
155
  test("health check passes for empty git repo (greenfield project)", () => {
135
- assert.ok(wouldPassHealthCheck(dir, existsSync), "empty git repo should pass health check (greenfield)");
136
- assert.ok(!hasRecognizedProjectFiles(dir, existsSync), "empty git repo has no recognized project files");
156
+ const empty = createEmptyGitRepo();
157
+ try {
158
+ assert.ok(wouldPassHealthCheck(empty, existsSync), "empty git repo should pass health check (greenfield)");
159
+ assert.equal(classifyProject(empty).kind, "greenfield");
160
+ } finally {
161
+ rmSync(empty, { recursive: true, force: true });
162
+ }
163
+ });
164
+
165
+ test("health check classifies README-only repo as untyped existing, not greenfield", () => {
166
+ assert.ok(wouldPassHealthCheck(dir, existsSync), "README-only repo should pass health check");
167
+ assert.equal(classifyProject(dir).kind, "untyped-existing");
137
168
  });
138
169
  });
139
170
 
@@ -235,4 +235,11 @@ describe("removeWorktree — missing worktree", () => {
235
235
  "should not throw when worktree does not exist",
236
236
  );
237
237
  });
238
+
239
+ test("deleteBranch is quiet when the branch is already gone", () => {
240
+ assert.doesNotThrow(
241
+ () => removeWorktree(base, "nonexistent", { deleteBranch: true }),
242
+ "missing branch should be treated as already cleaned up",
243
+ );
244
+ });
238
245
  });
@@ -56,11 +56,11 @@ test("#2616: findNestedGitDirs ignores .git files (worktree pointers)", (t) => {
56
56
  );
57
57
  });
58
58
 
59
- test("#2616: findNestedGitDirs skips excluded directories (node_modules, .gsd, target)", (t) => {
59
+ test("#2616: findNestedGitDirs skips excluded directories (node_modules, .gsd, .bg-shell, target)", (t) => {
60
60
  const root = makeRoot(t);
61
61
 
62
62
  // All three of these contain a .git *directory*, but the scan must skip them.
63
- for (const excluded of ["node_modules", ".gsd", "target"]) {
63
+ for (const excluded of ["node_modules", ".gsd", ".bg-shell", "target"]) {
64
64
  const inside = join(root, excluded, "vendored-pkg");
65
65
  mkdirSync(join(inside, ".git"), { recursive: true });
66
66
  }
@@ -73,6 +73,13 @@ test("#2616: findNestedGitDirs skips excluded directories (node_modules, .gsd, t
73
73
  );
74
74
  });
75
75
 
76
+ test("#2616: findNestedGitDirs ignores normal missing child .git probes", (t) => {
77
+ const root = makeRoot(t);
78
+ mkdirSync(join(root, "plain-dir"), { recursive: true });
79
+
80
+ assert.deepEqual(findNestedGitDirs(root), []);
81
+ });
82
+
76
83
  test("#2616: findNestedGitDirs finds deeply nested repos", (t) => {
77
84
  const root = makeRoot(t);
78
85
  const deep = join(root, "a", "b", "c", "scaffolded");
@@ -1,3 +1,5 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: WorktreeResolver unit and regression tests.
1
3
  import test from "node:test";
2
4
  import assert from "node:assert/strict";
3
5
  import { mkdtempSync, rmSync, mkdirSync, realpathSync } from "node:fs";
@@ -9,6 +11,17 @@ import {
9
11
  type NotifyCtx,
10
12
  } from "../worktree-resolver.js";
11
13
  import { AutoSession } from "../auto/session.js";
14
+ import {
15
+ closeDatabase,
16
+ insertMilestone,
17
+ openDatabase,
18
+ } from "../gsd-db.js";
19
+ import { registerAutoWorker } from "../db/auto-workers.js";
20
+ import {
21
+ claimMilestoneLease,
22
+ getMilestoneLease,
23
+ releaseMilestoneLease,
24
+ } from "../db/milestone-leases.js";
12
25
 
13
26
  // ─── Helpers ─────────────────────────────────────────────────────────────────
14
27
 
@@ -19,11 +32,12 @@ interface CallLog {
19
32
  }
20
33
 
21
34
  function makeSession(
22
- overrides?: Partial<{ basePath: string; originalBasePath: string }>,
35
+ overrides?: Partial<AutoSession>,
23
36
  ): AutoSession {
24
37
  const s = new AutoSession();
25
38
  s.basePath = overrides?.basePath ?? "/project";
26
39
  s.originalBasePath = overrides?.originalBasePath ?? "/project";
40
+ Object.assign(s, overrides);
27
41
  return s;
28
42
  }
29
43
 
@@ -182,6 +196,17 @@ function findCalls(calls: CallLog[], fn: string): CallLog[] {
182
196
  return calls.filter((c) => c.fn === fn);
183
197
  }
184
198
 
199
+ function makeDbBase(): string {
200
+ const base = mkdtempSync(join(tmpdir(), "gsd-worktree-resolver-"));
201
+ mkdirSync(join(base, ".gsd"), { recursive: true });
202
+ return base;
203
+ }
204
+
205
+ function cleanupDbBase(base: string): void {
206
+ try { closeDatabase(); } catch { /* noop */ }
207
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
208
+ }
209
+
185
210
  // ─── Getter Tests ────────────────────────────────────────────────────────────
186
211
 
187
212
  test("workPath returns s.basePath", () => {
@@ -371,6 +396,43 @@ test("enterMilestone does not create double-nested worktree when originalBasePat
371
396
  );
372
397
  });
373
398
 
399
+ test("enterMilestone reacquires a released same-milestone lease before worktree entry", (t) => {
400
+ const base = makeDbBase();
401
+ t.after(() => cleanupDbBase(base));
402
+ openDatabase(join(base, ".gsd", "gsd.db"));
403
+ insertMilestone({ id: "M001", title: "Test milestone", status: "active" });
404
+
405
+ const workerId = registerAutoWorker({ projectRootRealpath: base });
406
+ const originalClaim = claimMilestoneLease(workerId, "M001");
407
+ assert.equal(originalClaim.ok, true);
408
+ if (!originalClaim.ok) throw new Error("expected test lease claim");
409
+ assert.equal(releaseMilestoneLease(workerId, "M001", originalClaim.token), true);
410
+
411
+ const s = makeSession({
412
+ basePath: base,
413
+ originalBasePath: base,
414
+ workerId,
415
+ currentMilestoneId: "M001",
416
+ milestoneLeaseToken: originalClaim.token,
417
+ });
418
+ const deps = makeDeps({
419
+ createAutoWorktree: (basePath: string, milestoneId: string) => join(basePath, ".gsd", "worktrees", milestoneId),
420
+ });
421
+ const ctx = makeNotifyCtx();
422
+ const resolver = new WorktreeResolver(s, deps);
423
+
424
+ resolver.enterMilestone("M001", ctx);
425
+
426
+ const row = getMilestoneLease("M001");
427
+ assert.ok(row);
428
+ assert.equal(row.worker_id, workerId);
429
+ assert.equal(row.status, "held");
430
+ assert.equal(row.fencing_token, originalClaim.token + 1);
431
+ assert.equal(s.milestoneLeaseToken, originalClaim.token + 1);
432
+ assert.equal(s.basePath, join(base, ".gsd", "worktrees", "M001"));
433
+ assert.equal(ctx.messages.some((m) => m.level === "error"), false);
434
+ });
435
+
374
436
  // ─── enterMilestone Tests (branch mode) ──────────────────────────────────────
375
437
 
376
438
  test("enterMilestone in branch mode calls enterBranchModeForMilestone and rebuilds GitService", () => {