@the-open-engine/zeroshot 6.0.3 → 6.2.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 +6 -0
- package/README.md +46 -0
- package/cli/commands/cmdproof.js +442 -0
- package/cli/index.js +20 -0
- package/package.json +1 -1
- package/src/agent/agent-command-proofs-context.js +55 -0
- package/src/agent/agent-context-builder.js +3 -0
- package/src/agent/agent-quality-gate-schema.js +9 -0
- package/src/agent/agent-quality-gates-context.js +5 -0
- package/src/agent/agent-task-executor.js +19 -0
- package/src/command-proofs.js +169 -0
- package/src/logic-engine.js +7 -26
- package/src/orchestrator.js +67 -2
- package/src/quality-gates.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- **logic-engine:** prevent trigger sandbox evaluation from freezing host prototypes while preserving `return`-based logic scripts; reported in [#496](https://github.com/the-open-engine/zeroshot/pull/496).
|
|
6
|
+
|
|
1
7
|
# [5.3.0](https://github.com/covibes/zeroshot/compare/v5.2.1...v5.3.0) (2026-01-12)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -159,6 +159,52 @@ The `id` and optional `scope` are generic. A repo may bind `repo-quality` to any
|
|
|
159
159
|
|
|
160
160
|
The pusher fails closed before commit, push, PR creation, or merge when a configured gate is missing, failing, unavailable, stale, older than `IMPLEMENTATION_READY`, or lacks usable evidence. If no `requiredQualityGates` are configured, Zeroshot preserves its existing validator consensus behavior.
|
|
161
161
|
|
|
162
|
+
## Cmdproof Command Reuse
|
|
163
|
+
|
|
164
|
+
Clusters can make expensive exact commands reusable across workers and validators with `cmdproof`. Zeroshot does not intercept arbitrary shell commands; it gives agents a cluster-scoped helper for configured command proofs.
|
|
165
|
+
|
|
166
|
+
Issue bodies can opt in per run:
|
|
167
|
+
|
|
168
|
+
````markdown
|
|
169
|
+
```zeroshot-command-proofs
|
|
170
|
+
[
|
|
171
|
+
{
|
|
172
|
+
"id": "repo-ci",
|
|
173
|
+
"profile": "ci-equivalent",
|
|
174
|
+
"scope": "repo",
|
|
175
|
+
"description": "Repository local CI-equivalent gate",
|
|
176
|
+
"command": "bash ./scripts/ci/run-local-ci-equivalent.sh"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
```
|
|
180
|
+
````
|
|
181
|
+
|
|
182
|
+
Repos can also configure the same commands in `.zeroshot/settings.json`:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"ship": {
|
|
187
|
+
"commandProofs": [
|
|
188
|
+
{
|
|
189
|
+
"id": "repo-ci",
|
|
190
|
+
"profile": "ci-equivalent",
|
|
191
|
+
"scope": "repo",
|
|
192
|
+
"description": "Repository local CI-equivalent gate",
|
|
193
|
+
"command": "bash ./scripts/ci/run-local-ci-equivalent.sh"
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Agents receive instructions to use:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
zeroshot cmdproof check repo-ci
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The helper uses a cluster-local cache and trusted-agent key under `~/.zeroshot/cmdproof/<cluster-id>/`, runs `cmdproof verify` first, and falls back to `cmdproof prove --fallback run` on misses. In `--ship` flows, configured command proofs are also required handoff quality gates.
|
|
207
|
+
|
|
162
208
|
## When to Use Zeroshot
|
|
163
209
|
|
|
164
210
|
Zeroshot performs best when tasks have clear acceptance criteria.
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
normalizeCommandProofs,
|
|
9
|
+
resolveConfiguredCommandProofs,
|
|
10
|
+
} = require('../../src/command-proofs');
|
|
11
|
+
|
|
12
|
+
function pushCurrentArg(state) {
|
|
13
|
+
if (!state.current) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
state.args.push(state.current);
|
|
17
|
+
state.current = '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function consumeEscapedChar(state, char) {
|
|
21
|
+
state.current += char;
|
|
22
|
+
state.escaped = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function consumeQuotedChar(state, char) {
|
|
26
|
+
if (char === state.quote) {
|
|
27
|
+
state.quote = null;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
state.current += char;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function consumeUnquotedChar(state, char) {
|
|
34
|
+
if (char === '\\') {
|
|
35
|
+
state.escaped = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (char === '"' || char === "'") {
|
|
39
|
+
state.quote = char;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (/\s/.test(char)) {
|
|
43
|
+
pushCurrentArg(state);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
state.current += char;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function consumeCommandChar(state, char) {
|
|
50
|
+
if (state.escaped) {
|
|
51
|
+
consumeEscapedChar(state, char);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (state.quote) {
|
|
55
|
+
consumeQuotedChar(state, char);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
consumeUnquotedChar(state, char);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseCommandToArgv(command) {
|
|
62
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const state = { args: [], current: '', quote: null, escaped: false };
|
|
67
|
+
|
|
68
|
+
for (const char of command.trim()) {
|
|
69
|
+
consumeCommandChar(state, char);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state.escaped) {
|
|
73
|
+
state.current += '\\';
|
|
74
|
+
}
|
|
75
|
+
if (state.quote) {
|
|
76
|
+
throw new Error(`Unterminated quote in command: ${command}`);
|
|
77
|
+
}
|
|
78
|
+
pushCurrentArg(state);
|
|
79
|
+
return state.args;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildCmdproofArgs(mode, proof, paths) {
|
|
83
|
+
const argv = parseCommandToArgv(proof.command);
|
|
84
|
+
if (argv.length === 0) {
|
|
85
|
+
throw new Error(`Command proof ${proof.id} has an empty command`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (mode === 'prove') {
|
|
89
|
+
return [
|
|
90
|
+
'prove',
|
|
91
|
+
'--profile',
|
|
92
|
+
proof.profile,
|
|
93
|
+
'--cas',
|
|
94
|
+
paths.cacheDir,
|
|
95
|
+
'--fallback',
|
|
96
|
+
'run',
|
|
97
|
+
'--signing-key',
|
|
98
|
+
paths.privateKeyPath,
|
|
99
|
+
'--',
|
|
100
|
+
...argv,
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (mode === 'verify') {
|
|
105
|
+
return [
|
|
106
|
+
'verify',
|
|
107
|
+
'--profile',
|
|
108
|
+
proof.profile,
|
|
109
|
+
'--cas',
|
|
110
|
+
paths.cacheDir,
|
|
111
|
+
'--trusted-key',
|
|
112
|
+
paths.publicKeyPath,
|
|
113
|
+
'--',
|
|
114
|
+
...argv,
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw new Error(`Unsupported cmdproof mode: ${mode}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseEnvProofs(env) {
|
|
122
|
+
if (!env.ZEROSHOT_COMMAND_PROOFS) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return normalizeCommandProofs(JSON.parse(env.ZEROSHOT_COMMAND_PROOFS));
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new Error(`Invalid ZEROSHOT_COMMAND_PROOFS JSON: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveProofs(env, cwd) {
|
|
133
|
+
const envProofs = parseEnvProofs(env);
|
|
134
|
+
if (envProofs.length > 0) {
|
|
135
|
+
return envProofs;
|
|
136
|
+
}
|
|
137
|
+
return resolveConfiguredCommandProofs({}, { cwd });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveProof(id, env, cwd) {
|
|
141
|
+
const proofs = resolveProofs(env, cwd);
|
|
142
|
+
const proof = proofs.find((candidate) => candidate.id === id);
|
|
143
|
+
if (!proof) {
|
|
144
|
+
throw new Error(`Unknown command proof id "${id}"`);
|
|
145
|
+
}
|
|
146
|
+
return proof;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolvePaths(env) {
|
|
150
|
+
const clusterId = env.ZEROSHOT_CLUSTER_ID || 'local';
|
|
151
|
+
const root = path.join(os.homedir(), '.zeroshot', 'cmdproof', clusterId);
|
|
152
|
+
const cacheDir = env.CMDPROOF_CACHE_DIR || path.join(root, 'cache');
|
|
153
|
+
const keyDir = env.CMDPROOF_KEY_DIR || path.join(root, 'keys');
|
|
154
|
+
return {
|
|
155
|
+
cacheDir,
|
|
156
|
+
keyDir,
|
|
157
|
+
lockDir: env.ZEROSHOT_CMDPROOF_LOCK_DIR || path.join(root, 'locks'),
|
|
158
|
+
privateKeyPath: path.join(keyDir, 'private-key.json'),
|
|
159
|
+
publicKeyPath: path.join(keyDir, 'public-key.json'),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function statusOf(result) {
|
|
164
|
+
if (typeof result.status === 'number') {
|
|
165
|
+
return result.status;
|
|
166
|
+
}
|
|
167
|
+
if (result.error) {
|
|
168
|
+
return 127;
|
|
169
|
+
}
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function spawnCmdproof(args, options) {
|
|
174
|
+
const result = options.spawnSyncFn('cmdproof', args, {
|
|
175
|
+
cwd: options.cwd,
|
|
176
|
+
env: options.env,
|
|
177
|
+
encoding: 'utf8',
|
|
178
|
+
});
|
|
179
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
180
|
+
throw new Error('cmdproof binary not found on PATH');
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parsePositiveInt(value, fallback) {
|
|
186
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
187
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sleepMs(ms) {
|
|
191
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function proofLockName(proof, verifyReport = null) {
|
|
195
|
+
const actionKey = getVerifyActionKey(verifyReport);
|
|
196
|
+
const digest = crypto
|
|
197
|
+
.createHash('sha256')
|
|
198
|
+
.update(`${proof.id}\0${proof.profile}\0${proof.command}\0${actionKey}`)
|
|
199
|
+
.digest('hex')
|
|
200
|
+
.slice(0, 16);
|
|
201
|
+
const safeId = proof.id.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
202
|
+
return `${safeId}-${digest}.lock`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getVerifyActionKey(verifyReport) {
|
|
206
|
+
return verifyReport?.actionKey || verifyReport?.action_key || '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function proofLockPath(proof, paths, verifyReport = null) {
|
|
210
|
+
return path.join(paths.lockDir, proofLockName(proof, verifyReport));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function removeLock(lockPath) {
|
|
214
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function lockOwnerState(lockPath) {
|
|
218
|
+
try {
|
|
219
|
+
const metadata = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
220
|
+
if (!Number.isInteger(metadata.pid) || metadata.pid <= 0) {
|
|
221
|
+
return 'unknown';
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
process.kill(metadata.pid, 0);
|
|
225
|
+
return 'alive';
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return error?.code === 'ESRCH' ? 'gone' : 'unknown';
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
return 'unknown';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function lockIsStale(lockPath, staleMs) {
|
|
235
|
+
try {
|
|
236
|
+
const stat = fs.statSync(lockPath);
|
|
237
|
+
const ownerState = lockOwnerState(lockPath);
|
|
238
|
+
if (ownerState === 'alive') {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (ownerState === 'gone') {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return Date.now() - stat.mtimeMs > staleMs;
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function writeLockMetadata(lockPath, proof) {
|
|
251
|
+
const metadata = {
|
|
252
|
+
pid: process.pid,
|
|
253
|
+
proofId: proof.id,
|
|
254
|
+
profile: proof.profile,
|
|
255
|
+
command: proof.command,
|
|
256
|
+
createdAt: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
fs.writeFileSync(path.join(lockPath, 'owner.json'), `${JSON.stringify(metadata, null, 2)}\n`, {
|
|
259
|
+
mode: 0o600,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function tryAcquireProofLock(proof, paths, env, verifyReport = null) {
|
|
264
|
+
const lockPath = proofLockPath(proof, paths, verifyReport);
|
|
265
|
+
const staleMs = parsePositiveInt(env.ZEROSHOT_CMDPROOF_LOCK_STALE_MS, 6 * 60 * 60 * 1000);
|
|
266
|
+
fs.mkdirSync(paths.lockDir, { recursive: true, mode: 0o700 });
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
fs.mkdirSync(lockPath, { mode: 0o700 });
|
|
270
|
+
writeLockMetadata(lockPath, proof);
|
|
271
|
+
return { acquired: true, lockPath };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error?.code !== 'EEXIST') {
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
if (!lockIsStale(lockPath, staleMs)) {
|
|
277
|
+
return { acquired: false, lockPath };
|
|
278
|
+
}
|
|
279
|
+
removeLock(lockPath);
|
|
280
|
+
try {
|
|
281
|
+
fs.mkdirSync(lockPath, { mode: 0o700 });
|
|
282
|
+
writeLockMetadata(lockPath, proof);
|
|
283
|
+
return { acquired: true, lockPath };
|
|
284
|
+
} catch (retryError) {
|
|
285
|
+
if (retryError?.code === 'EEXIST') {
|
|
286
|
+
return { acquired: false, lockPath };
|
|
287
|
+
}
|
|
288
|
+
throw retryError;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function ensureKeypair(paths, options) {
|
|
294
|
+
fs.mkdirSync(paths.cacheDir, { recursive: true, mode: 0o700 });
|
|
295
|
+
fs.mkdirSync(paths.keyDir, { recursive: true, mode: 0o700 });
|
|
296
|
+
if (fs.existsSync(paths.privateKeyPath) && fs.existsSync(paths.publicKeyPath)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const result = spawnCmdproof(
|
|
301
|
+
[
|
|
302
|
+
'keygen',
|
|
303
|
+
'--purpose',
|
|
304
|
+
'trusted-agent',
|
|
305
|
+
'--out',
|
|
306
|
+
paths.privateKeyPath,
|
|
307
|
+
'--public-out',
|
|
308
|
+
paths.publicKeyPath,
|
|
309
|
+
],
|
|
310
|
+
options
|
|
311
|
+
);
|
|
312
|
+
if (statusOf(result) !== 0) {
|
|
313
|
+
throw new Error(`cmdproof keygen failed with exit code ${statusOf(result)}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function writeResult(result, stdout, stderr) {
|
|
318
|
+
if (result.stdout) stdout.write(result.stdout);
|
|
319
|
+
if (result.stderr) stderr.write(result.stderr);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseJsonObject(text) {
|
|
323
|
+
try {
|
|
324
|
+
return JSON.parse(String(text || '').trim());
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function shouldFallbackFromVerify(result) {
|
|
331
|
+
const parsed = parseJsonObject(result.stdout);
|
|
332
|
+
if (parsed?.status === 'reused_proof') {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
return statusOf(result) === 2;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function runMode(mode, proof, paths, options) {
|
|
339
|
+
const result = spawnCmdproof(buildCmdproofArgs(mode, proof, paths), options);
|
|
340
|
+
writeResult(result, options.stdout, options.stderr);
|
|
341
|
+
return statusOf(result);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function runProveWithLock(proof, paths, options, lockPath) {
|
|
345
|
+
try {
|
|
346
|
+
return runMode('prove', proof, paths, options);
|
|
347
|
+
} finally {
|
|
348
|
+
removeLock(lockPath);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function verifyProof(proof, paths, options, { write = true } = {}) {
|
|
353
|
+
const result = spawnCmdproof(buildCmdproofArgs('verify', proof, paths), options);
|
|
354
|
+
if (write) {
|
|
355
|
+
writeResult(result, options.stdout, options.stderr);
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function waitForProofOrProve(proof, paths, options, verifyReport) {
|
|
361
|
+
const startedAt = Date.now();
|
|
362
|
+
const waitMs = parsePositiveInt(options.env.ZEROSHOT_CMDPROOF_WAIT_MS, 30 * 60 * 1000);
|
|
363
|
+
const pollMs = parsePositiveInt(options.env.ZEROSHOT_CMDPROOF_POLL_MS, 2500);
|
|
364
|
+
|
|
365
|
+
while (Date.now() - startedAt < waitMs) {
|
|
366
|
+
sleepMs(pollMs);
|
|
367
|
+
const verifyResult = verifyProof(proof, paths, options, { write: false });
|
|
368
|
+
const latestReport = parseJsonObject(verifyResult.stdout) || verifyReport;
|
|
369
|
+
if (!shouldFallbackFromVerify(verifyResult)) {
|
|
370
|
+
writeResult(verifyResult, options.stdout, options.stderr);
|
|
371
|
+
return statusOf(verifyResult);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const lockReport = getVerifyActionKey(latestReport) ? latestReport : verifyReport;
|
|
375
|
+
const lock = tryAcquireProofLock(proof, paths, options.env, lockReport);
|
|
376
|
+
if (lock.acquired) {
|
|
377
|
+
options.stderr.write(
|
|
378
|
+
`zeroshot cmdproof check ${proof.id}: no reusable proof appeared; acquired proof lock after wait.\n`
|
|
379
|
+
);
|
|
380
|
+
return runProveWithLock(proof, paths, options, lock.lockPath);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
options.stderr.write(
|
|
385
|
+
`zeroshot cmdproof check ${proof.id}: timed out waiting ${waitMs}ms for in-flight proof; leaving miss for caller.\n`
|
|
386
|
+
);
|
|
387
|
+
const finalVerify = verifyProof(proof, paths, options, { write: true });
|
|
388
|
+
return statusOf(finalVerify);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function runCheck(proof, paths, options) {
|
|
392
|
+
const verifyResult = verifyProof(proof, paths, options);
|
|
393
|
+
if (!shouldFallbackFromVerify(verifyResult)) {
|
|
394
|
+
return statusOf(verifyResult);
|
|
395
|
+
}
|
|
396
|
+
const verifyReport = parseJsonObject(verifyResult.stdout);
|
|
397
|
+
if (!getVerifyActionKey(verifyReport)) {
|
|
398
|
+
return runMode('prove', proof, paths, options);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const lock = tryAcquireProofLock(proof, paths, options.env, verifyReport);
|
|
402
|
+
if (lock.acquired) {
|
|
403
|
+
return runProveWithLock(proof, paths, options, lock.lockPath);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
options.stderr.write(
|
|
407
|
+
`zeroshot cmdproof check ${proof.id}: proof miss; waiting for in-flight proof from another agent.\n`
|
|
408
|
+
);
|
|
409
|
+
return waitForProofOrProve(proof, paths, options, verifyReport);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function runCmdproof({
|
|
413
|
+
mode,
|
|
414
|
+
id,
|
|
415
|
+
env = process.env,
|
|
416
|
+
cwd = process.cwd(),
|
|
417
|
+
spawnSyncFn = spawnSync,
|
|
418
|
+
stdout = process.stdout,
|
|
419
|
+
stderr = process.stderr,
|
|
420
|
+
}) {
|
|
421
|
+
if (!['prove', 'verify', 'check'].includes(mode)) {
|
|
422
|
+
throw new Error(`Unsupported cmdproof mode: ${mode}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const proof = resolveProof(id, env, cwd);
|
|
426
|
+
const paths = resolvePaths(env);
|
|
427
|
+
const options = { cwd, env, spawnSyncFn, stdout, stderr };
|
|
428
|
+
ensureKeypair(paths, options);
|
|
429
|
+
|
|
430
|
+
if (mode === 'check') {
|
|
431
|
+
return runCheck(proof, paths, options);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return runMode(mode, proof, paths, options);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
buildCmdproofArgs,
|
|
439
|
+
parseCommandToArgv,
|
|
440
|
+
runCmdproof,
|
|
441
|
+
proofLockName,
|
|
442
|
+
};
|
package/cli/index.js
CHANGED
|
@@ -62,6 +62,7 @@ const {
|
|
|
62
62
|
const { requirePreflight } = require('../src/preflight');
|
|
63
63
|
const { providersCommand, setDefaultCommand, setupCommand } = require('./commands/providers');
|
|
64
64
|
const { runInspectCommand } = require('./commands/inspect');
|
|
65
|
+
const { runCmdproof } = require('./commands/cmdproof');
|
|
65
66
|
const {
|
|
66
67
|
markDetachedSetupFailed,
|
|
67
68
|
registerDetachedSetupCluster,
|
|
@@ -2540,6 +2541,25 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
|
|
|
2540
2541
|
// Task run - single-agent background task
|
|
2541
2542
|
const taskCmd = program.command('task').description('Single-agent task management');
|
|
2542
2543
|
|
|
2544
|
+
const cmdproofCmd = program
|
|
2545
|
+
.command('cmdproof')
|
|
2546
|
+
.description('Run configured cmdproof command proofs');
|
|
2547
|
+
|
|
2548
|
+
for (const mode of ['prove', 'verify', 'check']) {
|
|
2549
|
+
cmdproofCmd
|
|
2550
|
+
.command(`${mode} <id>`)
|
|
2551
|
+
.description(`${mode} a configured command proof`)
|
|
2552
|
+
.action((id) => {
|
|
2553
|
+
try {
|
|
2554
|
+
const exitCode = runCmdproof({ mode, id });
|
|
2555
|
+
process.exit(exitCode);
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
console.error('Error:', error.message);
|
|
2558
|
+
process.exit(1);
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2543
2563
|
taskCmd
|
|
2544
2564
|
.command('run <prompt>')
|
|
2545
2565
|
.description('Run a single-agent background task')
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { normalizeCommandProofs } = require('../command-proofs');
|
|
2
|
+
|
|
3
|
+
function appendProofDetails(lines, proof, index) {
|
|
4
|
+
const scope = proof.scope ? `, scope: ${proof.scope}` : '';
|
|
5
|
+
lines.push(`${index + 1}. id: ${proof.id}, profile: ${proof.profile}${scope}`);
|
|
6
|
+
if (proof.description) {
|
|
7
|
+
lines.push(` description: ${proof.description}`);
|
|
8
|
+
}
|
|
9
|
+
lines.push(` command: ${proof.command}`);
|
|
10
|
+
lines.push(` helper: zeroshot cmdproof check ${proof.id}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildWorkerInstructions(lines) {
|
|
14
|
+
lines.push(
|
|
15
|
+
'',
|
|
16
|
+
'For these exact commands:',
|
|
17
|
+
'- Run `zeroshot cmdproof check <id>` instead of the raw command.',
|
|
18
|
+
'- Treat the helper exit code as the command exit code.',
|
|
19
|
+
'- If you need to mention evidence, include the helper output and the configured command id.',
|
|
20
|
+
''
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildValidatorInstructions(lines) {
|
|
25
|
+
lines.push(
|
|
26
|
+
'',
|
|
27
|
+
'For proof-backed validation:',
|
|
28
|
+
'- Run `zeroshot cmdproof check <id>` before considering the raw command.',
|
|
29
|
+
'- Use the helper output as quality-gate evidence.',
|
|
30
|
+
'- Only run the raw command directly if the helper itself is unavailable.',
|
|
31
|
+
''
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildCommandProofsSection(config) {
|
|
36
|
+
const proofs = normalizeCommandProofs(config.commandProofs);
|
|
37
|
+
if (proofs.length === 0) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines = ['## Reusable Command Proofs', '', 'Configured proof-backed commands:'];
|
|
42
|
+
proofs.forEach((proof, index) => appendProofDetails(lines, proof, index));
|
|
43
|
+
|
|
44
|
+
if (config.role === 'validator') {
|
|
45
|
+
buildValidatorInstructions(lines);
|
|
46
|
+
} else {
|
|
47
|
+
buildWorkerInstructions(lines);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
buildCommandProofsSection,
|
|
55
|
+
};
|
|
@@ -30,6 +30,7 @@ const {
|
|
|
30
30
|
buildValidatorSkipSection,
|
|
31
31
|
} = require('./agent-context-sections');
|
|
32
32
|
const { buildRequiredQualityGatesSection } = require('./agent-quality-gates-context');
|
|
33
|
+
const { buildCommandProofsSection } = require('./agent-command-proofs-context');
|
|
33
34
|
|
|
34
35
|
function pushStaticPack({ packs, packId, section, text, order, options = {} }) {
|
|
35
36
|
if (!text) {
|
|
@@ -82,6 +83,7 @@ function buildStaticSections(params) {
|
|
|
82
83
|
header: buildHeaderContext({ id, role, iteration, isIsolated }),
|
|
83
84
|
instructions: buildInstructionsSection({ config, selectedPrompt, id }),
|
|
84
85
|
repoTooling: buildRepoToolingSection({ config, worktree }),
|
|
86
|
+
commandProofs: buildCommandProofsSection(config),
|
|
85
87
|
legacyOutputSchema: buildLegacyOutputSchemaSection(config),
|
|
86
88
|
queuedGuidance: queuedGuidance || '',
|
|
87
89
|
requiredQualityGates: buildRequiredQualityGatesSection(config),
|
|
@@ -100,6 +102,7 @@ function buildPacks(params) {
|
|
|
100
102
|
'header',
|
|
101
103
|
'instructions',
|
|
102
104
|
'repoTooling',
|
|
105
|
+
'commandProofs',
|
|
103
106
|
'queuedGuidance',
|
|
104
107
|
'legacyOutputSchema',
|
|
105
108
|
'requiredQualityGates',
|
|
@@ -28,6 +28,15 @@ function buildQualityGateSchema() {
|
|
|
28
28
|
command: { type: 'string' },
|
|
29
29
|
exitCode: { type: 'integer' },
|
|
30
30
|
output: { type: 'string' },
|
|
31
|
+
proof: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
description: 'Optional command proof metadata when the gate used cmdproof.',
|
|
34
|
+
properties: {
|
|
35
|
+
profile: { type: 'string' },
|
|
36
|
+
reused: { type: 'boolean' },
|
|
37
|
+
status: { type: 'string' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
31
40
|
},
|
|
32
41
|
required: ['command', 'exitCode', 'output'],
|
|
33
42
|
},
|
|
@@ -8,6 +8,10 @@ function appendGateDetails(lines, gate, index) {
|
|
|
8
8
|
if (gate.command) {
|
|
9
9
|
lines.push(` command: ${gate.command}`);
|
|
10
10
|
}
|
|
11
|
+
if (gate.profile || gate.proofProfile) {
|
|
12
|
+
lines.push(` cmdproof profile: ${gate.profile || gate.proofProfile}`);
|
|
13
|
+
lines.push(` cmdproof helper: zeroshot cmdproof check ${gate.id}`);
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function buildRequiredQualityGatesSection(config) {
|
|
@@ -35,6 +39,7 @@ function buildRequiredQualityGatesSection(config) {
|
|
|
35
39
|
'',
|
|
36
40
|
'For each configured gate:',
|
|
37
41
|
'- Run the configured command when one is provided by repo or cluster config.',
|
|
42
|
+
'- For gates with a cmdproof profile, run `zeroshot cmdproof check <id>` before considering a raw command.',
|
|
38
43
|
'- Put the command, numeric exit code, and string output in `evidence`.',
|
|
39
44
|
'- Set `status` to `PASS` only when the gate completes successfully.',
|
|
40
45
|
'- If a required gate fails, set `approved` to false and publish status `FAIL`.',
|
|
@@ -780,6 +780,25 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
|
780
780
|
const { claudeConfigDir = null } = options;
|
|
781
781
|
const spawnEnv = { ...process.env };
|
|
782
782
|
const agentCwd = agent.config?.cwd || agent.worktree?.path || process.cwd();
|
|
783
|
+
const clusterId = agent.cluster?.id || agent.cluster_id || process.env.ZEROSHOT_CLUSTER_ID;
|
|
784
|
+
|
|
785
|
+
if (clusterId) {
|
|
786
|
+
spawnEnv.ZEROSHOT_CLUSTER_ID = clusterId;
|
|
787
|
+
const cmdproofRoot = path.join(os.homedir(), '.zeroshot', 'cmdproof', clusterId);
|
|
788
|
+
if (!spawnEnv.CMDPROOF_CACHE_DIR) {
|
|
789
|
+
spawnEnv.CMDPROOF_CACHE_DIR = path.join(cmdproofRoot, 'cache');
|
|
790
|
+
}
|
|
791
|
+
if (!spawnEnv.CMDPROOF_KEY_DIR) {
|
|
792
|
+
spawnEnv.CMDPROOF_KEY_DIR = path.join(cmdproofRoot, 'keys');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const commandProofs = Array.isArray(agent.config?.commandProofs)
|
|
797
|
+
? agent.config.commandProofs
|
|
798
|
+
: agent.cluster?.commandProofs || [];
|
|
799
|
+
if (commandProofs.length > 0) {
|
|
800
|
+
spawnEnv.ZEROSHOT_COMMAND_PROOFS = JSON.stringify(commandProofs);
|
|
801
|
+
}
|
|
783
802
|
|
|
784
803
|
if (providerName === 'claude') {
|
|
785
804
|
Object.assign(spawnEnv, buildClaudeEnv(modelSpec));
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const { readRepoSettings } = require('../lib/repo-settings');
|
|
2
|
+
|
|
3
|
+
const PROOF_BLOCK_PATTERN = /^```zeroshot-command-proofs\s*\n([\s\S]*?)^```/m;
|
|
4
|
+
|
|
5
|
+
function hasOwn(value, key) {
|
|
6
|
+
return Object.prototype.hasOwnProperty.call(value || {}, key);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function trimString(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setOptionalString(target, source, key) {
|
|
14
|
+
const value = trimString(source[key]);
|
|
15
|
+
if (value) {
|
|
16
|
+
target[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeCommandProof(proof) {
|
|
21
|
+
if (!proof || typeof proof !== 'object') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const id = trimString(proof.id || proof.name);
|
|
26
|
+
const profile = trimString(proof.profile || proof.proofProfile);
|
|
27
|
+
const command = trimString(proof.command);
|
|
28
|
+
if (!id || !profile || !command) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalized = { id, profile, command };
|
|
33
|
+
setOptionalString(normalized, proof, 'scope');
|
|
34
|
+
setOptionalString(normalized, proof, 'description');
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeCommandProofs(value) {
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return value.map(normalizeCommandProof).filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseCommandProofsFromText(text) {
|
|
46
|
+
if (typeof text !== 'string' || text.trim() === '') {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const match = text.match(PROOF_BLOCK_PATTERN);
|
|
51
|
+
if (!match) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(match[1]);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`Invalid zeroshot-command-proofs JSON: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return normalizeCommandProofs(parsed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mergeCommandProofs(...sources) {
|
|
66
|
+
const byId = new Map();
|
|
67
|
+
const order = [];
|
|
68
|
+
|
|
69
|
+
for (const source of sources) {
|
|
70
|
+
for (const proof of normalizeCommandProofs(source)) {
|
|
71
|
+
if (!byId.has(proof.id)) {
|
|
72
|
+
order.push(proof.id);
|
|
73
|
+
}
|
|
74
|
+
byId.set(proof.id, proof);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return order.map((id) => byId.get(id));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function commandProofToQualityGate(proof) {
|
|
82
|
+
const normalized = normalizeCommandProof(proof);
|
|
83
|
+
if (!normalized) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const gate = {
|
|
88
|
+
id: normalized.id,
|
|
89
|
+
profile: normalized.profile,
|
|
90
|
+
command: normalized.command,
|
|
91
|
+
commandProof: true,
|
|
92
|
+
};
|
|
93
|
+
setOptionalString(gate, normalized, 'scope');
|
|
94
|
+
setOptionalString(gate, normalized, 'description');
|
|
95
|
+
return gate;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getCommandProofSource(options, repoSettings) {
|
|
99
|
+
if (hasOwn(options, 'commandProofs')) {
|
|
100
|
+
return options.commandProofs;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (options.ship && typeof options.ship === 'object' && hasOwn(options.ship, 'commandProofs')) {
|
|
104
|
+
return options.ship.commandProofs;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const settingsShip = repoSettings?.ship;
|
|
108
|
+
if (settingsShip && typeof settingsShip === 'object' && hasOwn(settingsShip, 'commandProofs')) {
|
|
109
|
+
return settingsShip.commandProofs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (hasOwn(repoSettings, 'commandProofs')) {
|
|
113
|
+
return repoSettings.commandProofs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getClusterCommandProofSource(config, options) {
|
|
120
|
+
if (hasOwn(options, 'commandProofs')) {
|
|
121
|
+
return options.commandProofs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (options.ship && typeof options.ship === 'object' && hasOwn(options.ship, 'commandProofs')) {
|
|
125
|
+
return options.ship.commandProofs;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (config?.ship && typeof config.ship === 'object' && hasOwn(config.ship, 'commandProofs')) {
|
|
129
|
+
return config.ship.commandProofs;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (hasOwn(config, 'commandProofs')) {
|
|
133
|
+
return config.commandProofs;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveConfiguredCommandProofs(config = {}, options = {}) {
|
|
140
|
+
const configuredSource = getClusterCommandProofSource(config, options);
|
|
141
|
+
if (configuredSource !== undefined) {
|
|
142
|
+
return normalizeCommandProofs(configuredSource);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const repoSettingsResult = readRepoSettings(options.cwd || process.cwd());
|
|
146
|
+
const repoSettings = repoSettingsResult.settings || {};
|
|
147
|
+
return normalizeCommandProofs(getCommandProofSource(options, repoSettings));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveClusterCommandProofs(config = {}, options = {}, inputText = '') {
|
|
151
|
+
return mergeCommandProofs(
|
|
152
|
+
resolveConfiguredCommandProofs(config, options),
|
|
153
|
+
parseCommandProofsFromText(inputText)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function commandProofsToQualityGates(commandProofs) {
|
|
158
|
+
return normalizeCommandProofs(commandProofs).map(commandProofToQualityGate).filter(Boolean);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
commandProofToQualityGate,
|
|
163
|
+
commandProofsToQualityGates,
|
|
164
|
+
mergeCommandProofs,
|
|
165
|
+
normalizeCommandProofs,
|
|
166
|
+
parseCommandProofsFromText,
|
|
167
|
+
resolveClusterCommandProofs,
|
|
168
|
+
resolveConfiguredCommandProofs,
|
|
169
|
+
};
|
package/src/logic-engine.js
CHANGED
|
@@ -82,10 +82,6 @@ function buildAgentContext(agent) {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function getSafeBuiltins() {
|
|
86
|
-
return { Set, Map, Array, Object, String, Number, Boolean, Math, Date, JSON };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
85
|
function getQuietConsole() {
|
|
90
86
|
return {
|
|
91
87
|
log: () => {},
|
|
@@ -114,33 +110,19 @@ class LogicEngine {
|
|
|
114
110
|
// Build sandbox context
|
|
115
111
|
const context = this._buildContext(agent, message);
|
|
116
112
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
isolatedContext.Object = Object.freeze({ ...Object });
|
|
123
|
-
isolatedContext.Array = Array;
|
|
124
|
-
isolatedContext.Function = Function;
|
|
113
|
+
// Contextify before execution so scripts use VM-owned globals instead of
|
|
114
|
+
// host constructors. Keep the function-body contract: trigger scripts use
|
|
115
|
+
// return statements throughout built-in templates and user configs.
|
|
116
|
+
const sandbox = { ...context };
|
|
117
|
+
vm.createContext(sandbox);
|
|
125
118
|
|
|
126
|
-
// Copy safe context properties
|
|
127
|
-
Object.assign(isolatedContext, context);
|
|
128
|
-
|
|
129
|
-
// Wrap script to prevent prototype access
|
|
130
119
|
const wrappedScript = `(function() {
|
|
131
120
|
'use strict';
|
|
132
|
-
// Prevent prototype pollution
|
|
133
|
-
const frozenObject = Object;
|
|
134
|
-
const frozenArray = Array;
|
|
135
|
-
Object.freeze(frozenObject.prototype);
|
|
136
|
-
Object.freeze(frozenArray.prototype);
|
|
137
|
-
|
|
138
121
|
${script}
|
|
139
122
|
})()`;
|
|
140
123
|
|
|
141
|
-
//
|
|
142
|
-
vm.
|
|
143
|
-
const result = vm.runInContext(wrappedScript, isolatedContext, {
|
|
124
|
+
// Run in context
|
|
125
|
+
const result = vm.runInContext(wrappedScript, sandbox, {
|
|
144
126
|
timeout: this.timeout,
|
|
145
127
|
displayErrors: true,
|
|
146
128
|
});
|
|
@@ -168,7 +150,6 @@ class LogicEngine {
|
|
|
168
150
|
ledger: ledgerAPI,
|
|
169
151
|
cluster: buildClusterAPI(this.cluster, clusterId),
|
|
170
152
|
helpers: buildHelpers(ledgerAPI),
|
|
171
|
-
...getSafeBuiltins(),
|
|
172
153
|
console: getQuietConsole(),
|
|
173
154
|
};
|
|
174
155
|
}
|
package/src/orchestrator.js
CHANGED
|
@@ -49,6 +49,11 @@ const { normalizeProviderName } = require('../lib/provider-names');
|
|
|
49
49
|
const { getProvider } = require('./providers');
|
|
50
50
|
const StateSnapshotter = require('./state-snapshotter');
|
|
51
51
|
const { resolveClusterRequiredQualityGates } = require('./quality-gates');
|
|
52
|
+
const {
|
|
53
|
+
commandProofsToQualityGates,
|
|
54
|
+
mergeCommandProofs,
|
|
55
|
+
resolveClusterCommandProofs,
|
|
56
|
+
} = require('./command-proofs');
|
|
52
57
|
const crypto = require('crypto');
|
|
53
58
|
|
|
54
59
|
function applyModelOverride(agentConfig, modelOverride) {
|
|
@@ -118,6 +123,46 @@ function applyRequiredQualityGatesToValidators(config, requiredQualityGates) {
|
|
|
118
123
|
}
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
function applyCommandProofsToAgent(agentConfig, commandProofs) {
|
|
127
|
+
if (!Array.isArray(commandProofs) || commandProofs.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
agentConfig.commandProofs = mergeCommandProofs(agentConfig.commandProofs, commandProofs);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function applyCommandProofsToAgents(config, commandProofs) {
|
|
135
|
+
if (!Array.isArray(commandProofs) || commandProofs.length === 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const agentConfig of config.agents || []) {
|
|
140
|
+
applyCommandProofsToAgent(agentConfig, commandProofs);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mergeQualityGates(...sources) {
|
|
145
|
+
const byId = new Map();
|
|
146
|
+
const order = [];
|
|
147
|
+
|
|
148
|
+
for (const source of sources) {
|
|
149
|
+
if (!Array.isArray(source)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const gate of source) {
|
|
153
|
+
if (!gate?.id) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!byId.has(gate.id)) {
|
|
157
|
+
order.push(gate.id);
|
|
158
|
+
}
|
|
159
|
+
byId.set(gate.id, gate);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return order.map((id) => byId.get(id));
|
|
164
|
+
}
|
|
165
|
+
|
|
121
166
|
function getTriggerTopic(trigger) {
|
|
122
167
|
return typeof trigger === 'string' ? trigger : trigger?.topic;
|
|
123
168
|
}
|
|
@@ -521,6 +566,7 @@ class Orchestrator {
|
|
|
521
566
|
isolation,
|
|
522
567
|
autoPr: clusterData.autoPr || false,
|
|
523
568
|
prOptions: clusterData.prOptions || null,
|
|
569
|
+
commandProofs: clusterData.commandProofs || [],
|
|
524
570
|
issue: clusterData.issue || null,
|
|
525
571
|
};
|
|
526
572
|
|
|
@@ -801,6 +847,8 @@ class Orchestrator {
|
|
|
801
847
|
autoPr: cluster.autoPr || false,
|
|
802
848
|
// Persist PR options for resume
|
|
803
849
|
prOptions: cluster.prOptions || null,
|
|
850
|
+
// Persist cluster-scoped command proof configuration for resume and dynamic agents
|
|
851
|
+
commandProofs: cluster.commandProofs || [],
|
|
804
852
|
// Persist model override for consistent agent spawning on resume
|
|
805
853
|
modelOverride: cluster.modelOverride || null,
|
|
806
854
|
// Persist issue number for heroshot/external tools
|
|
@@ -1007,9 +1055,9 @@ class Orchestrator {
|
|
|
1007
1055
|
config,
|
|
1008
1056
|
clusterId
|
|
1009
1057
|
);
|
|
1010
|
-
|
|
1058
|
+
let requiredQualityGates = resolveClusterRequiredQualityGates(config, options);
|
|
1059
|
+
let commandProofs = resolveClusterCommandProofs(config, options);
|
|
1011
1060
|
options.requiredQualityGates = requiredQualityGates;
|
|
1012
|
-
applyRequiredQualityGatesToValidators(config, requiredQualityGates);
|
|
1013
1061
|
|
|
1014
1062
|
// Build cluster object
|
|
1015
1063
|
// CRITICAL: initComplete promise ensures ISSUE_OPENED is published before stop() completes
|
|
@@ -1034,6 +1082,7 @@ class Orchestrator {
|
|
|
1034
1082
|
_resolveInitComplete: resolveInitComplete,
|
|
1035
1083
|
autoPr: options.autoPr || false,
|
|
1036
1084
|
requiredQualityGates,
|
|
1085
|
+
commandProofs,
|
|
1037
1086
|
// PR configuration options (persisted for resume)
|
|
1038
1087
|
prOptions: buildPrOptions(options, requiredQualityGates),
|
|
1039
1088
|
// Model override for all agents (applied to dynamically added agents)
|
|
@@ -1134,6 +1183,21 @@ class Orchestrator {
|
|
|
1134
1183
|
throw new Error('Either issue, file, or text input is required');
|
|
1135
1184
|
}
|
|
1136
1185
|
|
|
1186
|
+
commandProofs = mergeCommandProofs(
|
|
1187
|
+
commandProofs,
|
|
1188
|
+
resolveClusterCommandProofs(config, options, inputData.context)
|
|
1189
|
+
);
|
|
1190
|
+
requiredQualityGates = mergeQualityGates(
|
|
1191
|
+
requiredQualityGates,
|
|
1192
|
+
commandProofsToQualityGates(commandProofs)
|
|
1193
|
+
);
|
|
1194
|
+
options.requiredQualityGates = requiredQualityGates;
|
|
1195
|
+
cluster.requiredQualityGates = requiredQualityGates;
|
|
1196
|
+
cluster.commandProofs = commandProofs;
|
|
1197
|
+
cluster.prOptions = buildPrOptions(options, requiredQualityGates);
|
|
1198
|
+
applyCommandProofsToAgents(config, commandProofs);
|
|
1199
|
+
applyRequiredQualityGatesToValidators(config, requiredQualityGates);
|
|
1200
|
+
|
|
1137
1201
|
// Detect git platform for --pr mode (independent of issue provider)
|
|
1138
1202
|
if (options.autoPr) {
|
|
1139
1203
|
const { detectGitContext } = require('../lib/git-remote-utils');
|
|
@@ -3188,6 +3252,7 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
3188
3252
|
}
|
|
3189
3253
|
|
|
3190
3254
|
applyRequiredQualityGatesToAgent(agentConfig, cluster.requiredQualityGates);
|
|
3255
|
+
applyCommandProofsToAgent(agentConfig, cluster.commandProofs);
|
|
3191
3256
|
if (cluster.autoPr) {
|
|
3192
3257
|
applyPushBlockedRepairTrigger(agentConfig);
|
|
3193
3258
|
}
|
package/src/quality-gates.js
CHANGED
|
@@ -45,6 +45,11 @@ function normalizeObjectGate(gate) {
|
|
|
45
45
|
setOptionalString(normalized, gate, 'scope');
|
|
46
46
|
setOptionalString(normalized, gate, 'description');
|
|
47
47
|
setOptionalString(normalized, gate, 'command');
|
|
48
|
+
setOptionalString(normalized, gate, 'profile');
|
|
49
|
+
setOptionalString(normalized, gate, 'proofProfile');
|
|
50
|
+
if (gate.commandProof === true) {
|
|
51
|
+
normalized.commandProof = true;
|
|
52
|
+
}
|
|
48
53
|
return normalized;
|
|
49
54
|
}
|
|
50
55
|
|