gsd-pi 2.61.0-dev.7aed0bf → 2.62.0-dev.a987556

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 (119) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +47 -3
  2. package/dist/resources/extensions/gsd/auto-start.js +11 -6
  3. package/dist/resources/extensions/gsd/auto-timers.js +8 -2
  4. package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
  5. package/dist/resources/extensions/gsd/auto.js +24 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
  7. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
  8. package/dist/resources/extensions/gsd/commands-handlers.js +18 -7
  9. package/dist/resources/extensions/gsd/db-writer.js +64 -28
  10. package/dist/resources/extensions/gsd/preferences-models.js +74 -0
  11. package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
  12. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  13. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  14. package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
  15. package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
  16. package/dist/resources/extensions/gsd/skill-health.js +7 -3
  17. package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
  18. package/dist/web/standalone/.next/BUILD_ID +1 -1
  19. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  20. package/dist/web/standalone/.next/build-manifest.json +2 -2
  21. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  22. package/dist/web/standalone/.next/required-server-files.json +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  24. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.html +1 -1
  40. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  47. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  48. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  49. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  50. package/dist/web/standalone/server.js +1 -1
  51. package/package.json +1 -1
  52. package/packages/mcp-server/src/cli.ts +1 -1
  53. package/packages/mcp-server/src/index.ts +15 -1
  54. package/packages/mcp-server/src/readers/captures.ts +119 -0
  55. package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
  56. package/packages/mcp-server/src/readers/index.ts +16 -0
  57. package/packages/mcp-server/src/readers/knowledge.ts +111 -0
  58. package/packages/mcp-server/src/readers/metrics.ts +118 -0
  59. package/packages/mcp-server/src/readers/paths.ts +217 -0
  60. package/packages/mcp-server/src/readers/readers.test.ts +509 -0
  61. package/packages/mcp-server/src/readers/roadmap.ts +263 -0
  62. package/packages/mcp-server/src/readers/state.ts +223 -0
  63. package/packages/mcp-server/src/server.ts +134 -3
  64. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
  65. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  66. package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
  67. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  68. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
  69. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  70. package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
  71. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
  72. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
  73. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
  75. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
  78. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
  80. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
  82. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
  84. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  85. package/packages/pi-coding-agent/package.json +1 -1
  86. package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
  87. package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
  88. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
  89. package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
  90. package/pkg/package.json +1 -1
  91. package/src/resources/extensions/ask-user-questions.ts +60 -4
  92. package/src/resources/extensions/gsd/auto-start.ts +11 -6
  93. package/src/resources/extensions/gsd/auto-timers.ts +8 -2
  94. package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
  95. package/src/resources/extensions/gsd/auto.ts +25 -0
  96. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  97. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
  98. package/src/resources/extensions/gsd/commands-handlers.ts +20 -7
  99. package/src/resources/extensions/gsd/db-writer.ts +67 -30
  100. package/src/resources/extensions/gsd/preferences-models.ts +78 -0
  101. package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
  102. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  103. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  104. package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
  105. package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
  106. package/src/resources/extensions/gsd/skill-health.ts +7 -3
  107. package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
  108. package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
  109. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
  110. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
  111. package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
  112. package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
  113. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
  114. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +108 -0
  115. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
  116. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
  117. package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
  118. /package/dist/web/standalone/.next/static/{b7FOoMHaUb3FPoLNbxar4 → AsCFY1___4XTI6GkTQkFb}/_buildManifest.js +0 -0
  119. /package/dist/web/standalone/.next/static/{b7FOoMHaUb3FPoLNbxar4 → AsCFY1___4XTI6GkTQkFb}/_ssgManifest.js +0 -0
@@ -0,0 +1,509 @@
1
+ // GSD MCP Server — reader tests
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import { describe, it, before, after } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import { randomBytes } from 'node:crypto';
10
+
11
+ import { readProgress } from './state.js';
12
+ import { readRoadmap } from './roadmap.js';
13
+ import { readHistory } from './metrics.js';
14
+ import { readCaptures } from './captures.js';
15
+ import { readKnowledge } from './knowledge.js';
16
+ import { runDoctorLite } from './doctor-lite.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Test fixture helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function tmpProject(): string {
23
+ const dir = join(tmpdir(), `gsd-mcp-test-${randomBytes(4).toString('hex')}`);
24
+ mkdirSync(dir, { recursive: true });
25
+ return dir;
26
+ }
27
+
28
+ function writeFixture(base: string, relPath: string, content: string): void {
29
+ const full = join(base, relPath);
30
+ mkdirSync(join(full, '..'), { recursive: true });
31
+ writeFileSync(full, content, 'utf-8');
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // readProgress tests
36
+ // ---------------------------------------------------------------------------
37
+
38
+ describe('readProgress', () => {
39
+ let projectDir: string;
40
+
41
+ before(() => {
42
+ projectDir = tmpProject();
43
+
44
+ writeFixture(projectDir, '.gsd/STATE.md', `# GSD State
45
+
46
+ **Active Milestone:** M002: Auth System
47
+ **Active Slice:** S01: Login flow
48
+ **Phase:** execution
49
+ **Requirements Status:** 5 active · 2 validated · 1 deferred · 0 out of scope
50
+
51
+ ## Milestone Registry
52
+
53
+ - ☑ **M001:** Core Setup
54
+ - 🔄 **M002:** Auth System
55
+ - ⬜ **M003:** Dashboard
56
+
57
+ ## Blockers
58
+
59
+ - Waiting on OAuth provider approval
60
+
61
+ ## Next Action
62
+
63
+ Execute T02 in S01 — implement token refresh.
64
+ `);
65
+
66
+ // Create filesystem structure
67
+ const m1 = '.gsd/milestones/M001/slices/S01/tasks';
68
+ writeFixture(projectDir, `${m1}/T01-PLAN.md`, '# T01');
69
+ writeFixture(projectDir, `${m1}/T01-SUMMARY.md`, '# T01 done');
70
+
71
+ const m2 = '.gsd/milestones/M002/slices/S01/tasks';
72
+ writeFixture(projectDir, `${m2}/T01-PLAN.md`, '# T01');
73
+ writeFixture(projectDir, `${m2}/T01-SUMMARY.md`, '# T01 done');
74
+ writeFixture(projectDir, `${m2}/T02-PLAN.md`, '# T02');
75
+
76
+ mkdirSync(join(projectDir, '.gsd/milestones/M003'), { recursive: true });
77
+ });
78
+
79
+ after(() => rmSync(projectDir, { recursive: true, force: true }));
80
+
81
+ it('parses active milestone from STATE.md', () => {
82
+ const result = readProgress(projectDir);
83
+ assert.deepEqual(result.activeMilestone, { id: 'M002', title: 'Auth System' });
84
+ });
85
+
86
+ it('parses active slice', () => {
87
+ const result = readProgress(projectDir);
88
+ assert.deepEqual(result.activeSlice, { id: 'S01', title: 'Login flow' });
89
+ });
90
+
91
+ it('parses phase', () => {
92
+ const result = readProgress(projectDir);
93
+ assert.equal(result.phase, 'execute');
94
+ });
95
+
96
+ it('parses milestone counts from registry', () => {
97
+ const result = readProgress(projectDir);
98
+ assert.equal(result.milestones.total, 3);
99
+ assert.equal(result.milestones.done, 1);
100
+ assert.equal(result.milestones.active, 1);
101
+ assert.equal(result.milestones.pending, 1);
102
+ });
103
+
104
+ it('counts tasks from filesystem', () => {
105
+ const result = readProgress(projectDir);
106
+ assert.equal(result.tasks.total, 3);
107
+ assert.equal(result.tasks.done, 2);
108
+ assert.equal(result.tasks.pending, 1);
109
+ });
110
+
111
+ it('parses blockers', () => {
112
+ const result = readProgress(projectDir);
113
+ assert.equal(result.blockers.length, 1);
114
+ assert.ok(result.blockers[0].includes('OAuth'));
115
+ });
116
+
117
+ it('parses requirements', () => {
118
+ const result = readProgress(projectDir);
119
+ assert.equal(result.requirements?.active, 5);
120
+ assert.equal(result.requirements?.validated, 2);
121
+ assert.equal(result.requirements?.deferred, 1);
122
+ });
123
+
124
+ it('parses next action', () => {
125
+ const result = readProgress(projectDir);
126
+ assert.ok(result.nextAction.includes('T02'));
127
+ });
128
+
129
+ it('returns defaults for missing .gsd/', () => {
130
+ const empty = tmpProject();
131
+ const result = readProgress(empty);
132
+ assert.equal(result.phase, 'unknown');
133
+ assert.equal(result.milestones.total, 0);
134
+ rmSync(empty, { recursive: true, force: true });
135
+ });
136
+ });
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // readRoadmap tests
140
+ // ---------------------------------------------------------------------------
141
+
142
+ describe('readRoadmap', () => {
143
+ let projectDir: string;
144
+
145
+ before(() => {
146
+ projectDir = tmpProject();
147
+
148
+ writeFixture(projectDir, '.gsd/milestones/M001/M001-CONTEXT.md', '# M001: Core Setup\n');
149
+ writeFixture(projectDir, '.gsd/milestones/M001/M001-ROADMAP.md', `# M001: Core Setup
150
+
151
+ ## Vision
152
+
153
+ Build the foundation for the project.
154
+
155
+ ## Slice Overview
156
+
157
+ | ID | Slice | Risk | Depends | Done | After this |
158
+ |----|-------|------|---------|------|------------|
159
+ | S01 | Database schema | low | — | ☑ | DB ready |
160
+ | S02 | API endpoints | medium | S01 | 🟫 | REST API live |
161
+ `);
162
+
163
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/S01-PLAN.md', `# S01: Database schema
164
+
165
+ ## Tasks
166
+
167
+ - [x] **T01: Create migrations** — Set up schema
168
+ - [x] **T02: Seed data** — Initial seed
169
+ `);
170
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01');
171
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md', '# T01 done');
172
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md', '# T02');
173
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md', '# T02 done');
174
+
175
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S02/S02-PLAN.md', `# S02: API endpoints
176
+
177
+ ## Tasks
178
+
179
+ - [ ] **T01: Auth routes** — Implement auth
180
+ - [ ] **T02: User routes** — CRUD users
181
+ `);
182
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md', '# T01');
183
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md', '# T02');
184
+ });
185
+
186
+ after(() => rmSync(projectDir, { recursive: true, force: true }));
187
+
188
+ it('returns milestone structure', () => {
189
+ const result = readRoadmap(projectDir);
190
+ assert.equal(result.milestones.length, 1);
191
+ assert.equal(result.milestones[0].id, 'M001');
192
+ assert.equal(result.milestones[0].title, 'Core Setup');
193
+ });
194
+
195
+ it('reads vision from roadmap', () => {
196
+ const result = readRoadmap(projectDir);
197
+ assert.ok(result.milestones[0].vision.includes('foundation'));
198
+ });
199
+
200
+ it('parses slices from roadmap table', () => {
201
+ const result = readRoadmap(projectDir);
202
+ const slices = result.milestones[0].slices;
203
+ assert.equal(slices.length, 2);
204
+ assert.equal(slices[0].id, 'S01');
205
+ assert.equal(slices[0].title, 'Database schema');
206
+ assert.equal(slices[1].id, 'S02');
207
+ });
208
+
209
+ it('derives slice status from task summaries', () => {
210
+ const result = readRoadmap(projectDir);
211
+ const slices = result.milestones[0].slices;
212
+ assert.equal(slices[0].status, 'done');
213
+ assert.equal(slices[1].status, 'pending');
214
+ });
215
+
216
+ it('includes tasks in slices', () => {
217
+ const result = readRoadmap(projectDir);
218
+ const s01Tasks = result.milestones[0].slices[0].tasks;
219
+ assert.equal(s01Tasks.length, 2);
220
+ assert.equal(s01Tasks[0].status, 'done');
221
+ });
222
+
223
+ it('filters by milestoneId', () => {
224
+ const result = readRoadmap(projectDir, 'M999');
225
+ assert.equal(result.milestones.length, 0);
226
+ });
227
+ });
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // readHistory tests
231
+ // ---------------------------------------------------------------------------
232
+
233
+ describe('readHistory', () => {
234
+ let projectDir: string;
235
+
236
+ before(() => {
237
+ projectDir = tmpProject();
238
+ writeFixture(projectDir, '.gsd/metrics.json', JSON.stringify({
239
+ version: 1,
240
+ projectStartedAt: 1700000000000,
241
+ units: [
242
+ {
243
+ type: 'execute-task',
244
+ id: 'M001/S01/T01',
245
+ model: 'claude-sonnet-4',
246
+ startedAt: 1700001000000,
247
+ finishedAt: 1700002000000,
248
+ tokens: { input: 10000, output: 3000, cacheRead: 2000, cacheWrite: 1000, total: 16000 },
249
+ cost: 0.05,
250
+ toolCalls: 8,
251
+ apiRequests: 3,
252
+ },
253
+ {
254
+ type: 'execute-task',
255
+ id: 'M001/S01/T02',
256
+ model: 'claude-sonnet-4',
257
+ startedAt: 1700003000000,
258
+ finishedAt: 1700004000000,
259
+ tokens: { input: 15000, output: 5000, cacheRead: 3000, cacheWrite: 1500, total: 24500 },
260
+ cost: 0.08,
261
+ toolCalls: 12,
262
+ apiRequests: 5,
263
+ },
264
+ ],
265
+ }));
266
+ });
267
+
268
+ after(() => rmSync(projectDir, { recursive: true, force: true }));
269
+
270
+ it('returns all entries sorted by most recent', () => {
271
+ const result = readHistory(projectDir);
272
+ assert.equal(result.entries.length, 2);
273
+ assert.equal(result.entries[0].id, 'M001/S01/T02'); // most recent first
274
+ });
275
+
276
+ it('computes totals', () => {
277
+ const result = readHistory(projectDir);
278
+ assert.equal(result.totals.units, 2);
279
+ assert.equal(result.totals.cost, 0.13);
280
+ assert.equal(result.totals.tokens.total, 40500);
281
+ });
282
+
283
+ it('respects limit', () => {
284
+ const result = readHistory(projectDir, 1);
285
+ assert.equal(result.entries.length, 1);
286
+ assert.equal(result.totals.units, 2); // totals still reflect all
287
+ });
288
+
289
+ it('returns empty for missing metrics', () => {
290
+ const empty = tmpProject();
291
+ mkdirSync(join(empty, '.gsd'), { recursive: true });
292
+ const result = readHistory(empty);
293
+ assert.equal(result.entries.length, 0);
294
+ assert.equal(result.totals.units, 0);
295
+ rmSync(empty, { recursive: true, force: true });
296
+ });
297
+ });
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // readCaptures tests
301
+ // ---------------------------------------------------------------------------
302
+
303
+ describe('readCaptures', () => {
304
+ let projectDir: string;
305
+
306
+ before(() => {
307
+ projectDir = tmpProject();
308
+ writeFixture(projectDir, '.gsd/CAPTURES.md', `# Captures
309
+
310
+ ### CAP-aaa11111
311
+
312
+ **Text:** Add rate limiting to API
313
+ **Captured:** 2026-04-01T10:00:00Z
314
+ **Status:** pending
315
+
316
+ ### CAP-bbb22222
317
+
318
+ **Text:** Refactor auth module
319
+ **Captured:** 2026-04-02T10:00:00Z
320
+ **Status:** resolved
321
+ **Classification:** inject
322
+ **Resolution:** Added to M003 roadmap
323
+ **Rationale:** Important for security
324
+ **Resolved:** 2026-04-03T10:00:00Z
325
+ **Milestone:** M003
326
+
327
+ ### CAP-ccc33333
328
+
329
+ **Text:** Nice to have: dark mode
330
+ **Captured:** 2026-04-02T11:00:00Z
331
+ **Status:** resolved
332
+ **Classification:** defer
333
+ **Resolution:** Deferred to future
334
+ **Rationale:** Not blocking
335
+ **Resolved:** 2026-04-03T11:00:00Z
336
+ `);
337
+ });
338
+
339
+ after(() => rmSync(projectDir, { recursive: true, force: true }));
340
+
341
+ it('reads all captures', () => {
342
+ const result = readCaptures(projectDir, 'all');
343
+ assert.equal(result.captures.length, 3);
344
+ assert.equal(result.counts.total, 3);
345
+ });
346
+
347
+ it('filters pending captures', () => {
348
+ const result = readCaptures(projectDir, 'pending');
349
+ assert.equal(result.captures.length, 1);
350
+ assert.equal(result.captures[0].id, 'CAP-aaa11111');
351
+ });
352
+
353
+ it('filters actionable captures (inject, replan, quick-task)', () => {
354
+ const result = readCaptures(projectDir, 'actionable');
355
+ assert.equal(result.captures.length, 1);
356
+ assert.equal(result.captures[0].id, 'CAP-bbb22222');
357
+ });
358
+
359
+ it('counts correctly regardless of filter', () => {
360
+ const result = readCaptures(projectDir, 'pending');
361
+ assert.equal(result.counts.total, 3);
362
+ assert.equal(result.counts.pending, 1);
363
+ assert.equal(result.counts.actionable, 1);
364
+ });
365
+
366
+ it('returns empty for missing CAPTURES.md', () => {
367
+ const empty = tmpProject();
368
+ mkdirSync(join(empty, '.gsd'), { recursive: true });
369
+ const result = readCaptures(empty);
370
+ assert.equal(result.captures.length, 0);
371
+ rmSync(empty, { recursive: true, force: true });
372
+ });
373
+ });
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // readKnowledge tests
377
+ // ---------------------------------------------------------------------------
378
+
379
+ describe('readKnowledge', () => {
380
+ let projectDir: string;
381
+
382
+ before(() => {
383
+ projectDir = tmpProject();
384
+ writeFixture(projectDir, '.gsd/KNOWLEDGE.md', `# Project Knowledge
385
+
386
+ ## Rules
387
+
388
+ | # | Scope | Rule | Why | Added |
389
+ |---|-------|------|-----|-------|
390
+ | K001 | auth | Hash passwords with bcrypt | Security requirement | manual |
391
+ | K002 | db | Use transactions for multi-table | Data consistency | auto |
392
+
393
+ ## Patterns
394
+
395
+ | # | Pattern | Where | Notes |
396
+ |---|---------|-------|-------|
397
+ | P001 | Singleton services | services/ | Prevents duplication |
398
+
399
+ ## Lessons Learned
400
+
401
+ | # | What Happened | Root Cause | Fix | Scope |
402
+ |---|--------------|------------|-----|-------|
403
+ | L001 | CI tests failed | Env diff | Added setup script | testing |
404
+ `);
405
+ });
406
+
407
+ after(() => rmSync(projectDir, { recursive: true, force: true }));
408
+
409
+ it('reads all knowledge entries', () => {
410
+ const result = readKnowledge(projectDir);
411
+ assert.equal(result.entries.length, 4);
412
+ });
413
+
414
+ it('counts by type', () => {
415
+ const result = readKnowledge(projectDir);
416
+ assert.equal(result.counts.rules, 2);
417
+ assert.equal(result.counts.patterns, 1);
418
+ assert.equal(result.counts.lessons, 1);
419
+ });
420
+
421
+ it('parses rule fields correctly', () => {
422
+ const result = readKnowledge(projectDir);
423
+ const k001 = result.entries.find((e) => e.id === 'K001');
424
+ assert.ok(k001);
425
+ assert.equal(k001.type, 'rule');
426
+ assert.equal(k001.scope, 'auth');
427
+ assert.ok(k001.content.includes('bcrypt'));
428
+ });
429
+
430
+ it('returns empty for missing KNOWLEDGE.md', () => {
431
+ const empty = tmpProject();
432
+ mkdirSync(join(empty, '.gsd'), { recursive: true });
433
+ const result = readKnowledge(empty);
434
+ assert.equal(result.entries.length, 0);
435
+ rmSync(empty, { recursive: true, force: true });
436
+ });
437
+ });
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // runDoctorLite tests
441
+ // ---------------------------------------------------------------------------
442
+
443
+ describe('runDoctorLite', () => {
444
+ let projectDir: string;
445
+
446
+ before(() => {
447
+ projectDir = tmpProject();
448
+
449
+ // M001: complete milestone (has summary)
450
+ writeFixture(projectDir, '.gsd/PROJECT.md', '# Test Project');
451
+ writeFixture(projectDir, '.gsd/STATE.md', '# GSD State');
452
+ writeFixture(projectDir, '.gsd/milestones/M001/M001-CONTEXT.md', '# M001');
453
+ writeFixture(projectDir, '.gsd/milestones/M001/M001-ROADMAP.md', '# Roadmap');
454
+ writeFixture(projectDir, '.gsd/milestones/M001/M001-SUMMARY.md', '# Done');
455
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/S01-PLAN.md', '# Plan');
456
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01');
457
+ writeFixture(projectDir, '.gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md', '# T01 done');
458
+
459
+ // M002: incomplete — has all tasks done but no SUMMARY
460
+ writeFixture(projectDir, '.gsd/milestones/M002/M002-CONTEXT.md', '# M002');
461
+ writeFixture(projectDir, '.gsd/milestones/M002/M002-ROADMAP.md', '# Roadmap');
462
+ writeFixture(projectDir, '.gsd/milestones/M002/slices/S01/S01-PLAN.md', '# Plan');
463
+ writeFixture(projectDir, '.gsd/milestones/M002/slices/S01/tasks/T01-PLAN.md', '# T01');
464
+ writeFixture(projectDir, '.gsd/milestones/M002/slices/S01/tasks/T01-SUMMARY.md', '# T01 done');
465
+
466
+ // M003: empty — no context, no slices
467
+ mkdirSync(join(projectDir, '.gsd/milestones/M003'), { recursive: true });
468
+ });
469
+
470
+ after(() => rmSync(projectDir, { recursive: true, force: true }));
471
+
472
+ it('detects all-slices-done-missing-summary', () => {
473
+ const result = runDoctorLite(projectDir);
474
+ const issue = result.issues.find((i) => i.code === 'all_slices_done_missing_summary');
475
+ assert.ok(issue, 'Should detect M002 missing summary');
476
+ assert.equal(issue.unitId, 'M002');
477
+ });
478
+
479
+ it('detects missing context', () => {
480
+ const result = runDoctorLite(projectDir);
481
+ const issue = result.issues.find(
482
+ (i) => i.code === 'missing_context' && i.unitId === 'M003',
483
+ );
484
+ assert.ok(issue, 'Should detect M003 missing context');
485
+ });
486
+
487
+ it('scopes to a single milestone', () => {
488
+ const result = runDoctorLite(projectDir, 'M001');
489
+ const m002Issues = result.issues.filter((i) => i.unitId.startsWith('M002'));
490
+ assert.equal(m002Issues.length, 0, 'Should not include M002 when scoped to M001');
491
+ });
492
+
493
+ it('returns ok:true for healthy project', () => {
494
+ const healthy = tmpProject();
495
+ writeFixture(healthy, '.gsd/PROJECT.md', '# Project');
496
+ writeFixture(healthy, '.gsd/STATE.md', '# State');
497
+ const result = runDoctorLite(healthy);
498
+ assert.equal(result.ok, true);
499
+ rmSync(healthy, { recursive: true, force: true });
500
+ });
501
+
502
+ it('handles missing .gsd/ gracefully', () => {
503
+ const empty = tmpProject();
504
+ const result = runDoctorLite(empty);
505
+ assert.equal(result.ok, true);
506
+ assert.equal(result.issues[0].code, 'no_gsd_directory');
507
+ rmSync(empty, { recursive: true, force: true });
508
+ });
509
+ });