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,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
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
25
|
+
* Detect whether a JSON string contains XML parameter tags
|
|
26
|
+
* (i.e. `<parameter name="X">value</parameter>`).
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* Some models mix XML tool-call syntax into JSON string values,
|
|
29
|
+
* producing hybrid output that fails JSON.parse.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
|
|
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
|
|
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;
|