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,113 @@
1
+ // GSD-2 + src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts
2
+ // Regression: parallel-orchestrator's `_createMilestoneWorktree` must
3
+ // fast-forward a reused milestone branch onto integration before creating
4
+ // the worktree, matching the behavior added to the auto-mode path in
5
+ // commit 8996cb68e (#5549 post-merge audit, R3).
6
+
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { execFileSync } from "node:child_process";
10
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ import { _createMilestoneWorktree } from "../parallel-orchestrator.js";
15
+
16
+ const NO_PROMPT_ENV = {
17
+ ...process.env,
18
+ GIT_TERMINAL_PROMPT: "0",
19
+ GIT_AUTHOR_NAME: "test",
20
+ GIT_AUTHOR_EMAIL: "test@example.com",
21
+ GIT_COMMITTER_NAME: "test",
22
+ GIT_COMMITTER_EMAIL: "test@example.com",
23
+ };
24
+
25
+ function git(cwd: string, ...args: string[]): string {
26
+ return execFileSync("git", args, {
27
+ cwd,
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ encoding: "utf-8",
30
+ env: NO_PROMPT_ENV,
31
+ });
32
+ }
33
+
34
+ function rev(cwd: string, ref: string): string {
35
+ return git(cwd, "rev-parse", ref).trim();
36
+ }
37
+
38
+ describe("_createMilestoneWorktree fast-forwards reused milestone branches (#5549 R3)", () => {
39
+ let repo: string;
40
+
41
+ beforeEach(() => {
42
+ repo = mkdtempSync(join(tmpdir(), "parallel-orch-ff-"));
43
+ git(repo, "init", "-q", "-b", "main");
44
+ git(repo, "config", "user.email", "test@example.com");
45
+ git(repo, "config", "user.name", "test");
46
+ writeFileSync(join(repo, "seed.txt"), "seed\n");
47
+ git(repo, "add", "seed.txt");
48
+ git(repo, "commit", "-q", "-m", "initial");
49
+ // Minimal .gsd/ structure so syncGsdStateToWorktree doesn't crash on a
50
+ // bare repo. We don't care if it copies anything — only that the FF ran.
51
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
52
+ });
53
+
54
+ afterEach(() => {
55
+ rmSync(repo, { recursive: true, force: true });
56
+ });
57
+
58
+ test("reused milestone branch behind main is fast-forwarded before worktree creation", () => {
59
+ // Worker N drained M001 in a previous run, leaving milestone/M001 forked
60
+ // from old main. Worker N+1 picks up M002 — but the milestone/M002 branch
61
+ // was created from old main in a sibling run, so it's now N commits behind.
62
+ git(repo, "branch", "milestone/M002");
63
+ const m002Initial = rev(repo, "milestone/M002");
64
+
65
+ writeFileSync(join(repo, "seed.txt"), "main moved forward\n");
66
+ git(repo, "add", "seed.txt");
67
+ git(repo, "commit", "-q", "-m", "main advanced");
68
+ const mainTip = rev(repo, "main");
69
+
70
+ assert.notEqual(m002Initial, mainTip, "main must be ahead before the test");
71
+
72
+ // _createMilestoneWorktree may throw inside createWorktree/syncGsdStateToWorktree
73
+ // (e.g. if the worktree-manager has stricter requirements than this minimal
74
+ // repo provides). The fast-forward runs BEFORE those calls, so the branch
75
+ // ref should have moved regardless. Catch and assert on observable state.
76
+ try {
77
+ _createMilestoneWorktree(repo, "M002");
78
+ } catch {
79
+ // Fine — we only care that FF executed.
80
+ }
81
+
82
+ assert.equal(
83
+ rev(repo, "milestone/M002"),
84
+ mainTip,
85
+ "milestone/M002 must be fast-forwarded to main's tip before worktree is built",
86
+ );
87
+ });
88
+
89
+ test("diverged milestone branch is NOT touched (would lose work)", () => {
90
+ git(repo, "checkout", "-q", "-b", "milestone/M002");
91
+ writeFileSync(join(repo, "wip.txt"), "milestone-only work\n");
92
+ git(repo, "add", "wip.txt");
93
+ git(repo, "commit", "-q", "-m", "M002 work");
94
+ const m002Tip = rev(repo, "milestone/M002");
95
+
96
+ git(repo, "checkout", "-q", "main");
97
+ writeFileSync(join(repo, "seed.txt"), "main moved forward\n");
98
+ git(repo, "add", "seed.txt");
99
+ git(repo, "commit", "-q", "-m", "main advanced");
100
+
101
+ try {
102
+ _createMilestoneWorktree(repo, "M002");
103
+ } catch {
104
+ // ignored
105
+ }
106
+
107
+ assert.equal(
108
+ rev(repo, "milestone/M002"),
109
+ m002Tip,
110
+ "diverged milestone branch must NOT be touched — would lose committed work",
111
+ );
112
+ });
113
+ });
@@ -1819,7 +1819,7 @@ describe("checkFilePathConsistency completed-task output exemption (#4071)", ()
1819
1819
  );
1820
1820
  });
1821
1821
 
1822
- test("pending task at higher index still causes a missing-file error", (t) => {
1822
+ test("pending task at higher index does NOT cause a duplicate consistency error (ordering check handles it)", (t) => {
1823
1823
  const tempDir = join(tmpdir(), `pre-exec-fc-pending-${Date.now()}`);
1824
1824
  mkdirSync(tempDir, { recursive: true });
1825
1825
  t.after(() => rmSync(tempDir, { recursive: true, force: true }));
@@ -1841,14 +1841,16 @@ describe("checkFilePathConsistency completed-task output exemption (#4071)", ()
1841
1841
  }),
1842
1842
  ];
1843
1843
 
1844
+ // checkFilePathConsistency suppresses the error here because checkTaskOrdering
1845
+ // will fire a more precise "sequence violation" error for the same file.
1846
+ // The combined output of runPreExecutionChecks still flags the issue — just
1847
+ // once, via the ordering check, instead of twice.
1844
1848
  const results = checkFilePathConsistency(tasks, tempDir);
1845
1849
  assert.equal(
1846
1850
  results.length,
1847
- 1,
1848
- "pending task at higher index must still be flagged the file is not available yet",
1851
+ 0,
1852
+ "consistency check must not duplicate what the ordering check already reports",
1849
1853
  );
1850
- assert.equal(results[0].blocking, true);
1851
- assert.equal(results[0].target, "artifacts/output.json");
1852
1854
  });
1853
1855
  });
1854
1856
 
@@ -0,0 +1,230 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Verifies low-risk auto-prompt duplication cuts render through prompt builders.
3
+
4
+ import test from "node:test";
5
+ import type { TestContext } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+
11
+ import { invalidateAllCaches } from "../cache.ts";
12
+ import {
13
+ closeDatabase,
14
+ insertMilestone,
15
+ insertSlice,
16
+ insertTask,
17
+ openDatabase,
18
+ upsertMilestonePlanning,
19
+ } from "../gsd-db.ts";
20
+
21
+ type AutoPromptBuilders = typeof import("../auto-prompts.ts");
22
+
23
+ function makeBase(prefix: string): string {
24
+ const base = mkdtempSync(join(tmpdir(), prefix));
25
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
26
+ return base;
27
+ }
28
+
29
+ function cleanup(base: string): void {
30
+ try { closeDatabase(); } catch { /* noop */ }
31
+ invalidateAllCaches();
32
+ rmSync(base, { recursive: true, force: true });
33
+ }
34
+
35
+ async function loadAutoPromptBuilders(t: TestContext): Promise<AutoPromptBuilders> {
36
+ const previousGsdHome = process.env.GSD_HOME;
37
+ const isolatedHome = mkdtempSync(join(tmpdir(), "gsd-prompt-loader-home-"));
38
+ process.env.GSD_HOME = isolatedHome;
39
+ t.after(() => {
40
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
41
+ else process.env.GSD_HOME = previousGsdHome;
42
+ rmSync(isolatedHome, { recursive: true, force: true });
43
+ });
44
+ return import(`../auto-prompts.ts?promptDupCuts=${Date.now()}-${Math.random()}`) as Promise<AutoPromptBuilders>;
45
+ }
46
+
47
+ function seedDb(base: string, taskStatus = "complete"): void {
48
+ openDatabase(join(base, ".gsd", "gsd.db"));
49
+ insertMilestone({ id: "M001", title: "Prompt Cuts", status: "active", depends_on: [] });
50
+ upsertMilestonePlanning("M001", {
51
+ title: "Prompt Cuts",
52
+ status: "active",
53
+ vision: "Reduce duplicate prompt reads.",
54
+ successCriteria: ["Prompt builders render compact context."],
55
+ keyRisks: [],
56
+ proofStrategy: [],
57
+ verificationContract: "",
58
+ verificationIntegration: "",
59
+ verificationOperational: "",
60
+ verificationUat: "",
61
+ definitionOfDone: [],
62
+ requirementCoverage: "",
63
+ boundaryMapMarkdown: "",
64
+ });
65
+ insertSlice({
66
+ id: "S01",
67
+ milestoneId: "M001",
68
+ title: "Prompt Slice",
69
+ status: "active",
70
+ risk: "low",
71
+ depends: [],
72
+ demo: "",
73
+ sequence: 1,
74
+ });
75
+ insertTask({
76
+ id: "T01",
77
+ sliceId: "S01",
78
+ milestoneId: "M001",
79
+ title: "Task one",
80
+ status: taskStatus,
81
+ });
82
+ }
83
+
84
+ function writeRoadmapAndPlan(base: string): void {
85
+ writeFileSync(
86
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
87
+ [
88
+ "# M001 Roadmap",
89
+ "## Slices",
90
+ "- [ ] **S01: Prompt Slice** `risk:low` `depends:[]`",
91
+ ].join("\n"),
92
+ );
93
+ writeFileSync(
94
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
95
+ [
96
+ "# S01 Plan",
97
+ "",
98
+ "**Goal:** Reduce duplicate prompt reads.",
99
+ "",
100
+ "## Tasks",
101
+ "- [x] **T01: Task one** `est:15m`",
102
+ ].join("\n"),
103
+ );
104
+ }
105
+
106
+ function writeTaskSummary(base: string, options?: { blocker?: boolean; repeatedNarrative?: string }): void {
107
+ const narrative = options?.repeatedNarrative ?? "This full implementation narrative should stay out of closer prompts.";
108
+ writeFileSync(
109
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"),
110
+ [
111
+ "---",
112
+ "id: T01",
113
+ "parent: S01",
114
+ "milestone: M001",
115
+ "provides:",
116
+ " - prompt context reduction",
117
+ "key_files:",
118
+ " - src/resources/extensions/gsd/auto-prompts.ts",
119
+ "key_decisions:",
120
+ " - use compact excerpts before full reads",
121
+ "patterns_established:",
122
+ " - excerpt-first complete-slice context",
123
+ "observability_surfaces: []",
124
+ "duration: 15m",
125
+ "verification_result: passed",
126
+ "completed_at: 2026-05-06T12:00:00Z",
127
+ `blocker_discovered: ${options?.blocker ? "true" : "false"}`,
128
+ "---",
129
+ "",
130
+ "# T01: Task one",
131
+ "**One-line result.**",
132
+ "",
133
+ "## What Happened",
134
+ narrative,
135
+ "",
136
+ "## Verification",
137
+ "node:test passed.",
138
+ "",
139
+ "## Diagnostics",
140
+ "Prompt size stayed bounded.",
141
+ ].join("\n"),
142
+ );
143
+ }
144
+
145
+ test("execute-task rendering makes memory_query and template disk reads fallback-only", async (t) => {
146
+ const base = makeBase("gsd-execute-dup-cuts-");
147
+ t.after(() => cleanup(base));
148
+ invalidateAllCaches();
149
+
150
+ seedDb(base, "pending");
151
+ writeRoadmapAndPlan(base);
152
+ writeFileSync(
153
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-PLAN.md"),
154
+ "# T01 Plan\n\nDo the prompt edit.\n",
155
+ );
156
+
157
+ const { buildExecuteTaskPrompt } = await loadAutoPromptBuilders(t);
158
+ const prompt = await buildExecuteTaskPrompt("M001", "S01", "Prompt Slice", "T01", "Task one", base);
159
+
160
+ assert.match(prompt, /Call `memory_query`.*only when no injected memory block exists or the inlined memory\/context is insufficient/s);
161
+ assert.doesNotMatch(prompt, /Call `memory_query` with 2-4 keywords from the task title and touched files unless this is purely mechanical/);
162
+ assert.match(prompt, /Use the inlined Task Summary template below/);
163
+ assert.match(prompt, /Read `.*task-summary\.md` only if the inlined template is absent or visibly truncated/);
164
+ assert.doesNotMatch(prompt, /Read the template at `.*task-summary\.md`/);
165
+ assert.match(prompt, /### Output Template: Task Summary/);
166
+ });
167
+
168
+ test("complete-slice renders task summary excerpts without full summary bodies", async (t) => {
169
+ const base = makeBase("gsd-complete-slice-excerpts-");
170
+ t.after(() => cleanup(base));
171
+ invalidateAllCaches();
172
+
173
+ seedDb(base);
174
+ writeRoadmapAndPlan(base);
175
+ const repeatedNarrative = "FULL_TASK_BODY_SHOULD_NOT_RENDER ".repeat(40);
176
+ writeTaskSummary(base, { repeatedNarrative });
177
+
178
+ const { buildCompleteSlicePrompt } = await loadAutoPromptBuilders(t);
179
+ const prompt = await buildCompleteSlicePrompt("M001", "Prompt Cuts", "S01", "Prompt Slice", base);
180
+
181
+ assert.match(prompt, /### Task Summary: T01 \(excerpt\)/);
182
+ assert.match(prompt, /On-demand.*read `\.gsd\/milestones\/M001\/slices\/S01\/tasks\/T01-SUMMARY\.md` only when this excerpt is absent\/truncated/s);
183
+ assert.doesNotMatch(prompt, /FULL_TASK_BODY_SHOULD_NOT_RENDER/);
184
+ assert.match(prompt, /Review the inlined task-summary excerpts/);
185
+ });
186
+
187
+ test("complete-slice caps malformed task summaries instead of inlining full bodies", async (t) => {
188
+ const base = makeBase("gsd-complete-slice-malformed-excerpts-");
189
+ t.after(() => cleanup(base));
190
+ invalidateAllCaches();
191
+
192
+ seedDb(base);
193
+ writeRoadmapAndPlan(base);
194
+ writeFileSync(
195
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"),
196
+ [
197
+ "# Legacy summary without frontmatter id",
198
+ "LEGACY_FULL_BODY_SHOULD_BE_CAPPED ".repeat(200),
199
+ ].join("\n"),
200
+ );
201
+
202
+ const { buildCompleteSlicePrompt } = await loadAutoPromptBuilders(t);
203
+ const prompt = await buildCompleteSlicePrompt("M001", "Prompt Cuts", "S01", "Prompt Slice", base);
204
+
205
+ assert.match(prompt, /Truncated malformed summary/);
206
+ assert.ok(prompt.length < 20_000);
207
+ assert.ok((prompt.match(/LEGACY_FULL_BODY_SHOULD_BE_CAPPED/g) ?? []).length < 60);
208
+ });
209
+
210
+ test("replan-slice renders blocker summary excerpt and tells the agent to read full only on demand", async (t) => {
211
+ const base = makeBase("gsd-replan-excerpts-");
212
+ t.after(() => cleanup(base));
213
+ invalidateAllCaches();
214
+
215
+ seedDb(base);
216
+ writeRoadmapAndPlan(base);
217
+ writeTaskSummary(base, {
218
+ blocker: true,
219
+ repeatedNarrative: "FULL_BLOCKER_BODY_SHOULD_NOT_RENDER ".repeat(40),
220
+ });
221
+
222
+ const { buildReplanSlicePrompt } = await loadAutoPromptBuilders(t);
223
+ const prompt = await buildReplanSlicePrompt("M001", "Prompt Cuts", "S01", "Prompt Slice", base);
224
+
225
+ assert.match(prompt, /### Blocker Task Summary: T01 \(excerpt\)/);
226
+ assert.match(prompt, /Use the inlined blocker summary excerpt first/);
227
+ assert.match(prompt, /Read the full blocker task summary only if the excerpt is absent, marked truncated, or lacks the specific blocker evidence needed to replan/);
228
+ assert.doesNotMatch(prompt, /FULL_BLOCKER_BODY_SHOULD_NOT_RENDER/);
229
+ assert.doesNotMatch(prompt, /Read the blocker task summary carefully/);
230
+ });
@@ -28,8 +28,8 @@ describe('query-tools ensureDbOpen usage (#3672)', () => {
28
28
  });
29
29
 
30
30
  test('calls ensureDbOpen() before DB queries', () => {
31
- assert.match(source, /await ensureDbOpen\(\)/,
32
- 'query-tools should call await ensureDbOpen()');
31
+ assert.match(source, /await ensureDbOpen\([^)]*\)/,
32
+ 'query-tools should call await ensureDbOpen(...)');
33
33
  });
34
34
 
35
35
  test('no longer imports isDbAvailable in the execute path', () => {
@@ -41,7 +41,7 @@ describe('query-tools ensureDbOpen usage (#3672)', () => {
41
41
  });
42
42
 
43
43
  test('uses dbAvailable result from ensureDbOpen', () => {
44
- assert.match(source, /dbAvailable\s*=\s*await ensureDbOpen\(\)/,
44
+ assert.match(source, /dbAvailable\s*=\s*await ensureDbOpen\([^)]*\)/,
45
45
  'should store ensureDbOpen result in dbAvailable');
46
46
  });
47
47
  });
@@ -24,7 +24,7 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
24
24
  it('savedTools is declared before the discuss scoping block', () => {
25
25
  // savedTools must be declared before the discuss-* check
26
26
  const savedToolsDecl = src.indexOf('let savedTools')
27
- const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-"))')
27
+ const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-")')
28
28
  assert.ok(savedToolsDecl !== -1, 'savedTools variable must be declared')
29
29
  assert.ok(discussCheck !== -1, 'discuss-* type check must exist')
30
30
  assert.ok(
@@ -33,40 +33,61 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
33
33
  )
34
34
  })
35
35
 
36
- it('savedTools captures current tools inside the discuss block', () => {
37
- const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-"))')
36
+ it('savedTools captures current tools before scoping can mutate active state', () => {
37
+ const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-")')
38
38
  assert.ok(discussCheck !== -1)
39
39
 
40
- // Look for savedTools assignment within the discuss block
41
- const blockAfter = extractSourceRegion(src, 'if (unitType?.startsWith("discuss-"))')
40
+ const currentToolsDecl = src.indexOf('const currentTools = pi.getActiveTools()')
41
+ const savedToolsAssign = src.indexOf('savedTools = {', currentToolsDecl)
42
+ const firstMutation = src.indexOf('pi.setActiveTools(scopedTools)')
42
43
  assert.ok(
43
- blockAfter.includes('savedTools = currentTools'),
44
- 'savedTools must be assigned from currentTools inside the discuss block',
44
+ currentToolsDecl !== -1 && savedToolsAssign !== -1 && firstMutation !== -1,
45
+ 'guided-flow.ts must capture current tools, save them, and then scope active tools',
46
+ )
47
+ assert.ok(
48
+ currentToolsDecl < savedToolsAssign && savedToolsAssign < firstMutation,
49
+ 'savedTools must capture currentTools before any discuss scoping mutation',
50
+ )
51
+ assert.ok(
52
+ src.slice(savedToolsAssign, firstMutation).includes('tools: currentTools'),
53
+ 'savedTools must include currentTools before the first scoping mutation',
45
54
  )
46
55
  })
47
56
 
57
+ it('scoping and workflow read happen inside the restore try block', () => {
58
+ const savedToolsDecl = src.indexOf('let savedTools')
59
+ const tryIdx = src.indexOf('try {', savedToolsDecl)
60
+ const firstMutation = src.indexOf('pi.setActiveTools(scopedTools)')
61
+ const workflowRead = src.indexOf('readFileSync(workflowPath')
62
+ const finallyIdx = src.indexOf('} finally {', tryIdx)
63
+
64
+ assert.ok(savedToolsDecl !== -1, 'savedTools variable must be declared')
65
+ assert.ok(tryIdx !== -1, 'restore try block must exist')
66
+ assert.ok(firstMutation !== -1, 'discuss scoping mutation must exist')
67
+ assert.ok(workflowRead !== -1, 'workflow file read must exist')
68
+ assert.ok(finallyIdx !== -1, 'restore finally block must exist')
69
+ assert.ok(tryIdx < firstMutation && firstMutation < finallyIdx, 'scoping mutation must be inside try/finally')
70
+ assert.ok(tryIdx < workflowRead && workflowRead < finallyIdx, 'workflow file read must be inside try/finally')
71
+ })
72
+
48
73
  it('savedTools is restored after sendMessage', () => {
49
74
  // #4573: guided-flow.ts now contains multiple `triggerTurn: true` calls
50
75
  // (ready-phrase and empty-turn recovery paths). The discuss-flow scoping
51
- // sendMessage is the one that follows `savedTools = currentTools`, so
76
+ // sendMessage is the one that follows `tools: currentTools`, so
52
77
  // anchor the search there rather than at the first `triggerTurn: true`.
53
- const savedToolsAssign = src.indexOf('savedTools = currentTools')
54
- assert.ok(savedToolsAssign !== -1, 'savedTools = currentTools must exist')
78
+ const savedToolsAssign = src.indexOf('tools: currentTools')
79
+ assert.ok(savedToolsAssign !== -1, 'savedTools must capture currentTools')
55
80
 
56
81
  const sendMsg = src.indexOf('triggerTurn: true', savedToolsAssign)
57
82
  assert.ok(sendMsg !== -1, 'discuss-flow sendMessage with triggerTurn must exist after savedTools capture')
58
83
 
59
- // After sendMessage, savedTools should be restored via setActiveTools.
84
+ // After sendMessage, savedTools should be restored via the shared helper.
60
85
  // Use fromIdx to anchor at the discuss-flow sendMessage, not the first
61
86
  // triggerTurn: true occurrence in the file.
62
87
  const afterSend = extractSourceRegion(src, 'triggerTurn: true', { fromIdx: savedToolsAssign })
63
88
  assert.ok(
64
- afterSend.includes('if (savedTools)'),
65
- 'savedTools restoration guard must exist after sendMessage',
66
- )
67
- assert.ok(
68
- afterSend.includes('setActiveTools(savedTools)'),
69
- 'setActiveTools(savedTools) must be called to restore the full tool set',
89
+ afterSend.includes('restoreGsdWorkflowTools(pi, savedTools)'),
90
+ 'restoreGsdWorkflowTools(pi, savedTools) must restore the full scoped state',
70
91
  )
71
92
  })
72
93
  })
@@ -0,0 +1,96 @@
1
+ // GSD-2 + src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts
2
+ // Regression: bootstrap must rederive `currentMilestoneId` from an unmerged
3
+ // completed milestone branch when in-memory state was lost across a process
4
+ // restart (#5538-followup).
5
+
6
+ import test from "node:test";
7
+ import assert from "node:assert/strict";
8
+
9
+ import { _selectResumableMilestone } from "../auto-start.js";
10
+
11
+ const ALL_COMPLETE = (_id: string) => true;
12
+ const NEVER_COMPLETE = (_id: string) => false;
13
+ const HAS_COMMITS = (_branch: string) => 1;
14
+ const NO_COMMITS = (_branch: string) => 0;
15
+
16
+ test("returns null when no branches exist", () => {
17
+ const result = _selectResumableMilestone([], new Set(), ALL_COMPLETE, HAS_COMMITS);
18
+ assert.equal(result, null);
19
+ });
20
+
21
+ test("returns null when every milestone branch is already merged", () => {
22
+ const branches = ["milestone/M001", "milestone/M002"];
23
+ const merged = new Set(branches);
24
+ const result = _selectResumableMilestone(branches, merged, ALL_COMPLETE, HAS_COMMITS);
25
+ assert.equal(result, null);
26
+ });
27
+
28
+ test("returns null when no candidate milestones are complete", () => {
29
+ const branches = ["milestone/M002"];
30
+ const result = _selectResumableMilestone(branches, new Set(), NEVER_COMPLETE, HAS_COMMITS);
31
+ assert.equal(result, null);
32
+ });
33
+
34
+ test("returns null when no branch has commits ahead", () => {
35
+ const branches = ["milestone/M002"];
36
+ const result = _selectResumableMilestone(branches, new Set(), ALL_COMPLETE, NO_COMMITS);
37
+ assert.equal(result, null);
38
+ });
39
+
40
+ test("returns the unmerged completed milestone (regression: M002 stranded after restart)", () => {
41
+ // Repro of test12345 state: M001 ✅ merged, M002 ✅ unmerged, M003 🔄.
42
+ // Bootstrap must seed currentMilestoneId = "M002" so the loop's transition
43
+ // guard fires when the next iteration sees mid="M003".
44
+ const branches = ["milestone/M002", "milestone/M003"];
45
+ const merged = new Set<string>(); // none merged from this list
46
+ const isComplete = (id: string) => id === "M002"; // M003 still in progress
47
+ const result = _selectResumableMilestone(branches, merged, isComplete, HAS_COMMITS);
48
+ assert.equal(result, "M002");
49
+ });
50
+
51
+ test("picks the lex-greatest milestone when multiple candidates exist", () => {
52
+ // M001, M002 both unmerged complete -> pick M002 (the most recent).
53
+ const branches = ["milestone/M001", "milestone/M002"];
54
+ const result = _selectResumableMilestone(
55
+ branches,
56
+ new Set(),
57
+ ALL_COMPLETE,
58
+ HAS_COMMITS,
59
+ );
60
+ assert.equal(result, "M002");
61
+ });
62
+
63
+ test("ignores branch names that don't follow the milestone/ prefix", () => {
64
+ const branches = ["feature/something", "milestone/M002", "fix/bug-1"];
65
+ const result = _selectResumableMilestone(
66
+ branches,
67
+ new Set(),
68
+ ALL_COMPLETE,
69
+ HAS_COMMITS,
70
+ );
71
+ assert.equal(result, "M002");
72
+ });
73
+
74
+ test("isComplete callback throwing skips that milestone but does not crash", () => {
75
+ const branches = ["milestone/M001", "milestone/M002"];
76
+ const isComplete = (id: string) => {
77
+ if (id === "M001") throw new Error("db unavailable for M001");
78
+ return true;
79
+ };
80
+ // Implementation choice: a thrown isComplete is propagated. Wrapping in
81
+ // try/catch happens at the production wrapper level (findUnmergedCompletedMilestone).
82
+ // Verify the helper itself surfaces the error so callers see real failures.
83
+ assert.throws(() => _selectResumableMilestone(branches, new Set(), isComplete, HAS_COMMITS));
84
+ });
85
+
86
+ test("commitsAhead callback throwing for one branch falls through to others", () => {
87
+ // Production wrapper: commits-ahead failures should not abort the search.
88
+ // The helper catches throws from commitsAhead per-branch and treats as 0.
89
+ const branches = ["milestone/M001", "milestone/M002"];
90
+ const commitsAhead = (branch: string) => {
91
+ if (branch === "milestone/M001") throw new Error("rev-walk failed");
92
+ return 5;
93
+ };
94
+ const result = _selectResumableMilestone(branches, new Set(), ALL_COMPLETE, commitsAhead);
95
+ assert.equal(result, "M002");
96
+ });
@@ -206,6 +206,83 @@ test("session_start does NOT call setFooter or suppress gsd-health when isAutoAc
206
206
  assert.equal(healthWidgetHideCount, 0, "gsd-health must NOT be hidden when isAutoActive() is false");
207
207
  });
208
208
 
209
+ test("session_start installs the welcome screen as the TUI header", async (t) => {
210
+ const dir = join(
211
+ tmpdir(),
212
+ `gsd-welcome-header-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
213
+ );
214
+ mkdirSync(join(dir, "bin"), { recursive: true });
215
+ mkdirSync(join(dir, "dist"), { recursive: true });
216
+ writeFileSync(join(dir, "bin", "welcome-screen.js"), "export const stale = true;\n", "utf-8");
217
+ writeFileSync(
218
+ join(dir, "dist", "welcome-screen.js"),
219
+ [
220
+ "export function buildWelcomeScreenLines(opts) {",
221
+ " return [`welcome ${opts.version} ${opts.remoteChannel ?? 'none'} ${opts.width}`];",
222
+ "}",
223
+ "",
224
+ ].join("\n"),
225
+ "utf-8",
226
+ );
227
+
228
+ const originalCwd = process.cwd();
229
+ const originalGsdPkgRoot = process.env.GSD_PKG_ROOT;
230
+ const originalGsdBinPath = process.env.GSD_BIN_PATH;
231
+ const originalGsdVersion = process.env.GSD_VERSION;
232
+ const originalFirstRunBanner = process.env.GSD_FIRST_RUN_BANNER;
233
+ process.chdir(dir);
234
+ process.env.GSD_PKG_ROOT = dir;
235
+ process.env.GSD_BIN_PATH = join(dir, "bin", "loader.js");
236
+ process.env.GSD_VERSION = "9.9.9-test";
237
+ delete process.env.GSD_FIRST_RUN_BANNER;
238
+ t.after(() => {
239
+ process.chdir(originalCwd);
240
+ if (originalGsdPkgRoot === undefined) delete process.env.GSD_PKG_ROOT;
241
+ else process.env.GSD_PKG_ROOT = originalGsdPkgRoot;
242
+ if (originalGsdBinPath === undefined) delete process.env.GSD_BIN_PATH;
243
+ else process.env.GSD_BIN_PATH = originalGsdBinPath;
244
+ if (originalGsdVersion === undefined) delete process.env.GSD_VERSION;
245
+ else process.env.GSD_VERSION = originalGsdVersion;
246
+ if (originalFirstRunBanner === undefined) delete process.env.GSD_FIRST_RUN_BANNER;
247
+ else process.env.GSD_FIRST_RUN_BANNER = originalFirstRunBanner;
248
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
249
+ });
250
+
251
+ const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
252
+ const pi = {
253
+ on(event: string, handler: (event: unknown, ctx: any) => Promise<void> | void) {
254
+ handlers.set(event, handler);
255
+ },
256
+ } as any;
257
+
258
+ registerHooks(pi, []);
259
+
260
+ const sessionStart = handlers.get("session_start");
261
+ assert.ok(sessionStart, "session_start handler must be registered");
262
+
263
+ let headerFactory: ((tui: unknown, theme: unknown) => { render(width: number): string[] }) | undefined;
264
+ await sessionStart!({}, {
265
+ hasUI: true,
266
+ ui: {
267
+ notify: () => {},
268
+ setStatus: () => {},
269
+ setFooter: () => {},
270
+ setHeader: (factory: typeof headerFactory) => {
271
+ headerFactory = factory;
272
+ },
273
+ setWorkingMessage: () => {},
274
+ onTerminalInput: () => () => {},
275
+ setWidget: () => {},
276
+ },
277
+ sessionManager: { getSessionId: () => null },
278
+ model: null,
279
+ } as any);
280
+
281
+ assert.equal(typeof headerFactory, "function", "session_start should install a header factory");
282
+ const header = headerFactory!({}, {});
283
+ assert.deepEqual(header.render(123), ["welcome 9.9.9-test none 123"]);
284
+ });
285
+
209
286
  test("session_start and session_switch apply disabled model provider policy from current preferences", async (t) => {
210
287
  const dir = join(
211
288
  tmpdir(),