@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fermindi/pwn-cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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": {
@@ -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.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
- // --- Branch isolation: create/checkout task branch ---
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) {