Skip to content

Commit c96cdca

Browse files
committed
feat: add stale spec warnings to MCP board tool
- Add getStaleSpecs helper to detect specs in-progress for > 7 days - Add updated_at to SpecData type for staleness calculation - Board tool now returns warnings array with stale spec alerts - Helps enforce SDD workflow compliance Part of spec 121: MCP-First Agent Experience
1 parent b9d26a9 commit c96cdca

File tree

23 files changed

+641
-73
lines changed

23 files changed

+641
-73
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ Hard dependency - spec cannot start until dependencies complete.
188188
- Never leave specs with stale status
189189
- **Always validate before completing work:**
190190
- Run `node bin/lean-spec.js validate` to check spec structure and frontmatter (use local build, not `npx`)
191+
- Run `node bin/lean-spec.js validate --check-deps` to verify content/frontmatter dependency alignment
191192
- Run `cd docs-site && npm run build` to ensure documentation site builds successfully
192193
- Update spec status to `complete` with `lean-spec update <spec> --status complete`
193194
- Fix any validation errors or build failures before marking work complete

packages/cli/src/commands/validate.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { StructureValidator } from '../validators/structure.js';
2121
import { CorruptionValidator } from '../validators/corruption.js';
2222
import { SubSpecValidator } from '../validators/sub-spec.js';
2323
import { ComplexityValidator } from '../validators/complexity.js';
24+
import { DependencyAlignmentValidator } from '../validators/dependency-alignment.js';
2425
import type { ValidationRule, ValidationResult } from '../utils/validation-framework.js';
2526
import { formatValidationResults, type FormatOptions } from '../utils/validate-formatter.js';
2627

@@ -33,6 +34,7 @@ export interface ValidateOptions {
3334
json?: boolean; // Shorthand for --format json
3435
rule?: string; // Filter by specific rule name
3536
warningsOnly?: boolean; // Treat all issues as warnings, never fail (useful for CI)
37+
checkDeps?: boolean; // Include dependency alignment check
3638
}
3739

3840
interface ValidationResultWithSpec {
@@ -56,6 +58,7 @@ export function validateCommand(): Command {
5658
.option('--json', 'Output as JSON (shorthand for --format json)')
5759
.option('--rule <rule>', 'Filter by specific rule name (e.g., max-lines, frontmatter)')
5860
.option('--warnings-only', 'Treat all issues as warnings, never fail (useful for CI pre-release checks)')
61+
.option('--check-deps', 'Check for content/frontmatter dependency alignment')
5962
.action(async (specs: string[] | undefined, options: ValidateOptions) => {
6063
const passed = await validateSpecs({
6164
maxLines: options.maxLines,
@@ -65,6 +68,7 @@ export function validateCommand(): Command {
6568
format: options.json ? 'json' : options.format,
6669
rule: options.rule,
6770
warningsOnly: options.warningsOnly,
71+
checkDeps: options.checkDeps,
6872
});
6973
process.exit(passed ? 0 : 1);
7074
});
@@ -116,6 +120,20 @@ export async function validateSpecs(options: ValidateOptions = {}): Promise<bool
116120
new SubSpecValidator({ maxLines: options.maxLines }),
117121
];
118122

123+
// Add dependency alignment validator if requested
124+
if (options.checkDeps) {
125+
// Collect existing spec numbers for validation (only active specs, not archived)
126+
const activeSpecs = await loadAllSpecs({ includeArchived: false });
127+
const existingSpecNumbers = new Set<string>();
128+
for (const s of activeSpecs) {
129+
const match = s.name.match(/^(\d{3})/);
130+
if (match) {
131+
existingSpecNumbers.add(match[1]);
132+
}
133+
}
134+
validators.push(new DependencyAlignmentValidator({ existingSpecNumbers }));
135+
}
136+
119137
// Run validation
120138
const results: ValidationResultWithSpec[] = [];
121139

packages/cli/src/mcp/helpers.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import * as fs from 'node:fs/promises';
66
import { countTokens } from '@leanspec/core';
77
import { loadSubFiles } from '../spec-loader.js';
8-
import type { SpecData, SubSpecReference } from './types.js';
8+
import type { SpecData, SubSpecReference, BoardData } from './types.js';
99

1010
/**
1111
* Format error messages for MCP responses
@@ -15,6 +15,45 @@ export function formatErrorMessage(prefix: string, error: unknown): string {
1515
return `${prefix}: ${errorMsg}`;
1616
}
1717

18+
/**
19+
* Stale spec information
20+
*/
21+
export interface StaleSpec {
22+
name: string;
23+
daysStale: number;
24+
}
25+
26+
/**
27+
* Get specs that have been in-progress for too long
28+
* Default threshold: 7 days
29+
*/
30+
export function getStaleSpecs(board: BoardData, thresholdDays = 7): StaleSpec[] {
31+
const now = new Date();
32+
const staleSpecs: StaleSpec[] = [];
33+
34+
for (const spec of board.columns['in-progress']) {
35+
// Check updated_at first, then fall back to created date
36+
const lastActivity = spec.updated_at || spec.created;
37+
if (!lastActivity) continue;
38+
39+
try {
40+
const activityDate = new Date(lastActivity);
41+
const daysSinceActivity = Math.floor((now.getTime() - activityDate.getTime()) / (1000 * 60 * 60 * 24));
42+
43+
if (daysSinceActivity >= thresholdDays) {
44+
staleSpecs.push({
45+
name: spec.name,
46+
daysStale: daysSinceActivity,
47+
});
48+
}
49+
} catch {
50+
// Invalid date format, skip
51+
}
52+
}
53+
54+
return staleSpecs;
55+
}
56+
1857
/**
1958
* Convert spec info to serializable SpecData format
2059
*/
@@ -30,6 +69,7 @@ export function specToData(spec: any): SpecData {
3069
assignee: spec.frontmatter.assignee,
3170
description: spec.frontmatter.description,
3271
customFields: spec.frontmatter.custom,
72+
updated_at: spec.frontmatter.updated_at,
3373
};
3474
}
3575

packages/cli/src/mcp/prompts/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
66
import { planProjectRoadmapPrompt } from './plan-project-roadmap.js';
77
import { projectProgressOverviewPrompt } from './project-progress-overview.js';
88
import { sddCheckpointPrompt } from './sdd-checkpoint.js';
9+
import { specCreationWorkflowPrompt } from './spec-creation-workflow.js';
910
import { updateSpecStatusPrompt } from './update-spec-status.js';
1011

1112
/**
@@ -15,5 +16,6 @@ export function registerPrompts(server: McpServer): void {
1516
server.registerPrompt(...projectProgressOverviewPrompt());
1617
server.registerPrompt(...planProjectRoadmapPrompt());
1718
server.registerPrompt(...sddCheckpointPrompt());
19+
server.registerPrompt(...specCreationWorkflowPrompt());
1820
server.registerPrompt(...updateSpecStatusPrompt());
1921
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Spec Creation Workflow prompt - Guide complete spec creation with dependency linking
3+
*/
4+
5+
/**
6+
* Spec Creation Workflow prompt definition
7+
* Use this when creating new specs to ensure proper dependency linking
8+
*/
9+
export function specCreationWorkflowPrompt() {
10+
return [
11+
'create-spec',
12+
{
13+
title: 'Create Spec with Dependencies',
14+
description: 'Complete workflow for creating a new spec including proper dependency linking. Prevents the common issue of content referencing specs without frontmatter links.',
15+
},
16+
() => ({
17+
messages: [
18+
{
19+
role: 'user' as const,
20+
content: {
21+
type: 'text' as const,
22+
text: `## Create Spec Workflow 📝
23+
24+
Follow these steps to create a well-linked spec:
25+
26+
### Step 1: Pre-Creation Research
27+
Before creating, use \`search\` to find related specs:
28+
- Search for similar features or components
29+
- Identify potential dependencies
30+
- Note specs to reference
31+
32+
### Step 2: Create the Spec
33+
Use \`create\` with the spec details:
34+
\`\`\`
35+
create {
36+
"name": "your-spec-name",
37+
"title": "Human Readable Title",
38+
"description": "Initial overview content...",
39+
"priority": "medium",
40+
"tags": ["relevant", "tags"]
41+
}
42+
\`\`\`
43+
44+
### Step 3: Link Dependencies (CRITICAL)
45+
After creating, **immediately** link any referenced specs:
46+
47+
For each spec mentioned in content:
48+
- "Depends on spec 045" → \`link { "spec": "your-spec", "dependsOn": ["045"] }\`
49+
- "Related to spec 072" → \`link { "spec": "your-spec", "related": ["072"] }\`
50+
- "See spec 110" → \`link { "spec": "your-spec", "related": ["110"] }\`
51+
52+
### Step 4: Verify
53+
Use \`deps\` to verify all links are in place:
54+
\`\`\`
55+
deps { "spec": "your-spec" }
56+
\`\`\`
57+
58+
### Step 5: Validate
59+
Run dependency alignment check:
60+
\`\`\`
61+
validate { "specs": ["your-spec"], "checkDeps": true }
62+
\`\`\`
63+
64+
### Common Patterns to Link
65+
66+
| Content Pattern | Link Type |
67+
|----------------|-----------|
68+
| "depends on", "blocked by", "requires" | dependsOn |
69+
| "related to", "see also", "similar to" | related |
70+
| "builds on" | dependsOn (if blocking) or related |
71+
| "## Related Specs" section | related (link each one) |
72+
73+
**Remember:** Content and frontmatter must stay aligned!`,
74+
},
75+
},
76+
],
77+
})
78+
] as const;
79+
}

packages/cli/src/mcp/tools/board.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { z } from 'zod';
66
import { loadAllSpecs } from '../../spec-loader.js';
7-
import { formatErrorMessage, specToData } from '../helpers.js';
7+
import { formatErrorMessage, specToData, getStaleSpecs } from '../helpers.js';
88
import type { ToolDefinition, BoardData } from '../types.js';
99

1010
/**
@@ -41,12 +41,28 @@ export function boardTool(): ToolDefinition {
4141
inputSchema: {},
4242
outputSchema: {
4343
board: z.any(),
44+
warnings: z.array(z.string()).optional(),
4445
},
4546
},
4647
async (_input, _extra) => {
4748
try {
4849
const board = await getBoardData();
49-
const output = { board };
50+
51+
// Check for stale specs (in-progress for > 7 days)
52+
const staleSpecs = getStaleSpecs(board);
53+
const warnings: string[] = [];
54+
55+
if (staleSpecs.length > 0) {
56+
for (const spec of staleSpecs) {
57+
warnings.push(`⚠️ Spec "${spec.name}" has been in-progress for ${spec.daysStale} days. Consider updating status.`);
58+
}
59+
}
60+
61+
const output = {
62+
board,
63+
...(warnings.length > 0 ? { warnings } : {}),
64+
};
65+
5066
return {
5167
content: [{ type: 'text' as const, text: JSON.stringify(output, null, 2) }],
5268
structuredContent: output,

packages/cli/src/mcp/tools/validate.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ export function validateTool(): ToolDefinition {
1515
'validate',
1616
{
1717
title: 'Validate Specs',
18-
description: 'Validate specifications for quality issues like excessive length, missing sections, or complexity problems. Use this before committing changes or for project health checks.',
18+
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.',
1919
inputSchema: {
2020
specs: z.array(z.string()).optional().describe('Specific specs to validate. If omitted, validates all specs in the project.'),
2121
maxLines: z.number().optional().describe('Custom line limit for complexity checks (default: 400 lines).'),
22+
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.'),
2223
},
2324
outputSchema: {
2425
passed: z.boolean(),
@@ -41,6 +42,7 @@ export function validateTool(): ToolDefinition {
4142
const passed = await validateSpecs({
4243
maxLines: input.maxLines,
4344
specs: input.specs,
45+
checkDeps: input.checkDeps,
4446
});
4547

4648
const output = {

packages/cli/src/mcp/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type SpecData = {
3232
description?: string;
3333
customFields?: Record<string, unknown>;
3434
subSpecs?: SubSpecReference[]; // Only when viewing main spec
35+
updated_at?: string; // ISO timestamp for stale detection
3536
};
3637

3738
/**

0 commit comments

Comments
 (0)