gsd-pi 2.80.0-dev.2be7caf18 → 2.80.0-dev.3065f7bfa

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 (268) hide show
  1. package/README.md +1 -0
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +178 -108
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +20 -30
  6. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  7. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
  10. package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
  11. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  12. package/dist/resources/extensions/gsd/auto-start.js +197 -6
  13. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  14. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  15. package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
  16. package/dist/resources/extensions/gsd/auto.js +79 -24
  17. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +42 -2
  18. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
  19. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
  20. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
  21. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
  22. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
  23. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
  24. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +238 -28
  25. package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
  26. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  27. package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
  28. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  29. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  30. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  31. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  32. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
  33. package/dist/resources/extensions/gsd/git-service.js +36 -4
  34. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  35. package/dist/resources/extensions/gsd/guided-flow.js +66 -32
  36. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  37. package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
  38. package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
  39. package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
  40. package/dist/resources/extensions/gsd/pre-execution-checks.js +22 -0
  41. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  42. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  43. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  45. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  47. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  48. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  49. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  50. package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
  51. package/dist/resources/extensions/gsd/worktree-resolver.js +68 -21
  52. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  53. package/dist/web/standalone/.next/BUILD_ID +1 -1
  54. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  55. package/dist/web/standalone/.next/build-manifest.json +2 -2
  56. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  57. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.html +1 -1
  74. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  81. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  83. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  84. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  85. package/package.json +3 -3
  86. package/packages/mcp-server/src/workflow-tools.test.ts +9 -2
  87. package/packages/native/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -1
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/agent.d.ts +5 -0
  92. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  93. package/packages/pi-agent-core/dist/agent.js +2 -0
  94. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  95. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  96. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  97. package/packages/pi-agent-core/dist/index.js +2 -0
  98. package/packages/pi-agent-core/dist/index.js.map +1 -1
  99. package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
  100. package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
  101. package/packages/pi-agent-core/dist/token-audit.js +221 -0
  102. package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
  103. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  104. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  105. package/packages/pi-agent-core/dist/types.js.map +1 -1
  106. package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
  107. package/packages/pi-agent-core/src/agent-loop.ts +4 -1
  108. package/packages/pi-agent-core/src/agent.ts +8 -0
  109. package/packages/pi-agent-core/src/index.ts +2 -0
  110. package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
  111. package/packages/pi-agent-core/src/token-audit.ts +287 -0
  112. package/packages/pi-agent-core/src/types.ts +14 -0
  113. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  114. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  115. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
  117. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  119. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/agent-session.js +41 -7
  121. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
  124. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  126. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
  128. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
  130. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
  132. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
  135. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
  137. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
  138. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
  139. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
  140. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
  141. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
  143. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  144. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
  145. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  146. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
  147. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
  149. package/packages/pi-coding-agent/dist/core/system-prompt.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-tool-refresh.test.ts +25 -0
  152. package/packages/pi-coding-agent/src/core/agent-session.ts +45 -7
  153. package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
  154. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
  155. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
  156. package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
  157. package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
  158. package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
  159. package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
  160. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
  161. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
  162. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  163. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  164. package/src/resources/extensions/gsd/auto/phases.ts +251 -172
  165. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  166. package/src/resources/extensions/gsd/auto/run-unit.ts +25 -30
  167. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  168. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
  169. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
  170. package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
  171. package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
  172. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  173. package/src/resources/extensions/gsd/auto-start.ts +230 -9
  174. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  175. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  176. package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
  177. package/src/resources/extensions/gsd/auto.ts +87 -19
  178. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +49 -1
  179. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
  180. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
  181. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
  182. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
  183. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
  184. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
  185. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +283 -28
  186. package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
  187. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  188. package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
  189. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  190. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  191. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  192. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  193. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
  194. package/src/resources/extensions/gsd/git-service.ts +46 -8
  195. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  196. package/src/resources/extensions/gsd/guided-flow.ts +84 -39
  197. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  198. package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
  199. package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
  200. package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
  201. package/src/resources/extensions/gsd/pre-execution-checks.ts +23 -0
  202. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  203. package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  204. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  206. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  207. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  208. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
  209. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +417 -10
  210. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  211. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  212. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
  213. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  214. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +3 -1
  215. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
  216. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
  217. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  218. package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
  219. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  220. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  221. package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
  222. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
  223. package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
  224. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  225. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
  226. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +239 -1
  227. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
  228. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
  229. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  230. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
  231. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
  232. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
  233. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
  234. package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
  235. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
  236. package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
  237. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +45 -5
  238. package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
  239. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  240. package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
  241. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
  242. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  243. package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
  244. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +106 -0
  245. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  246. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  247. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +38 -0
  248. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  249. package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
  250. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
  251. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
  252. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  253. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  254. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
  255. package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
  256. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  257. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
  258. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
  259. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
  260. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +167 -4
  261. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  262. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  263. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  264. package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
  265. package/src/resources/extensions/gsd/worktree-resolver.ts +85 -19
  266. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
  267. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_buildManifest.js +0 -0
  268. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_ssgManifest.js +0 -0
@@ -0,0 +1,313 @@
1
+ // gsd-2 / dispatch rule coverage canary test
2
+ //
3
+ // Iterates DISPATCH_RULES in order against representative GSDState stubs and
4
+ // asserts that the first matching rule has the expected name and unitType
5
+ // (mirroring auto-dispatch's first-match-wins semantics). The goal is a
6
+ // canary: if a future PR adds a new rule in the wrong position and steals
7
+ // a match from an existing one, this test fails.
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+
15
+ import { DISPATCH_RULES } from "../auto-dispatch.ts";
16
+ import type { DispatchContext, DispatchAction } from "../auto-dispatch.ts";
17
+ import type { GSDState } from "../types.ts";
18
+
19
+ // ─── State helpers ────────────────────────────────────────────────────────
20
+
21
+ function makeState(overrides: Partial<GSDState> = {}): GSDState {
22
+ return {
23
+ activeMilestone: { id: "M001", title: "Test Milestone" },
24
+ activeSlice: null,
25
+ activeTask: null,
26
+ phase: "pre-planning",
27
+ recentDecisions: [],
28
+ blockers: [],
29
+ nextAction: "",
30
+ registry: [],
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function makeCtx(basePath: string, state: GSDState, mid = "M001"): DispatchContext {
36
+ return {
37
+ basePath,
38
+ mid,
39
+ midTitle: "Test Milestone",
40
+ state,
41
+ prefs: undefined,
42
+ };
43
+ }
44
+
45
+ // ─── Disk scaffold helpers ────────────────────────────────────────────────
46
+
47
+ function writeMilestoneFile(basePath: string, mid: string, suffix: string, content = "stub\n"): void {
48
+ const dir = join(basePath, ".gsd", "milestones", mid);
49
+ mkdirSync(dir, { recursive: true });
50
+ writeFileSync(join(dir, `${mid}-${suffix}.md`), content);
51
+ }
52
+
53
+ function writeSliceFile(
54
+ basePath: string,
55
+ mid: string,
56
+ sid: string,
57
+ suffix: string,
58
+ content = "stub\n",
59
+ ): void {
60
+ const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid);
61
+ mkdirSync(dir, { recursive: true });
62
+ writeFileSync(join(dir, `${sid}-${suffix}.md`), content);
63
+ }
64
+
65
+ function writeTaskPlan(basePath: string, mid: string, sid: string, tid: string): void {
66
+ const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks");
67
+ mkdirSync(dir, { recursive: true });
68
+ writeFileSync(join(dir, `${tid}-PLAN.md`), `# ${tid}\n\n## Steps\n- [ ] Step\n`);
69
+ }
70
+
71
+ // ─── Rule evaluation ──────────────────────────────────────────────────────
72
+
73
+ interface MatchEntry {
74
+ ruleName: string;
75
+ result: DispatchAction;
76
+ }
77
+
78
+ // First-match-wins semantics: walks DISPATCH_RULES in order and stops at the
79
+ // first non-null result. This mirrors the production resolver and is the
80
+ // canary against rule reordering or shadowing.
81
+ async function findFirstMatch(ctx: DispatchContext): Promise<MatchEntry | null> {
82
+ for (const rule of DISPATCH_RULES) {
83
+ const result = await rule.match(ctx);
84
+ if (result) return { ruleName: rule.name, result };
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function assertMatch(
90
+ match: MatchEntry | null,
91
+ expected: { ruleName: string; action: DispatchAction["action"]; unitType?: string },
92
+ scenario: string,
93
+ ): void {
94
+ assert.ok(match, `${scenario}: no rule matched`);
95
+ assert.equal(match.ruleName, expected.ruleName, `${scenario}: matched rule mismatch`);
96
+ assert.equal(match.result.action, expected.action, `${scenario}: action mismatch`);
97
+ if (expected.action === "dispatch" && expected.unitType) {
98
+ assert.ok(
99
+ match.result.action === "dispatch" && match.result.unitType === expected.unitType,
100
+ `${scenario}: unitType mismatch (got ${
101
+ match.result.action === "dispatch" ? match.result.unitType : match.result.action
102
+ })`,
103
+ );
104
+ }
105
+ }
106
+
107
+ // ─── Tests ────────────────────────────────────────────────────────────────
108
+
109
+ test("dispatch-rule-coverage: escalating-task → stop (info)", async (t) => {
110
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-esc-"));
111
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
112
+
113
+ const ctx = makeCtx(
114
+ tmp,
115
+ makeState({
116
+ phase: "escalating-task",
117
+ activeSlice: { id: "S01", title: "Slice" },
118
+ nextAction: "Resolve escalation X",
119
+ }),
120
+ );
121
+ const match = await findFirstMatch(ctx);
122
+ assertMatch(
123
+ match,
124
+ { ruleName: "escalating-task → pause-for-escalation", action: "stop" },
125
+ "escalating-task",
126
+ );
127
+ });
128
+
129
+ test("dispatch-rule-coverage: pre-planning, no CONTEXT → discuss-milestone", async (t) => {
130
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-disc-"));
131
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
132
+
133
+ // Bare milestone dir, no CONTEXT/RESEARCH/ROADMAP files.
134
+ mkdirSync(join(tmp, ".gsd", "milestones", "M001"), { recursive: true });
135
+
136
+ const ctx = makeCtx(tmp, makeState({ phase: "pre-planning" }));
137
+ const match = await findFirstMatch(ctx);
138
+ assertMatch(
139
+ match,
140
+ {
141
+ ruleName: "pre-planning (no context) → discuss-milestone",
142
+ action: "dispatch",
143
+ unitType: "discuss-milestone",
144
+ },
145
+ "pre-planning no context",
146
+ );
147
+ });
148
+
149
+ test("dispatch-rule-coverage: pre-planning, has CONTEXT, no RESEARCH → research-milestone", async (t) => {
150
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-res-"));
151
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
152
+
153
+ writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
154
+
155
+ const ctx = makeCtx(tmp, makeState({ phase: "pre-planning" }));
156
+ const match = await findFirstMatch(ctx);
157
+ assertMatch(
158
+ match,
159
+ {
160
+ ruleName: "pre-planning (no research) → research-milestone",
161
+ action: "dispatch",
162
+ unitType: "research-milestone",
163
+ },
164
+ "pre-planning no research",
165
+ );
166
+ });
167
+
168
+ test("dispatch-rule-coverage: pre-planning, has CONTEXT + RESEARCH → plan-milestone", async (t) => {
169
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-plan-m-"));
170
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
171
+
172
+ writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
173
+ writeMilestoneFile(tmp, "M001", "RESEARCH", "# Research\n");
174
+
175
+ const ctx = makeCtx(tmp, makeState({ phase: "pre-planning" }));
176
+ const match = await findFirstMatch(ctx);
177
+ assertMatch(
178
+ match,
179
+ {
180
+ ruleName: "pre-planning (has research) → plan-milestone",
181
+ action: "dispatch",
182
+ unitType: "plan-milestone",
183
+ },
184
+ "pre-planning has research",
185
+ );
186
+ });
187
+
188
+ test("dispatch-rule-coverage: planning with active slice and skip_research → plan-slice", async (t) => {
189
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-plan-s-"));
190
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
191
+
192
+ writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
193
+ writeMilestoneFile(tmp, "M001", "ROADMAP", "# Roadmap\n");
194
+
195
+ const state = makeState({
196
+ phase: "planning",
197
+ activeSlice: { id: "S01", title: "First Slice" },
198
+ });
199
+ const ctx: DispatchContext = {
200
+ basePath: tmp,
201
+ mid: "M001",
202
+ midTitle: "Test Milestone",
203
+ state,
204
+ // Skip slice research so the parallel/single research rules fall through.
205
+ prefs: { phases: { skip_slice_research: true } } as DispatchContext["prefs"],
206
+ };
207
+ const match = await findFirstMatch(ctx);
208
+ assertMatch(
209
+ match,
210
+ {
211
+ ruleName: "planning → plan-slice",
212
+ action: "dispatch",
213
+ unitType: "plan-slice",
214
+ },
215
+ "planning → plan-slice",
216
+ );
217
+ });
218
+
219
+ test("dispatch-rule-coverage: executing with task plan present → execute-task", async (t) => {
220
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-exec-"));
221
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
222
+
223
+ writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
224
+ writeSliceFile(tmp, "M001", "S01", "PLAN", "# Plan\n");
225
+ writeTaskPlan(tmp, "M001", "S01", "T01");
226
+
227
+ const state = makeState({
228
+ phase: "executing",
229
+ activeSlice: { id: "S01", title: "First Slice" },
230
+ activeTask: { id: "T01", title: "First Task" },
231
+ });
232
+ // Disable reactive dispatch so the parallel batching rule falls through.
233
+ const ctx: DispatchContext = {
234
+ basePath: tmp,
235
+ mid: "M001",
236
+ midTitle: "Test Milestone",
237
+ state,
238
+ prefs: { reactive_execution: { enabled: false } } as DispatchContext["prefs"],
239
+ };
240
+ const match = await findFirstMatch(ctx);
241
+ assertMatch(
242
+ match,
243
+ {
244
+ ruleName: "executing → execute-task",
245
+ action: "dispatch",
246
+ unitType: "execute-task",
247
+ },
248
+ "executing → execute-task",
249
+ );
250
+ });
251
+
252
+ test("dispatch-rule-coverage: summarizing → complete-slice", async (t) => {
253
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-sum-"));
254
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
255
+
256
+ // Rule "execution-entry phase (no context)" fires for summarizing if CONTEXT
257
+ // is missing — write it so the summarizing rule wins.
258
+ writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
259
+
260
+ const ctx = makeCtx(
261
+ tmp,
262
+ makeState({
263
+ phase: "summarizing",
264
+ activeSlice: { id: "S01", title: "First Slice" },
265
+ }),
266
+ );
267
+ const match = await findFirstMatch(ctx);
268
+ assertMatch(
269
+ match,
270
+ {
271
+ ruleName: "summarizing → complete-slice",
272
+ action: "dispatch",
273
+ unitType: "complete-slice",
274
+ },
275
+ "summarizing → complete-slice",
276
+ );
277
+ });
278
+
279
+ test("dispatch-rule-coverage: complete phase → stop", async (t) => {
280
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-done-"));
281
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
282
+
283
+ const ctx = makeCtx(
284
+ tmp,
285
+ makeState({
286
+ phase: "complete",
287
+ activeMilestone: null,
288
+ lastCompletedMilestone: { id: "M001", title: "Test Milestone" },
289
+ }),
290
+ );
291
+ const match = await findFirstMatch(ctx);
292
+ assertMatch(
293
+ match,
294
+ { ruleName: "complete → stop", action: "stop" },
295
+ "complete → stop",
296
+ );
297
+ });
298
+
299
+ // ─── Ordering canary: every scenario above resolves to exactly one rule ────
300
+
301
+ test("dispatch-rule-coverage: rule registry has the expected size", () => {
302
+ // Sanity check that complements the per-state assertions: if someone adds a
303
+ // new rule, this number changes — prompting them to add a state stub above.
304
+ // Exact count is a brittle but useful canary; update when adding rules
305
+ // intentionally.
306
+ assert.equal(
307
+ DISPATCH_RULES.length,
308
+ 29,
309
+ `DISPATCH_RULES length changed (got ${DISPATCH_RULES.length}). ` +
310
+ "If you added a rule, add a state stub to dispatch-rule-coverage.test.ts " +
311
+ "and update this expected count.",
312
+ );
313
+ });
@@ -39,6 +39,7 @@ test("execute-task prompt renders compact execution and completion gates", async
39
39
  taskPlanPath: ".gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md",
40
40
  priorTaskLines: "- None",
41
41
  skillActivation: "Load relevant skills.",
42
+ inlinedTemplates: "### Output Template: Task Summary\nSource: `templates/task-summary.md`",
42
43
  templatesDir: join(fixtureRoot, "templates"),
43
44
  taskSummaryTemplatePath: "C:\\Users\\Test\\.gsd\\agent\\extensions\\gsd\\templates\\task-summary.md",
44
45
  verificationBudget: "~10K chars",
@@ -46,12 +47,14 @@ test("execute-task prompt renders compact execution and completion gates", async
46
47
  });
47
48
 
48
49
  assert.match(prompt, /You execute\./);
49
- assert.match(prompt, /Call `memory_query`/);
50
+ assert.match(prompt, /Call `memory_query`.*only when no injected memory block exists/s);
50
51
  assert.match(prompt, /Before any `Write` that creates an artifact or output file/);
51
52
  assert.match(prompt, /Build real behavior/);
52
53
  assert.match(prompt, /Background process rule/);
53
54
  assert.match(prompt, /blocker_discovered: true/);
54
- assert.match(prompt, /C:\\Users\\Test\\.gsd\\agent\\extensions\\gsd\\templates\\task-summary\.md/);
55
+ assert.match(prompt, /Use the inlined Task Summary template below/);
56
+ assert.match(prompt, /Read `C:\\Users\\Test\\.gsd\\agent\\extensions\\gsd\\templates\\task-summary\.md` only if the inlined template is absent or visibly truncated/);
57
+ assert.match(prompt, /### Output Template: Task Summary/);
55
58
  assert.doesNotMatch(prompt, /\{\{templatesDir\}\}\/task-summary\.md/);
56
59
  assert.match(prompt, /Call `gsd_task_complete`/);
57
60
  assert.match(prompt, /Do not run git commands/);
@@ -0,0 +1,219 @@
1
+ // GSD-2 + src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts
2
+ // Regression: when createAutoWorktree reuses an existing milestone branch,
3
+ // it must be fast-forwarded onto integration so the next milestone forks
4
+ // from up-to-date code (#5538-followup).
5
+
6
+ import { describe, test, beforeEach, afterEach } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { execFileSync } from "node:child_process";
9
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { basename, join } from "node:path";
12
+
13
+ import {
14
+ fastForwardReusedMilestoneBranchIfSafe,
15
+ _isBranchCheckedOutElsewhere,
16
+ } from "../auto-worktree.js";
17
+
18
+ const NO_PROMPT_ENV = {
19
+ ...process.env,
20
+ GIT_TERMINAL_PROMPT: "0",
21
+ GIT_AUTHOR_NAME: "test",
22
+ GIT_AUTHOR_EMAIL: "test@example.com",
23
+ GIT_COMMITTER_NAME: "test",
24
+ GIT_COMMITTER_EMAIL: "test@example.com",
25
+ };
26
+
27
+ function git(cwd: string, ...args: string[]): string {
28
+ return execFileSync("git", args, {
29
+ cwd,
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ encoding: "utf-8",
32
+ env: NO_PROMPT_ENV,
33
+ });
34
+ }
35
+
36
+ function rev(cwd: string, ref: string): string {
37
+ return git(cwd, "rev-parse", ref).trim();
38
+ }
39
+
40
+ describe("fastForwardReusedMilestoneBranchIfSafe", () => {
41
+ let repo: string;
42
+
43
+ beforeEach(() => {
44
+ repo = mkdtempSync(join(tmpdir(), "ff-reused-branch-"));
45
+ git(repo, "init", "-q", "-b", "main");
46
+ git(repo, "config", "user.email", "test@example.com");
47
+ git(repo, "config", "user.name", "test");
48
+ writeFileSync(join(repo, "seed.txt"), "seed\n");
49
+ git(repo, "add", "seed.txt");
50
+ git(repo, "commit", "-q", "-m", "initial");
51
+ });
52
+
53
+ afterEach(() => {
54
+ rmSync(repo, { recursive: true, force: true });
55
+ });
56
+
57
+ test("fast-forwards a milestone branch that is strictly behind integration (regression: stale base)", () => {
58
+ // Create milestone/M001 from main's initial commit, then advance main.
59
+ git(repo, "branch", "milestone/M001");
60
+ const m001Initial = rev(repo, "milestone/M001");
61
+
62
+ writeFileSync(join(repo, "seed.txt"), "advanced\n");
63
+ git(repo, "add", "seed.txt");
64
+ git(repo, "commit", "-q", "-m", "main moved forward");
65
+ const mainTip = rev(repo, "main");
66
+
67
+ assert.notEqual(m001Initial, mainTip, "main must be ahead before the test");
68
+
69
+ fastForwardReusedMilestoneBranchIfSafe(repo, "M001", "milestone/M001");
70
+
71
+ assert.equal(
72
+ rev(repo, "milestone/M001"),
73
+ mainTip,
74
+ "milestone/M001 must be fast-forwarded to main's tip",
75
+ );
76
+ });
77
+
78
+ test("does not touch a milestone branch that has its own commits ahead", () => {
79
+ // Branch from main, add a unique commit, then advance main.
80
+ git(repo, "checkout", "-q", "-b", "milestone/M001");
81
+ writeFileSync(join(repo, "milestone-only.txt"), "milestone work\n");
82
+ git(repo, "add", "milestone-only.txt");
83
+ git(repo, "commit", "-q", "-m", "M001 work");
84
+ const milestoneTip = rev(repo, "milestone/M001");
85
+
86
+ git(repo, "checkout", "-q", "main");
87
+ writeFileSync(join(repo, "seed.txt"), "advanced\n");
88
+ git(repo, "add", "seed.txt");
89
+ git(repo, "commit", "-q", "-m", "main moved forward");
90
+
91
+ fastForwardReusedMilestoneBranchIfSafe(repo, "M001", "milestone/M001");
92
+
93
+ assert.equal(
94
+ rev(repo, "milestone/M001"),
95
+ milestoneTip,
96
+ "diverged milestone branch must NOT be touched (would lose work)",
97
+ );
98
+ });
99
+
100
+ test("is a no-op when milestone branch is already up-to-date with main", () => {
101
+ git(repo, "branch", "milestone/M001");
102
+ const before = rev(repo, "milestone/M001");
103
+
104
+ fastForwardReusedMilestoneBranchIfSafe(repo, "M001", "milestone/M001");
105
+
106
+ assert.equal(rev(repo, "milestone/M001"), before, "ref must not move");
107
+ });
108
+
109
+ test("does nothing when the milestone branch does not exist", () => {
110
+ // Should silently return — no error, no side effects.
111
+ assert.doesNotThrow(() =>
112
+ fastForwardReusedMilestoneBranchIfSafe(repo, "M999", "milestone/M999"),
113
+ );
114
+ });
115
+
116
+ test("does nothing in a non-git directory", () => {
117
+ const nonRepo = mkdtempSync(join(tmpdir(), "ff-not-a-repo-"));
118
+ try {
119
+ assert.doesNotThrow(() =>
120
+ fastForwardReusedMilestoneBranchIfSafe(nonRepo, "M001", "milestone/M001"),
121
+ );
122
+ } finally {
123
+ rmSync(nonRepo, { recursive: true, force: true });
124
+ }
125
+ });
126
+
127
+ test("skips fast-forward when branch is checked out in another worktree (peer-review regression)", () => {
128
+ // Codex peer review caught: `nativeUpdateRef` succeeds even when the
129
+ // branch is checked out in a linked worktree, leaving that worktree's
130
+ // HEAD inconsistent with its index/work tree. The fix calls
131
+ // `nativeWorktreeList` first and skips the FF if any worktree owns the
132
+ // target branch. This test sets up the exact scenario.
133
+ git(repo, "branch", "milestone/M001");
134
+ const m001Initial = rev(repo, "milestone/M001");
135
+
136
+ // Add a linked worktree that checks out milestone/M001.
137
+ const wtPath = join(repo, "..", `${basename(repo)}-wt`);
138
+ git(repo, "worktree", "add", wtPath, "milestone/M001");
139
+
140
+ // Advance main so a fast-forward would otherwise apply.
141
+ writeFileSync(join(repo, "seed.txt"), "advanced\n");
142
+ git(repo, "add", "seed.txt");
143
+ git(repo, "commit", "-q", "-m", "main moved forward");
144
+
145
+ try {
146
+ fastForwardReusedMilestoneBranchIfSafe(repo, "M001", "milestone/M001");
147
+
148
+ assert.equal(
149
+ rev(repo, "milestone/M001"),
150
+ m001Initial,
151
+ "milestone/M001 must NOT move while a linked worktree has it checked out",
152
+ );
153
+ } finally {
154
+ git(repo, "worktree", "remove", "--force", wtPath);
155
+ rmSync(wtPath, { recursive: true, force: true });
156
+ }
157
+ });
158
+ });
159
+
160
+ describe("_isBranchCheckedOutElsewhere", () => {
161
+ let repo: string;
162
+
163
+ beforeEach(() => {
164
+ repo = mkdtempSync(join(tmpdir(), "is-checked-out-"));
165
+ git(repo, "init", "-q", "-b", "main");
166
+ git(repo, "config", "user.email", "test@example.com");
167
+ git(repo, "config", "user.name", "test");
168
+ writeFileSync(join(repo, "seed.txt"), "seed\n");
169
+ git(repo, "add", "seed.txt");
170
+ git(repo, "commit", "-q", "-m", "initial");
171
+ });
172
+
173
+ afterEach(() => {
174
+ rmSync(repo, { recursive: true, force: true });
175
+ });
176
+
177
+ test("returns true when branch is checked out in a linked worktree", () => {
178
+ git(repo, "branch", "milestone/M001");
179
+ const wtPath = join(repo, "..", `${basename(repo)}-wt`);
180
+ git(repo, "worktree", "add", wtPath, "milestone/M001");
181
+ try {
182
+ assert.equal(_isBranchCheckedOutElsewhere(repo, "milestone/M001"), true);
183
+ } finally {
184
+ git(repo, "worktree", "remove", "--force", wtPath);
185
+ rmSync(wtPath, { recursive: true, force: true });
186
+ }
187
+ });
188
+
189
+ test("returns true when branch is checked out in the main worktree itself", () => {
190
+ // The default checkout. `git worktree list --porcelain` reports the
191
+ // primary worktree too, so a branch checked out there counts as
192
+ // "checked out elsewhere" relative to a fresh ref update intent.
193
+ git(repo, "checkout", "-q", "-b", "milestone/M002");
194
+ assert.equal(_isBranchCheckedOutElsewhere(repo, "milestone/M002"), true);
195
+ });
196
+
197
+ test("returns false when branch exists but is not checked out anywhere", () => {
198
+ git(repo, "branch", "milestone/M003");
199
+ assert.equal(_isBranchCheckedOutElsewhere(repo, "milestone/M003"), false);
200
+ });
201
+
202
+ test("returns false for an unknown branch in a clean repo", () => {
203
+ assert.equal(_isBranchCheckedOutElsewhere(repo, "milestone/M999"), false);
204
+ });
205
+
206
+ test("returns false on a non-git directory (empty worktree list)", () => {
207
+ // nativeWorktreeList does not throw on a non-repo — it returns []. The
208
+ // parent function `fastForwardReusedMilestoneBranchIfSafe` never reaches
209
+ // this code path on a non-repo because `nativeBranchExists` short-circuits
210
+ // earlier. Documenting actual behavior so future readers don't expect a
211
+ // fail-safe `true` here.
212
+ const nonRepo = mkdtempSync(join(tmpdir(), "is-checked-out-not-repo-"));
213
+ try {
214
+ assert.equal(_isBranchCheckedOutElsewhere(nonRepo, "milestone/M001"), false);
215
+ } finally {
216
+ rmSync(nonRepo, { recursive: true, force: true });
217
+ }
218
+ });
219
+ });
@@ -0,0 +1,132 @@
1
+ // GSD-2 + src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts
2
+ // Regression: a thrown error from `_mergeBranchMode` (made fail-loud in
3
+ // commit 68ef58a3c) must be caught at the survivor-finalize call site so
4
+ // bootstrap surfaces an error notify instead of an unhandled exception
5
+ // propagating to the slash-command caller (#5549 post-merge audit, R2).
6
+
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+
10
+ import { _finalizeSurvivorBranch } from "../auto-start.js";
11
+ import type { WorktreeResolver } from "../worktree-resolver.js";
12
+
13
+ interface FakeResolverState {
14
+ mergeCalls: Array<{ milestoneId: string }>;
15
+ shouldThrow?: unknown;
16
+ }
17
+
18
+ function fakeResolver(state: FakeResolverState): WorktreeResolver {
19
+ return {
20
+ mergeAndExit: (milestoneId: string) => {
21
+ state.mergeCalls.push({ milestoneId });
22
+ if (state.shouldThrow) throw state.shouldThrow;
23
+ },
24
+ } as unknown as WorktreeResolver;
25
+ }
26
+
27
+ interface FakeUiState {
28
+ notifications: Array<{ message: string; level: string }>;
29
+ }
30
+
31
+ function fakeUi(state: FakeUiState): {
32
+ notify: (msg: string, level?: "info" | "warning" | "error" | "success") => void;
33
+ } {
34
+ return {
35
+ notify: (message: string, level?: "info" | "warning" | "error" | "success") => {
36
+ state.notifications.push({ message, level: level ?? "info" });
37
+ },
38
+ };
39
+ }
40
+
41
+ test("happy path: survivor merge runs, returns merged:true, info notify announces the merge", () => {
42
+ const resolverState: FakeResolverState = { mergeCalls: [] };
43
+ const uiState: FakeUiState = { notifications: [] };
44
+ const result = _finalizeSurvivorBranch(
45
+ fakeResolver(resolverState),
46
+ "M001",
47
+ fakeUi(uiState),
48
+ );
49
+
50
+ assert.deepEqual(result, { merged: true });
51
+ assert.deepEqual(resolverState.mergeCalls, [{ milestoneId: "M001" }]);
52
+ assert.equal(uiState.notifications.length, 1);
53
+ assert.equal(uiState.notifications[0].level, "info");
54
+ assert.match(
55
+ uiState.notifications[0].message,
56
+ /Milestone M001 is complete but branch\/worktree was not finalized/,
57
+ );
58
+ });
59
+
60
+ test("regression: thrown error from mergeAndExit (e.g. wrong-branch) is caught and surfaced as error notify", () => {
61
+ // Pre-PR-5549 commit 5: `_mergeBranchMode` returned false silently.
62
+ // Post-commit 5: it throws. Without this fix, the throw at auto-start.ts
63
+ // line ~810 would bubble through `bootstrapAutoSession` to
64
+ // `startAutoDetached`'s top-level .catch as an unhandled-exception log
65
+ // — observable as a stack trace instead of a clean failure notification.
66
+ const boom = new Error("dirty working tree blocks checkout");
67
+ const resolverState: FakeResolverState = { mergeCalls: [], shouldThrow: boom };
68
+ const uiState: FakeUiState = { notifications: [] };
69
+
70
+ // Must not throw.
71
+ const result = _finalizeSurvivorBranch(
72
+ fakeResolver(resolverState),
73
+ "M001",
74
+ fakeUi(uiState),
75
+ );
76
+
77
+ assert.equal(result.merged, false);
78
+ assert.equal(result.error, boom);
79
+
80
+ // Two notifies: info (announce) + error (the failure detail).
81
+ assert.equal(uiState.notifications.length, 2);
82
+ assert.equal(uiState.notifications[0].level, "info");
83
+ assert.equal(uiState.notifications[1].level, "error");
84
+ assert.match(
85
+ uiState.notifications[1].message,
86
+ /Survivor-branch finalization for M001 failed/,
87
+ );
88
+ assert.match(uiState.notifications[1].message, /dirty working tree blocks checkout/);
89
+ assert.match(uiState.notifications[1].message, /Resolve manually/);
90
+ });
91
+
92
+ test("non-Error thrown values are stringified into the user-facing message", () => {
93
+ const resolverState: FakeResolverState = {
94
+ mergeCalls: [],
95
+ shouldThrow: "git lock contention",
96
+ };
97
+ const uiState: FakeUiState = { notifications: [] };
98
+
99
+ const result = _finalizeSurvivorBranch(
100
+ fakeResolver(resolverState),
101
+ "M001",
102
+ fakeUi(uiState),
103
+ );
104
+
105
+ assert.equal(result.merged, false);
106
+ assert.equal(result.error, resolverState.shouldThrow);
107
+ assert.equal(uiState.notifications[1].level, "error");
108
+ assert.match(uiState.notifications[1].message, /git lock contention/);
109
+ });
110
+
111
+ test("inner notifications from mergeAndExit's NotifyCtx reach the same UI", () => {
112
+ // The resolver's NotifyCtx must be wired to ui.notify so messages emitted
113
+ // inside mergeAndExit (e.g. "Milestone Mxxx merged. Pushed to remote.")
114
+ // appear in the same UI stream as the outer notifies.
115
+ const uiState: FakeUiState = { notifications: [] };
116
+ const ui = fakeUi(uiState);
117
+
118
+ const resolver = {
119
+ mergeAndExit: (_milestoneId: string, ctx: { notify: (msg: string, level?: "info" | "warning" | "error" | "success") => void }) => {
120
+ ctx.notify("Milestone M001 merged.", "success");
121
+ },
122
+ } as unknown as WorktreeResolver;
123
+
124
+ const result = _finalizeSurvivorBranch(resolver, "M001", ui);
125
+
126
+ assert.equal(result.merged, true);
127
+ // 1: outer announce
128
+ // 2: inner success
129
+ assert.equal(uiState.notifications.length, 2);
130
+ assert.equal(uiState.notifications[1].level, "success");
131
+ assert.equal(uiState.notifications[1].message, "Milestone M001 merged.");
132
+ });