@covibes/zeroshot 5.2.1 → 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 (98) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +226 -195
  3. package/cli/commands/providers.js +149 -0
  4. package/cli/index.js +3145 -2366
  5. package/cli/lib/first-run.js +40 -3
  6. package/cli/message-formatters-normal.js +28 -6
  7. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  8. package/cluster-templates/base-templates/full-workflow.json +99 -316
  9. package/cluster-templates/base-templates/single-worker.json +23 -15
  10. package/cluster-templates/base-templates/worker-validator.json +105 -36
  11. package/cluster-templates/conductor-bootstrap.json +9 -7
  12. package/lib/docker-config.js +14 -1
  13. package/lib/git-remote-utils.js +165 -0
  14. package/lib/id-detector.js +10 -7
  15. package/lib/provider-defaults.js +62 -0
  16. package/lib/provider-detection.js +59 -0
  17. package/lib/provider-names.js +57 -0
  18. package/lib/settings/claude-auth.js +78 -0
  19. package/lib/settings.js +298 -15
  20. package/lib/stream-json-parser.js +4 -238
  21. package/package.json +27 -6
  22. package/scripts/setup-merge-queue.sh +170 -0
  23. package/scripts/validate-templates.js +100 -0
  24. package/src/agent/agent-config.js +140 -63
  25. package/src/agent/agent-context-builder.js +336 -165
  26. package/src/agent/agent-hook-executor.js +337 -67
  27. package/src/agent/agent-lifecycle.js +386 -287
  28. package/src/agent/agent-stuck-detector.js +7 -7
  29. package/src/agent/agent-task-executor.js +944 -683
  30. package/src/agent/output-extraction.js +217 -0
  31. package/src/agent/output-reformatter.js +175 -0
  32. package/src/agent/schema-utils.js +146 -0
  33. package/src/agent-wrapper.js +112 -31
  34. package/src/agents/git-pusher-template.js +285 -0
  35. package/src/claude-task-runner.js +145 -44
  36. package/src/config-router.js +13 -13
  37. package/src/config-validator.js +1049 -563
  38. package/src/input-helpers.js +65 -0
  39. package/src/isolation-manager.js +499 -320
  40. package/src/issue-providers/README.md +305 -0
  41. package/src/issue-providers/azure-devops-provider.js +273 -0
  42. package/src/issue-providers/base-provider.js +232 -0
  43. package/src/issue-providers/github-provider.js +179 -0
  44. package/src/issue-providers/gitlab-provider.js +241 -0
  45. package/src/issue-providers/index.js +196 -0
  46. package/src/issue-providers/jira-provider.js +239 -0
  47. package/src/ledger.js +50 -11
  48. package/src/lib/safe-exec.js +88 -0
  49. package/src/orchestrator.js +1348 -757
  50. package/src/preflight.js +306 -149
  51. package/src/process-metrics.js +98 -56
  52. package/src/providers/anthropic/cli-builder.js +73 -0
  53. package/src/providers/anthropic/index.js +204 -0
  54. package/src/providers/anthropic/models.js +23 -0
  55. package/src/providers/anthropic/output-parser.js +177 -0
  56. package/src/providers/base-provider.js +251 -0
  57. package/src/providers/capabilities.js +60 -0
  58. package/src/providers/google/cli-builder.js +55 -0
  59. package/src/providers/google/index.js +116 -0
  60. package/src/providers/google/models.js +24 -0
  61. package/src/providers/google/output-parser.js +101 -0
  62. package/src/providers/index.js +91 -0
  63. package/src/providers/openai/cli-builder.js +133 -0
  64. package/src/providers/openai/index.js +136 -0
  65. package/src/providers/openai/models.js +21 -0
  66. package/src/providers/openai/output-parser.js +143 -0
  67. package/src/providers/opencode/cli-builder.js +42 -0
  68. package/src/providers/opencode/index.js +103 -0
  69. package/src/providers/opencode/models.js +55 -0
  70. package/src/providers/opencode/output-parser.js +122 -0
  71. package/src/schemas/sub-cluster.js +68 -39
  72. package/src/status-footer.js +94 -48
  73. package/src/sub-cluster-wrapper.js +92 -36
  74. package/src/task-runner.js +8 -6
  75. package/src/template-resolver.js +12 -9
  76. package/src/tui/data-poller.js +123 -99
  77. package/src/tui/formatters.js +4 -3
  78. package/src/tui/keybindings.js +259 -318
  79. package/src/tui/layout.js +20 -3
  80. package/src/tui/renderer.js +11 -21
  81. package/task-lib/attachable-watcher.js +150 -111
  82. package/task-lib/claude-recovery.js +156 -0
  83. package/task-lib/commands/episodes.js +105 -0
  84. package/task-lib/commands/list.js +3 -3
  85. package/task-lib/commands/logs.js +231 -189
  86. package/task-lib/commands/resume.js +3 -2
  87. package/task-lib/commands/run.js +12 -3
  88. package/task-lib/commands/schedules.js +111 -61
  89. package/task-lib/config.js +0 -2
  90. package/task-lib/runner.js +128 -50
  91. package/task-lib/scheduler.js +3 -3
  92. package/task-lib/store.js +464 -152
  93. package/task-lib/tui/formatters.js +94 -45
  94. package/task-lib/tui/renderer.js +13 -3
  95. package/task-lib/tui.js +73 -32
  96. package/task-lib/watcher.js +157 -100
  97. package/src/agents/git-pusher-agent.json +0 -20
  98. package/src/github.js +0 -103
@@ -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
@@ -22,7 +107,7 @@ const vm = require('vm');
22
107
  * @param {Object} params.orchestrator - Orchestrator instance
23
108
  * @returns {Promise<void>}
24
109
  */
25
- function executeHook(params) {
110
+ async function executeHook(params) {
26
111
  const { hook, agent, message, result, cluster } = params;
27
112
 
28
113
  if (!hook) {
@@ -39,62 +124,82 @@ 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 = executeTransform({
47
- transform: hook.transform,
48
- context,
49
- agent,
50
- });
51
- } else {
52
- // Existing: Use template substitution
53
- messageToPublish = 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 {Object} Message to publish
81
- */
82
- 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) {
96
- resultData = agent._parseResultOutput(context.result.output);
97
- } else if (scriptUsesResult) {
176
+ let resultData = null;
177
+ try {
178
+ resultData = await agent._parseResultOutput(context.result.output);
179
+ } catch (parseError) {
180
+ logTransformParseFailure({ agent, context, parseError });
181
+ throw new Error(
182
+ `Transform script cannot run: result parsing failed. ` +
183
+ `Agent: ${agent.id}, Error: ${parseError.message}`
184
+ );
185
+ }
186
+
187
+ const accessedFields = getAccessedFields(script);
188
+ const missingFields = accessedFields.filter((f) => resultData[f] === undefined);
189
+ if (missingFields.length > 0) {
190
+ logMissingResultFields({ agent, context, accessedFields, missingFields, resultData });
191
+ const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
192
+ throw new Error(
193
+ `Transform script accesses undefined fields: ${missingFields.join(', ')}. ` +
194
+ `Agent ${agent.id} (task ${taskId}) output missing required fields. ` +
195
+ `Check agent's jsonSchema and output format.`
196
+ );
197
+ }
198
+
199
+ return resultData;
200
+ }
201
+
202
+ if (scriptUsesResult) {
98
203
  const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
99
204
  const outputLength = (context.result?.output || '').length;
100
205
  throw new Error(
@@ -105,17 +210,15 @@ function executeTransform(params) {
105
210
  );
106
211
  }
107
212
 
108
- // Helper functions exposed to transform scripts
213
+ return null;
214
+ }
215
+
216
+ function buildTransformSandbox({ resultData, context, agent }) {
109
217
  const helpers = {
110
- /**
111
- * Get cluster config based on complexity and task type
112
- * Returns: { base: 'template-name', params: { ... } }
113
- */
114
218
  getConfig: require('../config-router').getConfig,
115
219
  };
116
220
 
117
- // Build sandbox context
118
- const sandbox = {
221
+ return {
119
222
  result: resultData,
120
223
  triggeringMessage: context.triggeringMessage,
121
224
  helpers,
@@ -126,19 +229,20 @@ function executeTransform(params) {
126
229
  warn: (...args) => console.warn('[transform]', ...args),
127
230
  },
128
231
  };
232
+ }
129
233
 
130
- // Execute in VM sandbox with timeout
234
+ function runTransformScript(script, sandbox) {
131
235
  const vmContext = vm.createContext(sandbox);
132
236
  const wrappedScript = `(function() { ${script} })()`;
133
237
 
134
- let result;
135
238
  try {
136
- result = vm.runInContext(wrappedScript, vmContext, { timeout: 5000 });
239
+ return vm.runInContext(wrappedScript, vmContext, { timeout: 5000 });
137
240
  } catch (err) {
138
241
  throw new Error(`Transform script error: ${err.message}`);
139
242
  }
243
+ }
140
244
 
141
- // Validate result
245
+ function validateTransformResult(result) {
142
246
  if (!result || typeof result !== 'object') {
143
247
  throw new Error(
144
248
  `Transform script must return an object with topic and content, got: ${typeof result}`
@@ -150,6 +254,76 @@ function executeTransform(params) {
150
254
  if (!result.content) {
151
255
  throw new Error(`Transform script result must have a 'content' property`);
152
256
  }
257
+ }
258
+
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
+ }
281
+
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`);
286
+ }
287
+ }
288
+
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}`);
310
+ }
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);
153
327
 
154
328
  return result;
155
329
  }
@@ -163,9 +337,9 @@ function executeTransform(params) {
163
337
  * @param {Object} params.context - Execution context
164
338
  * @param {Object} params.agent - Agent instance
165
339
  * @param {Object} params.cluster - Cluster object
166
- * @returns {Object} Substituted configuration
340
+ * @returns {Promise<Object>} Substituted configuration
167
341
  */
168
- function substituteTemplate(params) {
342
+ async function substituteTemplate(params) {
169
343
  const { config, context, agent, cluster } = params;
170
344
 
171
345
  if (!config) {
@@ -205,7 +379,6 @@ function substituteTemplate(params) {
205
379
  if (taskId !== 'UNKNOWN') {
206
380
  console.error(`\nFetching task logs for ${taskId}...`);
207
381
  try {
208
- const { execSync } = require('child_process');
209
382
  const ctPath = agent._getClaudeTasksPath();
210
383
  taskLogs = execSync(`${ctPath} logs ${taskId} --lines 100`, {
211
384
  encoding: 'utf-8',
@@ -243,7 +416,7 @@ function substituteTemplate(params) {
243
416
  );
244
417
  }
245
418
  // Parse result output - WILL THROW if no JSON block
246
- resultData = agent._parseResultOutput(context.result.output);
419
+ resultData = await agent._parseResultOutput(context.result.output);
247
420
  }
248
421
 
249
422
  // Helper to escape a value for JSON string substitution
@@ -260,12 +433,25 @@ function substituteTemplate(params) {
260
433
  return stringified.slice(1, -1);
261
434
  };
262
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
+
263
443
  let substituted = json
264
- .replace(/\{\{cluster\.id\}\}/g, cluster.id)
444
+ .replace(/\{\{cluster\.id\}\}/g, escapeTemplatePatterns(cluster.id))
265
445
  .replace(/\{\{cluster\.createdAt\}\}/g, String(cluster.createdAt))
266
446
  .replace(/\{\{iteration\}\}/g, String(agent.iteration))
267
- .replace(/\{\{error\.message\}\}/g, escapeForJsonString(context.error?.message ?? ''))
268
- .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
+ );
269
455
 
270
456
  // Substitute ALL result.* variables dynamically from parsed resultData
271
457
  if (resultData) {
@@ -291,7 +477,8 @@ function substituteTemplate(params) {
291
477
  return String(value);
292
478
  }
293
479
  // Strings need to be quoted and escaped for JSON
294
- return JSON.stringify(value);
480
+ // Also escape any template-like patterns in the content to prevent false positives
481
+ return escapeTemplatePatterns(JSON.stringify(value));
295
482
  });
296
483
  }
297
484
 
@@ -322,8 +509,91 @@ function substituteTemplate(params) {
322
509
  return result;
323
510
  }
324
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
+
325
593
  module.exports = {
326
594
  executeHook,
327
595
  executeTransform,
328
596
  substituteTemplate,
597
+ evaluateHookLogic,
598
+ deepMerge,
329
599
  };