@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 +5 -0
- package/package.json +1 -1
- package/src/commands/run.js +47 -14
- package/src/utils/github.js +6 -5
- package/src/utils/provider.js +85 -2
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
package/src/commands/run.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/utils/github.js
CHANGED
|
@@ -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
|
|
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
|
|
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`;
|
package/src/utils/provider.js
CHANGED
|
@@ -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
|
};
|