gsd-pi 2.76.0-dev.76f9a2dc5 → 2.76.0-dev.97807402

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 (221) 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/resources/extensions/claude-code-cli/readiness.js +4 -3
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  6. package/dist/resources/extensions/gsd/auto/phases.js +14 -0
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  10. package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
  11. package/dist/resources/extensions/gsd/auto-start.js +27 -18
  12. package/dist/resources/extensions/gsd/auto-worktree.js +30 -48
  13. package/dist/resources/extensions/gsd/auto.js +13 -17
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  15. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  16. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  17. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
  19. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
  20. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  21. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  22. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  23. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  24. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  25. package/dist/resources/extensions/gsd/gsd-db.js +115 -7
  26. package/dist/resources/extensions/gsd/guided-flow.js +189 -0
  27. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  28. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  29. package/dist/resources/extensions/gsd/model-router.js +36 -3
  30. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
  31. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  32. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  33. package/dist/resources/extensions/gsd/preferences.js +17 -17
  34. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  35. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  36. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  37. package/dist/resources/extensions/gsd/safety/file-change-validator.js +9 -3
  38. package/dist/resources/extensions/gsd/safety/safety-harness.js +4 -0
  39. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  40. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  41. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  42. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  43. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  44. package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
  45. package/dist/resources/skills/write-docs/SKILL.md +2 -1
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  78. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  79. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  80. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  81. package/dist/web/standalone/server.js +1 -1
  82. package/package.json +1 -1
  83. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  85. package/packages/mcp-server/dist/remote-questions.js +732 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  87. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  88. package/packages/mcp-server/dist/server.js +18 -1
  89. package/packages/mcp-server/dist/server.js.map +1 -1
  90. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  92. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  93. package/packages/mcp-server/package.json +2 -1
  94. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  95. package/packages/mcp-server/src/remote-questions.ts +916 -0
  96. package/packages/mcp-server/src/server.ts +19 -1
  97. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  98. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  99. package/packages/mcp-server/tsconfig.test.json +19 -0
  100. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  101. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
  103. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  105. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  106. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  107. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  108. package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
  109. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  110. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  111. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  112. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  114. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  117. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  119. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  120. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  121. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  122. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  123. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  125. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  128. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  130. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  134. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  135. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  136. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  137. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  138. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  139. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  140. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  141. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  142. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  143. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  144. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  145. package/src/resources/extensions/gsd/auto/phases.ts +14 -0
  146. package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
  147. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  148. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -2
  149. package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
  150. package/src/resources/extensions/gsd/auto-start.ts +29 -19
  151. package/src/resources/extensions/gsd/auto-worktree.ts +34 -52
  152. package/src/resources/extensions/gsd/auto.ts +12 -17
  153. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  154. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  155. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  156. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  157. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
  158. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
  159. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  160. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  161. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  162. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  163. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  164. package/src/resources/extensions/gsd/gsd-db.ts +122 -7
  165. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  166. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  167. package/src/resources/extensions/gsd/journal.ts +2 -1
  168. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  169. package/src/resources/extensions/gsd/model-router.ts +42 -1
  170. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  171. package/src/resources/extensions/gsd/preferences-types.ts +46 -0
  172. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  173. package/src/resources/extensions/gsd/preferences.ts +17 -17
  174. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  175. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  176. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  177. package/src/resources/extensions/gsd/safety/file-change-validator.ts +13 -2
  178. package/src/resources/extensions/gsd/safety/safety-harness.ts +6 -0
  179. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +116 -0
  180. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
  181. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  182. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  183. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  184. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  185. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
  186. package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
  187. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  188. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  189. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +38 -0
  190. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
  191. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
  193. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  194. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  196. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
  197. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  198. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  199. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  200. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  201. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  202. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  203. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  204. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  205. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  206. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
  207. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  208. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  209. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  210. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  211. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  212. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  213. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  214. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  215. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  216. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  217. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  218. package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
  219. package/src/resources/skills/write-docs/SKILL.md +2 -1
  220. /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
  221. /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
@@ -0,0 +1,123 @@
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 {
8
+ buildSnapshot,
9
+ readCompactionSnapshot,
10
+ writeCompactionSnapshot,
11
+ DEFAULT_SNAPSHOT_BYTES,
12
+ } from '../compaction-snapshot.ts';
13
+ import { closeDatabase, openDatabase } from '../gsd-db.ts';
14
+ import { createMemory } from '../memory-store.ts';
15
+ import { executeResume } from '../tools/resume-tool.ts';
16
+
17
+ function freshBase(): string {
18
+ return mkdtempSync(join(tmpdir(), 'gsd-snap-'));
19
+ }
20
+
21
+ function cleanup(dir: string): void {
22
+ rmSync(dir, { recursive: true, force: true });
23
+ }
24
+
25
+ test('buildSnapshot: renders memories, exec history, and active context', () => {
26
+ const snap = buildSnapshot({
27
+ generatedAt: new Date('2026-04-20T12:00:00.000Z'),
28
+ activeContext: 'M001 / S01 / T01 — wire gsd_exec',
29
+ memories: [
30
+ { id: 'MEM001', category: 'gotcha', content: 'FTS5 needs Porter tokenizer', confidence: 0.9,
31
+ source_unit_type: null, source_unit_id: null, created_at: '', updated_at: '',
32
+ superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null },
33
+ ],
34
+ execHistory: [
35
+ {
36
+ id: 'abc',
37
+ runtime: 'bash',
38
+ purpose: 'count TODOs',
39
+ started_at: '', finished_at: '', duration_ms: 10,
40
+ exit_code: 0, signal: null, timed_out: false,
41
+ stdout_bytes: 1, stderr_bytes: 0, stdout_truncated: false, stderr_truncated: false,
42
+ stdout_path: '/tmp/abc.stdout', stderr_path: '/tmp/abc.stderr', meta_path: '/tmp/abc.meta.json',
43
+ },
44
+ ],
45
+ });
46
+ assert.match(snap, /Active context/);
47
+ assert.match(snap, /M001 \/ S01 \/ T01/);
48
+ assert.match(snap, /FTS5 needs Porter tokenizer/);
49
+ assert.match(snap, /\[abc\] bash exit:0 — count TODOs/);
50
+ });
51
+
52
+ test('buildSnapshot: enforces the byte cap with a truncation marker', () => {
53
+ const longMemories = Array.from({ length: 50 }, (_v, i) => ({
54
+ id: `MEM${String(i).padStart(3, '0')}`,
55
+ category: 'gotcha',
56
+ content: 'x'.repeat(200),
57
+ confidence: 0.8,
58
+ source_unit_type: null,
59
+ source_unit_id: null,
60
+ created_at: '',
61
+ updated_at: '',
62
+ superseded_by: null,
63
+ hit_count: 0,
64
+ scope: 'project',
65
+ seq: i,
66
+ tags: [] as string[],
67
+ structured_fields: null,
68
+ }));
69
+ const snap = buildSnapshot(
70
+ { generatedAt: new Date(), memories: longMemories, execHistory: [] },
71
+ { maxBytes: 512, maxMemories: 50 },
72
+ );
73
+ assert.ok(Buffer.byteLength(snap, 'utf-8') <= 512, 'should respect cap');
74
+ assert.match(snap, /\[truncated\]/, 'should include truncation marker');
75
+ });
76
+
77
+ test('buildSnapshot: handles empty state with an explanatory placeholder', () => {
78
+ const snap = buildSnapshot({ generatedAt: new Date(), memories: [], execHistory: [] });
79
+ assert.match(snap, /_No durable memories/);
80
+ assert.ok(Buffer.byteLength(snap, 'utf-8') <= DEFAULT_SNAPSHOT_BYTES);
81
+ });
82
+
83
+ test('writeCompactionSnapshot + readCompactionSnapshot + executeResume: end-to-end', () => {
84
+ const base = freshBase();
85
+ try {
86
+ openDatabase(':memory:');
87
+ createMemory({ category: 'architecture', content: 'Single-writer DB through gsd-db.ts', confidence: 0.95 });
88
+ createMemory({ category: 'convention', content: 'Prefer typed helpers over raw SQL', confidence: 0.9 });
89
+
90
+ const out = writeCompactionSnapshot(base, { activeContext: 'M099 resume check' });
91
+ assert.ok(out.path.endsWith('last-snapshot.md'));
92
+ assert.ok(out.bytes > 0);
93
+ assert.equal(out.memories, 2);
94
+
95
+ const contents = readCompactionSnapshot(base);
96
+ assert.ok(contents);
97
+ assert.match(contents!, /Single-writer DB through gsd-db\.ts/);
98
+ assert.match(contents!, /M099 resume check/);
99
+
100
+ const tool = executeResume({}, { baseDir: base });
101
+ assert.ok(!tool.isError);
102
+ assert.equal(tool.details.found, true);
103
+ assert.match(tool.content[0].text, /Single-writer DB through gsd-db\.ts/);
104
+
105
+ // also verify the file content matches (without trailing newline)
106
+ const raw = readFileSync(out.path, 'utf-8');
107
+ assert.ok(raw.endsWith('\n'));
108
+ } finally {
109
+ closeDatabase();
110
+ cleanup(base);
111
+ }
112
+ });
113
+
114
+ test('executeResume: reports friendly empty state when no snapshot exists', () => {
115
+ const base = freshBase();
116
+ try {
117
+ const result = executeResume({}, { baseDir: base });
118
+ assert.equal(result.details.found, false);
119
+ assert.match(result.content[0].text, /No snapshot found/);
120
+ } finally {
121
+ cleanup(base);
122
+ }
123
+ });
@@ -125,9 +125,9 @@ console.log('\n=== complete-slice: schema v6 migration ===');
125
125
 
126
126
  const adapter = _getAdapter()!;
127
127
 
128
- // Verify schema version is current (v21ADR-013 structured_fields column)
128
+ // Verify schema version is current (v22quality_gates DDL fix)
129
129
  const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
130
- assertEq(versionRow?.['v'], 21, 'schema version should be 21');
130
+ assertEq(versionRow?.['v'], 22, 'schema version should be 22');
131
131
 
132
132
  // Verify slices table has full_summary_md and full_uat_md columns
133
133
  const cols = adapter.prepare("PRAGMA table_info(slices)").all();
@@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ===');
109
109
 
110
110
  const adapter = _getAdapter()!;
111
111
 
112
- // Verify schema version is current (v21ADR-013 structured_fields column)
112
+ // Verify schema version is current (v22quality_gates DDL fix)
113
113
  const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
114
- assertEq(versionRow?.['v'], 21, 'schema version should be 21');
114
+ assertEq(versionRow?.['v'], 22, 'schema version should be 22');
115
115
 
116
116
  // Verify all 4 new tables exist
117
117
  const tables = adapter.prepare(
@@ -768,6 +768,37 @@ test("runProviderChecks detects claude.cmd in PATH on Windows (#4503)", { skip:
768
768
  });
769
769
  });
770
770
 
771
+ test("runProviderChecks detects claude.exe in PATH on Windows (#4548)", { skip: process.platform !== "win32" }, () => {
772
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-exe-home-")));
773
+ const binDir = join(tmpHome, "bin");
774
+ mkdirSync(binDir, { recursive: true });
775
+
776
+ // Some Windows installs ship a direct claude.exe binary (not a .cmd shim).
777
+ const fakeClaudeExe = join(binDir, "claude.exe");
778
+ writeFileSync(fakeClaudeExe, "");
779
+
780
+ withEnv({
781
+ HOME: tmpHome,
782
+ ANTHROPIC_API_KEY: undefined,
783
+ ANTHROPIC_OAUTH_TOKEN: undefined,
784
+ COPILOT_GITHUB_TOKEN: undefined,
785
+ GH_TOKEN: undefined,
786
+ GITHUB_TOKEN: undefined,
787
+ PATH: `${binDir};${process.env.PATH ?? ""}`,
788
+ PATHEXT: ".COM;.EXE;.BAT;.CMD",
789
+ }, () => {
790
+ try {
791
+ const results = runProviderChecks();
792
+ const anthropic = results.find(r => r.name === "anthropic");
793
+ assert.ok(anthropic, "anthropic result should exist");
794
+ assert.equal(anthropic!.status, "ok", "should be ok when claude.exe is in PATH (#4548)");
795
+ assert.ok(anthropic!.message.toLowerCase().includes("claude"), "should mention claude-code as source");
796
+ } finally {
797
+ rmSync(tmpHome, { recursive: true, force: true });
798
+ }
799
+ });
800
+ });
801
+
771
802
  test("PROVIDER_ROUTES includes google-gemini-cli as route for google (#2922)", async () => {
772
803
  const { readFileSync: readFS } = await import("node:fs");
773
804
  const { dirname: dirn, join: joinPath } = await import("node:path");
@@ -389,7 +389,7 @@ describe('ensure-db-open', () => {
389
389
  assert.ok(db, 'adapter should be available after ensureDbOpen');
390
390
  assert.equal(
391
391
  db.prepare('SELECT MAX(version) as version FROM schema_version').get()?.version,
392
- 21,
392
+ 22,
393
393
  'legacy DB should migrate to current schema version',
394
394
  );
395
395
 
@@ -348,7 +348,7 @@ test("ADR-011 P2: schema v20 fresh DB has all escalation columns on tasks + sour
348
348
  assert.ok(decCols.includes("source"), "decisions table must have source column");
349
349
 
350
350
  const version = adapter.prepare("SELECT MAX(version) as v FROM schema_version").get();
351
- assert.equal(version?.["v"], 21);
351
+ assert.equal(version?.["v"], 22);
352
352
  });
353
353
 
354
354
  test("ADR-011 P2: findUnappliedEscalationOverride returns null when escalation_pending=1 (still pending)", (t) => {
@@ -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
+ });
@@ -35,6 +35,44 @@ test("validateFileChanges works on repos with a single commit (no HEAD~1)", (t)
35
35
  assert.deepEqual(audit.missingFiles, []);
36
36
  });
37
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
+
38
76
  test("validateFileChanges ignores inline descriptions in expected output paths", (t) => {
39
77
  const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
40
78
  t.after(() => rmSync(base, { recursive: true, force: true }));