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

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 (237) hide show
  1. package/dist/cli.js +0 -19
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +29 -0
  4. package/dist/resources/extensions/gsd/auto/loop.js +71 -8
  5. package/dist/resources/extensions/gsd/auto/phases.js +150 -94
  6. package/dist/resources/extensions/gsd/auto/resolve.js +12 -0
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +10 -30
  8. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  9. package/dist/resources/extensions/gsd/auto/workflow-dispatch-claim.js +33 -1
  10. package/dist/resources/extensions/gsd/auto/workflow-worker-heartbeat.js +9 -1
  11. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
  14. package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
  15. package/dist/resources/extensions/gsd/auto-start.js +197 -6
  16. package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
  17. package/dist/resources/extensions/gsd/auto.js +18 -22
  18. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +86 -19
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
  20. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
  21. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
  22. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
  23. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
  24. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +298 -54
  26. package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
  27. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  28. package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
  29. package/dist/resources/extensions/gsd/db/unit-dispatches.js +53 -0
  30. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
  31. package/dist/resources/extensions/gsd/guided-flow.js +47 -28
  32. package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
  33. package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
  35. package/dist/resources/extensions/gsd/pre-execution-checks.js +15 -0
  36. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  37. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  39. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  41. package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
  42. package/dist/resources/extensions/gsd/worktree-resolver.js +35 -4
  43. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  44. package/dist/web/standalone/.next/BUILD_ID +1 -1
  45. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  46. package/dist/web/standalone/.next/build-manifest.json +2 -2
  47. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/dist/welcome-screen.d.ts +2 -0
  77. package/dist/welcome-screen.js +9 -7
  78. package/package.json +1 -1
  79. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  80. package/packages/pi-agent-core/dist/agent-loop.js +4 -1
  81. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  82. package/packages/pi-agent-core/dist/agent.d.ts +5 -0
  83. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  84. package/packages/pi-agent-core/dist/agent.js +2 -0
  85. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  86. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  87. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  88. package/packages/pi-agent-core/dist/index.js +2 -0
  89. package/packages/pi-agent-core/dist/index.js.map +1 -1
  90. package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
  91. package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
  92. package/packages/pi-agent-core/dist/token-audit.js +221 -0
  93. package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
  94. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  95. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/types.js.map +1 -1
  97. package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
  98. package/packages/pi-agent-core/src/agent-loop.ts +4 -1
  99. package/packages/pi-agent-core/src/agent.ts +8 -0
  100. package/packages/pi-agent-core/src/index.ts +2 -0
  101. package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
  102. package/packages/pi-agent-core/src/token-audit.ts +287 -0
  103. package/packages/pi-agent-core/src/types.ts +14 -0
  104. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
  106. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  108. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/agent-session.js +36 -7
  110. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
  113. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
  117. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
  119. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
  121. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
  124. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
  126. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
  127. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
  128. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
  129. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
  130. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
  132. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
  134. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
  136. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
  138. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  139. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
  140. package/packages/pi-coding-agent/src/core/agent-session.ts +40 -7
  141. package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
  142. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
  143. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
  144. package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
  145. package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
  146. package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
  147. package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
  148. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
  149. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
  150. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  151. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +30 -0
  152. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +26 -0
  153. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  154. package/src/resources/extensions/gsd/auto/loop.ts +84 -8
  155. package/src/resources/extensions/gsd/auto/phases.ts +218 -154
  156. package/src/resources/extensions/gsd/auto/resolve.ts +19 -0
  157. package/src/resources/extensions/gsd/auto/run-unit.ts +10 -29
  158. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  159. package/src/resources/extensions/gsd/auto/workflow-dispatch-claim.ts +63 -1
  160. package/src/resources/extensions/gsd/auto/workflow-worker-heartbeat.ts +14 -1
  161. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
  163. package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
  164. package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
  165. package/src/resources/extensions/gsd/auto-start.ts +230 -9
  166. package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
  167. package/src/resources/extensions/gsd/auto.ts +18 -18
  168. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +100 -18
  169. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
  170. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
  171. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
  172. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
  173. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
  174. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
  175. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +347 -54
  176. package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
  177. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  178. package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
  179. package/src/resources/extensions/gsd/db/unit-dispatches.ts +66 -0
  180. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
  181. package/src/resources/extensions/gsd/guided-flow.ts +52 -35
  182. package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
  183. package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
  184. package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
  185. package/src/resources/extensions/gsd/pre-execution-checks.ts +16 -0
  186. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  187. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  188. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  189. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  190. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  191. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
  192. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +361 -10
  193. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
  194. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  195. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
  196. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
  197. package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
  198. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  199. package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
  200. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
  201. package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
  202. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
  203. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
  204. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
  205. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
  206. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
  207. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
  208. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
  209. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
  210. package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
  211. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
  212. package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
  213. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +7 -5
  214. package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
  215. package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
  216. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
  217. package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
  218. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +77 -0
  219. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +166 -0
  220. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  221. package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
  222. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
  223. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
  224. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +50 -1
  225. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
  226. package/src/resources/extensions/gsd/tests/workflow-dispatch-claim.test.ts +142 -0
  227. package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
  228. package/src/resources/extensions/gsd/tests/workflow-worker-heartbeat.test.ts +32 -1
  229. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
  230. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
  231. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
  232. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +104 -3
  233. package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
  234. package/src/resources/extensions/gsd/worktree-resolver.ts +49 -4
  235. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
  236. /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_buildManifest.js +0 -0
  237. /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_ssgManifest.js +0 -0
@@ -0,0 +1,166 @@
1
+ // GSD-2 + src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts
2
+ // Regression: session-transition aborts must not be classified as user cancellations.
3
+
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+
7
+ import {
8
+ _handleSessionSwitchAgentEnd,
9
+ isClaudeCodeSessionSwitchAbortMessage,
10
+ } from "../bootstrap/agent-end-recovery.js";
11
+ import type { ErrorContext } from "../auto/types.js";
12
+
13
+ test("user-abort message during session-switch is dropped (not propagated as cancellation)", () => {
14
+ // The Anthropic SDK emits this exact string when newSession() aborts an
15
+ // in-flight stream during a unit-to-unit session transition. Before the fix
16
+ // this was misclassified as a user cancellation and killed auto-mode with
17
+ // "Auto-mode stopped — Unit aborted: Claude Code process aborted by user".
18
+ let cancelledWith: ErrorContext | null = null;
19
+ const resolveCancelled = (ctx: ErrorContext) => {
20
+ cancelledWith = ctx;
21
+ return true;
22
+ };
23
+
24
+ _handleSessionSwitchAgentEnd(
25
+ { stopReason: "error", errorMessage: "Claude Code process aborted by user" },
26
+ resolveCancelled,
27
+ );
28
+ assert.equal(cancelledWith, null, "SDK user-abort during session-switch must not propagate cancellation");
29
+
30
+ _handleSessionSwitchAgentEnd(
31
+ { stopReason: "error", errorMessage: "Request aborted by user" },
32
+ resolveCancelled,
33
+ );
34
+ assert.equal(cancelledWith, null, "proxy user-abort during session-switch must not propagate cancellation");
35
+ });
36
+
37
+ test("genuine stopReason='aborted' with errorMessage during session-switch still propagates", () => {
38
+ // Regression guard for prior behavior: genuine aborts with diagnostic content
39
+ // continue to surface as cancellations so transient-pause recovery can run.
40
+ let cancelledWith: { message: string; category: string; isTransient?: boolean } | null = null;
41
+ const resolveCancelled = (ctx: ErrorContext) => {
42
+ cancelledWith = ctx;
43
+ return true;
44
+ };
45
+
46
+ _handleSessionSwitchAgentEnd(
47
+ {
48
+ stopReason: "aborted",
49
+ errorMessage: "stream torn down mid-flight",
50
+ content: [{ type: "text", text: "partial output" }],
51
+ },
52
+ resolveCancelled,
53
+ );
54
+
55
+ assert.deepEqual(cancelledWith, {
56
+ message: "stream torn down mid-flight",
57
+ category: "aborted",
58
+ isTransient: true,
59
+ });
60
+ });
61
+
62
+ test("Claude Code stream-aborted placeholder during session-switch is dropped", () => {
63
+ let cancelledWith: unknown = null;
64
+ const resolveCancelled = (ctx: ErrorContext) => {
65
+ cancelledWith = ctx;
66
+ return true;
67
+ };
68
+
69
+ _handleSessionSwitchAgentEnd(
70
+ {
71
+ stopReason: "aborted",
72
+ content: [{ type: "text", text: "Claude Code stream aborted by caller" }],
73
+ },
74
+ resolveCancelled,
75
+ );
76
+
77
+ assert.equal(cancelledWith, null);
78
+ });
79
+
80
+ test("Claude Code session-switch abort detection is narrow", () => {
81
+ assert.equal(
82
+ isClaudeCodeSessionSwitchAbortMessage({
83
+ stopReason: "error",
84
+ content: [{ type: "text", text: "Claude Code error: Claude Code process aborted by user" }],
85
+ }),
86
+ false,
87
+ );
88
+ assert.equal(
89
+ isClaudeCodeSessionSwitchAbortMessage({
90
+ stopReason: "aborted",
91
+ content: [{ type: "text", text: "Claude Code stream aborted by caller" }],
92
+ }),
93
+ true,
94
+ );
95
+ assert.equal(
96
+ isClaudeCodeSessionSwitchAbortMessage({
97
+ stopReason: "aborted",
98
+ content: [{ type: "text", text: "partial output before network failure" }],
99
+ }),
100
+ false,
101
+ );
102
+ assert.equal(
103
+ isClaudeCodeSessionSwitchAbortMessage({
104
+ stopReason: "aborted",
105
+ content: [{ type: "text", text: "Request aborted by user\nAPI Error: 529 overloaded" }],
106
+ }),
107
+ false,
108
+ );
109
+ assert.equal(
110
+ isClaudeCodeSessionSwitchAbortMessage({
111
+ stopReason: "error",
112
+ errorMessage: "Request aborted by user",
113
+ content: [{ type: "text", text: "Claude Code process aborted by user" }],
114
+ }),
115
+ true,
116
+ );
117
+ });
118
+
119
+ test("empty-content aborted during session-switch is silently ignored", () => {
120
+ // Empty-content aborted is a non-fatal LLM stop; we must not pause/cancel.
121
+ let cancelledWith: unknown = null;
122
+ const resolveCancelled = (ctx: ErrorContext) => {
123
+ cancelledWith = ctx;
124
+ return true;
125
+ };
126
+
127
+ _handleSessionSwitchAgentEnd(
128
+ { stopReason: "aborted", content: [] },
129
+ resolveCancelled,
130
+ );
131
+
132
+ assert.equal(cancelledWith, null);
133
+ });
134
+
135
+ test("non-abort errors during session-switch are not propagated through this helper", () => {
136
+ // Real provider errors (rate-limit, network, unsupported-model) are handled
137
+ // by the post-switch retry pipeline — not by the in-flight switch handler.
138
+ let cancelledWith: unknown = null;
139
+ const resolveCancelled = (ctx: ErrorContext) => {
140
+ cancelledWith = ctx;
141
+ return true;
142
+ };
143
+
144
+ _handleSessionSwitchAgentEnd(
145
+ { stopReason: "error", errorMessage: "rate limit exceeded" },
146
+ resolveCancelled,
147
+ );
148
+
149
+ assert.equal(cancelledWith, null);
150
+ });
151
+
152
+ test("malformed lastMsg is rejected gracefully", () => {
153
+ let calls = 0;
154
+ const resolveCancelled = (_ctx: unknown) => {
155
+ calls += 1;
156
+ return true;
157
+ };
158
+
159
+ _handleSessionSwitchAgentEnd(undefined, resolveCancelled as never);
160
+ _handleSessionSwitchAgentEnd(null, resolveCancelled as never);
161
+ _handleSessionSwitchAgentEnd("not an object", resolveCancelled as never);
162
+ _handleSessionSwitchAgentEnd({}, resolveCancelled as never);
163
+ _handleSessionSwitchAgentEnd({ stopReason: "completed" }, resolveCancelled as never);
164
+
165
+ assert.equal(calls, 0, "malformed or non-abort lastMsg must not invoke cancellation");
166
+ });
@@ -291,6 +291,7 @@ describe("#2945 Bug 3: mergeAndExit must teardown worktree after successful merg
291
291
  getAutoWorktreePath: () => null,
292
292
  autoCommitCurrentBranch: () => {},
293
293
  getCurrentBranch: () => "main",
294
+ checkoutBranch: () => {},
294
295
  autoWorktreeBranch: () => "gsd/M001",
295
296
  resolveMilestoneFile: () => "/mock/roadmap.md",
296
297
  readFileSync: () => "# Roadmap content",
@@ -0,0 +1,112 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: System context memory gating regression tests.
3
+
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+
7
+ import { buildContextMessage, isLowEntropyResumePrompt, loadMemoryBlock } from "../bootstrap/system-context.ts";
8
+ import { closeDatabase, openDatabase } from "../gsd-db.ts";
9
+ import { createMemory } from "../memory-store.ts";
10
+
11
+ test("buildContextMessage marks hidden guided context when memory is supplied", () => {
12
+ const message = buildContextMessage({
13
+ memoryBlock: "\n\n[MEMORY]\n\n- keep this",
14
+ injection: "[GSD Guided Execute Context]\nUse the task plan.",
15
+ forensicsInjection: null,
16
+ });
17
+
18
+ assert.ok(message, "expected hidden context message");
19
+ assert.equal(message.customType, "gsd-guided-context");
20
+ assert.equal(message.display, false);
21
+ assert.match(message.content, /\[GSD Context Metadata\]\n- Memory supplied: yes/);
22
+ assert.ok(
23
+ message.content.indexOf("Memory supplied: yes") < message.content.indexOf("[GSD Guided Execute Context]"),
24
+ "memory marker should appear before guided context",
25
+ );
26
+ });
27
+
28
+ test("buildContextMessage caps hidden context by default", () => {
29
+ const original = process.env.PI_GSD_CONTEXT_MAX_CHARS;
30
+ delete process.env.PI_GSD_CONTEXT_MAX_CHARS;
31
+ try {
32
+ const message = buildContextMessage({
33
+ memoryBlock: "",
34
+ injection: `[GSD Guided Execute Context]\n${"large context\n".repeat(500)}`,
35
+ forensicsInjection: null,
36
+ });
37
+
38
+ assert.ok(message, "expected hidden context message");
39
+ assert.equal(message.customType, "gsd-guided-context");
40
+ assert.ok(message.content.length <= 4000);
41
+ assert.match(message.content, /\[GSD Context Truncated\]/);
42
+ } finally {
43
+ if (original === undefined) delete process.env.PI_GSD_CONTEXT_MAX_CHARS;
44
+ else process.env.PI_GSD_CONTEXT_MAX_CHARS = original;
45
+ }
46
+ });
47
+
48
+ test("buildContextMessage supports explicit context cap override", () => {
49
+ const original = process.env.PI_GSD_CONTEXT_MAX_CHARS;
50
+ process.env.PI_GSD_CONTEXT_MAX_CHARS = "1200";
51
+ try {
52
+ const message = buildContextMessage({
53
+ memoryBlock: "",
54
+ injection: `[GSD Guided Execute Context]\n${"large context\n".repeat(200)}`,
55
+ forensicsInjection: null,
56
+ });
57
+
58
+ assert.ok(message, "expected hidden context message");
59
+ assert.equal(message.customType, "gsd-guided-context");
60
+ assert.ok(message.content.length <= 1200);
61
+ assert.match(message.content, /\[GSD Context Truncated\]/);
62
+ } finally {
63
+ if (original === undefined) delete process.env.PI_GSD_CONTEXT_MAX_CHARS;
64
+ else process.env.PI_GSD_CONTEXT_MAX_CHARS = original;
65
+ }
66
+ });
67
+
68
+ test("buildContextMessage does not add memory marker when only guided context is supplied", () => {
69
+ const message = buildContextMessage({
70
+ memoryBlock: "",
71
+ injection: "[GSD Guided Execute Context]\nUse the task plan.",
72
+ forensicsInjection: null,
73
+ });
74
+
75
+ assert.ok(message, "expected guided context message");
76
+ assert.equal(message.customType, "gsd-guided-context");
77
+ assert.doesNotMatch(message.content, /Memory supplied: yes/);
78
+ });
79
+
80
+ test("loadMemoryBlock keeps critical memories while gating prompt-relevant query hits", async () => {
81
+ closeDatabase();
82
+ assert.equal(openDatabase(":memory:"), true);
83
+ try {
84
+ createMemory({
85
+ category: "gotcha",
86
+ content: "Always preserve critical resume safety context.",
87
+ confidence: 0.95,
88
+ });
89
+ createMemory({
90
+ category: "preference",
91
+ content: "React dashboard preference should only appear for a React prompt query.",
92
+ confidence: 0.95,
93
+ });
94
+
95
+ const withPromptRelevant = await loadMemoryBlock("React dashboard", { includePromptRelevant: true });
96
+ assert.match(withPromptRelevant, /critical resume safety context/);
97
+ assert.match(withPromptRelevant, /React dashboard preference/);
98
+
99
+ const withoutPromptRelevant = await loadMemoryBlock("React dashboard", { includePromptRelevant: false });
100
+ assert.match(withoutPromptRelevant, /critical resume safety context/);
101
+ assert.doesNotMatch(withoutPromptRelevant, /React dashboard preference/);
102
+ } finally {
103
+ closeDatabase();
104
+ }
105
+ });
106
+
107
+ test("isLowEntropyResumePrompt identifies bare resume prompts only", () => {
108
+ assert.equal(isLowEntropyResumePrompt("continue"), true);
109
+ assert.equal(isLowEntropyResumePrompt("Go ahead."), true);
110
+ assert.equal(isLowEntropyResumePrompt("run the tests"), false);
111
+ assert.equal(isLowEntropyResumePrompt("/gsd auto"), false);
112
+ });
@@ -1,9 +1,5 @@
1
- // GSD bootstrap + system-context-message-routing.test — regression coverage
2
- // for #5019. `memoryBlock` is FTS-queried against the user prompt and changes
3
- // per call; embedding it in the cached system prefix invalidates Anthropic
4
- // prompt-cache hits on every request. The fix routes memory through the
5
- // existing context-message channel (volatile user-message suffix) and combines
6
- // it with any active guided-execute or forensics injection.
1
+ // Project/App: GSD-2
2
+ // File Purpose: Regression coverage for volatile system-context message routing.
7
3
 
8
4
  import { describe, test } from "node:test";
9
5
  import assert from "node:assert/strict";
@@ -11,6 +7,8 @@ import assert from "node:assert/strict";
11
7
  import { buildContextMessage } from "../bootstrap/system-context.ts";
12
8
 
13
9
  describe("buildContextMessage (#5019 — memory routing)", () => {
10
+ const markedMemory = "[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]\nrule one";
11
+
14
12
  test("returns null when nothing to inject", () => {
15
13
  const result = buildContextMessage({
16
14
  memoryBlock: "",
@@ -37,7 +35,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
37
35
  });
38
36
  assert.ok(result, "expected a context message");
39
37
  assert.equal(result.customType, "gsd-memory");
40
- assert.equal(result.content, "[MEMORY]\nrule one\nrule two");
38
+ assert.equal(result.content, "[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]\nrule one\nrule two");
41
39
  assert.equal(result.display, false);
42
40
  });
43
41
 
@@ -71,7 +69,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
71
69
  });
72
70
  assert.ok(result);
73
71
  assert.equal(result.customType, "gsd-guided-context");
74
- assert.equal(result.content, "[MEMORY]\nrule one\n\n[GUIDED]\nexecute T01");
72
+ assert.equal(result.content, `${markedMemory}\n\n[GUIDED]\nexecute T01`);
75
73
  });
76
74
 
77
75
  test("memory + forensics: memory prepended, customType is gsd-forensics", () => {
@@ -82,7 +80,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
82
80
  });
83
81
  assert.ok(result);
84
82
  assert.equal(result.customType, "gsd-forensics");
85
- assert.equal(result.content, "[MEMORY]\nrule one\n\n[FORENSICS]\ninvestigation context");
83
+ assert.equal(result.content, `${markedMemory}\n\n[FORENSICS]\ninvestigation context`);
86
84
  });
87
85
 
88
86
  test("guided takes precedence over forensics when both are somehow present", () => {
@@ -0,0 +1,291 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Tests for opt-in GSD tool surface reduction.
3
+
4
+ import assert from "node:assert/strict";
5
+ import test from "node:test";
6
+
7
+ import { buildMinimalAutoGsdToolSet, buildMinimalGsdToolSet, buildMinimalGsdWorkflowToolSet, buildRequestScopedGsdToolSet, MINIMAL_AUTO_BASE_TOOL_NAMES, MINIMAL_GSD_TOOL_NAMES, restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch } from "../bootstrap/register-hooks.ts";
8
+
9
+ test("buildMinimalGsdToolSet preserves non-GSD tools and replaces broad GSD surface", () => {
10
+ const result = buildMinimalGsdToolSet([
11
+ "bash",
12
+ "read",
13
+ "browser_open",
14
+ "gsd_plan_milestone",
15
+ "gsd_task_complete",
16
+ "gsd_exec",
17
+ "gsd_exec_search",
18
+ "gsd_resume",
19
+ "gsd_milestone_status",
20
+ "gsd_checkpoint_db",
21
+ "memory_query",
22
+ "capture_thought",
23
+ "gsd_graph",
24
+ ]);
25
+
26
+ assert.ok(result.includes("bash"));
27
+ assert.ok(result.includes("read"));
28
+ assert.ok(result.includes("browser_open"));
29
+ for (const toolName of MINIMAL_GSD_TOOL_NAMES) {
30
+ assert.ok(result.includes(toolName), `expected ${toolName}`);
31
+ }
32
+ assert.ok(!result.includes("gsd_plan_milestone"));
33
+ assert.ok(!result.includes("gsd_task_complete"));
34
+ assert.ok(!result.includes("gsd_graph"));
35
+ });
36
+
37
+ test("buildMinimalGsdToolSet deduplicates preserved and minimal tools", () => {
38
+ const result = buildMinimalGsdToolSet(["bash", "bash", "memory_query"]);
39
+
40
+ assert.deepEqual(result.filter((toolName) => toolName === "bash"), ["bash"]);
41
+ assert.deepEqual(result.filter((toolName) => toolName === "memory_query"), ["memory_query"]);
42
+ });
43
+
44
+ test("buildMinimalGsdToolSet does not reintroduce provider-filtered GSD tools", () => {
45
+ const result = buildMinimalGsdToolSet(["bash", "read", "memory_query"]);
46
+
47
+ assert.deepEqual(result, ["bash", "read", "memory_query"]);
48
+ assert.ok(!result.includes("gsd_exec"));
49
+ });
50
+
51
+ test("buildMinimalAutoGsdToolSet keeps unit-specific completion tools without aliases", () => {
52
+ const result = buildMinimalAutoGsdToolSet([
53
+ "ask_user_questions",
54
+ "bash",
55
+ "read",
56
+ "lsp",
57
+ "browser_click",
58
+ "gsd_task_complete",
59
+ "gsd_complete_task",
60
+ "gsd_exec",
61
+ "gsd_exec_search",
62
+ "gsd_resume",
63
+ "gsd_milestone_status",
64
+ "gsd_checkpoint_db",
65
+ "gsd_slice_complete",
66
+ "gsd_complete_slice",
67
+ "memory_query",
68
+ "capture_thought",
69
+ ], "execute-task");
70
+
71
+ assert.ok(result.includes("ask_user_questions"));
72
+ assert.ok(result.includes("bash"));
73
+ assert.ok(result.includes("read"));
74
+ assert.ok(result.includes("gsd_task_complete"));
75
+ assert.ok(result.includes("memory_query"));
76
+ assert.ok(!result.includes("lsp"));
77
+ assert.ok(!result.includes("browser_click"));
78
+ assert.ok(!result.includes("gsd_complete_task"));
79
+ assert.ok(!result.includes("gsd_slice_complete"));
80
+ assert.ok(!result.includes("gsd_complete_slice"));
81
+ });
82
+
83
+ test("buildMinimalAutoGsdToolSet keeps only the auto base non-GSD tools", () => {
84
+ const result = buildMinimalAutoGsdToolSet([
85
+ "ask_user_questions",
86
+ "bash",
87
+ "bg_shell",
88
+ "browser_wait_for",
89
+ "edit",
90
+ "glob",
91
+ "grep",
92
+ "lsp",
93
+ "ls",
94
+ "mac_find",
95
+ "read",
96
+ "subagent",
97
+ "write",
98
+ "gsd_exec",
99
+ "gsd_exec_search",
100
+ "gsd_resume",
101
+ "gsd_milestone_status",
102
+ "gsd_checkpoint_db",
103
+ "memory_query",
104
+ "capture_thought",
105
+ ], "execute-task");
106
+
107
+ for (const toolName of MINIMAL_AUTO_BASE_TOOL_NAMES) {
108
+ assert.ok(result.includes(toolName), `expected ${toolName}`);
109
+ }
110
+ assert.ok(!result.includes("browser_wait_for"));
111
+ assert.ok(!result.includes("lsp"));
112
+ assert.ok(!result.includes("mac_find"));
113
+ assert.ok(!result.includes("subagent"));
114
+ });
115
+
116
+ test("buildMinimalAutoGsdToolSet includes closeout tool for complete-slice", () => {
117
+ const result = buildMinimalAutoGsdToolSet([
118
+ "bash",
119
+ "read",
120
+ "subagent",
121
+ "gsd_exec",
122
+ "gsd_exec_search",
123
+ "gsd_resume",
124
+ "gsd_milestone_status",
125
+ "gsd_checkpoint_db",
126
+ "gsd_task_complete",
127
+ "gsd_slice_complete",
128
+ "gsd_complete_slice",
129
+ "memory_query",
130
+ "capture_thought",
131
+ ], "complete-slice");
132
+
133
+ assert.ok(result.includes("gsd_slice_complete"));
134
+ assert.ok(result.includes("subagent"));
135
+ assert.ok(result.includes("capture_thought"));
136
+ assert.ok(!result.includes("gsd_task_complete"));
137
+ assert.ok(!result.includes("gsd_complete_slice"));
138
+ });
139
+
140
+ test("buildMinimalAutoGsdToolSet covers execute-task-simple", () => {
141
+ const result = buildMinimalAutoGsdToolSet([
142
+ "bash",
143
+ "read",
144
+ "gsd_task_complete",
145
+ "gsd_decision_save",
146
+ "gsd_plan_task",
147
+ "memory_query",
148
+ "capture_thought",
149
+ ], "execute-task-simple");
150
+
151
+ assert.ok(result.includes("gsd_task_complete"));
152
+ assert.ok(result.includes("gsd_decision_save"));
153
+ assert.ok(!result.includes("gsd_plan_task"));
154
+ });
155
+
156
+ test("buildMinimalGsdWorkflowToolSet keeps workflow GSD tools but drops broad non-GSD tools", () => {
157
+ const result = buildMinimalGsdWorkflowToolSet([
158
+ "ask_user_questions",
159
+ "bash",
160
+ "bg_shell",
161
+ "browser_wait_for",
162
+ "edit",
163
+ "lsp",
164
+ "mac_find",
165
+ "read",
166
+ "subagent",
167
+ "write",
168
+ "gsd_plan_milestone",
169
+ "gsd_complete_milestone",
170
+ "gsd_task_complete",
171
+ "gsd_summary_save",
172
+ "memory_query",
173
+ "capture_thought",
174
+ "gsd_exec",
175
+ "gsd_exec_search",
176
+ "gsd_resume",
177
+ "gsd_milestone_status",
178
+ "gsd_checkpoint_db",
179
+ "gsd_graph",
180
+ ]);
181
+
182
+ assert.ok(result.includes("ask_user_questions"));
183
+ assert.ok(result.includes("bash"));
184
+ assert.ok(result.includes("bg_shell"));
185
+ assert.ok(result.includes("read"));
186
+ assert.ok(result.includes("write"));
187
+ assert.ok(result.includes("gsd_plan_milestone"));
188
+ assert.ok(result.includes("gsd_complete_milestone"));
189
+ assert.ok(result.includes("gsd_task_complete"));
190
+ assert.ok(result.includes("gsd_summary_save"));
191
+ assert.ok(!result.includes("browser_wait_for"));
192
+ assert.ok(!result.includes("lsp"));
193
+ assert.ok(!result.includes("mac_find"));
194
+ assert.ok(!result.includes("subagent"));
195
+ assert.ok(!result.includes("gsd_graph"));
196
+ });
197
+
198
+ test("buildRequestScopedGsdToolSet scopes queued workflow custom-message requests", () => {
199
+ const result = buildRequestScopedGsdToolSet([
200
+ "ask_user_questions",
201
+ "bash",
202
+ "browser_wait_for",
203
+ "lsp",
204
+ "read",
205
+ "write",
206
+ "gsd_plan_milestone",
207
+ "gsd_complete_milestone",
208
+ "gsd_task_complete",
209
+ "gsd_graph",
210
+ "memory_query",
211
+ "capture_thought",
212
+ ], [{ customType: "gsd-run" }, { customType: "gsd-memory" }]);
213
+
214
+ assert.ok(result);
215
+ assert.ok(result.includes("ask_user_questions"));
216
+ assert.ok(result.includes("bash"));
217
+ assert.ok(result.includes("read"));
218
+ assert.ok(result.includes("write"));
219
+ assert.ok(result.includes("gsd_plan_milestone"));
220
+ assert.ok(result.includes("gsd_complete_milestone"));
221
+ assert.ok(!result.includes("browser_wait_for"));
222
+ assert.ok(!result.includes("lsp"));
223
+ assert.ok(!result.includes("gsd_graph"));
224
+ });
225
+
226
+ test("buildRequestScopedGsdToolSet ignores stale workflow messages outside the current request tail", () => {
227
+ assert.equal(buildRequestScopedGsdToolSet(["bash", "gsd_plan_milestone"], []), undefined);
228
+ });
229
+
230
+ test("scopeGsdWorkflowToolsForDispatch applies and restores per-unit skill visibility", () => {
231
+ const calls: Array<{ kind: "tools" | "skills"; value: string[] | undefined }> = [];
232
+ let activeTools = [
233
+ "bash",
234
+ "read",
235
+ "lsp",
236
+ "gsd_plan_milestone",
237
+ "gsd_decision_save",
238
+ "memory_query",
239
+ "capture_thought",
240
+ ];
241
+ let visibleSkills: string[] | undefined = ["previous-skill"];
242
+
243
+ const state = scopeGsdWorkflowToolsForDispatch({
244
+ getActiveTools: () => activeTools,
245
+ setActiveTools: (names) => {
246
+ activeTools = names;
247
+ calls.push({ kind: "tools", value: names });
248
+ },
249
+ getVisibleSkills: () => visibleSkills,
250
+ setVisibleSkills: (names) => {
251
+ visibleSkills = names;
252
+ calls.push({ kind: "skills", value: names });
253
+ },
254
+ }, "plan-milestone");
255
+
256
+ assert.ok(state);
257
+ assert.deepEqual(visibleSkills, [
258
+ "write-milestone-brief",
259
+ "decompose-into-slices",
260
+ "design-an-interface",
261
+ "grill-me",
262
+ "write-docs",
263
+ "api-design",
264
+ "tdd",
265
+ "verify-before-complete",
266
+ ]);
267
+ assert.ok(!activeTools.includes("lsp"));
268
+
269
+ restoreGsdWorkflowTools({
270
+ setActiveTools: (names) => {
271
+ activeTools = names;
272
+ calls.push({ kind: "tools", value: names });
273
+ },
274
+ setVisibleSkills: (names) => {
275
+ visibleSkills = names;
276
+ calls.push({ kind: "skills", value: names });
277
+ },
278
+ }, state);
279
+
280
+ assert.deepEqual(activeTools, [
281
+ "bash",
282
+ "read",
283
+ "lsp",
284
+ "gsd_plan_milestone",
285
+ "gsd_decision_save",
286
+ "memory_query",
287
+ "capture_thought",
288
+ ]);
289
+ assert.deepEqual(visibleSkills, ["previous-skill"]);
290
+ assert.equal(calls.filter((call) => call.kind === "skills").length, 2);
291
+ });
@@ -13,7 +13,7 @@ import {
13
13
  insertSlice,
14
14
  } from "../gsd-db.ts";
15
15
  import { registerAutoWorker } from "../db/auto-workers.ts";
16
- import { claimMilestoneLease } from "../db/milestone-leases.ts";
16
+ import { claimMilestoneLease, releaseMilestoneLease } from "../db/milestone-leases.ts";
17
17
  import {
18
18
  recordDispatchClaim,
19
19
  markRunning,
@@ -105,6 +105,55 @@ test("partial unique index rejects double-claim of the same active unit", (t) =>
105
105
  }
106
106
  });
107
107
 
108
+ test("recordDispatchClaim cancels stale active dispatch after lease takeover", (t) => {
109
+ const base = makeBase();
110
+ t.after(() => cleanup(base));
111
+ const { workerId: firstWorkerId, leaseToken: firstLeaseToken } = setup(base);
112
+
113
+ const first = recordDispatchClaim({
114
+ traceId: "t-first",
115
+ workerId: firstWorkerId,
116
+ milestoneLeaseToken: firstLeaseToken,
117
+ milestoneId: "M001",
118
+ sliceId: "S01",
119
+ unitType: "plan-slice",
120
+ unitId: "M001/S01",
121
+ });
122
+ assert.equal(first.ok, true);
123
+ if (!first.ok) return;
124
+ markRunning(first.dispatchId);
125
+
126
+ assert.equal(releaseMilestoneLease(firstWorkerId, "M001", firstLeaseToken), true);
127
+ const takeoverWorkerId = registerAutoWorker({ projectRootRealpath: base });
128
+ const takeoverLease = claimMilestoneLease(takeoverWorkerId, "M001");
129
+ assert.equal(takeoverLease.ok, true);
130
+ if (!takeoverLease.ok) return;
131
+
132
+ const second = recordDispatchClaim({
133
+ traceId: "t-takeover",
134
+ workerId: takeoverWorkerId,
135
+ milestoneLeaseToken: takeoverLease.token,
136
+ milestoneId: "M001",
137
+ sliceId: "S01",
138
+ unitType: "plan-slice",
139
+ unitId: "M001/S01",
140
+ attemptN: 2,
141
+ });
142
+
143
+ assert.equal(second.ok, true);
144
+ if (!second.ok) return;
145
+
146
+ const recent = getRecentForUnit("M001/S01", 5);
147
+ assert.equal(recent.length, 2);
148
+ assert.equal(recent[0].id, second.dispatchId);
149
+ assert.equal(recent[0].status, "claimed");
150
+ assert.equal(recent[0].worker_id, takeoverWorkerId);
151
+ assert.equal(recent[0].attempt_n, 2);
152
+ assert.equal(recent[1].id, first.dispatchId);
153
+ assert.equal(recent[1].status, "canceled");
154
+ assert.equal(recent[1].exit_reason, "stale-dispatch-lease-takeover");
155
+ });
156
+
108
157
  test("after markCompleted, a fresh claim for the same unit succeeds", (t) => {
109
158
  const base = makeBase();
110
159
  t.after(() => cleanup(base));