@sienklogic/plan-build-run 2.41.0 → 2.42.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/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/hooks/hooks.json +5 -0
- package/plugins/pbr/scripts/lib/circuit-state.js +134 -0
- package/plugins/pbr/scripts/lib/config.js +17 -0
- package/plugins/pbr/scripts/lib/core.js +5 -3
- package/plugins/pbr/scripts/lib/gates/advisories.js +125 -0
- package/plugins/pbr/scripts/lib/gates/build-dependency.js +100 -0
- package/plugins/pbr/scripts/lib/gates/build-executor.js +79 -0
- package/plugins/pbr/scripts/lib/gates/helpers.js +62 -0
- package/plugins/pbr/scripts/lib/gates/milestone-complete.js +120 -0
- package/plugins/pbr/scripts/lib/gates/plan-executor.js +36 -0
- package/plugins/pbr/scripts/lib/gates/quick-executor.js +76 -0
- package/plugins/pbr/scripts/lib/gates/review-planner.js +61 -0
- package/plugins/pbr/scripts/lib/gates/review-verifier.js +69 -0
- package/plugins/pbr/scripts/local-llm/client.js +29 -6
- package/plugins/pbr/scripts/run-hook.js +3 -0
- package/plugins/pbr/scripts/validate-task.js +10 -605
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate: milestone complete verification check.
|
|
5
|
+
* When active skill is "milestone" and a general/planner agent is spawned
|
|
6
|
+
* for a "complete" operation, verify all milestone phases have VERIFICATION.md.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { readActiveSkill, readCurrentPhaseInt } = require('./helpers');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse VERIFICATION.md frontmatter to extract status field.
|
|
15
|
+
* Returns the status string or 'unknown' if not parseable.
|
|
16
|
+
* @param {string} filePath - path to VERIFICATION.md
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function getVerificationStatus(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
22
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
23
|
+
if (!fmMatch) return 'unknown';
|
|
24
|
+
const statusMatch = fmMatch[1].match(/^status:\s*(\S+)/m);
|
|
25
|
+
return statusMatch ? statusMatch[1] : 'unknown';
|
|
26
|
+
} catch (_e) {
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Blocking check: when the active skill is "milestone" and a general/planner agent
|
|
33
|
+
* is being spawned for a "complete" operation, verify all milestone phases have VERIFICATION.md.
|
|
34
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
35
|
+
* @param {object} data - hook data with tool_input
|
|
36
|
+
* @returns {{ block: boolean, reason: string }|null}
|
|
37
|
+
*/
|
|
38
|
+
function checkMilestoneCompleteGate(data) {
|
|
39
|
+
const toolInput = data.tool_input || {};
|
|
40
|
+
const subagentType = toolInput.subagent_type || '';
|
|
41
|
+
const description = toolInput.description || '';
|
|
42
|
+
|
|
43
|
+
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
44
|
+
const planningDir = path.join(cwd, '.planning');
|
|
45
|
+
|
|
46
|
+
// Only gate when active skill is "milestone"
|
|
47
|
+
const activeSkill = readActiveSkill(planningDir);
|
|
48
|
+
if (activeSkill !== 'milestone') return null;
|
|
49
|
+
|
|
50
|
+
// Only gate pbr:general and pbr:planner
|
|
51
|
+
if (subagentType !== 'pbr:general' && subagentType !== 'pbr:planner') return null;
|
|
52
|
+
|
|
53
|
+
// Only gate "complete" operations
|
|
54
|
+
if (!/complete/i.test(description)) return null;
|
|
55
|
+
|
|
56
|
+
// Read STATE.md for current phase
|
|
57
|
+
const currentPhase = readCurrentPhaseInt(planningDir);
|
|
58
|
+
if (!currentPhase) return null;
|
|
59
|
+
|
|
60
|
+
// Read ROADMAP.md and find the milestone containing the current phase
|
|
61
|
+
const roadmapFile = path.join(planningDir, 'ROADMAP.md');
|
|
62
|
+
try {
|
|
63
|
+
const roadmap = fs.readFileSync(roadmapFile, 'utf8');
|
|
64
|
+
|
|
65
|
+
// Split into milestone sections
|
|
66
|
+
const milestoneSections = roadmap.split(/^## Milestone:/m).slice(1);
|
|
67
|
+
|
|
68
|
+
for (const section of milestoneSections) {
|
|
69
|
+
// Parse phase numbers from table rows
|
|
70
|
+
const phaseNumbers = [];
|
|
71
|
+
const tableRowRegex = /^\|\s*(\d+)\s*\|/gm;
|
|
72
|
+
let match;
|
|
73
|
+
while ((match = tableRowRegex.exec(section)) !== null) {
|
|
74
|
+
phaseNumbers.push(parseInt(match[1], 10));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if current phase is in this milestone
|
|
78
|
+
if (!phaseNumbers.includes(currentPhase)) continue;
|
|
79
|
+
|
|
80
|
+
// Found the right milestone — check all phases have VERIFICATION.md
|
|
81
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
82
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
83
|
+
|
|
84
|
+
for (const phaseNum of phaseNumbers) {
|
|
85
|
+
const paddedPhase = String(phaseNum).padStart(2, '0');
|
|
86
|
+
const pDirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(paddedPhase + '-'));
|
|
87
|
+
if (pDirs.length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
block: true,
|
|
90
|
+
reason: `Milestone complete gate: phase ${paddedPhase} directory not found.\n\nAll milestone phases must exist and have a passing VERIFICATION.md before the milestone can be completed.\n\nRun /pbr:review ${paddedPhase} to verify the phase (it must reach status: passed).`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const verificationFile = path.join(phasesDir, pDirs[0], 'VERIFICATION.md');
|
|
94
|
+
const hasVerification = fs.existsSync(verificationFile);
|
|
95
|
+
if (!hasVerification) {
|
|
96
|
+
return {
|
|
97
|
+
block: true,
|
|
98
|
+
reason: `Milestone complete gate: phase ${paddedPhase} (${pDirs[0]}) lacks VERIFICATION.md.\n\nAll milestone phases must have a passing VERIFICATION.md before the milestone can be completed.\n\nRun /pbr:review ${paddedPhase} to verify the phase (it must reach status: passed).`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const verStatus = getVerificationStatus(verificationFile);
|
|
102
|
+
if (verStatus === 'gaps_found') {
|
|
103
|
+
return {
|
|
104
|
+
block: true,
|
|
105
|
+
reason: `Milestone complete gate: phase ${paddedPhase} VERIFICATION.md has status: gaps_found.\n\nAll gaps must be closed before the milestone can be completed. The verifier found issues that need resolution.\n\nRun /pbr:review ${paddedPhase} to close gaps (phase must reach status: passed).`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// All phases verified
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
} catch (_e) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { checkMilestoneCompleteGate, getVerificationStatus };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate: plan skill executor block.
|
|
5
|
+
* The plan skill should never spawn executors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { readActiveSkill } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Blocking check: when the active skill is "plan", block executor spawning.
|
|
13
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
14
|
+
* @param {object} data - hook data with tool_input
|
|
15
|
+
* @returns {{ block: boolean, reason: string }|null}
|
|
16
|
+
*/
|
|
17
|
+
function checkPlanExecutorGate(data) {
|
|
18
|
+
const toolInput = data.tool_input || {};
|
|
19
|
+
const subagentType = toolInput.subagent_type || '';
|
|
20
|
+
|
|
21
|
+
// Only gate pbr:executor
|
|
22
|
+
if (subagentType !== 'pbr:executor') return null;
|
|
23
|
+
|
|
24
|
+
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
25
|
+
const planningDir = path.join(cwd, '.planning');
|
|
26
|
+
|
|
27
|
+
const activeSkill = readActiveSkill(planningDir);
|
|
28
|
+
if (activeSkill !== 'plan') return null;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
block: true,
|
|
32
|
+
reason: 'Plan skill cannot spawn executors.\n\nThe plan skill creates plans; the build skill executes them. Spawning an executor from the plan skill violates the separation of concerns.\n\nRun /pbr:build to execute plans instead.'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { checkPlanExecutorGate };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate: quick executor PLAN.md check.
|
|
5
|
+
* When active skill is "quick" and pbr:executor is spawned, verify
|
|
6
|
+
* that at least one .planning/quick/{NNN}-{slug}/PLAN.md exists.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { readActiveSkill } = require('./helpers');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Blocking check: when the active skill is "quick" and an executor is being
|
|
15
|
+
* spawned, verify that at least one .planning/quick/{NNN}-{slug}/PLAN.md exists.
|
|
16
|
+
* Returns { block: true, reason: "..." } if the executor should be blocked,
|
|
17
|
+
* or null if it's OK to proceed.
|
|
18
|
+
* @param {object} data - hook data with tool_input
|
|
19
|
+
* @returns {{ block: boolean, reason: string }|null}
|
|
20
|
+
*/
|
|
21
|
+
function checkQuickExecutorGate(data) {
|
|
22
|
+
const toolInput = data.tool_input || {};
|
|
23
|
+
const subagentType = toolInput.subagent_type || '';
|
|
24
|
+
|
|
25
|
+
// Only gate pbr:executor
|
|
26
|
+
if (subagentType !== 'pbr:executor') return null;
|
|
27
|
+
|
|
28
|
+
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
29
|
+
const planningDir = path.join(cwd, '.planning');
|
|
30
|
+
|
|
31
|
+
// Only gate when active skill is "quick"
|
|
32
|
+
const activeSkill = readActiveSkill(planningDir);
|
|
33
|
+
if (activeSkill !== 'quick') return null;
|
|
34
|
+
|
|
35
|
+
// Check for any PLAN.md in .planning/quick/*/
|
|
36
|
+
const quickDir = path.join(planningDir, 'quick');
|
|
37
|
+
if (!fs.existsSync(quickDir)) {
|
|
38
|
+
return {
|
|
39
|
+
block: true,
|
|
40
|
+
reason: 'Cannot spawn executor: .planning/quick/ directory does not exist.\n\nThe quick skill must create the task directory and PLAN.md before an executor can run (Steps 4-6).\n\nRe-run /pbr:quick to create the quick task directory and PLAN.md.'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const dirs = fs.readdirSync(quickDir).filter(d => {
|
|
46
|
+
return /^\d{3}-/.test(d) && fs.statSync(path.join(quickDir, d)).isDirectory();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Look for the most recent quick task dir that has a PLAN.md
|
|
50
|
+
const hasPlan = dirs.some(d => {
|
|
51
|
+
const planFile = path.join(quickDir, d, 'PLAN.md');
|
|
52
|
+
try {
|
|
53
|
+
const stat = fs.statSync(planFile);
|
|
54
|
+
return stat.size > 0;
|
|
55
|
+
} catch (_e) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!hasPlan) {
|
|
61
|
+
return {
|
|
62
|
+
block: true,
|
|
63
|
+
reason: 'Cannot spawn executor: no PLAN.md found in any .planning/quick/*/ directory.\n\nThe quick skill must write a non-empty PLAN.md inside .planning/quick/{NNN}-{slug}/ before an executor can run (Steps 4-6).\n\nRe-run /pbr:quick to create the quick task directory and PLAN.md.'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
} catch (_e) {
|
|
67
|
+
return {
|
|
68
|
+
block: true,
|
|
69
|
+
reason: 'Cannot spawn executor: failed to read .planning/quick/ directory.\n\nThe directory exists but could not be read, possibly due to a permissions issue or filesystem error.\n\nRe-run /pbr:quick to recreate the quick task directory and PLAN.md.'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { checkQuickExecutorGate };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate: review planner VERIFICATION.md check.
|
|
5
|
+
* When active skill is "review" and pbr:planner is spawned, verify
|
|
6
|
+
* that a VERIFICATION.md exists in the current phase directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { readActiveSkill, readCurrentPhase } = require('./helpers');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Blocking check: when the active skill is "review" and a planner is being
|
|
15
|
+
* spawned, verify that a VERIFICATION.md exists in the current phase directory.
|
|
16
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
17
|
+
* @param {object} data - hook data with tool_input
|
|
18
|
+
* @returns {{ block: boolean, reason: string }|null}
|
|
19
|
+
*/
|
|
20
|
+
function checkReviewPlannerGate(data) {
|
|
21
|
+
const toolInput = data.tool_input || {};
|
|
22
|
+
const subagentType = toolInput.subagent_type || '';
|
|
23
|
+
|
|
24
|
+
// Only gate pbr:planner
|
|
25
|
+
if (subagentType !== 'pbr:planner') return null;
|
|
26
|
+
|
|
27
|
+
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
28
|
+
const planningDir = path.join(cwd, '.planning');
|
|
29
|
+
|
|
30
|
+
// Only gate when active skill is "review"
|
|
31
|
+
const activeSkill = readActiveSkill(planningDir);
|
|
32
|
+
if (activeSkill !== 'review') return null;
|
|
33
|
+
|
|
34
|
+
// Read STATE.md for current phase
|
|
35
|
+
const currentPhase = readCurrentPhase(planningDir);
|
|
36
|
+
if (!currentPhase) return null;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
40
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
41
|
+
|
|
42
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
43
|
+
if (dirs.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
46
|
+
const hasVerification = fs.existsSync(path.join(phaseDir, 'VERIFICATION.md'));
|
|
47
|
+
|
|
48
|
+
if (!hasVerification) {
|
|
49
|
+
return {
|
|
50
|
+
block: true,
|
|
51
|
+
reason: 'Review planner gate: cannot spawn planner without VERIFICATION.md.\n\nGap closure requires an existing VERIFICATION.md to identify which gaps need closing. Without it, the planner has no input.\n\nRun /pbr:review {N} to create VERIFICATION.md first.'
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
} catch (_e) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { checkReviewPlannerGate };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate: review verifier SUMMARY.md check.
|
|
5
|
+
* When active skill is "review" and pbr:verifier is spawned, verify
|
|
6
|
+
* that a SUMMARY*.md exists in the current phase directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { readActiveSkill, readCurrentPhase } = require('./helpers');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Blocking check: when the active skill is "review" and a verifier is being
|
|
15
|
+
* spawned, verify that a SUMMARY*.md exists in the current phase directory.
|
|
16
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
17
|
+
* @param {object} data - hook data with tool_input
|
|
18
|
+
* @returns {{ block: boolean, reason: string }|null}
|
|
19
|
+
*/
|
|
20
|
+
function checkReviewVerifierGate(data) {
|
|
21
|
+
const toolInput = data.tool_input || {};
|
|
22
|
+
const subagentType = toolInput.subagent_type || '';
|
|
23
|
+
|
|
24
|
+
// Only gate pbr:verifier
|
|
25
|
+
if (subagentType !== 'pbr:verifier') return null;
|
|
26
|
+
|
|
27
|
+
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
28
|
+
const planningDir = path.join(cwd, '.planning');
|
|
29
|
+
|
|
30
|
+
// Only gate when active skill is "review"
|
|
31
|
+
const activeSkill = readActiveSkill(planningDir);
|
|
32
|
+
if (activeSkill !== 'review') return null;
|
|
33
|
+
|
|
34
|
+
// Read STATE.md for current phase
|
|
35
|
+
const currentPhase = readCurrentPhase(planningDir);
|
|
36
|
+
if (!currentPhase) return null;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
40
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
41
|
+
|
|
42
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
43
|
+
if (dirs.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
46
|
+
const files = fs.readdirSync(phaseDir);
|
|
47
|
+
const hasSummary = files.some(f => {
|
|
48
|
+
if (!/^SUMMARY/i.test(f)) return false;
|
|
49
|
+
try {
|
|
50
|
+
return fs.statSync(path.join(phaseDir, f)).size > 0;
|
|
51
|
+
} catch (_e) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!hasSummary) {
|
|
57
|
+
return {
|
|
58
|
+
block: true,
|
|
59
|
+
reason: 'Review verifier gate: cannot spawn verifier without SUMMARY.md.\n\nThe verifier checks executor output against the plan. Without a SUMMARY.md, there is nothing to verify.\n\nRun /pbr:build {N} to create SUMMARY.md first.'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { checkReviewVerifierGate };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/* global fetch, AbortSignal, performance */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const {
|
|
5
|
+
isDisabledPersistent,
|
|
6
|
+
recordFailurePersistent,
|
|
7
|
+
resetCircuitPersistent
|
|
8
|
+
} = require('../lib/circuit-state');
|
|
9
|
+
|
|
4
10
|
// Circuit breaker: Map<operationType, { failures: number, disabled: boolean }>
|
|
5
11
|
const circuitState = new Map();
|
|
6
12
|
|
|
@@ -109,6 +115,7 @@ function resetCircuit(operationType) {
|
|
|
109
115
|
* @param {string} operationType - operation identifier for circuit breaker tracking
|
|
110
116
|
* @param {object} [options={}] - optional parameters
|
|
111
117
|
* @param {boolean} [options.logprobs] - if true, request logprobs from the API
|
|
118
|
+
* @param {string} [options.planningDir] - optional .planning directory for persistent circuit breaker state
|
|
112
119
|
* @returns {Promise<{ content: string, latency_ms: number, tokens: number, logprobsData: Array<{token: string, logprob: number}>|null }>}
|
|
113
120
|
*/
|
|
114
121
|
async function complete(config, prompt, operationType, options = {}) {
|
|
@@ -119,12 +126,19 @@ async function complete(config, prompt, operationType, options = {}) {
|
|
|
119
126
|
const numCtx = (config.advanced && config.advanced.num_ctx) || 4096;
|
|
120
127
|
const keepAlive = (config.advanced && config.advanced.keep_alive) || '30m';
|
|
121
128
|
const maxFailures = (config.advanced && config.advanced.disable_after_failures) || 3;
|
|
129
|
+
const planningDir = options.planningDir || null;
|
|
122
130
|
|
|
131
|
+
// Check in-memory circuit first (fast path), then persistent state
|
|
123
132
|
if (isDisabled(operationType, maxFailures)) {
|
|
124
133
|
const err = new Error('Circuit open for operation: ' + operationType);
|
|
125
134
|
err.type = 'circuit_open';
|
|
126
135
|
throw err;
|
|
127
136
|
}
|
|
137
|
+
if (planningDir && isDisabledPersistent(planningDir, operationType, maxFailures)) {
|
|
138
|
+
const err = new Error('Circuit open for operation: ' + operationType);
|
|
139
|
+
err.type = 'circuit_open';
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
128
142
|
|
|
129
143
|
const bodyObj = {
|
|
130
144
|
model,
|
|
@@ -185,6 +199,7 @@ async function complete(config, prompt, operationType, options = {}) {
|
|
|
185
199
|
if (isConnRefused) {
|
|
186
200
|
// Server not running — no point retrying
|
|
187
201
|
recordFailure(operationType, maxFailures);
|
|
202
|
+
if (planningDir) recordFailurePersistent(planningDir, operationType, maxFailures);
|
|
188
203
|
throw err;
|
|
189
204
|
}
|
|
190
205
|
|
|
@@ -197,18 +212,26 @@ async function complete(config, prompt, operationType, options = {}) {
|
|
|
197
212
|
}
|
|
198
213
|
|
|
199
214
|
// Final attempt or non-retryable error
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} else {
|
|
203
|
-
recordFailure(operationType, maxFailures);
|
|
204
|
-
}
|
|
215
|
+
recordFailure(operationType, maxFailures);
|
|
216
|
+
if (planningDir) recordFailurePersistent(planningDir, operationType, maxFailures);
|
|
205
217
|
throw err;
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
// Should not reach here, but guard anyway
|
|
210
222
|
recordFailure(operationType, maxFailures);
|
|
223
|
+
if (planningDir) recordFailurePersistent(planningDir, operationType, maxFailures);
|
|
211
224
|
throw lastErr;
|
|
212
225
|
}
|
|
213
226
|
|
|
214
|
-
module.exports = {
|
|
227
|
+
module.exports = {
|
|
228
|
+
tryParseJSON,
|
|
229
|
+
categorizeError,
|
|
230
|
+
isDisabled,
|
|
231
|
+
recordFailure,
|
|
232
|
+
resetCircuit,
|
|
233
|
+
complete,
|
|
234
|
+
isDisabledPersistent,
|
|
235
|
+
recordFailurePersistent,
|
|
236
|
+
resetCircuitPersistent
|
|
237
|
+
};
|
|
@@ -60,7 +60,10 @@ if (invokedViaEval) {
|
|
|
60
60
|
|
|
61
61
|
// When required as a module from -e bootstrap, export a runner function
|
|
62
62
|
if (typeof module !== 'undefined' && module.exports) {
|
|
63
|
+
const BOOTSTRAP_SNIPPET = "node -e \"var r=process.env.CLAUDE_PLUGIN_ROOT||'',m=r.match(/^\\/([a-zA-Z])\\/(.*)/);if(m)r=m[1]+String.fromCharCode(58)+String.fromCharCode(92)+m[2];require(require('path').resolve(r,'scripts','run-hook.js'))\"";
|
|
63
64
|
module.exports = runScript;
|
|
65
|
+
module.exports.BOOTSTRAP_SNIPPET = BOOTSTRAP_SNIPPET;
|
|
66
|
+
module.exports.runScript = runScript;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
// If we have a script name, run it immediately
|