@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
@@ -14,41 +14,45 @@
14
14
  const MAX_CONTEXT_CHARS = 500000;
15
15
 
16
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
- * @param {object} [params.worktree] - Worktree isolation state (if running in worktree mode)
29
- * @param {object} [params.isolation] - Docker isolation state (if running in Docker mode)
30
- * @returns {string} Assembled context string
17
+ * Generate an example object from a JSON schema
18
+ * Used to show models a concrete example of expected output
19
+ *
20
+ * @param {object} schema - JSON schema
21
+ * @returns {object|null} Example object or null if generation fails
31
22
  */
32
- function buildContext({
33
- id,
34
- role,
35
- iteration,
36
- config,
37
- messageBus,
38
- cluster,
39
- lastTaskEndTime,
40
- triggeringMessage,
41
- selectedPrompt,
42
- worktree,
43
- isolation,
44
- }) {
45
- const strategy = config.contextStrategy || { sources: [] };
23
+ function generateExampleFromSchema(schema) {
24
+ if (!schema || schema.type !== 'object' || !schema.properties) {
25
+ return null;
26
+ }
46
27
 
47
- let context = `You are agent "${id}" with role "${role}".\n\n`;
48
- context += `Iteration: ${iteration}\n\n`;
28
+ const example = {};
29
+
30
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
31
+ if (propSchema.enum && propSchema.enum.length > 0) {
32
+ // Use first enum value as example
33
+ example[key] = propSchema.enum[0];
34
+ } else if (propSchema.type === 'string') {
35
+ example[key] = propSchema.description || `${key} value`;
36
+ } else if (propSchema.type === 'boolean') {
37
+ example[key] = true;
38
+ } else if (propSchema.type === 'number' || propSchema.type === 'integer') {
39
+ example[key] = 0;
40
+ } else if (propSchema.type === 'array') {
41
+ if (propSchema.items?.type === 'string') {
42
+ example[key] = [];
43
+ } else {
44
+ example[key] = [];
45
+ }
46
+ } else if (propSchema.type === 'object') {
47
+ example[key] = generateExampleFromSchema(propSchema) || {};
48
+ }
49
+ }
49
50
 
50
- // GLOBAL RULE: NEVER ASK QUESTIONS - Vibe agents run non-interactively
51
- context += `## 🔴 CRITICAL: AUTONOMOUS EXECUTION REQUIRED\n\n`;
51
+ return example;
52
+ }
53
+
54
+ function buildAutonomousSection() {
55
+ let context = `## 🔴 CRITICAL: AUTONOMOUS EXECUTION REQUIRED\n\n`;
52
56
  context += `You are running in a NON-INTERACTIVE cluster environment.\n\n`;
53
57
  context += `**NEVER** use AskUserQuestion or ask for user input - there is NO user to respond.\n`;
54
58
  context += `**NEVER** ask "Would you like me to..." or "Should I..." - JUST DO IT.\n`;
@@ -57,9 +61,11 @@ function buildContext({
57
61
  context += `- Choose the option that maintains code quality and correctness\n`;
58
62
  context += `- If unsure between "fix the code" vs "relax the rules" → ALWAYS fix the code\n`;
59
63
  context += `- If unsure between "do more" vs "do less" → ALWAYS do what's required, nothing more\n\n`;
64
+ return context;
65
+ }
60
66
 
61
- // OUTPUT STYLE - NON-NEGOTIABLE
62
- context += `## 🔴 OUTPUT STYLE - NON-NEGOTIABLE\n\n`;
67
+ function buildOutputStyleSection() {
68
+ let context = `## 🔴 OUTPUT STYLE - NON-NEGOTIABLE\n\n`;
63
69
  context += `**ALL OUTPUT: Maximum informativeness, minimum verbosity. NO EXCEPTIONS.**\n\n`;
64
70
  context += `This applies to EVERYTHING you output:\n`;
65
71
  context += `- Text responses\n`;
@@ -74,59 +80,137 @@ function buildContext({
74
80
  context += `- Errors: DETAILED (stack traces, repro). NEVER compress errors.\n`;
75
81
  context += `- FORBIDDEN: "I'll help...", "Let me...", "I'm going to...", "Sure!", "Great!", "Certainly!"\n\n`;
76
82
  context += `Every token costs money. Waste nothing.\n\n`;
83
+ return context;
84
+ }
77
85
 
78
- // GIT OPERATIONS RESTRICTION - Only when NOT running in isolation mode
79
- // When isolated (worktree or Docker), agents CAN commit/push safely
80
- // When NOT isolated, agents are on main branch - git operations are dangerous
81
- const isIsolated = !!(worktree?.enabled || isolation?.enabled);
86
+ function buildGitOperationsSection() {
87
+ let context = `## 🚫 GIT OPERATIONS - FORBIDDEN\n\n`;
88
+ context += `NEVER commit, push, or create PRs. You only modify files.\n`;
89
+ context += `The git-pusher agent handles ALL git operations AFTER validators approve.\n\n`;
90
+ context += `- ❌ NEVER run: git add, git commit, git push, gh pr create\n`;
91
+ context += `- ❌ NEVER suggest committing changes\n`;
92
+ context += `- ✅ Only modify files and publish your completion message when done\n\n`;
93
+ return context;
94
+ }
95
+
96
+ function buildHeaderContext({ id, role, iteration, isIsolated }) {
97
+ let context = `You are agent "${id}" with role "${role}".\n\n`;
98
+ context += `Iteration: ${iteration}\n\n`;
99
+ context += buildAutonomousSection();
100
+ context += buildOutputStyleSection();
82
101
  if (!isIsolated) {
83
- context += `## 🚫 GIT OPERATIONS - FORBIDDEN\n\n`;
84
- context += `NEVER commit, push, or create PRs. You only modify files.\n`;
85
- context += `The git-pusher agent handles ALL git operations AFTER validators approve.\n\n`;
86
- context += `- ❌ NEVER run: git add, git commit, git push, gh pr create\n`;
87
- context += `- ❌ NEVER suggest committing changes\n`;
88
- context += `- ✅ Only modify files and publish your completion message when done\n\n`;
102
+ context += buildGitOperationsSection();
89
103
  }
104
+ return context;
105
+ }
90
106
 
91
- // Add prompt from config (system prompt, instructions, output format)
92
- // If selectedPrompt is provided (iteration-based), use it directly
93
- // Otherwise fall back to legacy config.prompt handling
94
- const promptText = selectedPrompt || (typeof config.prompt === 'string' ? config.prompt : config.prompt?.system);
107
+ function buildInstructionsSection({ config, selectedPrompt, id }) {
108
+ const promptText =
109
+ selectedPrompt || (typeof config.prompt === 'string' ? config.prompt : config.prompt?.system);
95
110
 
96
111
  if (promptText) {
97
- context += `## Instructions\n\n${promptText}\n\n`;
98
- } else if (config.prompt && typeof config.prompt !== 'string' && !config.prompt?.system) {
99
- // FAIL HARD: prompt exists but format is unrecognized (and no selectedPrompt provided)
112
+ return `## Instructions\n\n${promptText}\n\n`;
113
+ }
114
+
115
+ if (config.prompt && typeof config.prompt !== 'string' && !config.prompt?.system) {
100
116
  throw new Error(
101
117
  `Agent "${id}" has invalid prompt format. ` +
102
118
  `Expected string or object with .system property, got: ${JSON.stringify(config.prompt).slice(0, 100)}...`
103
119
  );
104
120
  }
105
121
 
106
- // Output format schema (if configured)
107
- if (config.prompt?.outputFormat) {
108
- context += `## Output Schema (REQUIRED)\n\n`;
109
- context += `\`\`\`json\n${JSON.stringify(config.prompt.outputFormat.example, null, 2)}\n\`\`\`\n\n`;
110
- context += `STRING VALUES IN THIS SCHEMA: Dense. Factual. No filler words. No pleasantries.\n`;
111
- if (config.prompt.outputFormat.rules) {
112
- for (const rule of config.prompt.outputFormat.rules) {
113
- context += `- ${rule}\n`;
114
- }
122
+ return '';
123
+ }
124
+
125
+ function buildLegacyOutputSchemaSection(config) {
126
+ if (!config.prompt?.outputFormat) return '';
127
+
128
+ let context = `## Output Schema (REQUIRED)\n\n`;
129
+ context += `\`\`\`json\n${JSON.stringify(config.prompt.outputFormat.example, null, 2)}\n\`\`\`\n\n`;
130
+ context += `STRING VALUES IN THIS SCHEMA: Dense. Factual. No filler words. No pleasantries.\n`;
131
+ if (config.prompt.outputFormat.rules) {
132
+ for (const rule of config.prompt.outputFormat.rules) {
133
+ context += `- ${rule}\n`;
115
134
  }
116
- context += '\n';
117
135
  }
136
+ context += '\n';
137
+ return context;
138
+ }
118
139
 
119
- // Add sources
120
- for (const source of strategy.sources) {
121
- // Resolve special 'since' values
122
- let sinceTimestamp = source.since;
123
- if (source.since === 'cluster_start') {
124
- sinceTimestamp = cluster.createdAt;
125
- } else if (source.since === 'last_task_end') {
126
- // Use timestamp of last task completion, or cluster start if no tasks completed yet
127
- sinceTimestamp = lastTaskEndTime || cluster.createdAt;
140
+ function buildJsonSchemaSection(config) {
141
+ if (!config.jsonSchema || config.outputFormat !== 'json') return '';
142
+
143
+ let context = `## 🔴 OUTPUT FORMAT - JSON ONLY\n\n`;
144
+ context += `Your response must be ONLY valid JSON. No other text before or after.\n`;
145
+ context += `Start with { and end with }. Nothing else.\n\n`;
146
+ context += `Required schema:\n`;
147
+ context += `\`\`\`json\n${JSON.stringify(config.jsonSchema, null, 2)}\n\`\`\`\n\n`;
148
+
149
+ const example = generateExampleFromSchema(config.jsonSchema);
150
+ if (example) {
151
+ context += `Example output:\n`;
152
+ context += `\`\`\`json\n${JSON.stringify(example, null, 2)}\n\`\`\`\n\n`;
153
+ }
154
+
155
+ context += `CRITICAL RULES:\n`;
156
+ context += `- Output ONLY the JSON object - no explanation, no thinking, no preamble\n`;
157
+ context += `- Use EXACTLY the enum values specified (case-sensitive)\n`;
158
+ context += `- Include ALL required fields\n\n`;
159
+ return context;
160
+ }
161
+
162
+ function resolveSourceSince(source, cluster, lastTaskEndTime, lastAgentStartTime) {
163
+ const sinceValue = source.since;
164
+
165
+ if (sinceValue === 'cluster_start') {
166
+ return cluster.createdAt;
167
+ }
168
+ if (sinceValue === 'last_task_end') {
169
+ return lastTaskEndTime || cluster.createdAt;
170
+ }
171
+ if (sinceValue === 'last_agent_start') {
172
+ return lastAgentStartTime || cluster.createdAt;
173
+ }
174
+
175
+ if (typeof sinceValue === 'string') {
176
+ const parsed = Date.parse(sinceValue);
177
+ if (Number.isNaN(parsed)) {
178
+ throw new Error(
179
+ `Agent context source for topic ${source.topic} has invalid since value "${sinceValue}". ` +
180
+ 'Use cluster_start, last_task_end, last_agent_start, or an ISO timestamp.'
181
+ );
128
182
  }
183
+ return parsed;
184
+ }
185
+
186
+ return sinceValue;
187
+ }
129
188
 
189
+ function formatSourceMessagesSection(source, messages) {
190
+ let context = `\n## Messages from topic: ${source.topic}\n\n`;
191
+ for (const msg of messages) {
192
+ context += `[${new Date(msg.timestamp).toISOString()}] ${msg.sender}:\n`;
193
+ if (msg.content?.text) {
194
+ context += `${msg.content.text}\n`;
195
+ }
196
+ if (msg.content?.data) {
197
+ context += `Data: ${JSON.stringify(msg.content.data, null, 2)}\n`;
198
+ }
199
+ context += '\n';
200
+ }
201
+ return context;
202
+ }
203
+
204
+ function buildSourcesSection({
205
+ strategy,
206
+ messageBus,
207
+ cluster,
208
+ lastTaskEndTime,
209
+ lastAgentStartTime,
210
+ }) {
211
+ let context = '';
212
+ for (const source of strategy.sources) {
213
+ const sinceTimestamp = resolveSourceSince(source, cluster, lastTaskEndTime, lastAgentStartTime);
130
214
  const messages = messageBus.query({
131
215
  cluster_id: cluster.id,
132
216
  topic: source.topic,
@@ -136,134 +220,221 @@ function buildContext({
136
220
  });
137
221
 
138
222
  if (messages.length > 0) {
139
- context += `\n## Messages from topic: ${source.topic}\n\n`;
140
- for (const msg of messages) {
141
- context += `[${new Date(msg.timestamp).toISOString()}] ${msg.sender}:\n`;
142
- if (msg.content?.text) {
143
- context += `${msg.content.text}\n`;
144
- }
145
- if (msg.content?.data) {
146
- context += `Data: ${JSON.stringify(msg.content.data, null, 2)}\n`;
147
- }
148
- context += '\n';
149
- }
223
+ context += formatSourceMessagesSection(source, messages);
150
224
  }
151
225
  }
226
+ return context;
227
+ }
152
228
 
153
- // Add triggering message
154
- context += `\n## Triggering Message\n\n`;
155
- context += `Topic: ${triggeringMessage.topic}\n`;
156
- context += `Sender: ${triggeringMessage.sender}\n`;
157
- if (triggeringMessage.content?.text) {
158
- context += `\n${triggeringMessage.content.text}\n`;
229
+ function collectCannotValidateCriteria(prevValidations) {
230
+ const cannotValidateCriteria = [];
231
+ for (const msg of prevValidations) {
232
+ const criteriaResults = msg.content?.data?.criteriaResults;
233
+ if (!Array.isArray(criteriaResults)) continue;
234
+ for (const cr of criteriaResults) {
235
+ if (cr.status !== 'CANNOT_VALIDATE' || !cr.id) continue;
236
+ if (cannotValidateCriteria.find((c) => c.id === cr.id)) continue;
237
+ cannotValidateCriteria.push({
238
+ id: cr.id,
239
+ reason: cr.reason || 'No reason provided',
240
+ });
241
+ }
159
242
  }
243
+ return cannotValidateCriteria;
244
+ }
160
245
 
161
- // DEFENSIVE TRUNCATION - Prevent context overflow errors
162
- // Strategy: Keep ISSUE_OPENED (original task) + most recent messages
163
- // Truncate from MIDDLE (oldest context messages) if too long
164
- const originalLength = context.length;
246
+ function buildCannotValidateSection(cannotValidateCriteria) {
247
+ if (cannotValidateCriteria.length === 0) return '';
165
248
 
166
- if (originalLength > MAX_CONTEXT_CHARS) {
167
- console.log(
168
- `[Context] Context too large (${originalLength} chars), truncating to prevent overflow...`
169
- );
249
+ let context = `\n## ⚠️ Permanently Unverifiable Criteria (SKIP THESE)\n\n`;
250
+ context += `The following criteria have PERMANENT environmental limitations (missing tools, no access).\n`;
251
+ context += `These limitations have not changed. Do NOT re-attempt verification.\n`;
252
+ context += `Mark these as CANNOT_VALIDATE again with the same reason.\n\n`;
253
+ for (const cv of cannotValidateCriteria) {
254
+ context += `- **${cv.id}**: ${cv.reason}\n`;
255
+ }
256
+ context += `\n`;
257
+ return context;
258
+ }
170
259
 
171
- // Split context into sections
172
- const lines = context.split('\n');
260
+ function buildValidatorSkipSection({ role, messageBus, cluster }) {
261
+ if (role !== 'validator') return '';
173
262
 
174
- // Find critical sections that must be preserved
175
- let issueOpenedStart = -1;
176
- let issueOpenedEnd = -1;
177
- let triggeringStart = -1;
263
+ const prevValidations = messageBus.query({
264
+ cluster_id: cluster.id,
265
+ topic: 'VALIDATION_RESULT',
266
+ since: cluster.createdAt,
267
+ limit: 50,
268
+ });
178
269
 
179
- for (let i = 0; i < lines.length; i++) {
180
- if (lines[i].includes('## Messages from topic: ISSUE_OPENED')) {
181
- issueOpenedStart = i;
182
- }
183
- if (issueOpenedStart !== -1 && issueOpenedEnd === -1 && lines[i].startsWith('## ')) {
184
- issueOpenedEnd = i;
185
- }
186
- if (lines[i].includes('## Triggering Message')) {
187
- triggeringStart = i;
188
- break;
189
- }
190
- }
270
+ const cannotValidateCriteria = collectCannotValidateCriteria(prevValidations);
271
+ return buildCannotValidateSection(cannotValidateCriteria);
272
+ }
191
273
 
192
- // Build truncated context:
193
- // 1. Header (agent info, instructions, output format)
194
- // 2. ISSUE_OPENED message (original task - NEVER truncate)
195
- // 3. Most recent N messages (whatever fits in budget)
196
- // 4. Triggering message (current event)
274
+ function buildTriggeringMessageSection(triggeringMessage) {
275
+ let context = `\n## Triggering Message\n\n`;
276
+ context += `Topic: ${triggeringMessage.topic}\n`;
277
+ context += `Sender: ${triggeringMessage.sender}\n`;
278
+ if (triggeringMessage.content?.text) {
279
+ context += `\n${triggeringMessage.content.text}\n`;
280
+ }
281
+ return context;
282
+ }
197
283
 
198
- const headerEnd = issueOpenedStart !== -1 ? issueOpenedStart : triggeringStart;
199
- const header = lines.slice(0, headerEnd).join('\n');
284
+ function findContextSectionIndices(lines) {
285
+ let issueOpenedStart = -1;
286
+ let issueOpenedEnd = -1;
287
+ let triggeringStart = -1;
200
288
 
201
- const issueOpened =
202
- issueOpenedStart !== -1 && issueOpenedEnd !== -1
203
- ? lines.slice(issueOpenedStart, issueOpenedEnd).join('\n')
204
- : '';
289
+ for (let i = 0; i < lines.length; i++) {
290
+ if (lines[i].includes('## Messages from topic: ISSUE_OPENED')) {
291
+ issueOpenedStart = i;
292
+ }
293
+ if (issueOpenedStart !== -1 && issueOpenedEnd === -1 && lines[i].startsWith('## ')) {
294
+ issueOpenedEnd = i;
295
+ }
296
+ if (lines[i].includes('## Triggering Message')) {
297
+ triggeringStart = i;
298
+ break;
299
+ }
300
+ }
205
301
 
206
- const triggeringMsg = lines.slice(triggeringStart).join('\n');
302
+ return { issueOpenedStart, issueOpenedEnd, triggeringStart };
303
+ }
207
304
 
208
- // Calculate remaining budget for recent messages
209
- const fixedSize = header.length + issueOpened.length + triggeringMsg.length;
210
- const budgetForRecent = MAX_CONTEXT_CHARS - fixedSize - 200; // 200 char buffer for markers
305
+ function collectRecentLines(middleLines, budgetForRecent) {
306
+ const recentLines = [];
307
+ let recentSize = 0;
211
308
 
212
- // Collect recent messages (from end backwards until budget exhausted)
213
- const recentLines = [];
214
- let recentSize = 0;
309
+ for (let i = middleLines.length - 1; i >= 0; i--) {
310
+ const line = middleLines[i];
311
+ const lineSize = line.length + 1;
215
312
 
216
- const middleStart = issueOpenedEnd !== -1 ? issueOpenedEnd : headerEnd;
217
- const middleEnd = triggeringStart;
218
- const middleLines = lines.slice(middleStart, middleEnd);
313
+ if (recentSize + lineSize > budgetForRecent) {
314
+ break;
315
+ }
219
316
 
220
- for (let i = middleLines.length - 1; i >= 0; i--) {
221
- const line = middleLines[i];
222
- const lineSize = line.length + 1; // +1 for newline
317
+ recentLines.unshift(line);
318
+ recentSize += lineSize;
319
+ }
223
320
 
224
- if (recentSize + lineSize > budgetForRecent) {
225
- break; // Budget exhausted
226
- }
321
+ return recentLines;
322
+ }
227
323
 
228
- recentLines.unshift(line);
229
- recentSize += lineSize;
230
- }
324
+ function truncateContextIfNeeded(context) {
325
+ const originalLength = context.length;
326
+ if (originalLength <= MAX_CONTEXT_CHARS) {
327
+ return context;
328
+ }
231
329
 
232
- // Assemble truncated context
233
- const parts = [header];
330
+ console.log(
331
+ `[Context] Context too large (${originalLength} chars), truncating to prevent overflow...`
332
+ );
234
333
 
235
- if (issueOpened) {
236
- parts.push(issueOpened);
237
- }
334
+ const lines = context.split('\n');
335
+ const { issueOpenedStart, issueOpenedEnd, triggeringStart } = findContextSectionIndices(lines);
238
336
 
239
- if (recentLines.length < middleLines.length) {
240
- // Some messages were truncated
241
- const truncatedCount = middleLines.length - recentLines.length;
242
- parts.push(
243
- `\n[...${truncatedCount} earlier context messages truncated to prevent overflow...]\n`
244
- );
245
- }
337
+ const headerEnd = issueOpenedStart !== -1 ? issueOpenedStart : triggeringStart;
338
+ const header = lines.slice(0, headerEnd).join('\n');
246
339
 
247
- if (recentLines.length > 0) {
248
- parts.push(recentLines.join('\n'));
249
- }
340
+ const issueOpened =
341
+ issueOpenedStart !== -1 && issueOpenedEnd !== -1
342
+ ? lines.slice(issueOpenedStart, issueOpenedEnd).join('\n')
343
+ : '';
344
+
345
+ const triggeringMsg = lines.slice(triggeringStart).join('\n');
250
346
 
251
- parts.push(triggeringMsg);
347
+ const fixedSize = header.length + issueOpened.length + triggeringMsg.length;
348
+ const budgetForRecent = MAX_CONTEXT_CHARS - fixedSize - 200;
252
349
 
253
- context = parts.join('\n');
350
+ const middleStart = issueOpenedEnd !== -1 ? issueOpenedEnd : headerEnd;
351
+ const middleEnd = triggeringStart;
352
+ const middleLines = lines.slice(middleStart, middleEnd);
353
+ const recentLines = collectRecentLines(middleLines, budgetForRecent);
254
354
 
255
- const truncatedLength = context.length;
256
- console.log(
257
- `[Context] Truncated from ${originalLength} to ${truncatedLength} chars (${Math.round((truncatedLength / originalLength) * 100)}% retained)`
355
+ const parts = [header];
356
+ if (issueOpened) {
357
+ parts.push(issueOpened);
358
+ }
359
+ if (recentLines.length < middleLines.length) {
360
+ const truncatedCount = middleLines.length - recentLines.length;
361
+ parts.push(
362
+ `\n[...${truncatedCount} earlier context messages truncated to prevent overflow...]\n`
258
363
  );
259
364
  }
365
+ if (recentLines.length > 0) {
366
+ parts.push(recentLines.join('\n'));
367
+ }
368
+ parts.push(triggeringMsg);
369
+
370
+ const truncatedContext = parts.join('\n');
371
+ const truncatedLength = truncatedContext.length;
372
+ console.log(
373
+ `[Context] Truncated from ${originalLength} to ${truncatedLength} chars (${Math.round((truncatedLength / originalLength) * 100)}% retained)`
374
+ );
260
375
 
261
- // Legacy maxTokens check (for backward compatibility with agent configs)
376
+ return truncatedContext;
377
+ }
378
+
379
+ function applyLegacyMaxTokens(context, strategy) {
262
380
  const maxTokens = strategy.maxTokens || 100000;
263
381
  const maxChars = maxTokens * 4;
264
382
  if (context.length > maxChars) {
265
- context = context.slice(0, maxChars) + '\n\n[Context truncated...]';
383
+ return context.slice(0, maxChars) + '\n\n[Context truncated...]';
266
384
  }
385
+ return context;
386
+ }
387
+
388
+ /**
389
+ * Build execution context for an agent
390
+ * @param {object} params - Context building parameters
391
+ * @param {string} params.id - Agent ID
392
+ * @param {string} params.role - Agent role
393
+ * @param {number} params.iteration - Current iteration number
394
+ * @param {any} params.config - Agent configuration
395
+ * @param {any} params.messageBus - Message bus for querying ledger
396
+ * @param {any} params.cluster - Cluster object
397
+ * @param {number} [params.lastTaskEndTime] - Timestamp of last task completion
398
+ * @param {number} [params.lastAgentStartTime] - Timestamp when this agent last started work
399
+ * @param {any} params.triggeringMessage - Message that triggered this execution
400
+ * @param {string} [params.selectedPrompt] - Pre-selected prompt from _selectPrompt() (iteration-based)
401
+ * @param {object} [params.worktree] - Worktree isolation state (if running in worktree mode)
402
+ * @param {object} [params.isolation] - Docker isolation state (if running in Docker mode)
403
+ * @returns {string} Assembled context string
404
+ */
405
+ function buildContext({
406
+ id,
407
+ role,
408
+ iteration,
409
+ config,
410
+ messageBus,
411
+ cluster,
412
+ lastTaskEndTime,
413
+ lastAgentStartTime,
414
+ triggeringMessage,
415
+ selectedPrompt,
416
+ worktree,
417
+ isolation,
418
+ }) {
419
+ const strategy = config.contextStrategy || { sources: [] };
420
+ const isIsolated = !!(worktree?.enabled || isolation?.enabled);
421
+
422
+ let context = buildHeaderContext({ id, role, iteration, isIsolated });
423
+ context += buildInstructionsSection({ config, selectedPrompt, id });
424
+ context += buildLegacyOutputSchemaSection(config);
425
+ context += buildJsonSchemaSection(config);
426
+ context += buildSourcesSection({
427
+ strategy,
428
+ messageBus,
429
+ cluster,
430
+ lastTaskEndTime,
431
+ lastAgentStartTime,
432
+ });
433
+ context += buildValidatorSkipSection({ role, messageBus, cluster });
434
+ context += buildTriggeringMessageSection(triggeringMessage);
435
+
436
+ context = truncateContextIfNeeded(context);
437
+ context = applyLegacyMaxTokens(context, strategy);
267
438
 
268
439
  return context;
269
440
  }