@fermindi/pwn-cli 0.9.3 → 0.9.5
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/package.json +1 -1
- package/src/services/batch-runner.js +83 -4
package/package.json
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* Completed files are cleaned up at the end; failed are kept for review.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { spawn } from 'child_process';
|
|
12
|
+
import { spawn, exec } from 'child_process';
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, createWriteStream, appendFileSync, unlinkSync, readdirSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
|
+
import { promisify } from 'util';
|
|
15
16
|
import ora from 'ora';
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
16
19
|
import chalk from 'chalk';
|
|
17
20
|
import {
|
|
18
21
|
parsePrdTasks,
|
|
@@ -31,7 +34,7 @@ import {
|
|
|
31
34
|
} from './batch-service.js';
|
|
32
35
|
|
|
33
36
|
// --- Constants ---
|
|
34
|
-
const RUNNER_VERSION = '2.
|
|
37
|
+
const RUNNER_VERSION = '2.3';
|
|
35
38
|
const DEFAULT_TIMEOUT_MS = 900_000; // 15 minutes fallback
|
|
36
39
|
const MIN_TIMEOUT_MS = 300_000; // 5 minutes minimum (claude init ~30-40s + real work)
|
|
37
40
|
|
|
@@ -388,7 +391,84 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
388
391
|
}
|
|
389
392
|
}
|
|
390
393
|
|
|
391
|
-
// ---
|
|
394
|
+
// --- Pre-validation: check existing feat branch before reset ---
|
|
395
|
+
let storyDone = false;
|
|
396
|
+
const taskBranchName = `feat/${task.id}`;
|
|
397
|
+
let branchExists = false;
|
|
398
|
+
try {
|
|
399
|
+
await execAsync(`git rev-parse --verify ${taskBranchName}`, { cwd });
|
|
400
|
+
branchExists = true;
|
|
401
|
+
} catch {}
|
|
402
|
+
|
|
403
|
+
if (branchExists) {
|
|
404
|
+
try {
|
|
405
|
+
// Checkout WITHOUT reset to inspect existing work
|
|
406
|
+
await checkoutBranch(taskBranchName, cwd, { stash: true });
|
|
407
|
+
const { stdout: logCount } = await execAsync(`git rev-list --count ${batchBranch}..HEAD`, { cwd });
|
|
408
|
+
const commits = parseInt(logCount.trim(), 10);
|
|
409
|
+
|
|
410
|
+
if (commits > 0) {
|
|
411
|
+
console.log(chalk.blue(` Pre-validation: found ${commits} commit(s) on ${taskBranchName}, running quality gates...`));
|
|
412
|
+
const preGates = await runGatesWithStatus(cwd);
|
|
413
|
+
|
|
414
|
+
if (preGates.success) {
|
|
415
|
+
console.log(chalk.green(` Pre-validation PASSED — skipping execution`));
|
|
416
|
+
markStoryDone(task.id, cwd);
|
|
417
|
+
appendProgress(progressPath, task.id, 'Pre-validation: existing code passed quality gates');
|
|
418
|
+
|
|
419
|
+
storyDone = true;
|
|
420
|
+
storiesCompleted++;
|
|
421
|
+
noProgressCount = 0;
|
|
422
|
+
|
|
423
|
+
if (taskFile) {
|
|
424
|
+
taskFile.status = 'completed';
|
|
425
|
+
taskFile.completed_at = new Date().toISOString();
|
|
426
|
+
saveTaskFile(taskFile, cwd);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
updateBatchState({
|
|
430
|
+
completed: [task.id],
|
|
431
|
+
current_task: null,
|
|
432
|
+
last_completed_at: new Date().toISOString()
|
|
433
|
+
}, cwd);
|
|
434
|
+
|
|
435
|
+
// Merge into batch branch
|
|
436
|
+
try {
|
|
437
|
+
await checkoutBranch(batchBranch, cwd, { stash: true });
|
|
438
|
+
await mergeBranch(taskBranchName, cwd);
|
|
439
|
+
mergedCount++;
|
|
440
|
+
mergedBranches.push(taskBranchName);
|
|
441
|
+
console.log(chalk.green(` Merged ${taskBranchName} → ${batchBranch}`));
|
|
442
|
+
} catch (err) {
|
|
443
|
+
console.log(chalk.red(` Merge failed: ${err.message}`));
|
|
444
|
+
console.log(chalk.yellow(` Branch ${taskBranchName} available for manual merge`));
|
|
445
|
+
unmergedBranches.push(taskBranchName);
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
console.log(chalk.yellow(` Pre-validation FAILED — will re-execute`));
|
|
449
|
+
// Return to batch branch so createTaskBranch resets properly
|
|
450
|
+
await checkoutBranch(batchBranch, cwd, { force: true, stash: true });
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
// No commits — return to batch branch for normal flow
|
|
454
|
+
await checkoutBranch(batchBranch, cwd, { force: true, stash: true });
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
// Checkout failed — proceed normally
|
|
458
|
+
try { await checkoutBranch(batchBranch, cwd, { force: true, stash: true }); } catch {}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (storyDone) {
|
|
463
|
+
branchesCreated.push(taskBranchName);
|
|
464
|
+
try {
|
|
465
|
+
await checkoutBranch(batchBranch, cwd, { force: true, stash: true });
|
|
466
|
+
console.log(chalk.dim(` Returned to branch: ${batchBranch}`));
|
|
467
|
+
} catch {}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// --- Branch isolation: create/checkout task branch (resets existing) ---
|
|
392
472
|
let taskBranch = null;
|
|
393
473
|
try {
|
|
394
474
|
taskBranch = await createTaskBranch(task.id, cwd);
|
|
@@ -411,7 +491,6 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
411
491
|
|
|
412
492
|
let retry = 0;
|
|
413
493
|
let rateLimitAttempts = 0;
|
|
414
|
-
let storyDone = false;
|
|
415
494
|
let errorContext = '';
|
|
416
495
|
|
|
417
496
|
while (retry <= MAX_RETRIES && !storyDone) {
|