@besales/ops-framework 0.1.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 +10 -0
- package/README.md +328 -0
- package/bin/build-check-context.mjs +67 -0
- package/bin/build-execution-ledger.mjs +54 -0
- package/bin/estimate-llm-input.mjs +160 -0
- package/bin/guard-task.mjs +384 -0
- package/bin/hash-task-artifacts.mjs +44 -0
- package/bin/init-project.mjs +49 -0
- package/bin/intake-execution-feedback.mjs +207 -0
- package/bin/intake-feedback.test.mjs +73 -0
- package/bin/learning-loop.mjs +658 -0
- package/bin/learning-loop.test.mjs +175 -0
- package/bin/lib/bootstrap-utils.mjs +542 -0
- package/bin/lib/bootstrap-utils.test.mjs +156 -0
- package/bin/lib/check-context-utils.mjs +1448 -0
- package/bin/lib/check-context-utils.test.mjs +497 -0
- package/bin/lib/execution-ledger-utils.mjs +162 -0
- package/bin/lib/execution-ledger-utils.test.mjs +74 -0
- package/bin/lib/llm-input-pack-utils.mjs +663 -0
- package/bin/lib/llm-input-pack-utils.test.mjs +262 -0
- package/bin/lib/project-config.mjs +229 -0
- package/bin/lib/project-config.test.mjs +102 -0
- package/bin/lib/task-manifest-utils.mjs +512 -0
- package/bin/lib/task-manifest-utils.test.mjs +218 -0
- package/bin/lib/task-metrics-utils.mjs +63 -0
- package/bin/lib/task-metrics-utils.test.mjs +40 -0
- package/bin/lib/test-setup.mjs +37 -0
- package/bin/new-task.mjs +42 -0
- package/bin/ops-agent.mjs +81 -0
- package/bin/preflight.mjs +56 -0
- package/bin/providers/external-cli-checker.mjs +190 -0
- package/bin/providers/openai-checker.mjs +62 -0
- package/bin/quality-gates.mjs +92 -0
- package/bin/run-check.mjs +559 -0
- package/bin/run-plan-check-loop.mjs +392 -0
- package/bin/run-verify.mjs +627 -0
- package/bin/self-lint.mjs +88 -0
- package/bin/supervisor-turn.mjs +146 -0
- package/bin/supervisor-turn.test.mjs +72 -0
- package/bin/task-manifest.mjs +57 -0
- package/bin/task-metrics.mjs +48 -0
- package/bin/transition.mjs +94 -0
- package/bin/validate-check-artifacts.mjs +418 -0
- package/config/default-agents.json +100 -0
- package/package.json +28 -0
- package/playbooks/checker-context.md +9 -0
- package/playbooks/complexity-performance.md +13 -0
- package/playbooks/production-rollout.md +9 -0
- package/playbooks/source-sync-provider.md +9 -0
- package/playbooks/ui-acceptance.md +9 -0
- package/prompts/checker.md +170 -0
- package/prompts/executor.md +54 -0
- package/prompts/planner.md +128 -0
- package/prompts/researcher.md +44 -0
- package/prompts/supervisor.md +337 -0
- package/prompts/verifier.md +128 -0
- package/templates/brief.md +15 -0
- package/templates/check-resolution.md +69 -0
- package/templates/check-result.json +32 -0
- package/templates/check.md +46 -0
- package/templates/execution-feedback.md +25 -0
- package/templates/execution.md +101 -0
- package/templates/human-gate-summary.md +49 -0
- package/templates/orchestration-log.md +8 -0
- package/templates/plan.md +86 -0
- package/templates/research.md +13 -0
- package/templates/retrospective.md +48 -0
- package/templates/status.md +53 -0
- package/templates/verify-result.json +19 -0
- package/templates/verify.md +41 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CODEX_COMMAND = 'codex';
|
|
7
|
+
|
|
8
|
+
export function resolveExternalCliProvider(providerName, config = {}) {
|
|
9
|
+
const configured = config.checkerProviders?.[providerName] || {};
|
|
10
|
+
if (providerName === 'codex-cli') {
|
|
11
|
+
return {
|
|
12
|
+
command: configured.command || DEFAULT_CODEX_COMMAND,
|
|
13
|
+
args: configured.args || ['exec', '--sandbox', 'read-only', '--output-last-message', '$OUTPUT_FILE', '--model', '$MODEL', '-'],
|
|
14
|
+
input: configured.input || 'stdin',
|
|
15
|
+
output: configured.output || 'file',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (providerName === 'claude-cli' || providerName === 'cloud-cli') {
|
|
19
|
+
return {
|
|
20
|
+
command: configured.command || (providerName === 'cloud-cli' ? 'cloud' : 'claude'),
|
|
21
|
+
args: configured.args || ['--print', '--model', '$MODEL'],
|
|
22
|
+
input: configured.input || 'stdin',
|
|
23
|
+
output: configured.output || 'stdout',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (providerName === 'custom-cli') {
|
|
27
|
+
if (!configured.command) {
|
|
28
|
+
const error = new Error('custom-cli requires checkerProviders.custom-cli.command in agents.json.');
|
|
29
|
+
error.failureReason = 'provider_unavailable';
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
command: configured.command,
|
|
34
|
+
args: configured.args || [],
|
|
35
|
+
input: configured.input || 'stdin',
|
|
36
|
+
output: configured.output || 'stdout',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runExternalCliChecker({
|
|
44
|
+
providerName,
|
|
45
|
+
providerConfig,
|
|
46
|
+
model,
|
|
47
|
+
reasoningEffort,
|
|
48
|
+
prompt,
|
|
49
|
+
cwd,
|
|
50
|
+
}) {
|
|
51
|
+
if (!providerConfig) {
|
|
52
|
+
const error = new Error(`Unknown external CLI provider: ${providerName}`);
|
|
53
|
+
error.failureReason = 'provider_unavailable';
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isCommandAvailable(providerConfig.command)) {
|
|
58
|
+
const error = new Error(`Checker CLI command is not available: ${providerConfig.command}`);
|
|
59
|
+
error.failureReason = 'provider_unavailable';
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `leakengine-checker-${providerName}-`));
|
|
64
|
+
const outputFile = path.join(tmpDir, 'checker-output.raw.md');
|
|
65
|
+
const args = providerConfig.args.map((arg) => expandArg(arg, { model, reasoningEffort, outputFile }));
|
|
66
|
+
const result = spawnSync(providerConfig.command, args, {
|
|
67
|
+
cwd,
|
|
68
|
+
input: providerConfig.input === 'stdin' ? prompt : undefined,
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
maxBuffer: 1024 * 1024 * 20,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (result.error) {
|
|
74
|
+
result.error.failureReason = 'provider_unavailable';
|
|
75
|
+
throw result.error;
|
|
76
|
+
}
|
|
77
|
+
if (result.status !== 0) {
|
|
78
|
+
const error = new Error(`Checker CLI exited with ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 1000)}`);
|
|
79
|
+
error.failureReason = result.status === 124 ? 'timeout' : 'provider_unavailable';
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rawOutput = providerConfig.output === 'file'
|
|
84
|
+
? readRequiredOutputFile(outputFile)
|
|
85
|
+
: result.stdout;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return {
|
|
89
|
+
...parseCheckerOutput(rawOutput),
|
|
90
|
+
rawOutput,
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
error.failureReason = 'invalid_json';
|
|
94
|
+
error.rawOutput = rawOutput;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseCheckerOutput(rawOutput) {
|
|
100
|
+
const trimmed = rawOutput.trim();
|
|
101
|
+
if (!trimmed) {
|
|
102
|
+
throw new Error('Checker CLI returned empty output.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(trimmed);
|
|
107
|
+
} catch {
|
|
108
|
+
const fenced = /```json\s*([\s\S]*?)```/.exec(trimmed);
|
|
109
|
+
if (fenced) {
|
|
110
|
+
return JSON.parse(fenced[1]);
|
|
111
|
+
}
|
|
112
|
+
const embeddedObject = extractFirstJsonObject(trimmed);
|
|
113
|
+
if (embeddedObject) {
|
|
114
|
+
return JSON.parse(embeddedObject);
|
|
115
|
+
}
|
|
116
|
+
throw new Error('Checker CLI output did not contain a valid JSON object.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractFirstJsonObject(value) {
|
|
121
|
+
const start = value.indexOf('{');
|
|
122
|
+
if (start < 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let depth = 0;
|
|
127
|
+
let inString = false;
|
|
128
|
+
let escaped = false;
|
|
129
|
+
|
|
130
|
+
for (let index = start; index < value.length; index += 1) {
|
|
131
|
+
const char = value[index];
|
|
132
|
+
|
|
133
|
+
if (inString) {
|
|
134
|
+
if (escaped) {
|
|
135
|
+
escaped = false;
|
|
136
|
+
} else if (char === '\\') {
|
|
137
|
+
escaped = true;
|
|
138
|
+
} else if (char === '"') {
|
|
139
|
+
inString = false;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (char === '"') {
|
|
145
|
+
inString = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (char === '{') {
|
|
150
|
+
depth += 1;
|
|
151
|
+
} else if (char === '}') {
|
|
152
|
+
depth -= 1;
|
|
153
|
+
if (depth === 0) {
|
|
154
|
+
return value.slice(start, index + 1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readRequiredOutputFile(outputFile) {
|
|
163
|
+
if (!fs.existsSync(outputFile)) {
|
|
164
|
+
const error = new Error(`Checker CLI did not create output file: ${outputFile}`);
|
|
165
|
+
error.failureReason = 'invalid_json';
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
return fs.readFileSync(outputFile, 'utf8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function expandArg(arg, { model, reasoningEffort, outputFile }) {
|
|
172
|
+
return String(arg)
|
|
173
|
+
.replaceAll('$MODEL', model || '')
|
|
174
|
+
.replaceAll('$REASONING_EFFORT', reasoningEffort || 'medium')
|
|
175
|
+
.replaceAll('$OUTPUT_FILE', outputFile);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isCommandAvailable(command) {
|
|
179
|
+
if (command.includes(path.sep)) {
|
|
180
|
+
return fs.existsSync(command);
|
|
181
|
+
}
|
|
182
|
+
const result = spawnSync('sh', ['-lc', `command -v ${shellQuote(command)}`], {
|
|
183
|
+
encoding: 'utf8',
|
|
184
|
+
});
|
|
185
|
+
return result.status === 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function shellQuote(value) {
|
|
189
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
190
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export async function runOpenAiChecker({
|
|
2
|
+
apiKey,
|
|
3
|
+
model,
|
|
4
|
+
messages,
|
|
5
|
+
temperature = 0,
|
|
6
|
+
}) {
|
|
7
|
+
if (!apiKey) {
|
|
8
|
+
const error = new Error('OPENAI_API_KEY is not set.');
|
|
9
|
+
error.failureReason = 'provider_unavailable';
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
if (!model) {
|
|
13
|
+
const error = new Error('Checker model is not configured.');
|
|
14
|
+
error.failureReason = 'provider_unavailable';
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Bearer ${apiKey}`,
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
model,
|
|
26
|
+
messages,
|
|
27
|
+
temperature,
|
|
28
|
+
response_format: { type: 'json_object' },
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const responseBody = await response.text();
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const error = new Error(`OpenAI checker request failed with HTTP ${response.status}: ${responseBody.slice(0, 500)}`);
|
|
35
|
+
error.failureReason = response.status === 408 || response.status === 429 || response.status >= 500
|
|
36
|
+
? 'provider_unavailable'
|
|
37
|
+
: 'unknown';
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let payload;
|
|
42
|
+
try {
|
|
43
|
+
payload = JSON.parse(responseBody);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
error.failureReason = 'invalid_json';
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const content = payload.choices?.[0]?.message?.content;
|
|
50
|
+
if (!content) {
|
|
51
|
+
const error = new Error('OpenAI checker response did not include message content.');
|
|
52
|
+
error.failureReason = 'invalid_json';
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(content);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
error.failureReason = 'invalid_json';
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
computeTaskContextInputs,
|
|
4
|
+
parseCliArgs,
|
|
5
|
+
readTaskFile,
|
|
6
|
+
resolveTaskDir,
|
|
7
|
+
validateExecutionEvidenceForPlan,
|
|
8
|
+
} from './lib/check-context-utils.mjs';
|
|
9
|
+
|
|
10
|
+
function main() {
|
|
11
|
+
const args = parseCliArgs(process.argv.slice(2));
|
|
12
|
+
const taskArg = args.positional[0];
|
|
13
|
+
if (!taskArg) {
|
|
14
|
+
fail('Usage: node ops/agent-pipeline/bin/quality-gates.mjs <TASK-id-or-task-path> [--json]');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const taskDir = resolveTaskDir(taskArg);
|
|
19
|
+
const taskId = path.basename(taskDir);
|
|
20
|
+
const inputs = computeTaskContextInputs(taskDir);
|
|
21
|
+
const executionIssues = validateExecutionEvidenceForPlan({
|
|
22
|
+
planContent: readTaskFile(taskDir, 'plan.md'),
|
|
23
|
+
executionContent: readTaskFile(taskDir, 'execution.md'),
|
|
24
|
+
});
|
|
25
|
+
const result = {
|
|
26
|
+
taskId,
|
|
27
|
+
riskProfile: inputs.risk.riskProfile,
|
|
28
|
+
riskTriggers: inputs.risk.riskTriggers,
|
|
29
|
+
planQuality: {
|
|
30
|
+
uiRequired: inputs.qualityGates.uiRequired,
|
|
31
|
+
uiAcceptanceComplete: inputs.qualityGates.uiAcceptance.present,
|
|
32
|
+
complexityRequired: inputs.qualityGates.complexityRequired,
|
|
33
|
+
complexityBudgetComplete: inputs.qualityGates.complexityBudget.present,
|
|
34
|
+
optimizationTier: inputs.qualityGates.optimizationTier,
|
|
35
|
+
optimizationRequired: inputs.qualityGates.optimizationRequired,
|
|
36
|
+
optimizationStrategyComplete: inputs.qualityGates.optimizationStrategy.present,
|
|
37
|
+
productionRolloutRequired: inputs.qualityGates.productionRolloutRequired,
|
|
38
|
+
productionRolloutComplete: inputs.qualityGates.productionRollout.present,
|
|
39
|
+
sourceSyncProviderRequired: inputs.qualityGates.sourceSyncProviderRequired,
|
|
40
|
+
sourceSyncProviderComplete: inputs.qualityGates.sourceSyncProvider.present,
|
|
41
|
+
missingSignals: inputs.qualityGates.missingSignals,
|
|
42
|
+
},
|
|
43
|
+
executionEvidence: {
|
|
44
|
+
issues: executionIssues,
|
|
45
|
+
},
|
|
46
|
+
ok: inputs.qualityGates.missingSignals.length === 0 && executionIssues.length === 0,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (args.flags.has('json')) {
|
|
50
|
+
console.log(JSON.stringify(result, null, 2));
|
|
51
|
+
process.exit(result.ok ? 0 : 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printHuman(result);
|
|
55
|
+
process.exit(result.ok ? 0 : 1);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
fail(error.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function printHuman(result) {
|
|
62
|
+
console.log(`Quality gates for ${result.taskId}`);
|
|
63
|
+
console.log(`- riskProfile: ${result.riskProfile}`);
|
|
64
|
+
console.log(`- riskTriggers: ${result.riskTriggers.join(', ') || 'none'}`);
|
|
65
|
+
console.log(`- UI acceptance: ${result.planQuality.uiRequired ? result.planQuality.uiAcceptanceComplete ? 'required / complete' : 'required / missing' : 'not required'}`);
|
|
66
|
+
console.log(`- Complexity budget: ${result.planQuality.complexityRequired ? result.planQuality.complexityBudgetComplete ? 'required / complete' : 'required / missing' : 'not required'}`);
|
|
67
|
+
console.log(`- Optimization strategy: ${result.planQuality.optimizationRequired ? `${result.planQuality.optimizationTier} required / ${result.planQuality.optimizationStrategyComplete ? 'complete' : 'missing'}` : `${result.planQuality.optimizationTier || 'O0'} / not required`}`);
|
|
68
|
+
console.log(`- Production rollout: ${result.planQuality.productionRolloutRequired ? result.planQuality.productionRolloutComplete ? 'required / complete' : 'required / missing' : 'not required'}`);
|
|
69
|
+
console.log(`- Source sync/provider: ${result.planQuality.sourceSyncProviderRequired ? result.planQuality.sourceSyncProviderComplete ? 'required / complete' : 'required / missing' : 'not required'}`);
|
|
70
|
+
if (result.planQuality.missingSignals.length > 0) {
|
|
71
|
+
console.log('Plan quality issues:');
|
|
72
|
+
for (const issue of result.planQuality.missingSignals) {
|
|
73
|
+
console.log(`- ${issue}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (result.executionEvidence.issues.length > 0) {
|
|
77
|
+
console.log('Execution evidence issues:');
|
|
78
|
+
for (const issue of result.executionEvidence.issues) {
|
|
79
|
+
console.log(`- ${issue.category}: ${issue.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (result.ok) {
|
|
83
|
+
console.log('Quality gates ok.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fail(message) {
|
|
88
|
+
console.error(`Error: ${message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main();
|