@jojonax/codex-copilot 1.1.0 → 1.2.1

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.1",
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
 
@@ -300,7 +300,29 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
300
300
  });
301
301
 
302
302
  log.info(`Waiting for review feedback... (timeout: ${waitTimeout}s)`);
303
- const gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
303
+
304
+ // When resuming from waiting_review, the review may have already arrived
305
+ // before the restart. Check for existing reviews first before polling.
306
+ const isResuming = (state.current_task === task.id && state.phase === 'review'
307
+ && state.phase_step === 'waiting_review' && round === startRound);
308
+
309
+ let gotReview;
310
+ if (isResuming) {
311
+ const existingReviews = github.getReviews(projectDir, prInfo.number);
312
+ const existingComments = github.getIssueComments(projectDir, prInfo.number);
313
+ const hasReview = existingReviews.some(r => r.state !== 'PENDING');
314
+ const hasBotComment = existingComments.some(c =>
315
+ c.user?.type === 'Bot' || c.user?.login?.includes('bot')
316
+ );
317
+ if (hasReview || hasBotComment) {
318
+ log.info('Found existing review — processing immediately');
319
+ gotReview = true;
320
+ } else {
321
+ gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
322
+ }
323
+ } else {
324
+ gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
325
+ }
304
326
 
305
327
  if (!gotReview) {
306
328
  log.warn('Review wait timed out');
@@ -328,13 +350,47 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
328
350
  return;
329
351
  }
330
352
 
331
- // Collect review feedback
353
+ // Collect review feedback (raw — classification done by AI)
332
354
  const feedback = github.collectReviewFeedback(projectDir, prInfo.number);
333
355
  if (!feedback) {
334
- log.info('No specific change requests found, proceeding');
356
+ log.info('No review feedback found proceeding');
335
357
  return;
336
358
  }
337
359
 
360
+ // Use AI to classify the review feedback
361
+ log.info('Classifying review feedback via AI...');
362
+ const classification = await provider.classifyReview(providerId, feedback, projectDir);
363
+
364
+ if (classification === 'pass') {
365
+ log.info('AI determined no actionable issues — proceeding ✅');
366
+ return;
367
+ }
368
+
369
+ if (classification === null) {
370
+ // AI classification failed — fall back to structural GitHub API signals
371
+ log.dim('AI classification unavailable, using structural fallback');
372
+ const inlineComments = github.getReviewComments(projectDir, prInfo.number);
373
+ const hasInlineComments = inlineComments && inlineComments.length > 0;
374
+
375
+ if (reviewState === 'CHANGES_REQUESTED') {
376
+ // Explicit change request — always treat as needing fixes
377
+ log.info('Review state: CHANGES_REQUESTED — entering fix phase');
378
+ } else if (reviewState === 'COMMENTED' && !hasInlineComments) {
379
+ // General comment with no code-level feedback — treat as passing
380
+ log.info('COMMENTED with no inline code comments — treating as passed ✅');
381
+ return;
382
+ } else if (!hasInlineComments) {
383
+ // Any other state (PENDING, null, etc.) with no inline comments — treat as passing
384
+ log.info('No inline code comments found — treating as passed ✅');
385
+ return;
386
+ } else {
387
+ // Has inline comments — treat as needing fixes
388
+ log.info(`Found ${inlineComments.length} inline code comment(s) — entering fix phase`);
389
+ }
390
+ }
391
+
392
+ // AI says FIX, or structural fallback indicates issues
393
+
338
394
  log.blank();
339
395
  log.warn(`Received review feedback (round ${round}/${maxRounds})`);
340
396
 
@@ -360,15 +416,18 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
360
416
  review_round: round,
361
417
  });
362
418
 
363
- // Push fix
419
+ // Push fix — only if there are actual changes
364
420
  if (!git.isClean(projectDir)) {
365
421
  git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})`);
422
+ git.pushBranch(projectDir, task.branch);
423
+ log.info('Fix pushed, waiting for next review round...');
424
+ // Brief wait for review bot to react
425
+ await sleep(10000);
426
+ } else {
427
+ // No actual changes were made by the fix — the AI determined nothing needed fixing
428
+ log.info('No changes needed after review — proceeding to merge ✅');
429
+ return;
366
430
  }
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
431
  }
373
432
  }
374
433
 
@@ -432,11 +491,7 @@ ${feedback}
432
491
  async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
433
492
  log.step('Phase 4/4: Merge PR');
434
493
 
435
- const doMerge = await confirm(`Merge PR #${prInfo.number}?`);
436
- if (!doMerge) {
437
- log.warn('User skipped merge');
438
- return;
439
- }
494
+ log.info(`Auto-merging PR #${prInfo.number}...`);
440
495
 
441
496
  try {
442
497
  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
  };