gsd-pi 2.17.0 → 2.19.0

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 (217) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  9. package/dist/resources/extensions/gsd/auto.ts +399 -29
  10. package/dist/resources/extensions/gsd/captures.ts +384 -0
  11. package/dist/resources/extensions/gsd/commands.ts +382 -23
  12. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  16. package/dist/resources/extensions/gsd/files.ts +123 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  18. package/dist/resources/extensions/gsd/index.ts +47 -3
  19. package/dist/resources/extensions/gsd/metrics.ts +48 -0
  20. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  21. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  22. package/dist/resources/extensions/gsd/paths.ts +9 -0
  23. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  24. package/dist/resources/extensions/gsd/preferences.ts +132 -1
  25. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  28. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  29. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  30. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  31. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  32. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  33. package/dist/resources/extensions/gsd/state.ts +15 -3
  34. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  35. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  37. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  38. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  45. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  46. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  47. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  48. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  49. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  50. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  51. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  52. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  56. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  57. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  58. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  59. package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
  60. package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  61. package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
  62. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  63. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  64. package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  65. package/dist/resources/extensions/remote-questions/format.ts +12 -6
  66. package/dist/resources/extensions/remote-questions/manager.ts +8 -0
  67. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  68. package/package.json +1 -1
  69. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  70. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  72. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  74. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  76. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  78. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  86. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  102. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  115. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  117. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/index.js +4 -1
  119. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/main.js +17 -2
  122. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  138. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  139. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  140. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  141. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  142. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  143. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  144. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  145. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  146. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  147. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  148. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  149. package/packages/pi-coding-agent/src/index.ts +5 -0
  150. package/packages/pi-coding-agent/src/main.ts +19 -2
  151. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  152. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  153. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  154. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  155. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  156. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  157. package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
  158. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  159. package/src/resources/extensions/gsd/auto.ts +399 -29
  160. package/src/resources/extensions/gsd/captures.ts +384 -0
  161. package/src/resources/extensions/gsd/commands.ts +382 -23
  162. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  163. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  164. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  165. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  166. package/src/resources/extensions/gsd/files.ts +123 -1
  167. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  168. package/src/resources/extensions/gsd/index.ts +47 -3
  169. package/src/resources/extensions/gsd/metrics.ts +48 -0
  170. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  171. package/src/resources/extensions/gsd/model-router.ts +256 -0
  172. package/src/resources/extensions/gsd/paths.ts +9 -0
  173. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  174. package/src/resources/extensions/gsd/preferences.ts +132 -1
  175. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  176. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  177. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  178. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  179. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  180. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  181. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  182. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  183. package/src/resources/extensions/gsd/state.ts +15 -3
  184. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  185. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  186. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  187. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  188. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  189. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  190. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  192. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  193. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  194. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  195. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  197. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  198. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  199. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  200. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  201. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  202. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  203. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  204. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  205. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  206. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  207. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  208. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  209. package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
  210. package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  211. package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
  212. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  213. package/src/resources/extensions/gsd/worktree.ts +22 -0
  214. package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  215. package/src/resources/extensions/remote-questions/format.ts +12 -6
  216. package/src/resources/extensions/remote-questions/manager.ts +8 -0
  217. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Unit tests for KNOWLEDGE.md integration.
3
+ *
4
+ * Tests:
5
+ * - KNOWLEDGE is registered in GSD_ROOT_FILES
6
+ * - resolveGsdRootFile resolves KNOWLEDGE paths correctly
7
+ * - inlineGsdRootFile works with the KNOWLEDGE key
8
+ * - before_agent_start hook includes/omits knowledge block appropriately
9
+ */
10
+
11
+ import test from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { tmpdir } from 'node:os';
16
+ import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
17
+ import { inlineGsdRootFile } from '../auto-prompts.ts';
18
+ import { appendKnowledge } from '../files.ts';
19
+
20
+ // ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
21
+
22
+ test('knowledge: KNOWLEDGE key exists in GSD_ROOT_FILES', () => {
23
+ assert.ok('KNOWLEDGE' in GSD_ROOT_FILES, 'GSD_ROOT_FILES should have KNOWLEDGE key');
24
+ assert.strictEqual(GSD_ROOT_FILES.KNOWLEDGE, 'KNOWLEDGE.md');
25
+ });
26
+
27
+ // ─── resolveGsdRootFile resolves KNOWLEDGE.md ───────────────────────────────
28
+
29
+ test('knowledge: resolveGsdRootFile returns canonical path when KNOWLEDGE.md exists', () => {
30
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
31
+ const gsdDir = join(tmp, '.gsd');
32
+ mkdirSync(gsdDir, { recursive: true });
33
+ writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n');
34
+
35
+ const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
36
+ assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
37
+
38
+ rmSync(tmp, { recursive: true, force: true });
39
+ });
40
+
41
+ test('knowledge: resolveGsdRootFile resolves when legacy knowledge.md exists', () => {
42
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
43
+ const gsdDir = join(tmp, '.gsd');
44
+ mkdirSync(gsdDir, { recursive: true });
45
+ writeFileSync(join(gsdDir, 'knowledge.md'), '# Project Knowledge\n');
46
+
47
+ const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
48
+ // On case-insensitive filesystems (macOS), canonical path matches;
49
+ // on case-sensitive (Linux), legacy path matches. Either is valid.
50
+ const canonical = join(gsdDir, 'KNOWLEDGE.md');
51
+ const legacy = join(gsdDir, 'knowledge.md');
52
+ assert.ok(
53
+ resolved === canonical || resolved === legacy,
54
+ `resolved path should be canonical or legacy, got: ${resolved}`,
55
+ );
56
+
57
+ rmSync(tmp, { recursive: true, force: true });
58
+ });
59
+
60
+ test('knowledge: resolveGsdRootFile returns canonical path when file does not exist', () => {
61
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
62
+ const gsdDir = join(tmp, '.gsd');
63
+ mkdirSync(gsdDir, { recursive: true });
64
+
65
+ const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
66
+ assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
67
+
68
+ rmSync(tmp, { recursive: true, force: true });
69
+ });
70
+
71
+ // ─── inlineGsdRootFile works with knowledge.md ─────────────────────────────
72
+
73
+ test('knowledge: inlineGsdRootFile returns content when KNOWLEDGE.md exists', async () => {
74
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
75
+ const gsdDir = join(tmp, '.gsd');
76
+ mkdirSync(gsdDir, { recursive: true });
77
+ writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n\n## Rules\n\nK001: Use real DB');
78
+
79
+ const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
80
+ assert.ok(result !== null, 'should return content');
81
+ assert.ok(result!.includes('Project Knowledge'), 'should include label');
82
+ assert.ok(result!.includes('K001'), 'should include knowledge content');
83
+
84
+ rmSync(tmp, { recursive: true, force: true });
85
+ });
86
+
87
+ test('knowledge: inlineGsdRootFile returns null when KNOWLEDGE.md does not exist', async () => {
88
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
89
+ const gsdDir = join(tmp, '.gsd');
90
+ mkdirSync(gsdDir, { recursive: true });
91
+
92
+ const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
93
+ assert.strictEqual(result, null, 'should return null when file does not exist');
94
+
95
+ rmSync(tmp, { recursive: true, force: true });
96
+ });
97
+
98
+ // ─── appendKnowledge creates file and appends entries ──────────────────────
99
+
100
+ test('knowledge: appendKnowledge creates KNOWLEDGE.md with rule when file does not exist', async () => {
101
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
102
+ const gsdDir = join(tmp, '.gsd');
103
+ mkdirSync(gsdDir, { recursive: true });
104
+
105
+ await appendKnowledge(tmp, 'rule', 'Use real DB for integration tests', 'M001/S01');
106
+
107
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
108
+ assert.ok(content.includes('# Project Knowledge'), 'should have header');
109
+ assert.ok(content.includes('K001'), 'should have K001 id');
110
+ assert.ok(content.includes('Use real DB for integration tests'), 'should have rule text');
111
+ assert.ok(content.includes('M001/S01'), 'should have scope');
112
+
113
+ rmSync(tmp, { recursive: true, force: true });
114
+ });
115
+
116
+ test('knowledge: appendKnowledge appends to existing KNOWLEDGE.md with auto-incrementing ID', async () => {
117
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
118
+ const gsdDir = join(tmp, '.gsd');
119
+ mkdirSync(gsdDir, { recursive: true });
120
+
121
+ // Create initial file with one rule
122
+ await appendKnowledge(tmp, 'rule', 'First rule', 'M001');
123
+ // Add second rule
124
+ await appendKnowledge(tmp, 'rule', 'Second rule', 'M001/S02');
125
+
126
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
127
+ assert.ok(content.includes('K001'), 'should have K001');
128
+ assert.ok(content.includes('K002'), 'should have K002');
129
+ assert.ok(content.includes('First rule'), 'should have first rule');
130
+ assert.ok(content.includes('Second rule'), 'should have second rule');
131
+
132
+ rmSync(tmp, { recursive: true, force: true });
133
+ });
134
+
135
+ test('knowledge: appendKnowledge handles pattern type', async () => {
136
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
137
+ const gsdDir = join(tmp, '.gsd');
138
+ mkdirSync(gsdDir, { recursive: true });
139
+
140
+ await appendKnowledge(tmp, 'pattern', 'Middleware chain for auth', 'M001');
141
+
142
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
143
+ assert.ok(content.includes('P001'), 'should have P001 id');
144
+ assert.ok(content.includes('Middleware chain for auth'), 'should have pattern text');
145
+
146
+ rmSync(tmp, { recursive: true, force: true });
147
+ });
148
+
149
+ test('knowledge: appendKnowledge handles lesson type', async () => {
150
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
151
+ const gsdDir = join(tmp, '.gsd');
152
+ mkdirSync(gsdDir, { recursive: true });
153
+
154
+ await appendKnowledge(tmp, 'lesson', 'API timeout on large payloads', 'M002');
155
+
156
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
157
+ assert.ok(content.includes('L001'), 'should have L001 id');
158
+ assert.ok(content.includes('API timeout on large payloads'), 'should have lesson text');
159
+
160
+ rmSync(tmp, { recursive: true, force: true });
161
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * memory-leak-guards.test.ts — Tests for #611 memory leak fixes.
3
+ *
4
+ * Verifies that module-level state accumulators are properly bounded
5
+ * and cleared to prevent OOM during long-running auto-mode sessions.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtempSync, rmSync, existsSync, readdirSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+
14
+ import { saveActivityLog, clearActivityLogState } from "../activity-log.ts";
15
+ import { clearPathCache } from "../paths.ts";
16
+ import type { ExtensionContext } from "@gsd/pi-coding-agent";
17
+
18
+ function createCtx(entries: unknown[]) {
19
+ return { sessionManager: { getEntries: () => entries } } as unknown as ExtensionContext;
20
+ }
21
+
22
+ // ─── activity-log: clearActivityLogState ─────────────────────────────────────
23
+
24
+ test("clearActivityLogState resets dedup state so identical saves write again", () => {
25
+ clearActivityLogState();
26
+ const baseDir = mkdtempSync(join(tmpdir(), "gsd-memleak-test-"));
27
+ try {
28
+ const entries = [{ role: "assistant", content: "test entry" }];
29
+ const ctx = createCtx(entries);
30
+
31
+ // First save
32
+ saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
33
+
34
+ const actDir = join(baseDir, ".gsd", "activity");
35
+ assert.equal(readdirSync(actDir).length, 1, "first save creates one file");
36
+
37
+ // Same content, same unit — deduped
38
+ saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
39
+ assert.equal(readdirSync(actDir).length, 1, "dedup prevents duplicate write");
40
+
41
+ // Clear state
42
+ clearActivityLogState();
43
+
44
+ // Same content again — after clear, writes again (fresh state)
45
+ saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
46
+ assert.equal(readdirSync(actDir).length, 2, "after clear, dedup state is reset");
47
+ } finally {
48
+ rmSync(baseDir, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ // ─── activity-log: streaming JSONL write ────────────────────────────────────
53
+
54
+ test("saveActivityLog writes valid JSONL via streaming", () => {
55
+ clearActivityLogState();
56
+ const baseDir = mkdtempSync(join(tmpdir(), "gsd-memleak-jsonl-"));
57
+ try {
58
+ const entries = [
59
+ { type: "message", message: { role: "user", content: "hello" } },
60
+ { type: "message", message: { role: "assistant", content: "world" } },
61
+ { type: "message", message: { role: "user", content: "test" } },
62
+ ];
63
+ const ctx = createCtx(entries);
64
+
65
+ saveActivityLog(ctx, baseDir, "execute-task", "M002/S01/T01");
66
+
67
+ const actDir = join(baseDir, ".gsd", "activity");
68
+ const files = readdirSync(actDir);
69
+ assert.equal(files.length, 1, "one file written");
70
+
71
+ const content = readFileSync(join(actDir, files[0]), "utf-8");
72
+ const lines = content.trim().split("\n");
73
+ assert.equal(lines.length, 3, "three JSONL lines");
74
+
75
+ for (const line of lines) {
76
+ assert.doesNotThrow(() => JSON.parse(line), `line is valid JSON`);
77
+ }
78
+ } finally {
79
+ rmSync(baseDir, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ // ─── paths.ts: directory cache bounds ───────────────────────────────────────
84
+
85
+ test("clearPathCache does not throw", () => {
86
+ assert.doesNotThrow(() => clearPathCache(), "clearPathCache should not throw");
87
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * milestone-transition-worktree.test.ts — Tests for #616 fix.
3
+ *
4
+ * Verifies that when auto-mode transitions between milestones, the
5
+ * worktree lifecycle is handled: old worktree merged, new worktree created.
6
+ *
7
+ * Uses source-level checks since the full auto-mode dispatch loop
8
+ * requires the @gsd/pi-coding-agent runtime.
9
+ */
10
+
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, realpathSync, readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+ import { execSync } from "node:child_process";
17
+
18
+ import { dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ import {
22
+ createAutoWorktree,
23
+ teardownAutoWorktree,
24
+ isInAutoWorktree,
25
+ getAutoWorktreeOriginalBase,
26
+ mergeMilestoneToMain,
27
+ } from "../auto-worktree.ts";
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+
31
+ function run(command: string, cwd: string): string {
32
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
33
+ }
34
+
35
+ function createTempRepo(): string {
36
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-mt-wt-test-")));
37
+ run("git init", dir);
38
+ run("git config user.email test@test.com", dir);
39
+ run("git config user.name Test", dir);
40
+ writeFileSync(join(dir, "README.md"), "# test\n");
41
+ run("git add .", dir);
42
+ run("git commit -m init", dir);
43
+ run("git branch -M main", dir);
44
+ return dir;
45
+ }
46
+
47
+ function createMilestoneArtifacts(dir: string, mid: string): void {
48
+ const msDir = join(dir, ".gsd", "milestones", mid);
49
+ mkdirSync(msDir, { recursive: true });
50
+ writeFileSync(join(msDir, "CONTEXT.md"), `# ${mid} Context\n`);
51
+ const roadmap = [
52
+ `# ${mid}: Test Milestone`,
53
+ "**Vision**: testing",
54
+ "## Success Criteria",
55
+ "- It works",
56
+ "## Slices",
57
+ "- [x] S01 — First slice",
58
+ ].join("\n");
59
+ writeFileSync(join(msDir, `${mid}-ROADMAP.md`), roadmap);
60
+ }
61
+
62
+ // ─── Milestone transition: worktree swap ─────────────────────────────────────
63
+
64
+ test("worktree swap on milestone transition: merge old, create new", () => {
65
+ const savedCwd = process.cwd();
66
+ let tempDir = "";
67
+
68
+ try {
69
+ tempDir = createTempRepo();
70
+
71
+ // Set up M001 and M002 milestone artifacts
72
+ createMilestoneArtifacts(tempDir, "M001");
73
+ createMilestoneArtifacts(tempDir, "M002");
74
+ run("git add .", tempDir);
75
+ run("git commit -m \"add milestones\"", tempDir);
76
+
77
+ // Phase 1: Create worktree for M001 (simulates auto-mode start)
78
+ const wt1 = createAutoWorktree(tempDir, "M001");
79
+ assert.equal(process.cwd(), wt1, "cwd should be in M001 worktree");
80
+ assert.ok(isInAutoWorktree(tempDir), "should be in auto-worktree");
81
+ assert.equal(getAutoWorktreeOriginalBase(), tempDir, "original base preserved");
82
+
83
+ // Add a commit in M001 worktree to simulate work
84
+ writeFileSync(join(wt1, "feature-m001.txt"), "M001 work\n");
85
+ run("git add .", wt1);
86
+ run("git commit -m \"feat(M001): add feature\"", wt1);
87
+
88
+ // Phase 2: Simulate milestone transition — merge M001, exit worktree
89
+ const roadmapPath = join(tempDir, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
90
+ const roadmapContent = readFileSync(roadmapPath, "utf-8");
91
+ mergeMilestoneToMain(tempDir, "M001", roadmapContent);
92
+
93
+ // After merge: cwd should be back at project root
94
+ assert.equal(process.cwd(), tempDir, "cwd restored to project root after merge");
95
+ assert.ok(!isInAutoWorktree(tempDir), "no longer in auto-worktree after merge");
96
+
97
+ // Verify M001 work was merged to main
98
+ const mainLog = run("git log --oneline -3", tempDir);
99
+ assert.ok(mainLog.includes("M001"), "M001 squash commit should be on main");
100
+
101
+ // Phase 3: Create new worktree for M002 (simulates new milestone)
102
+ const wt2 = createAutoWorktree(tempDir, "M002");
103
+ assert.equal(process.cwd(), wt2, "cwd should be in M002 worktree");
104
+ assert.ok(isInAutoWorktree(tempDir), "should be in M002 auto-worktree");
105
+
106
+ // The new worktree should have the M001 feature file (merged to main)
107
+ assert.ok(existsSync(join(wt2, "feature-m001.txt")), "M002 worktree inherits M001 merged work");
108
+
109
+ // Verify branch is correct
110
+ const branch = run("git branch --show-current", wt2);
111
+ assert.equal(branch, "milestone/M002", "M002 worktree on correct branch");
112
+
113
+ // Cleanup
114
+ teardownAutoWorktree(tempDir, "M002");
115
+ } finally {
116
+ process.chdir(savedCwd);
117
+ if (tempDir && existsSync(tempDir)) {
118
+ rmSync(tempDir, { recursive: true, force: true });
119
+ }
120
+ }
121
+ });
122
+
123
+ // ─── Verify the transition code path exists in auto.ts ──────────────────────
124
+
125
+ test("auto.ts milestone transition block contains worktree lifecycle", () => {
126
+ const autoSrc = readFileSync(
127
+ join(__dirname, "..", "auto.ts"),
128
+ "utf-8",
129
+ );
130
+
131
+ // The fix adds worktree merge + create inside the milestone transition block
132
+ assert.ok(
133
+ autoSrc.includes("Worktree lifecycle on milestone transition"),
134
+ "auto.ts should contain the worktree lifecycle comment marker",
135
+ );
136
+ assert.ok(
137
+ autoSrc.includes("mergeMilestoneToMain") && autoSrc.includes("mid !== currentMilestoneId"),
138
+ "auto.ts should call mergeMilestoneToMain during milestone transition",
139
+ );
140
+ assert.ok(
141
+ autoSrc.includes("createAutoWorktree") && autoSrc.includes("Created auto-worktree for"),
142
+ "auto.ts should create new worktree for incoming milestone",
143
+ );
144
+ });
@@ -0,0 +1,69 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { lookupModelCost, compareModelCost, BUNDLED_COST_TABLE } from "../model-cost-table.js";
5
+
6
+ // ─── lookupModelCost ─────────────────────────────────────────────────────────
7
+
8
+ test("lookupModelCost finds exact match", () => {
9
+ const entry = lookupModelCost("claude-opus-4-6");
10
+ assert.ok(entry);
11
+ assert.equal(entry.id, "claude-opus-4-6");
12
+ assert.ok(entry.inputPer1k > 0);
13
+ assert.ok(entry.outputPer1k > 0);
14
+ });
15
+
16
+ test("lookupModelCost strips provider prefix", () => {
17
+ const entry = lookupModelCost("anthropic/claude-opus-4-6");
18
+ assert.ok(entry);
19
+ assert.equal(entry.id, "claude-opus-4-6");
20
+ });
21
+
22
+ test("lookupModelCost returns undefined for unknown model", () => {
23
+ const entry = lookupModelCost("totally-unknown-model");
24
+ assert.equal(entry, undefined);
25
+ });
26
+
27
+ test("lookupModelCost finds haiku", () => {
28
+ const entry = lookupModelCost("claude-haiku-4-5");
29
+ assert.ok(entry);
30
+ assert.ok(entry.inputPer1k < 0.001, "haiku should be cheap");
31
+ });
32
+
33
+ // ─── compareModelCost ────────────────────────────────────────────────────────
34
+
35
+ test("haiku is cheaper than opus", () => {
36
+ assert.ok(compareModelCost("claude-haiku-4-5", "claude-opus-4-6") < 0);
37
+ });
38
+
39
+ test("opus is more expensive than sonnet", () => {
40
+ assert.ok(compareModelCost("claude-opus-4-6", "claude-sonnet-4-6") > 0);
41
+ });
42
+
43
+ test("same model has equal cost", () => {
44
+ assert.equal(compareModelCost("claude-opus-4-6", "claude-opus-4-6"), 0);
45
+ });
46
+
47
+ // ─── BUNDLED_COST_TABLE ──────────────────────────────────────────────────────
48
+
49
+ test("cost table has entries for all major providers", () => {
50
+ const ids = BUNDLED_COST_TABLE.map(e => e.id);
51
+ // Anthropic
52
+ assert.ok(ids.includes("claude-opus-4-6"));
53
+ assert.ok(ids.includes("claude-sonnet-4-6"));
54
+ assert.ok(ids.includes("claude-haiku-4-5"));
55
+ // OpenAI
56
+ assert.ok(ids.includes("gpt-4o"));
57
+ assert.ok(ids.includes("gpt-4o-mini"));
58
+ // Google
59
+ assert.ok(ids.includes("gemini-2.0-flash"));
60
+ });
61
+
62
+ test("all cost table entries have valid data", () => {
63
+ for (const entry of BUNDLED_COST_TABLE) {
64
+ assert.ok(entry.id, `entry missing id`);
65
+ assert.ok(entry.inputPer1k >= 0, `${entry.id} inputPer1k should be >= 0`);
66
+ assert.ok(entry.outputPer1k >= 0, `${entry.id} outputPer1k should be >= 0`);
67
+ assert.ok(entry.updatedAt, `${entry.id} missing updatedAt`);
68
+ }
69
+ });
@@ -0,0 +1,167 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ resolveModelForComplexity,
6
+ escalateTier,
7
+ defaultRoutingConfig,
8
+ } from "../model-router.js";
9
+ import type { DynamicRoutingConfig, RoutingDecision } from "../model-router.js";
10
+ import type { ClassificationResult } from "../complexity-classifier.js";
11
+
12
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
13
+
14
+ function makeClassification(tier: "light" | "standard" | "heavy", reason = "test"): ClassificationResult {
15
+ return { tier, reason, downgraded: false };
16
+ }
17
+
18
+ const AVAILABLE_MODELS = [
19
+ "claude-opus-4-6",
20
+ "claude-sonnet-4-6",
21
+ "claude-haiku-4-5",
22
+ "gpt-4o-mini",
23
+ ];
24
+
25
+ // ─── Passthrough when disabled ───────────────────────────────────────────────
26
+
27
+ test("returns configured model when routing is disabled", () => {
28
+ const config = { ...defaultRoutingConfig(), enabled: false };
29
+ const result = resolveModelForComplexity(
30
+ makeClassification("light"),
31
+ { primary: "claude-opus-4-6", fallbacks: [] },
32
+ config,
33
+ AVAILABLE_MODELS,
34
+ );
35
+ assert.equal(result.modelId, "claude-opus-4-6");
36
+ assert.equal(result.wasDowngraded, false);
37
+ });
38
+
39
+ test("returns configured model when no phase config", () => {
40
+ const config = { ...defaultRoutingConfig(), enabled: true };
41
+ const result = resolveModelForComplexity(
42
+ makeClassification("light"),
43
+ undefined,
44
+ config,
45
+ AVAILABLE_MODELS,
46
+ );
47
+ assert.equal(result.modelId, "");
48
+ assert.equal(result.wasDowngraded, false);
49
+ });
50
+
51
+ // ─── Downgrade-only semantics ────────────────────────────────────────────────
52
+
53
+ test("does not downgrade when tier matches configured model tier", () => {
54
+ const config = { ...defaultRoutingConfig(), enabled: true };
55
+ const result = resolveModelForComplexity(
56
+ makeClassification("heavy"),
57
+ { primary: "claude-opus-4-6", fallbacks: [] },
58
+ config,
59
+ AVAILABLE_MODELS,
60
+ );
61
+ assert.equal(result.modelId, "claude-opus-4-6");
62
+ assert.equal(result.wasDowngraded, false);
63
+ });
64
+
65
+ test("does not upgrade beyond configured model", () => {
66
+ const config = { ...defaultRoutingConfig(), enabled: true };
67
+ // Configured model is sonnet (standard), classification says heavy
68
+ const result = resolveModelForComplexity(
69
+ makeClassification("heavy"),
70
+ { primary: "claude-sonnet-4-6", fallbacks: [] },
71
+ config,
72
+ AVAILABLE_MODELS,
73
+ );
74
+ assert.equal(result.modelId, "claude-sonnet-4-6");
75
+ assert.equal(result.wasDowngraded, false);
76
+ });
77
+
78
+ test("downgrades from opus to haiku for light tier", () => {
79
+ const config = { ...defaultRoutingConfig(), enabled: true };
80
+ const result = resolveModelForComplexity(
81
+ makeClassification("light"),
82
+ { primary: "claude-opus-4-6", fallbacks: [] },
83
+ config,
84
+ AVAILABLE_MODELS,
85
+ );
86
+ // Should pick haiku or gpt-4o-mini (cheapest light tier)
87
+ assert.ok(
88
+ result.modelId === "claude-haiku-4-5" || result.modelId === "gpt-4o-mini",
89
+ `Expected light-tier model, got ${result.modelId}`,
90
+ );
91
+ assert.equal(result.wasDowngraded, true);
92
+ });
93
+
94
+ test("downgrades from opus to sonnet for standard tier", () => {
95
+ const config = { ...defaultRoutingConfig(), enabled: true };
96
+ const result = resolveModelForComplexity(
97
+ makeClassification("standard"),
98
+ { primary: "claude-opus-4-6", fallbacks: [] },
99
+ config,
100
+ AVAILABLE_MODELS,
101
+ );
102
+ assert.equal(result.modelId, "claude-sonnet-4-6");
103
+ assert.equal(result.wasDowngraded, true);
104
+ });
105
+
106
+ // ─── Explicit tier_models ────────────────────────────────────────────────────
107
+
108
+ test("uses explicit tier_models when configured", () => {
109
+ const config: DynamicRoutingConfig = {
110
+ ...defaultRoutingConfig(),
111
+ enabled: true,
112
+ tier_models: { light: "gpt-4o-mini", standard: "claude-sonnet-4-6" },
113
+ };
114
+ const result = resolveModelForComplexity(
115
+ makeClassification("light"),
116
+ { primary: "claude-opus-4-6", fallbacks: [] },
117
+ config,
118
+ AVAILABLE_MODELS,
119
+ );
120
+ assert.equal(result.modelId, "gpt-4o-mini");
121
+ assert.equal(result.wasDowngraded, true);
122
+ });
123
+
124
+ // ─── Fallback chain construction ─────────────────────────────────────────────
125
+
126
+ test("fallback chain includes configured primary as last resort", () => {
127
+ const config = { ...defaultRoutingConfig(), enabled: true };
128
+ const result = resolveModelForComplexity(
129
+ makeClassification("light"),
130
+ { primary: "claude-opus-4-6", fallbacks: ["claude-sonnet-4-6"] },
131
+ config,
132
+ AVAILABLE_MODELS,
133
+ );
134
+ assert.ok(result.wasDowngraded);
135
+ // Fallbacks should include the configured fallbacks and primary
136
+ assert.ok(result.fallbacks.includes("claude-opus-4-6"), "primary should be in fallbacks");
137
+ assert.ok(result.fallbacks.includes("claude-sonnet-4-6"), "configured fallback should be in fallbacks");
138
+ });
139
+
140
+ // ─── Escalation ──────────────────────────────────────────────────────────────
141
+
142
+ test("escalateTier moves light → standard", () => {
143
+ assert.equal(escalateTier("light"), "standard");
144
+ });
145
+
146
+ test("escalateTier moves standard → heavy", () => {
147
+ assert.equal(escalateTier("standard"), "heavy");
148
+ });
149
+
150
+ test("escalateTier returns null for heavy (max)", () => {
151
+ assert.equal(escalateTier("heavy"), null);
152
+ });
153
+
154
+ // ─── No suitable model available ─────────────────────────────────────────────
155
+
156
+ test("falls back to configured model when no light-tier model available", () => {
157
+ const config = { ...defaultRoutingConfig(), enabled: true };
158
+ // Only heavy-tier models available
159
+ const result = resolveModelForComplexity(
160
+ makeClassification("light"),
161
+ { primary: "claude-opus-4-6", fallbacks: [] },
162
+ config,
163
+ ["claude-opus-4-6"],
164
+ );
165
+ assert.equal(result.modelId, "claude-opus-4-6");
166
+ assert.equal(result.wasDowngraded, false);
167
+ });