@ateriss_/aiv-cli 1.0.1 → 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/README.md +165 -70
- package/dist/index.js +477 -188
- package/package.json +11 -4
- package/src/agents/architecture.ts +10 -14
- package/src/agents/base.ts +15 -7
- package/src/agents/business.ts +8 -13
- package/src/agents/security.ts +12 -14
- package/src/agents/triage.ts +66 -0
- package/src/cli/commands/check.ts +216 -0
- package/src/cli/commands/prs.ts +1 -1
- package/src/cli/commands/review.ts +92 -41
- package/src/cli/renderer.ts +61 -0
- package/src/cli/selector.ts +59 -6
- package/src/context/generator.ts +161 -65
- package/src/git/github.ts +53 -1
- package/src/git/local.ts +113 -0
- package/src/git/utils.ts +17 -8
- package/src/i18n/en.ts +26 -1
- package/src/i18n/es.ts +26 -1
- package/src/index.ts +2 -0
- package/src/orchestrator/index.ts +48 -2
- package/tsup.config.ts +1 -0
package/src/git/github.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { PullRequest, PRDiff, PRFile } from '../types';
|
|
1
|
+
import { PullRequest, PRDiff, PRFile, ReviewResult } from '../types';
|
|
2
|
+
|
|
3
|
+
const AIV_COMMENT_TAG = '<!-- aiv-review:';
|
|
2
4
|
|
|
3
5
|
const GITHUB_API = 'https://api.github.com';
|
|
4
6
|
|
|
@@ -59,6 +61,36 @@ export class GithubClient {
|
|
|
59
61
|
return { pr, files, rawDiff };
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
async findAivReview(owner: string, repo: string, prNumber: number): Promise<ReviewResult | null> {
|
|
65
|
+
const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`;
|
|
66
|
+
const res = await this.fetch(url);
|
|
67
|
+
if (!res.ok) return null;
|
|
68
|
+
|
|
69
|
+
const comments = await res.json() as any[];
|
|
70
|
+
for (const c of comments) {
|
|
71
|
+
const body: string = c.body ?? '';
|
|
72
|
+
if (!body.startsWith(AIV_COMMENT_TAG)) continue;
|
|
73
|
+
const end = body.indexOf(' -->');
|
|
74
|
+
if (end === -1) continue;
|
|
75
|
+
try {
|
|
76
|
+
const encoded = body.slice(AIV_COMMENT_TAG.length, end).trim();
|
|
77
|
+
return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as ReviewResult;
|
|
78
|
+
} catch { continue; }
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async postComment(owner: string, repo: string, prNumber: number, body: string): Promise<void> {
|
|
84
|
+
const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${prNumber}/comments`;
|
|
85
|
+
const { default: fetch } = await import('node-fetch');
|
|
86
|
+
const res = await fetch(url, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { ...this.headers, 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ body }),
|
|
90
|
+
}) as unknown as Response;
|
|
91
|
+
if (!res.ok) await this.throwError(res, `post comment on PR #${prNumber}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
async mergePR(
|
|
63
95
|
owner: string,
|
|
64
96
|
repo: string,
|
|
@@ -75,6 +107,26 @@ export class GithubClient {
|
|
|
75
107
|
if (!res.ok) await this.throwError(res, `merge PR #${prNumber}`);
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
async createPR(
|
|
111
|
+
owner: string,
|
|
112
|
+
repo: string,
|
|
113
|
+
title: string,
|
|
114
|
+
body: string,
|
|
115
|
+
head: string,
|
|
116
|
+
base: string,
|
|
117
|
+
): Promise<{ number: number; url: string }> {
|
|
118
|
+
const url = `${GITHUB_API}/repos/${owner}/${repo}/pulls`;
|
|
119
|
+
const { default: fetch } = await import('node-fetch');
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { ...this.headers, 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({ title, body, head, base }),
|
|
124
|
+
}) as unknown as Response;
|
|
125
|
+
if (!res.ok) await this.throwError(res, 'create PR');
|
|
126
|
+
const data = await res.json() as any;
|
|
127
|
+
return { number: data.number, url: data.html_url };
|
|
128
|
+
}
|
|
129
|
+
|
|
78
130
|
async submitReview(
|
|
79
131
|
owner: string,
|
|
80
132
|
repo: string,
|
package/src/git/local.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import type { PRDiff, PRFile, PullRequest } from '../types';
|
|
3
|
+
|
|
4
|
+
export function getCurrentBranch(cwd: string): string {
|
|
5
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { cwd, stdio: 'pipe' }).toString().trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Always prefers origin/<branch> (remote = source of truth). Falls back to local if no remote.
|
|
9
|
+
function resolveRef(cwd: string, branch: string): string {
|
|
10
|
+
try {
|
|
11
|
+
execSync(`git rev-parse --verify origin/${branch}`, { cwd, stdio: 'pipe' });
|
|
12
|
+
return `origin/${branch}`;
|
|
13
|
+
} catch {
|
|
14
|
+
return branch;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function detectBaseBranch(cwd: string): string {
|
|
19
|
+
const head = getCurrentBranch(cwd);
|
|
20
|
+
for (const b of ['main', 'master', 'develop']) {
|
|
21
|
+
if (b === head) continue;
|
|
22
|
+
// Check remote first, then local
|
|
23
|
+
for (const ref of [`origin/${b}`, b]) {
|
|
24
|
+
try {
|
|
25
|
+
execSync(`git rev-parse --verify ${ref}`, { cwd, stdio: 'pipe' });
|
|
26
|
+
return b;
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return 'main';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hasUnpushedCommits(cwd: string, branch: string): boolean {
|
|
34
|
+
try {
|
|
35
|
+
execSync(`git rev-parse --verify origin/${branch}`, { cwd, stdio: 'pipe' });
|
|
36
|
+
const out = execSync(`git log origin/${branch}..HEAD --oneline`, { cwd, stdio: 'pipe' })
|
|
37
|
+
.toString().trim();
|
|
38
|
+
return out.length > 0;
|
|
39
|
+
} catch {
|
|
40
|
+
// origin/<branch> doesn't exist — branch was never pushed
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getLastCommitTitle(cwd: string): string {
|
|
46
|
+
try {
|
|
47
|
+
return execSync('git log -1 --pretty=%s', { cwd, stdio: 'pipe' }).toString().trim();
|
|
48
|
+
} catch { return ''; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildLocalPRDiff(cwd: string, base: string): PRDiff {
|
|
52
|
+
const head = getCurrentBranch(cwd);
|
|
53
|
+
const ref = resolveRef(cwd, base);
|
|
54
|
+
|
|
55
|
+
const numstat = execSync(`git diff ${ref}...HEAD --numstat`, { cwd, stdio: 'pipe' })
|
|
56
|
+
.toString().trim();
|
|
57
|
+
const nameStatus = execSync(`git diff ${ref}...HEAD --name-status`, { cwd, stdio: 'pipe' })
|
|
58
|
+
.toString().trim();
|
|
59
|
+
const rawDiff = execSync(`git diff ${ref}...HEAD`, {
|
|
60
|
+
cwd, stdio: 'pipe', maxBuffer: 5 * 1024 * 1024,
|
|
61
|
+
}).toString();
|
|
62
|
+
|
|
63
|
+
const statsMap = new Map<string, { additions: number; deletions: number }>();
|
|
64
|
+
for (const line of numstat.split('\n').filter(Boolean)) {
|
|
65
|
+
const parts = line.split('\t');
|
|
66
|
+
if (parts.length >= 3) {
|
|
67
|
+
statsMap.set(parts[2], {
|
|
68
|
+
additions: Number.parseInt(parts[0]) || 0,
|
|
69
|
+
deletions: Number.parseInt(parts[1]) || 0,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const files: PRFile[] = [];
|
|
75
|
+
for (const line of nameStatus.split('\n').filter(Boolean)) {
|
|
76
|
+
const parts = line.split('\t');
|
|
77
|
+
if (parts.length < 2) continue;
|
|
78
|
+
const statusChar = parts[0][0];
|
|
79
|
+
const filename = parts[parts.length - 1];
|
|
80
|
+
const statusMap: Record<string, PRFile['status']> = {
|
|
81
|
+
A: 'added', M: 'modified', D: 'deleted', R: 'renamed', C: 'added',
|
|
82
|
+
};
|
|
83
|
+
const status = statusMap[statusChar] ?? 'modified';
|
|
84
|
+
const stats = statsMap.get(filename) ?? { additions: 0, deletions: 0 };
|
|
85
|
+
files.push({ filename, status, ...stats });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const log = (() => {
|
|
89
|
+
try {
|
|
90
|
+
return execSync(`git log ${ref}...HEAD --oneline`, { cwd, stdio: 'pipe' }).toString().trim();
|
|
91
|
+
} catch { return ''; }
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
const pr: PullRequest = {
|
|
95
|
+
id: 0,
|
|
96
|
+
number: 0,
|
|
97
|
+
title: `${head} → ${base}`,
|
|
98
|
+
author: 'local',
|
|
99
|
+
branch: head,
|
|
100
|
+
base,
|
|
101
|
+
url: '',
|
|
102
|
+
state: 'draft',
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
updatedAt: new Date().toISOString(),
|
|
105
|
+
additions: files.reduce((s, f) => s + f.additions, 0),
|
|
106
|
+
deletions: files.reduce((s, f) => s + f.deletions, 0),
|
|
107
|
+
changedFiles: files.length,
|
|
108
|
+
labels: [],
|
|
109
|
+
description: log || undefined,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return { pr, files, rawDiff };
|
|
113
|
+
}
|
package/src/git/utils.ts
CHANGED
|
@@ -26,18 +26,27 @@ export function parseGithubUrl(url: string): RepoInfo | null {
|
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const GITIGNORE_ENTRIES = [
|
|
30
|
+
'.aiv/tree.json', // auto-generated snapshot — large, not useful in diffs
|
|
31
|
+
'.aiv/checklist.md', // personal pre-PR draft — not team-relevant
|
|
32
|
+
];
|
|
33
|
+
|
|
29
34
|
export function appendGitignore(cwd: string): void {
|
|
30
35
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
31
|
-
const entry = '.aiv/';
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
fs.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const current = fs.existsSync(gitignorePath)
|
|
38
|
+
? fs.readFileSync(gitignorePath, 'utf8')
|
|
39
|
+
: '';
|
|
40
|
+
|
|
41
|
+
const missing = GITIGNORE_ENTRIES.filter(e => !current.includes(e));
|
|
42
|
+
if (missing.length === 0) return;
|
|
43
|
+
|
|
44
|
+
const block = '\n# aiv — auto-generated / local-only files\n' + missing.join('\n') + '\n';
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
if (fs.existsSync(gitignorePath)) {
|
|
47
|
+
fs.appendFileSync(gitignorePath, block, 'utf8');
|
|
48
|
+
} else {
|
|
49
|
+
fs.writeFileSync(gitignorePath, block.trimStart(), 'utf8');
|
|
41
50
|
}
|
|
42
51
|
}
|
|
43
52
|
|
package/src/i18n/en.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const en = {
|
|
|
42
42
|
prsColPR: 'PR',
|
|
43
43
|
prsColTitle: 'Title',
|
|
44
44
|
prsColAuthor: 'Author',
|
|
45
|
-
prsColBranch: 'Branch',
|
|
45
|
+
prsColBranch: 'Branch → Target',
|
|
46
46
|
prsColChanges: 'Changes',
|
|
47
47
|
prsColCreated: 'Created',
|
|
48
48
|
prsFooter: (count: number) => ` Showing ${count} open PR(s). Run aiv review <pr-number> to analyze.\n`,
|
|
@@ -108,6 +108,9 @@ export const en = {
|
|
|
108
108
|
accountsRepoHint: ' Repo config: .aiv/config.yml\n',
|
|
109
109
|
|
|
110
110
|
// ── orchestrator ───────────────────────────────────────────────────────────
|
|
111
|
+
orchestratorTriaging: 'Analyzing diff to select agents...',
|
|
112
|
+
orchestratorTriageResult: (agents: string, reason: string) => ` Agents selected: ${agents} — ${reason}`,
|
|
113
|
+
orchestratorTriageSkipped: 'No agents needed for this diff.',
|
|
111
114
|
orchestratorRunning: (agent: string) => ` Running ${agent} agent...`,
|
|
112
115
|
orchestratorDone: (agent: string, count: number, score: number) => ` ${agent} — ${count} finding(s) [score: ${score}]`,
|
|
113
116
|
orchestratorAgentFailed: (agent: string, msg: string) => ` ${agent} failed: ${msg}`,
|
|
@@ -183,6 +186,13 @@ export const en = {
|
|
|
183
186
|
postReviewMergeStrategyMerge: 'Merge commit',
|
|
184
187
|
postReviewMergeStrategySquash: 'Squash and merge',
|
|
185
188
|
postReviewMergeStrategyRebase: 'Rebase and merge',
|
|
189
|
+
postReviewPostingComment: 'Posting review as PR comment...',
|
|
190
|
+
postReviewCommentPosted: (n: number) => ` Review posted as comment on PR #${n}.`,
|
|
191
|
+
postReviewCommentFailed: (msg: string) => `Failed to post comment: ${msg}`,
|
|
192
|
+
reviewCachedFound: 'Found a previous aiv analysis on this PR.',
|
|
193
|
+
reviewCachedUse: 'Use cached analysis?',
|
|
194
|
+
reviewCachedUsing: 'Using cached analysis.',
|
|
195
|
+
reviewCachedSkipping: 'Running fresh analysis...',
|
|
186
196
|
|
|
187
197
|
// ── context generate ───────────────────────────────────────────────────────
|
|
188
198
|
contextGenerateTitle: '\n Generating context and rules with AI...\n',
|
|
@@ -194,6 +204,21 @@ export const en = {
|
|
|
194
204
|
contextGenerateWritten: (file: string) => ` ${file} written.`,
|
|
195
205
|
contextGenerateProviderError: (envVar: string) => `Missing AI key: ${envVar}. Check your provider config.`,
|
|
196
206
|
|
|
207
|
+
// ── precheck command ───────────────────────────────────────────────────────
|
|
208
|
+
precheckTitle: (branch: string, base: string) => `\n aiv check — ${branch} → ${base}\n`,
|
|
209
|
+
precheckNotGitRepo: 'Not a git repository.',
|
|
210
|
+
precheckBuilding: (base: string) => `Building diff against ${base}...`,
|
|
211
|
+
precheckDiffBuilt: (files: number) => `Diff ready: ${files} file(s) changed`,
|
|
212
|
+
precheckDiffFailed: (msg: string) => `Failed to build local diff: ${msg}`,
|
|
213
|
+
precheckNoChanges: 'No changes detected against base branch.',
|
|
214
|
+
precheckSelectAction: 'What would you like to do?',
|
|
215
|
+
precheckCreatingPR: 'Creating PR on GitHub...',
|
|
216
|
+
precheckPRCreated: (url: string) => ` PR created: ${url}`,
|
|
217
|
+
precheckPRFailed: (msg: string) => `Failed to create PR: ${msg}`,
|
|
218
|
+
precheckSavingChecklist: 'Saving checklist...',
|
|
219
|
+
precheckChecklistSaved: (path: string) => ` Checklist saved to ${path}`,
|
|
220
|
+
precheckMissingConfig: 'GitHub owner/repo not configured. Use --owner and --repo or configure in .aiv/config.yml',
|
|
221
|
+
|
|
197
222
|
// ── agents command ─────────────────────────────────────────────────────────
|
|
198
223
|
agentsTitle: '\n aiv — Available Agents\n',
|
|
199
224
|
agentsColAgent: 'Agent',
|
package/src/i18n/es.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const es: TranslationKeys = {
|
|
|
44
44
|
prsColPR: 'PR',
|
|
45
45
|
prsColTitle: 'Título',
|
|
46
46
|
prsColAuthor: 'Autor',
|
|
47
|
-
prsColBranch: 'Rama',
|
|
47
|
+
prsColBranch: 'Rama → Destino',
|
|
48
48
|
prsColChanges: 'Cambios',
|
|
49
49
|
prsColCreated: 'Creado',
|
|
50
50
|
prsFooter: (count: number) => ` Mostrando ${count} PR(s) abierto(s). Ejecuta aiv review <numero-pr> para analizar.\n`,
|
|
@@ -110,6 +110,9 @@ export const es: TranslationKeys = {
|
|
|
110
110
|
accountsRepoHint: ' Config repo: .aiv/config.yml\n',
|
|
111
111
|
|
|
112
112
|
// ── orchestrator ───────────────────────────────────────────────────────────
|
|
113
|
+
orchestratorTriaging: 'Analizando diff para seleccionar agentes...',
|
|
114
|
+
orchestratorTriageResult: (agents: string, reason: string) => ` Agentes seleccionados: ${agents} — ${reason}`,
|
|
115
|
+
orchestratorTriageSkipped: 'No se necesitan agentes para este diff.',
|
|
113
116
|
orchestratorRunning: (agent: string) => ` Ejecutando agente ${agent}...`,
|
|
114
117
|
orchestratorDone: (agent: string, count: number, score: number) => ` ${agent} — ${count} hallazgo(s) [puntuación: ${score}]`,
|
|
115
118
|
orchestratorAgentFailed: (agent: string, msg: string) => ` ${agent} falló: ${msg}`,
|
|
@@ -185,6 +188,13 @@ export const es: TranslationKeys = {
|
|
|
185
188
|
postReviewMergeStrategyMerge: 'Merge commit',
|
|
186
189
|
postReviewMergeStrategySquash: 'Squash and merge',
|
|
187
190
|
postReviewMergeStrategyRebase: 'Rebase and merge',
|
|
191
|
+
postReviewPostingComment: 'Publicando revisión como comentario del PR...',
|
|
192
|
+
postReviewCommentPosted: (n: number) => ` Revisión publicada como comentario en el PR #${n}.`,
|
|
193
|
+
postReviewCommentFailed: (msg: string) => `Error al publicar comentario: ${msg}`,
|
|
194
|
+
reviewCachedFound: 'Se encontró un análisis previo de aiv en este PR.',
|
|
195
|
+
reviewCachedUse: '¿Usar el análisis en caché?',
|
|
196
|
+
reviewCachedUsing: 'Usando análisis en caché.',
|
|
197
|
+
reviewCachedSkipping: 'Ejecutando análisis nuevo...',
|
|
188
198
|
|
|
189
199
|
// ── context generate ───────────────────────────────────────────────────────
|
|
190
200
|
contextGenerateTitle: '\n Generando contexto y reglas con IA...\n',
|
|
@@ -196,6 +206,21 @@ export const es: TranslationKeys = {
|
|
|
196
206
|
contextGenerateWritten: (file: string) => ` ${file} escrito.`,
|
|
197
207
|
contextGenerateProviderError: (envVar: string) => `Clave de IA faltante: ${envVar}. Revisa tu config de proveedor.`,
|
|
198
208
|
|
|
209
|
+
// ── precheck command ───────────────────────────────────────────────────────
|
|
210
|
+
precheckTitle: (branch: string, base: string) => `\n aiv check — ${branch} → ${base}\n`,
|
|
211
|
+
precheckNotGitRepo: 'No es un repositorio git.',
|
|
212
|
+
precheckBuilding: (base: string) => `Construyendo diff contra ${base}...`,
|
|
213
|
+
precheckDiffBuilt: (files: number) => `Diff listo: ${files} archivo(s) modificado(s)`,
|
|
214
|
+
precheckDiffFailed: (msg: string) => `Error al construir el diff local: ${msg}`,
|
|
215
|
+
precheckNoChanges: 'No se detectaron cambios contra la rama base.',
|
|
216
|
+
precheckSelectAction: '¿Qué deseas hacer?',
|
|
217
|
+
precheckCreatingPR: 'Creando PR en GitHub...',
|
|
218
|
+
precheckPRCreated: (url: string) => ` PR creado: ${url}`,
|
|
219
|
+
precheckPRFailed: (msg: string) => `Error al crear el PR: ${msg}`,
|
|
220
|
+
precheckSavingChecklist: 'Guardando checklist...',
|
|
221
|
+
precheckChecklistSaved: (path: string) => ` Checklist guardado en ${path}`,
|
|
222
|
+
precheckMissingConfig: 'Owner/repo de GitHub no configurado. Usa --owner y --repo o configúralo en .aiv/config.yml',
|
|
223
|
+
|
|
199
224
|
// ── agents command ─────────────────────────────────────────────────────────
|
|
200
225
|
agentsTitle: '\n aiv — Agentes Disponibles\n',
|
|
201
226
|
agentsColAgent: 'Agente',
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { reviewCommand } from './cli/commands/review';
|
|
|
8
8
|
import { contextCommand } from './cli/commands/context';
|
|
9
9
|
import { configCommand } from './cli/commands/config';
|
|
10
10
|
import { agentsCommand } from './cli/commands/agents';
|
|
11
|
+
import { checkCommand } from './cli/commands/check';
|
|
11
12
|
|
|
12
13
|
initLang();
|
|
13
14
|
printBanner();
|
|
@@ -25,5 +26,6 @@ program.addCommand(reviewCommand());
|
|
|
25
26
|
program.addCommand(contextCommand());
|
|
26
27
|
program.addCommand(configCommand());
|
|
27
28
|
program.addCommand(agentsCommand());
|
|
29
|
+
program.addCommand(checkCommand());
|
|
28
30
|
|
|
29
31
|
program.parse(process.argv);
|
|
@@ -6,6 +6,7 @@ import { BusinessReviewer } from '../agents/business';
|
|
|
6
6
|
import { ArchitectureReviewer } from '../agents/architecture';
|
|
7
7
|
import { SecurityReviewer } from '../agents/security';
|
|
8
8
|
import { BaseAgent, AgentContext } from '../agents/base';
|
|
9
|
+
import { triageAgents } from '../agents/triage';
|
|
9
10
|
import { t } from '../i18n';
|
|
10
11
|
|
|
11
12
|
export class Orchestrator {
|
|
@@ -14,9 +15,16 @@ export class Orchestrator {
|
|
|
14
15
|
private readonly rules: AivRules,
|
|
15
16
|
) {}
|
|
16
17
|
|
|
17
|
-
async run(prDiff: PRDiff, projectContext: string,
|
|
18
|
-
const
|
|
18
|
+
async run(prDiff: PRDiff, projectContext: string, requestedAgents: string[], auto: boolean): Promise<ReviewResult> {
|
|
19
|
+
const agentNames = auto
|
|
20
|
+
? await this.triage(prDiff, requestedAgents)
|
|
21
|
+
: requestedAgents;
|
|
22
|
+
|
|
23
|
+
if (agentNames.length === 0) {
|
|
24
|
+
return buildEmptyResult(prDiff);
|
|
25
|
+
}
|
|
19
26
|
|
|
27
|
+
const agents = buildAgents(agentNames, this.config);
|
|
20
28
|
const agentCtx: AgentContext = { diff: prDiff, projectContext, rules: this.rules };
|
|
21
29
|
const agentResults = await runAgents(agents, agentCtx);
|
|
22
30
|
|
|
@@ -37,6 +45,44 @@ export class Orchestrator {
|
|
|
37
45
|
generatedAt: new Date().toISOString(),
|
|
38
46
|
};
|
|
39
47
|
}
|
|
48
|
+
|
|
49
|
+
private async triage(prDiff: PRDiff, requested: string[]): Promise<string[]> {
|
|
50
|
+
const spinner = ora(t().orchestratorTriaging).start();
|
|
51
|
+
try {
|
|
52
|
+
const provider = createProviderFor(this.config, 'triage');
|
|
53
|
+
const result = await triageAgents(prDiff, provider);
|
|
54
|
+
const selected = result.agents.filter(a => requested.includes(a));
|
|
55
|
+
|
|
56
|
+
if (selected.length === 0) {
|
|
57
|
+
spinner.succeed(chalk.dim(t().orchestratorTriageSkipped));
|
|
58
|
+
} else {
|
|
59
|
+
spinner.succeed(chalk.dim(t().orchestratorTriageResult(
|
|
60
|
+
selected.map(a => chalk.cyan(a)).join(', '),
|
|
61
|
+
result.reasoning,
|
|
62
|
+
)));
|
|
63
|
+
}
|
|
64
|
+
return selected;
|
|
65
|
+
} catch {
|
|
66
|
+
spinner.warn(chalk.yellow('Triage failed — running all agents'));
|
|
67
|
+
return requested;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildEmptyResult(prDiff: PRDiff): ReviewResult {
|
|
73
|
+
return {
|
|
74
|
+
prNumber: prDiff.pr.number,
|
|
75
|
+
prTitle: prDiff.pr.title,
|
|
76
|
+
executiveSummary: t().orchestratorTriageSkipped,
|
|
77
|
+
riskScore: 0,
|
|
78
|
+
riskLabel: 'LOW',
|
|
79
|
+
agents: [],
|
|
80
|
+
businessRisks: [],
|
|
81
|
+
architectureIssues: [],
|
|
82
|
+
securityIssues: [],
|
|
83
|
+
possibleRegressions: [],
|
|
84
|
+
generatedAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
40
86
|
}
|
|
41
87
|
|
|
42
88
|
function buildAgents(names: string[], config: ResolvedConfig): BaseAgent[] {
|