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,124 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { listExecHistory, searchExecHistory } from '../exec-history.ts';
8
+ import { executeExecSearch } from '../tools/exec-search-tool.ts';
9
+
10
+ function freshBase(): string {
11
+ return mkdtempSync(join(tmpdir(), 'gsd-exec-history-'));
12
+ }
13
+
14
+ function cleanup(dir: string): void {
15
+ rmSync(dir, { recursive: true, force: true });
16
+ }
17
+
18
+ function writeRun(base: string, id: string, overrides: Record<string, unknown> = {}): void {
19
+ const dir = join(base, '.gsd', 'exec');
20
+ mkdirSync(dir, { recursive: true });
21
+ const stdoutPath = join(dir, `${id}.stdout`);
22
+ const stderrPath = join(dir, `${id}.stderr`);
23
+ const metaPath = join(dir, `${id}.meta.json`);
24
+ writeFileSync(stdoutPath, (overrides.stdout as string | undefined) ?? `stdout for ${id}\n`);
25
+ writeFileSync(stderrPath, '');
26
+ writeFileSync(
27
+ metaPath,
28
+ JSON.stringify({
29
+ id,
30
+ runtime: 'bash',
31
+ purpose: `purpose for ${id}`,
32
+ started_at: '2026-04-20T12:00:00.000Z',
33
+ finished_at: '2026-04-20T12:00:00.100Z',
34
+ duration_ms: 100,
35
+ exit_code: 0,
36
+ signal: null,
37
+ timed_out: false,
38
+ stdout_bytes: 12,
39
+ stderr_bytes: 0,
40
+ stdout_truncated: false,
41
+ stderr_truncated: false,
42
+ stdout_path: stdoutPath,
43
+ stderr_path: stderrPath,
44
+ ...overrides,
45
+ }),
46
+ );
47
+ }
48
+
49
+ test('listExecHistory: returns empty list when .gsd/exec missing', () => {
50
+ const base = freshBase();
51
+ try {
52
+ assert.deepEqual(listExecHistory(base), []);
53
+ } finally {
54
+ cleanup(base);
55
+ }
56
+ });
57
+
58
+ test('listExecHistory: skips malformed meta files', () => {
59
+ const base = freshBase();
60
+ try {
61
+ const dir = join(base, '.gsd', 'exec');
62
+ mkdirSync(dir, { recursive: true });
63
+ writeFileSync(join(dir, 'bad.meta.json'), '{not-json');
64
+ writeRun(base, 'ok-1');
65
+ const list = listExecHistory(base);
66
+ assert.equal(list.length, 1);
67
+ assert.equal(list[0]!.id, 'ok-1');
68
+ } finally {
69
+ cleanup(base);
70
+ }
71
+ });
72
+
73
+ test('searchExecHistory: filters by query, runtime, and failing_only', () => {
74
+ const base = freshBase();
75
+ try {
76
+ writeRun(base, 'playwright-run', { purpose: 'playwright snapshot' });
77
+ writeRun(base, 'grep-run', { purpose: 'grep TODOs' });
78
+ writeRun(base, 'failing-run', { exit_code: 1, purpose: 'boom' });
79
+ writeRun(base, 'node-run', { runtime: 'node', purpose: 'dedupe' });
80
+
81
+ const playwrightHits = searchExecHistory(base, { query: 'playwright' });
82
+ assert.equal(playwrightHits.length, 1);
83
+ assert.equal(playwrightHits[0]!.entry.id, 'playwright-run');
84
+
85
+ const failingHits = searchExecHistory(base, { failing_only: true });
86
+ assert.equal(failingHits.length, 1);
87
+ assert.equal(failingHits[0]!.entry.id, 'failing-run');
88
+
89
+ const nodeHits = searchExecHistory(base, { runtime: 'node' });
90
+ assert.equal(nodeHits.length, 1);
91
+ assert.equal(nodeHits[0]!.entry.runtime, 'node');
92
+
93
+ const unlimited = searchExecHistory(base, {});
94
+ assert.equal(unlimited.length, 4);
95
+ } finally {
96
+ cleanup(base);
97
+ }
98
+ });
99
+
100
+ test('executeExecSearch: returns helpful empty-state message when no matches', () => {
101
+ const base = freshBase();
102
+ try {
103
+ const result = executeExecSearch({ query: 'missing' }, { baseDir: base });
104
+ assert.ok(!result.isError);
105
+ assert.match(result.content[0].text, /No prior gsd_exec runs/);
106
+ } finally {
107
+ cleanup(base);
108
+ }
109
+ });
110
+
111
+ test('executeExecSearch: includes stdout_path and preview in details', () => {
112
+ const base = freshBase();
113
+ try {
114
+ writeRun(base, 'summary-run', { stdout: 'found 42 TODOs\n' });
115
+ const result = executeExecSearch({ query: 'summary' }, { baseDir: base });
116
+ const details = result.details as { results: Array<{ id: string; stdout_path: string }> };
117
+ assert.equal(details.results.length, 1);
118
+ assert.equal(details.results[0]!.id, 'summary-run');
119
+ assert.match(details.results[0]!.stdout_path, /summary-run\.stdout$/);
120
+ assert.match(result.content[0].text, /found 42 TODOs/);
121
+ } finally {
122
+ cleanup(base);
123
+ }
124
+ });
@@ -0,0 +1,210 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
8
+ import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
9
+ import { isContextModeEnabled } from '../preferences-types.ts';
10
+
11
+ function freshBase(): string {
12
+ return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
13
+ }
14
+
15
+ function cleanup(dir: string): void {
16
+ rmSync(dir, { recursive: true, force: true });
17
+ }
18
+
19
+ function baseOpts(base: string, overrides: Partial<ExecSandboxOptions> = {}): ExecSandboxOptions {
20
+ return {
21
+ baseDir: base,
22
+ clamp_timeout_ms: EXEC_DEFAULTS.clampTimeoutMs,
23
+ default_timeout_ms: 10_000,
24
+ stdout_cap_bytes: 1_024,
25
+ stderr_cap_bytes: 1_024,
26
+ digest_chars: 120,
27
+ env_allowlist: EXEC_DEFAULTS.envAllowlist,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ test('runExecSandbox: captures stdout, persists artifacts, returns digest', async () => {
33
+ const base = freshBase();
34
+ try {
35
+ const result = await runExecSandbox(
36
+ { runtime: 'bash', script: 'echo hello world' },
37
+ baseOpts(base),
38
+ );
39
+ assert.equal(result.exit_code, 0);
40
+ assert.equal(result.timed_out, false);
41
+ assert.ok(result.digest.includes('hello world'), `digest should contain stdout: ${result.digest}`);
42
+ assert.ok(result.stdout_path.startsWith(join(base, '.gsd', 'exec')), 'stdout path under .gsd/exec');
43
+ assert.equal(readFileSync(result.stdout_path, 'utf-8').trim(), 'hello world');
44
+ const meta = JSON.parse(readFileSync(result.meta_path, 'utf-8')) as Record<string, unknown>;
45
+ assert.equal(meta.runtime, 'bash');
46
+ assert.equal(meta.exit_code, 0);
47
+ } finally {
48
+ cleanup(base);
49
+ }
50
+ });
51
+
52
+ test('runExecSandbox: enforces stdout cap and marks truncation', async () => {
53
+ const base = freshBase();
54
+ try {
55
+ const result = await runExecSandbox(
56
+ // Emit far more than the cap so truncation triggers.
57
+ { runtime: 'bash', script: 'head -c 8000 /dev/urandom | base64' },
58
+ baseOpts(base, { stdout_cap_bytes: 256 }),
59
+ );
60
+ assert.equal(result.stdout_truncated, true, 'should mark stdout truncated');
61
+ assert.ok(result.stdout_bytes <= 256, `stdout_bytes within cap (got ${result.stdout_bytes})`);
62
+ const stdout = readFileSync(result.stdout_path, 'utf-8');
63
+ assert.ok(stdout.endsWith('[truncated: stdout cap reached]\n'), 'truncation marker appended');
64
+ } finally {
65
+ cleanup(base);
66
+ }
67
+ });
68
+
69
+ test('runExecSandbox: enforces timeout and surfaces timed_out', async () => {
70
+ const base = freshBase();
71
+ try {
72
+ const started = Date.now();
73
+ const result = await runExecSandbox(
74
+ { runtime: 'bash', script: 'sleep 10' },
75
+ baseOpts(base, { default_timeout_ms: 150, clamp_timeout_ms: 150 }),
76
+ );
77
+ const elapsed = Date.now() - started;
78
+ assert.equal(result.timed_out, true);
79
+ assert.ok(elapsed < 5_000, `should return well before 10s (took ${elapsed}ms)`);
80
+ } finally {
81
+ cleanup(base);
82
+ }
83
+ });
84
+
85
+ test('runExecSandbox: forwards only allowlisted env vars', async () => {
86
+ const base = freshBase();
87
+ try {
88
+ const result = await runExecSandbox(
89
+ { runtime: 'bash', script: 'echo PATH=$PATH SECRET=$GSD_TEST_SECRET' },
90
+ baseOpts(base, {
91
+ env_allowlist: [],
92
+ env: { PATH: '/usr/bin:/bin', HOME: '/tmp', GSD_TEST_SECRET: 'should-be-blocked' },
93
+ }),
94
+ );
95
+ const stdout = readFileSync(result.stdout_path, 'utf-8');
96
+ assert.ok(stdout.includes('PATH=/usr/bin:/bin'), 'PATH forwarded');
97
+ assert.ok(!stdout.includes('should-be-blocked'), 'non-allowlisted var blocked');
98
+ } finally {
99
+ cleanup(base);
100
+ }
101
+ });
102
+
103
+ test('runExecSandbox: node runtime executes JS', async () => {
104
+ const base = freshBase();
105
+ try {
106
+ const result = await runExecSandbox(
107
+ { runtime: 'node', script: 'console.log("node-ok:" + (1+2))' },
108
+ baseOpts(base),
109
+ );
110
+ assert.equal(result.exit_code, 0);
111
+ assert.ok(result.digest.includes('node-ok:3'));
112
+ } finally {
113
+ cleanup(base);
114
+ }
115
+ });
116
+
117
+ // ── exec-tool executor ────────────────────────────────────────────────────
118
+
119
+ test('executeGsdExec: runs by default when context_mode is unset', async () => {
120
+ const base = freshBase();
121
+ try {
122
+ const result = await executeGsdExec(
123
+ { runtime: 'bash', script: 'echo default-on-run' },
124
+ { baseDir: base, preferences: {} },
125
+ );
126
+ assert.ok(!result.isError, 'should succeed with no preferences');
127
+ assert.equal(result.details.operation, 'gsd_exec');
128
+ assert.equal(result.details.exit_code, 0);
129
+ assert.ok(result.content[0].text.includes('default-on-run'));
130
+ } finally {
131
+ cleanup(base);
132
+ }
133
+ });
134
+
135
+ test('executeGsdExec: runs when preferences is null (fresh project)', async () => {
136
+ const base = freshBase();
137
+ try {
138
+ const result = await executeGsdExec(
139
+ { runtime: 'bash', script: 'echo null-prefs-run' },
140
+ { baseDir: base, preferences: null },
141
+ );
142
+ assert.ok(!result.isError, 'null preferences should not disable');
143
+ assert.ok(result.content[0].text.includes('null-prefs-run'));
144
+ } finally {
145
+ cleanup(base);
146
+ }
147
+ });
148
+
149
+ test('executeGsdExec: blocked only when context_mode.enabled=false', async () => {
150
+ const base = freshBase();
151
+ try {
152
+ const result = await executeGsdExec(
153
+ { runtime: 'bash', script: 'echo should-not-run' },
154
+ { baseDir: base, preferences: { context_mode: { enabled: false } } },
155
+ );
156
+ assert.equal(result.isError, true);
157
+ assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
158
+ } finally {
159
+ cleanup(base);
160
+ }
161
+ });
162
+
163
+ test('executeGsdExec: runs when enabled explicitly set to true', async () => {
164
+ const base = freshBase();
165
+ try {
166
+ const result = await executeGsdExec(
167
+ { runtime: 'bash', script: 'echo explicit-on' },
168
+ { baseDir: base, preferences: { context_mode: { enabled: true } } },
169
+ );
170
+ assert.ok(!result.isError);
171
+ assert.ok(result.content[0].text.includes('explicit-on'));
172
+ } finally {
173
+ cleanup(base);
174
+ }
175
+ });
176
+
177
+ test('executeGsdExec: rejects empty script', async () => {
178
+ const base = freshBase();
179
+ try {
180
+ const result = await executeGsdExec(
181
+ { runtime: 'bash', script: ' ' },
182
+ { baseDir: base, preferences: { context_mode: { enabled: true } } },
183
+ );
184
+ assert.equal(result.isError, true);
185
+ assert.equal((result.details as { error?: string }).error, 'invalid_params');
186
+ } finally {
187
+ cleanup(base);
188
+ }
189
+ });
190
+
191
+ test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
192
+ assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
193
+ assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
194
+ assert.equal(isContextModeEnabled({}), true, 'empty prefs → on');
195
+ assert.equal(isContextModeEnabled({ context_mode: {} }), true, 'empty block → on');
196
+ assert.equal(isContextModeEnabled({ context_mode: { enabled: true } }), true);
197
+ assert.equal(isContextModeEnabled({ context_mode: { enabled: false } }), false);
198
+ });
199
+
200
+ test('buildExecOptions: clamps out-of-range values to safe defaults', () => {
201
+ const opts = buildExecOptions('/tmp/base', {
202
+ enabled: true,
203
+ exec_timeout_ms: 999_999_999,
204
+ exec_stdout_cap_bytes: 1,
205
+ exec_digest_chars: -20,
206
+ });
207
+ assert.equal(opts.default_timeout_ms, EXEC_DEFAULTS.clampTimeoutMs, 'timeout clamped to upper bound');
208
+ assert.equal(opts.stdout_cap_bytes, 4_096, 'stdout cap clamped to floor');
209
+ assert.equal(opts.digest_chars, 0, 'digest chars clamped to floor');
210
+ });
@@ -15,6 +15,64 @@ function git(cwd: string, ...args: string[]): string {
15
15
  }).trim();
16
16
  }
17
17
 
18
+ test("validateFileChanges works on repos with a single commit (no HEAD~1)", (t) => {
19
+ const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
20
+ t.after(() => rmSync(base, { recursive: true, force: true }));
21
+
22
+ git(base, "init");
23
+ git(base, "config", "user.email", "test@example.com");
24
+ git(base, "config", "user.name", "Test User");
25
+
26
+ writeFileSync(join(base, "foo.ts"), "export const x = 1;\n");
27
+ git(base, "add", ".");
28
+ git(base, "commit", "-m", "initial");
29
+
30
+ // With only one commit, HEAD~1 doesn't exist — this must not throw
31
+ const audit = validateFileChanges(base, ["foo.ts"], []);
32
+
33
+ assert.ok(audit, "audit should be produced for single-commit repo");
34
+ assert.deepEqual(audit.unexpectedFiles, []);
35
+ assert.deepEqual(audit.missingFiles, []);
36
+ });
37
+
38
+ test("validateFileChanges excludes allowlisted files from unexpected-change warnings", (t) => {
39
+ const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
40
+ t.after(() => rmSync(base, { recursive: true, force: true }));
41
+
42
+ mkdirSync(join(base, "tracking", "history"), { recursive: true });
43
+ git(base, "init");
44
+ git(base, "config", "user.email", "test@example.com");
45
+ git(base, "config", "user.name", "Test User");
46
+
47
+ writeFileSync(join(base, "src.ts"), "initial\n");
48
+ writeFileSync(join(base, "tracking", "history", "2026-04-20-snapshot.md"), "initial\n");
49
+ git(base, "add", ".");
50
+ git(base, "commit", "-m", "initial");
51
+
52
+ writeFileSync(join(base, "src.ts"), "updated\n");
53
+ writeFileSync(join(base, "tracking", "history", "2026-04-20-snapshot.md"), "updated\n");
54
+ git(base, "add", ".");
55
+ git(base, "commit", "-m", "update");
56
+
57
+ // Without allowlist: tracking/history snapshot is unexpected
58
+ const auditWithout = validateFileChanges(base, ["src.ts"], []);
59
+ assert.ok(auditWithout, "audit should be produced");
60
+ assert.ok(
61
+ auditWithout.unexpectedFiles.includes("tracking/history/2026-04-20-snapshot.md"),
62
+ "snapshot should be unexpected without allowlist",
63
+ );
64
+
65
+ // With glob allowlist: snapshot is excluded
66
+ const auditWith = validateFileChanges(base, ["src.ts"], [], ["tracking/history/**"]);
67
+ assert.ok(auditWith, "audit should be produced with allowlist");
68
+ assert.deepEqual(auditWith.unexpectedFiles, [], "no unexpected files when snapshot is allowlisted");
69
+ assert.equal(
70
+ auditWith.violations.filter(v => v.severity === "warning").length,
71
+ 0,
72
+ "no warnings when all unexpected files are allowlisted",
73
+ );
74
+ });
75
+
18
76
  test("validateFileChanges ignores inline descriptions in expected output paths", (t) => {
19
77
  const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
20
78
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -3,12 +3,14 @@ import assert from 'node:assert/strict';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
5
  import * as os from 'node:os';
6
+ import { createRequire } from 'node:module';
6
7
  import {
7
8
  openDatabase,
8
9
  closeDatabase,
9
10
  isDbAvailable,
10
11
  wasDbOpenAttempted,
11
12
  getDbProvider,
13
+ getDbStatus,
12
14
  insertDecision,
13
15
  getDecisionById,
14
16
  insertRequirement,
@@ -26,6 +28,8 @@ import {
26
28
  checkpointDatabase,
27
29
  } from '../gsd-db.ts';
28
30
 
31
+ const _require = createRequire(import.meta.url);
32
+
29
33
  // ═══════════════════════════════════════════════════════════════════════════
30
34
  // Helper: create a temp file path for file-backed DB tests
31
35
  // ═══════════════════════════════════════════════════════════════════════════
@@ -59,6 +63,20 @@ function withPlatform<T>(platform: NodeJS.Platform, fn: () => T): T {
59
63
  }
60
64
  }
61
65
 
66
+ function openRawSqliteForTest(dbPath: string): { exec(sql: string): void; close(): void } {
67
+ try {
68
+ const mod = _require('node:sqlite') as { DatabaseSync: new (path: string) => { exec(sql: string): void; close(): void } };
69
+ return new mod.DatabaseSync(dbPath);
70
+ } catch {
71
+ type SqliteCtor = new (path: string) => { exec(sql: string): void; close(): void };
72
+ const mod = _require('better-sqlite3') as
73
+ | SqliteCtor
74
+ | { default: SqliteCtor };
75
+ const DatabaseCtor: SqliteCtor = typeof mod === 'function' ? mod : mod.default;
76
+ return new DatabaseCtor(dbPath);
77
+ }
78
+ }
79
+
62
80
  // ═══════════════════════════════════════════════════════════════════════════
63
81
  // gsd-db tests
64
82
  // ═══════════════════════════════════════════════════════════════════════════
@@ -81,7 +99,7 @@ describe('gsd-db', () => {
81
99
  // Check schema_version table
82
100
  const adapter = _getAdapter()!;
83
101
  const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
84
- assert.deepStrictEqual(version?.['version'], 21, 'schema version should be 21');
102
+ assert.deepStrictEqual(version?.['version'], 22, 'schema version should be 22');
85
103
 
86
104
  // Check tables exist by querying them
87
105
  const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
@@ -404,6 +422,53 @@ describe('gsd-db', () => {
404
422
  cleanup(dbPath);
405
423
  });
406
424
 
425
+ test('gsd-db: legacy DB missing memories.scope opens and bootstraps index columns', () => {
426
+ const dbPath = tempDbPath();
427
+ const legacyDb = openRawSqliteForTest(dbPath);
428
+ legacyDb.exec(`
429
+ CREATE TABLE schema_version (
430
+ version INTEGER NOT NULL,
431
+ applied_at TEXT NOT NULL
432
+ );
433
+ INSERT INTO schema_version(version, applied_at) VALUES (17, '2026-04-20T00:00:00.000Z');
434
+ CREATE TABLE memories (
435
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
436
+ id TEXT NOT NULL UNIQUE,
437
+ category TEXT NOT NULL,
438
+ content TEXT NOT NULL,
439
+ confidence REAL NOT NULL DEFAULT 0.8,
440
+ source_unit_type TEXT,
441
+ source_unit_id TEXT,
442
+ created_at TEXT NOT NULL,
443
+ updated_at TEXT NOT NULL,
444
+ superseded_by TEXT DEFAULT NULL,
445
+ hit_count INTEGER NOT NULL DEFAULT 0
446
+ );
447
+ INSERT INTO memories(id, category, content, created_at, updated_at)
448
+ VALUES ('legacy-memory', 'note', 'legacy row', '2026-04-20T00:00:00.000Z', '2026-04-20T00:00:00.000Z');
449
+ `);
450
+ legacyDb.close();
451
+
452
+ assert.equal(openDatabase(dbPath), true, 'openDatabase should succeed for legacy DB missing memories.scope');
453
+
454
+ const adapter = _getAdapter()!;
455
+ const columns = adapter.prepare('PRAGMA table_info(memories)').all();
456
+ const names = columns.map((row) => row['name']);
457
+ assert.ok(names.includes('scope'), 'memories.scope should be added during bootstrap');
458
+ assert.ok(names.includes('tags'), 'memories.tags should be added during bootstrap');
459
+
460
+ const row = adapter.prepare(`SELECT scope, tags FROM memories WHERE id = 'legacy-memory'`).get();
461
+ assert.equal(row?.['scope'], 'project', 'legacy rows should receive default scope');
462
+ assert.equal(row?.['tags'], '[]', 'legacy rows should receive default tags');
463
+
464
+ const index = adapter.prepare(
465
+ "SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_memories_scope'",
466
+ ).get();
467
+ assert.equal(index?.['name'], 'idx_memories_scope', 'scope index should be created after bootstrap columns are present');
468
+
469
+ cleanup(dbPath);
470
+ });
471
+
407
472
  test('gsd-db: rowToTask tolerates legacy comma-separated task arrays', () => {
408
473
  openDatabase(':memory:');
409
474
 
@@ -561,6 +626,92 @@ describe('gsd-db', () => {
561
626
  });
562
627
  });
563
628
 
629
+ // ─── getDbStatus ───────────────────────────────────────────────────────────
630
+
631
+ describe('getDbStatus', () => {
632
+ test('getDbStatus: initial state before any open', () => {
633
+ closeDatabase();
634
+ const status = getDbStatus();
635
+ assert.strictEqual(status.available, false, 'available false before open');
636
+ assert.strictEqual(status.attempted, false, 'attempted false before open');
637
+ assert.strictEqual(status.lastError, null, 'lastError null before open');
638
+ assert.strictEqual(status.lastPhase, null, 'lastPhase null before open');
639
+ });
640
+
641
+ test('getDbStatus: available after successful open', () => {
642
+ openDatabase(':memory:');
643
+ const status = getDbStatus();
644
+ assert.strictEqual(status.available, true, 'available true after open');
645
+ assert.strictEqual(status.attempted, true, 'attempted true after open');
646
+ assert.ok(status.provider !== null, 'provider set after open');
647
+ assert.strictEqual(status.lastError, null, 'lastError null on success');
648
+ assert.strictEqual(status.lastPhase, null, 'lastPhase null on success');
649
+ closeDatabase();
650
+ });
651
+
652
+ test('getDbStatus: resets lastError/lastPhase after closeDatabase', () => {
653
+ // Simulate a failed open to set error state
654
+ const corruptPath = path.join(os.tmpdir(), `gsd-corrupt-${Date.now()}.db`);
655
+ fs.writeFileSync(corruptPath, Buffer.from('not a sqlite file at all!!!!!'));
656
+ try {
657
+ openDatabase(corruptPath);
658
+ } catch {
659
+ // expected
660
+ }
661
+ assert.ok(getDbStatus().lastError !== null, 'lastError set after failed open');
662
+
663
+ // closeDatabase should clear it even though no DB was opened
664
+ closeDatabase();
665
+ const status = getDbStatus();
666
+ assert.strictEqual(status.lastError, null, 'lastError cleared by closeDatabase');
667
+ assert.strictEqual(status.lastPhase, null, 'lastPhase cleared by closeDatabase');
668
+ assert.strictEqual(status.attempted, false, 'attempted reset by closeDatabase');
669
+ fs.unlinkSync(corruptPath);
670
+ });
671
+
672
+ test('getDbStatus: captures open-phase error on corrupt file', () => {
673
+ closeDatabase();
674
+ const corruptPath = path.join(os.tmpdir(), `gsd-corrupt-${Date.now()}.db`);
675
+ fs.writeFileSync(corruptPath, Buffer.from('not a sqlite file at all!!!!!'));
676
+ try {
677
+ openDatabase(corruptPath);
678
+ } catch {
679
+ // expected — both providers should reject a non-SQLite file
680
+ }
681
+ const status = getDbStatus();
682
+ if (!status.available) {
683
+ // open failed (expected in most environments)
684
+ assert.strictEqual(status.attempted, true, 'attempted true after failed open');
685
+ // provider may reject at raw-open level ("open") or at SQL init level ("initSchema")
686
+ assert.ok(
687
+ status.lastPhase === 'open' || status.lastPhase === 'initSchema',
688
+ `lastPhase should be "open" or "initSchema", got: ${status.lastPhase}`,
689
+ );
690
+ assert.ok(status.lastError instanceof Error, 'lastError is an Error');
691
+ }
692
+ // If somehow it succeeded (unlikely with garbage content), that's also fine
693
+ closeDatabase();
694
+ try { fs.unlinkSync(corruptPath); } catch { /* best effort */ }
695
+ });
696
+
697
+ test('getDbStatus: error state resets on next successful open', () => {
698
+ closeDatabase();
699
+ const corruptPath = path.join(os.tmpdir(), `gsd-corrupt-${Date.now()}.db`);
700
+ fs.writeFileSync(corruptPath, Buffer.from('not a sqlite file at all!!!!!'));
701
+ try { openDatabase(corruptPath); } catch { /* expected */ }
702
+ assert.ok(!getDbStatus().available, 'DB unavailable after corrupt open');
703
+
704
+ // Now open a valid in-memory DB — error state should clear
705
+ openDatabase(':memory:');
706
+ const status = getDbStatus();
707
+ assert.strictEqual(status.available, true, 'available after valid open');
708
+ assert.strictEqual(status.lastError, null, 'lastError cleared on successful open');
709
+ assert.strictEqual(status.lastPhase, null, 'lastPhase cleared on successful open');
710
+ closeDatabase();
711
+ try { fs.unlinkSync(corruptPath); } catch { /* best effort */ }
712
+ });
713
+ });
714
+
564
715
  // ─── Final Report ──────────────────────────────────────────────────────────
565
716
 
566
717
  });
@@ -178,6 +178,33 @@ test("init-wizard: multiple project files detected together", (t) => {
178
178
  }
179
179
  });
180
180
 
181
+ // ─── Git init + initial commit regression (#4530) ───────────────────────────
182
+
183
+ import { execFileSync } from "node:child_process";
184
+ import { nativeInit, nativeAddAll, nativeCommit } from "../native-git-bridge.ts";
185
+
186
+ test("init-wizard: nativeInit + nativeAddAll + nativeCommit produces a reachable HEAD (#4530)", (t) => {
187
+ // Regression: showProjectInit called nativeInit but never committed, leaving
188
+ // the branch unborn. git log and git worktree add both fail on zero-commit repos.
189
+ const dir = makeTempDir("git-init-commit");
190
+ t.after(() => { cleanup(dir); });
191
+
192
+ nativeInit(dir, "main");
193
+ execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir });
194
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: dir });
195
+ writeFileSync(join(dir, ".gitignore"), "*.log\n", "utf-8");
196
+
197
+ nativeAddAll(dir);
198
+ nativeCommit(dir, "chore: init project");
199
+
200
+ // git log must succeed (was: fatal: your current branch 'main' does not have any commits yet)
201
+ const subject = execFileSync("git", ["log", "-1", "--format=%s"], {
202
+ cwd: dir,
203
+ encoding: "utf-8",
204
+ }).trim();
205
+ assert.equal(subject, "chore: init project");
206
+ });
207
+
181
208
  test("init-wizard: v1 with both .planning/ and .gsd/ prioritizes v2", (t) => {
182
209
  const dir = makeTempDir("both-v1-v2");
183
210
  try {
@@ -42,7 +42,7 @@ describe('isolation:none stale branch guard (#3675)', () => {
42
42
  });
43
43
 
44
44
  test('guard is conditional on isolation mode "none"', () => {
45
- assert.match(source, /getIsolationMode\(\)\s*===\s*["']none["']/,
45
+ assert.match(source, /getIsolationMode\([^)]*\)\s*===\s*["']none["']/,
46
46
  'guard should only activate when isolation mode is "none"');
47
47
  });
48
48