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.
@@ -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
- 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"));
129
161
  // 0. Environment Validation
130
162
  if (!geminiKey || !process.env.GROQ_API_KEY) {
131
- console.error(chalk_1.default.red("āŒ Missing API Keys!"));
132
- console.log(chalk_1.default.yellow("\nPlease ensure you have a .env.local file with:"));
133
- console.log(chalk_1.default.white("- GEMINI_API_KEY (or GOOGLE_API_KEY)"));
134
- console.log(chalk_1.default.white("- GROQ_API_KEY"));
135
- console.log(chalk_1.default.white("\nCheck .env.example for a template.\n"));
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
- console.log(chalk_1.default.blue("šŸ”Ž Running Detective Engine..."));
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
- 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'?)"));
147
179
  return;
148
180
  }
149
181
  // 2. Load the Brain
150
182
  if (!fs.existsSync(STORAGE_DIR)) {
151
- 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."));
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
- 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.`));
164
196
  // 4. Chunk the Diff
165
197
  const fileChunks = chunkDiffByFile(diff);
166
198
  const totalTokens = estimateTokens(diff);
167
- console.log(chalk_1.default.dim(` Total diff: ~${totalTokens} tokens across ${fileChunks.length} files.`));
168
- // 5. Structured JSON Prompt
169
- 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.
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
- IMPORTANT:
191
- - Return ONLY valid JSON, no markdown or explanation
192
- - Each issue MUST have a fixPrompt that explains exactly how to fix it
193
- - 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": []}
194
237
 
195
238
  Context: ${relatedContext.slice(0, 800)}`;
196
239
  const findings = [];
197
240
  let chunkIndex = 0;
198
241
  let skippedCount = 0;
199
- 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"));
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
- console.log(chalk_1.default.dim(` ā­ļø Skipping ${fileName} (excluded)`));
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
- 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)`));
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
- process.stdout.write(chalk_1.default.dim(` šŸ“„ ${fileName}...`));
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
- console.log(` ${emoji}`);
284
+ logger_1.Logger.info(` ${emoji}`);
242
285
  }
243
286
  catch (error) {
244
- 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)})`));
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. 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
261
311
  const metrics = {
262
312
  filesScanned: fileChunks.length - skippedCount,
263
313
  totalTokens,
264
- violations: findings.reduce((sum, f) => sum + f.issues.length, 0),
265
- critical: findings.filter(f => f.severity === 'P0_CRITICAL').length,
266
- high: findings.filter(f => f.severity === 'P1_HIGH').length,
267
- medium: findings.filter(f => f.severity === 'P2_MEDIUM').length,
268
- 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,
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
- console.log("\n" + chalk_1.default.bold.green("šŸ“Š DASHBOARD GENERATED"));
284
- console.log(chalk_1.default.dim(` Saved to: ${OUTPUT_FILE}`));
285
- 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)));
286
343
  // Print summary to console
287
344
  const statusEmoji = report.status === 'APPROVED' ? '🟢' :
288
345
  report.status === 'NEEDS_REVIEW' ? '🟔' : 'šŸ”“';
289
- console.log(`\n${statusEmoji} Status: ${report.status.replace('_', ' ')}`);
290
- console.log(` šŸ›‘ Critical: ${metrics.critical}`);
291
- console.log(` āš ļø High: ${metrics.high}`);
292
- console.log(` šŸ“ Medium: ${metrics.medium}`);
293
- console.log(` āœ… LGTM: ${metrics.lgtm}`);
294
- console.log(`\nšŸ’” ${report.recommendedAction}`);
295
- 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
+ }
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
+ }