@fermindi/pwn-cli 0.8.0 → 0.9.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/package.json
CHANGED
|
@@ -22,7 +22,12 @@ import {
|
|
|
22
22
|
loadConfig,
|
|
23
23
|
commitTask,
|
|
24
24
|
updateBatchState,
|
|
25
|
-
clearBatchState
|
|
25
|
+
clearBatchState,
|
|
26
|
+
getCurrentBranch,
|
|
27
|
+
createTaskBranch,
|
|
28
|
+
checkoutBranch,
|
|
29
|
+
createBatchBranch,
|
|
30
|
+
mergeBranch
|
|
26
31
|
} from './batch-service.js';
|
|
27
32
|
|
|
28
33
|
// --- Constants ---
|
|
@@ -274,10 +279,21 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
274
279
|
|
|
275
280
|
// NO custom SIGINT handler — Ctrl+C uses default Node.js behavior (kills process group)
|
|
276
281
|
|
|
282
|
+
// --- Save original branch for isolation ---
|
|
283
|
+
const originalBranch = await getCurrentBranch(cwd);
|
|
284
|
+
|
|
285
|
+
// --- Create batch branch ---
|
|
286
|
+
const batchBranch = await createBatchBranch(cwd);
|
|
287
|
+
console.log(chalk.blue(` Batch branch: ${chalk.bold(batchBranch)}`));
|
|
288
|
+
|
|
277
289
|
// --- Main loop ---
|
|
278
290
|
let iteration = 0;
|
|
279
291
|
let noProgressCount = 0;
|
|
280
292
|
let storiesCompleted = 0;
|
|
293
|
+
let mergedCount = 0;
|
|
294
|
+
const branchesCreated = [];
|
|
295
|
+
const mergedBranches = [];
|
|
296
|
+
const unmergedBranches = [];
|
|
281
297
|
const batchStart = Date.now();
|
|
282
298
|
|
|
283
299
|
updateBatchState({
|
|
@@ -372,6 +388,23 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
372
388
|
}
|
|
373
389
|
}
|
|
374
390
|
|
|
391
|
+
// --- Branch isolation: create/checkout task branch ---
|
|
392
|
+
let taskBranch = null;
|
|
393
|
+
try {
|
|
394
|
+
taskBranch = await createTaskBranch(task.id, cwd);
|
|
395
|
+
branchesCreated.push(taskBranch);
|
|
396
|
+
console.log(chalk.blue(` Branch: ${chalk.bold(taskBranch)}`));
|
|
397
|
+
|
|
398
|
+
// Track branch in task file
|
|
399
|
+
if (taskFile) {
|
|
400
|
+
taskFile.branch = taskBranch;
|
|
401
|
+
saveTaskFile(taskFile, cwd);
|
|
402
|
+
}
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.log(chalk.red(` Failed to create branch feat/${task.id}: ${err.message}`));
|
|
405
|
+
console.log(chalk.yellow(' Continuing on current branch...'));
|
|
406
|
+
}
|
|
407
|
+
|
|
375
408
|
// --- Phase 2: Execution ---
|
|
376
409
|
const phaseLabel = noPlan ? '' : 'Phase 2';
|
|
377
410
|
console.log(chalk.blue(` ${noPlan ? 'Executing' : 'Phase 2: Executing'} ${task.id}...`));
|
|
@@ -400,8 +433,11 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
400
433
|
// Killed by signal (user did kill or Ctrl+C) — don't retry, exit
|
|
401
434
|
if (result.signal) {
|
|
402
435
|
console.log(chalk.yellow(` Killed by ${result.signal}`));
|
|
436
|
+
if (taskBranch) {
|
|
437
|
+
try { await checkoutBranch(batchBranch, cwd, { force: true }); } catch {}
|
|
438
|
+
}
|
|
403
439
|
clearBatchState(cwd);
|
|
404
|
-
printSummary(cwd, iteration, storiesCompleted, batchStart);
|
|
440
|
+
printSummary(cwd, iteration, storiesCompleted, batchStart, branchesCreated, batchBranch, originalBranch, mergedCount, unmergedBranches);
|
|
405
441
|
return;
|
|
406
442
|
}
|
|
407
443
|
|
|
@@ -504,6 +540,21 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
504
540
|
current_task: null,
|
|
505
541
|
last_completed_at: new Date().toISOString()
|
|
506
542
|
}, cwd);
|
|
543
|
+
|
|
544
|
+
// Merge successful task branch into batch branch
|
|
545
|
+
if (taskBranch) {
|
|
546
|
+
try {
|
|
547
|
+
await checkoutBranch(batchBranch, cwd);
|
|
548
|
+
await mergeBranch(taskBranch, cwd);
|
|
549
|
+
mergedCount++;
|
|
550
|
+
mergedBranches.push(taskBranch);
|
|
551
|
+
console.log(chalk.green(` Merged ${taskBranch} → ${batchBranch}`));
|
|
552
|
+
} catch (err) {
|
|
553
|
+
console.log(chalk.red(` Merge failed: ${err.message}`));
|
|
554
|
+
console.log(chalk.yellow(` Branch ${taskBranch} available for manual merge`));
|
|
555
|
+
unmergedBranches.push(taskBranch);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
507
558
|
} else {
|
|
508
559
|
console.log(chalk.red(` Quality gates FAILED`));
|
|
509
560
|
errorContext = gatesResult.errorOutput;
|
|
@@ -535,6 +586,19 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
535
586
|
noProgressCount++;
|
|
536
587
|
}
|
|
537
588
|
|
|
589
|
+
// --- Return to batch branch (force: discard dirty state from failed tasks) ---
|
|
590
|
+
if (taskBranch) {
|
|
591
|
+
try {
|
|
592
|
+
await checkoutBranch(batchBranch, cwd, { force: true });
|
|
593
|
+
console.log(chalk.dim(` Returned to branch: ${batchBranch}`));
|
|
594
|
+
} catch (err) {
|
|
595
|
+
console.log(chalk.red(` Warning: failed to return to ${batchBranch}: ${err.message}`));
|
|
596
|
+
}
|
|
597
|
+
if (!storyDone) {
|
|
598
|
+
unmergedBranches.push(taskBranch);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
538
602
|
if (noProgressCount >= MAX_NO_PROGRESS) {
|
|
539
603
|
console.log(chalk.red(`\nCIRCUIT BREAKER: ${MAX_NO_PROGRESS} consecutive failures. Stopping.`));
|
|
540
604
|
break;
|
|
@@ -542,7 +606,7 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
542
606
|
}
|
|
543
607
|
|
|
544
608
|
clearBatchState(cwd);
|
|
545
|
-
printSummary(cwd, iteration, storiesCompleted, batchStart);
|
|
609
|
+
printSummary(cwd, iteration, storiesCompleted, batchStart, branchesCreated, batchBranch, originalBranch, mergedCount, unmergedBranches);
|
|
546
610
|
}
|
|
547
611
|
|
|
548
612
|
/**
|
|
@@ -791,7 +855,7 @@ function printHeader(maxIter, phase, total, done, noPlan = false, cwd = process.
|
|
|
791
855
|
console.log(chalk.dim('─'.repeat(40)));
|
|
792
856
|
}
|
|
793
857
|
|
|
794
|
-
function printSummary(cwd, iterations, completed, startTime) {
|
|
858
|
+
function printSummary(cwd, iterations, completed, startTime, branchesCreated = [], batchBranch = null, originalBranch = null, mergedCount = 0, unmergedBranches = []) {
|
|
795
859
|
const stories = parsePrdTasks(cwd);
|
|
796
860
|
const total = stories.length;
|
|
797
861
|
const done = stories.filter(s => s.passes).length;
|
|
@@ -828,6 +892,21 @@ function printSummary(cwd, iterations, completed, startTime) {
|
|
|
828
892
|
if (cleaned > 0 || failedKept > 0) {
|
|
829
893
|
console.log(` Cleanup: ${chalk.green(`${cleaned} completed`)} removed, ${failedKept > 0 ? chalk.red(`${failedKept} failed`) : '0 failed'} kept for review`);
|
|
830
894
|
}
|
|
895
|
+
if (batchBranch) {
|
|
896
|
+
console.log(` Batch branch: ${chalk.cyan(batchBranch)}`);
|
|
897
|
+
console.log(` Merged: ${chalk.green(mergedCount)} tasks merged into batch branch`);
|
|
898
|
+
if (unmergedBranches.length > 0) {
|
|
899
|
+
const unique = [...new Set(unmergedBranches)];
|
|
900
|
+
console.log(` Unmerged: ${chalk.yellow(unique.length)} isolated: ${unique.map(b => chalk.yellow(b)).join(', ')}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (branchesCreated.length > 0) {
|
|
904
|
+
const unique = [...new Set(branchesCreated)];
|
|
905
|
+
console.log(` Branches: ${unique.map(b => chalk.cyan(b)).join(', ')}`);
|
|
906
|
+
}
|
|
907
|
+
if (originalBranch) {
|
|
908
|
+
console.log(` Original branch: ${chalk.green(originalBranch)} (intact)`);
|
|
909
|
+
}
|
|
831
910
|
console.log(chalk.dim('─'.repeat(40)));
|
|
832
911
|
console.log('');
|
|
833
912
|
}
|
|
@@ -13,6 +13,73 @@ import { getState, updateState, hasWorkspace } from '../core/state.js';
|
|
|
13
13
|
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
15
|
|
|
16
|
+
// --- Git Helpers ---
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the current git branch name
|
|
20
|
+
* @param {string} cwd - Working directory
|
|
21
|
+
* @returns {Promise<string>} Current branch name
|
|
22
|
+
*/
|
|
23
|
+
export async function getCurrentBranch(cwd = process.cwd()) {
|
|
24
|
+
const { stdout } = await execAsync('git branch --show-current', { cwd });
|
|
25
|
+
return stdout.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create and checkout a task branch from the current HEAD
|
|
30
|
+
* If branch already exists, just checkout.
|
|
31
|
+
* @param {string} taskId - Task ID (e.g. "SEC-007")
|
|
32
|
+
* @param {string} cwd - Working directory
|
|
33
|
+
* @returns {Promise<string>} Branch name
|
|
34
|
+
*/
|
|
35
|
+
export async function createTaskBranch(taskId, cwd = process.cwd()) {
|
|
36
|
+
const branch = `feat/${taskId}`;
|
|
37
|
+
try {
|
|
38
|
+
await execAsync(`git rev-parse --verify ${branch}`, { cwd });
|
|
39
|
+
// Branch exists (rerun) — checkout and reset to current HEAD
|
|
40
|
+
// so we don't carry stale/dirty state from previous attempt
|
|
41
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd });
|
|
42
|
+
const baseRef = stdout.trim();
|
|
43
|
+
await execAsync(`git checkout ${branch}`, { cwd });
|
|
44
|
+
await execAsync(`git reset --hard ${baseRef}`, { cwd });
|
|
45
|
+
} catch {
|
|
46
|
+
// Branch doesn't exist — create
|
|
47
|
+
await execAsync(`git checkout -b ${branch}`, { cwd });
|
|
48
|
+
}
|
|
49
|
+
return branch;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checkout an existing branch
|
|
54
|
+
* @param {string} branch - Branch name
|
|
55
|
+
* @param {string} cwd - Working directory
|
|
56
|
+
*/
|
|
57
|
+
export async function checkoutBranch(branch, cwd = process.cwd(), { force = false } = {}) {
|
|
58
|
+
const flag = force ? ' --force' : '';
|
|
59
|
+
await execAsync(`git checkout${flag} ${branch}`, { cwd });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a batch branch from the current HEAD
|
|
64
|
+
* @param {string} cwd - Working directory
|
|
65
|
+
* @returns {Promise<string>} Branch name (batch/{timestamp})
|
|
66
|
+
*/
|
|
67
|
+
export async function createBatchBranch(cwd = process.cwd()) {
|
|
68
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
69
|
+
const branch = `batch/${ts}`;
|
|
70
|
+
await execAsync(`git checkout -b ${branch}`, { cwd });
|
|
71
|
+
return branch;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Merge a source branch into the current branch
|
|
76
|
+
* @param {string} source - Source branch name
|
|
77
|
+
* @param {string} cwd - Working directory
|
|
78
|
+
*/
|
|
79
|
+
export async function mergeBranch(source, cwd = process.cwd()) {
|
|
80
|
+
await execAsync(`git merge ${source} --no-edit`, { cwd });
|
|
81
|
+
}
|
|
82
|
+
|
|
16
83
|
/**
|
|
17
84
|
* Default batch configuration
|
|
18
85
|
*/
|