@covibes/zeroshot 5.3.0 → 5.4.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.
Files changed (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. package/src/github.js +0 -139
@@ -5,9 +5,94 @@
5
5
  * - Hook execution (publish_message, stop_cluster, etc.)
6
6
  * - Template variable substitution
7
7
  * - Transform script execution in VM sandbox
8
+ * - Logic scripts for conditional config (like triggers)
8
9
  */
9
10
 
10
11
  const vm = require('vm');
12
+ const { execSync } = require('../lib/safe-exec'); // Enforces timeouts
13
+
14
+ /**
15
+ * Deep merge two objects, with source taking precedence
16
+ * @param {Object} target - Base object
17
+ * @param {Object} source - Override object
18
+ * @returns {Object} Merged object
19
+ */
20
+ function deepMerge(target, source) {
21
+ if (!source || typeof source !== 'object') return target;
22
+ if (!target || typeof target !== 'object') return source;
23
+
24
+ const result = { ...target };
25
+ for (const key of Object.keys(source)) {
26
+ if (
27
+ source[key] &&
28
+ typeof source[key] === 'object' &&
29
+ !Array.isArray(source[key]) &&
30
+ target[key] &&
31
+ typeof target[key] === 'object' &&
32
+ !Array.isArray(target[key])
33
+ ) {
34
+ result[key] = deepMerge(target[key], source[key]);
35
+ } else {
36
+ result[key] = source[key];
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+
42
+ async function parseResultDataForHookLogic({ agent, result }) {
43
+ if (!result?.output) return null;
44
+ try {
45
+ return await agent._parseResultOutput(result.output);
46
+ } catch (parseError) {
47
+ agent._log(
48
+ `⚠️ Hook logic: result parsing failed, continuing with null: ${parseError.message}`
49
+ );
50
+ return null;
51
+ }
52
+ }
53
+
54
+ async function resolveHookConfigWithLogic({ hook, agent, context, result }) {
55
+ if (!hook.logic) return hook.config;
56
+
57
+ const resultData = await parseResultDataForHookLogic({ agent, result });
58
+ const overrides = evaluateHookLogic({
59
+ logic: hook.logic,
60
+ resultData,
61
+ agent,
62
+ context,
63
+ });
64
+
65
+ if (!overrides) {
66
+ return hook.config;
67
+ }
68
+
69
+ agent._log(`Hook logic returned overrides: ${JSON.stringify(overrides)}`);
70
+ return deepMerge(hook.config, overrides);
71
+ }
72
+
73
+ async function resolvePublishMessage({ hook, agent, context, cluster, result }) {
74
+ if (hook.transform) {
75
+ return executeTransform({
76
+ transform: hook.transform,
77
+ context,
78
+ agent,
79
+ });
80
+ }
81
+
82
+ const effectiveConfig = await resolveHookConfigWithLogic({
83
+ hook,
84
+ agent,
85
+ context,
86
+ result,
87
+ });
88
+
89
+ return substituteTemplate({
90
+ config: effectiveConfig,
91
+ context,
92
+ agent,
93
+ cluster,
94
+ });
95
+ }
11
96
 
12
97
  /**
13
98
  * Execute a hook
@@ -39,101 +124,82 @@ async function executeHook(params) {
39
124
 
40
125
  // NO try/catch - errors must propagate
41
126
  if (hook.action === 'publish_message') {
42
- let messageToPublish;
43
-
44
- if (hook.transform) {
45
- // NEW: Execute transform script to generate message
46
- messageToPublish = await executeTransform({
47
- transform: hook.transform,
48
- context,
49
- agent,
50
- });
51
- } else {
52
- // Existing: Use template substitution
53
- messageToPublish = await substituteTemplate({
54
- config: hook.config,
55
- context,
56
- agent,
57
- cluster,
58
- });
59
- }
60
-
61
- // Publish via agent's _publish method
127
+ const messageToPublish = await resolvePublishMessage({
128
+ hook,
129
+ agent,
130
+ context,
131
+ cluster,
132
+ result,
133
+ });
62
134
  agent._publish(messageToPublish);
63
- } else if (hook.action === 'execute_system_command') {
135
+ return;
136
+ }
137
+
138
+ if (hook.action === 'execute_system_command') {
64
139
  throw new Error('execute_system_command not implemented');
65
- } else {
66
- throw new Error(`Unknown hook action: ${hook.action}`);
67
140
  }
141
+
142
+ throw new Error(`Unknown hook action: ${hook.action}`);
68
143
  }
69
144
 
70
- /**
71
- * Execute a hook transform script
72
- * Transform scripts return the message to publish, with access to:
73
- * - result: parsed agent output
74
- * - triggeringMessage: the message that triggered the agent
75
- * - helpers: { getConfig(complexity, taskType) }
76
- * @param {Object} params - Transform parameters
77
- * @param {Object} params.transform - Transform configuration
78
- * @param {Object} params.context - Execution context
79
- * @param {Object} params.agent - Agent instance
80
- * @returns {Promise<Object>} Message to publish
81
- */
82
- async function executeTransform(params) {
83
- const { transform, context, agent } = params;
84
- const { engine, script } = transform;
145
+ function getAccessedFields(script) {
146
+ return [...script.matchAll(/result\.([a-zA-Z_]+)/g)].map((m) => m[1]);
147
+ }
85
148
 
86
- if (engine !== 'javascript') {
87
- throw new Error(`Unsupported transform engine: ${engine}`);
88
- }
149
+ function logTransformParseFailure({ agent, context, parseError }) {
150
+ const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
151
+ console.error(`\n${'='.repeat(80)}`);
152
+ console.error(`🔴 TRANSFORM SCRIPT BLOCKED - RESULT PARSING FAILED`);
153
+ console.error(`${'='.repeat(80)}`);
154
+ console.error(`Agent: ${agent.id}, Role: ${agent.role}`);
155
+ console.error(`TaskID: ${taskId}`);
156
+ console.error(`Parse error: ${parseError.message}`);
157
+ console.error(`Output (last 500 chars): ${(context.result.output || '').slice(-500)}`);
158
+ console.error(`${'='.repeat(80)}\n`);
159
+ }
89
160
 
90
- // Parse result output if we have a result
91
- // VALIDATION: Check if script uses result.* variables and fail early if no output
92
- const scriptUsesResult = /\bresult\.[a-zA-Z]/.test(script);
93
- let resultData = null;
161
+ function logMissingResultFields({ agent, context, accessedFields, missingFields, resultData }) {
162
+ const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
163
+ console.error(`\n${'='.repeat(80)}`);
164
+ console.error(`🔴 TRANSFORM SCRIPT BLOCKED - MISSING REQUIRED FIELDS`);
165
+ console.error(`${'='.repeat(80)}`);
166
+ console.error(`Agent: ${agent.id}, Role: ${agent.role}, TaskID: ${taskId}`);
167
+ console.error(`Script accesses: ${accessedFields.join(', ')}`);
168
+ console.error(`Missing from result: ${missingFields.join(', ')}`);
169
+ console.error(`Result keys: ${Object.keys(resultData).join(', ')}`);
170
+ console.error(`Result data: ${JSON.stringify(resultData, null, 2)}`);
171
+ console.error(`${'='.repeat(80)}\n`);
172
+ }
94
173
 
174
+ async function parseTransformResultData({ context, agent, script, scriptUsesResult }) {
95
175
  if (context.result?.output) {
176
+ let resultData = null;
96
177
  try {
97
178
  resultData = await agent._parseResultOutput(context.result.output);
98
179
  } catch (parseError) {
99
- // FAIL FAST: Result parsing failed - don't continue with null data
100
- const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
101
- console.error(`\n${'='.repeat(80)}`);
102
- console.error(`🔴 TRANSFORM SCRIPT BLOCKED - RESULT PARSING FAILED`);
103
- console.error(`${'='.repeat(80)}`);
104
- console.error(`Agent: ${agent.id}, Role: ${agent.role}`);
105
- console.error(`TaskID: ${taskId}`);
106
- console.error(`Parse error: ${parseError.message}`);
107
- console.error(`Output (last 500 chars): ${(context.result.output || '').slice(-500)}`);
108
- console.error(`${'='.repeat(80)}\n`);
180
+ logTransformParseFailure({ agent, context, parseError });
109
181
  throw new Error(
110
182
  `Transform script cannot run: result parsing failed. ` +
111
183
  `Agent: ${agent.id}, Error: ${parseError.message}`
112
184
  );
113
185
  }
114
186
 
115
- // DEFENSIVE: Validate result has expected fields if script accesses them
116
- // Extract field names from script (e.g., result.complexity, result.taskType)
117
- const accessedFields = [...script.matchAll(/result\.([a-zA-Z_]+)/g)].map((m) => m[1]);
187
+ const accessedFields = getAccessedFields(script);
118
188
  const missingFields = accessedFields.filter((f) => resultData[f] === undefined);
119
189
  if (missingFields.length > 0) {
190
+ logMissingResultFields({ agent, context, accessedFields, missingFields, resultData });
120
191
  const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
121
- console.error(`\n${'='.repeat(80)}`);
122
- console.error(`🔴 TRANSFORM SCRIPT BLOCKED - MISSING REQUIRED FIELDS`);
123
- console.error(`${'='.repeat(80)}`);
124
- console.error(`Agent: ${agent.id}, Role: ${agent.role}, TaskID: ${taskId}`);
125
- console.error(`Script accesses: ${accessedFields.join(', ')}`);
126
- console.error(`Missing from result: ${missingFields.join(', ')}`);
127
- console.error(`Result keys: ${Object.keys(resultData).join(', ')}`);
128
- console.error(`Result data: ${JSON.stringify(resultData, null, 2)}`);
129
- console.error(`${'='.repeat(80)}\n`);
130
192
  throw new Error(
131
193
  `Transform script accesses undefined fields: ${missingFields.join(', ')}. ` +
132
194
  `Agent ${agent.id} (task ${taskId}) output missing required fields. ` +
133
195
  `Check agent's jsonSchema and output format.`
134
196
  );
135
197
  }
136
- } else if (scriptUsesResult) {
198
+
199
+ return resultData;
200
+ }
201
+
202
+ if (scriptUsesResult) {
137
203
  const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
138
204
  const outputLength = (context.result?.output || '').length;
139
205
  throw new Error(
@@ -144,17 +210,15 @@ async function executeTransform(params) {
144
210
  );
145
211
  }
146
212
 
147
- // Helper functions exposed to transform scripts
213
+ return null;
214
+ }
215
+
216
+ function buildTransformSandbox({ resultData, context, agent }) {
148
217
  const helpers = {
149
- /**
150
- * Get cluster config based on complexity and task type
151
- * Returns: { base: 'template-name', params: { ... } }
152
- */
153
218
  getConfig: require('../config-router').getConfig,
154
219
  };
155
220
 
156
- // Build sandbox context
157
- const sandbox = {
221
+ return {
158
222
  result: resultData,
159
223
  triggeringMessage: context.triggeringMessage,
160
224
  helpers,
@@ -165,19 +229,20 @@ async function executeTransform(params) {
165
229
  warn: (...args) => console.warn('[transform]', ...args),
166
230
  },
167
231
  };
232
+ }
168
233
 
169
- // Execute in VM sandbox with timeout
234
+ function runTransformScript(script, sandbox) {
170
235
  const vmContext = vm.createContext(sandbox);
171
236
  const wrappedScript = `(function() { ${script} })()`;
172
237
 
173
- let result;
174
238
  try {
175
- result = vm.runInContext(wrappedScript, vmContext, { timeout: 5000 });
239
+ return vm.runInContext(wrappedScript, vmContext, { timeout: 5000 });
176
240
  } catch (err) {
177
241
  throw new Error(`Transform script error: ${err.message}`);
178
242
  }
243
+ }
179
244
 
180
- // Validate result
245
+ function validateTransformResult(result) {
181
246
  if (!result || typeof result !== 'object') {
182
247
  throw new Error(
183
248
  `Transform script must return an object with topic and content, got: ${typeof result}`
@@ -189,41 +254,77 @@ async function executeTransform(params) {
189
254
  if (!result.content) {
190
255
  throw new Error(`Transform script result must have a 'content' property`);
191
256
  }
257
+ }
192
258
 
193
- // CRITICAL: Extra validation for CLUSTER_OPERATIONS - this is the make-or-break message
194
- // If this message is malformed, the cluster will hang forever
195
- if (result.topic === 'CLUSTER_OPERATIONS') {
196
- const operations = result.content?.data?.operations;
197
- if (!operations) {
198
- console.error(`\n${'='.repeat(80)}`);
199
- console.error(`🔴 CLUSTER_OPERATIONS MALFORMED - MISSING OPERATIONS ARRAY`);
200
- console.error(`${'='.repeat(80)}`);
201
- console.error(`Agent: ${agent.id}`);
202
- console.error(`Result: ${JSON.stringify(result, null, 2)}`);
203
- console.error(`${'='.repeat(80)}\n`);
204
- throw new Error(
205
- `CLUSTER_OPERATIONS message missing operations array. ` +
206
- `Agent ${agent.id} transform script returned invalid structure.`
207
- );
208
- }
209
- if (!Array.isArray(operations)) {
210
- throw new Error(`CLUSTER_OPERATIONS.operations must be an array, got: ${typeof operations}`);
211
- }
212
- if (operations.length === 0) {
213
- throw new Error(`CLUSTER_OPERATIONS.operations is empty - no operations to execute`);
214
- }
259
+ function validateClusterOperationsResult(result, agent) {
260
+ if (result.topic !== 'CLUSTER_OPERATIONS') return;
261
+
262
+ const operations = result.content?.data?.operations;
263
+ if (!operations) {
264
+ console.error(`\n${'='.repeat(80)}`);
265
+ console.error(`🔴 CLUSTER_OPERATIONS MALFORMED - MISSING OPERATIONS ARRAY`);
266
+ console.error(`${'='.repeat(80)}`);
267
+ console.error(`Agent: ${agent.id}`);
268
+ console.error(`Result: ${JSON.stringify(result, null, 2)}`);
269
+ console.error(`${'='.repeat(80)}\n`);
270
+ throw new Error(
271
+ `CLUSTER_OPERATIONS message missing operations array. ` +
272
+ `Agent ${agent.id} transform script returned invalid structure.`
273
+ );
274
+ }
275
+ if (!Array.isArray(operations)) {
276
+ throw new Error(`CLUSTER_OPERATIONS.operations must be an array, got: ${typeof operations}`);
277
+ }
278
+ if (operations.length === 0) {
279
+ throw new Error(`CLUSTER_OPERATIONS.operations is empty - no operations to execute`);
280
+ }
215
281
 
216
- // Validate each operation has required 'action' field
217
- for (let i = 0; i < operations.length; i++) {
218
- const op = operations[i];
219
- if (!op || !op.action) {
220
- throw new Error(`CLUSTER_OPERATIONS.operations[${i}] missing required 'action' field`);
221
- }
282
+ for (let i = 0; i < operations.length; i++) {
283
+ const op = operations[i];
284
+ if (!op || !op.action) {
285
+ throw new Error(`CLUSTER_OPERATIONS.operations[${i}] missing required 'action' field`);
222
286
  }
287
+ }
223
288
 
224
- agent._log(`✅ CLUSTER_OPERATIONS validated: ${operations.length} operations`);
289
+ agent._log(`✅ CLUSTER_OPERATIONS validated: ${operations.length} operations`);
290
+ }
291
+
292
+ /**
293
+ * Execute a hook transform script
294
+ * Transform scripts return the message to publish, with access to:
295
+ * - result: parsed agent output
296
+ * - triggeringMessage: the message that triggered the agent
297
+ * - helpers: { getConfig(complexity, taskType) }
298
+ * @param {Object} params - Transform parameters
299
+ * @param {Object} params.transform - Transform configuration
300
+ * @param {Object} params.context - Execution context
301
+ * @param {Object} params.agent - Agent instance
302
+ * @returns {Promise<Object>} Message to publish
303
+ */
304
+ async function executeTransform(params) {
305
+ const { transform, context, agent } = params;
306
+ const { engine, script } = transform;
307
+
308
+ if (engine !== 'javascript') {
309
+ throw new Error(`Unsupported transform engine: ${engine}`);
225
310
  }
226
311
 
312
+ // Parse result output if we have a result
313
+ // VALIDATION: Check if script uses result.* variables and fail early if no output
314
+ const scriptUsesResult = /\bresult\.[a-zA-Z]/.test(script);
315
+ const resultData = await parseTransformResultData({
316
+ context,
317
+ agent,
318
+ script,
319
+ scriptUsesResult,
320
+ });
321
+
322
+ const sandbox = buildTransformSandbox({ resultData, context, agent });
323
+ const result = runTransformScript(script, sandbox);
324
+
325
+ validateTransformResult(result);
326
+ validateClusterOperationsResult(result, agent);
327
+
227
328
  return result;
228
329
  }
229
330
 
@@ -278,7 +379,6 @@ async function substituteTemplate(params) {
278
379
  if (taskId !== 'UNKNOWN') {
279
380
  console.error(`\nFetching task logs for ${taskId}...`);
280
381
  try {
281
- const { execSync } = require('child_process');
282
382
  const ctPath = agent._getClaudeTasksPath();
283
383
  taskLogs = execSync(`${ctPath} logs ${taskId} --lines 100`, {
284
384
  encoding: 'utf-8',
@@ -333,12 +433,25 @@ async function substituteTemplate(params) {
333
433
  return stringified.slice(1, -1);
334
434
  };
335
435
 
436
+ // Helper to escape template-like patterns in substituted values
437
+ // This prevents content containing "{{result.foo}}" from being flagged as unsubstituted variables
438
+ // Uses Unicode escape for first brace: {{ -> \u007B{ (invisible to humans, breaks pattern match)
439
+ const escapeTemplatePatterns = (str) => {
440
+ return str.replace(/\{\{/g, '\\u007B{');
441
+ };
442
+
336
443
  let substituted = json
337
- .replace(/\{\{cluster\.id\}\}/g, cluster.id)
444
+ .replace(/\{\{cluster\.id\}\}/g, escapeTemplatePatterns(cluster.id))
338
445
  .replace(/\{\{cluster\.createdAt\}\}/g, String(cluster.createdAt))
339
446
  .replace(/\{\{iteration\}\}/g, String(agent.iteration))
340
- .replace(/\{\{error\.message\}\}/g, escapeForJsonString(context.error?.message ?? ''))
341
- .replace(/\{\{result\.output\}\}/g, escapeForJsonString(context.result?.output ?? ''));
447
+ .replace(
448
+ /\{\{error\.message\}\}/g,
449
+ escapeTemplatePatterns(escapeForJsonString(context.error?.message ?? ''))
450
+ )
451
+ .replace(
452
+ /\{\{result\.output\}\}/g,
453
+ escapeTemplatePatterns(escapeForJsonString(context.result?.output ?? ''))
454
+ );
342
455
 
343
456
  // Substitute ALL result.* variables dynamically from parsed resultData
344
457
  if (resultData) {
@@ -364,7 +477,8 @@ async function substituteTemplate(params) {
364
477
  return String(value);
365
478
  }
366
479
  // Strings need to be quoted and escaped for JSON
367
- return JSON.stringify(value);
480
+ // Also escape any template-like patterns in the content to prevent false positives
481
+ return escapeTemplatePatterns(JSON.stringify(value));
368
482
  });
369
483
  }
370
484
 
@@ -395,8 +509,91 @@ async function substituteTemplate(params) {
395
509
  return result;
396
510
  }
397
511
 
512
+ /**
513
+ * Evaluate hook logic script to get config overrides
514
+ * Similar to trigger logic, but returns an object to merge into config
515
+ * @param {Object} params - Evaluation parameters
516
+ * @param {Object} params.logic - Logic configuration { engine, script }
517
+ * @param {Object} params.resultData - Parsed agent result data
518
+ * @param {Object} params.agent - Agent instance
519
+ * @param {Object} params.context - Execution context
520
+ * @returns {Object|null} Config overrides to merge, or null if none
521
+ */
522
+ function evaluateHookLogic(params) {
523
+ const { logic, resultData, agent, context } = params;
524
+
525
+ if (!logic || !logic.script) {
526
+ return null;
527
+ }
528
+
529
+ if (logic.engine !== 'javascript') {
530
+ throw new Error(`Unsupported hook logic engine: ${logic.engine}`);
531
+ }
532
+
533
+ // Build sandbox context - similar to LogicEngine but focused on result data
534
+ const sandbox = {
535
+ // The parsed result from agent output - this is the main input
536
+ result: resultData || {},
537
+
538
+ // Agent context
539
+ agent: {
540
+ id: agent.id,
541
+ role: agent.role,
542
+ iteration: agent.iteration || 0,
543
+ },
544
+
545
+ // Triggering message (if available)
546
+ message: context.triggeringMessage || null,
547
+
548
+ // Safe built-ins
549
+ Set,
550
+ Map,
551
+ Array,
552
+ Object,
553
+ String,
554
+ Number,
555
+ Boolean,
556
+ Math,
557
+ Date,
558
+ JSON,
559
+
560
+ // Console for debugging (logs to agent log)
561
+ console: {
562
+ log: (...args) => agent._log('[hook-logic]', ...args),
563
+ error: (...args) => console.error('[hook-logic]', ...args),
564
+ warn: (...args) => console.warn('[hook-logic]', ...args),
565
+ },
566
+ };
567
+
568
+ // Execute in VM sandbox with timeout
569
+ const vmContext = vm.createContext(sandbox);
570
+ const wrappedScript = `(function() { 'use strict'; ${logic.script} })()`;
571
+
572
+ let result;
573
+ try {
574
+ result = vm.runInContext(wrappedScript, vmContext, { timeout: 1000 });
575
+ } catch (err) {
576
+ throw new Error(`Hook logic script error: ${err.message}`);
577
+ }
578
+
579
+ // Logic scripts can return:
580
+ // - undefined/null: no overrides
581
+ // - object: merge into config
582
+ if (result === undefined || result === null) {
583
+ return null;
584
+ }
585
+
586
+ if (typeof result !== 'object') {
587
+ throw new Error(`Hook logic script must return an object or undefined, got: ${typeof result}`);
588
+ }
589
+
590
+ return result;
591
+ }
592
+
398
593
  module.exports = {
399
594
  executeHook,
400
595
  executeTransform,
401
596
  substituteTemplate,
597
+ evaluateHookLogic,
598
+ deepMerge,
402
599
  };