lean-spec 0.2.6-dev.20251126022840 → 0.2.6-dev.20251126033915

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.
@@ -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 output = { board };
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
@@ -7989,7 +8328,8 @@ function createTool() {
7989
8328
  outputSchema: {
7990
8329
  success: z.boolean(),
7991
8330
  path: z.string(),
7992
- message: z.string()
8331
+ message: z.string(),
8332
+ reminder: z.string().optional()
7993
8333
  }
7994
8334
  },
7995
8335
  async (input, _extra) => {
@@ -8010,7 +8350,8 @@ function createTool() {
8010
8350
  const output = {
8011
8351
  success: true,
8012
8352
  path: capturedOutput.includes("Created:") ? capturedOutput.split("Created:")[1].split("\n")[0].trim() : "",
8013
- message: `Spec '${input.name}' created successfully`
8353
+ message: `Spec '${input.name}' created successfully`,
8354
+ reminder: "\u{1F4A1} Remember to update status to 'in-progress' when you start implementing! Use: update tool with status='in-progress'"
8014
8355
  };
8015
8356
  return {
8016
8357
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
@@ -8245,7 +8586,8 @@ function listTool() {
8245
8586
  includeArchived: z.boolean().optional().describe("Include archived specs in results (default: false). Set to true to see completed/archived work.")
8246
8587
  },
8247
8588
  outputSchema: {
8248
- specs: z.array(z.any())
8589
+ specs: z.array(z.any()),
8590
+ warnings: z.array(z.string()).optional()
8249
8591
  }
8250
8592
  },
8251
8593
  async (input, _extra) => {
@@ -8257,7 +8599,26 @@ function listTool() {
8257
8599
  assignee: input.assignee,
8258
8600
  includeArchived: input.includeArchived
8259
8601
  });
8260
- const output = { specs };
8602
+ const warnings = [];
8603
+ if (!input.status) {
8604
+ const inProgressSpecs = specs.filter((s) => s.status === "in-progress");
8605
+ const mockBoard = {
8606
+ columns: {
8607
+ planned: [],
8608
+ "in-progress": inProgressSpecs,
8609
+ complete: [],
8610
+ archived: []
8611
+ }
8612
+ };
8613
+ const staleSpecs = getStaleSpecs(mockBoard);
8614
+ for (const spec of staleSpecs) {
8615
+ warnings.push(`\u26A0\uFE0F Spec "${spec.name}" has been in-progress for ${spec.daysStale} days. Consider updating status.`);
8616
+ }
8617
+ }
8618
+ const output = {
8619
+ specs,
8620
+ ...warnings.length > 0 ? { warnings } : {}
8621
+ };
8261
8622
  return {
8262
8623
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
8263
8624
  structuredContent: output
@@ -8535,6 +8896,18 @@ function tokensTool() {
8535
8896
  }
8536
8897
  ];
8537
8898
  }
8899
+ function getStatusReminder(newStatus) {
8900
+ switch (newStatus) {
8901
+ case "in-progress":
8902
+ return "\u{1F4A1} Spec marked in-progress. Remember to document decisions in the spec as you work, and update to 'complete' when done!";
8903
+ case "complete":
8904
+ return "\u2705 Spec marked complete! Consider: Did you document what you learned? Are there follow-up specs needed?";
8905
+ case "planned":
8906
+ return "\u{1F4CB} Spec marked as planned. Update to 'in-progress' before you start implementing!";
8907
+ default:
8908
+ return void 0;
8909
+ }
8910
+ }
8538
8911
  function updateTool() {
8539
8912
  return [
8540
8913
  "update",
@@ -8550,7 +8923,8 @@ function updateTool() {
8550
8923
  },
8551
8924
  outputSchema: {
8552
8925
  success: z.boolean(),
8553
- message: z.string()
8926
+ message: z.string(),
8927
+ reminder: z.string().optional()
8554
8928
  }
8555
8929
  },
8556
8930
  async (input, _extra) => {
@@ -8566,9 +8940,11 @@ function updateTool() {
8566
8940
  if (input.tags !== void 0) updates.tags = input.tags;
8567
8941
  if (input.assignee !== void 0) updates.assignee = input.assignee;
8568
8942
  await updateSpec(input.specPath, updates);
8943
+ const reminder = getStatusReminder(input.status);
8569
8944
  const output = {
8570
8945
  success: true,
8571
- message: `Spec updated successfully`
8946
+ message: `Spec updated successfully`,
8947
+ ...reminder ? { reminder } : {}
8572
8948
  };
8573
8949
  return {
8574
8950
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
@@ -8594,10 +8970,11 @@ function validateTool() {
8594
8970
  "validate",
8595
8971
  {
8596
8972
  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.",
8973
+ 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
8974
  inputSchema: {
8599
8975
  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).")
8976
+ maxLines: z.number().optional().describe("Custom line limit for complexity checks (default: 400 lines)."),
8977
+ 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
8978
  },
8602
8979
  outputSchema: {
8603
8980
  passed: z.boolean(),
@@ -8618,7 +8995,8 @@ function validateTool() {
8618
8995
  };
8619
8996
  const passed = await validateSpecs({
8620
8997
  maxLines: input.maxLines,
8621
- specs: input.specs
8998
+ specs: input.specs,
8999
+ checkDeps: input.checkDeps
8622
9000
  });
8623
9001
  const output = {
8624
9002
  passed,
@@ -8968,6 +9346,132 @@ Provide a clear, actionable summary that helps understand project health and nex
8968
9346
  })
8969
9347
  ];
8970
9348
  }
9349
+
9350
+ // src/mcp/prompts/sdd-checkpoint.ts
9351
+ function sddCheckpointPrompt() {
9352
+ return [
9353
+ "checkpoint",
9354
+ {
9355
+ title: "SDD Compliance Checkpoint",
9356
+ description: "Periodic reminder to verify SDD workflow compliance - check spec status, update progress, ensure specs are in sync"
9357
+ },
9358
+ () => ({
9359
+ messages: [
9360
+ {
9361
+ role: "user",
9362
+ content: {
9363
+ type: "text",
9364
+ text: `## SDD Checkpoint \u2705
9365
+
9366
+ **Before continuing, let's verify SDD compliance:**
9367
+
9368
+ ### Step 1: Review Current State
9369
+ Use the \`board\` tool to see project status and identify:
9370
+ - Specs marked "in-progress" - are they still being worked on?
9371
+ - Specs that should be "complete" but aren't marked
9372
+ - Any work being done without a spec
9373
+
9374
+ ### Step 2: Check Your Current Task
9375
+ For the work you're currently doing:
9376
+ - **Is there a spec for it?** If not, create one with \`create\`
9377
+ - **Is the status correct?** Update with \`update\` if needed
9378
+ - **Have you documented decisions?** Add notes to the spec
9379
+
9380
+ ### Step 3: Update Progress
9381
+ For any specs you've worked on:
9382
+ 1. Update status if changed (\`planned\` \u2192 \`in-progress\` \u2192 \`complete\`)
9383
+ 2. Document key decisions or changes in the spec content
9384
+ 3. Link related specs if you discovered connections
9385
+
9386
+ ### Action Items
9387
+ Based on the board review:
9388
+ 1. List any specs with stale status
9389
+ 2. Identify work being done without specs
9390
+ 3. Suggest status updates needed
9391
+
9392
+ **Remember:**
9393
+ - Specs track implementation, not documentation
9394
+ - Update status BEFORE starting work, AFTER completing
9395
+ - Keep specs in sync with actual progress`
9396
+ }
9397
+ }
9398
+ ]
9399
+ })
9400
+ ];
9401
+ }
9402
+
9403
+ // src/mcp/prompts/spec-creation-workflow.ts
9404
+ function specCreationWorkflowPrompt() {
9405
+ return [
9406
+ "create-spec",
9407
+ {
9408
+ title: "Create Spec with Dependencies",
9409
+ description: "Complete workflow for creating a new spec including proper dependency linking. Prevents the common issue of content referencing specs without frontmatter links."
9410
+ },
9411
+ () => ({
9412
+ messages: [
9413
+ {
9414
+ role: "user",
9415
+ content: {
9416
+ type: "text",
9417
+ text: `## Create Spec Workflow \u{1F4DD}
9418
+
9419
+ Follow these steps to create a well-linked spec:
9420
+
9421
+ ### Step 1: Pre-Creation Research
9422
+ Before creating, use \`search\` to find related specs:
9423
+ - Search for similar features or components
9424
+ - Identify potential dependencies
9425
+ - Note specs to reference
9426
+
9427
+ ### Step 2: Create the Spec
9428
+ Use \`create\` with the spec details:
9429
+ \`\`\`
9430
+ create {
9431
+ "name": "your-spec-name",
9432
+ "title": "Human Readable Title",
9433
+ "description": "Initial overview content...",
9434
+ "priority": "medium",
9435
+ "tags": ["relevant", "tags"]
9436
+ }
9437
+ \`\`\`
9438
+
9439
+ ### Step 3: Link Dependencies (CRITICAL)
9440
+ After creating, **immediately** link any referenced specs:
9441
+
9442
+ For each spec mentioned in content:
9443
+ - "Depends on spec 045" \u2192 \`link { "spec": "your-spec", "dependsOn": ["045"] }\`
9444
+ - "Related to spec 072" \u2192 \`link { "spec": "your-spec", "related": ["072"] }\`
9445
+ - "See spec 110" \u2192 \`link { "spec": "your-spec", "related": ["110"] }\`
9446
+
9447
+ ### Step 4: Verify
9448
+ Use \`deps\` to verify all links are in place:
9449
+ \`\`\`
9450
+ deps { "spec": "your-spec" }
9451
+ \`\`\`
9452
+
9453
+ ### Step 5: Validate
9454
+ Run dependency alignment check:
9455
+ \`\`\`
9456
+ validate { "specs": ["your-spec"], "checkDeps": true }
9457
+ \`\`\`
9458
+
9459
+ ### Common Patterns to Link
9460
+
9461
+ | Content Pattern | Link Type |
9462
+ |----------------|-----------|
9463
+ | "depends on", "blocked by", "requires" | dependsOn |
9464
+ | "related to", "see also", "similar to" | related |
9465
+ | "builds on" | dependsOn (if blocking) or related |
9466
+ | "## Related Specs" section | related (link each one) |
9467
+
9468
+ **Remember:** Content and frontmatter must stay aligned!`
9469
+ }
9470
+ }
9471
+ ]
9472
+ })
9473
+ ];
9474
+ }
8971
9475
  function updateSpecStatusPrompt() {
8972
9476
  return [
8973
9477
  "update-spec-status",
@@ -8999,6 +9503,8 @@ Use the \`update\` tool: \`update <spec> --status ${status}\``
8999
9503
  function registerPrompts(server) {
9000
9504
  server.registerPrompt(...projectProgressOverviewPrompt());
9001
9505
  server.registerPrompt(...planProjectRoadmapPrompt());
9506
+ server.registerPrompt(...sddCheckpointPrompt());
9507
+ server.registerPrompt(...specCreationWorkflowPrompt());
9002
9508
  server.registerPrompt(...updateSpecStatusPrompt());
9003
9509
  }
9004
9510
 
@@ -9034,5 +9540,5 @@ if (import.meta.url === `file://${process.argv[1]}`) {
9034
9540
  }
9035
9541
 
9036
9542
  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-RTEGSMVL.js.map
9038
- //# sourceMappingURL=chunk-RTEGSMVL.js.map
9543
+ //# sourceMappingURL=chunk-VWR4YMIR.js.map
9544
+ //# sourceMappingURL=chunk-VWR4YMIR.js.map