@ateriss_/aiv-cli 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateriss_/aiv-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "AI-powered PR reviewer CLI — local-first, multi-agent semantic analysis",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "claude",
22
22
  "openai"
23
23
  ],
24
- "author": "",
24
+ "author": "Alexandra Linares Viña (Ateriss)",
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
27
  "@anthropic-ai/sdk": "^0.37.0",
@@ -6,23 +6,19 @@ export class ArchitectureReviewer extends BaseAgent {
6
6
 
7
7
  readonly systemPrompt = `You are a principal software architect performing a code review.
8
8
 
9
- Your job is to analyze Pull Request changes from an architectural and structural perspective.
10
-
11
- Focus on:
12
- - Layer violations (e.g., business logic leaking into controllers, DB logic in service layer)
13
- - Module coupling: are new dependencies being introduced unnecessarily?
14
- - Single Responsibility: are files/classes/functions doing too many things?
15
- - Dependency direction: does data flow in the correct direction through the system?
16
- - Abstraction quality: are new abstractions well-named, well-scoped, and necessary?
17
- - Scalability concerns: will this break under load or as the system grows?
18
- - Consistency with existing patterns in the codebase
19
- - Over-engineering or under-engineering
20
- - Fragile design decisions that will require future rework
9
+ First, assess whether this PR contains structural changes worth reviewing (new modules, refactors, dependency changes, layer interactions). If the diff only touches styles, templates, copy, config values, or trivial one-liners with no structural impact, return riskScore: 0, empty findings, and a one-line summary like "No architectural concerns."
10
+
11
+ When the diff IS structurally relevant, focus on:
12
+ - Layer violations (business logic in controllers, DB logic in service layer, etc.)
13
+ - Unnecessary coupling or new cross-module dependencies
14
+ - Single Responsibility violations: files/classes doing too many things
15
+ - Abstraction quality: poorly named, too broad, or unnecessary abstractions
16
+ - Structural decisions that will require future rework
21
17
 
22
18
  You are NOT checking syntax, linting, or security.
23
19
 
24
- Use the project context to understand the existing architecture. Flag deviations.
25
- Assign a riskScore from 0 (clean) to 100 (structural problem that needs blocking).
20
+ Be concise: titles 8 words, descriptions 2 sentences, suggestions ≤ 1 sentence, summary ≤ 3 sentences.
21
+ For INFO-level findings, write description as a single short phrase ending with " OK" or " — noted".
26
22
 
27
23
  Return only valid JSON as specified.`;
28
24
 
@@ -41,10 +41,18 @@ export abstract class BaseAgent {
41
41
  ? '\n\nIMPORTANT: Respond in Spanish. All string values in the JSON (summary, title, description, suggestion, possibleRegressions) must be written in Spanish.'
42
42
  : '';
43
43
 
44
+ const baseBranch = ctx.diff.pr.base;
45
+ const headBranch = ctx.diff.pr.branch;
46
+ const isProductionMerge = /^(main|master|production|prod|release)$/i.test(baseBranch);
47
+ const branchRisk = isProductionMerge
48
+ ? `⚠️ DIRECT MERGE TO ${baseBranch.toUpperCase()} — apply stricter scrutiny. Findings here affect production.`
49
+ : `Target: ${baseBranch} (non-production). Calibrate severity accordingly.`;
50
+
44
51
  return `## PR: #${ctx.diff.pr.number} — ${ctx.diff.pr.title}
45
52
 
46
53
  **Author:** ${ctx.diff.pr.author}
47
- **Branch:** ${ctx.diff.pr.branch} → ${ctx.diff.pr.base}
54
+ **Merge:** \`${headBranch}\`\`${baseBranch}\`
55
+ **${branchRisk}**
48
56
  **Description:** ${ctx.diff.pr.description ?? 'No description provided.'}
49
57
 
50
58
  ---
@@ -73,19 +81,19 @@ ${patches}
73
81
 
74
82
  Analyze the above and return a JSON response matching this schema:
75
83
  {
76
- "summary": "string concise summary of your findings",
84
+ "summary": "2-3 sentences max",
77
85
  "findings": [
78
86
  {
79
87
  "severity": "critical|high|medium|low|info",
80
88
  "category": "string",
81
- "title": "string",
82
- "description": "string",
89
+ "title": "string — 8 words max",
90
+ "description": "1-2 sentences max",
83
91
  "file": "string (optional)",
84
- "suggestion": "string (optional)"
92
+ "suggestion": "1 sentence max (optional)"
85
93
  }
86
94
  ],
87
95
  "riskScore": 0-100,
88
- "possibleRegressions": ["string"]
96
+ "possibleRegressions": ["one short phrase each"]
89
97
  }
90
98
 
91
99
  Return ONLY valid JSON. No markdown fences, no explanation outside the JSON.${langInstruction}`;
@@ -124,7 +132,7 @@ Return ONLY valid JSON. No markdown fences, no explanation outside the JSON.${la
124
132
  agentName: this.agentName,
125
133
  findings,
126
134
  summary: parsed.summary ?? '',
127
- riskScore: Math.max(0, Math.min(100, parseInt(parsed.riskScore ?? '0'))),
135
+ riskScore: Math.max(0, Math.min(100, Number.parseInt(parsed.riskScore ?? '0'))),
128
136
  };
129
137
  }
130
138
  }
@@ -6,23 +6,18 @@ export class BusinessReviewer extends BaseAgent {
6
6
 
7
7
  readonly systemPrompt = `You are a senior business analyst and domain expert performing a code review.
8
8
 
9
- Your job is to analyze Pull Request changes STRICTLY from a business and domain perspective.
10
-
11
- Focus on:
12
- - Business logic correctness: does the code behave as the domain requires?
13
- - Domain rule violations: are any business invariants broken?
14
- - Functional regressions: could this break existing behavior users depend on?
15
- - Side effects on other business flows (billing, notifications, state machines, etc.)
16
- - Missing required steps (auditing, logging, approvals, notifications)
9
+ First, assess whether this PR contains changes relevant to business logic (data mutations, calculations, state transitions, domain rules, flows, validations). If the diff is purely cosmetic (styles, colors, text copy, console.log removal, import reordering, type annotations only), return riskScore: 0, empty findings, and a one-line summary like "No business logic changes."
10
+
11
+ When the diff IS business-relevant, focus on:
12
+ - Business logic correctness and domain rule violations
13
+ - Functional regressions that break existing user-facing behavior
14
+ - Missing required steps (auditing, logging, approvals) per rules.yml
17
15
  - Incorrect calculations, status transitions, or conditional logic
18
- - Data integrity concerns (missing validations, incorrect defaults)
19
16
 
20
17
  You are NOT a linter, NOT a security scanner. You analyze MEANING, not syntax.
21
18
 
22
- If the rules.yml specifies required_calls or required_checks for a module, verify they are present.
23
-
24
- Be concrete. Reference specific lines or functions when relevant.
25
- Assign a riskScore from 0 (no risk) to 100 (critical business risk).
19
+ Be concise: titles 8 words, descriptions 2 sentences, suggestions 1 sentence, summary ≤ 3 sentences.
20
+ For INFO-level findings (positive or neutral observations), write description as a single short phrase ending with " — OK" or " — noted".
26
21
 
27
22
  Return only valid JSON as specified.`;
28
23
 
@@ -6,24 +6,22 @@ export class SecurityReviewer extends BaseAgent {
6
6
 
7
7
  readonly systemPrompt = `You are a senior application security engineer performing a code review.
8
8
 
9
- Your job is to analyze Pull Request changes for security vulnerabilities and risks.
10
-
11
- Focus on:
12
- - Authentication and authorization flaws (missing checks, privilege escalation)
13
- - Injection vulnerabilities (SQL, NoSQL, command, LDAP, template injection)
14
- - Insecure direct object references (IDOR)
15
- - Sensitive data exposure (logging secrets, returning PII, insecure storage)
16
- - Cryptographic issues (weak algorithms, hardcoded secrets, insecure RNG)
9
+ First, assess whether this PR touches code with security implications (auth, API endpoints, data storage, input handling, dependencies, secrets, permissions). If the diff only changes styles, UI copy, non-sensitive config values, or cosmetic code, return riskScore: 0, empty findings, and a one-line summary like "No security-relevant changes."
10
+
11
+ When the diff IS security-relevant, focus on:
12
+ - Auth/authorization flaws (missing checks, privilege escalation)
13
+ - Injection vulnerabilities (SQL, NoSQL, command injection)
14
+ - Sensitive data exposure (logging secrets, returning PII)
17
15
  - Input validation gaps (missing sanitization, unsafe deserialization)
18
- - Race conditions or TOCTOU vulnerabilities
19
16
  - Broken access control in API endpoints
20
- - SSRF, path traversal, open redirects
21
- - Dependency security (new packages with known issues)
17
+ - New dependencies with known vulnerabilities
22
18
 
23
- Mark findings involving sensitive_modules from the rules with higher severity.
19
+ Mark findings in sensitive_modules from rules.yml with higher severity.
20
+ Name the vulnerability class (OWASP) when applicable.
21
+ Assign a riskScore from 0 (no issues) to 100 (block immediately).
24
22
 
25
- Be precise: name the vulnerability class (OWASP), the file, and the specific line or pattern.
26
- Assign a riskScore from 0 (no security issues) to 100 (active vulnerability, block immediately).
23
+ Be concise: titles 8 words, descriptions ≤ 2 sentences, suggestions 1 sentence, summary ≤ 3 sentences.
24
+ For INFO-level findings, write description as a single short phrase ending with " OK" or " noted".
27
25
 
28
26
  Return only valid JSON as specified.`;
29
27
 
@@ -0,0 +1,66 @@
1
+ import { PRDiff } from '../types';
2
+ import { LLMProvider } from '../providers/base';
3
+
4
+ export type TriageResult = {
5
+ agents: Array<'business' | 'architecture' | 'security'>;
6
+ reasoning: string;
7
+ };
8
+
9
+ const ALL_AGENTS: TriageResult['agents'] = ['business', 'architecture', 'security'];
10
+
11
+ const SYSTEM_PROMPT = `You are a code review triage specialist.
12
+ Given a PR's metadata and changed file list, decide which review agents are needed.
13
+
14
+ Available agents:
15
+ - business: domain logic, business rules, calculations, state transitions, data integrity
16
+ - architecture: layer violations, module coupling, structural patterns, dependency direction
17
+ - security: auth, injection, data exposure, input validation, access control, secrets
18
+
19
+ Rules:
20
+ - Only include an agent if the changes are genuinely relevant to its domain
21
+ - Cosmetic changes (styles, colors, copy, console.log removal, comment edits) need NO agents — return []
22
+ - CSS/SCSS/style-only changes → skip all agents
23
+ - Purely adding tests with no production code change → business only (regression check), skip security and architecture
24
+ - Config-only changes (env vars, CI files) → security only if secrets/permissions involved, otherwise skip
25
+ - When in doubt about an agent, skip it — it's better to miss a low-risk finding than waste tokens
26
+
27
+ Return ONLY valid JSON: { "agents": ["security", "business"], "reasoning": "one sentence" }
28
+ No markdown fences. No explanation outside the JSON.`;
29
+
30
+ export async function triageAgents(diff: PRDiff, provider: LLMProvider): Promise<TriageResult> {
31
+ const fileList = diff.files
32
+ .map(f => `${f.status.toUpperCase()} ${f.filename} (+${f.additions}/-${f.deletions})`)
33
+ .join('\n');
34
+
35
+ const message = `PR #${diff.pr.number}: ${diff.pr.title}
36
+ Branch: ${diff.pr.branch} → ${diff.pr.base}
37
+ Description: ${diff.pr.description ?? 'none'}
38
+
39
+ Changed files (${diff.files.length}):
40
+ ${fileList}`;
41
+
42
+ try {
43
+ const response = await provider.complete(
44
+ [{ role: 'user', content: message }],
45
+ SYSTEM_PROMPT,
46
+ 256,
47
+ );
48
+
49
+ const match = /\{[\s\S]*\}/.exec(response.content.trim());
50
+ if (!match) return fallback();
51
+
52
+ const parsed = JSON.parse(match[0]) as { agents?: unknown; reasoning?: string };
53
+ const agents = (Array.isArray(parsed.agents) ? parsed.agents : [])
54
+ .filter((a): a is TriageResult['agents'][number] =>
55
+ a === 'business' || a === 'architecture' || a === 'security'
56
+ );
57
+
58
+ return { agents, reasoning: parsed.reasoning ?? '' };
59
+ } catch {
60
+ return fallback();
61
+ }
62
+ }
63
+
64
+ function fallback(): TriageResult {
65
+ return { agents: ALL_AGENTS, reasoning: 'triage failed — running all agents' };
66
+ }
@@ -0,0 +1,216 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { resolveConfig, loadRules, getGithubToken, isInitialized } from '../../config';
7
+ import type { ReviewResult, AgentFinding } from '../../types';
8
+ import { GithubClient } from '../../git/github';
9
+ import { detectRepoInfo, isGitRepo } from '../../git/utils';
10
+ import { getCurrentBranch, detectBaseBranch, buildLocalPRDiff, getLastCommitTitle, hasUnpushedCommits } from '../../git/local';
11
+ import { Orchestrator } from '../../orchestrator';
12
+ import { ContextManager } from '../../context/manager';
13
+ import { renderReview } from '../renderer';
14
+ import { selectPrecheckAction, promptPRDetails } from '../selector';
15
+ import { t } from '../../i18n';
16
+
17
+ function terminalLink(label: string, filePath: string): string {
18
+ const url = 'file:///' + filePath.replaceAll('\\', '/');
19
+ return `]8;;${url}${label}]8;;`;
20
+ }
21
+
22
+ export function checkCommand(): Command {
23
+ return new Command('check')
24
+ .alias('c')
25
+ .description('Analyze local diff before creating a PR')
26
+ .argument('[base]', 'Base branch to compare against (default: main/master)')
27
+ .option('--owner <owner>', 'GitHub owner (for PR creation)')
28
+ .option('--repo <repo>', 'GitHub repo (for PR creation)')
29
+ .option('--agent <agents...>', 'Run specific agents only (business, architecture, security)')
30
+ .option('--json', 'Output raw JSON result')
31
+ .action(async (baseArg: string | undefined, opts) => {
32
+ if (!isInitialized()) {
33
+ console.log(chalk.red(t().notInitialized));
34
+ return;
35
+ }
36
+
37
+ const cwd = process.cwd();
38
+
39
+ if (!isGitRepo(cwd)) {
40
+ console.log(chalk.red(t().precheckNotGitRepo));
41
+ return;
42
+ }
43
+
44
+ const head = getCurrentBranch(cwd);
45
+ const base = baseArg ?? detectBaseBranch(cwd);
46
+
47
+ console.log(chalk.bold(t().precheckTitle(head, base)));
48
+
49
+ const diffSpinner = ora(t().precheckBuilding(base)).start();
50
+ let prDiff: ReturnType<typeof buildLocalPRDiff>;
51
+ try {
52
+ prDiff = buildLocalPRDiff(cwd, base);
53
+ if (prDiff.files.length === 0) {
54
+ diffSpinner.warn(chalk.yellow(t().precheckNoChanges));
55
+ return;
56
+ }
57
+ diffSpinner.succeed(t().precheckDiffBuilt(prDiff.files.length));
58
+ } catch (e: any) {
59
+ diffSpinner.fail(chalk.red(t().precheckDiffFailed(e.message)));
60
+ return;
61
+ }
62
+
63
+ const config = resolveConfig();
64
+ const rules = loadRules();
65
+ const activeAgents = opts.agent ?? ['business', 'architecture', 'security'];
66
+ const auto = !opts.agent;
67
+
68
+ const ctxSpinner = ora(t().reviewLoadingContext).start();
69
+ const context = new ContextManager(cwd).buildReviewContext(prDiff);
70
+ ctxSpinner.succeed(t().reviewContextLoaded);
71
+ console.log(chalk.dim(t().reviewRunningAgents(activeAgents.join(', '))));
72
+
73
+ let result: ReviewResult;
74
+ try {
75
+ result = await new Orchestrator(config, rules).run(prDiff, context, activeAgents, auto);
76
+ } catch (e: any) {
77
+ console.log(chalk.red(t().reviewFailed(e.message)));
78
+ return;
79
+ }
80
+
81
+ if (opts.json) {
82
+ console.log(JSON.stringify(result, null, 2));
83
+ return;
84
+ }
85
+
86
+ renderReview(result);
87
+ if (!process.stdout.isTTY) return;
88
+
89
+ const needsPush = hasUnpushedCommits(cwd, head);
90
+ const hasFindings = result.riskScore > 0 || result.agents.some(a => a.findings.length > 0);
91
+ const action = await selectPrecheckAction(t().precheckSelectAction, needsPush, hasFindings);
92
+
93
+ if (action === 'skip') return;
94
+
95
+ if (action === 'save_checklist') {
96
+ await doSaveChecklist(cwd, result, head, base);
97
+ return;
98
+ }
99
+
100
+ if (action === 'create_pr') {
101
+ await doCreatePR(cwd, opts, config, result, head, base, needsPush);
102
+ }
103
+ });
104
+ }
105
+
106
+ async function doCreatePR(
107
+ cwd: string,
108
+ opts: any,
109
+ config: ReturnType<typeof resolveConfig>,
110
+ result: ReviewResult,
111
+ head: string,
112
+ base: string,
113
+ needsPush: boolean,
114
+ ): Promise<void> {
115
+ let token: string;
116
+ try {
117
+ token = getGithubToken(config);
118
+ } catch {
119
+ console.log(chalk.red(t().prsMissingToken(config.github.token_env)));
120
+ return;
121
+ }
122
+
123
+ const repoInfo = detectRepoInfo(cwd);
124
+ const owner = opts.owner ?? config.github.owner ?? repoInfo?.owner;
125
+ const repo = opts.repo ?? config.github.repo ?? repoInfo?.repo;
126
+
127
+ if (!owner || !repo) {
128
+ console.log(chalk.red(t().precheckMissingConfig));
129
+ return;
130
+ }
131
+
132
+ if (needsPush) {
133
+ const pushSpinner = ora(`Pushing ${chalk.cyan(head)}...`).start();
134
+ try {
135
+ const { execSync } = await import('node:child_process');
136
+ execSync(`git push origin ${head}`, { cwd, stdio: 'pipe' });
137
+ pushSpinner.succeed(chalk.green(`Branch ${chalk.cyan(head)} pushed.`));
138
+ } catch (e: any) {
139
+ pushSpinner.fail(chalk.red(`Push failed: ${e.message}`));
140
+ return;
141
+ }
142
+ }
143
+
144
+ const defaultTitle = getLastCommitTitle(cwd) || head;
145
+ const { title, body } = await promptPRDetails(defaultTitle);
146
+
147
+ const spinner = ora(t().precheckCreatingPR).start();
148
+ try {
149
+ const pr = await new GithubClient(token).createPR(owner, repo, title, body, head, base);
150
+ spinner.succeed(chalk.green(t().precheckPRCreated(pr.url)));
151
+ } catch (e: any) {
152
+ spinner.fail(chalk.red(t().precheckPRFailed(e.message)));
153
+ }
154
+ }
155
+
156
+ async function doSaveChecklist(cwd: string, result: ReviewResult, head: string, base: string): Promise<void> {
157
+ const aivDir = path.join(cwd, '.aiv');
158
+ if (!fs.existsSync(aivDir)) fs.mkdirSync(aivDir, { recursive: true });
159
+
160
+ const filePath = path.join(aivDir, 'checklist.md');
161
+ const content = buildChecklist(result, head, base);
162
+
163
+ const spinner = ora(t().precheckSavingChecklist).start();
164
+ try {
165
+ fs.writeFileSync(filePath, content, 'utf8');
166
+ const link = terminalLink('.aiv/checklist.md', filePath);
167
+ spinner.succeed(chalk.green(t().precheckChecklistSaved(link)));
168
+ } catch (e: any) {
169
+ spinner.fail(chalk.red(`Failed to save checklist: ${e.message}`));
170
+ }
171
+ }
172
+
173
+ function findingLine(f: AgentFinding, bold: boolean): string[] {
174
+ const loc = f.file ? ` — \`${f.file}\`` : '';
175
+ const label = bold ? `**[${f.severity.toUpperCase()}]**` : `[${f.severity.toUpperCase()}]`;
176
+ const row = `- [ ] ${label} ${f.title}${loc}`;
177
+ return f.suggestion ? [row, ` > ${f.suggestion}`] : [row];
178
+ }
179
+
180
+ function section(heading: string, items: string[]): string[] {
181
+ return [`## ${heading}`, '', ...items, ''];
182
+ }
183
+
184
+ function buildChecklist(result: ReviewResult, head: string, base: string): string {
185
+ const date = new Date(result.generatedAt).toISOString().split('T')[0];
186
+ const all = [...result.securityIssues, ...result.businessRisks, ...result.architectureIssues];
187
+
188
+ const critical = all.filter(f => f.severity === 'critical');
189
+ const high = all.filter(f => f.severity === 'high');
190
+ const medium = all.filter(f => f.severity === 'medium');
191
+ const low = all.filter(f => f.severity === 'low' || f.severity === 'info');
192
+
193
+ const body: string[] = [];
194
+
195
+ if (critical.length > 0 || high.length > 0) {
196
+ body.push(...section('Critical & High', [...critical, ...high].flatMap(f => findingLine(f, true))));
197
+ }
198
+ if (medium.length > 0) {
199
+ body.push(...section('Medium', medium.flatMap(f => findingLine(f, true))));
200
+ }
201
+ if (low.length > 0) {
202
+ body.push(...section('Low / Info', low.flatMap(f => findingLine(f, false))));
203
+ }
204
+ if (result.possibleRegressions.length > 0) {
205
+ body.push(...section('Possible Regressions', result.possibleRegressions.map(r => `- [ ] ${r}`)));
206
+ }
207
+
208
+ return [
209
+ `# aiv Check: ${head} → ${base}`,
210
+ `Generated: ${date} | Risk: ${result.riskLabel} (${result.riskScore}/100)`,
211
+ '',
212
+ ...body,
213
+ '---',
214
+ '*Generated by [aiv](https://www.npmjs.com/package/@ateriss_/aiv-cli)*',
215
+ ].join('\n');
216
+ }
@@ -63,7 +63,7 @@ export function prsCommand(): Command {
63
63
  chalk.cyan(`#${pr.number}`),
64
64
  truncate(pr.title, 50),
65
65
  chalk.dim(pr.author),
66
- chalk.dim(pr.branch),
66
+ chalk.dim(pr.branch) + chalk.dim(' → ') + chalk.cyan(pr.base),
67
67
  chalk.green(`+${pr.additions}`) + chalk.dim('/') + chalk.red(`-${pr.deletions}`),
68
68
  chalk.dim(formatDate(pr.createdAt)),
69
69
  ]);
@@ -26,6 +26,7 @@ export interface RunReviewOptions {
26
26
  export async function runReview(opts: RunReviewOptions): Promise<void> {
27
27
  const { prNumber, owner, repo, config, token } = opts;
28
28
  const rules = loadRules();
29
+ const auto = !opts.agents;
29
30
  const activeAgents = opts.agents ?? ['business', 'architecture', 'security'];
30
31
  const client = new GithubClient(token);
31
32
 
@@ -42,7 +43,7 @@ export async function runReview(opts: RunReviewOptions): Promise<void> {
42
43
  return;
43
44
  }
44
45
 
45
- const result = await resolveResult(client, { owner, repo, prNumber }, config, rules, prDiff, activeAgents, opts.json);
46
+ const result = await resolveResult(client, { owner, repo, prNumber }, { config, rules, agents: activeAgents, auto }, prDiff, opts.json);
46
47
  if (!result) return;
47
48
 
48
49
  if (opts.json) {
@@ -70,14 +71,16 @@ export async function runReview(opts: RunReviewOptions): Promise<void> {
70
71
  }
71
72
 
72
73
  interface RepoRef { owner: string; repo: string; prNumber: number; }
74
+ interface AgentOpts { config: ResolvedConfig; rules: ReturnType<typeof loadRules>; agents: string[]; auto: boolean; }
73
75
 
74
76
  async function resolveResult(
75
77
  client: GithubClient,
76
78
  ref: RepoRef,
77
- config: ResolvedConfig, rules: ReturnType<typeof loadRules>,
79
+ agentOpts: AgentOpts,
78
80
  prDiff: Awaited<ReturnType<GithubClient['getPRDiff']>>,
79
- activeAgents: string[], json?: boolean,
81
+ json?: boolean,
80
82
  ): Promise<ReviewResult | null> {
83
+ const { config, rules, agents: activeAgents, auto } = agentOpts;
81
84
  const { owner, repo, prNumber } = ref;
82
85
  if (!json && process.stdout.isTTY) {
83
86
  const cached = await client.findAivReview(owner, repo, prNumber);
@@ -98,7 +101,7 @@ async function resolveResult(
98
101
  console.log(chalk.dim(t().reviewRunningAgents(activeAgents.join(', '))));
99
102
 
100
103
  try {
101
- return await new Orchestrator(config, rules).run(prDiff, context, activeAgents);
104
+ return await new Orchestrator(config, rules).run(prDiff, context, activeAgents, auto);
102
105
  } catch (e: any) {
103
106
  console.log(chalk.red(t().reviewFailed(e.message)));
104
107
  return null;
@@ -94,13 +94,65 @@ export async function selectMergeStrategy(message: string): Promise<MergeStrateg
94
94
  return answers['strategy'] as MergeStrategy;
95
95
  }
96
96
 
97
+ export type PrecheckAction = 'create_pr' | 'save_checklist' | 'skip';
98
+
99
+ export async function selectPrecheckAction(message: string, needsPush: boolean, hasFindings: boolean): Promise<PrecheckAction> {
100
+ const inquirer = await getInquirer();
101
+ const prLabel = needsPush
102
+ ? chalk.cyan('🚀 Push y crear PR en GitHub')
103
+ : chalk.cyan('🚀 Crear PR en GitHub');
104
+
105
+ const checklistChoice = hasFindings
106
+ ? [{ name: chalk.yellow('📋 Guardar checklist en .aiv/'), value: 'save_checklist', short: 'Guardar Checklist' }]
107
+ : [];
108
+
109
+ const choices = [
110
+ { name: prLabel, value: 'create_pr', short: 'Crear PR' },
111
+ ...checklistChoice,
112
+ new inquirer.Separator('─'.repeat(42)),
113
+ { name: chalk.dim('↩ Skip'), value: 'skip', short: 'Skip' },
114
+ ];
115
+
116
+ const answers = await inquirer.prompt([{
117
+ type: 'list',
118
+ name: 'action',
119
+ message,
120
+ choices,
121
+ pageSize: 4,
122
+ loop: false,
123
+ }]);
124
+ return answers['action'] as PrecheckAction;
125
+ }
126
+
127
+ export async function promptPRDetails(defaultTitle: string): Promise<{ title: string; body: string }> {
128
+ const inquirer = await getInquirer();
129
+ const answers = await inquirer.prompt([
130
+ {
131
+ type: 'input',
132
+ name: 'title',
133
+ message: 'PR title:',
134
+ default: defaultTitle,
135
+ },
136
+ {
137
+ type: 'input',
138
+ name: 'body',
139
+ message: 'PR description (optional, Enter to skip):',
140
+ default: '',
141
+ },
142
+ ]);
143
+ return {
144
+ title: answers['title'] as string,
145
+ body: answers['body'] as string,
146
+ };
147
+ }
148
+
97
149
  export async function confirmReview(pr: PullRequest, label: string): Promise<boolean> {
98
150
  const inquirer = await getInquirer();
99
151
 
100
152
  const answers = await inquirer.prompt([{
101
153
  type: 'confirm',
102
154
  name: 'ok',
103
- message: `${label} ${chalk.cyan(`#${pr.number}`)} — ${pr.title}?`,
155
+ message: `${label} ${chalk.cyan('#' + pr.number)} — ${pr.title}?`,
104
156
  default: true,
105
157
  }]);
106
158