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