gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.97f5583d9

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 (271) hide show
  1. package/dist/claude-cli-check.js +32 -3
  2. package/dist/mcp-server.d.ts +7 -0
  3. package/dist/mcp-server.js +35 -1
  4. package/dist/resource-loader.d.ts +1 -1
  5. package/dist/resource-loader.js +2 -8
  6. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  7. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  8. package/dist/resources/extensions/gsd/auto/phases.js +42 -1
  9. package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
  10. package/dist/resources/extensions/gsd/auto/session.js +12 -0
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -3
  12. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +25 -2
  14. package/dist/resources/extensions/gsd/auto-prompts.js +14 -0
  15. package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
  16. package/dist/resources/extensions/gsd/auto-start.js +27 -18
  17. package/dist/resources/extensions/gsd/auto-worktree.js +51 -53
  18. package/dist/resources/extensions/gsd/auto.js +55 -27
  19. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  20. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  21. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  22. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  23. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +51 -5
  24. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +34 -2
  25. package/dist/resources/extensions/gsd/clean-root-preflight.js +93 -0
  26. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  27. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  28. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  29. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  30. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  31. package/dist/resources/extensions/gsd/gsd-db.js +115 -7
  32. package/dist/resources/extensions/gsd/guided-flow.js +189 -0
  33. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  34. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  35. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  36. package/dist/resources/extensions/gsd/model-router.js +36 -3
  37. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
  38. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  39. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  40. package/dist/resources/extensions/gsd/preferences.js +17 -17
  41. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  42. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  43. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  44. package/dist/resources/extensions/gsd/safety/evidence-collector.js +96 -0
  45. package/dist/resources/extensions/gsd/safety/file-change-validator.js +13 -5
  46. package/dist/resources/extensions/gsd/safety/safety-harness.js +5 -1
  47. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  48. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  49. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  50. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  51. package/dist/resources/extensions/gsd/uok/plan-v2.js +20 -3
  52. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  53. package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
  54. package/dist/resources/skills/write-docs/SKILL.md +2 -1
  55. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  56. package/dist/web/standalone/.next/BUILD_ID +1 -1
  57. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  58. package/dist/web/standalone/.next/build-manifest.json +2 -2
  59. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  60. package/dist/web/standalone/.next/required-server-files.json +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.html +1 -1
  78. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  85. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  87. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  88. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  89. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  90. package/dist/web/standalone/server.js +1 -1
  91. package/package.json +1 -1
  92. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  93. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  94. package/packages/mcp-server/dist/remote-questions.js +732 -0
  95. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  96. package/packages/mcp-server/dist/server.d.ts +7 -0
  97. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  98. package/packages/mcp-server/dist/server.js +41 -4
  99. package/packages/mcp-server/dist/server.js.map +1 -1
  100. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  101. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  102. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  103. package/packages/mcp-server/package.json +2 -1
  104. package/packages/mcp-server/src/mcp-server.test.ts +30 -0
  105. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  106. package/packages/mcp-server/src/remote-questions.ts +916 -0
  107. package/packages/mcp-server/src/server.ts +62 -10
  108. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  109. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  110. package/packages/mcp-server/tsconfig.test.json +19 -0
  111. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  112. package/packages/pi-ai/dist/providers/anthropic-auth.test.js +1 -1
  113. package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -1
  114. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  115. package/packages/pi-ai/dist/providers/anthropic-shared.js +27 -4
  116. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  117. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  118. package/packages/pi-ai/dist/providers/anthropic.js +8 -3
  119. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  120. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts +2 -0
  121. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts.map +1 -0
  122. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js +80 -0
  123. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js.map +1 -0
  124. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  125. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  126. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  127. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  128. package/packages/pi-ai/src/providers/anthropic-auth.test.ts +1 -1
  129. package/packages/pi-ai/src/providers/anthropic-shared.ts +26 -5
  130. package/packages/pi-ai/src/providers/anthropic.ts +9 -3
  131. package/packages/pi-ai/src/providers/minimax-tool-name.test.ts +98 -0
  132. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  133. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  134. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  136. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  137. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  138. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  140. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  142. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  143. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  144. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  145. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  146. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  147. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  148. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  149. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  151. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  153. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  155. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  157. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  158. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  159. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  161. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  162. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  163. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  164. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  165. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  166. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  167. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  168. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  169. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  170. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  171. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  172. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  173. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  174. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  175. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  176. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  177. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  178. package/src/resources/extensions/gsd/auto/loop-deps.ts +13 -0
  179. package/src/resources/extensions/gsd/auto/phases.ts +66 -1
  180. package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
  181. package/src/resources/extensions/gsd/auto/session.ts +22 -0
  182. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -3
  183. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  184. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  185. package/src/resources/extensions/gsd/auto-prompts.ts +28 -1
  186. package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
  187. package/src/resources/extensions/gsd/auto-start.ts +29 -19
  188. package/src/resources/extensions/gsd/auto-worktree.ts +62 -63
  189. package/src/resources/extensions/gsd/auto.ts +58 -27
  190. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  191. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  192. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  193. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  194. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +53 -5
  195. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +35 -2
  196. package/src/resources/extensions/gsd/clean-root-preflight.ts +111 -0
  197. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  198. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  199. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  200. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  201. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  202. package/src/resources/extensions/gsd/gsd-db.ts +122 -7
  203. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  204. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  205. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  206. package/src/resources/extensions/gsd/journal.ts +2 -1
  207. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  208. package/src/resources/extensions/gsd/model-router.ts +42 -1
  209. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  210. package/src/resources/extensions/gsd/preferences-types.ts +46 -0
  211. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  212. package/src/resources/extensions/gsd/preferences.ts +17 -17
  213. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  214. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  215. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  216. package/src/resources/extensions/gsd/safety/evidence-collector.ts +119 -0
  217. package/src/resources/extensions/gsd/safety/file-change-validator.ts +17 -4
  218. package/src/resources/extensions/gsd/safety/safety-harness.ts +9 -0
  219. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +119 -1
  220. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +12 -0
  221. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
  222. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +186 -0
  223. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  224. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  225. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  226. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  227. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  228. package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +1 -1
  229. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
  230. package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
  231. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  232. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  233. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +58 -0
  234. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
  235. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  236. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  237. package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
  238. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +2 -0
  239. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  240. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  241. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  242. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
  243. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  244. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +272 -0
  245. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  246. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  247. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  248. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  249. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  250. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  251. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +205 -0
  252. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  253. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  254. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
  255. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  256. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  257. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +23 -0
  258. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  259. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  260. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  261. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  262. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  263. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  264. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  265. package/src/resources/extensions/gsd/uok/plan-v2.ts +26 -3
  266. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  267. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  268. package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
  269. package/src/resources/skills/write-docs/SKILL.md +2 -1
  270. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → lLdDRDspgYzfz0bJAmUSz}/_buildManifest.js +0 -0
  271. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → lLdDRDspgYzfz0bJAmUSz}/_ssgManifest.js +0 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * GSD-2 / guided-flow — regression tests for #4573
3
+ *
4
+ * Covers two recovery paths:
5
+ * - maybeHandleReadyPhraseWithoutFiles: nudge when LLM emits
6
+ * "Milestone M001 ready." without writing CONTEXT.md / ROADMAP.md
7
+ * - maybeHandleEmptyIntentTurn: nudge when LLM narrates intent but
8
+ * emits no tool-use blocks
9
+ */
10
+
11
+ import { describe, test, beforeEach } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+
17
+ import {
18
+ setPendingAutoStart,
19
+ clearPendingAutoStart,
20
+ maybeHandleReadyPhraseWithoutFiles,
21
+ maybeHandleEmptyIntentTurn,
22
+ resetEmptyTurnCounter,
23
+ } from "../guided-flow.ts";
24
+
25
+ // ─── Test harness ──────────────────────────────────────────────────────────
26
+
27
+ interface MockCapture {
28
+ notifies: Array<{ msg: string; level: string }>;
29
+ messages: Array<{ payload: any; options: any }>;
30
+ }
31
+
32
+ function mkCapture(): MockCapture {
33
+ return { notifies: [], messages: [] };
34
+ }
35
+
36
+ function mkCtx(cap: MockCapture): any {
37
+ return {
38
+ ui: {
39
+ notify: (msg: string, level: string) => {
40
+ cap.notifies.push({ msg, level });
41
+ },
42
+ },
43
+ };
44
+ }
45
+
46
+ function mkPi(cap: MockCapture, opts: { sendThrows?: boolean } = {}): any {
47
+ return {
48
+ sendMessage: (payload: any, options: any) => {
49
+ if (opts.sendThrows) throw new Error("send failed");
50
+ cap.messages.push({ payload, options });
51
+ },
52
+ setActiveTools: () => undefined,
53
+ getActiveTools: () => [],
54
+ };
55
+ }
56
+
57
+ function mkBase(): string {
58
+ const base = mkdtempSync(join(tmpdir(), "gsd-4573-"));
59
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
60
+ return base;
61
+ }
62
+
63
+ function assistantMsg(text: string, opts: { toolUse?: boolean } = {}): any {
64
+ const content: any[] = [];
65
+ if (text) content.push({ type: "text", text });
66
+ if (opts.toolUse) content.push({ type: "tool_use", name: "whatever", input: {} });
67
+ return { role: "assistant", content };
68
+ }
69
+
70
+ // ─── ready-phrase recovery (Layer 2) ───────────────────────────────────────
71
+
72
+ describe("#4573 maybeHandleReadyPhraseWithoutFiles", () => {
73
+ beforeEach(() => {
74
+ clearPendingAutoStart();
75
+ resetEmptyTurnCounter();
76
+ });
77
+
78
+ test("no pending entry → no-op", () => {
79
+ const cap = mkCapture();
80
+ const event = { messages: [assistantMsg("Milestone M001 ready.")] };
81
+ const handled = maybeHandleReadyPhraseWithoutFiles(event);
82
+ assert.equal(handled, false);
83
+ assert.equal(cap.messages.length, 0);
84
+ });
85
+
86
+ test("pending entry, ready phrase, no files → notify + sendMessage", () => {
87
+ const base = mkBase();
88
+ try {
89
+ const cap = mkCapture();
90
+ setPendingAutoStart(base, {
91
+ basePath: base,
92
+ milestoneId: "M001",
93
+ ctx: mkCtx(cap),
94
+ pi: mkPi(cap),
95
+ });
96
+ const handled = maybeHandleReadyPhraseWithoutFiles({
97
+ messages: [assistantMsg("Milestone M001 ready.")],
98
+ });
99
+ assert.equal(handled, true);
100
+ assert.equal(cap.messages.length, 1);
101
+ assert.equal(cap.messages[0].payload.customType, "gsd-ready-no-files");
102
+ assert.equal(cap.messages[0].options.triggerTurn, true);
103
+ assert.ok(
104
+ cap.notifies.some((n) => /rejected/.test(n.msg)),
105
+ "user notified about rejection",
106
+ );
107
+ } finally {
108
+ clearPendingAutoStart();
109
+ }
110
+ });
111
+
112
+ test("retry cap — after MAX_READY_REJECTS the nudge stops and entry clears", () => {
113
+ const base = mkBase();
114
+ try {
115
+ const cap = mkCapture();
116
+ setPendingAutoStart(base, {
117
+ basePath: base,
118
+ milestoneId: "M001",
119
+ ctx: mkCtx(cap),
120
+ pi: mkPi(cap),
121
+ });
122
+ const event = { messages: [assistantMsg("Milestone M001 ready.")] };
123
+
124
+ const first = maybeHandleReadyPhraseWithoutFiles(event);
125
+ const second = maybeHandleReadyPhraseWithoutFiles(event);
126
+ const third = maybeHandleReadyPhraseWithoutFiles(event); // > MAX
127
+
128
+ assert.equal(first, true);
129
+ assert.equal(second, true);
130
+ assert.equal(third, true); // still returns true (handled via give-up)
131
+ assert.equal(cap.messages.length, 2, "only 2 nudges sent (MAX_READY_REJECTS=2)");
132
+ assert.ok(
133
+ cap.notifies.some((n) => /Stopping auto-nudge/.test(n.msg)),
134
+ "gives up with error notify",
135
+ );
136
+
137
+ // After giving up, a fresh re-entry starts clean
138
+ const fourth = maybeHandleReadyPhraseWithoutFiles(event);
139
+ assert.equal(fourth, false, "pending entry was cleared — nothing to handle");
140
+ } finally {
141
+ clearPendingAutoStart();
142
+ }
143
+ });
144
+
145
+ test("files present → no nudge (happy path already fired)", () => {
146
+ const base = mkBase();
147
+ try {
148
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# ctx");
149
+ const cap = mkCapture();
150
+ setPendingAutoStart(base, {
151
+ basePath: base,
152
+ milestoneId: "M001",
153
+ ctx: mkCtx(cap),
154
+ pi: mkPi(cap),
155
+ });
156
+ const handled = maybeHandleReadyPhraseWithoutFiles({
157
+ messages: [assistantMsg("Milestone M001 ready.")],
158
+ });
159
+ assert.equal(handled, false);
160
+ assert.equal(cap.messages.length, 0);
161
+ } finally {
162
+ clearPendingAutoStart();
163
+ }
164
+ });
165
+
166
+ test("last message lacks ready phrase → no-op", () => {
167
+ const base = mkBase();
168
+ try {
169
+ const cap = mkCapture();
170
+ setPendingAutoStart(base, {
171
+ basePath: base,
172
+ milestoneId: "M001",
173
+ ctx: mkCtx(cap),
174
+ pi: mkPi(cap),
175
+ });
176
+ const handled = maybeHandleReadyPhraseWithoutFiles({
177
+ messages: [assistantMsg("Let me think about the slices first.")],
178
+ });
179
+ assert.equal(handled, false);
180
+ assert.equal(cap.messages.length, 0);
181
+ } finally {
182
+ clearPendingAutoStart();
183
+ }
184
+ });
185
+
186
+ test("fresh entry after give-up resets counter", () => {
187
+ const base = mkBase();
188
+ try {
189
+ const cap = mkCapture();
190
+ // First cycle: exhaust cap
191
+ setPendingAutoStart(base, {
192
+ basePath: base,
193
+ milestoneId: "M001",
194
+ ctx: mkCtx(cap),
195
+ pi: mkPi(cap),
196
+ });
197
+ const event = { messages: [assistantMsg("Milestone M001 ready.")] };
198
+ maybeHandleReadyPhraseWithoutFiles(event);
199
+ maybeHandleReadyPhraseWithoutFiles(event);
200
+ maybeHandleReadyPhraseWithoutFiles(event); // clears entry
201
+
202
+ // New /gsd run — re-seeds entry; counter must be 0 again
203
+ cap.messages.length = 0;
204
+ setPendingAutoStart(base, {
205
+ basePath: base,
206
+ milestoneId: "M001",
207
+ ctx: mkCtx(cap),
208
+ pi: mkPi(cap),
209
+ });
210
+ const handled = maybeHandleReadyPhraseWithoutFiles(event);
211
+ assert.equal(handled, true);
212
+ assert.equal(cap.messages.length, 1, "fresh entry fires nudge again");
213
+ } finally {
214
+ clearPendingAutoStart();
215
+ }
216
+ });
217
+ });
218
+
219
+ // ─── empty-turn recovery (Layer 3) ────────────────────────────────────────
220
+
221
+ describe("#4573 maybeHandleEmptyIntentTurn", () => {
222
+ beforeEach(() => {
223
+ clearPendingAutoStart();
224
+ resetEmptyTurnCounter();
225
+ });
226
+
227
+ test("no pending entry + isAuto false → no-op (interactive discuss is user-driven)", () => {
228
+ const event = { messages: [assistantMsg("I'll write the CONTEXT.md now.")] };
229
+ const handled = maybeHandleEmptyIntentTurn(event, false);
230
+ assert.equal(handled, false);
231
+ });
232
+
233
+ test("text-only turn WITHOUT commit phrase → not flagged (legitimate text)", () => {
234
+ const base = mkBase();
235
+ try {
236
+ const cap = mkCapture();
237
+ setPendingAutoStart(base, {
238
+ basePath: base,
239
+ milestoneId: "M001",
240
+ ctx: mkCtx(cap),
241
+ pi: mkPi(cap),
242
+ });
243
+ const handled = maybeHandleEmptyIntentTurn(
244
+ { messages: [assistantMsg("Here is the roadmap preview — three slices.")] },
245
+ false,
246
+ );
247
+ assert.equal(handled, false);
248
+ assert.equal(cap.messages.length, 0);
249
+ } finally {
250
+ clearPendingAutoStart();
251
+ }
252
+ });
253
+
254
+ test("text-only turn ending in question → treated as user-handoff, not flagged", () => {
255
+ const base = mkBase();
256
+ try {
257
+ const cap = mkCapture();
258
+ setPendingAutoStart(base, {
259
+ basePath: base,
260
+ milestoneId: "M001",
261
+ ctx: mkCtx(cap),
262
+ pi: mkPi(cap),
263
+ });
264
+ const handled = maybeHandleEmptyIntentTurn(
265
+ { messages: [assistantMsg("Ready to write, or want to adjust?")] },
266
+ false,
267
+ );
268
+ assert.equal(handled, false);
269
+ } finally {
270
+ clearPendingAutoStart();
271
+ }
272
+ });
273
+
274
+ test("commit-intent phrase WITHOUT tool call → nudge fires", () => {
275
+ const base = mkBase();
276
+ try {
277
+ const cap = mkCapture();
278
+ setPendingAutoStart(base, {
279
+ basePath: base,
280
+ milestoneId: "M001",
281
+ ctx: mkCtx(cap),
282
+ pi: mkPi(cap),
283
+ });
284
+ const handled = maybeHandleEmptyIntentTurn(
285
+ { messages: [assistantMsg("I'll now write the CONTEXT.md file.")] },
286
+ false,
287
+ );
288
+ assert.equal(handled, true);
289
+ assert.equal(cap.messages.length, 1);
290
+ assert.equal(cap.messages[0].payload.customType, "gsd-empty-turn-recovery");
291
+ } finally {
292
+ clearPendingAutoStart();
293
+ }
294
+ });
295
+
296
+ test("commit-intent WITH tool-use block → not flagged", () => {
297
+ const base = mkBase();
298
+ try {
299
+ const cap = mkCapture();
300
+ setPendingAutoStart(base, {
301
+ basePath: base,
302
+ milestoneId: "M001",
303
+ ctx: mkCtx(cap),
304
+ pi: mkPi(cap),
305
+ });
306
+ const handled = maybeHandleEmptyIntentTurn(
307
+ { messages: [assistantMsg("I'll write the file now.", { toolUse: true })] },
308
+ false,
309
+ );
310
+ assert.equal(handled, false);
311
+ assert.equal(cap.messages.length, 0);
312
+ } finally {
313
+ clearPendingAutoStart();
314
+ }
315
+ });
316
+
317
+ test("ready phrase is NOT treated as empty-turn (handled by other recovery path)", () => {
318
+ const base = mkBase();
319
+ try {
320
+ const cap = mkCapture();
321
+ setPendingAutoStart(base, {
322
+ basePath: base,
323
+ milestoneId: "M001",
324
+ ctx: mkCtx(cap),
325
+ pi: mkPi(cap),
326
+ });
327
+ const handled = maybeHandleEmptyIntentTurn(
328
+ { messages: [assistantMsg("Milestone M001 ready.")] },
329
+ false,
330
+ );
331
+ assert.equal(handled, false);
332
+ } finally {
333
+ clearPendingAutoStart();
334
+ }
335
+ });
336
+
337
+ test("empty-turn retry cap — stops after MAX_EMPTY_TURN_RETRIES", () => {
338
+ const base = mkBase();
339
+ try {
340
+ const cap = mkCapture();
341
+ setPendingAutoStart(base, {
342
+ basePath: base,
343
+ milestoneId: "M001",
344
+ ctx: mkCtx(cap),
345
+ pi: mkPi(cap),
346
+ });
347
+ const event = { messages: [assistantMsg("I'll write the CONTEXT.md file.")] };
348
+
349
+ maybeHandleEmptyIntentTurn(event, false); // 1
350
+ maybeHandleEmptyIntentTurn(event, false); // 2
351
+ const third = maybeHandleEmptyIntentTurn(event, false); // > cap
352
+
353
+ assert.equal(cap.messages.length, 2, "only 2 nudges sent");
354
+ assert.equal(third, false, "after cap, no further injection");
355
+ assert.ok(
356
+ cap.notifies.some((n) => /Stopping auto-nudge/.test(n.msg)),
357
+ "user notified of give-up",
358
+ );
359
+ } finally {
360
+ clearPendingAutoStart();
361
+ }
362
+ });
363
+
364
+ test("resetEmptyTurnCounter clears state after a successful tool-use turn", () => {
365
+ const base = mkBase();
366
+ try {
367
+ const cap = mkCapture();
368
+ setPendingAutoStart(base, {
369
+ basePath: base,
370
+ milestoneId: "M001",
371
+ ctx: mkCtx(cap),
372
+ pi: mkPi(cap),
373
+ });
374
+ const event = { messages: [assistantMsg("I'll write the CONTEXT.md file.")] };
375
+
376
+ maybeHandleEmptyIntentTurn(event, false); // 1
377
+ maybeHandleEmptyIntentTurn(event, false); // 2 — at cap
378
+ resetEmptyTurnCounter(); // simulate a successful tool-use turn in between
379
+
380
+ cap.messages.length = 0;
381
+ const after = maybeHandleEmptyIntentTurn(event, false);
382
+ assert.equal(after, true, "counter reset — nudge fires again");
383
+ assert.equal(cap.messages.length, 1);
384
+ } finally {
385
+ clearPendingAutoStart();
386
+ }
387
+ });
388
+ });
@@ -45,9 +45,15 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
45
45
  })
46
46
 
47
47
  it('savedTools is restored after sendMessage', () => {
48
- // Find the sendMessage call
49
- const sendMsg = src.indexOf('triggerTurn: true')
50
- assert.ok(sendMsg !== -1, 'sendMessage with triggerTurn must exist')
48
+ // #4573: guided-flow.ts now contains multiple `triggerTurn: true` calls
49
+ // (ready-phrase and empty-turn recovery paths). The discuss-flow scoping
50
+ // sendMessage is the one that follows `savedTools = currentTools`, so
51
+ // anchor the search there rather than at the first `triggerTurn: true`.
52
+ const savedToolsAssign = src.indexOf('savedTools = currentTools')
53
+ assert.ok(savedToolsAssign !== -1, 'savedTools = currentTools must exist')
54
+
55
+ const sendMsg = src.indexOf('triggerTurn: true', savedToolsAssign)
56
+ assert.ok(sendMsg !== -1, 'discuss-flow sendMessage with triggerTurn must exist after savedTools capture')
51
57
 
52
58
  // After sendMessage, savedTools should be restored via setActiveTools
53
59
  const afterSend = src.slice(sendMsg, sendMsg + 500)
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Regression tests for three false-positive sources in the safety harness.
3
+ * Issue #4385
4
+ *
5
+ * Bug 1: Hardcoded BASH_READ_ONLY_RE — new legitimate commands blocked
6
+ * Bug 2: Non-persisted evidence — session restart causes false positive on resume
7
+ * Bug 3: git diff HEAD~1 scope check — fails on initial commits / shallow clones
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { execFileSync } from "node:child_process";
13
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ import { shouldBlockQueueExecution } from "../bootstrap/write-gate.ts";
18
+ import {
19
+ resetEvidence,
20
+ recordToolCall,
21
+ getEvidence,
22
+ saveEvidenceToDisk,
23
+ loadEvidenceFromDisk,
24
+ } from "../safety/evidence-collector.ts";
25
+ import { validateFileChanges } from "../safety/file-change-validator.ts";
26
+
27
+ // ─── Bug 1: Hardcoded Bash allowlist ────────────────────────────────────────
28
+
29
+ test("safety-harness-bug1: npm commands are not blocked during queue mode", () => {
30
+ const r = shouldBlockQueueExecution("bash", "npm run test", true);
31
+ assert.strictEqual(r.block, false, "npm run test must be read-only-safe");
32
+ });
33
+
34
+ test("safety-harness-bug1: npx commands are not blocked during queue mode", () => {
35
+ const r = shouldBlockQueueExecution("bash", "npx tsc --noEmit", true);
36
+ assert.strictEqual(r.block, false, "npx tsc --noEmit must pass");
37
+ });
38
+
39
+ test("safety-harness-bug1: tsx commands are not blocked during queue mode", () => {
40
+ const r = shouldBlockQueueExecution("bash", "tsx src/index.ts", true);
41
+ assert.strictEqual(
42
+ r.block,
43
+ false,
44
+ "tsx (TypeScript runner — read-only investigative) must pass",
45
+ );
46
+ });
47
+
48
+ test("safety-harness-bug1: node --print commands are not blocked during queue mode", () => {
49
+ const r = shouldBlockQueueExecution("bash", "node --print 'process.version'", true);
50
+ assert.strictEqual(r.block, false, "node --print must pass");
51
+ });
52
+
53
+ test("safety-harness-bug1: python read-only invocations are not blocked during queue mode", () => {
54
+ const r = shouldBlockQueueExecution("bash", "python -c 'import sys; print(sys.version)'", true);
55
+ assert.strictEqual(r.block, false, "python -c read-only must pass");
56
+ });
57
+
58
+ test("safety-harness-bug1: jq read-only command is not blocked during queue mode", () => {
59
+ const r = shouldBlockQueueExecution("bash", "jq '.version' package.json", true);
60
+ assert.strictEqual(r.block, false, "jq (read-only JSON query) must pass");
61
+ });
62
+
63
+ test("safety-harness-bug1: destructive commands are still blocked during queue mode", () => {
64
+ const r = shouldBlockQueueExecution("bash", "rm -rf dist/", true);
65
+ assert.strictEqual(r.block, true, "rm -rf must still be blocked");
66
+ });
67
+
68
+ // ─── Bug 2: Non-persisted evidence ──────────────────────────────────────────
69
+
70
+ test("safety-harness-bug2: evidence survives save/load round-trip (simulates session restart)", (t) => {
71
+ const base = mkdtempSync(join(tmpdir(), "gsd-evidence-persist-"));
72
+ t.after(() => rmSync(base, { recursive: true, force: true }));
73
+
74
+ resetEvidence();
75
+
76
+ // Simulate bash tool calls during unit execution
77
+ recordToolCall("tc-001", "Bash", { command: "npm run test:unit" });
78
+ recordToolCall("tc-002", "Bash", { command: "npx tsc --noEmit" });
79
+ recordToolCall("tc-003", "Write", { file_path: "src/foo.ts" });
80
+
81
+ const before = getEvidence();
82
+ assert.equal(before.length, 3, "three entries before save");
83
+
84
+ // Persist to disk
85
+ saveEvidenceToDisk(base, "M001", "S001", "T001");
86
+
87
+ // Simulate session restart: module-level array reset
88
+ resetEvidence();
89
+ assert.equal(getEvidence().length, 0, "in-memory cleared after reset");
90
+
91
+ // Resume: load from disk
92
+ loadEvidenceFromDisk(base, "M001", "S001", "T001");
93
+
94
+ const after = getEvidence();
95
+ assert.equal(after.length, 3, "evidence restored from disk after simulated restart");
96
+
97
+ const bashEntries = after.filter((e) => e.kind === "bash");
98
+ assert.equal(bashEntries.length, 2, "both bash entries restored");
99
+
100
+ const writeEntries = after.filter((e) => e.kind === "write");
101
+ assert.equal(writeEntries.length, 1, "write entry restored");
102
+ });
103
+
104
+ test("safety-harness-bug2: loadEvidenceFromDisk returns empty array when no file exists (fresh unit)", (t) => {
105
+ const base = mkdtempSync(join(tmpdir(), "gsd-evidence-nopersist-"));
106
+ t.after(() => rmSync(base, { recursive: true, force: true }));
107
+
108
+ resetEvidence();
109
+ loadEvidenceFromDisk(base, "M001", "S001", "T001");
110
+ assert.equal(getEvidence().length, 0, "no evidence on fresh unit is correct — not a false positive");
111
+ });
112
+
113
+ // ─── Bug 3: git diff HEAD~1 scope check ─────────────────────────────────────
114
+
115
+ test("safety-harness-bug3: validateFileChanges works on initial commit (no HEAD~1)", (t) => {
116
+ const base = mkdtempSync(join(tmpdir(), "gsd-initial-commit-"));
117
+ t.after(() => rmSync(base, { recursive: true, force: true }));
118
+
119
+ execFileSync("git", ["init"], { cwd: base });
120
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: base });
121
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: base });
122
+
123
+ writeFileSync(join(base, "index.ts"), "export const x = 1;\n");
124
+ execFileSync("git", ["add", "."], { cwd: base });
125
+ execFileSync("git", ["commit", "-m", "initial"], { cwd: base });
126
+
127
+ // On initial commit, HEAD~1 does not exist — must not throw or produce wrong results
128
+ const audit = validateFileChanges(base, ["index.ts"], []);
129
+
130
+ assert.ok(audit !== null, "audit must be produced for initial commit");
131
+ assert.deepEqual(audit!.unexpectedFiles, [], "no unexpected files on initial commit");
132
+ assert.deepEqual(audit!.missingFiles, [], "no missing files on initial commit");
133
+ });
134
+
135
+ test("safety-harness-bug3: validateFileChanges works on shallow clone (shallow repo without full history)", (t) => {
136
+ // Simulate shallow clone: create a repo, then clone it with depth=1
137
+ const origin = mkdtempSync(join(tmpdir(), "gsd-origin-"));
138
+ const shallow = mkdtempSync(join(tmpdir(), "gsd-shallow-"));
139
+ t.after(() => {
140
+ rmSync(origin, { recursive: true, force: true });
141
+ rmSync(shallow, { recursive: true, force: true });
142
+ });
143
+
144
+ // Set up origin with multiple commits
145
+ execFileSync("git", ["init"], { cwd: origin });
146
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: origin });
147
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: origin });
148
+ writeFileSync(join(origin, "a.ts"), "export const a = 1;\n");
149
+ execFileSync("git", ["add", "."], { cwd: origin });
150
+ execFileSync("git", ["commit", "-m", "first"], { cwd: origin });
151
+ writeFileSync(join(origin, "b.ts"), "export const b = 2;\n");
152
+ execFileSync("git", ["add", "."], { cwd: origin });
153
+ execFileSync("git", ["commit", "-m", "second"], { cwd: origin });
154
+
155
+ // Shallow clone with depth=1 — HEAD~1 will not exist
156
+ execFileSync("git", ["clone", "--depth=1", `file://${origin}`, shallow], {
157
+ stdio: ["ignore", "pipe", "pipe"],
158
+ });
159
+
160
+ // Verify the shallow clone has no parent (HEAD~1 unavailable)
161
+ let hasParent = true;
162
+ try {
163
+ execFileSync("git", ["rev-parse", "HEAD~1"], {
164
+ cwd: shallow,
165
+ stdio: ["ignore", "pipe", "pipe"],
166
+ });
167
+ } catch {
168
+ hasParent = false;
169
+ }
170
+ assert.equal(hasParent, false, "shallow clone should not have HEAD~1");
171
+
172
+ // validateFileChanges must not throw or give wrong results
173
+ const audit = validateFileChanges(shallow, ["b.ts"], []);
174
+ assert.ok(audit !== null, "audit must be produced even in shallow clone");
175
+ });
176
+
177
+ test("safety-harness-bug3: validateFileChanges works on merge commit", (t) => {
178
+ const base = mkdtempSync(join(tmpdir(), "gsd-merge-commit-"));
179
+ t.after(() => rmSync(base, { recursive: true, force: true }));
180
+
181
+ execFileSync("git", ["init", "-b", "main"], { cwd: base });
182
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: base });
183
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: base });
184
+
185
+ // Main branch: initial commit
186
+ writeFileSync(join(base, "main.ts"), "export const m = 1;\n");
187
+ execFileSync("git", ["add", "."], { cwd: base });
188
+ execFileSync("git", ["commit", "-m", "initial"], { cwd: base });
189
+
190
+ // Feature branch
191
+ execFileSync("git", ["checkout", "-b", "feature"], { cwd: base });
192
+ writeFileSync(join(base, "feature.ts"), "export const f = 2;\n");
193
+ execFileSync("git", ["add", "."], { cwd: base });
194
+ execFileSync("git", ["commit", "-m", "feature work"], { cwd: base });
195
+
196
+ // Merge back to main
197
+ execFileSync("git", ["checkout", "main"], { cwd: base });
198
+ execFileSync("git", ["merge", "--no-ff", "feature", "-m", "Merge feature"], { cwd: base });
199
+
200
+ // HEAD is now a merge commit with two parents — git diff HEAD~1 gives wrong scope
201
+ const audit = validateFileChanges(base, ["feature.ts"], []);
202
+
203
+ // Must produce a valid result without throwing
204
+ assert.ok(audit !== null, "audit must be produced for merge commit repo");
205
+ });