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,263 @@
1
+ // GSD MCP Server — roadmap structure reader
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import { readFileSync, existsSync } from 'node:fs';
5
+ import {
6
+ resolveGsdRoot,
7
+ findMilestoneIds,
8
+ resolveMilestoneFile,
9
+ findSliceIds,
10
+ resolveSliceFile,
11
+ findTaskFiles,
12
+ } from './paths.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface TaskInfo {
19
+ id: string;
20
+ title: string;
21
+ status: 'done' | 'pending';
22
+ }
23
+
24
+ export interface SliceInfo {
25
+ id: string;
26
+ title: string;
27
+ status: 'done' | 'active' | 'pending';
28
+ risk: string;
29
+ depends: string[];
30
+ demo: string;
31
+ tasks: TaskInfo[];
32
+ }
33
+
34
+ export interface MilestoneInfo {
35
+ id: string;
36
+ title: string;
37
+ status: 'done' | 'active' | 'pending' | 'parked';
38
+ vision: string;
39
+ slices: SliceInfo[];
40
+ }
41
+
42
+ export interface RoadmapResult {
43
+ milestones: MilestoneInfo[];
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // ROADMAP.md table parser
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function parseRoadmapTable(content: string): Array<{
51
+ id: string; title: string; risk: string; depends: string[]; done: boolean; demo: string;
52
+ }> {
53
+ const results: Array<{
54
+ id: string; title: string; risk: string; depends: string[]; done: boolean; demo: string;
55
+ }> = [];
56
+
57
+ // Try table format first: | S01 | Title | risk | depends | done-icon | demo |
58
+ const tableSection = content.match(/## (?:Slice[s]?|Slice Overview|Slice Table)\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
59
+ if (tableSection) {
60
+ const lines = tableSection[1].split('\n');
61
+ for (const line of lines) {
62
+ if (!line.includes('|')) continue;
63
+ const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
64
+ if (cells.length < 4) continue;
65
+ if (cells[0] === 'ID' || cells[0].startsWith('--')) continue;
66
+
67
+ const id = cells[0].match(/S\d+/)?.[0];
68
+ if (!id) continue;
69
+
70
+ const done = cells.some((c) => c === '\u2611' || c === '\u2705' || c.toLowerCase() === 'done');
71
+ const depends = (cells[3] ?? '').replace(/\u2014/g, '').split(',').map((d) => d.trim()).filter(Boolean);
72
+
73
+ results.push({
74
+ id,
75
+ title: cells[1] ?? '',
76
+ risk: cells[2] ?? 'medium',
77
+ depends,
78
+ done,
79
+ demo: cells[5] ?? '',
80
+ });
81
+ }
82
+ if (results.length > 0) return results;
83
+ }
84
+
85
+ // Try checkbox format: - [x] **S01: Title** `risk:high` `depends:[S01]`
86
+ const checkboxRe = /^-\s+\[([ xX])\]\s+\*\*(S\d+):\s*(.+?)\*\*(?:.*?`risk:(\w+)`)?(?:.*?`depends:\[([^\]]*)\]`)?/gm;
87
+ let match: RegExpExecArray | null;
88
+ while ((match = checkboxRe.exec(content)) !== null) {
89
+ const [, checked, id, title, risk, deps] = match;
90
+ results.push({
91
+ id,
92
+ title: title.trim(),
93
+ risk: risk ?? 'medium',
94
+ depends: deps ? deps.split(',').map((d) => d.trim()).filter(Boolean) : [],
95
+ done: checked !== ' ',
96
+ demo: '',
97
+ });
98
+ }
99
+ if (results.length > 0) return results;
100
+
101
+ // Try prose headers: ## S01: Title
102
+ const headerRe = /^##\s+(S\d+):\s*(.+)/gm;
103
+ while ((match = headerRe.exec(content)) !== null) {
104
+ results.push({
105
+ id: match[1],
106
+ title: match[2].trim(),
107
+ risk: 'medium',
108
+ depends: [],
109
+ done: false,
110
+ demo: '',
111
+ });
112
+ }
113
+
114
+ return results;
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // PLAN.md task parser
119
+ // ---------------------------------------------------------------------------
120
+
121
+ function parseSlicePlanTasks(content: string): Array<{ id: string; title: string; done: boolean }> {
122
+ const results: Array<{ id: string; title: string; done: boolean }> = [];
123
+
124
+ // Checkbox format: - [x] **T01: Title** — description
125
+ const taskRe = /^-\s+\[([ xX])\]\s+\*\*(T\d+):\s*(.+?)\*\*/gm;
126
+ let match: RegExpExecArray | null;
127
+ while ((match = taskRe.exec(content)) !== null) {
128
+ results.push({
129
+ id: match[2],
130
+ title: match[3].trim(),
131
+ done: match[1] !== ' ',
132
+ });
133
+ }
134
+ if (results.length > 0) return results;
135
+
136
+ // H3 format: ### T01: Title
137
+ const h3Re = /^###\s+(T\d+):\s*(.+)/gm;
138
+ while ((match = h3Re.exec(content)) !== null) {
139
+ results.push({
140
+ id: match[1],
141
+ title: match[2].trim(),
142
+ done: false,
143
+ });
144
+ }
145
+
146
+ return results;
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Milestone title from CONTEXT.md or ROADMAP.md H1
151
+ // ---------------------------------------------------------------------------
152
+
153
+ function readMilestoneTitle(gsdRoot: string, mid: string): string {
154
+ const ctxPath = resolveMilestoneFile(gsdRoot, mid, 'CONTEXT');
155
+ if (ctxPath && existsSync(ctxPath)) {
156
+ const content = readFileSync(ctxPath, 'utf-8');
157
+ const h1 = content.match(/^#\s+(?:M\d+:?\s*)?(.+)/m);
158
+ if (h1) return h1[1].trim();
159
+ }
160
+
161
+ const roadmapPath = resolveMilestoneFile(gsdRoot, mid, 'ROADMAP');
162
+ if (roadmapPath && existsSync(roadmapPath)) {
163
+ const content = readFileSync(roadmapPath, 'utf-8');
164
+ const h1 = content.match(/^#\s+(?:M\d+:?\s*)?(.+)/m);
165
+ if (h1) return h1[1].trim();
166
+ }
167
+
168
+ return mid;
169
+ }
170
+
171
+ function readVision(gsdRoot: string, mid: string): string {
172
+ const roadmapPath = resolveMilestoneFile(gsdRoot, mid, 'ROADMAP');
173
+ if (!roadmapPath || !existsSync(roadmapPath)) return '';
174
+
175
+ const content = readFileSync(roadmapPath, 'utf-8');
176
+ const section = content.match(/## Vision\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
177
+ return section ? section[1].trim() : '';
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Public API
182
+ // ---------------------------------------------------------------------------
183
+
184
+ export function readRoadmap(projectDir: string, filterMilestoneId?: string): RoadmapResult {
185
+ const gsd = resolveGsdRoot(projectDir);
186
+ let milestoneIds = findMilestoneIds(gsd);
187
+
188
+ if (filterMilestoneId) {
189
+ milestoneIds = milestoneIds.filter((id) => id === filterMilestoneId);
190
+ }
191
+
192
+ const milestones: MilestoneInfo[] = [];
193
+
194
+ for (const mid of milestoneIds) {
195
+ const title = readMilestoneTitle(gsd, mid);
196
+ const vision = readVision(gsd, mid);
197
+
198
+ const summaryPath = resolveMilestoneFile(gsd, mid, 'SUMMARY');
199
+ const hasSummary = summaryPath !== null && existsSync(summaryPath);
200
+
201
+ const roadmapPath = resolveMilestoneFile(gsd, mid, 'ROADMAP');
202
+ let roadmapSlices: ReturnType<typeof parseRoadmapTable> = [];
203
+ if (roadmapPath && existsSync(roadmapPath)) {
204
+ roadmapSlices = parseRoadmapTable(readFileSync(roadmapPath, 'utf-8'));
205
+ }
206
+
207
+ const fsSliceIds = findSliceIds(gsd, mid);
208
+ const sliceIdSet = new Set([
209
+ ...roadmapSlices.map((s) => s.id),
210
+ ...fsSliceIds,
211
+ ]);
212
+
213
+ const slices: SliceInfo[] = [];
214
+ for (const sid of Array.from(sliceIdSet).sort()) {
215
+ const roadmapEntry = roadmapSlices.find((s) => s.id === sid);
216
+ const taskFiles = findTaskFiles(gsd, mid, sid);
217
+
218
+ const planPath = resolveSliceFile(gsd, mid, sid, 'PLAN');
219
+ let planTasks: ReturnType<typeof parseSlicePlanTasks> = [];
220
+ if (planPath && existsSync(planPath)) {
221
+ planTasks = parseSlicePlanTasks(readFileSync(planPath, 'utf-8'));
222
+ }
223
+
224
+ const tasks: TaskInfo[] = [];
225
+ const seenIds = new Set<string>();
226
+
227
+ for (const pt of planTasks) {
228
+ const fsTask = taskFiles.find((t) => t.id === pt.id);
229
+ const done = fsTask?.hasSummary ?? pt.done;
230
+ tasks.push({ id: pt.id, title: pt.title, status: done ? 'done' : 'pending' });
231
+ seenIds.add(pt.id);
232
+ }
233
+ for (const ft of taskFiles) {
234
+ if (seenIds.has(ft.id)) continue;
235
+ tasks.push({ id: ft.id, title: ft.id, status: ft.hasSummary ? 'done' : 'pending' });
236
+ }
237
+
238
+ const allDone = tasks.length > 0 && tasks.every((t) => t.status === 'done');
239
+ const anyDone = tasks.some((t) => t.status === 'done');
240
+ const sliceStatus: SliceInfo['status'] = allDone ? 'done' : anyDone ? 'active' : 'pending';
241
+
242
+ slices.push({
243
+ id: sid,
244
+ title: roadmapEntry?.title ?? sid,
245
+ status: sliceStatus,
246
+ risk: roadmapEntry?.risk ?? 'medium',
247
+ depends: roadmapEntry?.depends ?? [],
248
+ demo: roadmapEntry?.demo ?? '',
249
+ tasks,
250
+ });
251
+ }
252
+
253
+ const allSlicesDone = slices.length > 0 && slices.every((s) => s.status === 'done');
254
+ const anySliceActive = slices.some((s) => s.status === 'active' || s.status === 'done');
255
+ const milestoneStatus: MilestoneInfo['status'] = hasSummary
256
+ ? 'done'
257
+ : allSlicesDone ? 'done' : anySliceActive ? 'active' : 'pending';
258
+
259
+ milestones.push({ id: mid, title, status: milestoneStatus, vision, slices });
260
+ }
261
+
262
+ return { milestones };
263
+ }
@@ -0,0 +1,223 @@
1
+ // GSD MCP Server — project state reader
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import { readFileSync, existsSync } from 'node:fs';
5
+ import {
6
+ resolveGsdRoot,
7
+ resolveRootFile,
8
+ findMilestoneIds,
9
+ resolveMilestoneDir,
10
+ resolveMilestoneFile,
11
+ findSliceIds,
12
+ findTaskFiles,
13
+ } from './paths.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface ProgressResult {
20
+ activeMilestone: { id: string; title: string } | null;
21
+ activeSlice: { id: string; title: string } | null;
22
+ activeTask: { id: string; title: string } | null;
23
+ phase: string;
24
+ milestones: { total: number; done: number; active: number; pending: number; parked: number };
25
+ slices: { total: number; done: number; active: number; pending: number };
26
+ tasks: { total: number; done: number; pending: number };
27
+ requirements: { active: number; validated: number; deferred: number; outOfScope: number } | null;
28
+ blockers: string[];
29
+ nextAction: string;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // STATE.md parser
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function parseBoldField(content: string, label: string): string | null {
37
+ const re = new RegExp(`\\*\\*${label}:\\*\\*\\s*(.+)`, 'i');
38
+ const m = content.match(re);
39
+ return m ? m[1].trim() : null;
40
+ }
41
+
42
+ function parseActiveRef(value: string | null): { id: string; title: string } | null {
43
+ if (!value || value.toLowerCase() === 'none' || value === '—') return null;
44
+ // "M001: Flight Simulator" or "M001"
45
+ const m = value.match(/^(M\d+|S\d+|T\d+):?\s*(.*)/);
46
+ if (m) return { id: m[1], title: m[2] || m[1] };
47
+ return { id: value, title: value };
48
+ }
49
+
50
+ function parsePhase(value: string | null): string {
51
+ if (!value) return 'unknown';
52
+ const lower = value.toLowerCase().trim();
53
+ if (lower.includes('research') || lower.includes('discuss')) return 'research';
54
+ if (lower.includes('plan')) return 'plan';
55
+ if (lower.includes('execut')) return 'execute';
56
+ if (lower.includes('complete') || lower.includes('done')) return 'complete';
57
+ return lower;
58
+ }
59
+
60
+ function parseRequirementsLine(value: string | null): ProgressResult['requirements'] | null {
61
+ if (!value) return null;
62
+ const active = value.match(/(\d+)\s*active/i);
63
+ const validated = value.match(/(\d+)\s*validated/i);
64
+ const deferred = value.match(/(\d+)\s*deferred/i);
65
+ const outOfScope = value.match(/(\d+)\s*out.of.scope/i);
66
+ if (!active && !validated && !deferred && !outOfScope) return null;
67
+ return {
68
+ active: active ? parseInt(active[1], 10) : 0,
69
+ validated: validated ? parseInt(validated[1], 10) : 0,
70
+ deferred: deferred ? parseInt(deferred[1], 10) : 0,
71
+ outOfScope: outOfScope ? parseInt(outOfScope[1], 10) : 0,
72
+ };
73
+ }
74
+
75
+ function parseBlockers(content: string): string[] {
76
+ const section = content.match(/## Blockers\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
77
+ if (!section) return [];
78
+ return section[1]
79
+ .split('\n')
80
+ .map((l) => l.replace(/^[-*]\s*/, '').trim())
81
+ .filter(Boolean);
82
+ }
83
+
84
+ function parseNextAction(content: string): string {
85
+ const section = content.match(/## Next Action\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
86
+ if (!section) return '';
87
+ return section[1].trim().split('\n')[0] || '';
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Milestone registry from STATE.md
92
+ // ---------------------------------------------------------------------------
93
+
94
+ interface RegistryEntry { id: string; status: 'done' | 'active' | 'pending' | 'parked' }
95
+
96
+ function parseMilestoneRegistry(content: string): RegistryEntry[] {
97
+ const section = content.match(/## Milestone Registry\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
98
+ if (!section) return [];
99
+ const entries: RegistryEntry[] = [];
100
+ for (const line of section[1].split('\n')) {
101
+ const m = line.match(/[-*]\s*(☑|✅|🔄|⬜|⏸)\s*\*\*(M\d+):\*\*/);
102
+ if (!m) continue;
103
+ const [, icon, id] = m;
104
+ let status: RegistryEntry['status'] = 'pending';
105
+ if (icon === '☑' || icon === '✅') status = 'done';
106
+ else if (icon === '🔄') status = 'active';
107
+ else if (icon === '⏸') status = 'parked';
108
+ entries.push({ id, status });
109
+ }
110
+ return entries;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Count slices/tasks by walking filesystem
115
+ // ---------------------------------------------------------------------------
116
+
117
+ function countSlicesAndTasks(gsdRoot: string, milestoneIds: string[]): {
118
+ slices: ProgressResult['slices'];
119
+ tasks: ProgressResult['tasks'];
120
+ } {
121
+ let sliceTotal = 0, sliceDone = 0, sliceActive = 0;
122
+ let taskTotal = 0, taskDone = 0;
123
+
124
+ for (const mid of milestoneIds) {
125
+ const sliceIds = findSliceIds(gsdRoot, mid);
126
+ sliceTotal += sliceIds.length;
127
+
128
+ for (const sid of sliceIds) {
129
+ const tasks = findTaskFiles(gsdRoot, mid, sid);
130
+ taskTotal += tasks.length;
131
+
132
+ const allDone = tasks.length > 0 && tasks.every((t) => t.hasSummary);
133
+ const anyDone = tasks.some((t) => t.hasSummary);
134
+
135
+ if (allDone) {
136
+ sliceDone++;
137
+ taskDone += tasks.length;
138
+ } else {
139
+ if (anyDone) sliceActive++;
140
+ taskDone += tasks.filter((t) => t.hasSummary).length;
141
+ }
142
+ }
143
+ }
144
+
145
+ return {
146
+ slices: {
147
+ total: sliceTotal,
148
+ done: sliceDone,
149
+ active: sliceActive,
150
+ pending: sliceTotal - sliceDone - sliceActive,
151
+ },
152
+ tasks: { total: taskTotal, done: taskDone, pending: taskTotal - taskDone },
153
+ };
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Public API
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export function readProgress(projectDir: string): ProgressResult {
161
+ const gsd = resolveGsdRoot(projectDir);
162
+ const statePath = resolveRootFile(gsd, 'STATE.md');
163
+
164
+ // Defaults
165
+ const result: ProgressResult = {
166
+ activeMilestone: null,
167
+ activeSlice: null,
168
+ activeTask: null,
169
+ phase: 'unknown',
170
+ milestones: { total: 0, done: 0, active: 0, pending: 0, parked: 0 },
171
+ slices: { total: 0, done: 0, active: 0, pending: 0 },
172
+ tasks: { total: 0, done: 0, pending: 0 },
173
+ requirements: null,
174
+ blockers: [],
175
+ nextAction: '',
176
+ };
177
+
178
+ if (!existsSync(statePath)) {
179
+ // No STATE.md — derive from filesystem only
180
+ const milestoneIds = findMilestoneIds(gsd);
181
+ result.milestones.total = milestoneIds.length;
182
+ result.milestones.pending = milestoneIds.length;
183
+ const counts = countSlicesAndTasks(gsd, milestoneIds);
184
+ result.slices = counts.slices;
185
+ result.tasks = counts.tasks;
186
+ return result;
187
+ }
188
+
189
+ const content = readFileSync(statePath, 'utf-8');
190
+
191
+ // Parse STATE.md fields
192
+ result.activeMilestone = parseActiveRef(parseBoldField(content, 'Active Milestone'));
193
+ result.activeSlice = parseActiveRef(parseBoldField(content, 'Active Slice'));
194
+ result.activeTask = parseActiveRef(parseBoldField(content, 'Active Task'));
195
+ result.phase = parsePhase(parseBoldField(content, 'Phase'));
196
+ result.requirements = parseRequirementsLine(parseBoldField(content, 'Requirements Status'));
197
+ result.blockers = parseBlockers(content);
198
+ result.nextAction = parseNextAction(content);
199
+
200
+ // Milestone counts from registry
201
+ const registry = parseMilestoneRegistry(content);
202
+ if (registry.length > 0) {
203
+ result.milestones.total = registry.length;
204
+ result.milestones.done = registry.filter((e) => e.status === 'done').length;
205
+ result.milestones.active = registry.filter((e) => e.status === 'active').length;
206
+ result.milestones.parked = registry.filter((e) => e.status === 'parked').length;
207
+ result.milestones.pending = registry.length -
208
+ result.milestones.done - result.milestones.active - result.milestones.parked;
209
+ } else {
210
+ // Fallback: count directories
211
+ const milestoneIds = findMilestoneIds(gsd);
212
+ result.milestones.total = milestoneIds.length;
213
+ result.milestones.pending = milestoneIds.length;
214
+ }
215
+
216
+ // Slice/task counts from filesystem
217
+ const milestoneIds = findMilestoneIds(gsd);
218
+ const counts = countSlicesAndTasks(gsd, milestoneIds);
219
+ result.slices = counts.slices;
220
+ result.tasks = counts.tasks;
221
+
222
+ return result;
223
+ }
@@ -1,5 +1,8 @@
1
1
  /**
2
- * MCP Server — registers 6 GSD orchestration tools on McpServer.
2
+ * MCP Server — registers GSD orchestration + read-only project state tools.
3
+ *
4
+ * Session tools (6): gsd_execute, gsd_status, gsd_result, gsd_cancel, gsd_query, gsd_resolve_blocker
5
+ * Read-only tools (6): gsd_progress, gsd_roadmap, gsd_history, gsd_doctor, gsd_captures, gsd_knowledge
3
6
  *
4
7
  * Uses dynamic imports for @modelcontextprotocol/sdk because TS Node16
5
8
  * cannot resolve the SDK's subpath exports statically (same pattern as
@@ -10,6 +13,12 @@ import { readFile, readdir, stat } from 'node:fs/promises';
10
13
  import { join, resolve } from 'node:path';
11
14
  import { z } from 'zod';
12
15
  import type { SessionManager } from './session-manager.js';
16
+ import { readProgress } from './readers/state.js';
17
+ import { readRoadmap } from './readers/roadmap.js';
18
+ import { readHistory } from './readers/metrics.js';
19
+ import { readCaptures } from './readers/captures.js';
20
+ import { readKnowledge } from './readers/knowledge.js';
21
+ import { runDoctorLite } from './readers/doctor-lite.js';
13
22
 
14
23
  // ---------------------------------------------------------------------------
15
24
  // Constants
@@ -17,7 +26,7 @@ import type { SessionManager } from './session-manager.js';
17
26
 
18
27
  const MCP_PKG = '@modelcontextprotocol/sdk';
19
28
  const SERVER_NAME = 'gsd';
20
- const SERVER_VERSION = '2.51.0';
29
+ const SERVER_VERSION = '2.53.0';
21
30
 
22
31
  // ---------------------------------------------------------------------------
23
32
  // Tool result helpers
@@ -106,7 +115,7 @@ interface McpServerInstance {
106
115
  // ---------------------------------------------------------------------------
107
116
 
108
117
  /**
109
- * Create and configure an MCP server with 6 GSD orchestration tools.
118
+ * Create and configure an MCP server with 12 GSD tools (6 session + 6 read-only).
110
119
  *
111
120
  * Returns the McpServer instance — call `connect(transport)` to start serving.
112
121
  * Uses dynamic imports for the MCP SDK to avoid TS subpath resolution issues.
@@ -274,5 +283,127 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
274
283
  },
275
284
  );
276
285
 
286
+ // =======================================================================
287
+ // READ-ONLY TOOLS — no session required, pure filesystem reads
288
+ // =======================================================================
289
+
290
+ // -----------------------------------------------------------------------
291
+ // gsd_progress — structured project progress metrics
292
+ // -----------------------------------------------------------------------
293
+ server.tool(
294
+ 'gsd_progress',
295
+ 'Get structured project progress: active milestone/slice/task, phase, completion counts, blockers, and next action. No session required — reads directly from .gsd/ on disk.',
296
+ {
297
+ projectDir: z.string().describe('Absolute path to the project directory'),
298
+ },
299
+ async (args: Record<string, unknown>) => {
300
+ const { projectDir } = args as { projectDir: string };
301
+ try {
302
+ return jsonContent(readProgress(projectDir));
303
+ } catch (err) {
304
+ return errorContent(err instanceof Error ? err.message : String(err));
305
+ }
306
+ },
307
+ );
308
+
309
+ // -----------------------------------------------------------------------
310
+ // gsd_roadmap — milestone/slice/task structure with status
311
+ // -----------------------------------------------------------------------
312
+ server.tool(
313
+ 'gsd_roadmap',
314
+ 'Get the full project roadmap structure: milestones with their slices, tasks, status, risk, and dependencies. Optionally filter to a single milestone. No session required.',
315
+ {
316
+ projectDir: z.string().describe('Absolute path to the project directory'),
317
+ milestoneId: z.string().optional().describe('Filter to a specific milestone (e.g. "M001")'),
318
+ },
319
+ async (args: Record<string, unknown>) => {
320
+ const { projectDir, milestoneId } = args as { projectDir: string; milestoneId?: string };
321
+ try {
322
+ return jsonContent(readRoadmap(projectDir, milestoneId));
323
+ } catch (err) {
324
+ return errorContent(err instanceof Error ? err.message : String(err));
325
+ }
326
+ },
327
+ );
328
+
329
+ // -----------------------------------------------------------------------
330
+ // gsd_history — execution history with cost/token metrics
331
+ // -----------------------------------------------------------------------
332
+ server.tool(
333
+ 'gsd_history',
334
+ 'Get execution history with cost, token usage, model, and duration per unit. Returns totals across all units. No session required.',
335
+ {
336
+ projectDir: z.string().describe('Absolute path to the project directory'),
337
+ limit: z.number().optional().describe('Max entries to return (most recent first). Default: all.'),
338
+ },
339
+ async (args: Record<string, unknown>) => {
340
+ const { projectDir, limit } = args as { projectDir: string; limit?: number };
341
+ try {
342
+ return jsonContent(readHistory(projectDir, limit));
343
+ } catch (err) {
344
+ return errorContent(err instanceof Error ? err.message : String(err));
345
+ }
346
+ },
347
+ );
348
+
349
+ // -----------------------------------------------------------------------
350
+ // gsd_doctor — lightweight structural health check
351
+ // -----------------------------------------------------------------------
352
+ server.tool(
353
+ 'gsd_doctor',
354
+ 'Run a lightweight structural health check on the .gsd/ directory. Checks for missing files, status inconsistencies, and orphaned state. No session required.',
355
+ {
356
+ projectDir: z.string().describe('Absolute path to the project directory'),
357
+ scope: z.string().optional().describe('Limit checks to a specific milestone (e.g. "M001")'),
358
+ },
359
+ async (args: Record<string, unknown>) => {
360
+ const { projectDir, scope } = args as { projectDir: string; scope?: string };
361
+ try {
362
+ return jsonContent(runDoctorLite(projectDir, scope));
363
+ } catch (err) {
364
+ return errorContent(err instanceof Error ? err.message : String(err));
365
+ }
366
+ },
367
+ );
368
+
369
+ // -----------------------------------------------------------------------
370
+ // gsd_captures — pending captures and ideas
371
+ // -----------------------------------------------------------------------
372
+ server.tool(
373
+ 'gsd_captures',
374
+ 'Get captured ideas and thoughts from CAPTURES.md with triage status. Filter by pending, actionable, or all. No session required.',
375
+ {
376
+ projectDir: z.string().describe('Absolute path to the project directory'),
377
+ filter: z.enum(['all', 'pending', 'actionable']).optional().describe('Filter captures (default: "all")'),
378
+ },
379
+ async (args: Record<string, unknown>) => {
380
+ const { projectDir, filter } = args as { projectDir: string; filter?: 'all' | 'pending' | 'actionable' };
381
+ try {
382
+ return jsonContent(readCaptures(projectDir, filter ?? 'all'));
383
+ } catch (err) {
384
+ return errorContent(err instanceof Error ? err.message : String(err));
385
+ }
386
+ },
387
+ );
388
+
389
+ // -----------------------------------------------------------------------
390
+ // gsd_knowledge — project knowledge base
391
+ // -----------------------------------------------------------------------
392
+ server.tool(
393
+ 'gsd_knowledge',
394
+ 'Get the project knowledge base: rules, patterns, and lessons learned accumulated during development. No session required.',
395
+ {
396
+ projectDir: z.string().describe('Absolute path to the project directory'),
397
+ },
398
+ async (args: Record<string, unknown>) => {
399
+ const { projectDir } = args as { projectDir: string };
400
+ try {
401
+ return jsonContent(readKnowledge(projectDir));
402
+ } catch (err) {
403
+ return errorContent(err instanceof Error ? err.message : String(err));
404
+ }
405
+ },
406
+ );
407
+
277
408
  return { server };
278
409
  }
@@ -22,15 +22,35 @@
22
22
  */
23
23
  export declare function hasYamlBulletLists(json: string): boolean;
24
24
  /**
25
- * Attempt to repair YAML-style bullet lists embedded in a JSON string.
25
+ * Detect whether a JSON string contains XML parameter tags
26
+ * (i.e. `<parameter name="X">value</parameter>`).
26
27
  *
27
- * Converts patterns like:
28
- * "keyDecisions": - Used Web Notification API..., "keyFiles": - file1
28
+ * Some models mix XML tool-call syntax into JSON string values,
29
+ * producing hybrid output that fails JSON.parse.
29
30
  *
30
- * Into:
31
- * "keyDecisions": ["Used Web Notification API..."], "keyFiles": ["file1"]
31
+ * @see https://github.com/gsd-build/gsd-2/issues/3403
32
+ */
33
+ export declare function hasXmlParameterTags(json: string): boolean;
34
+ /**
35
+ * Detect whether a JSON string contains truncated numeric values
36
+ * (e.g. `"exitCode": -,` or `"durationMs": ,`).
37
+ *
38
+ * Smaller models sometimes emit incomplete numbers when the value
39
+ * is cut off mid-generation.
40
+ *
41
+ * @see https://github.com/gsd-build/gsd-2/issues/3464
42
+ */
43
+ export declare function hasTruncatedNumbers(json: string): boolean;
44
+ /**
45
+ * Attempt to repair malformed JSON in LLM tool-call arguments.
46
+ *
47
+ * Handles three categories of malformation:
48
+ *
49
+ * 1. **YAML bullet lists** (#2660): `"key": - item1\n - item2` → `"key": ["item1", "item2"]`
50
+ * 2. **XML parameter tags** (#3403): `<parameter name="X">value</parameter>` → stripped to content
51
+ * 3. **Truncated numbers** (#3464): `"exitCode": -,` → `"exitCode": 0,`
32
52
  *
33
- * Returns the original string unchanged if no YAML patterns are detected
53
+ * Returns the original string unchanged if no patterns are detected
34
54
  * or if the repair itself would produce invalid JSON.
35
55
  */
36
56
  export declare function repairToolJson(json: string): string;