openairev 0.2.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,50 @@
1
+ import chalk from 'chalk';
2
+ import { configExists } from '../config/config-loader.js';
3
+
4
+ export const STAGE_LABELS = {
5
+ analyze: 'Analyzing',
6
+ awaiting_user: 'Waiting for User',
7
+ planning: 'Planning',
8
+ plan_review: 'Plan Review',
9
+ plan_fix: 'Fixing Plan',
10
+ implementation: 'Implementing',
11
+ code_review: 'Code Review',
12
+ code_fix: 'Fixing Code',
13
+ done: 'Done',
14
+ };
15
+
16
+ export function stageLabel(stage) {
17
+ return STAGE_LABELS[stage] || stage;
18
+ }
19
+
20
+ const STATUS_COLORS = {
21
+ approved: chalk.green,
22
+ completed: chalk.green,
23
+ active: chalk.blue,
24
+ blocked: chalk.yellow,
25
+ needs_changes: chalk.yellow,
26
+ in_progress: chalk.yellow,
27
+ max_rounds_reached: chalk.yellow,
28
+ reject: chalk.red,
29
+ rejected: chalk.red,
30
+ error: chalk.red,
31
+ };
32
+
33
+ export function statusColor(status) {
34
+ return STATUS_COLORS[status] || chalk.white;
35
+ }
36
+
37
+ export function timeAgo(date) {
38
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
39
+ if (seconds < 60) return 'just now';
40
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
41
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
42
+ return `${Math.floor(seconds / 86400)}d ago`;
43
+ }
44
+
45
+ export function requireConfig(cwd) {
46
+ if (!configExists(cwd)) {
47
+ console.log(chalk.red('No .openairev/config.yaml found. Run `openairev init` first.'));
48
+ process.exit(1);
49
+ }
50
+ }
@@ -0,0 +1,76 @@
1
+ import chalk from 'chalk';
2
+ import { listSessions } from '../session/session-manager.js';
3
+ import { listChains } from '../session/chain-manager.js';
4
+ import { statusColor, stageLabel } from './format-helpers.js';
5
+
6
+ export async function historyCommand(options) {
7
+ const cwd = process.cwd();
8
+ const limit = options.limit || 10;
9
+
10
+ if (options.chains) {
11
+ showChainHistory(cwd, limit);
12
+ } else {
13
+ showSessionHistory(cwd, limit);
14
+ }
15
+ }
16
+
17
+ function showChainHistory(cwd, limit) {
18
+ const chains = listChains(cwd, { limit });
19
+
20
+ if (chains.length === 0) {
21
+ console.log(chalk.dim('\nNo workflow chains found.\n'));
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.bold(`\nWorkflow Chains (last ${chains.length})\n`));
26
+
27
+ for (const c of chains) {
28
+ const date = new Date(c.updated).toLocaleString();
29
+ const executorName = c.participants?.executor || '?';
30
+ const reviewerName = c.participants?.reviewer || '?';
31
+ const roundCount = c.rounds?.length || 0;
32
+ const topic = c.task?.user_request;
33
+ const stage = c.stage ? ` [${stageLabel(c.stage)}]` : '';
34
+
35
+ console.log(` ${chalk.dim(c.chain_id)}`);
36
+ console.log(` ${date} ${statusColor(c.status)(c.status)}${chalk.dim(stage)} ${executorName} ↔ ${reviewerName}`);
37
+ console.log(` Rounds: ${roundCount}/${c.max_rounds}`);
38
+ if (topic) console.log(` Topic: ${chalk.dim(topic)}`);
39
+
40
+ const lastRound = c.rounds?.[c.rounds.length - 1];
41
+ if (lastRound?.review?.verdict) {
42
+ const v = lastRound.review.verdict;
43
+ console.log(` Last verdict: ${statusColor(v.status)(v.status)} (${((v.confidence || 0) * 100).toFixed(0)}%)`);
44
+ if (v.critical_issues?.length) {
45
+ console.log(` ${chalk.red(v.critical_issues.length + ' critical issue(s)')}`);
46
+ }
47
+ }
48
+ console.log('');
49
+ }
50
+ }
51
+
52
+ function showSessionHistory(cwd, limit) {
53
+ const sessions = listSessions(cwd, limit);
54
+
55
+ if (sessions.length === 0) {
56
+ console.log(chalk.dim('\nNo review sessions found.\n'));
57
+ return;
58
+ }
59
+
60
+ console.log(chalk.bold(`\nReview History (last ${sessions.length})\n`));
61
+
62
+ for (const s of sessions) {
63
+ const date = new Date(s.created).toLocaleString();
64
+ const verdict = s.final_verdict;
65
+ const verdictStr = verdict
66
+ ? `${verdict.status} (${((verdict.confidence || 0) * 100).toFixed(0)}%)`
67
+ : 'no verdict';
68
+
69
+ console.log(` ${chalk.dim(s.id)}`);
70
+ console.log(` ${date} ${statusColor(s.status)(s.status)} ${s.executor} → ${s.reviewer} ${chalk.dim(verdictStr)}`);
71
+ if (verdict?.critical_issues?.length) {
72
+ console.log(` ${chalk.red(verdict.critical_issues.length + ' critical issue(s)')}`);
73
+ }
74
+ console.log('');
75
+ }
76
+ }
@@ -0,0 +1,211 @@
1
+ import { writeFileSync, mkdirSync, copyFileSync, existsSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import inquirer from 'inquirer';
5
+ import YAML from 'yaml';
6
+ import chalk from 'chalk';
7
+ import { getConfigDir, getConfigPath, configExists } from '../config/config-loader.js';
8
+ import { detectAgent } from '../agents/detect.js';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const PROMPTS_SRC = join(__dirname, '../../prompts');
12
+
13
+ export async function initCommand() {
14
+ const cwd = process.cwd();
15
+
16
+ if (configExists(cwd)) {
17
+ const { overwrite } = await inquirer.prompt([{
18
+ type: 'confirm',
19
+ name: 'overwrite',
20
+ message: '.openairev/config.yaml already exists. Overwrite?',
21
+ default: false,
22
+ }]);
23
+ if (!overwrite) {
24
+ console.log(chalk.yellow('Init cancelled.'));
25
+ return;
26
+ }
27
+ }
28
+
29
+ console.log(chalk.bold('\nOpenAIRev Setup\n'));
30
+
31
+ // Detect available CLIs
32
+ const [claudeAvailable, codexAvailable] = await Promise.all([
33
+ detectAgent('claude'),
34
+ detectAgent('codex'),
35
+ ]);
36
+
37
+ console.log(` Claude Code CLI: ${claudeAvailable ? chalk.green('found') : chalk.red('not found')}`);
38
+ console.log(` Codex CLI: ${codexAvailable ? chalk.green('found') : chalk.red('not found')}\n`);
39
+
40
+ if (!claudeAvailable && !codexAvailable) {
41
+ console.log(chalk.red('No agent CLIs found. Install claude or codex CLI first.'));
42
+ process.exit(1);
43
+ }
44
+
45
+ const agents = [];
46
+ if (claudeAvailable) agents.push({ name: 'Claude Code', value: 'claude_code' });
47
+ if (codexAvailable) agents.push({ name: 'Codex CLI', value: 'codex' });
48
+
49
+ const answers = await inquirer.prompt([
50
+ {
51
+ type: 'checkbox',
52
+ name: 'agents',
53
+ message: 'Which agent CLIs do you want to use?',
54
+ choices: agents,
55
+ default: agents.map(a => a.value),
56
+ validate: v => v.length > 0 || 'Select at least one agent',
57
+ },
58
+ {
59
+ type: 'list',
60
+ name: 'claude_reviewer',
61
+ message: 'When Claude Code executes, who reviews?',
62
+ choices: [
63
+ { name: 'Codex (recommended)', value: 'codex' },
64
+ { name: 'Claude Code (self-review)', value: 'claude_code' },
65
+ { name: 'Skip review', value: null },
66
+ ],
67
+ when: a => a.agents.includes('claude_code'),
68
+ },
69
+ {
70
+ type: 'list',
71
+ name: 'codex_reviewer',
72
+ message: 'When Codex executes, who reviews?',
73
+ choices: [
74
+ { name: 'Claude Code (recommended)', value: 'claude_code' },
75
+ { name: 'Codex (self-review)', value: 'codex' },
76
+ { name: 'Skip review', value: null },
77
+ ],
78
+ when: a => a.agents.includes('codex'),
79
+ },
80
+ {
81
+ type: 'list',
82
+ name: 'trigger',
83
+ message: 'Review trigger mode?',
84
+ choices: [
85
+ { name: 'explicit (openairev review)', value: 'explicit' },
86
+ { name: 'auto (every output triggers review)', value: 'auto' },
87
+ ],
88
+ },
89
+ {
90
+ type: 'checkbox',
91
+ name: 'tool_selection',
92
+ message: 'Enable tool gates?',
93
+ choices: [
94
+ { name: 'Tests', value: 'run_tests', checked: true },
95
+ { name: 'Linter', value: 'run_lint', checked: true },
96
+ { name: 'Type checker', value: 'run_typecheck', checked: true },
97
+ ],
98
+ },
99
+ {
100
+ type: 'input',
101
+ name: 'test_cmd',
102
+ message: 'Test command?',
103
+ default: 'npm test',
104
+ when: a => a.tool_selection.includes('run_tests'),
105
+ },
106
+ {
107
+ type: 'input',
108
+ name: 'lint_cmd',
109
+ message: 'Lint command?',
110
+ default: 'npm run lint',
111
+ when: a => a.tool_selection.includes('run_lint'),
112
+ },
113
+ {
114
+ type: 'input',
115
+ name: 'typecheck_cmd',
116
+ message: 'Type check command?',
117
+ default: 'npx tsc --noEmit',
118
+ when: a => a.tool_selection.includes('run_typecheck'),
119
+ },
120
+ {
121
+ type: 'number',
122
+ name: 'claude_iterations',
123
+ message: 'Max iterations when Claude Code executes (Claude→Codex→Claude→... cycles)?',
124
+ default: 5,
125
+ when: a => a.agents.includes('claude_code') && a.claude_reviewer,
126
+ },
127
+ {
128
+ type: 'number',
129
+ name: 'codex_iterations',
130
+ message: 'Max iterations when Codex executes (Codex→Claude→Codex→... cycles)?',
131
+ default: 1,
132
+ when: a => a.agents.includes('codex') && a.codex_reviewer,
133
+ },
134
+ ]);
135
+
136
+ // Build config
137
+ const config = {
138
+ agents: {},
139
+ review_policy: {},
140
+ review_trigger: answers.trigger,
141
+ tools: buildToolsConfig(answers),
142
+ session: {
143
+ store_history: true,
144
+ archive_after: '7d',
145
+ },
146
+ };
147
+
148
+ if (answers.agents.includes('claude_code')) {
149
+ config.agents.claude_code = {
150
+ cmd: 'claude',
151
+ available: true,
152
+ };
153
+ if (answers.claude_reviewer) {
154
+ config.review_policy.claude_code = {
155
+ reviewer: answers.claude_reviewer,
156
+ max_iterations: answers.claude_iterations ?? 5,
157
+ };
158
+ }
159
+ }
160
+
161
+ if (answers.agents.includes('codex')) {
162
+ config.agents.codex = {
163
+ cmd: 'codex',
164
+ available: true,
165
+ };
166
+ if (answers.codex_reviewer) {
167
+ config.review_policy.codex = {
168
+ reviewer: answers.codex_reviewer,
169
+ max_iterations: answers.codex_iterations ?? 1,
170
+ };
171
+ }
172
+ }
173
+
174
+ // Write config
175
+ const configDir = getConfigDir(cwd);
176
+ mkdirSync(configDir, { recursive: true });
177
+ mkdirSync(join(configDir, 'sessions'), { recursive: true });
178
+ mkdirSync(join(configDir, 'prompts'), { recursive: true });
179
+
180
+ writeFileSync(getConfigPath(cwd), YAML.stringify(config));
181
+
182
+ // Copy prompt templates
183
+ const promptsDir = join(configDir, 'prompts');
184
+ copyIfMissing(join(PROMPTS_SRC, 'reviewer.md'), join(promptsDir, 'reviewer.md'));
185
+ copyIfMissing(join(PROMPTS_SRC, 'plan-reviewer.md'), join(promptsDir, 'plan-reviewer.md'));
186
+ copyIfMissing(join(PROMPTS_SRC, 'executor-feedback.md'), join(promptsDir, 'executor-feedback.md'));
187
+
188
+ console.log(`\n${chalk.green('✓')} Config written to .openairev/config.yaml`);
189
+ console.log(`${chalk.green('✓')} Prompt templates written to .openairev/prompts/`);
190
+ console.log(`\nRun ${chalk.cyan('openairev review')} to trigger a review.\n`);
191
+ }
192
+
193
+ function buildToolsConfig(answers) {
194
+ const tools = {};
195
+ if (answers.tool_selection?.includes('run_tests')) {
196
+ tools.run_tests = answers.test_cmd || 'npm test';
197
+ }
198
+ if (answers.tool_selection?.includes('run_lint')) {
199
+ tools.run_lint = answers.lint_cmd || 'npm run lint';
200
+ }
201
+ if (answers.tool_selection?.includes('run_typecheck')) {
202
+ tools.run_typecheck = answers.typecheck_cmd || 'npx tsc --noEmit';
203
+ }
204
+ return tools;
205
+ }
206
+
207
+ function copyIfMissing(src, dest) {
208
+ if (!existsSync(dest) && existsSync(src)) {
209
+ copyFileSync(src, dest);
210
+ }
211
+ }
@@ -0,0 +1,135 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { loadConfig, getMaxIterations } from '../config/config-loader.js';
4
+ import { listChains, loadChain, answerQuestion, hasPendingQuestions, transitionTo } from '../session/chain-manager.js';
5
+ import { runWorkflow } from '../orchestrator/orchestrator.js';
6
+ import { getDiff } from '../tools/git-tools.js';
7
+ import { requireConfig, statusColor, stageLabel, timeAgo } from './format-helpers.js';
8
+
9
+ export async function resumeCommand(options) {
10
+ const cwd = process.cwd();
11
+ requireConfig(cwd);
12
+
13
+ const config = loadConfig(cwd);
14
+ let chain;
15
+
16
+ if (options.chain) {
17
+ chain = loadChain(options.chain, cwd);
18
+ if (!chain) {
19
+ console.log(chalk.red(`Chain not found: ${options.chain}`));
20
+ process.exit(1);
21
+ }
22
+ } else {
23
+ const allChains = listChains(cwd);
24
+ const resumable = allChains.filter(c => c.status === 'active' || c.status === 'blocked');
25
+
26
+ if (resumable.length === 0) {
27
+ if (allChains.length === 0) {
28
+ console.log(chalk.dim('\nNo chains found. Run `openairev review` to start one.\n'));
29
+ return;
30
+ }
31
+ console.log(chalk.dim('\nNo active/blocked chains. Recent chains:\n'));
32
+ for (const c of allChains.slice(0, 5)) {
33
+ const ago = timeAgo(new Date(c.updated));
34
+ console.log(` ${chalk.dim(c.chain_id)} ${statusColor(c.status)(c.status)} ${stageLabel(c.stage)} ${ago}`);
35
+ }
36
+ console.log('');
37
+ return;
38
+ }
39
+
40
+ if (resumable.length === 1) {
41
+ chain = resumable[0];
42
+ } else {
43
+ const { selected } = await inquirer.prompt([{
44
+ type: 'list',
45
+ name: 'selected',
46
+ message: 'Which workflow to resume?',
47
+ choices: resumable.map(c => {
48
+ const ago = timeAgo(new Date(c.updated));
49
+ const label = `${c.task?.user_request || 'untitled'} — ${stageLabel(c.stage)} — ${ago}`;
50
+ return { name: label, value: c.chain_id };
51
+ }),
52
+ }]);
53
+ chain = loadChain(selected, cwd);
54
+ }
55
+ }
56
+
57
+ if (!chain) {
58
+ console.log(chalk.red('Failed to load chain.'));
59
+ process.exit(1);
60
+ }
61
+
62
+ const executor = chain.participants.executor;
63
+ const reviewerName = chain.participants.reviewer;
64
+
65
+ console.log(chalk.bold('\nResuming Workflow\n'));
66
+ console.log(` Chain: ${chalk.dim(chain.chain_id)}`);
67
+ console.log(` Stage: ${chalk.cyan(stageLabel(chain.stage))}`);
68
+ console.log(` Status: ${statusColor(chain.status)(chain.status)}`);
69
+ console.log(` Agents: ${chalk.cyan(executor)} ↔ ${chalk.cyan(reviewerName)}`);
70
+ console.log(` Rounds: ${chain.rounds.length}/${chain.max_rounds}`);
71
+ if (chain.task?.user_request) console.log(` Task: ${chalk.dim(chain.task.user_request)}`);
72
+
73
+ // Handle blocked chain — answer pending questions
74
+ if (chain.status === 'blocked' && hasPendingQuestions(chain)) {
75
+ const pending = chain.questions.filter(q => q.status === 'pending');
76
+ console.log(chalk.yellow.bold('\n Pending questions:\n'));
77
+
78
+ for (const q of pending) {
79
+ const { answer } = await inquirer.prompt([{
80
+ type: 'input',
81
+ name: 'answer',
82
+ message: q.question,
83
+ }]);
84
+ answerQuestion(chain, q.id, answer, cwd);
85
+ }
86
+
87
+ if (chain.stage === 'awaiting_user') {
88
+ transitionTo(chain, 'planning', cwd);
89
+ }
90
+ console.log(chalk.green('\n Questions answered. Resuming workflow...\n'));
91
+ }
92
+
93
+ let diff = '';
94
+ try { diff = getDiff(); } catch { /* ok */ }
95
+
96
+ const codeRoundsDone = chain.rounds.filter(r => r.kind === 'code_review').length;
97
+ if (codeRoundsDone >= chain.max_rounds) {
98
+ console.log(chalk.yellow('\nMax rounds reached. Start a new workflow with `openairev review`.'));
99
+ return;
100
+ }
101
+
102
+ try {
103
+ const result = await runWorkflow({
104
+ config, executor, reviewerName, maxRounds: chain.max_rounds,
105
+ diff, taskDescription: chain.task?.user_request,
106
+ specRef: chain.task?.spec_ref, tools: config.tools,
107
+ cwd, existingChain: chain,
108
+ onStageChange: (stage) => console.log(chalk.bold(`\n[${stageLabel(stage)}]\n`)),
109
+ onRoundEnd: (stage, review, toolResults) => {
110
+ if (toolResults) {
111
+ for (const [name, tr] of Object.entries(toolResults)) {
112
+ const icon = tr.passed ? chalk.green('✓') : chalk.red('✗');
113
+ console.log(` ${icon} ${name}: ${tr.output}`);
114
+ }
115
+ }
116
+ if (review.verdict) {
117
+ const v = review.verdict;
118
+ const color = statusColor(v.status);
119
+ console.log(` ${color(v.status.toUpperCase())} (${((v.confidence || 0) * 100).toFixed(0)}%)`);
120
+ if (v.critical_issues?.length) {
121
+ v.critical_issues.forEach(i => console.log(` ${chalk.red('•')} ${i}`));
122
+ }
123
+ }
124
+ },
125
+ });
126
+
127
+ console.log(chalk.bold(`\n${'='.repeat(40)}`));
128
+ console.log(` Status: ${statusColor(result.status)(result.status)}`);
129
+ if (result.message) console.log(` ${chalk.yellow(result.message)}`);
130
+ console.log('');
131
+ } catch (e) {
132
+ console.log(chalk.red(`\nResume failed: ${e.message}`));
133
+ process.exit(1);
134
+ }
135
+ }
@@ -0,0 +1,151 @@
1
+ import chalk from 'chalk';
2
+ import { readFileSync } from 'fs';
3
+ import { loadConfig, getReviewer, getMaxIterations } from '../config/config-loader.js';
4
+ import { getDiff } from '../tools/git-tools.js';
5
+ import { runReview } from '../review/review-runner.js';
6
+ import { runWorkflow } from '../orchestrator/orchestrator.js';
7
+ import { createSession, saveSession } from '../session/session-manager.js';
8
+ import { requireConfig, statusColor, stageLabel } from './format-helpers.js';
9
+
10
+ export async function reviewCommand(options) {
11
+ const cwd = process.cwd();
12
+ requireConfig(cwd);
13
+
14
+ const config = loadConfig(cwd);
15
+ const executor = options.executor || guessExecutor(config);
16
+ const reviewerName = options.reviewer || getReviewer(config, executor);
17
+
18
+ if (!reviewerName) {
19
+ console.log(chalk.red(`No reviewer configured for executor "${executor}". Run \`openairev init\` or use --reviewer.`));
20
+ process.exit(1);
21
+ }
22
+
23
+ const maxRounds = options.rounds || getMaxIterations(config, executor);
24
+ const skipAnalyze = options.once || options.quick;
25
+ const skipPlan = options.once || options.quick || !options.plan;
26
+
27
+ console.log(chalk.bold(`\nOpenAIRev\n`));
28
+ console.log(` Executor: ${chalk.cyan(executor)}`);
29
+ console.log(` Reviewer: ${chalk.cyan(reviewerName)}`);
30
+ console.log(` Max rounds: ${chalk.cyan(maxRounds)}`);
31
+
32
+ if (options.once) {
33
+ console.log(` Mode: ${chalk.cyan('single review')}`);
34
+ } else if (options.plan) {
35
+ console.log(` Mode: ${chalk.cyan('full workflow (analyze → plan → review → implement)')}`);
36
+ } else {
37
+ console.log(` Mode: ${chalk.cyan('implement → review loop')}`);
38
+ }
39
+
40
+ let diff = '';
41
+ if (options.file) {
42
+ console.log(` Source: ${chalk.cyan(options.file)}`);
43
+ diff = readFileSync(options.file, 'utf-8');
44
+ } else {
45
+ try { diff = getDiff(options.diff); } catch { /* no diff yet is ok for workflow mode */ }
46
+ if (diff?.trim()) console.log(` Diff lines: ${chalk.cyan(diff.split('\n').length)}`);
47
+ }
48
+
49
+ if (options.specRef) console.log(` Spec: ${chalk.cyan(options.specRef)}`);
50
+
51
+ if (options.dryRun) {
52
+ console.log(chalk.yellow('\n[Dry run] Would start workflow. Exiting.'));
53
+ return;
54
+ }
55
+
56
+ console.log('');
57
+
58
+ if (options.once) {
59
+ if (!diff?.trim()) {
60
+ console.log(chalk.yellow('No changes found. Stage some changes or specify --diff <ref>.'));
61
+ process.exit(0);
62
+ }
63
+ try {
64
+ console.log(chalk.dim('Starting review...\n'));
65
+ const review = await runReview(diff, { config, reviewerName, cwd });
66
+
67
+ const session = createSession({ executor, reviewer: reviewerName, diff_ref: options.diff || 'auto' });
68
+ session.iterations.push({ round: 1, review, timestamp: new Date().toISOString() });
69
+ session.final_verdict = review.verdict;
70
+ session.status = review.verdict ? 'completed' : 'error';
71
+ saveSession(session, cwd);
72
+
73
+ if (review.verdict) {
74
+ printVerdict(review.verdict);
75
+ console.log(chalk.dim(`\nSession: ${session.id}`));
76
+ } else {
77
+ console.log(chalk.yellow('\nReviewer did not return a structured verdict.'));
78
+ }
79
+ } catch (e) {
80
+ console.log(chalk.red(`\nReview failed: ${e.message}`));
81
+ process.exit(1);
82
+ }
83
+ } else {
84
+ try {
85
+ const result = await runWorkflow({
86
+ config, executor, reviewerName, maxRounds, diff, diffRef: options.diff,
87
+ taskDescription: options.task, specRef: options.specRef, tools: config.tools,
88
+ cwd, skipAnalyze, skipPlan,
89
+ onStageChange: (stage) => console.log(chalk.bold(`\n[${stageLabel(stage)}]\n`)),
90
+ onRoundEnd: (stage, review, toolResults) => {
91
+ if (toolResults) {
92
+ for (const [name, tr] of Object.entries(toolResults)) {
93
+ const icon = tr.passed ? chalk.green('✓') : chalk.red('✗');
94
+ console.log(` ${icon} ${name}: ${tr.output}`);
95
+ }
96
+ console.log('');
97
+ }
98
+ if (review.verdict) printVerdict(review.verdict);
99
+ },
100
+ });
101
+
102
+ console.log(chalk.bold(`\n${'='.repeat(40)}`));
103
+ console.log(chalk.bold('Workflow Complete\n'));
104
+ console.log(` Status: ${statusColor(result.status)(result.status)}`);
105
+ if (result.rounds) console.log(` Rounds: ${result.rounds}`);
106
+ if (result.stage) console.log(` Stage: ${stageLabel(result.stage)}`);
107
+ console.log(` Chain: ${chalk.dim(result.chain.chain_id)}`);
108
+ if (result.message) console.log(` Note: ${chalk.yellow(result.message)}`);
109
+ if (result.status === 'blocked') {
110
+ console.log(chalk.yellow(`\nWorkflow waiting for user input. Run \`openairev resume\`.`));
111
+ }
112
+ console.log('');
113
+ } catch (e) {
114
+ console.log(chalk.red(`\nWorkflow failed: ${e.message}`));
115
+ process.exit(1);
116
+ }
117
+ }
118
+ }
119
+
120
+ function printVerdict(verdict) {
121
+ console.log(chalk.bold('Verdict: ') + statusColor(verdict.status)(verdict.status.toUpperCase()));
122
+ console.log(`Risk: ${verdict.risk_level || 'unknown'}`);
123
+ console.log(`Confidence: ${((verdict.confidence || 0) * 100).toFixed(0)}%\n`);
124
+
125
+ const sections = [
126
+ ['critical_issues', chalk.red, 'Critical Issues'],
127
+ ['missing_requirements', chalk.red, 'Missing Requirements'],
128
+ ['test_gaps', chalk.yellow, 'Test Gaps'],
129
+ ['requirement_mismatches', chalk.yellow, 'Requirement Mismatches'],
130
+ ['sequencing_issues', chalk.yellow, 'Sequencing Issues'],
131
+ ['risks', chalk.yellow, 'Risks'],
132
+ ['repair_instructions', chalk.cyan, 'Repair Instructions'],
133
+ ];
134
+
135
+ for (const [key, color, label] of sections) {
136
+ if (verdict[key]?.length) {
137
+ console.log(color.bold(`${label}:`));
138
+ verdict[key].forEach(i => console.log(` ${color('•')} ${i}`));
139
+ }
140
+ }
141
+
142
+ if (verdict.false_positives_reconsidered?.length) {
143
+ console.log(chalk.dim('\nFalse Positives Dropped:'));
144
+ verdict.false_positives_reconsidered.forEach(i => console.log(` ${chalk.dim('~')} ${i}`));
145
+ }
146
+ }
147
+
148
+ function guessExecutor(config) {
149
+ const agents = Object.keys(config.agents || {}).filter(a => config.agents[a].available);
150
+ return agents[0] || 'claude_code';
151
+ }