@infinitedusky/indusk-mcp 0.4.0 → 0.6.0
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/bin/cli.js +16 -1
- package/dist/bin/commands/check-gates.d.ts +4 -0
- package/dist/bin/commands/check-gates.js +84 -0
- package/dist/bin/commands/init.d.ts +2 -0
- package/dist/bin/commands/init.js +177 -3
- package/dist/bin/commands/update.js +107 -7
- package/dist/lib/impl-parser.d.ts +2 -0
- package/dist/lib/impl-parser.js +34 -2
- package/dist/lib/verification-discovery.d.ts +6 -0
- package/dist/lib/verification-discovery.js +56 -0
- package/dist/server/index.js +2 -0
- package/dist/tools/lesson-tools.d.ts +2 -0
- package/dist/tools/lesson-tools.js +87 -0
- package/dist/tools/plan-tools.js +5 -0
- package/dist/tools/quality-tools.js +67 -52
- package/dist/tools/system-tools.js +36 -0
- package/package.json +1 -1
- package/skills/context.md +15 -0
- package/skills/domain/docker.md +33 -0
- package/skills/domain/nextjs.md +33 -0
- package/skills/domain/react.md +32 -0
- package/skills/domain/solidity.md +35 -0
- package/skills/domain/tailwind.md +28 -0
- package/skills/domain/testing.md +39 -0
- package/skills/domain/typescript.md +34 -0
- package/skills/domain/vitepress.md +39 -0
- package/skills/plan.md +41 -8
- package/skills/retrospective.md +18 -3
- package/skills/toolbelt.md +11 -6
- package/skills/verify.md +11 -0
- package/skills/work.md +50 -12
- package/templates/workflows/bugfix.md +62 -0
- package/templates/workflows/feature.md +16 -0
- package/templates/workflows/refactor.md +69 -0
- package/templates/workflows/spike.md +37 -0
package/dist/bin/cli.js
CHANGED
|
@@ -14,9 +14,15 @@ program
|
|
|
14
14
|
.command("init")
|
|
15
15
|
.description("Initialize a project with InDusk dev system")
|
|
16
16
|
.option("-f, --force", "Overwrite existing files (except CLAUDE.md and planning/)")
|
|
17
|
+
.option("--skills <list>", "Comma-separated domain skills to install (e.g., nextjs,tailwind)")
|
|
18
|
+
.option("--no-domain-skills", "Skip domain skill detection and installation")
|
|
17
19
|
.action(async (opts) => {
|
|
18
20
|
const { init } = await import("./commands/init.js");
|
|
19
|
-
await init(process.cwd(), {
|
|
21
|
+
await init(process.cwd(), {
|
|
22
|
+
force: opts.force ?? false,
|
|
23
|
+
skills: opts.skills,
|
|
24
|
+
noDomainSkills: opts.domainSkills === false,
|
|
25
|
+
});
|
|
20
26
|
});
|
|
21
27
|
program
|
|
22
28
|
.command("update")
|
|
@@ -25,6 +31,15 @@ program
|
|
|
25
31
|
const { update } = await import("./commands/update.js");
|
|
26
32
|
await update(process.cwd());
|
|
27
33
|
});
|
|
34
|
+
program
|
|
35
|
+
.command("check-gates")
|
|
36
|
+
.description("Validate plan execution gates — reports incomplete verification, context, and document items")
|
|
37
|
+
.option("--file <path>", "Path to a specific impl.md file")
|
|
38
|
+
.option("--phase <number>", "Check a specific phase number", Number.parseInt)
|
|
39
|
+
.action(async (opts) => {
|
|
40
|
+
const { checkGates } = await import("./commands/check-gates.js");
|
|
41
|
+
await checkGates(process.cwd(), { file: opts.file, phase: opts.phase });
|
|
42
|
+
});
|
|
28
43
|
program
|
|
29
44
|
.command("serve")
|
|
30
45
|
.description("Start the MCP server (used by Claude Code via .mcp.json)")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAllPhaseCompletions, parseImpl } from "../../lib/impl-parser.js";
|
|
4
|
+
export async function checkGates(projectRoot, options = {}) {
|
|
5
|
+
let implPath;
|
|
6
|
+
if (options.file) {
|
|
7
|
+
implPath = options.file;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
// Find active impl (in-progress status) in planning/
|
|
11
|
+
const planningDir = join(projectRoot, "planning");
|
|
12
|
+
if (!existsSync(planningDir)) {
|
|
13
|
+
console.error("No planning/ directory found");
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const plans = readdirSync(planningDir, { withFileTypes: true })
|
|
18
|
+
.filter((d) => d.isDirectory() && d.name !== "archive")
|
|
19
|
+
.map((d) => d.name);
|
|
20
|
+
let found = null;
|
|
21
|
+
for (const plan of plans) {
|
|
22
|
+
const path = join(planningDir, plan, "impl.md");
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
continue;
|
|
25
|
+
const content = readFileSync(path, "utf-8");
|
|
26
|
+
if (content.includes("status: in-progress")) {
|
|
27
|
+
found = path;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!found) {
|
|
32
|
+
console.error("No in-progress impl found in planning/");
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
implPath = found;
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(implPath)) {
|
|
39
|
+
console.error(`File not found: ${implPath}`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const parsed = parseImpl(implPath);
|
|
44
|
+
const completions = getAllPhaseCompletions(parsed);
|
|
45
|
+
if (options.phase) {
|
|
46
|
+
const phase = completions.find((c) => c.phase === options.phase);
|
|
47
|
+
if (!phase) {
|
|
48
|
+
console.error(`Phase ${options.phase} not found`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
printPhase(phase);
|
|
53
|
+
process.exitCode = phase.complete ? 0 : 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Report all phases
|
|
57
|
+
let allPass = true;
|
|
58
|
+
console.info(`Gate status: ${implPath}\n`);
|
|
59
|
+
for (const phase of completions) {
|
|
60
|
+
printPhase(phase);
|
|
61
|
+
if (!phase.complete)
|
|
62
|
+
allPass = false;
|
|
63
|
+
}
|
|
64
|
+
// Check for blockers
|
|
65
|
+
for (const phase of parsed.phases) {
|
|
66
|
+
if (phase.blocker) {
|
|
67
|
+
console.info(` BLOCKER in Phase ${phase.number}: ${phase.blocker}`);
|
|
68
|
+
allPass = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
console.info(allPass ? "\nAll gates pass." : "\nSome gates incomplete.");
|
|
72
|
+
process.exitCode = allPass ? 0 : 1;
|
|
73
|
+
}
|
|
74
|
+
function printPhase(phase) {
|
|
75
|
+
const status = phase.complete ? "PASS" : "FAIL";
|
|
76
|
+
console.info(`Phase ${phase.phase}: ${phase.name} — ${status} (${phase.checkedItems}/${phase.totalItems})`);
|
|
77
|
+
if (!phase.complete) {
|
|
78
|
+
for (const [gate, items] of Object.entries(phase.uncheckedByGate)) {
|
|
79
|
+
for (const item of items) {
|
|
80
|
+
console.info(` [${gate}] ${item}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -70,8 +70,56 @@ function createCgcIgnore(projectRoot) {
|
|
|
70
70
|
].join("\n"));
|
|
71
71
|
console.info(" create: .cgcignore");
|
|
72
72
|
}
|
|
73
|
+
const DOMAIN_DETECTION_MAP = [
|
|
74
|
+
{ signal: "dependency", match: "next", skill: "nextjs" },
|
|
75
|
+
{ signal: "dependency", match: "tailwindcss", skill: "tailwind" },
|
|
76
|
+
{ signal: "dependency", match: "react", skill: "react" },
|
|
77
|
+
{ signal: "devDependency", match: "typescript", skill: "typescript" },
|
|
78
|
+
{ signal: "devDependency", match: "vitest", skill: "testing" },
|
|
79
|
+
{ signal: "devDependency", match: "jest", skill: "testing" },
|
|
80
|
+
{ signal: "dependency", match: "vitepress", skill: "vitepress" },
|
|
81
|
+
];
|
|
82
|
+
const FILE_PATTERN_DETECTIONS = [
|
|
83
|
+
{ pattern: "*.sol", skill: "solidity" },
|
|
84
|
+
{ pattern: "Dockerfile*", skill: "docker" },
|
|
85
|
+
];
|
|
86
|
+
function detectDomainSkills(projectRoot) {
|
|
87
|
+
const detections = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
// Check package.json dependencies
|
|
90
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
91
|
+
if (existsSync(pkgPath)) {
|
|
92
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
93
|
+
const deps = pkg.dependencies ?? {};
|
|
94
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
95
|
+
for (const rule of DOMAIN_DETECTION_MAP) {
|
|
96
|
+
if (seen.has(rule.skill))
|
|
97
|
+
continue;
|
|
98
|
+
const source = rule.signal === "dependency" ? deps : devDeps;
|
|
99
|
+
if (source[rule.match]) {
|
|
100
|
+
detections.push({
|
|
101
|
+
skill: rule.skill,
|
|
102
|
+
signal: rule.signal,
|
|
103
|
+
match: rule.match,
|
|
104
|
+
});
|
|
105
|
+
seen.add(rule.skill);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Check file patterns
|
|
110
|
+
for (const rule of FILE_PATTERN_DETECTIONS) {
|
|
111
|
+
if (seen.has(rule.skill))
|
|
112
|
+
continue;
|
|
113
|
+
const matches = globSync(rule.pattern, { cwd: projectRoot, maxDepth: 3 });
|
|
114
|
+
if (matches.length > 0) {
|
|
115
|
+
detections.push({ skill: rule.skill, signal: "file-pattern", match: rule.pattern });
|
|
116
|
+
seen.add(rule.skill);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return detections;
|
|
120
|
+
}
|
|
73
121
|
export async function init(projectRoot, options = {}) {
|
|
74
|
-
const { force = false } = options;
|
|
122
|
+
const { force = false, skills, noDomainSkills = false } = options;
|
|
75
123
|
const projectName = basename(projectRoot);
|
|
76
124
|
console.info(`Initializing InDusk dev system...${force ? " (--force)" : ""}\n`);
|
|
77
125
|
// 1. Copy skills
|
|
@@ -91,7 +139,66 @@ export async function init(projectRoot, options = {}) {
|
|
|
91
139
|
cpSync(join(skillsSource, file), targetFile);
|
|
92
140
|
console.info(` ${existsSync(targetFile) ? "overwrite" : "create"}: .claude/skills/${skillName}/SKILL.md`);
|
|
93
141
|
}
|
|
94
|
-
// 2.
|
|
142
|
+
// 2. Install domain skills
|
|
143
|
+
if (!noDomainSkills) {
|
|
144
|
+
console.info("\n[Domain Skills]");
|
|
145
|
+
const domainSource = join(packageRoot, "skills/domain");
|
|
146
|
+
let skillsToInstall;
|
|
147
|
+
if (skills) {
|
|
148
|
+
skillsToInstall = skills.split(",").map((s) => s.trim());
|
|
149
|
+
console.info(` manual: installing ${skillsToInstall.join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const detections = detectDomainSkills(projectRoot);
|
|
153
|
+
skillsToInstall = detections.map((d) => d.skill);
|
|
154
|
+
if (detections.length > 0) {
|
|
155
|
+
for (const d of detections) {
|
|
156
|
+
console.info(` detected: ${d.skill} (${d.signal}: ${d.match})`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.info(" none detected");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const skillName of skillsToInstall) {
|
|
164
|
+
const sourceFile = join(domainSource, `${skillName}.md`);
|
|
165
|
+
if (!existsSync(sourceFile)) {
|
|
166
|
+
console.info(` skip: ${skillName} (not found in registry)`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const targetDir = join(skillsTarget, skillName);
|
|
170
|
+
const targetFile = join(targetDir, "SKILL.md");
|
|
171
|
+
if (existsSync(targetFile) && !force) {
|
|
172
|
+
console.info(` skip: .claude/skills/${skillName}/SKILL.md (already exists)`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
mkdirSync(targetDir, { recursive: true });
|
|
176
|
+
cpSync(sourceFile, targetFile);
|
|
177
|
+
console.info(` install: .claude/skills/${skillName}/SKILL.md`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.info("\n[Domain Skills]");
|
|
182
|
+
console.info(" skipped (--no-domain-skills)");
|
|
183
|
+
}
|
|
184
|
+
// 3. Copy community lessons
|
|
185
|
+
console.info("\n[Lessons]");
|
|
186
|
+
const lessonsSource = join(packageRoot, "lessons/community");
|
|
187
|
+
const lessonsTarget = join(projectRoot, ".claude/lessons");
|
|
188
|
+
mkdirSync(lessonsTarget, { recursive: true });
|
|
189
|
+
if (existsSync(lessonsSource)) {
|
|
190
|
+
const lessonFiles = globSync("community-*.md", { cwd: lessonsSource });
|
|
191
|
+
for (const file of lessonFiles) {
|
|
192
|
+
const targetFile = join(lessonsTarget, file);
|
|
193
|
+
if (existsSync(targetFile) && !force) {
|
|
194
|
+
console.info(` skip: .claude/lessons/${file} (already exists)`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
cpSync(join(lessonsSource, file), targetFile);
|
|
198
|
+
console.info(` ${existsSync(targetFile) ? "overwrite" : "create"}: .claude/lessons/${file}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 3. Create CLAUDE.md (never overwrite — write CLAUDE-NEW.md if exists)
|
|
95
202
|
console.info("\n[Project files]");
|
|
96
203
|
const claudeMdPath = join(projectRoot, "CLAUDE.md");
|
|
97
204
|
if (existsSync(claudeMdPath)) {
|
|
@@ -184,7 +291,74 @@ export async function init(projectRoot, options = {}) {
|
|
|
184
291
|
cpSync(join(packageRoot, "templates/biome.template.json"), biomePath);
|
|
185
292
|
console.info(` ${existsSync(biomePath) ? "overwrite" : "create"}: biome.json`);
|
|
186
293
|
}
|
|
187
|
-
// 7.
|
|
294
|
+
// 7. Install gate enforcement hooks
|
|
295
|
+
console.info("\n[Hooks]");
|
|
296
|
+
const hooksSource = join(packageRoot, "hooks");
|
|
297
|
+
const hooksTarget = join(projectRoot, ".claude/hooks");
|
|
298
|
+
const hookFiles = ["check-gates.js", "gate-reminder.js"];
|
|
299
|
+
if (existsSync(hooksSource)) {
|
|
300
|
+
mkdirSync(hooksTarget, { recursive: true });
|
|
301
|
+
for (const file of hookFiles) {
|
|
302
|
+
const sourceFile = join(hooksSource, file);
|
|
303
|
+
const targetFile = join(hooksTarget, file);
|
|
304
|
+
if (!existsSync(sourceFile))
|
|
305
|
+
continue;
|
|
306
|
+
if (existsSync(targetFile) && !force) {
|
|
307
|
+
console.info(` skip: .claude/hooks/${file} (already exists)`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
cpSync(sourceFile, targetFile);
|
|
311
|
+
console.info(` ${existsSync(targetFile) ? "overwrite" : "create"}: .claude/hooks/${file}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Merge hook config into .claude/settings.json
|
|
316
|
+
const claudeSettingsPath = join(projectRoot, ".claude/settings.json");
|
|
317
|
+
const hookConfig = {
|
|
318
|
+
PreToolUse: [
|
|
319
|
+
{
|
|
320
|
+
matcher: "Edit|Write",
|
|
321
|
+
hooks: [{ type: "command", command: "node .claude/hooks/check-gates.js" }],
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
PostToolUse: [
|
|
325
|
+
{
|
|
326
|
+
matcher: "Edit|Write",
|
|
327
|
+
hooks: [{ type: "command", command: "node .claude/hooks/gate-reminder.js" }],
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
if (existsSync(claudeSettingsPath)) {
|
|
332
|
+
const existing = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
|
|
333
|
+
existing.hooks = existing.hooks || {};
|
|
334
|
+
let hooksUpdated = false;
|
|
335
|
+
for (const [event, entries] of Object.entries(hookConfig)) {
|
|
336
|
+
const existingEntries = existing.hooks[event] || [];
|
|
337
|
+
// Check if our hook is already present
|
|
338
|
+
const hasOurHook = existingEntries.some((e) => e.hooks?.some((h) => h.command?.includes("check-gates") || h.command?.includes("gate-reminder")));
|
|
339
|
+
if (!hasOurHook || force) {
|
|
340
|
+
// Remove old entries if force, then add
|
|
341
|
+
if (force) {
|
|
342
|
+
existing.hooks[event] = existingEntries.filter((e) => !e.hooks?.some((h) => h.command?.includes("check-gates") || h.command?.includes("gate-reminder")));
|
|
343
|
+
}
|
|
344
|
+
existing.hooks[event] = [...(existing.hooks[event] || []), ...entries];
|
|
345
|
+
hooksUpdated = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (hooksUpdated) {
|
|
349
|
+
writeFileSync(claudeSettingsPath, `${JSON.stringify(existing, null, "\t")}\n`);
|
|
350
|
+
console.info(" update: .claude/settings.json (added hook config)");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
console.info(" skip: .claude/settings.json hooks (already configured)");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
const settings = { hooks: hookConfig };
|
|
358
|
+
writeFileSync(claudeSettingsPath, `${JSON.stringify(settings, null, "\t")}\n`);
|
|
359
|
+
console.info(" create: .claude/settings.json (with hook config)");
|
|
360
|
+
}
|
|
361
|
+
// 8. Create .cgcignore (always overwrite — package-owned)
|
|
188
362
|
createCgcIgnore(projectRoot);
|
|
189
363
|
// 8. Infrastructure: FalkorDB + CGC
|
|
190
364
|
console.info("\n[Infrastructure]");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { cpSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { globSync } from "glob";
|
|
@@ -14,21 +14,25 @@ export async function update(projectRoot) {
|
|
|
14
14
|
const skillsTarget = join(projectRoot, ".claude/skills");
|
|
15
15
|
const skillFiles = globSync("*.md", { cwd: skillsSource });
|
|
16
16
|
let updated = 0;
|
|
17
|
-
let
|
|
17
|
+
let added = 0;
|
|
18
|
+
let current = 0;
|
|
18
19
|
for (const file of skillFiles) {
|
|
19
20
|
const skillName = file.replace(".md", "");
|
|
20
21
|
const sourceFile = join(skillsSource, file);
|
|
21
|
-
const
|
|
22
|
+
const targetDir = join(skillsTarget, skillName);
|
|
23
|
+
const targetFile = join(targetDir, "SKILL.md");
|
|
22
24
|
if (!existsSync(targetFile)) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
mkdirSync(targetDir, { recursive: true });
|
|
26
|
+
cpSync(sourceFile, targetFile);
|
|
27
|
+
console.info(` added: ${skillName} (new skill)`);
|
|
28
|
+
added++;
|
|
25
29
|
continue;
|
|
26
30
|
}
|
|
27
31
|
const sourceHash = fileHash(sourceFile);
|
|
28
32
|
const targetHash = fileHash(targetFile);
|
|
29
33
|
if (sourceHash === targetHash) {
|
|
30
34
|
console.info(` current: ${skillName}`);
|
|
31
|
-
|
|
35
|
+
current++;
|
|
32
36
|
}
|
|
33
37
|
else {
|
|
34
38
|
cpSync(sourceFile, targetFile);
|
|
@@ -36,5 +40,101 @@ export async function update(projectRoot) {
|
|
|
36
40
|
updated++;
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
|
-
console.info(`\n${updated} updated, ${
|
|
43
|
+
console.info(`\n${added} added, ${updated} updated, ${current} current.`);
|
|
44
|
+
// Sync community lessons (only community- prefixed files)
|
|
45
|
+
console.info("\nChecking for lesson updates...\n");
|
|
46
|
+
const lessonsSource = join(packageRoot, "lessons/community");
|
|
47
|
+
const lessonsTarget = join(projectRoot, ".claude/lessons");
|
|
48
|
+
let lessonsAdded = 0;
|
|
49
|
+
let lessonsUpdated = 0;
|
|
50
|
+
let lessonsCurrent = 0;
|
|
51
|
+
if (existsSync(lessonsSource)) {
|
|
52
|
+
mkdirSync(lessonsTarget, { recursive: true });
|
|
53
|
+
const lessonFiles = globSync("community-*.md", { cwd: lessonsSource });
|
|
54
|
+
for (const file of lessonFiles) {
|
|
55
|
+
const sourceFile = join(lessonsSource, file);
|
|
56
|
+
const targetFile = join(lessonsTarget, file);
|
|
57
|
+
if (!existsSync(targetFile)) {
|
|
58
|
+
cpSync(sourceFile, targetFile);
|
|
59
|
+
console.info(` added: ${file}`);
|
|
60
|
+
lessonsAdded++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const sourceH = fileHash(sourceFile);
|
|
64
|
+
const targetH = fileHash(targetFile);
|
|
65
|
+
if (sourceH === targetH) {
|
|
66
|
+
console.info(` current: ${file}`);
|
|
67
|
+
lessonsCurrent++;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
cpSync(sourceFile, targetFile);
|
|
71
|
+
console.info(` updated: ${file}`);
|
|
72
|
+
lessonsUpdated++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.info(`\n${lessonsAdded} added, ${lessonsUpdated} updated, ${lessonsCurrent} current.`);
|
|
77
|
+
// Sync installed domain skills (only update ones already installed)
|
|
78
|
+
console.info("\nChecking for domain skill updates...\n");
|
|
79
|
+
const domainSource = join(packageRoot, "skills/domain");
|
|
80
|
+
let domainUpdated = 0;
|
|
81
|
+
let domainCurrent = 0;
|
|
82
|
+
if (existsSync(domainSource)) {
|
|
83
|
+
const domainFiles = globSync("*.md", { cwd: domainSource });
|
|
84
|
+
for (const file of domainFiles) {
|
|
85
|
+
const skillName = file.replace(".md", "");
|
|
86
|
+
const sourceFile = join(domainSource, file);
|
|
87
|
+
const targetFile = join(skillsTarget, skillName, "SKILL.md");
|
|
88
|
+
// Only update if already installed — don't install new domain skills during update
|
|
89
|
+
if (!existsSync(targetFile))
|
|
90
|
+
continue;
|
|
91
|
+
const sourceH = fileHash(sourceFile);
|
|
92
|
+
const targetH = fileHash(targetFile);
|
|
93
|
+
if (sourceH === targetH) {
|
|
94
|
+
console.info(` current: ${skillName}`);
|
|
95
|
+
domainCurrent++;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
cpSync(sourceFile, targetFile);
|
|
99
|
+
console.info(` updated: ${skillName}`);
|
|
100
|
+
domainUpdated++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (domainUpdated + domainCurrent > 0) {
|
|
105
|
+
console.info(`\n${domainUpdated} updated, ${domainCurrent} current.`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.info(" no domain skills installed");
|
|
109
|
+
}
|
|
110
|
+
// Sync hook scripts (only if hooks directory exists in target)
|
|
111
|
+
console.info("\nChecking for hook updates...\n");
|
|
112
|
+
const hooksSource = join(packageRoot, "hooks");
|
|
113
|
+
const hooksTarget = join(projectRoot, ".claude/hooks");
|
|
114
|
+
let hooksUpdated = 0;
|
|
115
|
+
let hooksCurrent = 0;
|
|
116
|
+
if (existsSync(hooksSource) && existsSync(hooksTarget)) {
|
|
117
|
+
const hookFiles = ["check-gates.js", "gate-reminder.js"];
|
|
118
|
+
for (const file of hookFiles) {
|
|
119
|
+
const sourceFile = join(hooksSource, file);
|
|
120
|
+
const targetFile = join(hooksTarget, file);
|
|
121
|
+
if (!existsSync(sourceFile) || !existsSync(targetFile))
|
|
122
|
+
continue;
|
|
123
|
+
const sourceH = fileHash(sourceFile);
|
|
124
|
+
const targetH = fileHash(targetFile);
|
|
125
|
+
if (sourceH === targetH) {
|
|
126
|
+
console.info(` current: ${file}`);
|
|
127
|
+
hooksCurrent++;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
cpSync(sourceFile, targetFile);
|
|
131
|
+
console.info(` updated: ${file}`);
|
|
132
|
+
hooksUpdated++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.info(`\n${hooksUpdated} updated, ${hooksCurrent} current.`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.info(" hooks not installed (run init to install)");
|
|
139
|
+
}
|
|
40
140
|
}
|
package/dist/lib/impl-parser.js
CHANGED
|
@@ -27,6 +27,8 @@ export function parseImplString(raw) {
|
|
|
27
27
|
let currentPhase = null;
|
|
28
28
|
let currentGateType = "implementation";
|
|
29
29
|
let currentGateLines = [];
|
|
30
|
+
let inForwardIntelligence = false;
|
|
31
|
+
let forwardIntelligenceLines = [];
|
|
30
32
|
function flushGate() {
|
|
31
33
|
if (!currentPhase)
|
|
32
34
|
return;
|
|
@@ -47,21 +49,51 @@ export function parseImplString(raw) {
|
|
|
47
49
|
number: Number.parseInt(phaseMatch[1], 10),
|
|
48
50
|
name: phaseMatch[2].trim(),
|
|
49
51
|
gates: [],
|
|
52
|
+
blocker: null,
|
|
53
|
+
forwardIntelligence: null,
|
|
50
54
|
};
|
|
51
55
|
currentGateType = "implementation";
|
|
52
56
|
currentGateLines = [];
|
|
53
57
|
continue;
|
|
54
58
|
}
|
|
59
|
+
// Forward Intelligence header: #### Phase N Forward Intelligence
|
|
60
|
+
const fiMatch = line.match(/^####\s+Phase\s+\d+\s+Forward Intelligence\b/);
|
|
61
|
+
if (fiMatch) {
|
|
62
|
+
flushGate();
|
|
63
|
+
inForwardIntelligence = true;
|
|
64
|
+
forwardIntelligenceLines = [];
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
55
67
|
// Gate header: #### Phase N Verification|Context|Document
|
|
56
68
|
const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|Context|Document)\b/);
|
|
57
69
|
if (gateMatch) {
|
|
70
|
+
if (inForwardIntelligence && currentPhase) {
|
|
71
|
+
currentPhase.forwardIntelligence = forwardIntelligenceLines.join("\n").trim() || null;
|
|
72
|
+
inForwardIntelligence = false;
|
|
73
|
+
}
|
|
58
74
|
flushGate();
|
|
59
75
|
currentGateType = GATE_SUFFIXES[gateMatch[1]];
|
|
60
76
|
continue;
|
|
61
77
|
}
|
|
62
|
-
|
|
78
|
+
// Blocker line: blocker: description
|
|
79
|
+
if (currentPhase) {
|
|
80
|
+
const blockerMatch = line.match(/^blocker:\s+(.*)/);
|
|
81
|
+
if (blockerMatch) {
|
|
82
|
+
currentPhase.blocker = blockerMatch[1].trim();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (inForwardIntelligence) {
|
|
87
|
+
forwardIntelligenceLines.push(line);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
currentGateLines.push(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Flush last forward intelligence, gate, and phase
|
|
94
|
+
if (inForwardIntelligence && currentPhase) {
|
|
95
|
+
currentPhase.forwardIntelligence = forwardIntelligenceLines.join("\n").trim() || null;
|
|
63
96
|
}
|
|
64
|
-
// Flush last gate and phase
|
|
65
97
|
flushGate();
|
|
66
98
|
if (currentPhase)
|
|
67
99
|
phases.push(currentPhase);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { globSync } from "glob";
|
|
4
|
+
const SCRIPT_PATTERNS = {
|
|
5
|
+
typecheck: "typecheck",
|
|
6
|
+
"type-check": "type-check",
|
|
7
|
+
lint: "lint",
|
|
8
|
+
test: "test",
|
|
9
|
+
build: "build",
|
|
10
|
+
check: "check",
|
|
11
|
+
"check:fix": "check:fix",
|
|
12
|
+
};
|
|
13
|
+
export function discoverVerificationCommands(projectRoot) {
|
|
14
|
+
const checks = [];
|
|
15
|
+
// 1. Read package.json scripts
|
|
16
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
17
|
+
if (existsSync(pkgPath)) {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
19
|
+
const scripts = pkg.scripts ?? {};
|
|
20
|
+
for (const [pattern, name] of Object.entries(SCRIPT_PATTERNS)) {
|
|
21
|
+
if (scripts[pattern]) {
|
|
22
|
+
const runner = existsSync(join(projectRoot, "pnpm-lock.yaml"))
|
|
23
|
+
? "pnpm"
|
|
24
|
+
: existsSync(join(projectRoot, "yarn.lock"))
|
|
25
|
+
? "yarn"
|
|
26
|
+
: "npm";
|
|
27
|
+
checks.push({
|
|
28
|
+
name,
|
|
29
|
+
command: `${runner} run ${pattern}`,
|
|
30
|
+
source: "package.json",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Check for tsc script variants
|
|
35
|
+
if (!checks.some((c) => c.name === "typecheck") && scripts.tsc) {
|
|
36
|
+
checks.push({ name: "typecheck", command: "npm run tsc", source: "package.json" });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 2. Detect tool configs
|
|
40
|
+
if (existsSync(join(projectRoot, "biome.json")) && !checks.some((c) => c.name === "check")) {
|
|
41
|
+
checks.push({ name: "biome", command: "npx biome check", source: "config-file" });
|
|
42
|
+
}
|
|
43
|
+
if (existsSync(join(projectRoot, "tsconfig.json")) &&
|
|
44
|
+
!checks.some((c) => c.name === "typecheck")) {
|
|
45
|
+
checks.push({ name: "typecheck", command: "npx tsc --noEmit", source: "config-file" });
|
|
46
|
+
}
|
|
47
|
+
const vitestConfigs = globSync("vitest.config.*", { cwd: projectRoot });
|
|
48
|
+
if (vitestConfigs.length > 0 && !checks.some((c) => c.name === "test")) {
|
|
49
|
+
checks.push({ name: "test", command: "npx vitest run", source: "config-file" });
|
|
50
|
+
}
|
|
51
|
+
const jestConfigs = globSync("jest.config.*", { cwd: projectRoot });
|
|
52
|
+
if (jestConfigs.length > 0 && !checks.some((c) => c.name === "test")) {
|
|
53
|
+
checks.push({ name: "test", command: "npx jest", source: "config-file" });
|
|
54
|
+
}
|
|
55
|
+
return checks;
|
|
56
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { registerContextTools } from "../tools/context-tools.js";
|
|
5
5
|
import { registerDocumentTools } from "../tools/document-tools.js";
|
|
6
6
|
import { registerGraphTools } from "../tools/graph-tools.js";
|
|
7
|
+
import { registerLessonTools } from "../tools/lesson-tools.js";
|
|
7
8
|
import { registerPlanTools } from "../tools/plan-tools.js";
|
|
8
9
|
import { registerQualityTools } from "../tools/quality-tools.js";
|
|
9
10
|
import { registerSystemTools } from "../tools/system-tools.js";
|
|
@@ -19,6 +20,7 @@ export async function startServer() {
|
|
|
19
20
|
registerDocumentTools(server, projectRoot);
|
|
20
21
|
registerSystemTools(server, projectRoot);
|
|
21
22
|
registerGraphTools(server, projectRoot);
|
|
23
|
+
registerLessonTools(server, projectRoot);
|
|
22
24
|
const transport = new StdioServerTransport();
|
|
23
25
|
await server.connect(transport);
|
|
24
26
|
}
|