jumpstart-mode 1.1.3 → 1.1.4

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.
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * holodeck.js — Jump Start E2E Simulation Runner
5
+ *
6
+ * Simulates the complete Jump Start lifecycle using Golden Master fixtures.
7
+ * Validates artifacts, verifies subagent traces, and checks handoff contracts.
8
+ *
9
+ * Usage:
10
+ * node bin/holodeck.js --scenario ecommerce
11
+ * node bin/holodeck.js --scenario ecommerce --verify-subagents
12
+ * node bin/holodeck.js --all
13
+ * node bin/holodeck.js --list
14
+ *
15
+ * Options:
16
+ * --scenario <name> Run a specific scenario
17
+ * --verify-subagents Enable strict subagent trace verification
18
+ * --all Run all available scenarios
19
+ * --list List available scenarios
20
+ * --output <path> Output report path (default: tests/e2e/reports/)
21
+ * --verbose Enable verbose output
22
+ * --help Show help
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { SimulationTracer } = require('./lib/simulation-tracer');
28
+ const { generateHandoffReport } = require('./lib/handoff-validator');
29
+ const { validateArtifact, validateMarkdownStructure, checkApproval } = require('./lib/validator');
30
+ const { updateState, resetState } = require('./lib/state-store');
31
+ const { logUsage, summarizeUsage } = require('./lib/usage');
32
+
33
+ // ─── Configuration ───────────────────────────────────────────────────────────
34
+
35
+ const SCENARIOS_DIR = path.join(__dirname, '..', 'tests', 'e2e', 'scenarios');
36
+ const REPORTS_DIR = path.join(__dirname, '..', 'tests', 'e2e', 'reports');
37
+ const HANDOFFS_DIR = path.join(__dirname, '..', '.jumpstart', 'handoffs');
38
+ const SCHEMAS_DIR = path.join(__dirname, '..', '.jumpstart', 'schemas');
39
+
40
+ const PHASE_CONFIG = [
41
+ { name: 'scout', dir: '00-scout', artifacts: ['codebase-context.md', 'insights.md'], hasSubagents: false },
42
+ { name: 'challenger', dir: '01-challenger', artifacts: ['challenger-brief.md', 'insights.md'], hasSubagents: false },
43
+ { name: 'analyst', dir: '02-analyst', artifacts: ['product-brief.md', 'insights.md'], hasSubagents: false },
44
+ { name: 'pm', dir: '03-pm', artifacts: ['prd.md', 'insights.md'], hasSubagents: false },
45
+ { name: 'architect', dir: '04-architect', artifacts: ['architecture.md', 'implementation-plan.md', 'insights.md'], hasSubagents: true, expectedSubagents: ['Jump Start: Security'] },
46
+ { name: 'developer', dir: '05-developer', artifacts: ['TODO.md'], hasSubagents: false }
47
+ ];
48
+
49
+ // ─── Utility Functions ───────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Parse command line arguments.
53
+ */
54
+ function parseArgs() {
55
+ const args = process.argv.slice(2);
56
+ const options = {
57
+ scenario: null,
58
+ verifySubagents: false,
59
+ all: false,
60
+ list: false,
61
+ output: REPORTS_DIR,
62
+ verbose: false,
63
+ help: false
64
+ };
65
+
66
+ for (let i = 0; i < args.length; i++) {
67
+ switch (args[i]) {
68
+ case '--scenario':
69
+ case '-s':
70
+ options.scenario = args[++i];
71
+ break;
72
+ case '--verify-subagents':
73
+ options.verifySubagents = true;
74
+ break;
75
+ case '--all':
76
+ case '-a':
77
+ options.all = true;
78
+ break;
79
+ case '--list':
80
+ case '-l':
81
+ options.list = true;
82
+ break;
83
+ case '--output':
84
+ case '-o':
85
+ options.output = args[++i];
86
+ break;
87
+ case '--verbose':
88
+ case '-v':
89
+ options.verbose = true;
90
+ break;
91
+ case '--help':
92
+ case '-h':
93
+ options.help = true;
94
+ break;
95
+ }
96
+ }
97
+
98
+ return options;
99
+ }
100
+
101
+ /**
102
+ * Print help message.
103
+ */
104
+ function printHelp() {
105
+ console.log(`
106
+ Jump Start Holodeck — E2E Simulation Runner
107
+
108
+ Usage:
109
+ node bin/holodeck.js --scenario <name> [options]
110
+ node bin/holodeck.js --all [options]
111
+ node bin/holodeck.js --list
112
+
113
+ Options:
114
+ --scenario, -s <name> Run a specific scenario
115
+ --verify-subagents Enable strict subagent trace verification
116
+ --all, -a Run all available scenarios
117
+ --list, -l List available scenarios
118
+ --output, -o <path> Output report directory
119
+ --verbose, -v Enable verbose output
120
+ --help, -h Show this help message
121
+
122
+ Examples:
123
+ node bin/holodeck.js --scenario ecommerce
124
+ node bin/holodeck.js --scenario ecommerce --verify-subagents
125
+ node bin/holodeck.js --all --output ./reports
126
+ `);
127
+ }
128
+
129
+ /**
130
+ * List available scenarios.
131
+ */
132
+ function listScenarios() {
133
+ if (!fs.existsSync(SCENARIOS_DIR)) {
134
+ console.log('No scenarios directory found. Create tests/e2e/scenarios/ first.');
135
+ return [];
136
+ }
137
+
138
+ const scenarios = fs.readdirSync(SCENARIOS_DIR)
139
+ .filter(f => fs.statSync(path.join(SCENARIOS_DIR, f)).isDirectory());
140
+
141
+ console.log('\nAvailable Scenarios:');
142
+ console.log('────────────────────');
143
+ if (scenarios.length === 0) {
144
+ console.log(' (none found)');
145
+ } else {
146
+ scenarios.forEach(s => {
147
+ const configPath = path.join(SCENARIOS_DIR, s, 'config.yaml');
148
+ const hasConfig = fs.existsSync(configPath) ? '✓' : '○';
149
+ console.log(` ${hasConfig} ${s}`);
150
+ });
151
+ }
152
+ console.log('');
153
+ return scenarios;
154
+ }
155
+
156
+ /**
157
+ * Create a temporary project directory for simulation.
158
+ */
159
+ function setupTempProject(scenario) {
160
+ const tmpDir = path.join(__dirname, '..', 'tests', 'e2e', '.tmp', scenario);
161
+
162
+ // Clean and recreate
163
+ if (fs.existsSync(tmpDir)) {
164
+ fs.rmSync(tmpDir, { recursive: true });
165
+ }
166
+ fs.mkdirSync(tmpDir, { recursive: true });
167
+ fs.mkdirSync(path.join(tmpDir, 'specs', 'insights'), { recursive: true });
168
+ fs.mkdirSync(path.join(tmpDir, 'specs', 'decisions'), { recursive: true });
169
+ fs.mkdirSync(path.join(tmpDir, '.jumpstart', 'state'), { recursive: true });
170
+
171
+ // Initialize state
172
+ resetState(path.join(tmpDir, '.jumpstart', 'state', 'state.json'));
173
+
174
+ return tmpDir;
175
+ }
176
+
177
+ /**
178
+ * Copy artifacts from scenario to target.
179
+ */
180
+ function copyArtifacts(srcDir, targetDir, artifacts, tracer) {
181
+ if (!fs.existsSync(srcDir)) {
182
+ tracer.logWarning(`Source directory not found: ${srcDir}`);
183
+ return [];
184
+ }
185
+
186
+ const copied = [];
187
+ for (const artifact of artifacts) {
188
+ const srcPath = path.join(srcDir, artifact);
189
+ if (fs.existsSync(srcPath)) {
190
+ // Determine target path based on artifact type
191
+ let targetPath;
192
+ if (artifact === 'insights.md') {
193
+ targetPath = path.join(targetDir, 'specs', 'insights', artifact);
194
+ } else {
195
+ targetPath = path.join(targetDir, 'specs', artifact);
196
+ }
197
+
198
+ // Ensure target directory exists
199
+ const targetDirPath = path.dirname(targetPath);
200
+ if (!fs.existsSync(targetDirPath)) {
201
+ fs.mkdirSync(targetDirPath, { recursive: true });
202
+ }
203
+
204
+ fs.copyFileSync(srcPath, targetPath);
205
+ copied.push(artifact);
206
+ tracer.logArtifact(`specs/${artifact}`);
207
+ } else {
208
+ tracer.logWarning(`Artifact not found: ${artifact}`);
209
+ }
210
+ }
211
+ return copied;
212
+ }
213
+
214
+ /**
215
+ * Validate artifacts for a phase.
216
+ */
217
+ function validateCurrentArtifacts(targetDir, phase, tracer, verbose) {
218
+ const errors = [];
219
+ const specsDir = path.join(targetDir, 'specs');
220
+
221
+ // Get phase artifacts
222
+ const phaseConfig = PHASE_CONFIG.find(p => p.name === phase);
223
+ if (!phaseConfig) return errors;
224
+
225
+ for (const artifact of phaseConfig.artifacts) {
226
+ if (artifact === 'insights.md') continue; // Skip insights validation
227
+
228
+ const artifactPath = path.join(specsDir, artifact);
229
+ if (!fs.existsSync(artifactPath)) {
230
+ // Not all artifacts are required (e.g., scout only runs for brownfield)
231
+ if (verbose) console.log(` ○ Skipping missing artifact: ${artifact}`);
232
+ continue;
233
+ }
234
+
235
+ // Validate structure
236
+ const content = fs.readFileSync(artifactPath, 'utf8');
237
+ const structureResult = validateMarkdownStructure(content, ['Phase Gate Approval']);
238
+ if (structureResult.missing.length > 0) {
239
+ errors.push(`${artifact}: Missing sections: ${structureResult.missing.join(', ')}`);
240
+ }
241
+
242
+ // Check approval
243
+ const approvalResult = checkApproval(artifactPath);
244
+ if (!approvalResult.approved && verbose) {
245
+ console.log(` ○ ${artifact} not yet approved`);
246
+ }
247
+ }
248
+
249
+ return errors;
250
+ }
251
+
252
+ /**
253
+ * Verify subagent traces in insights file.
254
+ */
255
+ function verifySubagentTraces(targetDir, phase, expectedSubagents, tracer) {
256
+ const insightsPath = path.join(targetDir, 'specs', 'insights', 'insights.md');
257
+
258
+ // Also check phase-specific insights
259
+ const phaseInsightsPath = path.join(targetDir, 'specs', 'insights', `${phase}-insights.md`);
260
+
261
+ let content = '';
262
+ if (fs.existsSync(insightsPath)) {
263
+ content += fs.readFileSync(insightsPath, 'utf8');
264
+ }
265
+ if (fs.existsSync(phaseInsightsPath)) {
266
+ content += fs.readFileSync(phaseInsightsPath, 'utf8');
267
+ }
268
+
269
+ const missing = [];
270
+ for (const agent of expectedSubagents) {
271
+ // Look for patterns like:
272
+ // - "Invoked @Jump Start: Security"
273
+ // - "**Contribution by Jump Start: Security**"
274
+ // - "[2026-02-09T14:00:00Z] Invoked @Jump Start: Security"
275
+ const patterns = [
276
+ new RegExp(`Invoked @?${agent.replace(':', '\\:')}`, 'i'),
277
+ new RegExp(`Contribution by ${agent.replace(':', '\\:')}`, 'i'),
278
+ new RegExp(`${agent.replace(':', '\\:')}.*(?:consultation|invoked|integrated)`, 'i')
279
+ ];
280
+
281
+ const found = patterns.some(p => p.test(content));
282
+ if (found) {
283
+ tracer.logSubagentVerified(agent);
284
+ } else {
285
+ missing.push(agent);
286
+ }
287
+ }
288
+
289
+ if (missing.length > 0) {
290
+ throw new Error(`Missing Subagent Traces: ${missing.join(', ')} not logged in ${phase} insights.`);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Verify final document creation.
296
+ */
297
+ function verifyFinalState(targetDir, tracer) {
298
+ const todoPath = path.join(targetDir, 'specs', 'TODO.md');
299
+ if (fs.existsSync(todoPath)) {
300
+ tracer.logDocumentCreation('TODO.md', 'CREATED');
301
+ } else {
302
+ tracer.logDocumentCreation('TODO.md', 'MISSING');
303
+ }
304
+
305
+ // Check for implementation plan
306
+ const implPlanPath = path.join(targetDir, 'specs', 'implementation-plan.md');
307
+ if (fs.existsSync(implPlanPath)) {
308
+ tracer.logDocumentCreation('implementation-plan.md', 'CREATED');
309
+ }
310
+ }
311
+
312
+ // ─── Main Runner ─────────────────────────────────────────────────────────────
313
+
314
+ /**
315
+ * Run a single scenario simulation.
316
+ */
317
+ async function runHolodeck(scenario, options = {}) {
318
+ const { verifySubagents = false, verbose = false } = options;
319
+ const scenarioDir = path.join(SCENARIOS_DIR, scenario);
320
+
321
+ if (!fs.existsSync(scenarioDir)) {
322
+ throw new Error(`Scenario not found: ${scenario}`);
323
+ }
324
+
325
+ console.log(`\n🚀 Running Holodeck simulation: ${scenario}`);
326
+ console.log(` Subagent verification: ${verifySubagents ? 'ENABLED' : 'disabled'}\n`);
327
+
328
+ const targetDir = setupTempProject(scenario);
329
+ const tracer = new SimulationTracer(targetDir, scenario);
330
+ const usageLogPath = path.join(targetDir, '.jumpstart', 'usage-log.json');
331
+ const statePath = path.join(targetDir, '.jumpstart', 'state', 'state.json');
332
+
333
+ for (let i = 0; i < PHASE_CONFIG.length; i++) {
334
+ const phase = PHASE_CONFIG[i];
335
+ const phaseSrcDir = path.join(scenarioDir, phase.dir);
336
+
337
+ // Skip phases that don't exist in this scenario
338
+ if (!fs.existsSync(phaseSrcDir)) {
339
+ if (verbose) console.log(` ○ Skipping ${phase.name} (no fixtures)`);
340
+ continue;
341
+ }
342
+
343
+ if (verbose) console.log(`\n ▸ Phase: ${phase.name}`);
344
+ tracer.startPhase(phase.name);
345
+
346
+ try {
347
+ // 1. INJECT: Copy Golden Masters to target specs/
348
+ const copied = copyArtifacts(phaseSrcDir, targetDir, phase.artifacts, tracer);
349
+ if (verbose) console.log(` Copied ${copied.length} artifacts`);
350
+
351
+ // 2. MOCK: Write Usage Logs
352
+ logUsage(usageLogPath, {
353
+ agent: phase.name.charAt(0).toUpperCase() + phase.name.slice(1),
354
+ phase: phase.name,
355
+ action: 'generation',
356
+ estimated_tokens: 1000 + Math.floor(Math.random() * 500)
357
+ });
358
+
359
+ // If phase has subagents, log subagent usage
360
+ if (phase.hasSubagents && phase.expectedSubagents) {
361
+ for (const subagent of phase.expectedSubagents) {
362
+ logUsage(usageLogPath, {
363
+ agent: subagent,
364
+ phase: phase.name,
365
+ action: 'consultation',
366
+ estimated_tokens: 300 + Math.floor(Math.random() * 200)
367
+ });
368
+ }
369
+ tracer.logCostTracking(1200, 500);
370
+ } else {
371
+ tracer.logCostTracking(1200, 0);
372
+ }
373
+
374
+ // 3. VALIDATE: Run Artifact Validators
375
+ const validationErrors = validateCurrentArtifacts(targetDir, phase.name, tracer, verbose);
376
+ if (validationErrors.length > 0) {
377
+ validationErrors.forEach(e => tracer.logError(e, phase.name));
378
+ throw new Error(`Validation failed for ${phase.name}: ${validationErrors.join('; ')}`);
379
+ }
380
+ if (verbose) console.log(` Validation: PASS`);
381
+
382
+ // 4. VERIFY SUBAGENTS (The "Robust" Check)
383
+ if (verifySubagents && phase.hasSubagents && phase.expectedSubagents) {
384
+ verifySubagentTraces(targetDir, phase.name, phase.expectedSubagents, tracer);
385
+ if (verbose) console.log(` Subagent traces: VERIFIED`);
386
+ }
387
+
388
+ // 5. HANDOFF: Verify contract with previous phase
389
+ if (i > 0) {
390
+ const upstream = PHASE_CONFIG[i - 1].name;
391
+ // Find the main artifact from upstream
392
+ const upstreamArtifact = PHASE_CONFIG[i - 1].artifacts[0];
393
+ const upstreamPath = path.join(targetDir, 'specs', upstreamArtifact);
394
+
395
+ if (fs.existsSync(upstreamPath) && fs.existsSync(HANDOFFS_DIR)) {
396
+ const report = generateHandoffReport(upstreamPath, upstream, phase.name, HANDOFFS_DIR);
397
+ if (report.valid) {
398
+ tracer.logHandoffValidation('PASS', report);
399
+ if (verbose) console.log(` Handoff (${upstream} → ${phase.name}): PASS`);
400
+ } else {
401
+ tracer.logHandoffValidation('FAIL', report);
402
+ if (verbose) console.log(` Handoff (${upstream} → ${phase.name}): FAIL - ${report.errors.join(', ')}`);
403
+ }
404
+ } else {
405
+ tracer.logHandoffValidation('SKIP');
406
+ if (verbose) console.log(` Handoff: SKIPPED (missing artifacts or schemas)`);
407
+ }
408
+ }
409
+
410
+ // 6. STATE: Update State Store
411
+ updateState({ phase: phase.name, status: 'approved' }, statePath);
412
+
413
+ tracer.endPhase(phase.name, 'PASS');
414
+
415
+ } catch (err) {
416
+ tracer.logError(err.message, phase.name);
417
+ tracer.endPhase(phase.name, 'FAIL');
418
+ if (!verbose) console.log(` ✗ ${phase.name}: ${err.message}`);
419
+ }
420
+ }
421
+
422
+ // 7. FINAL: Verify Document Creation
423
+ verifyFinalState(targetDir, tracer);
424
+
425
+ // Generate summary
426
+ const usageSummary = summarizeUsage(usageLogPath);
427
+ tracer.printSummary();
428
+
429
+ // Save report
430
+ const reportPath = path.join(options.output || REPORTS_DIR, `${scenario}-${Date.now()}.json`);
431
+ tracer.saveReport(reportPath);
432
+ console.log(`Report saved: ${reportPath}\n`);
433
+
434
+ return tracer.getReport();
435
+ }
436
+
437
+ /**
438
+ * Run all scenarios.
439
+ */
440
+ async function runAllScenarios(options) {
441
+ const scenarios = listScenarios();
442
+ if (scenarios.length === 0) {
443
+ console.log('No scenarios to run.');
444
+ return [];
445
+ }
446
+
447
+ const results = [];
448
+ for (const scenario of scenarios) {
449
+ try {
450
+ const report = await runHolodeck(scenario, options);
451
+ results.push({ scenario, success: report.success, report });
452
+ } catch (err) {
453
+ results.push({ scenario, success: false, error: err.message });
454
+ }
455
+ }
456
+
457
+ // Print summary
458
+ console.log('\n═══════════════════════════════════════════════════════');
459
+ console.log(' ALL SCENARIOS SUMMARY ');
460
+ console.log('═══════════════════════════════════════════════════════');
461
+ const passed = results.filter(r => r.success).length;
462
+ const failed = results.length - passed;
463
+ console.log(`Total: ${results.length} Passed: ${passed} Failed: ${failed}`);
464
+ results.forEach(r => {
465
+ const icon = r.success ? '✓' : '✗';
466
+ console.log(` ${icon} ${r.scenario}`);
467
+ });
468
+ console.log('═══════════════════════════════════════════════════════\n');
469
+
470
+ return results;
471
+ }
472
+
473
+ // ─── Main Entry Point ────────────────────────────────────────────────────────
474
+
475
+ async function main() {
476
+ const options = parseArgs();
477
+
478
+ if (options.help) {
479
+ printHelp();
480
+ process.exit(0);
481
+ }
482
+
483
+ if (options.list) {
484
+ listScenarios();
485
+ process.exit(0);
486
+ }
487
+
488
+ // Ensure output directory exists
489
+ if (!fs.existsSync(options.output)) {
490
+ fs.mkdirSync(options.output, { recursive: true });
491
+ }
492
+
493
+ try {
494
+ if (options.all) {
495
+ const results = await runAllScenarios(options);
496
+ const allPassed = results.every(r => r.success);
497
+ process.exit(allPassed ? 0 : 1);
498
+ } else if (options.scenario) {
499
+ const report = await runHolodeck(options.scenario, options);
500
+ process.exit(report.success ? 0 : 1);
501
+ } else {
502
+ console.log('Error: Please specify --scenario <name> or --all');
503
+ printHelp();
504
+ process.exit(1);
505
+ }
506
+ } catch (err) {
507
+ console.error(`\n❌ Error: ${err.message}\n`);
508
+ process.exit(1);
509
+ }
510
+ }
511
+
512
+ main();