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/dist/scripts/reviewer.js
CHANGED
|
@@ -43,11 +43,14 @@ 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
55
|
const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
53
56
|
const google = (0, google_1.createGoogleGenerativeAI)({ apiKey: geminiKey });
|
|
@@ -88,6 +91,33 @@ function chunkDiffByFile(diff) {
|
|
|
88
91
|
function sleep(ms) {
|
|
89
92
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
90
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
|
+
}
|
|
91
121
|
function parseReviewResponse(text) {
|
|
92
122
|
try {
|
|
93
123
|
// Try to extract JSON from the response
|
|
@@ -125,30 +155,32 @@ function parseReviewResponse(text) {
|
|
|
125
155
|
}
|
|
126
156
|
// --- Main ---
|
|
127
157
|
async function main() {
|
|
128
|
-
|
|
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"));
|
|
129
161
|
// 0. Environment Validation
|
|
130
162
|
if (!geminiKey || !process.env.GROQ_API_KEY) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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"));
|
|
136
168
|
return;
|
|
137
169
|
}
|
|
138
170
|
// 1. Detective
|
|
139
|
-
|
|
171
|
+
logger_1.Logger.info(chalk_1.default.blue("š Running Detective Engine..."));
|
|
140
172
|
const detective = new detective_1.Detective(SOURCE_DIR);
|
|
141
173
|
await detective.scan();
|
|
142
174
|
detective.report();
|
|
143
175
|
// 1. Get the Diff
|
|
144
176
|
const diff = await git.diff(["--staged"]);
|
|
145
177
|
if (!diff) {
|
|
146
|
-
|
|
178
|
+
logger_1.Logger.info(chalk_1.default.green("\nā
No staged changes to review. (Did you 'git add'?)"));
|
|
147
179
|
return;
|
|
148
180
|
}
|
|
149
181
|
// 2. Load the Brain
|
|
150
182
|
if (!fs.existsSync(STORAGE_DIR)) {
|
|
151
|
-
|
|
183
|
+
logger_1.Logger.error(chalk_1.default.red("ā Local Brain not found. Run 'pnpm run index:init' first."));
|
|
152
184
|
return;
|
|
153
185
|
}
|
|
154
186
|
const storageContext = await (0, llamaindex_1.storageContextFromDefaults)({ persistDir: STORAGE_DIR });
|
|
@@ -160,13 +192,13 @@ async function main() {
|
|
|
160
192
|
.join(" ").slice(0, 300) || "general context";
|
|
161
193
|
const contextNodes = await retriever.retrieve(keywords);
|
|
162
194
|
const relatedContext = contextNodes.map(n => n.node.getContent(llamaindex_1.MetadataMode.NONE).slice(0, 1500)).join("\n");
|
|
163
|
-
|
|
195
|
+
logger_1.Logger.info(chalk_1.default.yellow(`\nš§ Found ${contextNodes.length} context chunk.`));
|
|
164
196
|
// 4. Chunk the Diff
|
|
165
197
|
const fileChunks = chunkDiffByFile(diff);
|
|
166
198
|
const totalTokens = estimateTokens(diff);
|
|
167
|
-
|
|
168
|
-
// 5. Structured JSON Prompt
|
|
169
|
-
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.
|
|
170
202
|
|
|
171
203
|
Analyze the Git Diff and return a JSON response with this EXACT structure:
|
|
172
204
|
{
|
|
@@ -176,7 +208,8 @@ Analyze the Git Diff and return a JSON response with this EXACT structure:
|
|
|
176
208
|
"title": "Short issue title",
|
|
177
209
|
"description": "Detailed description of the problem",
|
|
178
210
|
"line": 42,
|
|
179
|
-
"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
|
|
180
213
|
}
|
|
181
214
|
]
|
|
182
215
|
}
|
|
@@ -187,29 +220,39 @@ SEVERITY GUIDE:
|
|
|
187
220
|
- P2_MEDIUM: Code quality, missing types, cleanup needed
|
|
188
221
|
- LGTM: No issues found (return empty issues array)
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
-
|
|
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": []}
|
|
194
237
|
|
|
195
238
|
Context: ${relatedContext.slice(0, 800)}`;
|
|
196
239
|
const findings = [];
|
|
197
240
|
let chunkIndex = 0;
|
|
198
241
|
let skippedCount = 0;
|
|
199
|
-
|
|
242
|
+
logger_1.Logger.info(chalk_1.default.blue("\nāļø Sending to Judge...\n"));
|
|
200
243
|
for (const chunk of fileChunks) {
|
|
201
244
|
chunkIndex++;
|
|
202
245
|
const fileName = chunk.match(/diff --git a\/(.+?) /)?.[1] || `Chunk ${chunkIndex}`;
|
|
203
246
|
// Skip excluded files
|
|
204
247
|
if (shouldSkipFile(fileName)) {
|
|
205
|
-
|
|
248
|
+
logger_1.Logger.info(chalk_1.default.dim(` āļø Skipping ${fileName} (excluded)`));
|
|
206
249
|
skippedCount++;
|
|
207
250
|
continue;
|
|
208
251
|
}
|
|
209
252
|
const chunkTokens = estimateTokens(chunk) + estimateTokens(systemPrompt);
|
|
210
253
|
// Skip huge files
|
|
211
254
|
if (chunkTokens > MAX_TOKENS_PER_REQUEST) {
|
|
212
|
-
|
|
255
|
+
logger_1.Logger.info(chalk_1.default.yellow(` ā ļø Skipping ${fileName} (too large: ~${chunkTokens} tokens)`));
|
|
213
256
|
findings.push({
|
|
214
257
|
file: fileName,
|
|
215
258
|
severity: config_1.Config.DEFAULT_SEVERITY,
|
|
@@ -221,7 +264,7 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
221
264
|
});
|
|
222
265
|
continue;
|
|
223
266
|
}
|
|
224
|
-
|
|
267
|
+
logger_1.Logger.progress(chalk_1.default.dim(` š ${fileName}...`));
|
|
225
268
|
try {
|
|
226
269
|
const { text } = await (0, ai_1.generateText)({
|
|
227
270
|
model: groq(MODEL_NAME),
|
|
@@ -238,10 +281,10 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
238
281
|
const emoji = response.severity === 'LGTM' ? 'ā
' :
|
|
239
282
|
response.severity === 'P0_CRITICAL' ? 'š' :
|
|
240
283
|
response.severity === 'P1_HIGH' ? 'ā ļø' : 'š';
|
|
241
|
-
|
|
284
|
+
logger_1.Logger.info(` ${emoji}`);
|
|
242
285
|
}
|
|
243
286
|
catch (error) {
|
|
244
|
-
|
|
287
|
+
logger_1.Logger.info(chalk_1.default.red(` ā (${error.message.slice(0, 50)})`));
|
|
245
288
|
findings.push({
|
|
246
289
|
file: fileName,
|
|
247
290
|
severity: config_1.Config.DEFAULT_SEVERITY,
|
|
@@ -257,22 +300,29 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
257
300
|
await sleep(DELAY_BETWEEN_CHUNKS_MS);
|
|
258
301
|
}
|
|
259
302
|
}
|
|
260
|
-
// 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
|
|
261
311
|
const metrics = {
|
|
262
312
|
filesScanned: fileChunks.length - skippedCount,
|
|
263
313
|
totalTokens,
|
|
264
|
-
violations:
|
|
265
|
-
critical:
|
|
266
|
-
high:
|
|
267
|
-
medium:
|
|
268
|
-
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,
|
|
269
319
|
};
|
|
270
320
|
const report = {
|
|
271
321
|
date: new Date().toISOString().split('T')[0],
|
|
272
322
|
reviewer: 'Detective Engine & Judge',
|
|
273
323
|
status: (0, dashboard_1.determineStatus)(metrics),
|
|
274
324
|
metrics,
|
|
275
|
-
findings,
|
|
325
|
+
findings: filteredFindings,
|
|
276
326
|
recommendedAction: (0, dashboard_1.generateRecommendation)(metrics)
|
|
277
327
|
};
|
|
278
328
|
// 7. Render and Save Dashboard
|
|
@@ -280,18 +330,63 @@ Context: ${relatedContext.slice(0, 800)}`;
|
|
|
280
330
|
// Ensure .jstar directory exists
|
|
281
331
|
fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true });
|
|
282
332
|
fs.writeFileSync(OUTPUT_FILE, dashboard);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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)));
|
|
286
343
|
// Print summary to console
|
|
287
344
|
const statusEmoji = report.status === 'APPROVED' ? 'š¢' :
|
|
288
345
|
report.status === 'NEEDS_REVIEW' ? 'š”' : 'š“';
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
391
|
}
|
|
297
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
|
+
}
|