@fermindi/pwn-cli 0.9.7 → 0.9.9

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.7",
3
+ "version": "0.9.9",
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": {
@@ -534,7 +534,7 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
534
534
  console.log(chalk.yellow(` Retry ${retry}/${MAX_RETRIES}`));
535
535
  }
536
536
 
537
- const prompt = buildPrompt(task.id, batchWorktreePath, mainCwd, errorContext);
537
+ const prompt = buildPrompt(task.id, batchWorktreePath, mainCwd, errorContext, retry);
538
538
  if (!prompt) {
539
539
  console.log(chalk.red(` Cannot build prompt for ${task.id} — skipping`));
540
540
  break;
@@ -840,6 +840,54 @@ function spawnClaude(prompt, task, iteration, maxIter, done, total, phase, logFi
840
840
  });
841
841
  }
842
842
 
843
+ /**
844
+ * Ensure Python dev tools (pytest, ruff, mypy, etc.) exist in the venv.
845
+ * Extracts tool names from gate_commands and verifies they're importable.
846
+ * Installs missing ones to prevent wasted retries on environment issues.
847
+ * @param {string} taskCwd - Worktree directory (has .venv symlink)
848
+ * @param {string} mainCwd - Main repo directory for config
849
+ */
850
+ async function ensureDevTools(taskCwd, mainCwd) {
851
+ const config = loadConfig(mainCwd);
852
+ const gateCommands = config.gate_commands;
853
+ if (!gateCommands) return;
854
+
855
+ // Find the python binary from gate commands
856
+ const sampleCmd = Object.values(gateCommands)[0] || '';
857
+ const pythonMatch = sampleCmd.match(/^(\S*python\S*)\s/);
858
+ if (!pythonMatch) return; // not a Python project
859
+
860
+ const python = pythonMatch[1];
861
+
862
+ // Extract module names from commands like ".venv/bin/python -m pytest --tb=short"
863
+ const modules = new Set();
864
+ for (const cmd of Object.values(gateCommands)) {
865
+ const modMatch = cmd.match(/-m\s+(\w+)/);
866
+ if (modMatch) modules.add(modMatch[1]);
867
+ }
868
+ if (modules.size === 0) return;
869
+
870
+ // Check which modules are missing
871
+ const missing = [];
872
+ for (const mod of modules) {
873
+ try {
874
+ await execAsync(`${python} -c "import ${mod}"`, { cwd: taskCwd, timeout: 10_000 });
875
+ } catch {
876
+ missing.push(mod);
877
+ }
878
+ }
879
+
880
+ if (missing.length > 0) {
881
+ console.log(chalk.yellow(` Dev tools missing in venv: ${missing.join(', ')}. Installing...`));
882
+ try {
883
+ await execAsync(`${python} -m pip install ${missing.join(' ')} --quiet`, { cwd: taskCwd, timeout: 60_000 });
884
+ console.log(chalk.green(` Installed: ${missing.join(', ')}`));
885
+ } catch (err) {
886
+ console.log(chalk.red(` Failed to install dev tools: ${err.message}`));
887
+ }
888
+ }
889
+ }
890
+
843
891
  /**
844
892
  * Run quality gates with real-time PASS/FAIL display.
845
893
  * @param {string} taskCwd - Directory where gates run (task worktree)
@@ -852,6 +900,9 @@ async function runGatesWithStatus(taskCwd, mainCwd = taskCwd) {
852
900
  let allPassed = true;
853
901
  let errorOutput = '';
854
902
 
903
+ // Pre-flight: ensure dev tools exist before running gates
904
+ await ensureDevTools(taskCwd, mainCwd);
905
+
855
906
  for (const gate of gates) {
856
907
  if (skipGates.includes(gate)) {
857
908
  console.log(chalk.dim(` ${gate}: SKIP`));
@@ -875,12 +926,14 @@ async function runGatesWithStatus(taskCwd, mainCwd = taskCwd) {
875
926
 
876
927
  /**
877
928
  * Build the prompt from template, substituting placeholders.
929
+ * On retries, generates a focused "fix" prompt instead of the full template.
878
930
  * @param {string} storyId - Story ID
879
931
  * @param {string} batchWorktreePath - Batch worktree (for prd.json, tracked file)
880
932
  * @param {string} mainCwd - Main repo (for prompt.md, gitignored file)
881
933
  * @param {string} extraContext - Error context from previous attempt
934
+ * @param {number} retry - Retry number (0 = first attempt)
882
935
  */
883
- function buildPrompt(storyId, batchWorktreePath, mainCwd, extraContext) {
936
+ function buildPrompt(storyId, batchWorktreePath, mainCwd, extraContext, retry = 0) {
884
937
  const prdPath = join(batchWorktreePath, '.ai', 'tasks', 'prd.json');
885
938
  const promptPath = join(mainCwd, '.ai', 'batch', 'prompt.md');
886
939
 
@@ -891,9 +944,36 @@ function buildPrompt(storyId, batchWorktreePath, mainCwd, extraContext) {
891
944
  return '';
892
945
  }
893
946
 
894
- const doneIds = prd.stories.filter(s => s.passes).map(s => s.id);
895
947
  const acList = (story.acceptance_criteria || []).map(ac => `- ${ac}`).join('\n') || 'None';
896
948
 
949
+ // --- Retry: focused fix prompt ---
950
+ if (retry > 0 && extraContext) {
951
+ return `You are fixing story ${storyId}: ${story.title}
952
+
953
+ ## Context
954
+ A previous attempt already implemented this story. The working tree contains the implementation.
955
+ Read the modified files to understand what was done.
956
+
957
+ ## Acceptance criteria
958
+ ${acList}
959
+
960
+ ## Quality gate failures
961
+ \`\`\`
962
+ ${extraContext}
963
+ \`\`\`
964
+
965
+ ## Instructions
966
+ 1. Read the existing code in the working tree
967
+ 2. Analyze the quality gate errors above
968
+ 3. Fix ONLY what's needed to make the gates pass
969
+ 4. If the errors indicate a deeper design problem that can't be patched, you may rewrite the implementation — but try fixing first
970
+ 5. Make sure the fix still satisfies all acceptance criteria above
971
+ 6. Commit your changes when done`;
972
+ }
973
+
974
+ // --- First attempt: full template ---
975
+ const doneIds = prd.stories.filter(s => s.passes).map(s => s.id);
976
+
897
977
  let depsList = 'None';
898
978
  if (story.dependencies?.length > 0) {
899
979
  depsList = story.dependencies
@@ -912,10 +992,6 @@ function buildPrompt(storyId, batchWorktreePath, mainCwd, extraContext) {
912
992
  prompt = prompt.replaceAll('{NOTES}', story.notes || '');
913
993
  prompt = prompt.replaceAll('{DEPENDENCIES}', depsList);
914
994
 
915
- if (extraContext) {
916
- prompt += `\n\n## Previous Attempt Failed\nThe previous attempt failed quality gates. Here is the error output:\n\n\`\`\`\n${extraContext}\n\`\`\`\n\nFix these issues before committing.`;
917
- }
918
-
919
995
  return prompt;
920
996
  }
921
997
 
@@ -5,7 +5,7 @@
5
5
  * checkpointing, and signal handling.
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, symlinkSync, unlinkSync, lstatSync } from 'fs';
8
+ import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, symlinkSync, unlinkSync, lstatSync, readlinkSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import os from 'os';
11
11
  import { exec } from 'child_process';
@@ -244,7 +244,22 @@ export function prepareWorktree(worktreePath, mainCwd) {
244
244
  for (const dir of DEP_DIRS) {
245
245
  const source = join(mainCwd, dir);
246
246
  const target = join(worktreePath, dir);
247
- if (existsSync(source) && !existsSync(target)) {
247
+ if (!existsSync(source)) continue;
248
+
249
+ // Validate existing symlinks (git may restore stale ones from history)
250
+ try {
251
+ const stats = lstatSync(target);
252
+ if (stats.isSymbolicLink()) {
253
+ if (readlinkSync(target) !== source) {
254
+ unlinkSync(target);
255
+ symlinkSync(source, target);
256
+ }
257
+ continue; // symlink exists and is correct
258
+ }
259
+ // Not a symlink (real dir) — leave it alone
260
+ continue;
261
+ } catch {
262
+ // Doesn't exist — create it
248
263
  symlinkSync(source, target);
249
264
  }
250
265
  }