@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
|
@@ -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)
|
|
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
|
}
|