@jojonax/codex-copilot 1.1.0 → 1.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/cli.js CHANGED
@@ -79,6 +79,11 @@ async function main() {
79
79
  console.log(' 2. codex-copilot init (auto-detect PRD and decompose tasks)');
80
80
  console.log(' 3. codex-copilot run (start automated dev loop)');
81
81
  console.log('');
82
+ console.log(' Update:');
83
+ console.log(' npm install -g @jojonax/codex-copilot@latest');
84
+ console.log('');
85
+ console.log(' \x1b[36mⓘ This is a help page. Exit and run the commands above directly in your terminal.\x1b[0m');
86
+ console.log('');
82
87
  break;
83
88
  }
84
89
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jojonax/codex-copilot",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "PRD-driven automated development orchestrator for CodeX / Cursor",
5
5
  "bin": {
6
6
  "codex-copilot": "./bin/cli.js"
@@ -10,7 +10,7 @@ import { resolve } from 'path';
10
10
  import { log, progressBar } from '../utils/logger.js';
11
11
  import { git } from '../utils/git.js';
12
12
  import { github } from '../utils/github.js';
13
- import { ask, confirm, closePrompt } from '../utils/prompt.js';
13
+ import { ask, closePrompt } from '../utils/prompt.js';
14
14
  import { createCheckpoint } from '../utils/checkpoint.js';
15
15
  import { provider } from '../utils/provider.js';
16
16
 
@@ -328,13 +328,47 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
328
328
  return;
329
329
  }
330
330
 
331
- // Collect review feedback
331
+ // Collect review feedback (raw — classification done by AI)
332
332
  const feedback = github.collectReviewFeedback(projectDir, prInfo.number);
333
333
  if (!feedback) {
334
- log.info('No specific change requests found, proceeding');
334
+ log.info('No review feedback found proceeding');
335
335
  return;
336
336
  }
337
337
 
338
+ // Use AI to classify the review feedback
339
+ log.info('Classifying review feedback via AI...');
340
+ const classification = await provider.classifyReview(providerId, feedback, projectDir);
341
+
342
+ if (classification === 'pass') {
343
+ log.info('AI determined no actionable issues — proceeding ✅');
344
+ return;
345
+ }
346
+
347
+ if (classification === null) {
348
+ // AI classification failed — fall back to structural GitHub API signals
349
+ log.dim('AI classification unavailable, using structural fallback');
350
+ const inlineComments = github.getReviewComments(projectDir, prInfo.number);
351
+ const hasInlineComments = inlineComments && inlineComments.length > 0;
352
+
353
+ if (reviewState === 'CHANGES_REQUESTED') {
354
+ // Explicit change request — always treat as needing fixes
355
+ log.info('Review state: CHANGES_REQUESTED — entering fix phase');
356
+ } else if (reviewState === 'COMMENTED' && !hasInlineComments) {
357
+ // General comment with no code-level feedback — treat as passing
358
+ log.info('COMMENTED with no inline code comments — treating as passed ✅');
359
+ return;
360
+ } else if (!hasInlineComments) {
361
+ // Any other state (PENDING, null, etc.) with no inline comments — treat as passing
362
+ log.info('No inline code comments found — treating as passed ✅');
363
+ return;
364
+ } else {
365
+ // Has inline comments — treat as needing fixes
366
+ log.info(`Found ${inlineComments.length} inline code comment(s) — entering fix phase`);
367
+ }
368
+ }
369
+
370
+ // AI says FIX, or structural fallback indicates issues
371
+
338
372
  log.blank();
339
373
  log.warn(`Received review feedback (round ${round}/${maxRounds})`);
340
374
 
@@ -360,15 +394,18 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
360
394
  review_round: round,
361
395
  });
362
396
 
363
- // Push fix
397
+ // Push fix — only if there are actual changes
364
398
  if (!git.isClean(projectDir)) {
365
399
  git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})`);
400
+ git.pushBranch(projectDir, task.branch);
401
+ log.info('Fix pushed, waiting for next review round...');
402
+ // Brief wait for review bot to react
403
+ await sleep(10000);
404
+ } else {
405
+ // No actual changes were made by the fix — the AI determined nothing needed fixing
406
+ log.info('No changes needed after review — proceeding to merge ✅');
407
+ return;
366
408
  }
367
- git.pushBranch(projectDir, task.branch);
368
- log.info('Fix pushed, waiting for next review round...');
369
-
370
- // Brief wait for review bot to react
371
- await sleep(10000);
372
409
  }
373
410
  }
374
411
 
@@ -432,11 +469,7 @@ ${feedback}
432
469
  async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
433
470
  log.step('Phase 4/4: Merge PR');
434
471
 
435
- const doMerge = await confirm(`Merge PR #${prInfo.number}?`);
436
- if (!doMerge) {
437
- log.warn('User skipped merge');
438
- return;
439
- }
472
+ log.info(`Auto-merging PR #${prInfo.number}...`);
440
473
 
441
474
  try {
442
475
  github.mergePR(projectDir, prInfo.number);
@@ -260,7 +260,8 @@ function validatePRNumber(prNumber) {
260
260
  }
261
261
 
262
262
  /**
263
- * Collect all review feedback as structured text
263
+ * Collect all review feedback as structured text.
264
+ * Returns raw feedback — classification is done by AI provider.
264
265
  */
265
266
  export function collectReviewFeedback(cwd, prNumber) {
266
267
  const reviews = getReviews(cwd, prNumber);
@@ -269,19 +270,19 @@ export function collectReviewFeedback(cwd, prNumber) {
269
270
 
270
271
  let feedback = '';
271
272
 
272
- // Review summary
273
+ // Review body text (skip APPROVED — already handled by state check)
273
274
  for (const r of reviews) {
274
- if (r.body && r.body.trim()) {
275
+ if (r.body && r.body.trim() && r.state !== 'APPROVED') {
275
276
  feedback += `### Review (${r.state})\n${r.body}\n\n`;
276
277
  }
277
278
  }
278
279
 
279
- // Inline comments
280
+ // Inline code comments (comments on specific diff lines)
280
281
  for (const c of comments) {
281
282
  feedback += `### ${c.path}:L${c.line || c.original_line}\n${c.body}\n\n`;
282
283
  }
283
284
 
284
- // Bot comments (Gemini Code Assist, etc.)
285
+ // Bot comments
285
286
  for (const c of issueComments) {
286
287
  if (c.user?.type === 'Bot' || c.user?.login?.includes('bot')) {
287
288
  feedback += `### Bot Review (${c.user.login})\n${c.body}\n\n`;
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import { execSync } from 'child_process';
13
- import { readFileSync } from 'fs';
13
+ import { readFileSync, writeFileSync } from 'fs';
14
+ import { resolve } from 'path';
14
15
  import { log } from './logger.js';
15
16
  import { ask } from './prompt.js';
16
17
  import { automator } from './automator.js';
@@ -326,7 +327,89 @@ function shellEscape(str) {
326
327
  return `'${str.replace(/'/g, "'\\''")}'`;
327
328
  }
328
329
 
330
+ /**
331
+ * Lightweight AI query — capture output rather than inherit stdio.
332
+ * Only works for CLI providers. Returns null for IDE providers.
333
+ *
334
+ * @param {string} providerId - Provider ID
335
+ * @param {string} question - The question/prompt text
336
+ * @param {string} cwd - Working directory
337
+ * @returns {Promise<string|null>} AI response text, or null if unsupported/failed
338
+ */
339
+ export async function queryAI(providerId, question, cwd) {
340
+ const prov = PROVIDERS[providerId];
341
+ if (!prov || prov.type !== 'cli') return null;
342
+
343
+ // Verify CLI is available
344
+ if (prov.detect) {
345
+ try {
346
+ const cmd = process.platform === 'win32'
347
+ ? `where ${prov.detect}`
348
+ : `which ${prov.detect}`;
349
+ execSync(cmd, { stdio: 'pipe' });
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ // Write question to temp file
356
+ const tmpPath = resolve(cwd, '.codex-copilot/_query_prompt.md');
357
+ writeFileSync(tmpPath, question);
358
+
359
+ const command = prov.buildCommand(tmpPath, cwd);
360
+
361
+ try {
362
+ const output = execSync(command, {
363
+ cwd,
364
+ encoding: 'utf-8',
365
+ stdio: ['pipe', 'pipe', 'pipe'],
366
+ timeout: 60000, // 60s timeout for classification
367
+ });
368
+ return output.trim();
369
+ } catch (err) {
370
+ log.dim(`AI query failed: ${(err.message || '').substring(0, 80)}`);
371
+ return null;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Use AI to classify code review feedback.
377
+ *
378
+ * Returns 'pass' if AI determines no actionable issues,
379
+ * 'fix' if there are issues to address, or null if classification failed.
380
+ *
381
+ * @param {string} providerId - Provider ID
382
+ * @param {string} feedbackText - The collected review feedback
383
+ * @param {string} cwd - Working directory
384
+ * @returns {Promise<'pass'|'fix'|null>}
385
+ */
386
+ export async function classifyReview(providerId, feedbackText, cwd) {
387
+ const classificationPrompt = `You are a code review classifier. Your ONLY job is to determine if the following code review feedback contains actionable issues that require code changes.
388
+
389
+ ## Code Review Feedback
390
+ ${feedbackText}
391
+
392
+ ## Instructions
393
+ - If the review says the code looks good, has no issues, is purely informational, or explicitly states no changes are needed: output exactly PASS
394
+ - If the review requests specific code changes, points out bugs, security issues, or improvements that need action: output exactly FIX
395
+
396
+ IMPORTANT: Output ONLY a single word on the first line: either PASS or FIX. No other text.`;
397
+
398
+ const response = await queryAI(providerId, classificationPrompt, cwd);
399
+ if (!response) return null;
400
+
401
+ // Parse the first meaningful line
402
+ const firstLine = response.split('\n').map(l => l.trim()).find(l => l.length > 0);
403
+ if (!firstLine) return null;
404
+
405
+ const upper = firstLine.toUpperCase();
406
+ if (upper.includes('PASS')) return 'pass';
407
+ if (upper.includes('FIX')) return 'fix';
408
+
409
+ return null; // Ambiguous — caller decides fallback
410
+ }
411
+
329
412
  export const provider = {
330
413
  getProvider, getAllProviderIds, detectAvailable,
331
- buildProviderChoices, executePrompt,
414
+ buildProviderChoices, executePrompt, queryAI, classifyReview,
332
415
  };