jstar-reviewer 2.1.4 ā 2.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.
- package/bin/jstar.js +12 -1
- package/dist/scripts/chat.js +150 -0
- package/dist/scripts/config.js +5 -1
- package/dist/scripts/core/critique.js +137 -0
- package/dist/scripts/core/debate.js +95 -0
- package/dist/scripts/detective.js +5 -4
- package/dist/scripts/reviewer.js +136 -41
- package/dist/scripts/session.js +273 -0
- package/dist/scripts/ui/interaction.js +43 -0
- package/dist/scripts/utils/logger.js +110 -0
- package/package.json +14 -10
- package/scripts/chat.ts +130 -0
- package/scripts/config.ts +5 -1
- package/scripts/core/critique.ts +162 -0
- package/scripts/core/debate.ts +111 -0
- package/scripts/detective.ts +5 -4
- package/scripts/reviewer.ts +151 -41
- package/scripts/session.ts +312 -0
- package/scripts/types.ts +9 -0
- package/scripts/ui/interaction.ts +38 -0
- package/scripts/utils/logger.ts +118 -0
- package/setup.js +1 -1
- package/scripts/local-embedding.ts +0 -55
package/scripts/reviewer.ts
CHANGED
|
@@ -5,12 +5,15 @@ import * as path from "path";
|
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import simpleGit from "simple-git";
|
|
8
|
+
import { Logger } from "./utils/logger";
|
|
8
9
|
import { Config } from "./config";
|
|
9
10
|
import { Detective } from "./detective";
|
|
10
11
|
import { GeminiEmbedding } from "./gemini-embedding";
|
|
11
12
|
import { MockLLM } from "./mock-llm";
|
|
12
13
|
import { FileFinding, DashboardReport, LLMReviewResponse, EMPTY_REVIEW } from "./types";
|
|
13
14
|
import { renderDashboard, determineStatus, generateRecommendation } from "./dashboard";
|
|
15
|
+
import { startInteractiveSession } from "./session";
|
|
16
|
+
import { critiqueFindings } from "./core/critique";
|
|
14
17
|
import {
|
|
15
18
|
VectorStoreIndex,
|
|
16
19
|
storageContextFromDefaults,
|
|
@@ -66,6 +69,38 @@ function sleep(ms: number): Promise<void> {
|
|
|
66
69
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Filter issues by confidence threshold and log what was removed
|
|
74
|
+
*/
|
|
75
|
+
function filterByConfidence(findings: FileFinding[]): FileFinding[] {
|
|
76
|
+
const threshold = Config.CONFIDENCE_THRESHOLD;
|
|
77
|
+
let removedCount = 0;
|
|
78
|
+
|
|
79
|
+
const filtered = findings.map(finding => {
|
|
80
|
+
const validIssues = finding.issues.filter(issue => {
|
|
81
|
+
const confidence = issue.confidenceScore ?? 5; // Default to high if not specified
|
|
82
|
+
if (confidence < threshold) {
|
|
83
|
+
removedCount++;
|
|
84
|
+
Logger.info(chalk.dim(` ā” Low confidence (${confidence}): "${issue.title}" - filtered out`));
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...finding,
|
|
92
|
+
issues: validIssues,
|
|
93
|
+
severity: validIssues.length === 0 ? 'LGTM' as const : finding.severity
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (removedCount > 0) {
|
|
98
|
+
Logger.info(chalk.blue(`\n š Confidence Filter: ${removedCount} low-confidence issue(s) removed\n`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return filtered;
|
|
102
|
+
}
|
|
103
|
+
|
|
69
104
|
function parseReviewResponse(text: string): LLMReviewResponse {
|
|
70
105
|
try {
|
|
71
106
|
// Try to extract JSON from the response
|
|
@@ -108,20 +143,23 @@ function parseReviewResponse(text: string): LLMReviewResponse {
|
|
|
108
143
|
|
|
109
144
|
// --- Main ---
|
|
110
145
|
async function main() {
|
|
111
|
-
|
|
146
|
+
// Initialize logger mode based on CLI flags
|
|
147
|
+
Logger.init();
|
|
148
|
+
|
|
149
|
+
Logger.info(chalk.blue("šµļø J-Star Reviewer: Analyzing your changes...\n"));
|
|
112
150
|
|
|
113
151
|
// 0. Environment Validation
|
|
114
152
|
if (!geminiKey || !process.env.GROQ_API_KEY) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
153
|
+
Logger.error(chalk.red("ā Missing API Keys!"));
|
|
154
|
+
Logger.info(chalk.yellow("\nPlease ensure you have a .env.local file with:"));
|
|
155
|
+
Logger.info(chalk.white("- GEMINI_API_KEY (or GOOGLE_API_KEY)"));
|
|
156
|
+
Logger.info(chalk.white("- GROQ_API_KEY"));
|
|
157
|
+
Logger.info(chalk.white("\nCheck .env.example for a template.\n"));
|
|
120
158
|
return;
|
|
121
159
|
}
|
|
122
160
|
|
|
123
161
|
// 1. Detective
|
|
124
|
-
|
|
162
|
+
Logger.info(chalk.blue("š Running Detective Engine..."));
|
|
125
163
|
|
|
126
164
|
const detective = new Detective(SOURCE_DIR);
|
|
127
165
|
await detective.scan();
|
|
@@ -130,13 +168,13 @@ async function main() {
|
|
|
130
168
|
// 1. Get the Diff
|
|
131
169
|
const diff = await git.diff(["--staged"]);
|
|
132
170
|
if (!diff) {
|
|
133
|
-
|
|
171
|
+
Logger.info(chalk.green("\nā
No staged changes to review. (Did you 'git add'?)"));
|
|
134
172
|
return;
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
// 2. Load the Brain
|
|
138
176
|
if (!fs.existsSync(STORAGE_DIR)) {
|
|
139
|
-
|
|
177
|
+
Logger.error(chalk.red("ā Local Brain not found. Run 'pnpm run index:init' first."));
|
|
140
178
|
return;
|
|
141
179
|
}
|
|
142
180
|
const storageContext = await storageContextFromDefaults({ persistDir: STORAGE_DIR });
|
|
@@ -150,15 +188,15 @@ async function main() {
|
|
|
150
188
|
const contextNodes = await retriever.retrieve(keywords);
|
|
151
189
|
const relatedContext = contextNodes.map(n => n.node.getContent(MetadataMode.NONE).slice(0, 1500)).join("\n");
|
|
152
190
|
|
|
153
|
-
|
|
191
|
+
Logger.info(chalk.yellow(`\nš§ Found ${contextNodes.length} context chunk.`));
|
|
154
192
|
|
|
155
193
|
// 4. Chunk the Diff
|
|
156
194
|
const fileChunks = chunkDiffByFile(diff);
|
|
157
195
|
const totalTokens = estimateTokens(diff);
|
|
158
|
-
|
|
196
|
+
Logger.info(chalk.dim(` Total diff: ~${totalTokens} tokens across ${fileChunks.length} files.`));
|
|
159
197
|
|
|
160
|
-
// 5. Structured JSON Prompt
|
|
161
|
-
const systemPrompt = `You are J-Star, a Senior Code Reviewer. Be
|
|
198
|
+
// 5. Structured JSON Prompt (Conservative)
|
|
199
|
+
const systemPrompt = `You are J-Star, a Senior Code Reviewer. Be CONSERVATIVE and PRECISE.
|
|
162
200
|
|
|
163
201
|
Analyze the Git Diff and return a JSON response with this EXACT structure:
|
|
164
202
|
{
|
|
@@ -168,7 +206,8 @@ Analyze the Git Diff and return a JSON response with this EXACT structure:
|
|
|
168
206
|
"title": "Short issue title",
|
|
169
207
|
"description": "Detailed description of the problem",
|
|
170
208
|
"line": 42,
|
|
171
|
-
"fixPrompt": "A specific prompt an AI can use to fix this issue"
|
|
209
|
+
"fixPrompt": "A specific prompt an AI can use to fix this issue",
|
|
210
|
+
"confidenceScore": 5
|
|
172
211
|
}
|
|
173
212
|
]
|
|
174
213
|
}
|
|
@@ -179,10 +218,20 @@ SEVERITY GUIDE:
|
|
|
179
218
|
- P2_MEDIUM: Code quality, missing types, cleanup needed
|
|
180
219
|
- LGTM: No issues found (return empty issues array)
|
|
181
220
|
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
221
|
+
CONFIDENCE SCORE (1-5) - BE HONEST:
|
|
222
|
+
- 5: Absolutely certain. The bug is obvious in the diff.
|
|
223
|
+
- 4: Very likely. Clear code smell or anti-pattern.
|
|
224
|
+
- 3: Probable issue, might be missing context.
|
|
225
|
+
- 2: Unsure, could be intentional.
|
|
226
|
+
- 1: Speculation, likely false positive.
|
|
227
|
+
|
|
228
|
+
CRITICAL RULES:
|
|
229
|
+
1. Only flag issues you are HIGHLY confident about (4-5).
|
|
230
|
+
2. Test mocks, stubs, and intentional patterns are NOT bugs.
|
|
231
|
+
3. If the code looks intentional or well-handled, it's probably fine.
|
|
232
|
+
4. When in doubt, lean towards LGTM.
|
|
233
|
+
5. Return ONLY valid JSON, no markdown.
|
|
234
|
+
6. If the file is clean: {"severity": "LGTM", "issues": []}
|
|
186
235
|
|
|
187
236
|
Context: ${relatedContext.slice(0, 800)}`;
|
|
188
237
|
|
|
@@ -190,7 +239,7 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
190
239
|
let chunkIndex = 0;
|
|
191
240
|
let skippedCount = 0;
|
|
192
241
|
|
|
193
|
-
|
|
242
|
+
Logger.info(chalk.blue("\nāļø Sending to Judge...\n"));
|
|
194
243
|
|
|
195
244
|
for (const chunk of fileChunks) {
|
|
196
245
|
chunkIndex++;
|
|
@@ -198,7 +247,7 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
198
247
|
|
|
199
248
|
// Skip excluded files
|
|
200
249
|
if (shouldSkipFile(fileName)) {
|
|
201
|
-
|
|
250
|
+
Logger.info(chalk.dim(` āļø Skipping ${fileName} (excluded)`));
|
|
202
251
|
skippedCount++;
|
|
203
252
|
continue;
|
|
204
253
|
}
|
|
@@ -207,7 +256,7 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
207
256
|
|
|
208
257
|
// Skip huge files
|
|
209
258
|
if (chunkTokens > MAX_TOKENS_PER_REQUEST) {
|
|
210
|
-
|
|
259
|
+
Logger.info(chalk.yellow(` ā ļø Skipping ${fileName} (too large: ~${chunkTokens} tokens)`));
|
|
211
260
|
findings.push({
|
|
212
261
|
file: fileName,
|
|
213
262
|
severity: Config.DEFAULT_SEVERITY,
|
|
@@ -220,7 +269,7 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
220
269
|
continue;
|
|
221
270
|
}
|
|
222
271
|
|
|
223
|
-
|
|
272
|
+
Logger.progress(chalk.dim(` š ${fileName}...`));
|
|
224
273
|
|
|
225
274
|
try {
|
|
226
275
|
const { text } = await generateText({
|
|
@@ -240,10 +289,10 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
240
289
|
const emoji = response.severity === 'LGTM' ? 'ā
' :
|
|
241
290
|
response.severity === 'P0_CRITICAL' ? 'š' :
|
|
242
291
|
response.severity === 'P1_HIGH' ? 'ā ļø' : 'š';
|
|
243
|
-
|
|
292
|
+
Logger.info(` ${emoji}`);
|
|
244
293
|
|
|
245
294
|
} catch (error: any) {
|
|
246
|
-
|
|
295
|
+
Logger.info(chalk.red(` ā (${error.message.slice(0, 50)})`));
|
|
247
296
|
findings.push({
|
|
248
297
|
file: fileName,
|
|
249
298
|
severity: Config.DEFAULT_SEVERITY,
|
|
@@ -261,15 +310,24 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
261
310
|
}
|
|
262
311
|
}
|
|
263
312
|
|
|
264
|
-
// 6.
|
|
313
|
+
// 6. Confidence Filtering
|
|
314
|
+
Logger.info(chalk.blue("\nšÆ Filtering by Confidence...\n"));
|
|
315
|
+
let filteredFindings = filterByConfidence(findings);
|
|
316
|
+
|
|
317
|
+
// 7. Self-Critique Pass (if enabled)
|
|
318
|
+
if (Config.ENABLE_SELF_CRITIQUE) {
|
|
319
|
+
filteredFindings = await critiqueFindings(filteredFindings, diff);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 8. Build Dashboard Report
|
|
265
323
|
const metrics = {
|
|
266
324
|
filesScanned: fileChunks.length - skippedCount,
|
|
267
325
|
totalTokens,
|
|
268
|
-
violations:
|
|
269
|
-
critical:
|
|
270
|
-
high:
|
|
271
|
-
medium:
|
|
272
|
-
lgtm:
|
|
326
|
+
violations: filteredFindings.reduce((sum, f) => sum + f.issues.length, 0),
|
|
327
|
+
critical: filteredFindings.filter(f => f.severity === 'P0_CRITICAL').length,
|
|
328
|
+
high: filteredFindings.filter(f => f.severity === 'P1_HIGH').length,
|
|
329
|
+
medium: filteredFindings.filter(f => f.severity === 'P2_MEDIUM').length,
|
|
330
|
+
lgtm: filteredFindings.filter(f => f.severity === 'LGTM').length,
|
|
273
331
|
};
|
|
274
332
|
|
|
275
333
|
const report: DashboardReport = {
|
|
@@ -277,7 +335,7 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
277
335
|
reviewer: 'Detective Engine & Judge',
|
|
278
336
|
status: determineStatus(metrics),
|
|
279
337
|
metrics,
|
|
280
|
-
findings,
|
|
338
|
+
findings: filteredFindings,
|
|
281
339
|
recommendedAction: generateRecommendation(metrics)
|
|
282
340
|
};
|
|
283
341
|
|
|
@@ -288,20 +346,72 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
288
346
|
fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true });
|
|
289
347
|
fs.writeFileSync(OUTPUT_FILE, dashboard);
|
|
290
348
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
349
|
+
// Save Session State for "jstar chat"
|
|
350
|
+
const SESSION_FILE = path.join(process.cwd(), ".jstar", "session.json");
|
|
351
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify({
|
|
352
|
+
date: report.date,
|
|
353
|
+
findings: report.findings,
|
|
354
|
+
metrics: report.metrics
|
|
355
|
+
}, null, 2));
|
|
356
|
+
|
|
357
|
+
Logger.info("\n" + chalk.bold.green("š DASHBOARD GENERATED"));
|
|
358
|
+
Logger.info(chalk.dim(` Saved to: ${OUTPUT_FILE}`));
|
|
359
|
+
Logger.info("\n" + chalk.bold.white("ā".repeat(50)));
|
|
294
360
|
|
|
295
361
|
// Print summary to console
|
|
296
362
|
const statusEmoji = report.status === 'APPROVED' ? 'š¢' :
|
|
297
363
|
report.status === 'NEEDS_REVIEW' ? 'š”' : 'š“';
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
364
|
+
Logger.info(`\n${statusEmoji} Status: ${report.status.replace('_', ' ')}`);
|
|
365
|
+
Logger.info(` š Critical: ${metrics.critical}`);
|
|
366
|
+
Logger.info(` ā ļø High: ${metrics.high}`);
|
|
367
|
+
Logger.info(` š Medium: ${metrics.medium}`);
|
|
368
|
+
Logger.info(` ā
LGTM: ${metrics.lgtm}`);
|
|
369
|
+
Logger.info(`\nš” ${report.recommendedAction}`);
|
|
370
|
+
Logger.info(chalk.dim(`\nš Full report: ${OUTPUT_FILE}`));
|
|
371
|
+
|
|
372
|
+
// 8. Interactive Session OR JSON Output
|
|
373
|
+
if (Logger.isHeadless()) {
|
|
374
|
+
// In JSON mode: output report to stdout and skip interactive session
|
|
375
|
+
Logger.json(report);
|
|
376
|
+
} else {
|
|
377
|
+
// Normal TUI mode: start interactive session
|
|
378
|
+
const { updatedFindings, hasUpdates } = await startInteractiveSession(findings, index);
|
|
379
|
+
|
|
380
|
+
if (hasUpdates) {
|
|
381
|
+
Logger.info(chalk.blue("\nš Updating Dashboard with session changes..."));
|
|
382
|
+
|
|
383
|
+
// Recalculate metrics
|
|
384
|
+
const newMetrics = {
|
|
385
|
+
filesScanned: fileChunks.length - skippedCount,
|
|
386
|
+
totalTokens,
|
|
387
|
+
violations: updatedFindings.reduce((sum, f) => sum + f.issues.length, 0),
|
|
388
|
+
critical: updatedFindings.filter(f => f.severity === 'P0_CRITICAL').length,
|
|
389
|
+
high: updatedFindings.filter(f => f.severity === 'P1_HIGH').length,
|
|
390
|
+
medium: updatedFindings.filter(f => f.severity === 'P2_MEDIUM').length,
|
|
391
|
+
lgtm: updatedFindings.filter(f => f.severity === 'LGTM').length,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const newReport: DashboardReport = {
|
|
395
|
+
...report, // Keep date/reviewer
|
|
396
|
+
metrics: newMetrics,
|
|
397
|
+
findings: updatedFindings,
|
|
398
|
+
status: determineStatus(newMetrics),
|
|
399
|
+
recommendedAction: generateRecommendation(newMetrics)
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const newDashboard = renderDashboard(newReport);
|
|
403
|
+
fs.writeFileSync(OUTPUT_FILE, newDashboard);
|
|
404
|
+
|
|
405
|
+
// Also update session file with new findings
|
|
406
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify({
|
|
407
|
+
date: newReport.date,
|
|
408
|
+
findings: newReport.findings,
|
|
409
|
+
metrics: newReport.metrics
|
|
410
|
+
}, null, 2));
|
|
411
|
+
|
|
412
|
+
Logger.info(chalk.bold.green("š DASHBOARD UPDATED"));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
305
415
|
}
|
|
306
416
|
|
|
307
417
|
main().catch(console.error);
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { FileFinding, ReviewIssue } from "./types";
|
|
2
|
+
import { showActionMenu, askForArgument } from "./ui/interaction";
|
|
3
|
+
import { debateIssue } from "./core/debate";
|
|
4
|
+
import { VectorStoreIndex } from "llamaindex";
|
|
5
|
+
import { Logger } from "./utils/logger";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import * as readline from "readline";
|
|
9
|
+
|
|
10
|
+
export async function startInteractiveSession(
|
|
11
|
+
findings: FileFinding[],
|
|
12
|
+
index: VectorStoreIndex
|
|
13
|
+
): Promise<{ updatedFindings: FileFinding[], hasUpdates: boolean }> {
|
|
14
|
+
|
|
15
|
+
// Deep clone to track local state without mutating original immediately (though we return updated findings)
|
|
16
|
+
const interactiveFindings: FileFinding[] = JSON.parse(JSON.stringify(findings));
|
|
17
|
+
let hasUpdates = false;
|
|
18
|
+
let active = true;
|
|
19
|
+
|
|
20
|
+
if (interactiveFindings.length === 0 || interactiveFindings.every(f => f.issues.length === 0)) {
|
|
21
|
+
return { updatedFindings: interactiveFindings, hasUpdates: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(chalk.bold.magenta("\nš£ļø Interactive Review Session"));
|
|
25
|
+
console.log(chalk.dim(" Use arrow keys to navigate. Select an issue to debate."));
|
|
26
|
+
|
|
27
|
+
while (active) {
|
|
28
|
+
// Re-calculate choices every loop to reflect status changes
|
|
29
|
+
// Flatten
|
|
30
|
+
const flatIssues: {
|
|
31
|
+
issue: ReviewIssue,
|
|
32
|
+
fileIndex: number,
|
|
33
|
+
issueIndex: number,
|
|
34
|
+
file: string
|
|
35
|
+
}[] = [];
|
|
36
|
+
|
|
37
|
+
interactiveFindings.forEach((f, fIdx) => {
|
|
38
|
+
f.issues.forEach((i, iIdx) => {
|
|
39
|
+
flatIssues.push({
|
|
40
|
+
issue: i,
|
|
41
|
+
fileIndex: fIdx,
|
|
42
|
+
issueIndex: iIdx,
|
|
43
|
+
file: f.file
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const choices = flatIssues.map((item, idx) => {
|
|
49
|
+
const i = item.issue;
|
|
50
|
+
const statusIcon = i.status === 'resolved' ? 'ā
' : i.status === 'ignored' ? 'šļø ' : '';
|
|
51
|
+
return {
|
|
52
|
+
title: `${statusIcon}${i.confidenceScore ? `[${i.confidenceScore}/5] ` : ''}${i.title} ${chalk.dim(`(${item.file})`)}`,
|
|
53
|
+
value: idx,
|
|
54
|
+
description: i.status ? `Marked as ${i.status}` : i.description.slice(0, 80)
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
choices.push({ title: 'šŖ Finish Review', value: -1, description: 'Exit and save report' });
|
|
59
|
+
|
|
60
|
+
const { selectedIdx } = await prompts({
|
|
61
|
+
type: 'select',
|
|
62
|
+
name: 'selectedIdx',
|
|
63
|
+
message: 'Select an issue:',
|
|
64
|
+
choices: choices,
|
|
65
|
+
initial: 0
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (selectedIdx === undefined || selectedIdx === -1) {
|
|
69
|
+
active = false;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const selected = flatIssues[selectedIdx];
|
|
74
|
+
const { issue, file } = selected;
|
|
75
|
+
|
|
76
|
+
// Show Details
|
|
77
|
+
console.log(chalk.cyan(`\nTitle: ${issue.title}`));
|
|
78
|
+
console.log(chalk.white(issue.description));
|
|
79
|
+
console.log(chalk.dim(`File: ${file}`));
|
|
80
|
+
if (issue.confidenceScore) console.log(chalk.yellow(`Confidence: ${issue.confidenceScore}/5`));
|
|
81
|
+
if (issue.status) console.log(chalk.green(`Status: ${issue.status}`));
|
|
82
|
+
|
|
83
|
+
// Action Menu
|
|
84
|
+
const action = await showActionMenu(issue.title);
|
|
85
|
+
|
|
86
|
+
if (action === 'discuss') {
|
|
87
|
+
const argument = await askForArgument();
|
|
88
|
+
const result = await debateIssue(
|
|
89
|
+
issue.title,
|
|
90
|
+
issue.description,
|
|
91
|
+
file,
|
|
92
|
+
argument,
|
|
93
|
+
index
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
console.log(chalk.yellow(`\nš¤ Bot: ${result.text}`));
|
|
97
|
+
|
|
98
|
+
if (result.severity === 'LGTM') {
|
|
99
|
+
console.log(chalk.green("ā
Issue withdrawn by AI!"));
|
|
100
|
+
// Direct update to our state
|
|
101
|
+
interactiveFindings[selected.fileIndex].issues[selected.issueIndex].status = 'resolved';
|
|
102
|
+
hasUpdates = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
} else if (action === 'ignore') {
|
|
106
|
+
console.log(chalk.dim('Issue ignored locally.'));
|
|
107
|
+
interactiveFindings[selected.fileIndex].issues[selected.issueIndex].status = 'ignored';
|
|
108
|
+
hasUpdates = true;
|
|
109
|
+
} else if (action === 'accept') {
|
|
110
|
+
console.log(chalk.green('Issue accepted.'));
|
|
111
|
+
} else if (action === 'exit') {
|
|
112
|
+
active = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Filter out resolved/ignored issues for the final report
|
|
117
|
+
const finalFindings: FileFinding[] = interactiveFindings.map(f => ({
|
|
118
|
+
...f,
|
|
119
|
+
issues: f.issues.filter(i => i.status !== 'resolved' && i.status !== 'ignored')
|
|
120
|
+
})).filter(f => f.issues.length > 0);
|
|
121
|
+
|
|
122
|
+
return { updatedFindings: finalFindings, hasUpdates };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Headless interface for flatIssues structure
|
|
127
|
+
*/
|
|
128
|
+
interface FlatIssue {
|
|
129
|
+
id: number;
|
|
130
|
+
title: string;
|
|
131
|
+
description: string;
|
|
132
|
+
file: string;
|
|
133
|
+
confidenceScore?: number;
|
|
134
|
+
status?: 'resolved' | 'ignored';
|
|
135
|
+
fileIndex: number;
|
|
136
|
+
issueIndex: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Headless command protocol
|
|
141
|
+
*/
|
|
142
|
+
interface HeadlessCommand {
|
|
143
|
+
action: 'list' | 'debate' | 'ignore' | 'accept' | 'exit';
|
|
144
|
+
issueId?: number;
|
|
145
|
+
argument?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Flatten findings into a simple array for headless mode
|
|
150
|
+
*/
|
|
151
|
+
function flattenIssues(findings: FileFinding[]): FlatIssue[] {
|
|
152
|
+
const flat: FlatIssue[] = [];
|
|
153
|
+
findings.forEach((f, fIdx) => {
|
|
154
|
+
f.issues.forEach((i, iIdx) => {
|
|
155
|
+
flat.push({
|
|
156
|
+
id: flat.length,
|
|
157
|
+
title: i.title,
|
|
158
|
+
description: i.description,
|
|
159
|
+
file: f.file,
|
|
160
|
+
confidenceScore: i.confidenceScore,
|
|
161
|
+
status: i.status,
|
|
162
|
+
fileIndex: fIdx,
|
|
163
|
+
issueIndex: iIdx
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
return flat;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Headless session for AI agents and CI/CD.
|
|
172
|
+
*
|
|
173
|
+
* Protocol:
|
|
174
|
+
* - Input (stdin): JSON commands, one per line
|
|
175
|
+
* { "action": "list" }
|
|
176
|
+
* { "action": "debate", "issueId": 0, "argument": "This is intentional" }
|
|
177
|
+
* { "action": "ignore", "issueId": 0 }
|
|
178
|
+
* { "action": "accept", "issueId": 0 }
|
|
179
|
+
* { "action": "exit" }
|
|
180
|
+
*
|
|
181
|
+
* - Output (stdout): JSON events, one per line
|
|
182
|
+
* { "type": "ready", "issues": [...] }
|
|
183
|
+
* { "type": "list", "issues": [...] }
|
|
184
|
+
* { "type": "response", "issueId": 0, "text": "...", "verdict": "LGTM" | "STANDS" }
|
|
185
|
+
* { "type": "update", "issueId": 0, "status": "ignored" | "resolved" | "accepted" }
|
|
186
|
+
* { "type": "error", "message": "..." }
|
|
187
|
+
* { "type": "done", "hasUpdates": true, "updatedFindings": [...] }
|
|
188
|
+
*/
|
|
189
|
+
export async function startHeadlessSession(
|
|
190
|
+
findings: FileFinding[],
|
|
191
|
+
index: VectorStoreIndex
|
|
192
|
+
): Promise<{ updatedFindings: FileFinding[], hasUpdates: boolean }> {
|
|
193
|
+
|
|
194
|
+
// Deep clone to track state
|
|
195
|
+
const sessionFindings: FileFinding[] = JSON.parse(JSON.stringify(findings));
|
|
196
|
+
let hasUpdates = false;
|
|
197
|
+
|
|
198
|
+
// Emit ready event with all issues
|
|
199
|
+
const issues = flattenIssues(sessionFindings);
|
|
200
|
+
Logger.json({ type: 'ready', issues });
|
|
201
|
+
|
|
202
|
+
// Create readline interface for stdin
|
|
203
|
+
const rl = readline.createInterface({
|
|
204
|
+
input: process.stdin,
|
|
205
|
+
output: process.stdout,
|
|
206
|
+
terminal: false
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Process commands
|
|
210
|
+
for await (const line of rl) {
|
|
211
|
+
if (!line.trim()) continue;
|
|
212
|
+
|
|
213
|
+
let cmd: HeadlessCommand;
|
|
214
|
+
try {
|
|
215
|
+
cmd = JSON.parse(line);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
Logger.json({ type: 'error', message: 'Invalid JSON command' });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const currentIssues = flattenIssues(sessionFindings);
|
|
222
|
+
|
|
223
|
+
switch (cmd.action) {
|
|
224
|
+
case 'list':
|
|
225
|
+
Logger.json({ type: 'list', issues: currentIssues });
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case 'debate':
|
|
229
|
+
if (cmd.issueId === undefined || !cmd.argument) {
|
|
230
|
+
Logger.json({ type: 'error', message: 'debate requires issueId and argument' });
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
const debateTarget = currentIssues.find(i => i.id === cmd.issueId);
|
|
234
|
+
if (!debateTarget) {
|
|
235
|
+
Logger.json({ type: 'error', message: `Issue ${cmd.issueId} not found` });
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const result = await debateIssue(
|
|
241
|
+
debateTarget.title,
|
|
242
|
+
debateTarget.description,
|
|
243
|
+
debateTarget.file,
|
|
244
|
+
cmd.argument,
|
|
245
|
+
index
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const verdict = result.severity === 'LGTM' ? 'LGTM' : 'STANDS';
|
|
249
|
+
Logger.json({
|
|
250
|
+
type: 'response',
|
|
251
|
+
issueId: cmd.issueId,
|
|
252
|
+
text: result.text,
|
|
253
|
+
verdict
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (result.severity === 'LGTM') {
|
|
257
|
+
sessionFindings[debateTarget.fileIndex].issues[debateTarget.issueIndex].status = 'resolved';
|
|
258
|
+
hasUpdates = true;
|
|
259
|
+
Logger.json({ type: 'update', issueId: cmd.issueId, status: 'resolved' });
|
|
260
|
+
}
|
|
261
|
+
} catch (e: any) {
|
|
262
|
+
Logger.json({ type: 'error', message: e.message });
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case 'ignore':
|
|
267
|
+
if (cmd.issueId === undefined) {
|
|
268
|
+
Logger.json({ type: 'error', message: 'ignore requires issueId' });
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
const ignoreTarget = currentIssues.find(i => i.id === cmd.issueId);
|
|
272
|
+
if (!ignoreTarget) {
|
|
273
|
+
Logger.json({ type: 'error', message: `Issue ${cmd.issueId} not found` });
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
sessionFindings[ignoreTarget.fileIndex].issues[ignoreTarget.issueIndex].status = 'ignored';
|
|
277
|
+
hasUpdates = true;
|
|
278
|
+
Logger.json({ type: 'update', issueId: cmd.issueId, status: 'ignored' });
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case 'accept':
|
|
282
|
+
if (cmd.issueId === undefined) {
|
|
283
|
+
Logger.json({ type: 'error', message: 'accept requires issueId' });
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
Logger.json({ type: 'update', issueId: cmd.issueId, status: 'accepted' });
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case 'exit':
|
|
290
|
+
// Filter out resolved/ignored for final report
|
|
291
|
+
const finalFindings: FileFinding[] = sessionFindings.map(f => ({
|
|
292
|
+
...f,
|
|
293
|
+
issues: f.issues.filter(i => i.status !== 'resolved' && i.status !== 'ignored')
|
|
294
|
+
})).filter(f => f.issues.length > 0);
|
|
295
|
+
|
|
296
|
+
Logger.json({ type: 'done', hasUpdates, updatedFindings: finalFindings });
|
|
297
|
+
rl.close();
|
|
298
|
+
return { updatedFindings: finalFindings, hasUpdates };
|
|
299
|
+
|
|
300
|
+
default:
|
|
301
|
+
Logger.json({ type: 'error', message: `Unknown action: ${(cmd as any).action}` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// If stdin closes without exit command, still return
|
|
306
|
+
const finalFindings: FileFinding[] = sessionFindings.map(f => ({
|
|
307
|
+
...f,
|
|
308
|
+
issues: f.issues.filter(i => i.status !== 'resolved' && i.status !== 'ignored')
|
|
309
|
+
})).filter(f => f.issues.length > 0);
|
|
310
|
+
|
|
311
|
+
return { updatedFindings: finalFindings, hasUpdates };
|
|
312
|
+
}
|
package/scripts/types.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface ReviewIssue {
|
|
|
10
10
|
description: string;
|
|
11
11
|
line?: number;
|
|
12
12
|
fixPrompt: string;
|
|
13
|
+
confidenceScore?: number;
|
|
14
|
+
status?: 'resolved' | 'ignored';
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface FileFinding {
|
|
@@ -35,6 +37,12 @@ export interface DashboardReport {
|
|
|
35
37
|
recommendedAction: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export interface SessionState {
|
|
41
|
+
date: string;
|
|
42
|
+
findings: FileFinding[];
|
|
43
|
+
metrics: DashboardReport['metrics'];
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
/**
|
|
39
47
|
* Schema for LLM response (per-file review)
|
|
40
48
|
*/
|
|
@@ -45,6 +53,7 @@ export interface LLMReviewResponse {
|
|
|
45
53
|
description: string;
|
|
46
54
|
line?: number;
|
|
47
55
|
fixPrompt: string;
|
|
56
|
+
confidenceScore?: number; // 1-5 confidence rating
|
|
48
57
|
}[];
|
|
49
58
|
}
|
|
50
59
|
|