@nclamvn/vibecode-cli 1.0.0 → 1.1.0

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,290 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Review Command
3
+ // ═══════════════════════════════════════════════════════════════════════════════
4
+
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import path from 'path';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import inquirer from 'inquirer';
11
+ import { workspaceExists, getProjectName } from '../core/workspace.js';
12
+ import {
13
+ getCurrentSessionId,
14
+ getCurrentSessionPath,
15
+ writeSessionFile,
16
+ readSessionFile,
17
+ sessionFileExists
18
+ } from '../core/session.js';
19
+ import { getCurrentState, transitionTo } from '../core/state-machine.js';
20
+ import { getSpecHash } from '../core/contract.js';
21
+ import { STATES } from '../config/constants.js';
22
+ import { getReviewReportTemplate } from '../config/templates.js';
23
+ import { pathExists } from '../utils/files.js';
24
+ import { printBox, printError, printSuccess, printWarning, printNextStep } from '../ui/output.js';
25
+
26
+ const execAsync = promisify(exec);
27
+
28
+ export async function reviewCommand(options = {}) {
29
+ const spinner = ora('Starting review...').start();
30
+
31
+ try {
32
+ // Check workspace
33
+ if (!await workspaceExists()) {
34
+ spinner.fail();
35
+ printError('No Vibecode workspace found. Run `vibecode init` first.');
36
+ process.exit(1);
37
+ }
38
+
39
+ // Check state
40
+ const currentState = await getCurrentState();
41
+ if (currentState !== STATES.BUILD_DONE) {
42
+ spinner.fail();
43
+ printError(`Cannot review in state: ${currentState}`);
44
+ console.log('Complete build first with `vibecode build --complete`.');
45
+ process.exit(1);
46
+ }
47
+
48
+ const projectName = await getProjectName();
49
+ const sessionId = await getCurrentSessionId();
50
+ const sessionPath = await getCurrentSessionPath();
51
+ const specHash = await getSpecHash();
52
+
53
+ spinner.text = 'Running automated checks...';
54
+
55
+ // Run automated checks
56
+ const checks = [];
57
+
58
+ // Check 1: Evidence exists
59
+ const evidencePath = path.join(sessionPath, 'evidence');
60
+ const hasEvidence = await pathExists(path.join(evidencePath, 'changes.diff')) ||
61
+ await pathExists(path.join(evidencePath, 'build.log'));
62
+ checks.push({
63
+ name: 'Evidence captured',
64
+ passed: hasEvidence,
65
+ message: hasEvidence ? 'Evidence files found' : 'No evidence files found'
66
+ });
67
+
68
+ // Check 2: Build report exists
69
+ const hasBuildReport = await sessionFileExists('build_report.md');
70
+ checks.push({
71
+ name: 'Build report',
72
+ passed: hasBuildReport,
73
+ message: hasBuildReport ? 'build_report.md exists' : 'Missing build_report.md'
74
+ });
75
+
76
+ // Check 3: Try npm test if package.json exists
77
+ const npmTestResult = await runNpmTest();
78
+ if (npmTestResult.ran) {
79
+ checks.push({
80
+ name: 'npm test',
81
+ passed: npmTestResult.passed,
82
+ message: npmTestResult.message
83
+ });
84
+ }
85
+
86
+ // Check 4: Try npm run lint if available
87
+ const lintResult = await runNpmLint();
88
+ if (lintResult.ran) {
89
+ checks.push({
90
+ name: 'npm run lint',
91
+ passed: lintResult.passed,
92
+ message: lintResult.message
93
+ });
94
+ }
95
+
96
+ // Check 5: Git status clean (no uncommitted changes)
97
+ const gitStatus = await checkGitStatus();
98
+ checks.push({
99
+ name: 'Git status',
100
+ passed: gitStatus.clean,
101
+ message: gitStatus.message
102
+ });
103
+
104
+ spinner.stop();
105
+
106
+ // Display automated check results
107
+ console.log();
108
+ console.log(chalk.cyan('📋 Automated Checks:'));
109
+ console.log(chalk.gray('─'.repeat(50)));
110
+
111
+ for (const check of checks) {
112
+ const icon = check.passed ? chalk.green('✅') : chalk.red('❌');
113
+ console.log(` ${icon} ${check.name}: ${chalk.gray(check.message)}`);
114
+ }
115
+
116
+ console.log(chalk.gray('─'.repeat(50)));
117
+
118
+ // If skip-manual flag, auto-determine result
119
+ if (options.skipManual) {
120
+ const allPassed = checks.every(c => c.passed);
121
+ await finalizeReview(allPassed, checks, projectName, sessionId, specHash);
122
+ return;
123
+ }
124
+
125
+ // Manual checklist
126
+ console.log();
127
+ console.log(chalk.cyan('📝 Manual Verification:'));
128
+
129
+ // Read contract for acceptance criteria
130
+ let acceptanceCriteria = [];
131
+ if (await sessionFileExists('contract.md')) {
132
+ const contract = await readSessionFile('contract.md');
133
+ const match = contract.match(/## ✔️ Acceptance Criteria[\s\S]*?(?=\n---|\n##|$)/);
134
+ if (match) {
135
+ const criteriaText = match[0];
136
+ const criteriaLines = criteriaText.match(/- \[[ x]\] .+/g) || [];
137
+ acceptanceCriteria = criteriaLines.map(line => line.replace(/- \[[ x]\] /, ''));
138
+ }
139
+ }
140
+
141
+ if (acceptanceCriteria.length > 0) {
142
+ console.log(chalk.gray('\nVerify each acceptance criterion from the contract:\n'));
143
+
144
+ const { verified } = await inquirer.prompt([
145
+ {
146
+ type: 'checkbox',
147
+ name: 'verified',
148
+ message: 'Check all criteria that are met:',
149
+ choices: acceptanceCriteria.map(c => ({ name: c, checked: false }))
150
+ }
151
+ ]);
152
+
153
+ const allCriteriaMet = verified.length === acceptanceCriteria.length;
154
+
155
+ checks.push({
156
+ name: 'Acceptance criteria',
157
+ passed: allCriteriaMet,
158
+ message: `${verified.length}/${acceptanceCriteria.length} criteria verified`
159
+ });
160
+ }
161
+
162
+ // Final confirmation
163
+ console.log();
164
+ const { approve } = await inquirer.prompt([
165
+ {
166
+ type: 'confirm',
167
+ name: 'approve',
168
+ message: 'Do you approve this build for release?',
169
+ default: false
170
+ }
171
+ ]);
172
+
173
+ const allPassed = approve && checks.every(c => c.passed);
174
+ await finalizeReview(allPassed, checks, projectName, sessionId, specHash);
175
+
176
+ } catch (error) {
177
+ spinner.fail('Review failed');
178
+ printError(error.message);
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ async function finalizeReview(passed, checks, projectName, sessionId, specHash) {
184
+ const spinner = ora('Generating review report...').start();
185
+
186
+ // Generate report
187
+ const reportContent = getReviewReportTemplate(
188
+ projectName,
189
+ sessionId,
190
+ specHash,
191
+ checks,
192
+ passed
193
+ );
194
+
195
+ await writeSessionFile('review_report.md', reportContent);
196
+
197
+ // Transition state
198
+ if (passed) {
199
+ await transitionTo(STATES.REVIEW_PASSED, 'review_passed');
200
+ spinner.succeed('Review PASSED!');
201
+
202
+ const content = `✅ REVIEW PASSED
203
+
204
+ Project: ${projectName}
205
+ Spec Hash: ${specHash}
206
+
207
+ All checks passed!
208
+ Ready for release.`;
209
+
210
+ console.log();
211
+ printBox(content, { borderColor: 'green' });
212
+ printNextStep('Run `vibecode snapshot` to create release');
213
+
214
+ } else {
215
+ await transitionTo(STATES.REVIEW_FAILED, 'review_failed');
216
+ spinner.warn('Review FAILED');
217
+
218
+ const failedChecks = checks.filter(c => !c.passed);
219
+
220
+ const content = `❌ REVIEW FAILED
221
+
222
+ Project: ${projectName}
223
+ Spec Hash: ${specHash}
224
+
225
+ Failed checks:
226
+ ${failedChecks.map(c => ` • ${c.name}: ${c.message}`).join('\n')}`;
227
+
228
+ console.log();
229
+ printBox(content, { borderColor: 'red' });
230
+ printNextStep('Fix issues and run `vibecode build --start` to rebuild');
231
+ }
232
+ }
233
+
234
+ async function runNpmTest() {
235
+ try {
236
+ // Check if package.json exists
237
+ const packageExists = await pathExists('package.json');
238
+ if (!packageExists) {
239
+ return { ran: false };
240
+ }
241
+
242
+ // Check if test script exists
243
+ const fs = await import('fs-extra');
244
+ const pkg = await fs.default.readJson('package.json');
245
+ if (!pkg.scripts?.test || pkg.scripts.test.includes('no test specified')) {
246
+ return { ran: false };
247
+ }
248
+
249
+ // Run tests
250
+ await execAsync('npm test', { timeout: 120000 });
251
+ return { ran: true, passed: true, message: 'All tests passed' };
252
+
253
+ } catch (error) {
254
+ return { ran: true, passed: false, message: error.message.split('\n')[0] };
255
+ }
256
+ }
257
+
258
+ async function runNpmLint() {
259
+ try {
260
+ const packageExists = await pathExists('package.json');
261
+ if (!packageExists) {
262
+ return { ran: false };
263
+ }
264
+
265
+ const fs = await import('fs-extra');
266
+ const pkg = await fs.default.readJson('package.json');
267
+ if (!pkg.scripts?.lint) {
268
+ return { ran: false };
269
+ }
270
+
271
+ await execAsync('npm run lint', { timeout: 60000 });
272
+ return { ran: true, passed: true, message: 'No linting errors' };
273
+
274
+ } catch (error) {
275
+ return { ran: true, passed: false, message: 'Linting errors found' };
276
+ }
277
+ }
278
+
279
+ async function checkGitStatus() {
280
+ try {
281
+ const { stdout } = await execAsync('git status --porcelain');
282
+ const clean = stdout.trim() === '';
283
+ return {
284
+ clean,
285
+ message: clean ? 'Working tree clean' : 'Uncommitted changes exist'
286
+ };
287
+ } catch (error) {
288
+ return { clean: true, message: 'Git not available' };
289
+ }
290
+ }
@@ -0,0 +1,252 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Snapshot Command
3
+ // ═══════════════════════════════════════════════════════════════════════════════
4
+
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import path from 'path';
8
+ import inquirer from 'inquirer';
9
+ import { exec } from 'child_process';
10
+ import { promisify } from 'util';
11
+ import { workspaceExists, getProjectName, loadState, saveState } from '../core/workspace.js';
12
+ import {
13
+ getCurrentSessionId,
14
+ getCurrentSessionPath,
15
+ writeSessionFile,
16
+ getSessionFilesStatus
17
+ } from '../core/session.js';
18
+ import { getCurrentState, transitionTo } from '../core/state-machine.js';
19
+ import { getSpecHash } from '../core/contract.js';
20
+ import { STATES } from '../config/constants.js';
21
+ import { getManifestTemplate } from '../config/templates.js';
22
+ import { pathExists, readJson, writeJson, appendToFile } from '../utils/files.js';
23
+ import { printBox, printError, printSuccess, printWarning, printNextStep } from '../ui/output.js';
24
+
25
+ const execAsync = promisify(exec);
26
+
27
+ export async function snapshotCommand(options = {}) {
28
+ try {
29
+ // Check workspace
30
+ if (!await workspaceExists()) {
31
+ printError('No Vibecode workspace found. Run `vibecode init` first.');
32
+ process.exit(1);
33
+ }
34
+
35
+ // Check state
36
+ const currentState = await getCurrentState();
37
+ if (currentState !== STATES.REVIEW_PASSED) {
38
+ printError(`Cannot create snapshot in state: ${currentState}`);
39
+ console.log('Review must pass first. Run `vibecode review`.');
40
+ process.exit(1);
41
+ }
42
+
43
+ const projectName = await getProjectName();
44
+ const sessionId = await getCurrentSessionId();
45
+ const sessionPath = await getCurrentSessionPath();
46
+ const specHash = await getSpecHash();
47
+
48
+ // Determine version bump
49
+ let versionBump = options.major ? 'major' : options.minor ? 'minor' : options.patch ? 'patch' : null;
50
+
51
+ if (!versionBump) {
52
+ const { bump } = await inquirer.prompt([
53
+ {
54
+ type: 'list',
55
+ name: 'bump',
56
+ message: 'What type of release is this?',
57
+ choices: [
58
+ { name: 'patch (bug fixes)', value: 'patch' },
59
+ { name: 'minor (new features)', value: 'minor' },
60
+ { name: 'major (breaking changes)', value: 'major' }
61
+ ],
62
+ default: 'patch'
63
+ }
64
+ ]);
65
+ versionBump = bump;
66
+ }
67
+
68
+ const spinner = ora('Creating snapshot...').start();
69
+
70
+ // Get current version and calculate new version
71
+ let currentVersion = '0.0.0';
72
+ let newVersion = '0.0.1';
73
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
74
+
75
+ if (await pathExists(packageJsonPath)) {
76
+ const pkg = await readJson(packageJsonPath);
77
+ currentVersion = pkg.version || '0.0.0';
78
+ newVersion = bumpVersion(currentVersion, versionBump);
79
+
80
+ // Update package.json
81
+ spinner.text = 'Updating package.json...';
82
+ pkg.version = newVersion;
83
+ await writeJson(packageJsonPath, pkg);
84
+ }
85
+
86
+ // Collect session files
87
+ spinner.text = 'Collecting files...';
88
+ const files = await collectSessionFiles(sessionPath);
89
+
90
+ // Generate manifest
91
+ spinner.text = 'Generating manifest...';
92
+ const manifest = getManifestTemplate(
93
+ projectName,
94
+ newVersion,
95
+ specHash,
96
+ sessionId,
97
+ files
98
+ );
99
+ await writeSessionFile('manifest.json', JSON.stringify(manifest, null, 2));
100
+
101
+ // Update CHANGELOG
102
+ spinner.text = 'Updating CHANGELOG...';
103
+ await updateChangelog(newVersion, specHash, projectName);
104
+
105
+ // Create git tag if git is available
106
+ spinner.text = 'Creating git tag...';
107
+ const gitTagged = await createGitTag(newVersion, specHash);
108
+
109
+ // Update state
110
+ const stateData = await loadState();
111
+ stateData.shipped_version = newVersion;
112
+ stateData.shipped_at = new Date().toISOString();
113
+ await saveState(stateData);
114
+
115
+ // Transition state
116
+ await transitionTo(STATES.SHIPPED, 'snapshot_created');
117
+
118
+ spinner.succeed('Snapshot created!');
119
+
120
+ // Success output
121
+ const content = `🚀 SHIPPED!
122
+
123
+ Project: ${projectName}
124
+ Version: ${currentVersion} → ${newVersion}
125
+ Spec Hash: ${specHash}
126
+ Session: ${sessionId}
127
+
128
+ Files:
129
+ • manifest.json - Release manifest
130
+ • CHANGELOG.md - Updated
131
+ ${gitTagged ? `• Git tag: v${newVersion}` : ''}`;
132
+
133
+ console.log();
134
+ printBox(content, { borderColor: 'green' });
135
+
136
+ console.log();
137
+ console.log(chalk.cyan('📦 Release Summary:'));
138
+ console.log(chalk.gray(` Version: ${newVersion}`));
139
+ console.log(chalk.gray(` Spec Hash: ${specHash}`));
140
+ console.log(chalk.gray(` Files: ${files.length} session files`));
141
+ if (gitTagged) {
142
+ console.log(chalk.gray(` Git Tag: v${newVersion}`));
143
+ }
144
+
145
+ console.log();
146
+ printSuccess('Your project has been shipped!');
147
+
148
+ if (await pathExists(packageJsonPath)) {
149
+ console.log();
150
+ console.log(chalk.cyan('📤 To publish:'));
151
+ console.log(chalk.gray(' npm publish'));
152
+ console.log(chalk.gray(' # or'));
153
+ console.log(chalk.gray(' git push && git push --tags'));
154
+ }
155
+
156
+ } catch (error) {
157
+ printError(error.message);
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ function bumpVersion(version, type) {
163
+ const parts = version.split('.').map(Number);
164
+
165
+ switch (type) {
166
+ case 'major':
167
+ return `${parts[0] + 1}.0.0`;
168
+ case 'minor':
169
+ return `${parts[0]}.${parts[1] + 1}.0`;
170
+ case 'patch':
171
+ default:
172
+ return `${parts[0]}.${parts[1]}.${parts[2] + 1}`;
173
+ }
174
+ }
175
+
176
+ async function collectSessionFiles(sessionPath) {
177
+ const files = [];
178
+ const fs = await import('fs-extra');
179
+
180
+ const sessionFiles = [
181
+ 'intake.md',
182
+ 'blueprint.md',
183
+ 'contract.md',
184
+ 'plan.md',
185
+ 'coder_pack.md',
186
+ 'build_report.md',
187
+ 'review_report.md'
188
+ ];
189
+
190
+ for (const file of sessionFiles) {
191
+ const filePath = path.join(sessionPath, file);
192
+ if (await pathExists(filePath)) {
193
+ const stat = await fs.default.stat(filePath);
194
+ files.push({
195
+ name: file,
196
+ size: stat.size,
197
+ modified: stat.mtime.toISOString()
198
+ });
199
+ }
200
+ }
201
+
202
+ return files;
203
+ }
204
+
205
+ async function updateChangelog(version, specHash, projectName) {
206
+ const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
207
+ const timestamp = new Date().toISOString().split('T')[0];
208
+
209
+ const newEntry = `
210
+ ## [${version}] - ${timestamp}
211
+
212
+ ### Shipped via Vibecode CLI
213
+ - Spec Hash: \`${specHash}\`
214
+ - Project: ${projectName}
215
+ - Generated by vibecode-cli
216
+
217
+ ---
218
+ `;
219
+
220
+ if (await pathExists(changelogPath)) {
221
+ const fs = await import('fs-extra');
222
+ const content = await fs.default.readFile(changelogPath, 'utf-8');
223
+
224
+ // Insert after the header
225
+ const headerMatch = content.match(/^# .+\n/);
226
+ if (headerMatch) {
227
+ const newContent = headerMatch[0] + newEntry + content.slice(headerMatch[0].length);
228
+ await fs.default.writeFile(changelogPath, newContent, 'utf-8');
229
+ } else {
230
+ await fs.default.writeFile(changelogPath, `# Changelog\n${newEntry}${content}`, 'utf-8');
231
+ }
232
+ } else {
233
+ const fs = await import('fs-extra');
234
+ await fs.default.writeFile(changelogPath, `# Changelog\n${newEntry}`, 'utf-8');
235
+ }
236
+ }
237
+
238
+ async function createGitTag(version, specHash) {
239
+ try {
240
+ // Check if git is available
241
+ await execAsync('git status');
242
+
243
+ // Create annotated tag
244
+ const tagMessage = `Release v${version}\n\nSpec Hash: ${specHash}\nGenerated by vibecode-cli`;
245
+ await execAsync(`git tag -a v${version} -m "${tagMessage}"`);
246
+
247
+ return true;
248
+ } catch (error) {
249
+ // Git not available or tag failed
250
+ return false;
251
+ }
252
+ }
@@ -58,7 +58,12 @@ export async function statusCommand(options = {}) {
58
58
  console.log(chalk.cyan('📁 Session Files:'));
59
59
  console.log(chalk.gray(` ├── intake.md ${filesStatus.intake ? '✅' : '⬜'}`));
60
60
  console.log(chalk.gray(` ├── blueprint.md ${filesStatus.blueprint ? '✅' : '⬜'}`));
61
- console.log(chalk.gray(` └── contract.md ${filesStatus.contract ? (specHash ? '🔒' : '📝') : '⬜'}`));
61
+ console.log(chalk.gray(` ├── contract.md ${filesStatus.contract ? (specHash ? '🔒' : '📝') : '⬜'}`));
62
+ console.log(chalk.gray(` ├── plan.md ${filesStatus.plan ? '✅' : '⬜'}`));
63
+ console.log(chalk.gray(` ├── coder_pack.md ${filesStatus.coderPack ? '✅' : '⬜'}`));
64
+ console.log(chalk.gray(` ├── build_report.md ${filesStatus.buildReport ? '✅' : '⬜'}`));
65
+ console.log(chalk.gray(` ├── review_report.md ${filesStatus.reviewReport ? '✅' : '⬜'}`));
66
+ console.log(chalk.gray(` └── manifest.json ${filesStatus.manifest ? '✅' : '⬜'}`));
62
67
  }
63
68
 
64
69
  // Next step hint
@@ -67,7 +72,13 @@ export async function statusCommand(options = {}) {
67
72
  [STATES.INTAKE_CAPTURED]: 'Create blueprint for your project',
68
73
  [STATES.BLUEPRINT_DRAFTED]: 'Create contract with deliverables',
69
74
  [STATES.CONTRACT_DRAFTED]: 'Run `vibecode lock` to finalize contract',
70
- [STATES.CONTRACT_LOCKED]: 'Contract locked! Ready for build (Phase B)',
75
+ [STATES.CONTRACT_LOCKED]: 'Run `vibecode plan` to create execution plan',
76
+ [STATES.PLAN_CREATED]: 'Run `vibecode build --start` to begin building',
77
+ [STATES.BUILD_IN_PROGRESS]: 'Build in progress. Run `vibecode build --complete` when done',
78
+ [STATES.BUILD_DONE]: 'Run `vibecode review` to validate your build',
79
+ [STATES.REVIEW_PASSED]: 'Run `vibecode snapshot` to create release',
80
+ [STATES.REVIEW_FAILED]: 'Fix issues and run `vibecode build --start` to rebuild',
81
+ [STATES.SHIPPED]: 'Project shipped! Start a new session with `vibecode start`',
71
82
  };
72
83
 
73
84
  printNextStep(hints[stateData.current_state] || 'Continue with your workflow');
@@ -3,51 +3,61 @@
3
3
  // Spec Hash: 0fe43335f5a325e3279a079ce616c052
4
4
  // ═══════════════════════════════════════════════════════════════════════════════
5
5
 
6
- export const VERSION = '1.0.0';
6
+ export const VERSION = '1.1.0';
7
7
  export const SPEC_HASH = '0fe43335f5a325e3279a079ce616c052';
8
8
 
9
9
  // ─────────────────────────────────────────────────────────────────────────────
10
- // State Machine
10
+ // State Machine - Phase A + B
11
11
  // ─────────────────────────────────────────────────────────────────────────────
12
12
 
13
13
  export const STATES = {
14
+ // Phase A: Planning
14
15
  INIT: 'INIT',
15
16
  INTAKE_CAPTURED: 'INTAKE_CAPTURED',
16
17
  BLUEPRINT_DRAFTED: 'BLUEPRINT_DRAFTED',
17
18
  CONTRACT_DRAFTED: 'CONTRACT_DRAFTED',
18
19
  CONTRACT_LOCKED: 'CONTRACT_LOCKED',
20
+ // Phase B: Execution
21
+ PLAN_CREATED: 'PLAN_CREATED',
19
22
  BUILD_IN_PROGRESS: 'BUILD_IN_PROGRESS',
20
23
  BUILD_DONE: 'BUILD_DONE',
21
24
  REVIEW_PASSED: 'REVIEW_PASSED',
22
- REVIEW_FAILED: 'REVIEW_FAILED'
25
+ REVIEW_FAILED: 'REVIEW_FAILED',
26
+ SHIPPED: 'SHIPPED'
23
27
  };
24
28
 
25
29
  export const TRANSITIONS = {
30
+ // Phase A transitions
26
31
  [STATES.INIT]: [STATES.INTAKE_CAPTURED],
27
32
  [STATES.INTAKE_CAPTURED]: [STATES.BLUEPRINT_DRAFTED],
28
33
  [STATES.BLUEPRINT_DRAFTED]: [STATES.CONTRACT_DRAFTED, STATES.INTAKE_CAPTURED],
29
34
  [STATES.CONTRACT_DRAFTED]: [STATES.CONTRACT_LOCKED, STATES.BLUEPRINT_DRAFTED],
30
- [STATES.CONTRACT_LOCKED]: [STATES.BUILD_IN_PROGRESS],
35
+ // Phase B transitions
36
+ [STATES.CONTRACT_LOCKED]: [STATES.PLAN_CREATED],
37
+ [STATES.PLAN_CREATED]: [STATES.BUILD_IN_PROGRESS],
31
38
  [STATES.BUILD_IN_PROGRESS]: [STATES.BUILD_DONE],
32
39
  [STATES.BUILD_DONE]: [STATES.REVIEW_PASSED, STATES.REVIEW_FAILED],
33
- [STATES.REVIEW_PASSED]: [],
34
- [STATES.REVIEW_FAILED]: [STATES.BUILD_IN_PROGRESS, STATES.CONTRACT_DRAFTED]
40
+ [STATES.REVIEW_PASSED]: [STATES.SHIPPED],
41
+ [STATES.REVIEW_FAILED]: [STATES.BUILD_IN_PROGRESS, STATES.PLAN_CREATED],
42
+ [STATES.SHIPPED]: []
35
43
  };
36
44
 
37
45
  // ─────────────────────────────────────────────────────────────────────────────
38
- // Progress Display Mapping
46
+ // Progress Display Mapping (7 stages)
39
47
  // ─────────────────────────────────────────────────────────────────────────────
40
48
 
41
49
  export const PROGRESS_MAP = {
42
- [STATES.INIT]: { intake: '🔄', blueprint: '⬜', contract: '⬜', build: '⬜', ship: '⬜' },
43
- [STATES.INTAKE_CAPTURED]: { intake: '✅', blueprint: '🔄', contract: '⬜', build: '⬜', ship: '⬜' },
44
- [STATES.BLUEPRINT_DRAFTED]: { intake: '✅', blueprint: '✅', contract: '🔄', build: '⬜', ship: '⬜' },
45
- [STATES.CONTRACT_DRAFTED]: { intake: '✅', blueprint: '✅', contract: '🔄', build: '⬜', ship: '⬜' },
46
- [STATES.CONTRACT_LOCKED]: { intake: '✅', blueprint: '✅', contract: '✅', build: '🔄', ship: '⬜' },
47
- [STATES.BUILD_IN_PROGRESS]: { intake: '✅', blueprint: '✅', contract: '✅', build: '🔄', ship: '⬜' },
48
- [STATES.BUILD_DONE]: { intake: '✅', blueprint: '✅', contract: '✅', build: '✅', ship: '🔄' },
49
- [STATES.REVIEW_PASSED]: { intake: '✅', blueprint: '✅', contract: '✅', build: '✅', ship: '' },
50
- [STATES.REVIEW_FAILED]: { intake: '✅', blueprint: '✅', contract: '✅', build: '⚠️', ship: '' }
50
+ [STATES.INIT]: { intake: '🔄', blueprint: '⬜', contract: '⬜', plan: '⬜', build: '⬜', review: '⬜', ship: '⬜' },
51
+ [STATES.INTAKE_CAPTURED]: { intake: '✅', blueprint: '🔄', contract: '⬜', plan: '⬜', build: '⬜', review: '⬜', ship: '⬜' },
52
+ [STATES.BLUEPRINT_DRAFTED]: { intake: '✅', blueprint: '✅', contract: '🔄', plan: '⬜', build: '⬜', review: '⬜', ship: '⬜' },
53
+ [STATES.CONTRACT_DRAFTED]: { intake: '✅', blueprint: '✅', contract: '🔄', plan: '⬜', build: '⬜', review: '⬜', ship: '⬜' },
54
+ [STATES.CONTRACT_LOCKED]: { intake: '✅', blueprint: '✅', contract: '✅', plan: '🔄', build: '⬜', review: '⬜', ship: '⬜' },
55
+ [STATES.PLAN_CREATED]: { intake: '✅', blueprint: '✅', contract: '✅', plan: '✅', build: '🔄', review: '⬜', ship: '⬜' },
56
+ [STATES.BUILD_IN_PROGRESS]: { intake: '✅', blueprint: '✅', contract: '✅', plan: '✅', build: '🔄', review: '⬜', ship: '⬜' },
57
+ [STATES.BUILD_DONE]: { intake: '✅', blueprint: '✅', contract: '✅', plan: '✅', build: '✅', review: '🔄', ship: '' },
58
+ [STATES.REVIEW_PASSED]: { intake: '✅', blueprint: '✅', contract: '✅', plan: '✅', build: '', review: '✅', ship: '🔄' },
59
+ [STATES.REVIEW_FAILED]: { intake: '✅', blueprint: '✅', contract: '✅', plan: '✅', build: '⚠️', review: '❌', ship: '⬜' },
60
+ [STATES.SHIPPED]: { intake: '✅', blueprint: '✅', contract: '✅', plan: '✅', build: '✅', review: '✅', ship: '✅' }
51
61
  };
52
62
 
53
63
  // ─────────────────────────────────────────────────────────────────────────────