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.
- package/.cursorrules +44 -42
- package/.github/agents/jumpstart-researcher.agent.md +1 -1
- package/.github/copilot-instructions.md +71 -69
- package/.jumpstart/agents/analyst.md +539 -537
- package/.jumpstart/agents/architect.md +740 -738
- package/.jumpstart/agents/developer.md +715 -713
- package/.jumpstart/compat/assistant-mapping.md +32 -0
- package/.jumpstart/guides/context7-usage.md +242 -0
- package/.jumpstart/templates/documentation-audit.md +8 -2
- package/CLAUDE.md +54 -52
- package/bin/headless-runner.js +658 -0
- package/bin/holodeck.js +512 -0
- package/bin/lib/headless-runner.js +658 -0
- package/bin/lib/holodeck.js +512 -0
- package/package.json +13 -2
package/bin/holodeck.js
ADDED
|
@@ -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();
|