@covibes/zeroshot 1.0.1

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 (57) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/LICENSE +21 -0
  3. package/README.md +364 -0
  4. package/cli/index.js +3990 -0
  5. package/cluster-templates/base-templates/debug-workflow.json +181 -0
  6. package/cluster-templates/base-templates/full-workflow.json +455 -0
  7. package/cluster-templates/base-templates/single-worker.json +48 -0
  8. package/cluster-templates/base-templates/worker-validator.json +131 -0
  9. package/cluster-templates/conductor-bootstrap.json +122 -0
  10. package/cluster-templates/conductor-junior-bootstrap.json +69 -0
  11. package/docker/zeroshot-cluster/Dockerfile +132 -0
  12. package/lib/completion.js +174 -0
  13. package/lib/id-detector.js +53 -0
  14. package/lib/settings.js +97 -0
  15. package/lib/stream-json-parser.js +236 -0
  16. package/package.json +121 -0
  17. package/src/agent/agent-config.js +121 -0
  18. package/src/agent/agent-context-builder.js +241 -0
  19. package/src/agent/agent-hook-executor.js +329 -0
  20. package/src/agent/agent-lifecycle.js +555 -0
  21. package/src/agent/agent-stuck-detector.js +256 -0
  22. package/src/agent/agent-task-executor.js +1034 -0
  23. package/src/agent/agent-trigger-evaluator.js +67 -0
  24. package/src/agent-wrapper.js +459 -0
  25. package/src/agents/git-pusher-agent.json +20 -0
  26. package/src/attach/attach-client.js +438 -0
  27. package/src/attach/attach-server.js +543 -0
  28. package/src/attach/index.js +35 -0
  29. package/src/attach/protocol.js +220 -0
  30. package/src/attach/ring-buffer.js +121 -0
  31. package/src/attach/socket-discovery.js +242 -0
  32. package/src/claude-task-runner.js +468 -0
  33. package/src/config-router.js +80 -0
  34. package/src/config-validator.js +598 -0
  35. package/src/github.js +103 -0
  36. package/src/isolation-manager.js +1042 -0
  37. package/src/ledger.js +429 -0
  38. package/src/logic-engine.js +223 -0
  39. package/src/message-bus-bridge.js +139 -0
  40. package/src/message-bus.js +202 -0
  41. package/src/name-generator.js +232 -0
  42. package/src/orchestrator.js +1938 -0
  43. package/src/schemas/sub-cluster.js +156 -0
  44. package/src/sub-cluster-wrapper.js +545 -0
  45. package/src/task-runner.js +28 -0
  46. package/src/template-resolver.js +347 -0
  47. package/src/tui/CHANGES.txt +133 -0
  48. package/src/tui/LAYOUT.md +261 -0
  49. package/src/tui/README.txt +192 -0
  50. package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
  51. package/src/tui/data-poller.js +325 -0
  52. package/src/tui/demo.js +208 -0
  53. package/src/tui/formatters.js +123 -0
  54. package/src/tui/index.js +193 -0
  55. package/src/tui/keybindings.js +383 -0
  56. package/src/tui/layout.js +317 -0
  57. package/src/tui/renderer.js +194 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * AgentContextBuilder - Build agent execution context from ledger
3
+ *
4
+ * Provides:
5
+ * - Context assembly from multiple message sources
6
+ * - Context strategy evaluation (topics, limits, since timestamps)
7
+ * - Prompt injection and formatting
8
+ * - Token-based truncation
9
+ * - Defensive context overflow prevention
10
+ */
11
+
12
+ // Defensive limit: 500,000 chars ≈ 125k tokens (safe buffer below 200k limit)
13
+ // Prevents "Prompt is too long" errors that kill tasks
14
+ const MAX_CONTEXT_CHARS = 500000;
15
+
16
+ /**
17
+ * Build execution context for an agent
18
+ * @param {object} params - Context building parameters
19
+ * @param {string} params.id - Agent ID
20
+ * @param {string} params.role - Agent role
21
+ * @param {number} params.iteration - Current iteration number
22
+ * @param {any} params.config - Agent configuration
23
+ * @param {any} params.messageBus - Message bus for querying ledger
24
+ * @param {any} params.cluster - Cluster object
25
+ * @param {number} [params.lastTaskEndTime] - Timestamp of last task completion
26
+ * @param {any} params.triggeringMessage - Message that triggered this execution
27
+ * @param {string} [params.selectedPrompt] - Pre-selected prompt from _selectPrompt() (iteration-based)
28
+ * @returns {string} Assembled context string
29
+ */
30
+ function buildContext({
31
+ id,
32
+ role,
33
+ iteration,
34
+ config,
35
+ messageBus,
36
+ cluster,
37
+ lastTaskEndTime,
38
+ triggeringMessage,
39
+ selectedPrompt,
40
+ }) {
41
+ const strategy = config.contextStrategy || { sources: [] };
42
+
43
+ let context = `You are agent "${id}" with role "${role}".\n\n`;
44
+ context += `Iteration: ${iteration}\n\n`;
45
+
46
+ // GLOBAL RULE: NEVER ASK QUESTIONS - Vibe agents run non-interactively
47
+ context += `## 🔴 CRITICAL: AUTONOMOUS EXECUTION REQUIRED\n\n`;
48
+ context += `You are running in a NON-INTERACTIVE cluster environment.\n\n`;
49
+ context += `**NEVER** use AskUserQuestion or ask for user input - there is NO user to respond.\n`;
50
+ context += `**NEVER** ask "Would you like me to..." or "Should I..." - JUST DO IT.\n`;
51
+ context += `**NEVER** wait for approval or confirmation - MAKE DECISIONS AUTONOMOUSLY.\n\n`;
52
+ context += `When facing choices:\n`;
53
+ context += `- Choose the option that maintains code quality and correctness\n`;
54
+ context += `- If unsure between "fix the code" vs "relax the rules" → ALWAYS fix the code\n`;
55
+ context += `- If unsure between "do more" vs "do less" → ALWAYS do what's required, nothing more\n\n`;
56
+
57
+ // Add prompt from config (system prompt, instructions, output format)
58
+ // If selectedPrompt is provided (iteration-based), use it directly
59
+ // Otherwise fall back to legacy config.prompt handling
60
+ const promptText = selectedPrompt || (typeof config.prompt === 'string' ? config.prompt : config.prompt?.system);
61
+
62
+ if (promptText) {
63
+ context += `## Instructions\n\n${promptText}\n\n`;
64
+ } else if (config.prompt && typeof config.prompt !== 'string' && !config.prompt?.system) {
65
+ // FAIL HARD: prompt exists but format is unrecognized (and no selectedPrompt provided)
66
+ throw new Error(
67
+ `Agent "${id}" has invalid prompt format. ` +
68
+ `Expected string or object with .system property, got: ${JSON.stringify(config.prompt).slice(0, 100)}...`
69
+ );
70
+ }
71
+
72
+ // Handle legacy outputFormat in prompt object (separate from iteration-based prompt selection)
73
+ if (config.prompt?.outputFormat) {
74
+ context += `## Output Format (REQUIRED)\n\n`;
75
+ context += `After completing your task, you MUST output a JSON block:\n\n`;
76
+ context += `\`\`\`json\n${JSON.stringify(config.prompt.outputFormat.example, null, 2)}\n\`\`\`\n\n`;
77
+
78
+ if (config.prompt.outputFormat.rules) {
79
+ context += `IMPORTANT:\n`;
80
+ for (const rule of config.prompt.outputFormat.rules) {
81
+ context += `- ${rule}\n`;
82
+ }
83
+ context += '\n';
84
+ }
85
+ }
86
+
87
+ // Add sources
88
+ for (const source of strategy.sources) {
89
+ // Resolve special 'since' values
90
+ let sinceTimestamp = source.since;
91
+ if (source.since === 'cluster_start') {
92
+ sinceTimestamp = cluster.createdAt;
93
+ } else if (source.since === 'last_task_end') {
94
+ // Use timestamp of last task completion, or cluster start if no tasks completed yet
95
+ sinceTimestamp = lastTaskEndTime || cluster.createdAt;
96
+ }
97
+
98
+ const messages = messageBus.query({
99
+ cluster_id: cluster.id,
100
+ topic: source.topic,
101
+ sender: source.sender,
102
+ since: sinceTimestamp,
103
+ limit: source.limit,
104
+ });
105
+
106
+ if (messages.length > 0) {
107
+ context += `\n## Messages from topic: ${source.topic}\n\n`;
108
+ for (const msg of messages) {
109
+ context += `[${new Date(msg.timestamp).toISOString()}] ${msg.sender}:\n`;
110
+ if (msg.content?.text) {
111
+ context += `${msg.content.text}\n`;
112
+ }
113
+ if (msg.content?.data) {
114
+ context += `Data: ${JSON.stringify(msg.content.data, null, 2)}\n`;
115
+ }
116
+ context += '\n';
117
+ }
118
+ }
119
+ }
120
+
121
+ // Add triggering message
122
+ context += `\n## Triggering Message\n\n`;
123
+ context += `Topic: ${triggeringMessage.topic}\n`;
124
+ context += `Sender: ${triggeringMessage.sender}\n`;
125
+ if (triggeringMessage.content?.text) {
126
+ context += `\n${triggeringMessage.content.text}\n`;
127
+ }
128
+
129
+ // DEFENSIVE TRUNCATION - Prevent context overflow errors
130
+ // Strategy: Keep ISSUE_OPENED (original task) + most recent messages
131
+ // Truncate from MIDDLE (oldest context messages) if too long
132
+ const originalLength = context.length;
133
+
134
+ if (originalLength > MAX_CONTEXT_CHARS) {
135
+ console.log(
136
+ `[Context] Context too large (${originalLength} chars), truncating to prevent overflow...`
137
+ );
138
+
139
+ // Split context into sections
140
+ const lines = context.split('\n');
141
+
142
+ // Find critical sections that must be preserved
143
+ let issueOpenedStart = -1;
144
+ let issueOpenedEnd = -1;
145
+ let triggeringStart = -1;
146
+
147
+ for (let i = 0; i < lines.length; i++) {
148
+ if (lines[i].includes('## Messages from topic: ISSUE_OPENED')) {
149
+ issueOpenedStart = i;
150
+ }
151
+ if (issueOpenedStart !== -1 && issueOpenedEnd === -1 && lines[i].startsWith('## ')) {
152
+ issueOpenedEnd = i;
153
+ }
154
+ if (lines[i].includes('## Triggering Message')) {
155
+ triggeringStart = i;
156
+ break;
157
+ }
158
+ }
159
+
160
+ // Build truncated context:
161
+ // 1. Header (agent info, instructions, output format)
162
+ // 2. ISSUE_OPENED message (original task - NEVER truncate)
163
+ // 3. Most recent N messages (whatever fits in budget)
164
+ // 4. Triggering message (current event)
165
+
166
+ const headerEnd = issueOpenedStart !== -1 ? issueOpenedStart : triggeringStart;
167
+ const header = lines.slice(0, headerEnd).join('\n');
168
+
169
+ const issueOpened =
170
+ issueOpenedStart !== -1 && issueOpenedEnd !== -1
171
+ ? lines.slice(issueOpenedStart, issueOpenedEnd).join('\n')
172
+ : '';
173
+
174
+ const triggeringMsg = lines.slice(triggeringStart).join('\n');
175
+
176
+ // Calculate remaining budget for recent messages
177
+ const fixedSize = header.length + issueOpened.length + triggeringMsg.length;
178
+ const budgetForRecent = MAX_CONTEXT_CHARS - fixedSize - 200; // 200 char buffer for markers
179
+
180
+ // Collect recent messages (from end backwards until budget exhausted)
181
+ const recentLines = [];
182
+ let recentSize = 0;
183
+
184
+ const middleStart = issueOpenedEnd !== -1 ? issueOpenedEnd : headerEnd;
185
+ const middleEnd = triggeringStart;
186
+ const middleLines = lines.slice(middleStart, middleEnd);
187
+
188
+ for (let i = middleLines.length - 1; i >= 0; i--) {
189
+ const line = middleLines[i];
190
+ const lineSize = line.length + 1; // +1 for newline
191
+
192
+ if (recentSize + lineSize > budgetForRecent) {
193
+ break; // Budget exhausted
194
+ }
195
+
196
+ recentLines.unshift(line);
197
+ recentSize += lineSize;
198
+ }
199
+
200
+ // Assemble truncated context
201
+ const parts = [header];
202
+
203
+ if (issueOpened) {
204
+ parts.push(issueOpened);
205
+ }
206
+
207
+ if (recentLines.length < middleLines.length) {
208
+ // Some messages were truncated
209
+ const truncatedCount = middleLines.length - recentLines.length;
210
+ parts.push(
211
+ `\n[...${truncatedCount} earlier context messages truncated to prevent overflow...]\n`
212
+ );
213
+ }
214
+
215
+ if (recentLines.length > 0) {
216
+ parts.push(recentLines.join('\n'));
217
+ }
218
+
219
+ parts.push(triggeringMsg);
220
+
221
+ context = parts.join('\n');
222
+
223
+ const truncatedLength = context.length;
224
+ console.log(
225
+ `[Context] Truncated from ${originalLength} to ${truncatedLength} chars (${Math.round((truncatedLength / originalLength) * 100)}% retained)`
226
+ );
227
+ }
228
+
229
+ // Legacy maxTokens check (for backward compatibility with agent configs)
230
+ const maxTokens = strategy.maxTokens || 100000;
231
+ const maxChars = maxTokens * 4;
232
+ if (context.length > maxChars) {
233
+ context = context.slice(0, maxChars) + '\n\n[Context truncated...]';
234
+ }
235
+
236
+ return context;
237
+ }
238
+
239
+ module.exports = {
240
+ buildContext,
241
+ };
@@ -0,0 +1,329 @@
1
+ /**
2
+ * AgentHookExecutor - Hook transformation and execution
3
+ *
4
+ * Provides:
5
+ * - Hook execution (publish_message, stop_cluster, etc.)
6
+ * - Template variable substitution
7
+ * - Transform script execution in VM sandbox
8
+ */
9
+
10
+ const vm = require('vm');
11
+
12
+ /**
13
+ * Execute a hook
14
+ * THROWS on failure - no silent errors
15
+ * @param {Object} params - Hook execution parameters
16
+ * @param {Object} params.hook - Hook configuration
17
+ * @param {Object} params.agent - Agent instance
18
+ * @param {Object} params.message - Triggering message
19
+ * @param {Object} params.result - Agent execution result
20
+ * @param {Object} params.messageBus - Message bus instance
21
+ * @param {Object} params.cluster - Cluster object
22
+ * @param {Object} params.orchestrator - Orchestrator instance
23
+ * @returns {Promise<void>}
24
+ */
25
+ function executeHook(params) {
26
+ const { hook, agent, message, result, cluster } = params;
27
+
28
+ if (!hook) {
29
+ return;
30
+ }
31
+
32
+ // Build context for hook execution
33
+ const context = {
34
+ result,
35
+ triggeringMessage: message,
36
+ agent,
37
+ cluster,
38
+ };
39
+
40
+ // NO try/catch - errors must propagate
41
+ 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
62
+ agent._publish(messageToPublish);
63
+ } else if (hook.action === 'execute_system_command') {
64
+ throw new Error('execute_system_command not implemented');
65
+ } else {
66
+ throw new Error(`Unknown hook action: ${hook.action}`);
67
+ }
68
+ }
69
+
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;
85
+
86
+ if (engine !== 'javascript') {
87
+ throw new Error(`Unsupported transform engine: ${engine}`);
88
+ }
89
+
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;
94
+
95
+ if (context.result?.output) {
96
+ resultData = agent._parseResultOutput(context.result.output);
97
+ } else if (scriptUsesResult) {
98
+ const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
99
+ const outputLength = (context.result?.output || '').length;
100
+ throw new Error(
101
+ `Transform script uses result.* variables but no output was captured. ` +
102
+ `Agent: ${agent.id}, TaskID: ${taskId}, Iteration: ${agent.iteration}, ` +
103
+ `Output length: ${outputLength}. ` +
104
+ `Check that the task completed successfully and the get-log-path command exists.`
105
+ );
106
+ }
107
+
108
+ // Helper functions exposed to transform scripts
109
+ const helpers = {
110
+ /**
111
+ * Get cluster config based on complexity and task type
112
+ * Returns: { base: 'template-name', params: { ... } }
113
+ */
114
+ getConfig: require('../config-router').getConfig,
115
+ };
116
+
117
+ // Build sandbox context
118
+ const sandbox = {
119
+ result: resultData,
120
+ triggeringMessage: context.triggeringMessage,
121
+ helpers,
122
+ JSON,
123
+ console: {
124
+ log: (...args) => agent._log('[transform]', ...args),
125
+ error: (...args) => console.error('[transform]', ...args),
126
+ warn: (...args) => console.warn('[transform]', ...args),
127
+ },
128
+ };
129
+
130
+ // Execute in VM sandbox with timeout
131
+ const vmContext = vm.createContext(sandbox);
132
+ const wrappedScript = `(function() { ${script} })()`;
133
+
134
+ let result;
135
+ try {
136
+ result = vm.runInContext(wrappedScript, vmContext, { timeout: 5000 });
137
+ } catch (err) {
138
+ throw new Error(`Transform script error: ${err.message}`);
139
+ }
140
+
141
+ // Validate result
142
+ if (!result || typeof result !== 'object') {
143
+ throw new Error(
144
+ `Transform script must return an object with topic and content, got: ${typeof result}`
145
+ );
146
+ }
147
+ if (!result.topic) {
148
+ throw new Error(`Transform script result must have a 'topic' property`);
149
+ }
150
+ if (!result.content) {
151
+ throw new Error(`Transform script result must have a 'content' property`);
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Substitute template variables in hook config
159
+ * ONLY parses result output if result.* variables are used
160
+ * THROWS on any error - no silent failures
161
+ * @param {Object} params - Substitution parameters
162
+ * @param {Object} params.config - Hook configuration
163
+ * @param {Object} params.context - Execution context
164
+ * @param {Object} params.agent - Agent instance
165
+ * @param {Object} params.cluster - Cluster object
166
+ * @returns {Object} Substituted configuration
167
+ */
168
+ function substituteTemplate(params) {
169
+ const { config, context, agent, cluster } = params;
170
+
171
+ if (!config) {
172
+ throw new Error('_substituteTemplate: config is required');
173
+ }
174
+
175
+ const json = JSON.stringify(config);
176
+
177
+ // Check if ANY result.* variables are used BEFORE parsing
178
+ // Generic pattern - no hardcoded field names, works with any agent config
179
+ const usesResultVars = /\{\{result\.[^}]+\}\}/.test(json);
180
+
181
+ let resultData = null;
182
+ if (usesResultVars) {
183
+ if (!context.result) {
184
+ throw new Error(
185
+ `Hook uses result.* variables but no result in context. ` +
186
+ `Agent: ${agent.id}, TaskID: ${agent.currentTaskId}, Iteration: ${agent.iteration}`
187
+ );
188
+ }
189
+ if (!context.result.output) {
190
+ // Log detailed context for debugging
191
+ const taskId = context.result.taskId || agent.currentTaskId || 'UNKNOWN';
192
+ console.error(`\n${'='.repeat(80)}`);
193
+ console.error(`🔴 HOOK FAILURE - EMPTY OUTPUT`);
194
+ console.error(`${'='.repeat(80)}`);
195
+ console.error(`Agent: ${agent.id}`);
196
+ console.error(`Task ID: ${taskId}`);
197
+ console.error(`Iteration: ${context.result.iteration || agent.iteration}`);
198
+ console.error(`Result success: ${context.result.success}`);
199
+ console.error(`Result error: ${context.result.error || 'none'}`);
200
+ console.error(`Output length: ${(context.result.output || '').length}`);
201
+ console.error(`Hook config: ${JSON.stringify(config, null, 2)}`);
202
+
203
+ // Auto-fetch and publish task logs for debugging
204
+ let taskLogs = 'Task logs unavailable';
205
+ if (taskId !== 'UNKNOWN') {
206
+ console.error(`\nFetching task logs for ${taskId}...`);
207
+ try {
208
+ const { execSync } = require('child_process');
209
+ const ctPath = agent._getClaudeTasksPath();
210
+ taskLogs = execSync(`${ctPath} logs ${taskId} --lines 100`, {
211
+ encoding: 'utf-8',
212
+ timeout: 5000,
213
+ maxBuffer: 1024 * 1024, // 1MB
214
+ }).trim();
215
+ console.error(`✓ Retrieved ${taskLogs.split('\n').length} lines of logs`);
216
+ } catch (err) {
217
+ taskLogs = `Failed to retrieve logs: ${err.message}`;
218
+ console.error(`✗ Failed to retrieve logs: ${err.message}`);
219
+ }
220
+ }
221
+ console.error(`${'='.repeat(80)}\n`);
222
+
223
+ // Publish task logs to message bus for visibility in zeroshot logs
224
+ agent._publish({
225
+ topic: 'AGENT_ERROR',
226
+ receiver: 'broadcast',
227
+ content: {
228
+ text: `Task logs for ${taskId} (last 100 lines)`,
229
+ data: {
230
+ taskId,
231
+ logs: taskLogs,
232
+ logsPreview: taskLogs.split('\n').slice(-20).join('\n'), // Last 20 lines as preview
233
+ },
234
+ },
235
+ });
236
+
237
+ throw new Error(
238
+ `Hook uses result.* variables but result.output is empty. ` +
239
+ `Agent: ${agent.id}, TaskID: ${taskId}, ` +
240
+ `Iteration: ${context.result.iteration || agent.iteration}, ` +
241
+ `Success: ${context.result.success}. ` +
242
+ `Task logs posted to message bus.`
243
+ );
244
+ }
245
+ // Parse result output - WILL THROW if no JSON block
246
+ resultData = agent._parseResultOutput(context.result.output);
247
+ }
248
+
249
+ // Helper to escape a value for JSON string substitution
250
+ // Uses JSON.stringify for ALL escaping - no manual replace() calls
251
+ const escapeForJsonString = (value) => {
252
+ if (value === null || value === undefined) {
253
+ throw new Error(`Cannot escape null/undefined value for JSON`);
254
+ }
255
+ // JSON.stringify handles ALL escaping (newlines, quotes, backslashes, control chars)
256
+ // .slice(1, -1) strips the outer quotes it adds
257
+ // For arrays/objects: stringify twice - once for JSON, once to escape for string embedding
258
+ const stringified =
259
+ typeof value === 'string' ? JSON.stringify(value) : JSON.stringify(JSON.stringify(value));
260
+ return stringified.slice(1, -1);
261
+ };
262
+
263
+ let substituted = json
264
+ .replace(/\{\{cluster\.id\}\}/g, cluster.id)
265
+ .replace(/\{\{cluster\.createdAt\}\}/g, String(cluster.createdAt))
266
+ .replace(/\{\{iteration\}\}/g, String(agent.iteration))
267
+ .replace(/\{\{error\.message\}\}/g, escapeForJsonString(context.error?.message ?? ''))
268
+ .replace(/\{\{result\.output\}\}/g, escapeForJsonString(context.result?.output ?? ''));
269
+
270
+ // Substitute ALL result.* variables dynamically from parsed resultData
271
+ if (resultData) {
272
+ // Generic substitution - replace {{result.fieldName}} with resultData[fieldName]
273
+ // No hardcoded field names - works with any agent output schema
274
+ // CRITICAL: For booleans/nulls/numbers, we need to match and remove surrounding quotes
275
+ // to produce valid JSON (e.g., "{{result.approved}}" -> true, not "true")
276
+ substituted = substituted.replace(/"?\{\{result\.([^}]+)\}\}"?/g, (match, fieldName) => {
277
+ const value = resultData[fieldName];
278
+ if (value === undefined) {
279
+ // Missing fields should gracefully default to null or empty values
280
+ // This allows optional schema fields without hardcoding field names
281
+ // If a field is truly required, the schema validation will catch it
282
+ console.warn(
283
+ `⚠️ Agent ${agent.id}: Template variable {{result.${fieldName}}} not found in output. ` +
284
+ `If this field is required by the schema, the agent violated its own schema. ` +
285
+ `Defaulting to null. Agent output keys: ${Object.keys(resultData).join(', ')}`
286
+ );
287
+ return 'null';
288
+ }
289
+ // Booleans, numbers, and null should be unquoted JSON primitives
290
+ if (typeof value === 'boolean' || typeof value === 'number' || value === null) {
291
+ return String(value);
292
+ }
293
+ // Strings need to be quoted and escaped for JSON
294
+ return JSON.stringify(value);
295
+ });
296
+ }
297
+
298
+ // Check for unsubstituted KNOWN template variables only
299
+ // KNOWN patterns: {{cluster.X}}, {{iteration}}, {{error.X}}, {{result.X}}
300
+ // Content may contain arbitrary {{...}} patterns (React dangerouslySetInnerHTML, Mustache, etc.)
301
+ // Those are NOT template variables - they're just content that happens to contain braces
302
+ const KNOWN_TEMPLATE_PREFIXES = ['cluster', 'iteration', 'error', 'result'];
303
+ const knownVariablePattern = new RegExp(
304
+ `\\{\\{(${KNOWN_TEMPLATE_PREFIXES.join('|')})(\\.[a-zA-Z_][a-zA-Z0-9_]*)?\\}\\}`,
305
+ 'g'
306
+ );
307
+ const remaining = substituted.match(knownVariablePattern);
308
+ if (remaining) {
309
+ throw new Error(`Unsubstituted template variables: ${remaining.join(', ')}`);
310
+ }
311
+
312
+ // Parse and validate result
313
+ let result;
314
+ try {
315
+ result = JSON.parse(substituted);
316
+ } catch (e) {
317
+ console.error('JSON parse failed. Substituted string:');
318
+ console.error(substituted);
319
+ throw new Error(`Template substitution produced invalid JSON: ${e.message}`);
320
+ }
321
+
322
+ return result;
323
+ }
324
+
325
+ module.exports = {
326
+ executeHook,
327
+ executeTransform,
328
+ substituteTemplate,
329
+ };