gsd-pi 2.67.0-dev.2142d3e → 2.67.0-dev.2367d7e

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 (233) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -70
  3. package/dist/resources/extensions/gsd/auto/session.js +10 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-start.js +16 -30
  6. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  7. package/dist/resources/extensions/gsd/auto.js +121 -59
  8. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  9. package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
  10. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
  11. package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
  12. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
  13. package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
  14. package/dist/resources/extensions/gsd/doctor.js +8 -4
  15. package/dist/resources/extensions/gsd/gsd-db.js +11 -0
  16. package/dist/resources/extensions/gsd/guided-flow.js +40 -31
  17. package/dist/resources/extensions/gsd/init-wizard.js +15 -12
  18. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  19. package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
  20. package/dist/resources/extensions/gsd/state.js +7 -2
  21. package/dist/resources/extensions/gsd/workflow-mcp.js +90 -19
  22. package/dist/web/standalone/.next/BUILD_ID +1 -1
  23. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  24. package/dist/web/standalone/.next/build-manifest.json +3 -3
  25. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  26. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.html +1 -1
  46. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  47. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  48. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  49. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  60. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  61. package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  62. package/package.json +4 -2
  63. package/packages/mcp-server/dist/cli.d.ts +9 -0
  64. package/packages/mcp-server/dist/cli.d.ts.map +1 -0
  65. package/packages/mcp-server/dist/cli.js +58 -0
  66. package/packages/mcp-server/dist/cli.js.map +1 -0
  67. package/packages/mcp-server/dist/index.d.ts +20 -0
  68. package/packages/mcp-server/dist/index.d.ts.map +1 -0
  69. package/packages/mcp-server/dist/index.js +14 -0
  70. package/packages/mcp-server/dist/index.js.map +1 -0
  71. package/packages/mcp-server/dist/readers/captures.d.ts +25 -0
  72. package/packages/mcp-server/dist/readers/captures.d.ts.map +1 -0
  73. package/packages/mcp-server/dist/readers/captures.js +67 -0
  74. package/packages/mcp-server/dist/readers/captures.js.map +1 -0
  75. package/packages/mcp-server/dist/readers/doctor-lite.d.ts +20 -0
  76. package/packages/mcp-server/dist/readers/doctor-lite.d.ts.map +1 -0
  77. package/packages/mcp-server/dist/readers/doctor-lite.js +173 -0
  78. package/packages/mcp-server/dist/readers/doctor-lite.js.map +1 -0
  79. package/packages/mcp-server/dist/readers/index.d.ts +14 -0
  80. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -0
  81. package/packages/mcp-server/dist/readers/index.js +10 -0
  82. package/packages/mcp-server/dist/readers/index.js.map +1 -0
  83. package/packages/mcp-server/dist/readers/knowledge.d.ts +18 -0
  84. package/packages/mcp-server/dist/readers/knowledge.d.ts.map +1 -0
  85. package/packages/mcp-server/dist/readers/knowledge.js +82 -0
  86. package/packages/mcp-server/dist/readers/knowledge.js.map +1 -0
  87. package/packages/mcp-server/dist/readers/metrics.d.ts +32 -0
  88. package/packages/mcp-server/dist/readers/metrics.d.ts.map +1 -0
  89. package/packages/mcp-server/dist/readers/metrics.js +74 -0
  90. package/packages/mcp-server/dist/readers/metrics.js.map +1 -0
  91. package/packages/mcp-server/dist/readers/paths.d.ts +42 -0
  92. package/packages/mcp-server/dist/readers/paths.d.ts.map +1 -0
  93. package/packages/mcp-server/dist/readers/paths.js +199 -0
  94. package/packages/mcp-server/dist/readers/paths.js.map +1 -0
  95. package/packages/mcp-server/dist/readers/roadmap.d.ts +26 -0
  96. package/packages/mcp-server/dist/readers/roadmap.d.ts.map +1 -0
  97. package/packages/mcp-server/dist/readers/roadmap.js +194 -0
  98. package/packages/mcp-server/dist/readers/roadmap.js.map +1 -0
  99. package/packages/mcp-server/dist/readers/state.d.ts +43 -0
  100. package/packages/mcp-server/dist/readers/state.d.ts.map +1 -0
  101. package/packages/mcp-server/dist/readers/state.js +184 -0
  102. package/packages/mcp-server/dist/readers/state.js.map +1 -0
  103. package/packages/mcp-server/dist/server.d.ts +28 -0
  104. package/packages/mcp-server/dist/server.d.ts.map +1 -0
  105. package/packages/mcp-server/dist/server.js +319 -0
  106. package/packages/mcp-server/dist/server.js.map +1 -0
  107. package/packages/mcp-server/dist/session-manager.d.ts +54 -0
  108. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -0
  109. package/packages/mcp-server/dist/session-manager.js +284 -0
  110. package/packages/mcp-server/dist/session-manager.js.map +1 -0
  111. package/packages/mcp-server/dist/types.d.ts +61 -0
  112. package/packages/mcp-server/dist/types.d.ts.map +1 -0
  113. package/packages/mcp-server/dist/types.js +11 -0
  114. package/packages/mcp-server/dist/types.js.map +1 -0
  115. package/packages/mcp-server/dist/workflow-tools.d.ts +9 -0
  116. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -0
  117. package/packages/mcp-server/dist/workflow-tools.js +532 -0
  118. package/packages/mcp-server/dist/workflow-tools.js.map +1 -0
  119. package/packages/mcp-server/src/workflow-tools.ts +13 -2
  120. package/packages/mcp-server/tsconfig.json +1 -1
  121. package/packages/pi-agent-core/dist/agent-loop.js +14 -6
  122. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  123. package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
  124. package/packages/pi-agent-core/src/agent-loop.ts +20 -6
  125. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  126. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  127. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  128. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  129. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  130. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  132. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  134. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  136. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  143. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
  144. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  145. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  149. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  151. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  152. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  153. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  154. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -12
  155. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  156. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  157. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  158. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  159. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
  160. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
  161. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
  162. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  163. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -15
  164. package/packages/rpc-client/dist/index.d.ts +10 -0
  165. package/packages/rpc-client/dist/index.d.ts.map +1 -0
  166. package/packages/rpc-client/dist/index.js +9 -0
  167. package/packages/rpc-client/dist/index.js.map +1 -0
  168. package/packages/rpc-client/dist/jsonl.d.ts +17 -0
  169. package/packages/rpc-client/dist/jsonl.d.ts.map +1 -0
  170. package/packages/rpc-client/dist/jsonl.js +54 -0
  171. package/packages/rpc-client/dist/jsonl.js.map +1 -0
  172. package/packages/rpc-client/dist/rpc-client.d.ts +259 -0
  173. package/packages/rpc-client/dist/rpc-client.d.ts.map +1 -0
  174. package/packages/rpc-client/dist/rpc-client.js +541 -0
  175. package/packages/rpc-client/dist/rpc-client.js.map +1 -0
  176. package/packages/rpc-client/dist/rpc-client.test.d.ts +2 -0
  177. package/packages/rpc-client/dist/rpc-client.test.d.ts.map +1 -0
  178. package/packages/rpc-client/dist/rpc-client.test.js +477 -0
  179. package/packages/rpc-client/dist/rpc-client.test.js.map +1 -0
  180. package/packages/rpc-client/dist/rpc-types.d.ts +566 -0
  181. package/packages/rpc-client/dist/rpc-types.d.ts.map +1 -0
  182. package/packages/rpc-client/dist/rpc-types.js +12 -0
  183. package/packages/rpc-client/dist/rpc-types.js.map +1 -0
  184. package/scripts/ensure-workspace-builds.cjs +2 -0
  185. package/scripts/link-workspace-packages.cjs +21 -14
  186. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +190 -93
  187. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +89 -116
  188. package/src/resources/extensions/gsd/auto/session.ts +10 -0
  189. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  190. package/src/resources/extensions/gsd/auto-start.ts +23 -55
  191. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  192. package/src/resources/extensions/gsd/auto.ts +133 -64
  193. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  194. package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
  195. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
  196. package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
  197. package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
  198. package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
  199. package/src/resources/extensions/gsd/doctor.ts +9 -5
  200. package/src/resources/extensions/gsd/gsd-db.ts +12 -0
  201. package/src/resources/extensions/gsd/guided-flow.ts +42 -36
  202. package/src/resources/extensions/gsd/init-wizard.ts +17 -11
  203. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  204. package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
  205. package/src/resources/extensions/gsd/state.ts +7 -1
  206. package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +29 -0
  207. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  208. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  209. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  210. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  211. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  212. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
  213. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  214. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  215. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
  216. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
  217. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  218. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  219. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  220. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
  221. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
  222. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  223. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +212 -13
  224. package/src/resources/extensions/gsd/workflow-mcp.ts +106 -19
  225. package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
  226. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
  227. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
  228. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
  229. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
  230. package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
  231. package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +0 -121
  232. /package/dist/web/standalone/.next/static/{xR6qurkuYSvyjBjRyJLxG → WMDT_0C0XDkBKtsAI_AX4}/_buildManifest.js +0 -0
  233. /package/dist/web/standalone/.next/static/{xR6qurkuYSvyjBjRyJLxG → WMDT_0C0XDkBKtsAI_AX4}/_ssgManifest.js +0 -0
@@ -219,39 +219,6 @@ describe('doctor-proactive', async () => {
219
219
  assert.ok(result.fixesApplied.some((f: string) => f.includes("STATE.md")), "reports STATE.md status as info");
220
220
  });
221
221
 
222
- test('health gate: pre-dispatch snapshot includes new untracked files', async () => {
223
- const dir = createRepoWithActiveMilestone();
224
- cleanups.push(dir);
225
-
226
- const pastDate = new Date(Date.now() - 45 * 60 * 1000).toISOString();
227
- run(`git commit --amend --no-edit --date="${pastDate}"`, dir);
228
- execSync(`git commit --amend --no-edit`, {
229
- cwd: dir,
230
- stdio: ["ignore", "pipe", "pipe"],
231
- encoding: "utf-8",
232
- env: { ...process.env, GIT_COMMITTER_DATE: pastDate },
233
- });
234
-
235
- writeFileSync(join(dir, "README.md"), "# test\nmodified content\n");
236
- writeFileSync(join(dir, "new-untracked.ts"), "export const preserved = true;\n");
237
-
238
- const result = await preDispatchHealthGate(dir);
239
- assert.ok(result.proceed, "dispatch still proceeds after snapshotting");
240
- assert.ok(
241
- result.fixesApplied.some((f: string) => f.includes("gsd snapshot")),
242
- "pre-dispatch gate creates a snapshot commit",
243
- );
244
-
245
- const log = run("git log -1 --oneline", dir);
246
- assert.ok(log.includes("gsd snapshot"), "snapshot commit is created");
247
-
248
- const files = run("git show --name-only --format= HEAD", dir);
249
- assert.ok(files.includes("README.md"), "snapshot keeps tracked modifications");
250
- assert.ok(files.includes("new-untracked.ts"), "snapshot also includes new untracked files");
251
- const status = run("git status --short", dir);
252
- assert.ok(!status.includes("new-untracked.ts"), "snapshot does not leave the new source file untracked");
253
- });
254
-
255
222
  test('health gate: stale crash lock auto-cleared', async () => {
256
223
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
257
224
  cleanups.push(dir);
@@ -0,0 +1,169 @@
1
+ /**
2
+ * GSD-2 — Regression tests for merge cwd restore (#2929)
3
+ * merge-cwd-restore.test.ts — Regression tests for #2929.
4
+ *
5
+ * Verifies:
6
+ * 1. MergeConflictError restores process.cwd() to the pre-merge directory.
7
+ * 2. autoCommitDirtyState does not run on the integration branch when cwd
8
+ * leaked there from a prior failed merge (parallel mode).
9
+ *
10
+ * Bug: PR #2298 added a stash lifecycle around mergeMilestoneToMain but the
11
+ * MergeConflictError throw path omitted the process.chdir(previousCwd) that
12
+ * the dirty-working-tree and divergence handlers both include. In parallel
13
+ * merge sequences, this left cwd on the integration branch, causing the next
14
+ * merge's autoCommitDirtyState to commit dirty files from OTHER milestones
15
+ * onto main.
16
+ */
17
+
18
+ import { describe, test, beforeEach, afterEach } from "node:test";
19
+ import assert from "node:assert/strict";
20
+ import {
21
+ mkdtempSync,
22
+ mkdirSync,
23
+ writeFileSync,
24
+ rmSync,
25
+ realpathSync,
26
+ } from "node:fs";
27
+ import { join } from "node:path";
28
+ import { tmpdir } from "node:os";
29
+ import { execSync } from "node:child_process";
30
+
31
+ import { mergeMilestoneToMain } from "../../auto-worktree.ts";
32
+ import { MergeConflictError } from "../../git-service.ts";
33
+
34
+ function run(cmd: string, cwd: string): string {
35
+ return execSync(cmd, {
36
+ cwd,
37
+ stdio: ["ignore", "pipe", "pipe"],
38
+ encoding: "utf-8",
39
+ }).trim();
40
+ }
41
+
42
+ function createTempRepo(): string {
43
+ const dir = realpathSync(
44
+ mkdtempSync(join(tmpdir(), "merge-cwd-restore-test-")),
45
+ );
46
+ run("git init -b main", dir);
47
+ run("git config user.email test@test.com", dir);
48
+ run("git config user.name Test", dir);
49
+ writeFileSync(join(dir, "README.md"), "# test\n");
50
+ writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n");
51
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
52
+ writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
53
+ run("git add .", dir);
54
+ run("git commit -m init", dir);
55
+ return dir;
56
+ }
57
+
58
+ function makeRoadmap(mid: string, title: string): string {
59
+ return [
60
+ `# ${mid}: Test milestone`,
61
+ "",
62
+ "## Slices",
63
+ "- [x] **S01: Test slice**",
64
+ ].join("\n");
65
+ }
66
+
67
+ describe("merge cwd restore (#2929)", () => {
68
+ let repo: string;
69
+ let savedCwd: string;
70
+
71
+ beforeEach(() => {
72
+ savedCwd = process.cwd();
73
+ repo = createTempRepo();
74
+ });
75
+
76
+ afterEach(() => {
77
+ process.chdir(savedCwd);
78
+ try { run("git reset --hard HEAD", repo); } catch { /* */ }
79
+ rmSync(repo, { recursive: true, force: true });
80
+ });
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────
83
+ // Test 1: MergeConflictError restores cwd (#2929 bug 2)
84
+ // ─────────────────────────────────────────────────────────────────────────
85
+
86
+ test("MergeConflictError restores cwd to pre-merge directory", () => {
87
+ // Create milestone branch that modifies README.md
88
+ run("git checkout -b milestone/M010", repo);
89
+ writeFileSync(join(repo, "README.md"), "# M010 version\n");
90
+ run("git add .", repo);
91
+ run('git commit -m "M010 changes README"', repo);
92
+ run("git checkout main", repo);
93
+
94
+ // Modify README.md on main to create a conflict
95
+ writeFileSync(join(repo, "README.md"), "# main version (diverged)\n");
96
+ run("git add .", repo);
97
+ run('git commit -m "main diverges README"', repo);
98
+
99
+ // cwd must be repo root (simulates parallel-merge calling from project root)
100
+ process.chdir(repo);
101
+ const cwdBefore = process.cwd();
102
+
103
+ let caught: unknown = null;
104
+ try {
105
+ mergeMilestoneToMain(repo, "M010", makeRoadmap("M010", "Conflict test"));
106
+ } catch (err) {
107
+ caught = err;
108
+ }
109
+
110
+ // Should have thrown a MergeConflictError
111
+ assert.ok(caught instanceof MergeConflictError, "expected MergeConflictError");
112
+
113
+ // Critical: cwd must be restored to where it was before the merge
114
+ const cwdAfter = process.cwd();
115
+ assert.equal(
116
+ cwdAfter,
117
+ cwdBefore,
118
+ "cwd should be restored after MergeConflictError — was left on integration branch before fix",
119
+ );
120
+ });
121
+
122
+ // ─────────────────────────────────────────────────────────────────────────
123
+ // Test 2: autoCommitDirtyState skipped when on integration branch (#2929 bug 1)
124
+ // ─────────────────────────────────────────────────────────────────────────
125
+
126
+ test("autoCommitDirtyState does not commit on integration branch in worktree mode", () => {
127
+ // Create milestone branch with real work
128
+ run("git checkout -b milestone/M010", repo);
129
+ writeFileSync(join(repo, "m010.ts"), "export const m010 = true;\n");
130
+ run("git add .", repo);
131
+ run('git commit -m "M010 work"', repo);
132
+ run("git checkout main", repo);
133
+
134
+ // Simulate the parallel-mode state: cwd is on main with dirty files
135
+ // from another milestone (as if a prior merge's MergeConflictError
136
+ // left cwd on main and syncStateToProjectRoot wrote these files).
137
+ writeFileSync(join(repo, "dirty-from-m020.txt"), "should not be committed\n");
138
+
139
+ // Set up roadmap so mergeMilestoneToMain can find milestone metadata
140
+ mkdirSync(join(repo, ".gsd", "milestones", "M010"), { recursive: true });
141
+ writeFileSync(
142
+ join(repo, ".gsd", "milestones", "M010", "M010-ROADMAP.md"),
143
+ makeRoadmap("M010", "First milestone"),
144
+ );
145
+
146
+ process.chdir(repo);
147
+
148
+ const result = mergeMilestoneToMain(
149
+ repo,
150
+ "M010",
151
+ makeRoadmap("M010", "First milestone"),
152
+ );
153
+
154
+ assert.ok(result.commitMessage.includes("M010"), "commit should be for M010");
155
+
156
+ // Verify the squash merge brought M010's work file
157
+ const mergeLog = run("git log --oneline --diff-filter=A -- m010.ts", repo);
158
+ assert.ok(mergeLog.length > 0, "m010.ts should be in a commit on main");
159
+
160
+ // The dirty file should NOT appear in the squash merge commit.
161
+ const squashCommit = run("git log --format=%H --grep='GSD-Milestone: M010' -1", repo);
162
+ assert.ok(squashCommit.length > 0, "should find the squash merge commit");
163
+ const filesInSquash = run(`git diff-tree --no-commit-id --name-only -r ${squashCommit}`, repo);
164
+ assert.ok(
165
+ !filesInSquash.includes("dirty-from-m020.txt"),
166
+ "dirty-from-m020.txt should NOT be in the squash merge commit",
167
+ );
168
+ });
169
+ });
@@ -0,0 +1,146 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import { assessInterruptedSession } from "../interrupted-session.ts";
9
+
10
+ function makeTmpBase(): string {
11
+ const base = join(tmpdir(), `gsd-auto-interrupted-${randomUUID()}`);
12
+ mkdirSync(join(base, ".gsd"), { recursive: true });
13
+ return base;
14
+ }
15
+
16
+ function cleanup(base: string): void {
17
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
18
+ }
19
+
20
+ function writeRoadmap(base: string, checked = false): void {
21
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
22
+ mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
23
+ writeFileSync(
24
+ join(milestoneDir, "M001-ROADMAP.md"),
25
+ [
26
+ "# M001: Test Milestone",
27
+ "",
28
+ "## Vision",
29
+ "",
30
+ "Test milestone.",
31
+ "",
32
+ "## Success Criteria",
33
+ "",
34
+ "- It works.",
35
+ "",
36
+ "## Slices",
37
+ "",
38
+ `- [${checked ? "x" : " "}] **S01: Test slice** \`risk:low\``,
39
+ " After this: Demo",
40
+ "",
41
+ "## Boundary Map",
42
+ "",
43
+ "- S01 → terminal",
44
+ " - Produces: done",
45
+ " - Consumes: nothing",
46
+ ].join("\n"),
47
+ "utf-8",
48
+ );
49
+ }
50
+
51
+ function writeCompleteArtifacts(base: string): void {
52
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
53
+ const sliceDir = join(milestoneDir, "slices", "S01");
54
+ mkdirSync(sliceDir, { recursive: true });
55
+ writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
56
+ writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
57
+ writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
58
+ }
59
+
60
+ function writeLock(base: string, unitType: string, unitId: string): void {
61
+ writeFileSync(
62
+ join(base, ".gsd", "auto.lock"),
63
+ JSON.stringify({
64
+ pid: 999999999,
65
+ startedAt: new Date().toISOString(),
66
+ unitType,
67
+ unitId,
68
+ unitStartedAt: new Date().toISOString(),
69
+ }, null, 2),
70
+ "utf-8",
71
+ );
72
+ }
73
+
74
+ function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
75
+ const runtimeDir = join(base, ".gsd", "runtime");
76
+ mkdirSync(runtimeDir, { recursive: true });
77
+ writeFileSync(
78
+ join(runtimeDir, "paused-session.json"),
79
+ JSON.stringify({ milestoneId, originalBasePath: base, stepMode }, null, 2),
80
+ "utf-8",
81
+ );
82
+ }
83
+
84
+ test("direct /gsd auto stale complete repo yields stale classification with no recovery payload", async () => {
85
+ const base = makeTmpBase();
86
+ try {
87
+ writeRoadmap(base, true);
88
+ writeCompleteArtifacts(base);
89
+ writeLock(base, "execute-task", "M001/S01/T01");
90
+
91
+ const assessment = await assessInterruptedSession(base);
92
+ assert.equal(assessment.classification, "stale");
93
+ assert.equal(assessment.recoveryPrompt, null);
94
+ assert.equal(assessment.hasResumableDiskState, false);
95
+ } finally {
96
+ cleanup(base);
97
+ }
98
+ });
99
+
100
+ test("direct /gsd auto paused-session metadata remains recoverable when work is unfinished", async () => {
101
+ const base = makeTmpBase();
102
+ try {
103
+ writeRoadmap(base, false);
104
+ writePausedSession(base, "M001", false);
105
+ writeLock(base, "execute-task", "M001/S01/T01");
106
+
107
+ const assessment = await assessInterruptedSession(base);
108
+ assert.equal(assessment.classification, "recoverable");
109
+ assert.equal(assessment.pausedSession?.milestoneId, "M001");
110
+ } finally {
111
+ cleanup(base);
112
+ }
113
+ });
114
+
115
+ test("direct /gsd auto stale paused-session metadata is treated as stale when no resumable work remains", async () => {
116
+ const base = makeTmpBase();
117
+ try {
118
+ writeRoadmap(base, true);
119
+ writeCompleteArtifacts(base);
120
+ writePausedSession(base, "M999", true);
121
+
122
+ const assessment = await assessInterruptedSession(base);
123
+ assert.equal(assessment.classification, "stale");
124
+ assert.equal(assessment.hasResumableDiskState, false);
125
+ } finally {
126
+ cleanup(base);
127
+ }
128
+ });
129
+
130
+ test("direct /gsd auto source only resumes paused-session metadata for recoverable state with real recovery signals", async () => {
131
+ const source = await import(`node:fs/promises`).then((fs) =>
132
+ fs.readFile(new URL("../auto.ts", import.meta.url), "utf-8")
133
+ );
134
+ assert.ok(source.includes('const shouldResumePausedSession ='));
135
+ assert.ok(source.includes('freshStartAssessment.classification === "recoverable"'));
136
+ assert.ok(source.includes('&& ('));
137
+ assert.ok(source.includes('freshStartAssessment.hasResumableDiskState'));
138
+ assert.ok(source.includes('|| !!freshStartAssessment.recoveryPrompt'));
139
+ assert.ok(source.includes('|| !!freshStartAssessment.lock'));
140
+ });
141
+
142
+ test("auto module imports successfully after interrupted-session changes", async () => {
143
+ const mod = await import(`../auto.ts?ts=${Date.now()}-${Math.random()}`);
144
+ assert.equal(typeof mod.startAuto, "function");
145
+ assert.equal(typeof mod.pauseAuto, "function");
146
+ });
@@ -0,0 +1,136 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import { assessInterruptedSession } from "../interrupted-session.ts";
9
+
10
+ function makeTmpBase(): string {
11
+ const base = join(tmpdir(), `gsd-smart-entry-${randomUUID()}`);
12
+ mkdirSync(join(base, ".gsd"), { recursive: true });
13
+ return base;
14
+ }
15
+
16
+ function cleanup(base: string): void {
17
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
18
+ }
19
+
20
+ function writeRoadmap(base: string, checked = false): void {
21
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
22
+ mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
23
+ writeFileSync(
24
+ join(milestoneDir, "M001-ROADMAP.md"),
25
+ [
26
+ "# M001: Test Milestone",
27
+ "",
28
+ "## Vision",
29
+ "",
30
+ "Test milestone.",
31
+ "",
32
+ "## Success Criteria",
33
+ "",
34
+ "- It works.",
35
+ "",
36
+ "## Slices",
37
+ "",
38
+ `- [${checked ? "x" : " "}] **S01: Test slice** \`risk:low\``,
39
+ " After this: Demo",
40
+ "",
41
+ "## Boundary Map",
42
+ "",
43
+ "- S01 → terminal",
44
+ " - Produces: done",
45
+ " - Consumes: nothing",
46
+ ].join("\n"),
47
+ "utf-8",
48
+ );
49
+ }
50
+
51
+ function writeCompleteArtifacts(base: string): void {
52
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
53
+ const sliceDir = join(milestoneDir, "slices", "S01");
54
+ mkdirSync(sliceDir, { recursive: true });
55
+ writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
56
+ writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
57
+ writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
58
+ }
59
+
60
+ function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
61
+ const runtimeDir = join(base, ".gsd", "runtime");
62
+ mkdirSync(runtimeDir, { recursive: true });
63
+ writeFileSync(
64
+ join(runtimeDir, "paused-session.json"),
65
+ JSON.stringify({ milestoneId, originalBasePath: base, stepMode }, null, 2),
66
+ "utf-8",
67
+ );
68
+ }
69
+
70
+ function writeLock(base: string, unitType: string, unitId: string): void {
71
+ writeFileSync(
72
+ join(base, ".gsd", "auto.lock"),
73
+ JSON.stringify({
74
+ pid: 999999999,
75
+ startedAt: new Date().toISOString(),
76
+ unitType,
77
+ unitId,
78
+ unitStartedAt: new Date().toISOString(),
79
+ }, null, 2),
80
+ "utf-8",
81
+ );
82
+ }
83
+
84
+ test("guided-flow stale complete scenario classifies as stale so the resume prompt can be suppressed", async () => {
85
+ const base = makeTmpBase();
86
+ try {
87
+ writeRoadmap(base, true);
88
+ writeCompleteArtifacts(base);
89
+ writeLock(base, "execute-task", "M001/S01/T01");
90
+
91
+ const assessment = await assessInterruptedSession(base);
92
+ assert.equal(assessment.classification, "stale");
93
+ assert.equal(assessment.recoveryPrompt, null);
94
+ } finally {
95
+ cleanup(base);
96
+ }
97
+ });
98
+
99
+ test("guided-flow paused-session scenario classifies as recoverable so resume remains available", async () => {
100
+ const base = makeTmpBase();
101
+ try {
102
+ writeRoadmap(base, false);
103
+ writePausedSession(base);
104
+ writeLock(base, "execute-task", "M001/S01/T01");
105
+
106
+ const assessment = await assessInterruptedSession(base);
107
+ assert.equal(assessment.classification, "recoverable");
108
+ assert.equal(assessment.pausedSession?.milestoneId, "M001");
109
+ } finally {
110
+ cleanup(base);
111
+ }
112
+ });
113
+
114
+ test("guided-flow stale paused-session scenario is suppressed when no resumable work remains", async () => {
115
+ const base = makeTmpBase();
116
+ try {
117
+ writeRoadmap(base, true);
118
+ writeCompleteArtifacts(base);
119
+ writePausedSession(base, "M999", true);
120
+
121
+ const assessment = await assessInterruptedSession(base);
122
+ assert.equal(assessment.classification, "stale");
123
+ assert.equal(assessment.hasResumableDiskState, false);
124
+ } finally {
125
+ cleanup(base);
126
+ }
127
+ });
128
+
129
+ test("guided-flow source uses step-aware resume and clears stale paused metadata without changing discuss handoff semantics", () => {
130
+ const source = readFileSync(join(import.meta.dirname, "..", "guided-flow.ts"), "utf-8");
131
+ assert.ok(source.includes('const interrupted = await assessInterruptedSession(basePath);'));
132
+ assert.ok(source.includes('resumeLabel = interrupted.pausedSession?.stepMode'));
133
+ assert.ok(source.includes('step: interrupted.pausedSession?.stepMode ?? false'));
134
+ assert.ok(source.includes('unlinkSync(join(gsdRoot(basePath), "runtime", "paused-session.json"))'));
135
+ assert.ok(source.includes('pendingAutoStartMap.set(basePath,'));
136
+ });
@@ -0,0 +1,85 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import {
8
+ ensureProjectWorkflowMcpConfig,
9
+ GSD_WORKFLOW_MCP_SERVER_NAME,
10
+ } from "../mcp-project-config.ts";
11
+
12
+ test("ensureProjectWorkflowMcpConfig creates .mcp.json with the workflow server", () => {
13
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-init-"));
14
+ mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
15
+
16
+ try {
17
+ const result = ensureProjectWorkflowMcpConfig(projectRoot);
18
+ assert.equal(result.status, "created");
19
+ assert.equal(existsSync(result.configPath), true);
20
+
21
+ const parsed = JSON.parse(readFileSync(result.configPath, "utf-8")) as {
22
+ mcpServers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }>;
23
+ };
24
+ const server = parsed.mcpServers?.[GSD_WORKFLOW_MCP_SERVER_NAME];
25
+ assert.ok(server, "workflow server should be written to mcpServers");
26
+ assert.equal(typeof server?.command, "string");
27
+ assert.equal(Array.isArray(server?.args), true);
28
+ assert.equal(server?.env?.GSD_WORKFLOW_PROJECT_ROOT, projectRoot);
29
+ assert.match(server?.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "", /workflow-tool-executors\.js$/);
30
+ assert.match(server?.env?.GSD_WORKFLOW_WRITE_GATE_MODULE ?? "", /write-gate\.js$/);
31
+ } finally {
32
+ rmSync(projectRoot, { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ test("ensureProjectWorkflowMcpConfig preserves existing mcp servers", () => {
37
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-init-"));
38
+ mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
39
+ const configPath = join(projectRoot, ".mcp.json");
40
+
41
+ writeFileSync(
42
+ configPath,
43
+ `${JSON.stringify({
44
+ mcpServers: {
45
+ railway: {
46
+ command: "npx",
47
+ args: ["railway-mcp"],
48
+ },
49
+ },
50
+ }, null, 2)}\n`,
51
+ "utf-8",
52
+ );
53
+
54
+ try {
55
+ const result = ensureProjectWorkflowMcpConfig(projectRoot);
56
+ assert.equal(result.status, "updated");
57
+
58
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as {
59
+ mcpServers?: Record<string, { command?: string; args?: string[] }>;
60
+ };
61
+ assert.deepEqual(parsed.mcpServers?.railway, {
62
+ command: "npx",
63
+ args: ["railway-mcp"],
64
+ });
65
+ assert.ok(parsed.mcpServers?.[GSD_WORKFLOW_MCP_SERVER_NAME]);
66
+ } finally {
67
+ rmSync(projectRoot, { recursive: true, force: true });
68
+ }
69
+ });
70
+
71
+ test("ensureProjectWorkflowMcpConfig is idempotent when config is already current", () => {
72
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-init-"));
73
+ mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
74
+
75
+ try {
76
+ const first = ensureProjectWorkflowMcpConfig(projectRoot);
77
+ const second = ensureProjectWorkflowMcpConfig(projectRoot);
78
+
79
+ assert.equal(first.status, "created");
80
+ assert.equal(second.status, "unchanged");
81
+ assert.equal(first.configPath, second.configPath);
82
+ } finally {
83
+ rmSync(projectRoot, { recursive: true, force: true });
84
+ }
85
+ });
@@ -2,6 +2,7 @@ import test, { describe } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
 
4
4
  import {
5
+ formatMcpInitResult,
5
6
  formatMcpStatusReport,
6
7
  formatMcpServerDetail,
7
8
  type McpServerStatus,
@@ -101,3 +102,17 @@ describe("formatMcpServerDetail", () => {
101
102
  assert.match(result, /disconnected/i);
102
103
  });
103
104
  });
105
+
106
+ describe("formatMcpInitResult", () => {
107
+ test("shows created message with config path", () => {
108
+ const result = formatMcpInitResult("created", "/tmp/project/.mcp.json", "/tmp/project");
109
+ assert.match(result, /created project mcp config/i);
110
+ assert.match(result, /\/tmp\/project\/\.mcp\.json/);
111
+ assert.match(result, /claude code/i);
112
+ });
113
+
114
+ test("shows unchanged message when config is current", () => {
115
+ const result = formatMcpInitResult("unchanged", "/tmp/project/.mcp.json", "/tmp/project");
116
+ assert.match(result, /already up to date/i);
117
+ });
118
+ });
@@ -32,6 +32,17 @@ test("isVerificationNotApplicable: 'None planned' is not applicable", () => {
32
32
  assert.equal(isVerificationNotApplicable("None planned"), true);
33
33
  });
34
34
 
35
+ test("isVerificationNotApplicable: 'None — <rationale>' is not applicable (#3897)", () => {
36
+ assert.equal(
37
+ isVerificationNotApplicable("None — no new background jobs, workers, or lifecycle changes introduced."),
38
+ true,
39
+ );
40
+ });
41
+
42
+ test("isVerificationNotApplicable: em dash without spaces is not applicable (#3897)", () => {
43
+ assert.equal(isVerificationNotApplicable("none—inline"), true);
44
+ });
45
+
35
46
  test("isVerificationNotApplicable: 'N/A' is not applicable", () => {
36
47
  assert.equal(isVerificationNotApplicable("N/A"), true);
37
48
  });