@hegemonart/get-design-done 1.27.6 → 1.27.7
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +6 -3
- package/CHANGELOG.md +49 -0
- package/package.json +5 -4
- package/reference/registry.json +7 -0
- package/reference/schemas/mcp-gdd-tools.schema.json +381 -0
- package/scripts/install.cjs +42 -0
- package/scripts/lib/gsd-health-mirror/index.cjs +105 -0
- package/scripts/lib/gsd-health-mirror/index.d.cts +14 -0
- package/scripts/lib/install/mcp-register.cjs +235 -0
- package/scripts/lib/install/mcp-register.d.cts +64 -0
- package/scripts/lib/intel-store/index.cjs +55 -0
- package/scripts/lib/intel-store/index.d.cts +11 -0
- package/scripts/lib/mcp-tools-lint/index.cjs +216 -0
- package/scripts/lib/mcp-tools-lint/index.d.cts +74 -0
- package/scripts/lib/reflections-reader/index.cjs +107 -0
- package/scripts/lib/reflections-reader/index.d.cts +18 -0
- package/scripts/lib/roadmap-reader/index.cjs +81 -0
- package/scripts/lib/roadmap-reader/index.d.cts +13 -0
- package/scripts/lib/snapshot-reader/index.cjs +70 -0
- package/scripts/lib/snapshot-reader/index.d.cts +28 -0
- package/scripts/mcp-servers/gdd-mcp/README.md +66 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_cycle_recap.schema.json +30 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_decisions_list.schema.json +32 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_events_tail.schema.json +22 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_health.schema.json +30 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_intel_get.schema.json +24 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_learnings_digest.schema.json +22 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_phase_current.schema.json +22 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_phases_list.schema.json +31 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_plans_list.schema.json +33 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_reflections_latest.schema.json +21 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_status.schema.json +23 -0
- package/scripts/mcp-servers/gdd-mcp/schemas/gdd_telemetry_query.schema.json +23 -0
- package/scripts/mcp-servers/gdd-mcp/server.ts +317 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_cycle_recap.ts +37 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_decisions_list.ts +33 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_events_tail.ts +26 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts +19 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_intel_get.ts +32 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_learnings_digest.ts +23 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_phase_current.ts +29 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_phases_list.ts +26 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_plans_list.ts +39 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_reflections_latest.ts +25 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_status.ts +31 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_telemetry_query.ts +27 -0
- package/scripts/mcp-servers/gdd-mcp/tools/index.ts +75 -0
- package/scripts/mcp-servers/gdd-mcp/tools/shared.ts +134 -0
- package/skills/health/SKILL.md +36 -0
- package/skills/next/SKILL.md +28 -3
- package/skills/progress/SKILL.md +21 -6
- package/skills/resume/SKILL.md +26 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/lib/gsd-health-mirror/index.cjs — Plan 27.7-02
|
|
3
|
+
//
|
|
4
|
+
// Pure read-only mirror of skills/health/SKILL.md's check surface.
|
|
5
|
+
// NO subprocess spawn — just inspects 4 well-known files/dirs and
|
|
6
|
+
// reports status. Used by the gdd_health MCP tool.
|
|
7
|
+
//
|
|
8
|
+
// Surface:
|
|
9
|
+
// async getHealthChecks(rootDir) → { checks: HealthCheck[] }
|
|
10
|
+
//
|
|
11
|
+
// The 4 checks (in stable order) are:
|
|
12
|
+
// 1. claude_md — CLAUDE.md presence
|
|
13
|
+
// 2. planning_dir — .planning/ presence
|
|
14
|
+
// 3. design_dir — .design/ presence
|
|
15
|
+
// 4. package_json — package.json present AND parseable
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
function fileExists(p) {
|
|
21
|
+
try {
|
|
22
|
+
return fs.statSync(p).isFile();
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function dirExists(p) {
|
|
29
|
+
try {
|
|
30
|
+
return fs.statSync(p).isDirectory();
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getHealthChecks(rootDir) {
|
|
37
|
+
const checks = [];
|
|
38
|
+
|
|
39
|
+
// 1. CLAUDE.md
|
|
40
|
+
{
|
|
41
|
+
const p = path.join(rootDir, 'CLAUDE.md');
|
|
42
|
+
const present = fileExists(p);
|
|
43
|
+
checks.push({
|
|
44
|
+
name: 'claude_md',
|
|
45
|
+
status: present ? 'ok' : 'warn',
|
|
46
|
+
detail: present ? p : 'CLAUDE.md not found at project root',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. .planning/
|
|
51
|
+
{
|
|
52
|
+
const p = path.join(rootDir, '.planning');
|
|
53
|
+
const present = dirExists(p);
|
|
54
|
+
checks.push({
|
|
55
|
+
name: 'planning_dir',
|
|
56
|
+
status: present ? 'ok' : 'warn',
|
|
57
|
+
detail: present ? p : '.planning/ not found at project root',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. .design/
|
|
62
|
+
{
|
|
63
|
+
const p = path.join(rootDir, '.design');
|
|
64
|
+
const present = dirExists(p);
|
|
65
|
+
checks.push({
|
|
66
|
+
name: 'design_dir',
|
|
67
|
+
status: present ? 'ok' : 'warn',
|
|
68
|
+
detail: present ? p : '.design/ not found at project root',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. package.json — present + parseable
|
|
73
|
+
{
|
|
74
|
+
const p = path.join(rootDir, 'package.json');
|
|
75
|
+
if (!fileExists(p)) {
|
|
76
|
+
checks.push({
|
|
77
|
+
name: 'package_json',
|
|
78
|
+
status: 'warn',
|
|
79
|
+
detail: 'package.json not found at project root',
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
try {
|
|
83
|
+
const body = await fs.promises.readFile(p, 'utf8');
|
|
84
|
+
const parsed = JSON.parse(body);
|
|
85
|
+
const name = typeof parsed.name === 'string' ? parsed.name : '(unknown)';
|
|
86
|
+
const version = typeof parsed.version === 'string' ? parsed.version : '0.0.0';
|
|
87
|
+
checks.push({
|
|
88
|
+
name: 'package_json',
|
|
89
|
+
status: 'ok',
|
|
90
|
+
detail: name + '@' + version,
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
checks.push({
|
|
94
|
+
name: 'package_json',
|
|
95
|
+
status: 'fail',
|
|
96
|
+
detail: 'parse error: ' + (err && err.message ? err.message : String(err)),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { checks };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { getHealthChecks };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// scripts/lib/gsd-health-mirror/index.d.cts — TypeScript ambient declarations
|
|
2
|
+
// for the gsd-health-mirror CJS module. Plan 27.7-02.
|
|
3
|
+
|
|
4
|
+
export interface HealthCheck {
|
|
5
|
+
name: string;
|
|
6
|
+
status: 'ok' | 'warn' | 'fail';
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HealthChecksResult {
|
|
11
|
+
checks: HealthCheck[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getHealthChecks(rootDir: string): Promise<HealthChecksResult>;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/lib/install/mcp-register.cjs
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Plan 27.7-04 — registers `gdd-mcp` with the two harnesses that matter
|
|
5
|
+
// (Claude Code, Codex) and detects existing registration. Idempotent;
|
|
6
|
+
// graceful absent-CLI fallback (D-07).
|
|
7
|
+
//
|
|
8
|
+
// Pure library — no side effects on require. Invoked by:
|
|
9
|
+
// - scripts/install.cjs --register-mcp (opt-in; default off per D-07)
|
|
10
|
+
// - skills/health/SKILL.md check-mcp-registration step (read-only detect)
|
|
11
|
+
//
|
|
12
|
+
// spawnFn injection allows tests to mock child_process.spawnSync without
|
|
13
|
+
// touching real CLIs in CI.
|
|
14
|
+
//
|
|
15
|
+
// Threat model: scripts/install.cjs --register-mcp writes to harness user-
|
|
16
|
+
// level config. Command args are hardcoded in HARNESSES (no command-
|
|
17
|
+
// injection surface); `--` separator before MCP_NAME prevents flag
|
|
18
|
+
// injection (T-27.7-04-06).
|
|
19
|
+
|
|
20
|
+
const { spawnSync } = require('node:child_process');
|
|
21
|
+
|
|
22
|
+
const MCP_NAME = 'gdd-mcp';
|
|
23
|
+
|
|
24
|
+
const HARNESSES = Object.freeze({
|
|
25
|
+
claude: Object.freeze({
|
|
26
|
+
binary: 'claude',
|
|
27
|
+
addArgs: Object.freeze(['mcp', 'add', MCP_NAME, '-s', 'user', '--', MCP_NAME]),
|
|
28
|
+
listArgs: Object.freeze(['mcp', 'list']),
|
|
29
|
+
listMatchPattern: /\bgdd-mcp\b/,
|
|
30
|
+
}),
|
|
31
|
+
codex: Object.freeze({
|
|
32
|
+
binary: 'codex',
|
|
33
|
+
addArgs: Object.freeze(['mcp', 'add', MCP_NAME, '--', MCP_NAME]),
|
|
34
|
+
listArgs: Object.freeze(['mcp', 'list']),
|
|
35
|
+
listMatchPattern: /\bgdd-mcp\b/,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the command tuple for a given harness + mode.
|
|
41
|
+
* Currently only 'register' (add) is supported in command-build; 'detect'
|
|
42
|
+
* uses listArgs internally, 'unregister' is reserved for future work.
|
|
43
|
+
*/
|
|
44
|
+
function buildHarnessCommand(harness, mode = 'register') {
|
|
45
|
+
const h = HARNESSES[harness];
|
|
46
|
+
if (!h) throw new Error('Unknown harness: ' + harness);
|
|
47
|
+
if (mode === 'register') {
|
|
48
|
+
return { binary: h.binary, args: Array.from(h.addArgs) };
|
|
49
|
+
}
|
|
50
|
+
if (mode === 'detect') {
|
|
51
|
+
return { binary: h.binary, args: Array.from(h.listArgs) };
|
|
52
|
+
}
|
|
53
|
+
throw new Error('Unsupported mode: ' + mode);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect whether the harness CLI is on PATH. Runs `<binary> --version` and
|
|
58
|
+
* returns true iff exit code is 0. Catches ENOENT (binary missing).
|
|
59
|
+
*/
|
|
60
|
+
function detectHarnessPresent(harness, spawnFn = spawnSync) {
|
|
61
|
+
const h = HARNESSES[harness];
|
|
62
|
+
if (!h) throw new Error('Unknown harness: ' + harness);
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = spawnFn(h.binary, ['--version'], {
|
|
66
|
+
stdio: 'pipe',
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
});
|
|
69
|
+
} catch (_e) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (!result) return false;
|
|
73
|
+
if (result.error && result.error.code === 'ENOENT') return false;
|
|
74
|
+
return result.status === 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect whether gdd-mcp is already registered with the given harness.
|
|
79
|
+
* Runs `<binary> mcp list` and matches against listMatchPattern.
|
|
80
|
+
*/
|
|
81
|
+
function isAlreadyRegistered(harness, spawnFn = spawnSync) {
|
|
82
|
+
const h = HARNESSES[harness];
|
|
83
|
+
if (!h) throw new Error('Unknown harness: ' + harness);
|
|
84
|
+
let result;
|
|
85
|
+
try {
|
|
86
|
+
result = spawnFn(h.binary, Array.from(h.listArgs), {
|
|
87
|
+
stdio: 'pipe',
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
});
|
|
90
|
+
} catch (_e) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (!result || result.status !== 0) return false;
|
|
94
|
+
const stdout = (result.stdout || '').toString();
|
|
95
|
+
return h.listMatchPattern.test(stdout);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Register gdd-mcp with the given harness.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} opts
|
|
102
|
+
* @param {'claude'|'codex'} opts.harness
|
|
103
|
+
* @param {'register'|'unregister'|'detect'} [opts.mode='register']
|
|
104
|
+
* @param {boolean} [opts.dryRun=false]
|
|
105
|
+
* @param {Function} [opts.spawnFn] child_process.spawnSync substitute
|
|
106
|
+
* @returns {object} {harness, action, detected, command, applied,
|
|
107
|
+
* idempotent_skip, notice?, stdout?, stderr?,
|
|
108
|
+
* exit_code?, dry_run?}
|
|
109
|
+
*/
|
|
110
|
+
function registerMcp({ harness, mode = 'register', dryRun = false, spawnFn = spawnSync } = {}) {
|
|
111
|
+
if (!HARNESSES[harness]) {
|
|
112
|
+
throw new Error('Unknown harness: ' + harness + ' (expected one of: ' + Object.keys(HARNESSES).join(', ') + ')');
|
|
113
|
+
}
|
|
114
|
+
if (mode !== 'register' && mode !== 'detect' && mode !== 'unregister') {
|
|
115
|
+
throw new Error('Unsupported mode: ' + mode);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 1 — detect harness CLI on PATH
|
|
119
|
+
if (!detectHarnessPresent(harness, spawnFn)) {
|
|
120
|
+
return {
|
|
121
|
+
harness,
|
|
122
|
+
action: mode,
|
|
123
|
+
detected: false,
|
|
124
|
+
command: null,
|
|
125
|
+
applied: false,
|
|
126
|
+
idempotent_skip: false,
|
|
127
|
+
notice: harness + ' CLI not on PATH — skipping ' + MCP_NAME + ' registration',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Step 2 — idempotency check: already registered?
|
|
132
|
+
if (isAlreadyRegistered(harness, spawnFn)) {
|
|
133
|
+
return {
|
|
134
|
+
harness,
|
|
135
|
+
action: mode,
|
|
136
|
+
detected: true,
|
|
137
|
+
command: null,
|
|
138
|
+
applied: false,
|
|
139
|
+
idempotent_skip: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Step 3 — build + dispatch add command
|
|
144
|
+
const { binary, args } = buildHarnessCommand(harness, 'register');
|
|
145
|
+
const commandStr = binary + ' ' + args.join(' ');
|
|
146
|
+
|
|
147
|
+
if (dryRun) {
|
|
148
|
+
return {
|
|
149
|
+
harness,
|
|
150
|
+
action: mode,
|
|
151
|
+
detected: true,
|
|
152
|
+
command: commandStr,
|
|
153
|
+
applied: false,
|
|
154
|
+
idempotent_skip: false,
|
|
155
|
+
dry_run: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let result;
|
|
160
|
+
try {
|
|
161
|
+
result = spawnFn(binary, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return {
|
|
164
|
+
harness,
|
|
165
|
+
action: mode,
|
|
166
|
+
detected: true,
|
|
167
|
+
command: commandStr,
|
|
168
|
+
applied: false,
|
|
169
|
+
idempotent_skip: false,
|
|
170
|
+
stderr: (e && e.message) || String(e),
|
|
171
|
+
exit_code: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const stdout = (result && result.stdout) || '';
|
|
175
|
+
const stderr = (result && result.stderr) || '';
|
|
176
|
+
const exit_code = result ? result.status : null;
|
|
177
|
+
return {
|
|
178
|
+
harness,
|
|
179
|
+
action: mode,
|
|
180
|
+
detected: true,
|
|
181
|
+
command: commandStr,
|
|
182
|
+
applied: exit_code === 0,
|
|
183
|
+
idempotent_skip: false,
|
|
184
|
+
stdout: stdout.toString(),
|
|
185
|
+
stderr: stderr.toString(),
|
|
186
|
+
exit_code,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Detect overall MCP registration state across all known harnesses.
|
|
192
|
+
*
|
|
193
|
+
* @param {object} [opts]
|
|
194
|
+
* @param {Function} [opts.spawnFn]
|
|
195
|
+
* @returns {{harnesses: Array, summary: string}}
|
|
196
|
+
*/
|
|
197
|
+
function detectMcpRegistration({ spawnFn = spawnSync } = {}) {
|
|
198
|
+
const harnessIds = Object.keys(HARNESSES);
|
|
199
|
+
const results = harnessIds.map((harness) => {
|
|
200
|
+
const present = detectHarnessPresent(harness, spawnFn);
|
|
201
|
+
let registered;
|
|
202
|
+
if (present) {
|
|
203
|
+
registered = isAlreadyRegistered(harness, spawnFn);
|
|
204
|
+
} else {
|
|
205
|
+
registered = undefined;
|
|
206
|
+
}
|
|
207
|
+
return { harness, present, registered };
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const anyPresent = results.some((r) => r.present);
|
|
211
|
+
const registeredHarnesses = results.filter((r) => r.registered === true).map((r) => r.harness);
|
|
212
|
+
|
|
213
|
+
let summary;
|
|
214
|
+
if (!anyPresent) {
|
|
215
|
+
summary = 'unknown (claude/codex CLI not found)';
|
|
216
|
+
} else if (registeredHarnesses.length === 0) {
|
|
217
|
+
summary = 'not registered';
|
|
218
|
+
} else if (registeredHarnesses.length === harnessIds.length) {
|
|
219
|
+
summary = 'registered with ' + registeredHarnesses.join('+');
|
|
220
|
+
} else {
|
|
221
|
+
summary = 'registered with ' + registeredHarnesses.join('+');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { harnesses: results, summary };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
registerMcp,
|
|
229
|
+
detectMcpRegistration,
|
|
230
|
+
detectHarnessPresent,
|
|
231
|
+
isAlreadyRegistered,
|
|
232
|
+
buildHarnessCommand,
|
|
233
|
+
HARNESSES,
|
|
234
|
+
MCP_NAME,
|
|
235
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// scripts/lib/install/mcp-register.d.cts
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Plan 27.7-04 — TypeScript ambient declarations for the mcp-register lib.
|
|
4
|
+
// Sibling .d.cts kept in sync with mcp-register.cjs (Phase 27.6 lesson —
|
|
5
|
+
// precautionary for TS consumers).
|
|
6
|
+
|
|
7
|
+
import type { spawnSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
export interface Harness {
|
|
10
|
+
readonly binary: string;
|
|
11
|
+
readonly addArgs: readonly string[];
|
|
12
|
+
readonly listArgs: readonly string[];
|
|
13
|
+
readonly listMatchPattern: RegExp;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const HARNESSES: Readonly<Record<'claude' | 'codex', Harness>>;
|
|
17
|
+
export const MCP_NAME: string;
|
|
18
|
+
|
|
19
|
+
export type HarnessId = 'claude' | 'codex';
|
|
20
|
+
export type RegisterMode = 'register' | 'unregister' | 'detect';
|
|
21
|
+
|
|
22
|
+
export interface RegisterMcpResult {
|
|
23
|
+
harness: HarnessId;
|
|
24
|
+
action: RegisterMode;
|
|
25
|
+
detected: boolean;
|
|
26
|
+
command: string | null;
|
|
27
|
+
applied: boolean;
|
|
28
|
+
idempotent_skip: boolean;
|
|
29
|
+
notice?: string;
|
|
30
|
+
stdout?: string;
|
|
31
|
+
stderr?: string;
|
|
32
|
+
exit_code?: number | null;
|
|
33
|
+
dry_run?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HarnessDetectEntry {
|
|
37
|
+
harness: HarnessId;
|
|
38
|
+
present: boolean;
|
|
39
|
+
registered: boolean | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DetectResult {
|
|
43
|
+
harnesses: HarnessDetectEntry[];
|
|
44
|
+
summary: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type SpawnFn = typeof spawnSync;
|
|
48
|
+
|
|
49
|
+
export interface RegisterMcpOptions {
|
|
50
|
+
harness: HarnessId;
|
|
51
|
+
mode?: RegisterMode;
|
|
52
|
+
dryRun?: boolean;
|
|
53
|
+
spawnFn?: SpawnFn;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DetectMcpRegistrationOptions {
|
|
57
|
+
spawnFn?: SpawnFn;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerMcp(opts: RegisterMcpOptions): RegisterMcpResult;
|
|
61
|
+
export function detectMcpRegistration(opts?: DetectMcpRegistrationOptions): DetectResult;
|
|
62
|
+
export function detectHarnessPresent(harness: HarnessId, spawnFn?: SpawnFn): boolean;
|
|
63
|
+
export function isAlreadyRegistered(harness: HarnessId, spawnFn?: SpawnFn): boolean;
|
|
64
|
+
export function buildHarnessCommand(harness: HarnessId, mode?: RegisterMode): { binary: string; args: string[] };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/lib/intel-store/index.cjs — Plan 27.7-02
|
|
3
|
+
//
|
|
4
|
+
// Slice reader over <rootDir>/.design/intel/<slice_id>.json. Different
|
|
5
|
+
// surface from scripts/lib/design-search.cjs (which does cross-cycle
|
|
6
|
+
// FTS/grep recall) — see CONTEXT.md Warning #7.
|
|
7
|
+
//
|
|
8
|
+
// Surface:
|
|
9
|
+
// class IntelNotFoundError extends Error — code='directory_not_found'
|
|
10
|
+
// async readSlice(rootDir, sliceId) — parsed slice | null
|
|
11
|
+
// listSlices(rootDir) — string[] of slice ids
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
class IntelNotFoundError extends Error {
|
|
17
|
+
constructor(dir) {
|
|
18
|
+
super('source directory not found: ' + dir);
|
|
19
|
+
this.name = 'IntelNotFoundError';
|
|
20
|
+
this.code = 'directory_not_found';
|
|
21
|
+
this.dir = dir;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read slice <rootDir>/.design/intel/<sliceId>.json. Returns parsed
|
|
26
|
+
* JSON or `null` if the slice file is missing. Throws
|
|
27
|
+
* IntelNotFoundError when the intel directory itself is absent. */
|
|
28
|
+
async function readSlice(rootDir, sliceId) {
|
|
29
|
+
const dir = path.join(rootDir, '.design', 'intel');
|
|
30
|
+
if (!fs.existsSync(dir)) {
|
|
31
|
+
throw new IntelNotFoundError(dir);
|
|
32
|
+
}
|
|
33
|
+
const file = path.join(dir, sliceId + '.json');
|
|
34
|
+
if (!fs.existsSync(file)) return null;
|
|
35
|
+
const body = await fs.promises.readFile(file, 'utf8');
|
|
36
|
+
return JSON.parse(body);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** List slice ids (file basenames without extension) under .design/intel/.
|
|
40
|
+
* Throws IntelNotFoundError when the directory is absent. */
|
|
41
|
+
function listSlices(rootDir) {
|
|
42
|
+
const dir = path.join(rootDir, '.design', 'intel');
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
throw new IntelNotFoundError(dir);
|
|
45
|
+
}
|
|
46
|
+
const entries = fs.readdirSync(dir);
|
|
47
|
+
const ids = [];
|
|
48
|
+
for (const name of entries) {
|
|
49
|
+
if (!name.endsWith('.json')) continue;
|
|
50
|
+
ids.push(name.slice(0, -'.json'.length));
|
|
51
|
+
}
|
|
52
|
+
return ids;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { readSlice, listSlices, IntelNotFoundError };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// scripts/lib/intel-store/index.d.cts — TypeScript ambient declarations
|
|
2
|
+
// for the intel-store CJS module. Plan 27.7-02.
|
|
3
|
+
|
|
4
|
+
export class IntelNotFoundError extends Error {
|
|
5
|
+
code: 'directory_not_found';
|
|
6
|
+
dir: string;
|
|
7
|
+
constructor(dir: string);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function readSlice(rootDir: string, sliceId: string): Promise<unknown | null>;
|
|
11
|
+
export function listSlices(rootDir: string): string[];
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/lib/mcp-tools-lint/index.cjs
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Plan 27.7-03 — static lint for the gdd-mcp tools directory.
|
|
5
|
+
//
|
|
6
|
+
// 4 invariants enforced (origin: Phase 27.7 CONTEXT decisions):
|
|
7
|
+
//
|
|
8
|
+
// forbid-fs-path (D-06): No direct `node:fs`/`node:path` (or bare `fs`/
|
|
9
|
+
// `path`) imports inside individual tool .ts
|
|
10
|
+
// files. Tools must be thin wrappers — all
|
|
11
|
+
// filesystem I/O routes through scripts/lib/*
|
|
12
|
+
// helpers (gdd-state, intel-store, etc.). The
|
|
13
|
+
// `index.ts` and `shared.ts` siblings ARE
|
|
14
|
+
// infrastructure and are exempt.
|
|
15
|
+
//
|
|
16
|
+
// max-loc (D-06): Each tool .ts file ≤ 30 non-blank-non-comment
|
|
17
|
+
// LOC. Exempt: index.ts, shared.ts.
|
|
18
|
+
//
|
|
19
|
+
// no-write-names (D-04): Hard-block every write-verb tool name. A tool
|
|
20
|
+
// name matching /_(create|update|delete|append|
|
|
21
|
+
// clear|write|set)(_|$)/ is rejected. The MCP
|
|
22
|
+
// server is read-only by design.
|
|
23
|
+
//
|
|
24
|
+
// tool-count-cap (D-03): ≤ 12 files matching `gdd_*.ts` glob in the
|
|
25
|
+
// tools directory. Hard cap. Adding a 13th tool
|
|
26
|
+
// requires re-scoping in a new plan.
|
|
27
|
+
//
|
|
28
|
+
// Public API:
|
|
29
|
+
// lintMcpToolsDir({dir, maxLoc?, toolCap?, exemptions?}) →
|
|
30
|
+
// { violations: LintViolation[], summary: { files_scanned, violations_count } }
|
|
31
|
+
//
|
|
32
|
+
// Consumed by tests/gdd-mcp-tools-lint.test.cjs and
|
|
33
|
+
// tests/phase-27-7-baseline.test.cjs (Plan 27.7-07).
|
|
34
|
+
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
|
|
38
|
+
const DEFAULT_EXEMPTIONS = new Set(['index.ts', 'shared.ts']);
|
|
39
|
+
const DEFAULT_MAX_LOC = 30;
|
|
40
|
+
const DEFAULT_TOOL_CAP = 12;
|
|
41
|
+
const TOOL_FILE_GLOB = /^gdd_[a-z0-9_]+\.ts$/;
|
|
42
|
+
|
|
43
|
+
const FORBIDDEN_IMPORT_PATTERNS = Object.freeze([
|
|
44
|
+
/from\s+['"]node:fs['"]/,
|
|
45
|
+
/from\s+['"]node:fs\/promises['"]/,
|
|
46
|
+
/from\s+['"]node:path['"]/,
|
|
47
|
+
/from\s+['"]fs['"]/,
|
|
48
|
+
/from\s+['"]path['"]/,
|
|
49
|
+
/require\s*\(\s*['"]node:fs['"]\s*\)/,
|
|
50
|
+
/require\s*\(\s*['"]node:fs\/promises['"]\s*\)/,
|
|
51
|
+
/require\s*\(\s*['"]fs['"]\s*\)/,
|
|
52
|
+
/require\s*\(\s*['"]node:path['"]\s*\)/,
|
|
53
|
+
/require\s*\(\s*['"]path['"]\s*\)/,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// Write-verb pattern: matches when the verb is preceded by `_` and either
|
|
57
|
+
// followed by `_` or end-of-string. e.g. `gdd_decision_append` matches;
|
|
58
|
+
// `gdd_appendix_list` does NOT (the verb must be the trailing token of a
|
|
59
|
+
// `_`-separated name).
|
|
60
|
+
const WRITE_NAME_PATTERN = /_(create|update|delete|append|clear|write|set)(?:_|$)/;
|
|
61
|
+
|
|
62
|
+
const RULES = Object.freeze([
|
|
63
|
+
'forbid-fs-path',
|
|
64
|
+
'max-loc',
|
|
65
|
+
'no-write-names',
|
|
66
|
+
'tool-count-cap',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Count non-blank-non-comment lines.
|
|
71
|
+
* - Blank lines (whitespace-only) → excluded.
|
|
72
|
+
* - Lines whose first non-whitespace char is `//` (line comment) → excluded.
|
|
73
|
+
* - Lines starting with `/*` or `*` (block comment opener/continuation) → excluded.
|
|
74
|
+
*/
|
|
75
|
+
function countLoc(text) {
|
|
76
|
+
return text.split('\n').filter((line) => {
|
|
77
|
+
const t = line.trim();
|
|
78
|
+
if (t.length === 0) return false;
|
|
79
|
+
if (t.startsWith('//')) return false;
|
|
80
|
+
if (t.startsWith('/*')) return false;
|
|
81
|
+
if (t.startsWith('*')) return false;
|
|
82
|
+
return true;
|
|
83
|
+
}).length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scan source text line-by-line for the FORBIDDEN_IMPORT_PATTERNS.
|
|
88
|
+
* Returns [{rule, line, message}, …] (file is filled by the caller).
|
|
89
|
+
*/
|
|
90
|
+
function scanForbiddenImports(text) {
|
|
91
|
+
const violations = [];
|
|
92
|
+
const lines = text.split('\n');
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
for (const re of FORBIDDEN_IMPORT_PATTERNS) {
|
|
96
|
+
const m = line.match(re);
|
|
97
|
+
if (m) {
|
|
98
|
+
violations.push({
|
|
99
|
+
rule: 'forbid-fs-path',
|
|
100
|
+
line: i + 1,
|
|
101
|
+
message: 'forbidden import: ' + m[0],
|
|
102
|
+
});
|
|
103
|
+
break; // one violation per line is enough.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return violations;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract the `export const name = '…'` value. Tolerates `"` or `'` quotes
|
|
112
|
+
* and arbitrary whitespace. Returns {name, line} or null.
|
|
113
|
+
*/
|
|
114
|
+
function extractToolName(text) {
|
|
115
|
+
const lines = text.split('\n');
|
|
116
|
+
const re = /export\s+const\s+name\s*(?::\s*[A-Za-z<>{}\s,|]+)?\s*=\s*['"]([^'"]+)['"]/;
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
const m = lines[i].match(re);
|
|
119
|
+
if (m) return { name: m[1], line: i + 1 };
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Main entry point. Scans `dir` for *.ts files, applies the 4 rules,
|
|
126
|
+
* and returns a structured result.
|
|
127
|
+
*
|
|
128
|
+
* @param {{dir: string, maxLoc?: number, toolCap?: number, exemptions?: Set<string>}} opts
|
|
129
|
+
* @returns {{violations: Array<{file: string, rule: string, line: number, message: string}>, summary: {files_scanned: number, violations_count: number}}}
|
|
130
|
+
*/
|
|
131
|
+
function lintMcpToolsDir(opts) {
|
|
132
|
+
if (!opts || typeof opts.dir !== 'string' || opts.dir.length === 0) {
|
|
133
|
+
throw new Error('lintMcpToolsDir: opts.dir is required');
|
|
134
|
+
}
|
|
135
|
+
const dir = opts.dir;
|
|
136
|
+
const maxLoc = typeof opts.maxLoc === 'number' ? opts.maxLoc : DEFAULT_MAX_LOC;
|
|
137
|
+
const toolCap =
|
|
138
|
+
typeof opts.toolCap === 'number' ? opts.toolCap : DEFAULT_TOOL_CAP;
|
|
139
|
+
const exemptions =
|
|
140
|
+
opts.exemptions instanceof Set ? opts.exemptions : DEFAULT_EXEMPTIONS;
|
|
141
|
+
|
|
142
|
+
const violations = [];
|
|
143
|
+
|
|
144
|
+
const entries = fs.readdirSync(dir);
|
|
145
|
+
const tsFiles = entries.filter((e) => e.endsWith('.ts'));
|
|
146
|
+
|
|
147
|
+
// Rule D — tool-count-cap (matches `gdd_*.ts` files only; index/shared
|
|
148
|
+
// never count toward the cap).
|
|
149
|
+
const toolFiles = entries.filter((e) => TOOL_FILE_GLOB.test(e));
|
|
150
|
+
if (toolFiles.length > toolCap) {
|
|
151
|
+
violations.push({
|
|
152
|
+
file: dir,
|
|
153
|
+
rule: 'tool-count-cap',
|
|
154
|
+
line: 0,
|
|
155
|
+
message: 'count=' + toolFiles.length + ' > cap=' + toolCap,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Rules A, B, C — per-file scans.
|
|
160
|
+
for (const fname of tsFiles) {
|
|
161
|
+
const text = fs.readFileSync(path.join(dir, fname), 'utf8');
|
|
162
|
+
const isExempt = exemptions.has(fname);
|
|
163
|
+
|
|
164
|
+
// Rule A — forbid-fs-path (skip exemptions).
|
|
165
|
+
if (!isExempt) {
|
|
166
|
+
const fsViolations = scanForbiddenImports(text);
|
|
167
|
+
for (const v of fsViolations) {
|
|
168
|
+
violations.push({ file: fname, ...v });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Rule B — max-loc (skip exemptions).
|
|
173
|
+
if (!isExempt) {
|
|
174
|
+
const loc = countLoc(text);
|
|
175
|
+
if (loc > maxLoc) {
|
|
176
|
+
violations.push({
|
|
177
|
+
file: fname,
|
|
178
|
+
rule: 'max-loc',
|
|
179
|
+
line: 0,
|
|
180
|
+
message: 'loc=' + loc + ' > max=' + maxLoc,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Rule C — no-write-names. Applies to ALL ts files including
|
|
186
|
+
// exemptions (you should not even define a write-named symbol in
|
|
187
|
+
// index.ts or shared.ts — that would be a different bug).
|
|
188
|
+
const ext = extractToolName(text);
|
|
189
|
+
if (ext && WRITE_NAME_PATTERN.test(ext.name)) {
|
|
190
|
+
violations.push({
|
|
191
|
+
file: fname,
|
|
192
|
+
rule: 'no-write-names',
|
|
193
|
+
line: ext.line,
|
|
194
|
+
message: 'write tool name: ' + ext.name,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
violations,
|
|
201
|
+
summary: {
|
|
202
|
+
files_scanned: tsFiles.length,
|
|
203
|
+
violations_count: violations.length,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
lintMcpToolsDir,
|
|
210
|
+
RULES,
|
|
211
|
+
DEFAULT_EXEMPTIONS,
|
|
212
|
+
DEFAULT_MAX_LOC,
|
|
213
|
+
DEFAULT_TOOL_CAP,
|
|
214
|
+
FORBIDDEN_IMPORT_PATTERNS,
|
|
215
|
+
WRITE_NAME_PATTERN,
|
|
216
|
+
};
|