@the-open-engine/zeroshot 6.0.2 → 6.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 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,268 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+
6
+ const {
7
+ normalizeCommandProofs,
8
+ resolveConfiguredCommandProofs,
9
+ } = require('../../src/command-proofs');
10
+
11
+ function pushCurrentArg(state) {
12
+ if (!state.current) {
13
+ return;
14
+ }
15
+ state.args.push(state.current);
16
+ state.current = '';
17
+ }
18
+
19
+ function consumeEscapedChar(state, char) {
20
+ state.current += char;
21
+ state.escaped = false;
22
+ }
23
+
24
+ function consumeQuotedChar(state, char) {
25
+ if (char === state.quote) {
26
+ state.quote = null;
27
+ return;
28
+ }
29
+ state.current += char;
30
+ }
31
+
32
+ function consumeUnquotedChar(state, char) {
33
+ if (char === '\\') {
34
+ state.escaped = true;
35
+ return;
36
+ }
37
+ if (char === '"' || char === "'") {
38
+ state.quote = char;
39
+ return;
40
+ }
41
+ if (/\s/.test(char)) {
42
+ pushCurrentArg(state);
43
+ return;
44
+ }
45
+ state.current += char;
46
+ }
47
+
48
+ function consumeCommandChar(state, char) {
49
+ if (state.escaped) {
50
+ consumeEscapedChar(state, char);
51
+ return;
52
+ }
53
+ if (state.quote) {
54
+ consumeQuotedChar(state, char);
55
+ return;
56
+ }
57
+ consumeUnquotedChar(state, char);
58
+ }
59
+
60
+ function parseCommandToArgv(command) {
61
+ if (typeof command !== 'string' || command.trim() === '') {
62
+ return [];
63
+ }
64
+
65
+ const state = { args: [], current: '', quote: null, escaped: false };
66
+
67
+ for (const char of command.trim()) {
68
+ consumeCommandChar(state, char);
69
+ }
70
+
71
+ if (state.escaped) {
72
+ state.current += '\\';
73
+ }
74
+ if (state.quote) {
75
+ throw new Error(`Unterminated quote in command: ${command}`);
76
+ }
77
+ pushCurrentArg(state);
78
+ return state.args;
79
+ }
80
+
81
+ function buildCmdproofArgs(mode, proof, paths) {
82
+ const argv = parseCommandToArgv(proof.command);
83
+ if (argv.length === 0) {
84
+ throw new Error(`Command proof ${proof.id} has an empty command`);
85
+ }
86
+
87
+ if (mode === 'prove') {
88
+ return [
89
+ 'prove',
90
+ '--profile',
91
+ proof.profile,
92
+ '--cas',
93
+ paths.cacheDir,
94
+ '--fallback',
95
+ 'run',
96
+ '--signing-key',
97
+ paths.privateKeyPath,
98
+ '--',
99
+ ...argv,
100
+ ];
101
+ }
102
+
103
+ if (mode === 'verify') {
104
+ return [
105
+ 'verify',
106
+ '--profile',
107
+ proof.profile,
108
+ '--cas',
109
+ paths.cacheDir,
110
+ '--trusted-key',
111
+ paths.publicKeyPath,
112
+ '--',
113
+ ...argv,
114
+ ];
115
+ }
116
+
117
+ throw new Error(`Unsupported cmdproof mode: ${mode}`);
118
+ }
119
+
120
+ function parseEnvProofs(env) {
121
+ if (!env.ZEROSHOT_COMMAND_PROOFS) {
122
+ return [];
123
+ }
124
+ try {
125
+ return normalizeCommandProofs(JSON.parse(env.ZEROSHOT_COMMAND_PROOFS));
126
+ } catch (error) {
127
+ throw new Error(`Invalid ZEROSHOT_COMMAND_PROOFS JSON: ${error.message}`);
128
+ }
129
+ }
130
+
131
+ function resolveProofs(env, cwd) {
132
+ const envProofs = parseEnvProofs(env);
133
+ if (envProofs.length > 0) {
134
+ return envProofs;
135
+ }
136
+ return resolveConfiguredCommandProofs({}, { cwd });
137
+ }
138
+
139
+ function resolveProof(id, env, cwd) {
140
+ const proofs = resolveProofs(env, cwd);
141
+ const proof = proofs.find((candidate) => candidate.id === id);
142
+ if (!proof) {
143
+ throw new Error(`Unknown command proof id "${id}"`);
144
+ }
145
+ return proof;
146
+ }
147
+
148
+ function resolvePaths(env) {
149
+ const clusterId = env.ZEROSHOT_CLUSTER_ID || 'local';
150
+ const root = path.join(os.homedir(), '.zeroshot', 'cmdproof', clusterId);
151
+ const cacheDir = env.CMDPROOF_CACHE_DIR || path.join(root, 'cache');
152
+ const keyDir = env.CMDPROOF_KEY_DIR || path.join(root, 'keys');
153
+ return {
154
+ cacheDir,
155
+ keyDir,
156
+ privateKeyPath: path.join(keyDir, 'private-key.json'),
157
+ publicKeyPath: path.join(keyDir, 'public-key.json'),
158
+ };
159
+ }
160
+
161
+ function statusOf(result) {
162
+ if (typeof result.status === 'number') {
163
+ return result.status;
164
+ }
165
+ if (result.error) {
166
+ return 127;
167
+ }
168
+ return 1;
169
+ }
170
+
171
+ function spawnCmdproof(args, options) {
172
+ const result = options.spawnSyncFn('cmdproof', args, {
173
+ cwd: options.cwd,
174
+ env: options.env,
175
+ encoding: 'utf8',
176
+ });
177
+ if (result.error && result.error.code === 'ENOENT') {
178
+ throw new Error('cmdproof binary not found on PATH');
179
+ }
180
+ return result;
181
+ }
182
+
183
+ function ensureKeypair(paths, options) {
184
+ fs.mkdirSync(paths.cacheDir, { recursive: true, mode: 0o700 });
185
+ fs.mkdirSync(paths.keyDir, { recursive: true, mode: 0o700 });
186
+ if (fs.existsSync(paths.privateKeyPath) && fs.existsSync(paths.publicKeyPath)) {
187
+ return;
188
+ }
189
+
190
+ const result = spawnCmdproof(
191
+ [
192
+ 'keygen',
193
+ '--purpose',
194
+ 'trusted-agent',
195
+ '--out',
196
+ paths.privateKeyPath,
197
+ '--public-out',
198
+ paths.publicKeyPath,
199
+ ],
200
+ options
201
+ );
202
+ if (statusOf(result) !== 0) {
203
+ throw new Error(`cmdproof keygen failed with exit code ${statusOf(result)}`);
204
+ }
205
+ }
206
+
207
+ function writeResult(result, stdout, stderr) {
208
+ if (result.stdout) stdout.write(result.stdout);
209
+ if (result.stderr) stderr.write(result.stderr);
210
+ }
211
+
212
+ function parseJsonObject(text) {
213
+ try {
214
+ return JSON.parse(String(text || '').trim());
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ function shouldFallbackFromVerify(result) {
221
+ const parsed = parseJsonObject(result.stdout);
222
+ if (parsed?.status === 'reused_proof') {
223
+ return false;
224
+ }
225
+ return statusOf(result) === 2;
226
+ }
227
+
228
+ function runMode(mode, proof, paths, options) {
229
+ const result = spawnCmdproof(buildCmdproofArgs(mode, proof, paths), options);
230
+ writeResult(result, options.stdout, options.stderr);
231
+ return statusOf(result);
232
+ }
233
+
234
+ function runCmdproof({
235
+ mode,
236
+ id,
237
+ env = process.env,
238
+ cwd = process.cwd(),
239
+ spawnSyncFn = spawnSync,
240
+ stdout = process.stdout,
241
+ stderr = process.stderr,
242
+ }) {
243
+ if (!['prove', 'verify', 'check'].includes(mode)) {
244
+ throw new Error(`Unsupported cmdproof mode: ${mode}`);
245
+ }
246
+
247
+ const proof = resolveProof(id, env, cwd);
248
+ const paths = resolvePaths(env);
249
+ const options = { cwd, env, spawnSyncFn, stdout, stderr };
250
+ ensureKeypair(paths, options);
251
+
252
+ if (mode === 'check') {
253
+ const verifyResult = spawnCmdproof(buildCmdproofArgs('verify', proof, paths), options);
254
+ writeResult(verifyResult, stdout, stderr);
255
+ if (!shouldFallbackFromVerify(verifyResult)) {
256
+ return statusOf(verifyResult);
257
+ }
258
+ return runMode('prove', proof, paths, options);
259
+ }
260
+
261
+ return runMode(mode, proof, paths, options);
262
+ }
263
+
264
+ module.exports = {
265
+ buildCmdproofArgs,
266
+ parseCommandToArgv,
267
+ runCmdproof,
268
+ };
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')
@@ -7,8 +7,9 @@ function commandExists(command) {
7
7
  if (command.includes(path.sep)) {
8
8
  return fs.existsSync(command);
9
9
  }
10
+ const probe = process.platform === 'win32' ? `where ${command}` : `command -v ${command}`;
10
11
  try {
11
- execSync(`command -v ${command}`, { stdio: 'pipe' });
12
+ execSync(probe, { stdio: 'pipe' });
12
13
  return true;
13
14
  } catch {
14
15
  return false;
@@ -20,9 +21,11 @@ function getCommandPath(command) {
20
21
  if (command.includes(path.sep)) {
21
22
  return fs.existsSync(command) ? command : null;
22
23
  }
24
+ const probe = process.platform === 'win32' ? `where ${command}` : `command -v ${command}`;
23
25
  try {
24
- const output = execSync(`command -v ${command}`, { encoding: 'utf8', stdio: 'pipe' });
25
- return output.trim() || null;
26
+ const output = execSync(probe, { encoding: 'utf8', stdio: 'pipe' });
27
+ // `where` can return multiple matches (one per line); take the first.
28
+ return output.split(/\r?\n/)[0].trim() || null;
26
29
  } catch {
27
30
  return null;
28
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.0.2",
3
+ "version": "6.1.0",
4
4
  "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -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
+ };
@@ -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
- // Create isolated context with frozen prototypes
118
- // This prevents prototype pollution attacks
119
- const isolatedContext = {};
120
-
121
- // Freeze Object, Array, Function prototypes in the sandbox
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
- // Create and run in context
142
- vm.createContext(isolatedContext);
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
  }
@@ -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
- const requiredQualityGates = resolveClusterRequiredQualityGates(config, options);
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
  }
@@ -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