gsd-pi 2.62.0-dev.f6ad485 → 2.62.1-dev.1ae2b74
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.
- package/dist/resources/extensions/ask-user-questions.js +47 -3
- package/dist/resources/extensions/gsd/auto/loop.js +8 -1
- package/dist/resources/extensions/gsd/auto/phases.js +10 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +6 -4
- package/dist/resources/extensions/gsd/auto-start.js +11 -6
- package/dist/resources/extensions/gsd/auto-timers.js +8 -2
- package/dist/resources/extensions/gsd/auto-verification.js +14 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
- package/dist/resources/extensions/gsd/auto.js +24 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
- package/dist/resources/extensions/gsd/db-writer.js +64 -28
- package/dist/resources/extensions/gsd/preferences-models.js +74 -0
- package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
- package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
- package/dist/resources/extensions/gsd/skill-health.js +7 -3
- package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
- package/dist/resources/extensions/gsd/state.js +1 -0
- package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -3
- package/dist/resources/extensions/gsd/workflow-logger.js +13 -8
- package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/cli.ts +1 -1
- package/packages/mcp-server/src/index.ts +15 -1
- package/packages/mcp-server/src/readers/captures.ts +119 -0
- package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
- package/packages/mcp-server/src/readers/index.ts +16 -0
- package/packages/mcp-server/src/readers/knowledge.ts +111 -0
- package/packages/mcp-server/src/readers/metrics.ts +118 -0
- package/packages/mcp-server/src/readers/paths.ts +217 -0
- package/packages/mcp-server/src/readers/readers.test.ts +509 -0
- package/packages/mcp-server/src/readers/roadmap.ts +263 -0
- package/packages/mcp-server/src/readers/state.ts +223 -0
- package/packages/mcp-server/src/server.ts +134 -3
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
- package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +60 -4
- package/src/resources/extensions/gsd/auto/loop.ts +8 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-start.ts +11 -6
- package/src/resources/extensions/gsd/auto-timers.ts +8 -2
- package/src/resources/extensions/gsd/auto-verification.ts +14 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
- package/src/resources/extensions/gsd/auto.ts +25 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
- package/src/resources/extensions/gsd/db-writer.ts +67 -30
- package/src/resources/extensions/gsd/preferences-models.ts +78 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
- package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
- package/src/resources/extensions/gsd/skill-health.ts +7 -3
- package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
- package/src/resources/extensions/gsd/state.ts +1 -0
- package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +17 -41
- package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -5
- package/src/resources/extensions/gsd/workflow-logger.ts +13 -8
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -1
- package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
- /package/dist/web/standalone/.next/static/{fbkSIi4k8fmB8mi0Sq9sF → erQZ_8_1lkclnPJLJnCxG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{fbkSIi4k8fmB8mi0Sq9sF → erQZ_8_1lkclnPJLJnCxG}/_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
|
+
});
|