Skip to content

Commit d0aa31b

Browse files
committed
Merge PR #7: Plan automated spec generation feature
Resolved conflict in mcp-server/src/index.ts: - Kept both F008 (roadmap generation) and F002 (automated spec generation) tools - Added 5 new MCP tools for automated spec generation: - stackshift_generate_all_specs (full automation) - stackshift_create_constitution (constitution.md from specs) - stackshift_create_feature_specs (extract and create feature specs) - stackshift_create_impl_plans (implementation plans for gaps) Adds automated spec generation feature with: - Markdown parser for extracting structure from functional specs - Spec generator with template engine - File writer utilities with atomic operations - Complete F002 specification and implementation plan - Comprehensive test coverage for new utilities
2 parents e7a962b + 1e7ab23 commit d0aa31b

28 files changed

+12014
-0
lines changed

mcp-server/src/index.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import { completeSpecToolHandler } from './tools/complete-spec.js';
2424
import { implementToolHandler } from './tools/implement.js';
2525
import { cruiseControlToolHandler } from './tools/cruise-control.js';
2626
import { generateRoadmapToolHandler } from './tools/generate-roadmap.js';
27+
import { generateAllSpecsToolHandler } from './tools/generate-all-specs.js';
28+
import { createConstitutionToolHandler } from './tools/create-constitution.js';
29+
import { createFeatureSpecsToolHandler } from './tools/create-feature-specs.js';
30+
import { createImplPlansToolHandler } from './tools/create-impl-plans.js';
2731
import { getStateResource, getProgressResource, getRouteResource } from './resources/index.js';
2832

2933
const server = new Server(
@@ -211,6 +215,86 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
211215
},
212216
},
213217
},
218+
{
219+
name: 'stackshift_generate_all_specs',
220+
description:
221+
'F002: Automated Spec Generation - Generate constitution, feature specs, and implementation plans automatically',
222+
inputSchema: {
223+
type: 'object',
224+
properties: {
225+
directory: {
226+
type: 'string',
227+
description: 'Path to project directory',
228+
},
229+
route: {
230+
type: 'string',
231+
enum: ['greenfield', 'brownfield'],
232+
description: 'Route choice (optional if already set via analyze)',
233+
},
234+
},
235+
},
236+
},
237+
{
238+
name: 'stackshift_create_constitution',
239+
description:
240+
'F002: Generate constitution.md from functional specification',
241+
inputSchema: {
242+
type: 'object',
243+
properties: {
244+
directory: {
245+
type: 'string',
246+
description: 'Path to project directory',
247+
},
248+
route: {
249+
type: 'string',
250+
enum: ['greenfield', 'brownfield'],
251+
description: 'Route choice',
252+
},
253+
outputPath: {
254+
type: 'string',
255+
description: 'Custom output path (default: .specify/memory/constitution.md)',
256+
},
257+
},
258+
},
259+
},
260+
{
261+
name: 'stackshift_create_feature_specs',
262+
description:
263+
'F002: Extract features and generate individual spec files',
264+
inputSchema: {
265+
type: 'object',
266+
properties: {
267+
directory: {
268+
type: 'string',
269+
description: 'Path to project directory',
270+
},
271+
route: {
272+
type: 'string',
273+
enum: ['greenfield', 'brownfield'],
274+
description: 'Route choice',
275+
},
276+
},
277+
},
278+
},
279+
{
280+
name: 'stackshift_create_impl_plans',
281+
description:
282+
'F002: Generate implementation plans for PARTIAL and MISSING features',
283+
inputSchema: {
284+
type: 'object',
285+
properties: {
286+
directory: {
287+
type: 'string',
288+
description: 'Path to project directory',
289+
},
290+
route: {
291+
type: 'string',
292+
enum: ['greenfield', 'brownfield'],
293+
description: 'Route choice',
294+
},
295+
},
296+
},
297+
},
214298
],
215299
};
216300
});
@@ -275,6 +359,18 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
275359
case 'stackshift_generate_roadmap':
276360
return await generateRoadmapToolHandler(args || {});
277361

362+
case 'stackshift_generate_all_specs':
363+
return await generateAllSpecsToolHandler(args || {});
364+
365+
case 'stackshift_create_constitution':
366+
return await createConstitutionToolHandler(args || {});
367+
368+
case 'stackshift_create_feature_specs':
369+
return await createFeatureSpecsToolHandler(args || {});
370+
371+
case 'stackshift_create_impl_plans':
372+
return await createImplPlansToolHandler(args || {});
373+
278374
default:
279375
throw new Error(`Unknown tool: ${name}`);
280376
}
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/**
2+
* Create Constitution Tool - Automated constitution generation from functional spec
3+
*
4+
* Part of F002-automated-spec-generation
5+
* Automates GitHub Spec Kit constitution creation
6+
*/
7+
8+
import * as fs from 'fs/promises';
9+
import * as path from 'path';
10+
import * as crypto from 'crypto';
11+
import { createDefaultValidator } from '../utils/security.js';
12+
import { StateManager } from '../utils/state-manager.js';
13+
import { MarkdownParser } from '../utils/markdown-parser.js';
14+
import { SpecGenerator, MarkdownDocument } from '../utils/spec-generator.js';
15+
import { TemplateEngine } from '../utils/template-engine.js';
16+
import { FileWriter } from '../utils/file-writer.js';
17+
18+
interface CreateConstitutionArgs {
19+
directory?: string;
20+
route?: 'greenfield' | 'brownfield';
21+
outputPath?: string;
22+
}
23+
24+
interface ProgressUpdate {
25+
phase: string;
26+
status: 'starting' | 'in-progress' | 'completed' | 'error';
27+
message: string;
28+
details?: Record<string, any>;
29+
}
30+
31+
export async function createConstitutionToolHandler(args: CreateConstitutionArgs) {
32+
const progress: ProgressUpdate[] = [];
33+
34+
try {
35+
progress.push({
36+
phase: 'initialization',
37+
status: 'starting',
38+
message: 'Starting constitution generation',
39+
});
40+
41+
// SECURITY: Validate directory
42+
const validator = createDefaultValidator();
43+
const directory = validator.validateDirectory(args.directory || process.cwd());
44+
45+
// Load state to get route
46+
const stateManager = new StateManager(directory);
47+
const state = await stateManager.load();
48+
const route = (args.route || state.path) as 'greenfield' | 'brownfield';
49+
50+
if (!route || (route !== 'greenfield' && route !== 'brownfield')) {
51+
throw new Error('Route must be "greenfield" or "brownfield". Run stackshift_analyze first or specify --route parameter.');
52+
}
53+
54+
progress.push({
55+
phase: 'initialization',
56+
status: 'completed',
57+
message: `Using ${route} route`,
58+
details: { route },
59+
});
60+
61+
// Find functional specification
62+
progress.push({
63+
phase: 'loading',
64+
status: 'starting',
65+
message: 'Loading functional specification',
66+
});
67+
68+
const funcSpecPath = path.join(
69+
directory,
70+
'docs',
71+
'reverse-engineering',
72+
'functional-specification.md'
73+
);
74+
75+
let content: string;
76+
try {
77+
content = await fs.readFile(funcSpecPath, 'utf-8');
78+
} catch (error) {
79+
throw new Error(
80+
`Functional specification not found at ${funcSpecPath}. Run stackshift_reverse_engineer first.`
81+
);
82+
}
83+
84+
// Parse markdown
85+
const parser = new MarkdownParser();
86+
const nodes = parser.parse(content);
87+
const stats = await fs.stat(funcSpecPath);
88+
const checksum = crypto.createHash('sha256').update(content).digest('hex');
89+
90+
const doc: MarkdownDocument = {
91+
filePath: funcSpecPath,
92+
content,
93+
nodes,
94+
metadata: {
95+
fileName: path.basename(funcSpecPath),
96+
fileSize: stats.size,
97+
lastModified: stats.mtime,
98+
checksum,
99+
},
100+
};
101+
102+
progress.push({
103+
phase: 'loading',
104+
status: 'completed',
105+
message: 'Functional specification loaded',
106+
details: {
107+
filePath: funcSpecPath,
108+
size: stats.size,
109+
nodes: nodes.length,
110+
},
111+
});
112+
113+
// Extract constitution data
114+
progress.push({
115+
phase: 'extraction',
116+
status: 'starting',
117+
message: 'Extracting constitution data from specification',
118+
});
119+
120+
const generator = new SpecGenerator();
121+
const constitutionData = await generator.extractConstitution(doc, route);
122+
123+
progress.push({
124+
phase: 'extraction',
125+
status: 'completed',
126+
message: 'Constitution data extracted successfully',
127+
details: {
128+
purpose: constitutionData.purpose.substring(0, 100) + '...',
129+
valuesCount: constitutionData.values.length,
130+
standardsCount: constitutionData.developmentStandards.length,
131+
metricsCount: constitutionData.qualityMetrics.length,
132+
hasTechnicalStack: !!constitutionData.technicalStack,
133+
},
134+
});
135+
136+
// Load template
137+
progress.push({
138+
phase: 'templating',
139+
status: 'starting',
140+
message: 'Loading constitution template',
141+
});
142+
143+
const templateEngine = new TemplateEngine(path.join(directory, 'plugin', 'templates'));
144+
const templateName =
145+
route === 'greenfield'
146+
? 'constitution-agnostic-template'
147+
: 'constitution-prescriptive-template';
148+
149+
let template: string;
150+
try {
151+
template = await templateEngine.loadTemplate(templateName);
152+
} catch (error) {
153+
throw new Error(
154+
`Constitution template not found: ${templateName}.md. Ensure plugin/templates/ directory exists with required templates.`
155+
);
156+
}
157+
158+
progress.push({
159+
phase: 'templating',
160+
status: 'in-progress',
161+
message: 'Populating template with data',
162+
});
163+
164+
// Prepare template data
165+
const templateData: Record<string, any> = {
166+
purpose: constitutionData.purpose,
167+
values: constitutionData.values,
168+
developmentStandards: constitutionData.developmentStandards.map((s) => ({
169+
category: s.category,
170+
description: s.description,
171+
level: s.enforcementLevel,
172+
})),
173+
qualityMetrics: constitutionData.qualityMetrics.map((m) => ({
174+
name: m.name,
175+
target: m.target,
176+
measurement: m.measurement,
177+
})),
178+
governance: {
179+
decisionMaking: constitutionData.governance.decisionMaking,
180+
changeApproval: constitutionData.governance.changeApproval,
181+
conflictResolution: constitutionData.governance.conflictResolution,
182+
},
183+
};
184+
185+
// Add technical stack for brownfield
186+
if (route === 'brownfield' && constitutionData.technicalStack) {
187+
templateData.technicalStack = constitutionData.technicalStack;
188+
templateData.hasTechnicalStack = true;
189+
} else {
190+
templateData.hasTechnicalStack = false;
191+
}
192+
193+
// Populate template
194+
const populatedContent = templateEngine.populate(template, templateData);
195+
196+
progress.push({
197+
phase: 'templating',
198+
status: 'completed',
199+
message: 'Template populated successfully',
200+
details: {
201+
templateName,
202+
contentLength: populatedContent.length,
203+
},
204+
});
205+
206+
// Write constitution file
207+
progress.push({
208+
phase: 'writing',
209+
status: 'starting',
210+
message: 'Writing constitution file',
211+
});
212+
213+
const outputPath = args.outputPath ||
214+
path.join(directory, '.specify', 'memory', 'constitution.md');
215+
216+
const writer = new FileWriter(path.dirname(outputPath));
217+
const result = await writer.writeFile(outputPath, populatedContent);
218+
219+
progress.push({
220+
phase: 'writing',
221+
status: 'completed',
222+
message: 'Constitution file written successfully',
223+
details: {
224+
filePath: result.filePath,
225+
bytesWritten: result.bytesWritten,
226+
checksum: result.checksum,
227+
},
228+
});
229+
230+
// Return success response
231+
return {
232+
success: true,
233+
route,
234+
constitutionPath: result.filePath,
235+
stats: {
236+
purpose: constitutionData.purpose.substring(0, 150),
237+
valuesCount: constitutionData.values.length,
238+
standardsCount: constitutionData.developmentStandards.length,
239+
metricsCount: constitutionData.qualityMetrics.length,
240+
bytesWritten: result.bytesWritten,
241+
},
242+
progress,
243+
message: `✅ Constitution generated successfully at ${result.filePath}`,
244+
};
245+
} catch (error) {
246+
progress.push({
247+
phase: 'error',
248+
status: 'error',
249+
message: error instanceof Error ? error.message : String(error),
250+
details: { error: String(error) },
251+
});
252+
253+
return {
254+
success: false,
255+
error: error instanceof Error ? error.message : String(error),
256+
progress,
257+
message: `❌ Failed to generate constitution: ${error instanceof Error ? error.message : String(error)}`,
258+
};
259+
}
260+
}

0 commit comments

Comments
 (0)