@fermindi/pwn-cli 0.9.4 → 0.9.6

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fermindi/pwn-cli",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Professional AI Workspace - Inject structured memory and automation into any project for AI-powered development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -391,34 +391,24 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
391
391
  }
392
392
  }
393
393
 
394
- // --- Branch isolation: create/checkout task branch ---
395
- let taskBranch = null;
394
+ // --- Pre-validation: check existing feat branch before reset ---
395
+ let storyDone = false;
396
+ const taskBranchName = `feat/${task.id}`;
397
+ let branchExists = false;
396
398
  try {
397
- taskBranch = await createTaskBranch(task.id, cwd);
398
- branchesCreated.push(taskBranch);
399
- console.log(chalk.blue(` Branch: ${chalk.bold(taskBranch)}`));
399
+ await execAsync(`git rev-parse --verify ${taskBranchName}`, { cwd });
400
+ branchExists = true;
401
+ } catch {}
400
402
 
401
- // Track branch in task file
402
- if (taskFile) {
403
- taskFile.branch = taskBranch;
404
- saveTaskFile(taskFile, cwd);
405
- }
406
- } catch (err) {
407
- console.log(chalk.red(` Failed to create branch feat/${task.id}: ${err.message}`));
408
- console.log(chalk.yellow(' Continuing on current branch...'));
409
- }
410
-
411
- // --- Pre-validation: check if existing branch already passes gates ---
412
- let storyDone = false;
413
- if (taskBranch) {
414
- // Check if this branch has commits beyond the batch branch
403
+ if (branchExists) {
415
404
  try {
416
- const { stdout: diffStat } = await execAsync('git diff --stat HEAD~1 2>/dev/null || echo ""', { cwd });
405
+ // Checkout WITHOUT reset to inspect existing work
406
+ await checkoutBranch(taskBranchName, cwd, { stash: true });
417
407
  const { stdout: logCount } = await execAsync(`git rev-list --count ${batchBranch}..HEAD`, { cwd });
418
408
  const commits = parseInt(logCount.trim(), 10);
419
409
 
420
410
  if (commits > 0) {
421
- console.log(chalk.blue(` Pre-validation: found ${commits} commit(s) on ${taskBranch}, running quality gates...`));
411
+ console.log(chalk.blue(` Pre-validation: found ${commits} commit(s) on ${taskBranchName}, running quality gates...`));
422
412
  const preGates = await runGatesWithStatus(cwd);
423
413
 
424
414
  if (preGates.success) {
@@ -445,35 +435,56 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
445
435
  // Merge into batch branch
446
436
  try {
447
437
  await checkoutBranch(batchBranch, cwd, { stash: true });
448
- await mergeBranch(taskBranch, cwd);
438
+ await mergeBranch(taskBranchName, cwd);
449
439
  mergedCount++;
450
- mergedBranches.push(taskBranch);
451
- console.log(chalk.green(` Merged ${taskBranch} → ${batchBranch}`));
440
+ mergedBranches.push(taskBranchName);
441
+ console.log(chalk.green(` Merged ${taskBranchName} → ${batchBranch}`));
452
442
  } catch (err) {
453
443
  console.log(chalk.red(` Merge failed: ${err.message}`));
454
- console.log(chalk.yellow(` Branch ${taskBranch} available for manual merge`));
455
- unmergedBranches.push(taskBranch);
444
+ console.log(chalk.yellow(` Branch ${taskBranchName} available for manual merge`));
445
+ unmergedBranches.push(taskBranchName);
456
446
  }
457
447
  } else {
458
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 });
459
451
  }
452
+ } else {
453
+ // No commits — return to batch branch for normal flow
454
+ await checkoutBranch(batchBranch, cwd, { force: true, stash: true });
460
455
  }
461
456
  } catch {
462
- // No commits or comparison failed — proceed normally
457
+ // Checkout failed — proceed normally
458
+ try { await checkoutBranch(batchBranch, cwd, { force: true, stash: true }); } catch {}
463
459
  }
464
460
  }
465
461
 
466
462
  if (storyDone) {
467
- // Return to batch branch
468
- if (taskBranch) {
469
- try {
470
- await checkoutBranch(batchBranch, cwd, { force: true, stash: true });
471
- console.log(chalk.dim(` Returned to branch: ${batchBranch}`));
472
- } catch {}
473
- }
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 {}
474
468
  continue;
475
469
  }
476
470
 
471
+ // --- Branch isolation: create/checkout task branch (resets existing) ---
472
+ let taskBranch = null;
473
+ try {
474
+ taskBranch = await createTaskBranch(task.id, cwd);
475
+ branchesCreated.push(taskBranch);
476
+ console.log(chalk.blue(` Branch: ${chalk.bold(taskBranch)}`));
477
+
478
+ // Track branch in task file
479
+ if (taskFile) {
480
+ taskFile.branch = taskBranch;
481
+ saveTaskFile(taskFile, cwd);
482
+ }
483
+ } catch (err) {
484
+ console.log(chalk.red(` Failed to create branch feat/${task.id}: ${err.message}`));
485
+ console.log(chalk.yellow(' Continuing on current branch...'));
486
+ }
487
+
477
488
  // --- Phase 2: Execution ---
478
489
  const phaseLabel = noPlan ? '' : 'Phase 2';
479
490
  console.log(chalk.blue(` ${noPlan ? 'Executing' : 'Phase 2: Executing'} ${task.id}...`));
@@ -50,16 +50,10 @@ export async function createTaskBranch(taskId, cwd = process.cwd()) {
50
50
  await checkoutBranch(branch, cwd, { stash: true });
51
51
  await execAsync(`git reset --hard ${baseRef}`, { cwd });
52
52
  } else {
53
- // Stash before -b too (untracked files block checkout -b as well)
54
- let didStash = false;
55
- try {
56
- const { stdout } = await execAsync('git stash --include-untracked', { cwd });
57
- didStash = !stdout.includes('No local changes');
58
- } catch {}
53
+ // Stash + drop to unblock checkout -b (metadata files re-created by runner)
54
+ try { await execAsync('git stash --include-untracked', { cwd }); } catch {}
59
55
  await execAsync(`git checkout -b ${branch}`, { cwd });
60
- if (didStash) {
61
- try { await execAsync('git stash pop', { cwd }); } catch {}
62
- }
56
+ try { await execAsync('git stash drop', { cwd }); } catch {}
63
57
  }
64
58
  return branch;
65
59
  }
@@ -73,25 +67,18 @@ export async function createTaskBranch(taskId, cwd = process.cwd()) {
73
67
  * @param {{ force?: boolean, stash?: boolean }} options
74
68
  */
75
69
  export async function checkoutBranch(branch, cwd = process.cwd(), { force = false, stash = false } = {}) {
76
- let didStash = false;
77
70
  if (stash) {
78
- try {
79
- const { stdout } = await execAsync('git stash --include-untracked', { cwd });
80
- didStash = !stdout.includes('No local changes');
81
- } catch {}
71
+ // Stash only to unblock checkout, then drop — these are runner
72
+ // metadata files (.ai/batch/tasks/*.json) that get re-created by
73
+ // saveTaskFile, so restoring them causes merge conflicts for no gain.
74
+ try { await execAsync('git stash --include-untracked', { cwd }); } catch {}
82
75
  }
83
76
 
84
77
  const flag = force ? ' --force' : '';
85
- try {
86
- await execAsync(`git checkout${flag} ${branch}`, { cwd });
87
- } catch (err) {
88
- // If checkout failed, drop stash so it doesn't pile up
89
- if (didStash) { try { await execAsync('git stash drop', { cwd }); } catch {} }
90
- throw err;
91
- }
78
+ await execAsync(`git checkout${flag} ${branch}`, { cwd });
92
79
 
93
- if (didStash) {
94
- try { await execAsync('git stash pop', { cwd }); } catch {}
80
+ if (stash) {
81
+ try { await execAsync('git stash drop', { cwd }); } catch {}
95
82
  }
96
83
  }
97
84