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.
@@ -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 google = (0, google_1.createGoogleGenerativeAI)({ apiKey: process.env.GOOGLE_API_KEY });
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
- console.log(chalk_1.default.blue("šŸ•µļø J-Star Reviewer: Analyzing your changes...\n"));
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 (!process.env.GOOGLE_API_KEY || !process.env.GROQ_API_KEY) {
130
- console.error(chalk_1.default.red("āŒ Missing API Keys!"));
131
- console.log(chalk_1.default.yellow("\nPlease ensure you have a .env.local file with:"));
132
- console.log(chalk_1.default.white("- GOOGLE_API_KEY"));
133
- console.log(chalk_1.default.white("- GROQ_API_KEY"));
134
- console.log(chalk_1.default.white("\nCheck .env.example for a template.\n"));
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
- console.log(chalk_1.default.blue("šŸ”Ž Running Detective Engine..."));
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
- console.log(chalk_1.default.green("\nāœ… No staged changes to review. (Did you 'git add'?)"));
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
- console.error(chalk_1.default.red("āŒ Local Brain not found. Run 'pnpm run index:init' first."));
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
- console.log(chalk_1.default.yellow(`\n🧠 Found ${contextNodes.length} context chunk.`));
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
- console.log(chalk_1.default.dim(` Total diff: ~${totalTokens} tokens across ${fileChunks.length} files.`));
167
- // 5. Structured JSON Prompt
168
- const systemPrompt = `You are J-Star, a Senior Code Reviewer. Be direct and professional.
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
- IMPORTANT:
190
- - Return ONLY valid JSON, no markdown or explanation
191
- - Each issue MUST have a fixPrompt that explains exactly how to fix it
192
- - If the file is clean, return {"severity": "LGTM", "issues": []}
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
- console.log(chalk_1.default.blue("\nāš–ļø Sending to Judge...\n"));
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
- console.log(chalk_1.default.dim(` ā­ļø Skipping ${fileName} (excluded)`));
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
- console.log(chalk_1.default.yellow(` āš ļø Skipping ${fileName} (too large: ~${chunkTokens} tokens)`));
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
- process.stdout.write(chalk_1.default.dim(` šŸ“„ ${fileName}...`));
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
- console.log(` ${emoji}`);
284
+ logger_1.Logger.info(` ${emoji}`);
241
285
  }
242
286
  catch (error) {
243
- console.log(chalk_1.default.red(` āŒ (${error.message.slice(0, 50)})`));
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. Build Dashboard Report
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: findings.reduce((sum, f) => sum + f.issues.length, 0),
264
- critical: findings.filter(f => f.severity === 'P0_CRITICAL').length,
265
- high: findings.filter(f => f.severity === 'P1_HIGH').length,
266
- medium: findings.filter(f => f.severity === 'P2_MEDIUM').length,
267
- lgtm: findings.filter(f => f.severity === 'LGTM').length,
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
- console.log("\n" + chalk_1.default.bold.green("šŸ“Š DASHBOARD GENERATED"));
283
- console.log(chalk_1.default.dim(` Saved to: ${OUTPUT_FILE}`));
284
- console.log("\n" + chalk_1.default.bold.white("─".repeat(50)));
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
- console.log(`\n${statusEmoji} Status: ${report.status.replace('_', ' ')}`);
289
- console.log(` šŸ›‘ Critical: ${metrics.critical}`);
290
- console.log(` āš ļø High: ${metrics.high}`);
291
- console.log(` šŸ“ Medium: ${metrics.medium}`);
292
- console.log(` āœ… LGTM: ${metrics.lgtm}`);
293
- console.log(`\nšŸ’” ${report.recommendedAction}`);
294
- console.log(chalk_1.default.dim(`\nšŸ“„ Full report: ${OUTPUT_FILE}`));
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
+ }