lean-spec 0.2.6-dev.20251126022313 → 0.2.6-dev.20251126030344
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/{chunk-RTEGSMVL.js → chunk-IXCZPYB7.js} +483 -14
- package/dist/chunk-IXCZPYB7.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/mcp-server.js +1 -1
- package/package.json +1 -1
- package/templates/detailed/AGENTS.md +100 -72
- package/templates/standard/AGENTS.md +100 -72
- package/dist/chunk-RTEGSMVL.js.map +0 -1
|
@@ -15,7 +15,7 @@ import { Command } from 'commander';
|
|
|
15
15
|
import { execSync, spawn } from 'child_process';
|
|
16
16
|
import ora from 'ora';
|
|
17
17
|
import stripAnsi from 'strip-ansi';
|
|
18
|
-
import { select } from '@inquirer/prompts';
|
|
18
|
+
import { select, checkbox } from '@inquirer/prompts';
|
|
19
19
|
import dayjs3 from 'dayjs';
|
|
20
20
|
import { marked } from 'marked';
|
|
21
21
|
import { markedTerminal } from 'marked-terminal';
|
|
@@ -1892,6 +1892,95 @@ async function copyTemplate(source, target, cwd = process.cwd()) {
|
|
|
1892
1892
|
console.log(chalk19.gray(` Edit: ${targetPath}`));
|
|
1893
1893
|
console.log(chalk19.gray(` Use with: lean-spec create <spec-name> --template=${templateName}`));
|
|
1894
1894
|
}
|
|
1895
|
+
var AI_TOOL_CONFIGS = {
|
|
1896
|
+
claude: {
|
|
1897
|
+
file: "CLAUDE.md",
|
|
1898
|
+
description: "Claude Code / Claude Desktop (CLAUDE.md)",
|
|
1899
|
+
default: true,
|
|
1900
|
+
usesSymlink: true
|
|
1901
|
+
},
|
|
1902
|
+
gemini: {
|
|
1903
|
+
file: "GEMINI.md",
|
|
1904
|
+
description: "Gemini CLI (GEMINI.md)",
|
|
1905
|
+
default: false,
|
|
1906
|
+
usesSymlink: true
|
|
1907
|
+
},
|
|
1908
|
+
copilot: {
|
|
1909
|
+
file: "AGENTS.md",
|
|
1910
|
+
description: "GitHub Copilot (AGENTS.md - default)",
|
|
1911
|
+
default: true,
|
|
1912
|
+
usesSymlink: false
|
|
1913
|
+
// Primary file, no symlink needed
|
|
1914
|
+
},
|
|
1915
|
+
cursor: {
|
|
1916
|
+
file: "AGENTS.md",
|
|
1917
|
+
description: "Cursor (uses AGENTS.md)",
|
|
1918
|
+
default: false,
|
|
1919
|
+
usesSymlink: false
|
|
1920
|
+
},
|
|
1921
|
+
windsurf: {
|
|
1922
|
+
file: "AGENTS.md",
|
|
1923
|
+
description: "Windsurf (uses AGENTS.md)",
|
|
1924
|
+
default: false,
|
|
1925
|
+
usesSymlink: false
|
|
1926
|
+
},
|
|
1927
|
+
cline: {
|
|
1928
|
+
file: "AGENTS.md",
|
|
1929
|
+
description: "Cline (uses AGENTS.md)",
|
|
1930
|
+
default: false,
|
|
1931
|
+
usesSymlink: false
|
|
1932
|
+
},
|
|
1933
|
+
warp: {
|
|
1934
|
+
file: "AGENTS.md",
|
|
1935
|
+
description: "Warp Terminal (uses AGENTS.md)",
|
|
1936
|
+
default: false,
|
|
1937
|
+
usesSymlink: false
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
async function createAgentToolSymlinks(cwd, selectedTools) {
|
|
1941
|
+
const results = [];
|
|
1942
|
+
const isWindows = process.platform === "win32";
|
|
1943
|
+
const filesToCreate = /* @__PURE__ */ new Set();
|
|
1944
|
+
for (const tool of selectedTools) {
|
|
1945
|
+
const config = AI_TOOL_CONFIGS[tool];
|
|
1946
|
+
if (config.usesSymlink) {
|
|
1947
|
+
filesToCreate.add(config.file);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
for (const file of filesToCreate) {
|
|
1951
|
+
const targetPath = path15.join(cwd, file);
|
|
1952
|
+
try {
|
|
1953
|
+
try {
|
|
1954
|
+
await fs9.access(targetPath);
|
|
1955
|
+
results.push({ file, skipped: true });
|
|
1956
|
+
continue;
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
if (isWindows) {
|
|
1960
|
+
const windowsContent = `# ${file}
|
|
1961
|
+
|
|
1962
|
+
> **Note**: This file is a copy of AGENTS.md for tools that expect ${file}.
|
|
1963
|
+
>
|
|
1964
|
+
> **Important**: Edit AGENTS.md instead. Then run \`lean-spec init\` to regenerate this file,
|
|
1965
|
+
> or manually copy AGENTS.md to ${file}.
|
|
1966
|
+
|
|
1967
|
+
See AGENTS.md for the full LeanSpec AI agent instructions.
|
|
1968
|
+
`;
|
|
1969
|
+
await fs9.writeFile(targetPath, windowsContent, "utf-8");
|
|
1970
|
+
results.push({ file, created: true, error: "created as copy (Windows)" });
|
|
1971
|
+
} else {
|
|
1972
|
+
await fs9.symlink("AGENTS.md", targetPath);
|
|
1973
|
+
results.push({ file, created: true });
|
|
1974
|
+
}
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
results.push({
|
|
1977
|
+
file,
|
|
1978
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return results;
|
|
1983
|
+
}
|
|
1895
1984
|
async function detectExistingSystemPrompts(cwd) {
|
|
1896
1985
|
const commonFiles = [
|
|
1897
1986
|
"AGENTS.md",
|
|
@@ -2084,7 +2173,7 @@ var __dirname = path15.dirname(fileURLToPath(import.meta.url));
|
|
|
2084
2173
|
var TEMPLATES_DIR = path15.join(__dirname, "..", "templates");
|
|
2085
2174
|
var EXAMPLES_DIR = path15.join(TEMPLATES_DIR, "examples");
|
|
2086
2175
|
function initCommand() {
|
|
2087
|
-
return new Command("init").description("Initialize LeanSpec in current directory").option("-y, --yes", "Skip prompts and use defaults (quick start with standard template)").option("--template <name>", "Use specific template (standard or detailed)").option("--example [name]", "Scaffold an example project for tutorials (interactive if no name provided)").option("--name <dirname>", "Custom directory name for example project").option("--list", "List available example projects").action(async (options) => {
|
|
2176
|
+
return new Command("init").description("Initialize LeanSpec in current directory").option("-y, --yes", "Skip prompts and use defaults (quick start with standard template)").option("--template <name>", "Use specific template (standard or detailed)").option("--example [name]", "Scaffold an example project for tutorials (interactive if no name provided)").option("--name <dirname>", "Custom directory name for example project").option("--list", "List available example projects").option("--agent-tools <tools>", 'AI tools to create symlinks for (comma-separated: claude,gemini,copilot or "all" or "none")').action(async (options) => {
|
|
2088
2177
|
if (options.list) {
|
|
2089
2178
|
await listExamples();
|
|
2090
2179
|
return;
|
|
@@ -2093,10 +2182,10 @@ function initCommand() {
|
|
|
2093
2182
|
await scaffoldExample(options.example, options.name);
|
|
2094
2183
|
return;
|
|
2095
2184
|
}
|
|
2096
|
-
await initProject(options.yes, options.template);
|
|
2185
|
+
await initProject(options.yes, options.template, options.agentTools);
|
|
2097
2186
|
});
|
|
2098
2187
|
}
|
|
2099
|
-
async function initProject(skipPrompts = false, templateOption) {
|
|
2188
|
+
async function initProject(skipPrompts = false, templateOption, agentToolsOption) {
|
|
2100
2189
|
const cwd = process.cwd();
|
|
2101
2190
|
try {
|
|
2102
2191
|
await fs9.access(path15.join(cwd, ".lean-spec", "config.json"));
|
|
@@ -2110,8 +2199,21 @@ async function initProject(skipPrompts = false, templateOption) {
|
|
|
2110
2199
|
console.log("");
|
|
2111
2200
|
let setupMode = "quick";
|
|
2112
2201
|
let templateName = templateOption || "standard";
|
|
2202
|
+
let selectedAgentTools = [];
|
|
2203
|
+
if (agentToolsOption) {
|
|
2204
|
+
if (agentToolsOption === "all") {
|
|
2205
|
+
selectedAgentTools = Object.keys(AI_TOOL_CONFIGS);
|
|
2206
|
+
} else if (agentToolsOption === "none") {
|
|
2207
|
+
selectedAgentTools = [];
|
|
2208
|
+
} else {
|
|
2209
|
+
selectedAgentTools = agentToolsOption.split(",").map((t) => t.trim());
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2113
2212
|
if (skipPrompts) {
|
|
2114
2213
|
console.log(chalk19.gray("Using defaults: quick start with standard template"));
|
|
2214
|
+
if (!agentToolsOption) {
|
|
2215
|
+
selectedAgentTools = ["claude", "copilot"];
|
|
2216
|
+
}
|
|
2115
2217
|
console.log("");
|
|
2116
2218
|
} else if (!templateOption) {
|
|
2117
2219
|
setupMode = await select({
|
|
@@ -2217,6 +2319,19 @@ async function initProject(skipPrompts = false, templateOption) {
|
|
|
2217
2319
|
templateConfig.structure.pattern = "flat";
|
|
2218
2320
|
templateConfig.structure.prefix = "";
|
|
2219
2321
|
}
|
|
2322
|
+
if (!skipPrompts && !agentToolsOption && setupMode !== "quick") {
|
|
2323
|
+
const toolChoices = Object.entries(AI_TOOL_CONFIGS).map(([key, config]) => ({
|
|
2324
|
+
name: config.description,
|
|
2325
|
+
value: key,
|
|
2326
|
+
checked: config.default
|
|
2327
|
+
}));
|
|
2328
|
+
selectedAgentTools = await checkbox({
|
|
2329
|
+
message: "Which AI tools do you use? (creates symlinks for tool-specific instruction files)",
|
|
2330
|
+
choices: toolChoices
|
|
2331
|
+
});
|
|
2332
|
+
} else if (!agentToolsOption && setupMode === "quick") {
|
|
2333
|
+
selectedAgentTools = ["claude", "copilot"];
|
|
2334
|
+
}
|
|
2220
2335
|
const templatesDir = path15.join(cwd, ".lean-spec", "templates");
|
|
2221
2336
|
try {
|
|
2222
2337
|
await fs9.mkdir(templatesDir, { recursive: true });
|
|
@@ -2310,6 +2425,18 @@ async function initProject(skipPrompts = false, templateOption) {
|
|
|
2310
2425
|
console.error(chalk19.red("Error copying AGENTS.md:"), error);
|
|
2311
2426
|
process.exit(1);
|
|
2312
2427
|
}
|
|
2428
|
+
if (selectedAgentTools.length > 0) {
|
|
2429
|
+
const symlinkResults = await createAgentToolSymlinks(cwd, selectedAgentTools);
|
|
2430
|
+
for (const result of symlinkResults) {
|
|
2431
|
+
if (result.created) {
|
|
2432
|
+
console.log(chalk19.green(`\u2713 Created ${result.file} \u2192 AGENTS.md`));
|
|
2433
|
+
} else if (result.skipped) {
|
|
2434
|
+
console.log(chalk19.yellow(`\u26A0 Skipped ${result.file} (already exists)`));
|
|
2435
|
+
} else if (result.error) {
|
|
2436
|
+
console.log(chalk19.yellow(`\u26A0 Could not create ${result.file}: ${result.error}`));
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2313
2440
|
}
|
|
2314
2441
|
const filesDir = path15.join(templateDir, "files");
|
|
2315
2442
|
try {
|
|
@@ -4492,6 +4619,174 @@ var SubSpecValidator = class {
|
|
|
4492
4619
|
}
|
|
4493
4620
|
}
|
|
4494
4621
|
};
|
|
4622
|
+
var SPEC_REF_PATTERNS = [
|
|
4623
|
+
// "spec 045", "Spec 045", "spec-045"
|
|
4624
|
+
/\bspec[- ]?(\d{3})\b/gi,
|
|
4625
|
+
// "045-unified-dashboard" (full spec folder name)
|
|
4626
|
+
/\b(\d{3})-[a-z][a-z0-9-]+\b/gi
|
|
4627
|
+
];
|
|
4628
|
+
var DEPENDS_ON_PATTERNS = [
|
|
4629
|
+
/depends on[:\s]+.*?\b(\d{3})\b/gi,
|
|
4630
|
+
/blocked by[:\s]+.*?\b(\d{3})\b/gi,
|
|
4631
|
+
/requires[:\s]+.*?spec[:\s]*(\d{3})\b/gi,
|
|
4632
|
+
/prerequisite[:\s]+.*?\b(\d{3})\b/gi,
|
|
4633
|
+
/after[:\s]+.*?spec[:\s]*(\d{3})\b/gi
|
|
4634
|
+
];
|
|
4635
|
+
var RELATED_PATTERNS = [
|
|
4636
|
+
/related to[:\s]+.*?\b(\d{3})\b/gi,
|
|
4637
|
+
/see (also )?spec[:\s]*(\d{3})\b/gi,
|
|
4638
|
+
/builds on[:\s]+.*?\b(\d{3})\b/gi,
|
|
4639
|
+
/similar to[:\s]+.*?\b(\d{3})\b/gi,
|
|
4640
|
+
/complements[:\s]+.*?\b(\d{3})\b/gi,
|
|
4641
|
+
/\brelated[:\s]+\[?(\d{3})/gi
|
|
4642
|
+
];
|
|
4643
|
+
var DependencyAlignmentValidator = class {
|
|
4644
|
+
name = "dependency-alignment";
|
|
4645
|
+
description = "Detect content references to specs not linked in frontmatter";
|
|
4646
|
+
strict;
|
|
4647
|
+
existingSpecNumbers;
|
|
4648
|
+
constructor(options = {}) {
|
|
4649
|
+
this.strict = options.strict ?? false;
|
|
4650
|
+
this.existingSpecNumbers = options.existingSpecNumbers ?? null;
|
|
4651
|
+
}
|
|
4652
|
+
/**
|
|
4653
|
+
* Set the existing spec numbers to validate against
|
|
4654
|
+
*/
|
|
4655
|
+
setExistingSpecNumbers(numbers) {
|
|
4656
|
+
this.existingSpecNumbers = numbers;
|
|
4657
|
+
}
|
|
4658
|
+
validate(spec, content) {
|
|
4659
|
+
const errors = [];
|
|
4660
|
+
const warnings = [];
|
|
4661
|
+
let parsed;
|
|
4662
|
+
try {
|
|
4663
|
+
parsed = matter2(content, {
|
|
4664
|
+
engines: {
|
|
4665
|
+
yaml: (str) => yaml2.load(str, { schema: yaml2.FAILSAFE_SCHEMA })
|
|
4666
|
+
}
|
|
4667
|
+
});
|
|
4668
|
+
} catch {
|
|
4669
|
+
return { passed: true, errors: [], warnings: [] };
|
|
4670
|
+
}
|
|
4671
|
+
const frontmatter = parsed.data;
|
|
4672
|
+
const bodyContent = parsed.content;
|
|
4673
|
+
const currentDependsOn = this.normalizeDeps(frontmatter.depends_on);
|
|
4674
|
+
const currentRelated = this.normalizeDeps(frontmatter.related);
|
|
4675
|
+
const allCurrentDeps = /* @__PURE__ */ new Set([...currentDependsOn, ...currentRelated]);
|
|
4676
|
+
const selfNumber = this.extractSpecNumber(spec.name);
|
|
4677
|
+
const detectedRefs = this.detectReferences(bodyContent, selfNumber);
|
|
4678
|
+
const missingDependsOn = [];
|
|
4679
|
+
const missingRelated = [];
|
|
4680
|
+
for (const ref of detectedRefs) {
|
|
4681
|
+
if (allCurrentDeps.has(ref.specNumber)) continue;
|
|
4682
|
+
if (this.existingSpecNumbers && !this.existingSpecNumbers.has(ref.specNumber)) continue;
|
|
4683
|
+
if (ref.type === "depends_on") {
|
|
4684
|
+
missingDependsOn.push(ref);
|
|
4685
|
+
} else {
|
|
4686
|
+
missingRelated.push(ref);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (missingDependsOn.length > 0) {
|
|
4690
|
+
const specNumbers = [...new Set(missingDependsOn.map((r) => r.specNumber))];
|
|
4691
|
+
const issue = {
|
|
4692
|
+
message: `Content references blocking dependencies not in frontmatter: ${specNumbers.join(", ")}`,
|
|
4693
|
+
suggestion: `Run: lean-spec link ${spec.name} --depends-on ${specNumbers.join(" ")}`
|
|
4694
|
+
};
|
|
4695
|
+
if (this.strict) {
|
|
4696
|
+
errors.push(issue);
|
|
4697
|
+
} else {
|
|
4698
|
+
warnings.push(issue);
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
if (missingRelated.length > 0) {
|
|
4702
|
+
const specNumbers = [...new Set(missingRelated.map((r) => r.specNumber))];
|
|
4703
|
+
const issue = {
|
|
4704
|
+
message: `Content references related specs not in frontmatter: ${specNumbers.join(", ")}`,
|
|
4705
|
+
suggestion: `Run: lean-spec link ${spec.name} --related ${specNumbers.join(" ")}`
|
|
4706
|
+
};
|
|
4707
|
+
if (this.strict) {
|
|
4708
|
+
errors.push(issue);
|
|
4709
|
+
} else {
|
|
4710
|
+
warnings.push(issue);
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
return {
|
|
4714
|
+
passed: errors.length === 0,
|
|
4715
|
+
errors,
|
|
4716
|
+
warnings
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
/**
|
|
4720
|
+
* Normalize dependency field to array of spec numbers
|
|
4721
|
+
*/
|
|
4722
|
+
normalizeDeps(deps) {
|
|
4723
|
+
if (!deps) return [];
|
|
4724
|
+
const depArray = Array.isArray(deps) ? deps : [deps];
|
|
4725
|
+
return depArray.map((d) => {
|
|
4726
|
+
const str = String(d);
|
|
4727
|
+
const match = str.match(/(\d{3})/);
|
|
4728
|
+
return match ? match[1] : str;
|
|
4729
|
+
}).filter(Boolean);
|
|
4730
|
+
}
|
|
4731
|
+
/**
|
|
4732
|
+
* Extract spec number from spec name (e.g., "045-unified-dashboard" -> "045")
|
|
4733
|
+
*/
|
|
4734
|
+
extractSpecNumber(specName) {
|
|
4735
|
+
const match = specName.match(/^(\d{3})/);
|
|
4736
|
+
return match ? match[1] : null;
|
|
4737
|
+
}
|
|
4738
|
+
/**
|
|
4739
|
+
* Detect spec references in content
|
|
4740
|
+
*/
|
|
4741
|
+
detectReferences(content, selfNumber) {
|
|
4742
|
+
const refs = [];
|
|
4743
|
+
const seenNumbers = /* @__PURE__ */ new Set();
|
|
4744
|
+
for (const pattern of DEPENDS_ON_PATTERNS) {
|
|
4745
|
+
const matches = content.matchAll(new RegExp(pattern));
|
|
4746
|
+
for (const match of matches) {
|
|
4747
|
+
const specNumber = match[1];
|
|
4748
|
+
if (specNumber && specNumber !== selfNumber && !seenNumbers.has(specNumber)) {
|
|
4749
|
+
seenNumbers.add(specNumber);
|
|
4750
|
+
refs.push({
|
|
4751
|
+
specNumber,
|
|
4752
|
+
type: "depends_on",
|
|
4753
|
+
context: match[0].substring(0, 50)
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
for (const pattern of RELATED_PATTERNS) {
|
|
4759
|
+
const matches = content.matchAll(new RegExp(pattern));
|
|
4760
|
+
for (const match of matches) {
|
|
4761
|
+
const specNumber = match[2] || match[1];
|
|
4762
|
+
if (specNumber && specNumber !== selfNumber && !seenNumbers.has(specNumber)) {
|
|
4763
|
+
seenNumbers.add(specNumber);
|
|
4764
|
+
refs.push({
|
|
4765
|
+
specNumber,
|
|
4766
|
+
type: "related",
|
|
4767
|
+
context: match[0].substring(0, 50)
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
for (const pattern of SPEC_REF_PATTERNS) {
|
|
4773
|
+
const matches = content.matchAll(new RegExp(pattern));
|
|
4774
|
+
for (const match of matches) {
|
|
4775
|
+
const specNumber = match[1];
|
|
4776
|
+
if (specNumber && specNumber !== selfNumber && !seenNumbers.has(specNumber)) {
|
|
4777
|
+
seenNumbers.add(specNumber);
|
|
4778
|
+
refs.push({
|
|
4779
|
+
specNumber,
|
|
4780
|
+
type: "related",
|
|
4781
|
+
// Default to related for ambiguous refs
|
|
4782
|
+
context: match[0].substring(0, 50)
|
|
4783
|
+
});
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
return refs;
|
|
4788
|
+
}
|
|
4789
|
+
};
|
|
4495
4790
|
function groupIssuesByFile(results) {
|
|
4496
4791
|
const fileMap = /* @__PURE__ */ new Map();
|
|
4497
4792
|
const addIssue = (filePath, issue, spec) => {
|
|
@@ -4679,7 +4974,7 @@ Validating ${specs.length} specs...
|
|
|
4679
4974
|
|
|
4680
4975
|
// src/commands/validate.ts
|
|
4681
4976
|
function validateCommand() {
|
|
4682
|
-
return new Command("validate").description("Validate specs for quality issues").argument("[specs...]", "Specific specs to validate (optional)").option("--max-lines <number>", "Custom line limit (default: 400)", parseInt).option("--verbose", "Show passing specs").option("--quiet", "Suppress warnings, only show errors").option("--format <format>", "Output format: default, json, compact", "default").option("--json", "Output as JSON (shorthand for --format json)").option("--rule <rule>", "Filter by specific rule name (e.g., max-lines, frontmatter)").option("--warnings-only", "Treat all issues as warnings, never fail (useful for CI pre-release checks)").action(async (specs, options) => {
|
|
4977
|
+
return new Command("validate").description("Validate specs for quality issues").argument("[specs...]", "Specific specs to validate (optional)").option("--max-lines <number>", "Custom line limit (default: 400)", parseInt).option("--verbose", "Show passing specs").option("--quiet", "Suppress warnings, only show errors").option("--format <format>", "Output format: default, json, compact", "default").option("--json", "Output as JSON (shorthand for --format json)").option("--rule <rule>", "Filter by specific rule name (e.g., max-lines, frontmatter)").option("--warnings-only", "Treat all issues as warnings, never fail (useful for CI pre-release checks)").option("--check-deps", "Check for content/frontmatter dependency alignment").action(async (specs, options) => {
|
|
4683
4978
|
const passed = await validateSpecs({
|
|
4684
4979
|
maxLines: options.maxLines,
|
|
4685
4980
|
specs: specs && specs.length > 0 ? specs : void 0,
|
|
@@ -4687,7 +4982,8 @@ function validateCommand() {
|
|
|
4687
4982
|
quiet: options.quiet,
|
|
4688
4983
|
format: options.json ? "json" : options.format,
|
|
4689
4984
|
rule: options.rule,
|
|
4690
|
-
warningsOnly: options.warningsOnly
|
|
4985
|
+
warningsOnly: options.warningsOnly,
|
|
4986
|
+
checkDeps: options.checkDeps
|
|
4691
4987
|
});
|
|
4692
4988
|
process.exit(passed ? 0 : 1);
|
|
4693
4989
|
});
|
|
@@ -4727,6 +5023,17 @@ async function validateSpecs(options = {}) {
|
|
|
4727
5023
|
new CorruptionValidator(),
|
|
4728
5024
|
new SubSpecValidator({ maxLines: options.maxLines })
|
|
4729
5025
|
];
|
|
5026
|
+
if (options.checkDeps) {
|
|
5027
|
+
const activeSpecs = await loadAllSpecs({ includeArchived: false });
|
|
5028
|
+
const existingSpecNumbers = /* @__PURE__ */ new Set();
|
|
5029
|
+
for (const s of activeSpecs) {
|
|
5030
|
+
const match = s.name.match(/^(\d{3})/);
|
|
5031
|
+
if (match) {
|
|
5032
|
+
existingSpecNumbers.add(match[1]);
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
validators.push(new DependencyAlignmentValidator({ existingSpecNumbers }));
|
|
5036
|
+
}
|
|
4730
5037
|
const results = [];
|
|
4731
5038
|
for (const spec of specs) {
|
|
4732
5039
|
let content;
|
|
@@ -7724,6 +8031,26 @@ function formatErrorMessage(prefix, error) {
|
|
|
7724
8031
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
7725
8032
|
return `${prefix}: ${errorMsg}`;
|
|
7726
8033
|
}
|
|
8034
|
+
function getStaleSpecs(board, thresholdDays = 7) {
|
|
8035
|
+
const now = /* @__PURE__ */ new Date();
|
|
8036
|
+
const staleSpecs = [];
|
|
8037
|
+
for (const spec of board.columns["in-progress"]) {
|
|
8038
|
+
const lastActivity = spec.updated_at || spec.created;
|
|
8039
|
+
if (!lastActivity) continue;
|
|
8040
|
+
try {
|
|
8041
|
+
const activityDate = new Date(lastActivity);
|
|
8042
|
+
const daysSinceActivity = Math.floor((now.getTime() - activityDate.getTime()) / (1e3 * 60 * 60 * 24));
|
|
8043
|
+
if (daysSinceActivity >= thresholdDays) {
|
|
8044
|
+
staleSpecs.push({
|
|
8045
|
+
name: spec.name,
|
|
8046
|
+
daysStale: daysSinceActivity
|
|
8047
|
+
});
|
|
8048
|
+
}
|
|
8049
|
+
} catch {
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
8052
|
+
return staleSpecs;
|
|
8053
|
+
}
|
|
7727
8054
|
function specToData(spec) {
|
|
7728
8055
|
return {
|
|
7729
8056
|
name: spec.name,
|
|
@@ -7735,7 +8062,8 @@ function specToData(spec) {
|
|
|
7735
8062
|
priority: spec.frontmatter.priority,
|
|
7736
8063
|
assignee: spec.frontmatter.assignee,
|
|
7737
8064
|
description: spec.frontmatter.description,
|
|
7738
|
-
customFields: spec.frontmatter.custom
|
|
8065
|
+
customFields: spec.frontmatter.custom,
|
|
8066
|
+
updated_at: spec.frontmatter.updated_at
|
|
7739
8067
|
};
|
|
7740
8068
|
}
|
|
7741
8069
|
function extractH1(content) {
|
|
@@ -7903,13 +8231,24 @@ function boardTool() {
|
|
|
7903
8231
|
description: "Get Kanban board view of all specs organized by status. Use this to visualize workflow, see what's in progress, or identify bottlenecks. Returns specs grouped into planned/in-progress/complete/archived columns.",
|
|
7904
8232
|
inputSchema: {},
|
|
7905
8233
|
outputSchema: {
|
|
7906
|
-
board: z.any()
|
|
8234
|
+
board: z.any(),
|
|
8235
|
+
warnings: z.array(z.string()).optional()
|
|
7907
8236
|
}
|
|
7908
8237
|
},
|
|
7909
8238
|
async (_input, _extra) => {
|
|
7910
8239
|
try {
|
|
7911
8240
|
const board = await getBoardData();
|
|
7912
|
-
const
|
|
8241
|
+
const staleSpecs = getStaleSpecs(board);
|
|
8242
|
+
const warnings = [];
|
|
8243
|
+
if (staleSpecs.length > 0) {
|
|
8244
|
+
for (const spec of staleSpecs) {
|
|
8245
|
+
warnings.push(`\u26A0\uFE0F Spec "${spec.name}" has been in-progress for ${spec.daysStale} days. Consider updating status.`);
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
const output = {
|
|
8249
|
+
board,
|
|
8250
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
8251
|
+
};
|
|
7913
8252
|
return {
|
|
7914
8253
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
7915
8254
|
structuredContent: output
|
|
@@ -8594,10 +8933,11 @@ function validateTool() {
|
|
|
8594
8933
|
"validate",
|
|
8595
8934
|
{
|
|
8596
8935
|
title: "Validate Specs",
|
|
8597
|
-
description: "Validate specifications for quality issues like excessive length, missing sections, or complexity problems. Use this before committing changes or for project health checks.",
|
|
8936
|
+
description: "Validate specifications for quality issues like excessive length, missing sections, or complexity problems. Use this before committing changes or for project health checks. Use checkDeps to detect content/frontmatter dependency misalignment.",
|
|
8598
8937
|
inputSchema: {
|
|
8599
8938
|
specs: z.array(z.string()).optional().describe("Specific specs to validate. If omitted, validates all specs in the project."),
|
|
8600
|
-
maxLines: z.number().optional().describe("Custom line limit for complexity checks (default: 400 lines).")
|
|
8939
|
+
maxLines: z.number().optional().describe("Custom line limit for complexity checks (default: 400 lines)."),
|
|
8940
|
+
checkDeps: z.boolean().optional().describe("Check for content/frontmatter dependency alignment. Detects when spec content references other specs but those references are not in frontmatter depends_on/related fields.")
|
|
8601
8941
|
},
|
|
8602
8942
|
outputSchema: {
|
|
8603
8943
|
passed: z.boolean(),
|
|
@@ -8618,7 +8958,8 @@ function validateTool() {
|
|
|
8618
8958
|
};
|
|
8619
8959
|
const passed = await validateSpecs({
|
|
8620
8960
|
maxLines: input.maxLines,
|
|
8621
|
-
specs: input.specs
|
|
8961
|
+
specs: input.specs,
|
|
8962
|
+
checkDeps: input.checkDeps
|
|
8622
8963
|
});
|
|
8623
8964
|
const output = {
|
|
8624
8965
|
passed,
|
|
@@ -8968,6 +9309,132 @@ Provide a clear, actionable summary that helps understand project health and nex
|
|
|
8968
9309
|
})
|
|
8969
9310
|
];
|
|
8970
9311
|
}
|
|
9312
|
+
|
|
9313
|
+
// src/mcp/prompts/sdd-checkpoint.ts
|
|
9314
|
+
function sddCheckpointPrompt() {
|
|
9315
|
+
return [
|
|
9316
|
+
"checkpoint",
|
|
9317
|
+
{
|
|
9318
|
+
title: "SDD Compliance Checkpoint",
|
|
9319
|
+
description: "Periodic reminder to verify SDD workflow compliance - check spec status, update progress, ensure specs are in sync"
|
|
9320
|
+
},
|
|
9321
|
+
() => ({
|
|
9322
|
+
messages: [
|
|
9323
|
+
{
|
|
9324
|
+
role: "user",
|
|
9325
|
+
content: {
|
|
9326
|
+
type: "text",
|
|
9327
|
+
text: `## SDD Checkpoint \u2705
|
|
9328
|
+
|
|
9329
|
+
**Before continuing, let's verify SDD compliance:**
|
|
9330
|
+
|
|
9331
|
+
### Step 1: Review Current State
|
|
9332
|
+
Use the \`board\` tool to see project status and identify:
|
|
9333
|
+
- Specs marked "in-progress" - are they still being worked on?
|
|
9334
|
+
- Specs that should be "complete" but aren't marked
|
|
9335
|
+
- Any work being done without a spec
|
|
9336
|
+
|
|
9337
|
+
### Step 2: Check Your Current Task
|
|
9338
|
+
For the work you're currently doing:
|
|
9339
|
+
- **Is there a spec for it?** If not, create one with \`create\`
|
|
9340
|
+
- **Is the status correct?** Update with \`update\` if needed
|
|
9341
|
+
- **Have you documented decisions?** Add notes to the spec
|
|
9342
|
+
|
|
9343
|
+
### Step 3: Update Progress
|
|
9344
|
+
For any specs you've worked on:
|
|
9345
|
+
1. Update status if changed (\`planned\` \u2192 \`in-progress\` \u2192 \`complete\`)
|
|
9346
|
+
2. Document key decisions or changes in the spec content
|
|
9347
|
+
3. Link related specs if you discovered connections
|
|
9348
|
+
|
|
9349
|
+
### Action Items
|
|
9350
|
+
Based on the board review:
|
|
9351
|
+
1. List any specs with stale status
|
|
9352
|
+
2. Identify work being done without specs
|
|
9353
|
+
3. Suggest status updates needed
|
|
9354
|
+
|
|
9355
|
+
**Remember:**
|
|
9356
|
+
- Specs track implementation, not documentation
|
|
9357
|
+
- Update status BEFORE starting work, AFTER completing
|
|
9358
|
+
- Keep specs in sync with actual progress`
|
|
9359
|
+
}
|
|
9360
|
+
}
|
|
9361
|
+
]
|
|
9362
|
+
})
|
|
9363
|
+
];
|
|
9364
|
+
}
|
|
9365
|
+
|
|
9366
|
+
// src/mcp/prompts/spec-creation-workflow.ts
|
|
9367
|
+
function specCreationWorkflowPrompt() {
|
|
9368
|
+
return [
|
|
9369
|
+
"create-spec",
|
|
9370
|
+
{
|
|
9371
|
+
title: "Create Spec with Dependencies",
|
|
9372
|
+
description: "Complete workflow for creating a new spec including proper dependency linking. Prevents the common issue of content referencing specs without frontmatter links."
|
|
9373
|
+
},
|
|
9374
|
+
() => ({
|
|
9375
|
+
messages: [
|
|
9376
|
+
{
|
|
9377
|
+
role: "user",
|
|
9378
|
+
content: {
|
|
9379
|
+
type: "text",
|
|
9380
|
+
text: `## Create Spec Workflow \u{1F4DD}
|
|
9381
|
+
|
|
9382
|
+
Follow these steps to create a well-linked spec:
|
|
9383
|
+
|
|
9384
|
+
### Step 1: Pre-Creation Research
|
|
9385
|
+
Before creating, use \`search\` to find related specs:
|
|
9386
|
+
- Search for similar features or components
|
|
9387
|
+
- Identify potential dependencies
|
|
9388
|
+
- Note specs to reference
|
|
9389
|
+
|
|
9390
|
+
### Step 2: Create the Spec
|
|
9391
|
+
Use \`create\` with the spec details:
|
|
9392
|
+
\`\`\`
|
|
9393
|
+
create {
|
|
9394
|
+
"name": "your-spec-name",
|
|
9395
|
+
"title": "Human Readable Title",
|
|
9396
|
+
"description": "Initial overview content...",
|
|
9397
|
+
"priority": "medium",
|
|
9398
|
+
"tags": ["relevant", "tags"]
|
|
9399
|
+
}
|
|
9400
|
+
\`\`\`
|
|
9401
|
+
|
|
9402
|
+
### Step 3: Link Dependencies (CRITICAL)
|
|
9403
|
+
After creating, **immediately** link any referenced specs:
|
|
9404
|
+
|
|
9405
|
+
For each spec mentioned in content:
|
|
9406
|
+
- "Depends on spec 045" \u2192 \`link { "spec": "your-spec", "dependsOn": ["045"] }\`
|
|
9407
|
+
- "Related to spec 072" \u2192 \`link { "spec": "your-spec", "related": ["072"] }\`
|
|
9408
|
+
- "See spec 110" \u2192 \`link { "spec": "your-spec", "related": ["110"] }\`
|
|
9409
|
+
|
|
9410
|
+
### Step 4: Verify
|
|
9411
|
+
Use \`deps\` to verify all links are in place:
|
|
9412
|
+
\`\`\`
|
|
9413
|
+
deps { "spec": "your-spec" }
|
|
9414
|
+
\`\`\`
|
|
9415
|
+
|
|
9416
|
+
### Step 5: Validate
|
|
9417
|
+
Run dependency alignment check:
|
|
9418
|
+
\`\`\`
|
|
9419
|
+
validate { "specs": ["your-spec"], "checkDeps": true }
|
|
9420
|
+
\`\`\`
|
|
9421
|
+
|
|
9422
|
+
### Common Patterns to Link
|
|
9423
|
+
|
|
9424
|
+
| Content Pattern | Link Type |
|
|
9425
|
+
|----------------|-----------|
|
|
9426
|
+
| "depends on", "blocked by", "requires" | dependsOn |
|
|
9427
|
+
| "related to", "see also", "similar to" | related |
|
|
9428
|
+
| "builds on" | dependsOn (if blocking) or related |
|
|
9429
|
+
| "## Related Specs" section | related (link each one) |
|
|
9430
|
+
|
|
9431
|
+
**Remember:** Content and frontmatter must stay aligned!`
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
]
|
|
9435
|
+
})
|
|
9436
|
+
];
|
|
9437
|
+
}
|
|
8971
9438
|
function updateSpecStatusPrompt() {
|
|
8972
9439
|
return [
|
|
8973
9440
|
"update-spec-status",
|
|
@@ -8999,6 +9466,8 @@ Use the \`update\` tool: \`update <spec> --status ${status}\``
|
|
|
8999
9466
|
function registerPrompts(server) {
|
|
9000
9467
|
server.registerPrompt(...projectProgressOverviewPrompt());
|
|
9001
9468
|
server.registerPrompt(...planProjectRoadmapPrompt());
|
|
9469
|
+
server.registerPrompt(...sddCheckpointPrompt());
|
|
9470
|
+
server.registerPrompt(...specCreationWorkflowPrompt());
|
|
9002
9471
|
server.registerPrompt(...updateSpecStatusPrompt());
|
|
9003
9472
|
}
|
|
9004
9473
|
|
|
@@ -9034,5 +9503,5 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
9034
9503
|
}
|
|
9035
9504
|
|
|
9036
9505
|
export { analyzeCommand, archiveCommand, backfillCommand, boardCommand, checkCommand, compactCommand, createCommand, createMcpServer, depsCommand, examplesCommand, filesCommand, ganttCommand, initCommand, linkCommand, listCommand, mcpCommand, migrateCommand, openCommand, searchCommand, splitCommand, statsCommand, templatesCommand, timelineCommand, tokensCommand, uiCommand, unlinkCommand, updateCommand, validateCommand, viewCommand };
|
|
9037
|
-
//# sourceMappingURL=chunk-
|
|
9038
|
-
//# sourceMappingURL=chunk-
|
|
9506
|
+
//# sourceMappingURL=chunk-IXCZPYB7.js.map
|
|
9507
|
+
//# sourceMappingURL=chunk-IXCZPYB7.js.map
|