specdrive-cli 0.1.10 → 0.1.12

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.
Files changed (74) hide show
  1. package/README.md +955 -697
  2. package/agents/00-onboarding.md +261 -0
  3. package/agents/01-constitution.md +214 -201
  4. package/agents/02-specification.md +249 -226
  5. package/agents/03-uiux.md +156 -144
  6. package/agents/04-cascade.md +151 -122
  7. package/agents/05-discover-skills.md +136 -136
  8. package/agents/06-documentation.md +158 -145
  9. package/agents/07-implementation.md +201 -169
  10. package/agents/08-performance.md +179 -165
  11. package/agents/09-review-complete.md +239 -168
  12. package/agents/10-security.md +180 -167
  13. package/agents/11-test.md +195 -0
  14. package/commands/gates.js +73 -73
  15. package/commands/manifest.json +113 -95
  16. package/commands/permissions.json +39 -0
  17. package/commands/router.js +151 -127
  18. package/commands/tools.json +19 -19
  19. package/dashboard/app.js +394 -0
  20. package/dashboard/index.html +74 -0
  21. package/dashboard/server.js +166 -0
  22. package/dashboard/style.css +157 -0
  23. package/mcp/mcp.json +31 -0
  24. package/mcp/server.js +108 -0
  25. package/package.json +35 -32
  26. package/schemas/config.schema.json +20 -0
  27. package/schemas/workflow-state.schema.json +149 -38
  28. package/scripts/anti-redundancy.js +176 -176
  29. package/scripts/audit-log.js +46 -46
  30. package/scripts/check-permission.js +87 -0
  31. package/scripts/diff-spec.js +50 -50
  32. package/scripts/diff-version.js +96 -0
  33. package/scripts/generate-adapters.js +80 -80
  34. package/scripts/generate-from-template.js +97 -97
  35. package/scripts/generate-openapi.js +75 -75
  36. package/scripts/github-team-sync.js +80 -80
  37. package/scripts/install-hooks.js +20 -20
  38. package/scripts/load-plugins.js +65 -65
  39. package/scripts/migrate-openspec.js +318 -0
  40. package/scripts/migrate-speckit.js +322 -0
  41. package/scripts/migrate.js +12 -62
  42. package/scripts/onboard.js +312 -0
  43. package/scripts/pre-commit.js +56 -20
  44. package/scripts/team.js +113 -113
  45. package/scripts/test-adapters.js +118 -118
  46. package/scripts/test-create.js +13 -13
  47. package/scripts/test-end-to-end.js +137 -137
  48. package/scripts/test-router.js +110 -110
  49. package/scripts/test-state-transitions.js +146 -146
  50. package/scripts/test-validator.js +152 -152
  51. package/scripts/validate-config.js +36 -0
  52. package/scripts/validate-governance.js +150 -130
  53. package/scripts/verify.js +525 -0
  54. package/scripts/version-new.js +202 -0
  55. package/src/index.js +1010 -807
  56. package/templates/expo/plan.json +12 -0
  57. package/templates/expo/spec.json +12 -0
  58. package/templates/expo/tasks.json +5 -0
  59. package/templates/fastapi/plan.json +12 -0
  60. package/templates/fastapi/spec.json +12 -0
  61. package/templates/fastapi/tasks.json +5 -0
  62. package/templates/generic/plan.json +12 -0
  63. package/templates/generic/spec.json +11 -0
  64. package/templates/generic/tasks.json +5 -0
  65. package/templates/nextjs/plan.json +23 -0
  66. package/templates/nextjs/spec.json +12 -0
  67. package/templates/nextjs/tasks.json +5 -0
  68. package/templates/react-node/plan.json +15 -0
  69. package/templates/react-node/spec.json +12 -0
  70. package/templates/react-node/tasks.json +5 -0
  71. package/templates/registry.json +30 -0
  72. package/templates/turborepo/plan.json +12 -0
  73. package/templates/turborepo/spec.json +12 -0
  74. package/templates/turborepo/tasks.json +5 -0
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const projectRoot = process.cwd();
7
+
8
+ // ---------- Phase 1: Plan Reader ----------
9
+
10
+ function loadPlan(featureName) {
11
+ const planPath = path.join(projectRoot, ".sdrive", "governance", featureName, "plan.json");
12
+
13
+ if (!fs.existsSync(planPath)) {
14
+ console.error(`❌ plan.json not found for feature "${featureName}".`);
15
+ process.exit(1);
16
+ }
17
+
18
+ return JSON.parse(fs.readFileSync(planPath, "utf8"));
19
+ }
20
+
21
+ function extractPlanItems(plan) {
22
+ const items = [];
23
+
24
+ (plan.architecture?.components || []).forEach((c) => {
25
+ items.push({ type: "component", name: c.name, action: c.action });
26
+ });
27
+
28
+ (plan.apiContracts || []).forEach((api) => {
29
+ items.push({ type: "endpoint", method: api.method, path: api.path, action: api.action });
30
+ });
31
+
32
+ (plan.dataModel?.entities || []).forEach((entity) => {
33
+ items.push({ type: "entity", name: entity.name, action: entity.action });
34
+ (entity.fields || []).forEach((field) => {
35
+ items.push({
36
+ type: "field",
37
+ entity: entity.name,
38
+ name: field.name,
39
+ fieldType: field.type,
40
+ action: field.action
41
+ });
42
+ });
43
+ });
44
+
45
+ (plan.dependencies || []).forEach((dep) => {
46
+ items.push({ type: "dependency", name: dep.name, action: dep.action });
47
+ });
48
+
49
+ return items;
50
+ }
51
+
52
+ // ---------- Phase 2: Code Scanner ----------
53
+
54
+ const CANDIDATE_ROOTS = [
55
+ "src",
56
+ "app",
57
+ "lib",
58
+ "pages",
59
+ "server",
60
+ "internal",
61
+ "cmd",
62
+ "pkg",
63
+ "api",
64
+ "apps",
65
+ "packages"
66
+ ];
67
+
68
+ const IGNORED_DIRS = new Set([
69
+ "node_modules",
70
+ ".git",
71
+ "dist",
72
+ "build",
73
+ ".next",
74
+ ".expo",
75
+ ".turbo",
76
+ "coverage",
77
+ "out",
78
+ ".sdrive",
79
+ ".cache",
80
+ "vendor"
81
+ ]);
82
+
83
+ const CODE_EXTENSIONS = [
84
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
85
+ ".py", ".go", ".rs", ".dart", ".java", ".kt",
86
+ ".rb", ".php", ".cs", ".swift"
87
+ ];
88
+
89
+ function readConstitutionSources() {
90
+ const constitutionPath = path.join(projectRoot, ".sdrive", "constitution.md");
91
+
92
+ if (!fs.existsSync(constitutionPath)) return [];
93
+
94
+ const content = fs.readFileSync(constitutionPath, "utf8");
95
+ const matches = content.match(/`([^`]+)\/`/g) || [];
96
+
97
+ return matches
98
+ .map((m) => m.replace(/`/g, "").replace(/\/$/, ""))
99
+ .filter((p) => CANDIDATE_ROOTS.includes(p));
100
+ }
101
+
102
+ function detectSourceRoots() {
103
+ const fromConstitution = readConstitutionSources();
104
+
105
+ if (fromConstitution.length > 0) {
106
+ return fromConstitution.filter((r) => fs.existsSync(path.join(projectRoot, r)));
107
+ }
108
+
109
+ return CANDIDATE_ROOTS.filter((r) => fs.existsSync(path.join(projectRoot, r)));
110
+ }
111
+
112
+ function walkDirectory(dir, files = []) {
113
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
114
+
115
+ for (const entry of entries) {
116
+ if (IGNORED_DIRS.has(entry.name)) continue;
117
+
118
+ const fullPath = path.join(dir, entry.name);
119
+
120
+ if (entry.isDirectory()) {
121
+ walkDirectory(fullPath, files);
122
+ } else if (CODE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
123
+ files.push(fullPath);
124
+ }
125
+ }
126
+
127
+ return files;
128
+ }
129
+
130
+ function scanCode() {
131
+ const roots = detectSourceRoots();
132
+
133
+ if (roots.length === 0) {
134
+ console.warn("⚠️ No source folders detected.");
135
+ return { files: [], roots: [] };
136
+ }
137
+
138
+ console.log(`🔍 Scanning roots: ${roots.join(", ")}\n`);
139
+
140
+ const files = [];
141
+ roots.forEach((root) => {
142
+ walkDirectory(path.join(projectRoot, root), files);
143
+ });
144
+
145
+ console.log(`📄 Found ${files.length} source files.\n`);
146
+
147
+ return { files, roots };
148
+ }
149
+
150
+
151
+ // ---------- Phase 3: Diff Engine ----------
152
+
153
+ function readFileContent(filePath) {
154
+ try {
155
+ return fs.readFileSync(filePath, "utf8");
156
+ } catch (error) {
157
+ return "";
158
+ }
159
+ }
160
+
161
+ function detectComponents(files) {
162
+ const components = [];
163
+
164
+ files.forEach((file) => {
165
+ const content = readFileContent(file);
166
+ const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
167
+ const basename = path.basename(file, path.extname(file));
168
+
169
+ // React / Vue / Svelte components
170
+ const componentMatches = content.match(
171
+ /(?:export\s+)?(?:function|const|class)\s+([A-Z][A-Za-z0-9_]+)/
172
+ );
173
+
174
+ if (componentMatches && /components?\//i.test(relative)) {
175
+ components.push({
176
+ name: componentMatches[1],
177
+ file: relative,
178
+ basename
179
+ });
180
+ }
181
+ });
182
+
183
+ return components;
184
+ }
185
+
186
+ function detectEndpoints(files) {
187
+ const endpoints = [];
188
+
189
+ files.forEach((file) => {
190
+ const content = readFileContent(file);
191
+ const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
192
+
193
+ // Express / Fastify / Koa
194
+ const expressMatches = content.matchAll(
195
+ /(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([^'"`]+)['"`]/gi
196
+ );
197
+
198
+ for (const match of expressMatches) {
199
+ endpoints.push({
200
+ method: match[1].toUpperCase(),
201
+ path: match[2],
202
+ file: relative
203
+ });
204
+ }
205
+
206
+ // FastAPI / Flask
207
+ const fastapiMatches = content.matchAll(
208
+ /@(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([^'"`]+)['"`]/gi
209
+ );
210
+
211
+ for (const match of fastapiMatches) {
212
+ endpoints.push({
213
+ method: match[1].toUpperCase(),
214
+ path: match[2],
215
+ file: relative
216
+ });
217
+ }
218
+
219
+ // Next.js API routes
220
+ if (/pages\/api\//.test(relative)) {
221
+ const route = relative.replace("pages/api", "").replace(/\.[jt]sx?$/, "");
222
+ endpoints.push({
223
+ method: "GET",
224
+ path: `/api${route}`,
225
+ file: relative
226
+ });
227
+ }
228
+ });
229
+
230
+ return endpoints;
231
+ }
232
+
233
+ function detectEntities(files) {
234
+ const entities = [];
235
+
236
+ files.forEach((file) => {
237
+ const content = readFileContent(file);
238
+ const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
239
+
240
+ // SQL CREATE TABLE
241
+ const sqlMatches = content.matchAll(/CREATE TABLE\s+(\w+)/gi);
242
+ for (const match of sqlMatches) {
243
+ entities.push({
244
+ name: match[1],
245
+ file: relative
246
+ });
247
+ }
248
+
249
+ // Prisma models
250
+ const prismaMatches = content.matchAll(/model\s+(\w+)/g);
251
+ for (const match of prismaMatches) {
252
+ entities.push({
253
+ name: match[1],
254
+ file: relative
255
+ });
256
+ }
257
+
258
+ // TypeORM entities
259
+ const typeormMatches = content.matchAll(/@Entity\([^)]*\)\s*class\s+(\w+)/g);
260
+ for (const match of typeormMatches) {
261
+ entities.push({
262
+ name: match[1],
263
+ file: relative
264
+ });
265
+ }
266
+ });
267
+
268
+ return entities;
269
+ }
270
+
271
+ function detectFields(files) {
272
+ const fields = [];
273
+
274
+ files.forEach((file) => {
275
+ const content = readFileContent(file);
276
+ const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
277
+
278
+ // SQL fields inside CREATE TABLE
279
+ const tableBlocks = content.matchAll(
280
+ /CREATE TABLE\s+(\w+)\s*\(([\s\S]*?)\)/gi
281
+ );
282
+
283
+ for (const block of tableBlocks) {
284
+ const entity = block[1];
285
+ const body = block[2];
286
+
287
+ const fieldMatches = body.matchAll(/^\s*(\w+)\s+\w+/gm);
288
+
289
+ for (const field of fieldMatches) {
290
+ fields.push({
291
+ entity,
292
+ name: field[1],
293
+ file: relative
294
+ });
295
+ }
296
+ }
297
+
298
+ // Prisma fields
299
+ const prismaBlocks = content.matchAll(/model\s+(\w+)\s*\{([\s\S]*?)\}/g);
300
+
301
+ for (const block of prismaBlocks) {
302
+ const entity = block[1];
303
+ const body = block[2];
304
+
305
+ const fieldMatches = body.matchAll(/^\s*(\w+)\s+\w+/gm);
306
+
307
+ for (const field of fieldMatches) {
308
+ fields.push({
309
+ entity,
310
+ name: field[1],
311
+ file: relative
312
+ });
313
+ }
314
+ }
315
+ });
316
+
317
+ return fields;
318
+ }
319
+
320
+ function detectDependencies() {
321
+ const pkgPath = path.join(projectRoot, "package.json");
322
+
323
+ if (!fs.existsSync(pkgPath)) return [];
324
+
325
+ try {
326
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
327
+ return [
328
+ ...Object.keys(pkg.dependencies || {}),
329
+ ...Object.keys(pkg.devDependencies || {})
330
+ ];
331
+ } catch (error) {
332
+ return [];
333
+ }
334
+ }
335
+
336
+ function diffPlanAgainstCode(planItems, code) {
337
+ const matched = [];
338
+ const missing = [];
339
+ const renamed = [];
340
+ const extra = [];
341
+
342
+ const codeComponents = detectComponents(code.files);
343
+ const codeEndpoints = detectEndpoints(code.files);
344
+ const codeEntities = detectEntities(code.files);
345
+ const codeFields = detectFields(code.files);
346
+ const codeDependencies = detectDependencies();
347
+
348
+ planItems.forEach((item) => {
349
+ if (item.type === "component") {
350
+ const found = codeComponents.find((c) => c.name === item.name);
351
+
352
+ if (found) {
353
+ matched.push({ ...item, file: found.file });
354
+ } else {
355
+ // possible rename: same folder or similar name
356
+ const similar = codeComponents.find((c) =>
357
+ c.name.toLowerCase().includes(item.name.toLowerCase().slice(0, 5))
358
+ );
359
+
360
+ if (similar) {
361
+ renamed.push({ ...item, to: similar.name, file: similar.file });
362
+ } else {
363
+ missing.push(item);
364
+ }
365
+ }
366
+ }
367
+
368
+ if (item.type === "endpoint") {
369
+ const found = codeEndpoints.find(
370
+ (e) => e.method === item.method && e.path === item.path
371
+ );
372
+
373
+ if (found) {
374
+ matched.push({ ...item, file: found.file });
375
+ } else {
376
+ missing.push(item);
377
+ }
378
+ }
379
+
380
+ if (item.type === "entity") {
381
+ const found = codeEntities.find((e) => e.name === item.name);
382
+
383
+ if (found) {
384
+ matched.push({ ...item, file: found.file });
385
+ } else {
386
+ missing.push(item);
387
+ }
388
+ }
389
+
390
+ if (item.type === "field") {
391
+ const found = codeFields.find(
392
+ (f) => f.entity === item.entity && f.name === item.name
393
+ );
394
+
395
+ if (found) {
396
+ matched.push({ ...item, file: found.file });
397
+ } else {
398
+ missing.push(item);
399
+ }
400
+ }
401
+
402
+ if (item.type === "dependency") {
403
+ if (codeDependencies.includes(item.name)) {
404
+ matched.push(item);
405
+ } else {
406
+ missing.push(item);
407
+ }
408
+ }
409
+ });
410
+
411
+ // Extra code detection
412
+ codeComponents.forEach((c) => {
413
+ if (!planItems.some((p) => p.type === "component" && p.name === c.name)) {
414
+ extra.push({ type: "component", name: c.name, file: c.file });
415
+ }
416
+ });
417
+
418
+ codeEndpoints.forEach((e) => {
419
+ if (!planItems.some((p) => p.type === "endpoint" && p.path === e.path)) {
420
+ extra.push({ type: "endpoint", method: e.method, path: e.path, file: e.file });
421
+ }
422
+ });
423
+
424
+ return { matched, missing, renamed, extra };
425
+ }
426
+
427
+
428
+ // ---------- Phase 4: Report Writer ----------
429
+
430
+ function determineStatus(result) {
431
+ if (result.missing.length > 0) return "fail";
432
+ if (result.renamed.length > 0) return "fail";
433
+ if (result.extra.length > 0) return "warn";
434
+ return "pass";
435
+ }
436
+
437
+ function writeReport(featureName, result) {
438
+ const status = determineStatus(result);
439
+
440
+ const report = {
441
+ feature: featureName,
442
+ status,
443
+ checkedAt: new Date().toISOString(),
444
+ summary: {
445
+ matched: result.matched.length,
446
+ missing: result.missing.length,
447
+ renamed: result.renamed.length,
448
+ extra: result.extra.length
449
+ },
450
+ items: {
451
+ matched: result.matched,
452
+ missing: result.missing,
453
+ renamed: result.renamed,
454
+ extra: result.extra
455
+ }
456
+ };
457
+
458
+ const reportPath = path.join(
459
+ projectRoot,
460
+ ".sdrive",
461
+ "governance",
462
+ featureName,
463
+ "verification.json"
464
+ );
465
+
466
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
467
+
468
+ return { status, reportPath };
469
+ }
470
+
471
+
472
+ // ---------- Entry ----------
473
+
474
+ const featureName = process.argv[2];
475
+
476
+ if (!featureName) {
477
+ console.error("❌ Usage: node verify.js <feature-name>");
478
+ process.exit(1);
479
+ }
480
+
481
+ const plan = loadPlan(featureName);
482
+ const planItems = extractPlanItems(plan);
483
+ const code = scanCode();
484
+
485
+ const result = diffPlanAgainstCode(planItems, code);
486
+ const { status, reportPath } = writeReport(featureName, result);
487
+
488
+ console.log("\n📊 Verification Result\n");
489
+ console.log(`✅ Matched: ${result.matched.length}`);
490
+ console.log(`❌ Missing: ${result.missing.length}`);
491
+ console.log(`⚠️ Renamed: ${result.renamed.length}`);
492
+ console.log(`➕ Extra: ${result.extra.length}`);
493
+ console.log(`\n🧾 Status: ${status.toUpperCase()}`);
494
+ console.log(`📁 Report: ${path.relative(projectRoot, reportPath)}\n`);
495
+
496
+ if (result.missing.length > 0) {
497
+ console.log("❌ Missing:");
498
+ result.missing.forEach((i) => {
499
+ console.log(` [${i.type}] ${i.name || i.path}`);
500
+ });
501
+ }
502
+
503
+ if (result.renamed.length > 0) {
504
+ console.log("\n⚠️ Renamed:");
505
+ result.renamed.forEach((i) => {
506
+ console.log(` [${i.type}] ${i.name} → ${i.to}`);
507
+ });
508
+ }
509
+
510
+ if (result.extra.length > 0) {
511
+ console.log("\n➕ Extra (not in plan):");
512
+ result.extra.forEach((i) => {
513
+ console.log(` [${i.type}] ${i.name || i.path}`);
514
+ });
515
+ }
516
+
517
+ process.exit(status === "fail" ? 1 : 0);
518
+
519
+ module.exports = {
520
+ loadPlan,
521
+ extractPlanItems,
522
+ scanCode,
523
+ diffPlanAgainstCode,
524
+ writeReport
525
+ };
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const projectRoot = process.cwd();
7
+
8
+ const featureName = process.argv[2];
9
+
10
+ if (!featureName) {
11
+ console.error("❌ Usage: node version-new.js <feature>");
12
+ process.exit(1);
13
+ }
14
+
15
+ const sdriveRoot = path.join(projectRoot, ".sdrive");
16
+ const workflowStatePath = path.join(sdriveRoot, "workflow-state.json");
17
+
18
+ const specFeatureDir = findFeatureDir(path.join(sdriveRoot, "specs", "ongoing"), featureName);
19
+ const govFeatureDir = findFeatureDir(path.join(sdriveRoot, "governance"), featureName);
20
+
21
+ if (!specFeatureDir && !govFeatureDir) {
22
+ console.error(`❌ Feature "${featureName}" not found in ongoing or governance.`);
23
+ process.exit(1);
24
+ }
25
+
26
+ function findFeatureDir(baseDir, name) {
27
+ if (!fs.existsSync(baseDir)) return null;
28
+
29
+ const candidate = path.join(baseDir, name);
30
+ if (fs.existsSync(candidate)) return candidate;
31
+
32
+ return null;
33
+ }
34
+
35
+ function loadWorkflowState() {
36
+ if (!fs.existsSync(workflowStatePath)) {
37
+ console.error("❌ workflow-state.json not found.");
38
+ process.exit(1);
39
+ }
40
+ return JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
41
+ }
42
+
43
+ function saveWorkflowState(state) {
44
+ fs.writeFileSync(workflowStatePath, JSON.stringify(state, null, 2));
45
+ }
46
+
47
+ function computeNextVersion(feature) {
48
+ if (!feature.versions || feature.versions.length === 0) {
49
+ return "v1";
50
+ }
51
+
52
+ const maxVersion = feature.versions
53
+ .map((v) => parseInt(v.id.replace("v", ""), 10))
54
+ .reduce((a, b) => Math.max(a, b), 0);
55
+
56
+ return `v${maxVersion + 1}`;
57
+ }
58
+
59
+ function migrateToVersions(feature, specDir, govDir) {
60
+ console.log("🔧 Migrating feature to version folders...");
61
+
62
+ const v1SpecDir = path.join(specDir, "v1");
63
+ const v1GovDir = path.join(govDir, "v1");
64
+
65
+ fs.mkdirSync(v1SpecDir, { recursive: true });
66
+ fs.mkdirSync(v1GovDir, { recursive: true });
67
+
68
+ const specFiles = ["spec.md", "plan.md", "tasks.md"];
69
+ const govFiles = ["spec.json", "plan.json", "tasks.json", "traceability.json"];
70
+
71
+ specFiles.forEach((file) => {
72
+ const src = path.join(specDir, file);
73
+ const dest = path.join(v1SpecDir, file);
74
+ if (fs.existsSync(src)) {
75
+ fs.renameSync(src, dest);
76
+ console.log(` Moved: ${file} → v1/${file}`);
77
+ }
78
+ });
79
+
80
+ govFiles.forEach((file) => {
81
+ const src = path.join(govDir, file);
82
+ const dest = path.join(v1GovDir, file);
83
+ if (fs.existsSync(src)) {
84
+ fs.renameSync(src, dest);
85
+ console.log(` Moved: ${file} → v1/${file}`);
86
+ }
87
+ });
88
+
89
+ return {
90
+ id: "v1",
91
+ phase: feature.phase || "ongoing",
92
+ currentStep: feature.currentStep || "specification",
93
+ gates: feature.gates || {
94
+ specification: "pending",
95
+ planning: "pending",
96
+ implementation: "pending",
97
+ review: "pending"
98
+ },
99
+ history: feature.history || []
100
+ };
101
+ }
102
+
103
+ function createVersionSkeleton(version, specDir, govDir) {
104
+ const versionSpecDir = path.join(specDir, version);
105
+ const versionGovDir = path.join(govDir, version);
106
+
107
+ fs.mkdirSync(versionSpecDir, { recursive: true });
108
+ fs.mkdirSync(versionGovDir, { recursive: true });
109
+
110
+ fs.writeFileSync(
111
+ path.join(versionSpecDir, "spec.md"),
112
+ `---\ntitle: ${featureName} (${version})\nstatus: backlog\ncreated: ${new Date().toISOString()}\nupdated: ${new Date().toISOString()}\n---\n\n# ${featureName} (${version})\n`
113
+ );
114
+
115
+ fs.writeFileSync(
116
+ path.join(versionSpecDir, "plan.md"),
117
+ `# Technical Plan: ${featureName} (${version})\n`
118
+ );
119
+
120
+ fs.writeFileSync(
121
+ path.join(versionSpecDir, "tasks.md"),
122
+ `# Execution Tasks: ${featureName} (${version})\n`
123
+ );
124
+
125
+ fs.writeFileSync(
126
+ path.join(versionGovDir, "spec.json"),
127
+ JSON.stringify({ feature: featureName, title: featureName, version: 1, overview: {}, userStories: [], functionalRequirements: [] }, null, 2)
128
+ );
129
+
130
+ fs.writeFileSync(
131
+ path.join(versionGovDir, "plan.json"),
132
+ JSON.stringify({ feature: featureName, version: 1, architecture: {}, dataModel: {} }, null, 2)
133
+ );
134
+
135
+ fs.writeFileSync(
136
+ path.join(versionGovDir, "tasks.json"),
137
+ JSON.stringify({ feature: featureName, version: 1, phases: [] }, null, 2)
138
+ );
139
+
140
+ fs.writeFileSync(
141
+ path.join(versionGovDir, "traceability.json"),
142
+ JSON.stringify({ feature: featureName, version: 1, rows: [] }, null, 2)
143
+ );
144
+ }
145
+
146
+ function main() {
147
+ const state = loadWorkflowState();
148
+ const feature = state.features?.find((f) => f.name === featureName);
149
+
150
+ if (!feature) {
151
+ console.error(`❌ Feature "${featureName}" not found in workflow-state.json.`);
152
+ process.exit(1);
153
+ }
154
+
155
+ // Migrate if needed
156
+ if (!feature.versions || feature.versions.length === 0) {
157
+ const v1 = migrateToVersions(feature, specFeatureDir, govFeatureDir);
158
+ feature.versions = [v1];
159
+ feature.currentVersion = "v1";
160
+
161
+ // Remove old top-level fields
162
+ delete feature.phase;
163
+ delete feature.currentStep;
164
+ delete feature.gates;
165
+ delete feature.history;
166
+ }
167
+
168
+ const nextVersionId = computeNextVersion(feature);
169
+
170
+ console.log(`📦 Creating version ${nextVersionId} for "${featureName}"`);
171
+
172
+ createVersionSkeleton(nextVersionId, specFeatureDir, govFeatureDir);
173
+
174
+ feature.versions.push({
175
+ id: nextVersionId,
176
+ phase: "ongoing",
177
+ currentStep: "specification",
178
+ gates: {
179
+ specification: "pending",
180
+ planning: "pending",
181
+ implementation: "pending",
182
+ review: "pending"
183
+ },
184
+ history: [
185
+ {
186
+ event: "version_created",
187
+ timestamp: new Date().toISOString()
188
+ }
189
+ ]
190
+ });
191
+
192
+ feature.currentVersion = nextVersionId;
193
+
194
+ saveWorkflowState(state);
195
+
196
+ console.log(`✅ Version ${nextVersionId} created.`);
197
+ console.log(` Spec folder: .sdrive/specs/ongoing/${featureName}/${nextVersionId}/`);
198
+ console.log(` Governance: .sdrive/governance/${featureName}/${nextVersionId}/`);
199
+ console.log(` Next: run /sdrive:propose to fill it.`);
200
+ }
201
+
202
+ main();