@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
@@ -51,42 +51,8 @@ function generateExampleFromSchema(schema) {
51
51
  return example;
52
52
  }
53
53
 
54
- /**
55
- * Build execution context for an agent
56
- * @param {object} params - Context building parameters
57
- * @param {string} params.id - Agent ID
58
- * @param {string} params.role - Agent role
59
- * @param {number} params.iteration - Current iteration number
60
- * @param {any} params.config - Agent configuration
61
- * @param {any} params.messageBus - Message bus for querying ledger
62
- * @param {any} params.cluster - Cluster object
63
- * @param {number} [params.lastTaskEndTime] - Timestamp of last task completion
64
- * @param {any} params.triggeringMessage - Message that triggered this execution
65
- * @param {string} [params.selectedPrompt] - Pre-selected prompt from _selectPrompt() (iteration-based)
66
- * @param {object} [params.worktree] - Worktree isolation state (if running in worktree mode)
67
- * @param {object} [params.isolation] - Docker isolation state (if running in Docker mode)
68
- * @returns {string} Assembled context string
69
- */
70
- function buildContext({
71
- id,
72
- role,
73
- iteration,
74
- config,
75
- messageBus,
76
- cluster,
77
- lastTaskEndTime,
78
- triggeringMessage,
79
- selectedPrompt,
80
- worktree,
81
- isolation,
82
- }) {
83
- const strategy = config.contextStrategy || { sources: [] };
84
-
85
- let context = `You are agent "${id}" with role "${role}".\n\n`;
86
- context += `Iteration: ${iteration}\n\n`;
87
-
88
- // GLOBAL RULE: NEVER ASK QUESTIONS - Vibe agents run non-interactively
89
- context += `## 🔴 CRITICAL: AUTONOMOUS EXECUTION REQUIRED\n\n`;
54
+ function buildAutonomousSection() {
55
+ let context = `## 🔴 CRITICAL: AUTONOMOUS EXECUTION REQUIRED\n\n`;
90
56
  context += `You are running in a NON-INTERACTIVE cluster environment.\n\n`;
91
57
  context += `**NEVER** use AskUserQuestion or ask for user input - there is NO user to respond.\n`;
92
58
  context += `**NEVER** ask "Would you like me to..." or "Should I..." - JUST DO IT.\n`;
@@ -95,9 +61,11 @@ function buildContext({
95
61
  context += `- Choose the option that maintains code quality and correctness\n`;
96
62
  context += `- If unsure between "fix the code" vs "relax the rules" → ALWAYS fix the code\n`;
97
63
  context += `- If unsure between "do more" vs "do less" → ALWAYS do what's required, nothing more\n\n`;
64
+ return context;
65
+ }
98
66
 
99
- // OUTPUT STYLE - NON-NEGOTIABLE
100
- context += `## 🔴 OUTPUT STYLE - NON-NEGOTIABLE\n\n`;
67
+ function buildOutputStyleSection() {
68
+ let context = `## 🔴 OUTPUT STYLE - NON-NEGOTIABLE\n\n`;
101
69
  context += `**ALL OUTPUT: Maximum informativeness, minimum verbosity. NO EXCEPTIONS.**\n\n`;
102
70
  context += `This applies to EVERYTHING you output:\n`;
103
71
  context += `- Text responses\n`;
@@ -112,83 +80,137 @@ function buildContext({
112
80
  context += `- Errors: DETAILED (stack traces, repro). NEVER compress errors.\n`;
113
81
  context += `- FORBIDDEN: "I'll help...", "Let me...", "I'm going to...", "Sure!", "Great!", "Certainly!"\n\n`;
114
82
  context += `Every token costs money. Waste nothing.\n\n`;
83
+ return context;
84
+ }
115
85
 
116
- // GIT OPERATIONS RESTRICTION - Only when NOT running in isolation mode
117
- // When isolated (worktree or Docker), agents CAN commit/push safely
118
- // When NOT isolated, agents are on main branch - git operations are dangerous
119
- 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();
120
101
  if (!isIsolated) {
121
- context += `## 🚫 GIT OPERATIONS - FORBIDDEN\n\n`;
122
- context += `NEVER commit, push, or create PRs. You only modify files.\n`;
123
- context += `The git-pusher agent handles ALL git operations AFTER validators approve.\n\n`;
124
- context += `- ❌ NEVER run: git add, git commit, git push, gh pr create\n`;
125
- context += `- ❌ NEVER suggest committing changes\n`;
126
- context += `- ✅ Only modify files and publish your completion message when done\n\n`;
102
+ context += buildGitOperationsSection();
127
103
  }
104
+ return context;
105
+ }
128
106
 
129
- // Add prompt from config (system prompt, instructions, output format)
130
- // If selectedPrompt is provided (iteration-based), use it directly
131
- // Otherwise fall back to legacy config.prompt handling
107
+ function buildInstructionsSection({ config, selectedPrompt, id }) {
132
108
  const promptText =
133
109
  selectedPrompt || (typeof config.prompt === 'string' ? config.prompt : config.prompt?.system);
134
110
 
135
111
  if (promptText) {
136
- context += `## Instructions\n\n${promptText}\n\n`;
137
- } else if (config.prompt && typeof config.prompt !== 'string' && !config.prompt?.system) {
138
- // 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) {
139
116
  throw new Error(
140
117
  `Agent "${id}" has invalid prompt format. ` +
141
118
  `Expected string or object with .system property, got: ${JSON.stringify(config.prompt).slice(0, 100)}...`
142
119
  );
143
120
  }
144
121
 
145
- // Output format schema (if configured via legacy format)
146
- if (config.prompt?.outputFormat) {
147
- context += `## Output Schema (REQUIRED)\n\n`;
148
- context += `\`\`\`json\n${JSON.stringify(config.prompt.outputFormat.example, null, 2)}\n\`\`\`\n\n`;
149
- context += `STRING VALUES IN THIS SCHEMA: Dense. Factual. No filler words. No pleasantries.\n`;
150
- if (config.prompt.outputFormat.rules) {
151
- for (const rule of config.prompt.outputFormat.rules) {
152
- context += `- ${rule}\n`;
153
- }
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`;
154
134
  }
155
- context += '\n';
156
135
  }
136
+ context += '\n';
137
+ return context;
138
+ }
157
139
 
158
- // AUTO-INJECT JSON OUTPUT INSTRUCTIONS when jsonSchema is defined
159
- // This ensures ALL agents with structured output schemas get explicit "output ONLY JSON" instructions
160
- // Critical for less capable models (Codex, Gemini) that output prose without explicit instructions
161
- if (config.jsonSchema && config.outputFormat === 'json') {
162
- context += `## 🔴 OUTPUT FORMAT - JSON ONLY\n\n`;
163
- context += `Your response must be ONLY valid JSON. No other text before or after.\n`;
164
- context += `Start with { and end with }. Nothing else.\n\n`;
165
- context += `Required schema:\n`;
166
- context += `\`\`\`json\n${JSON.stringify(config.jsonSchema, null, 2)}\n\`\`\`\n\n`;
167
-
168
- // Generate example from schema
169
- const example = generateExampleFromSchema(config.jsonSchema);
170
- if (example) {
171
- context += `Example output:\n`;
172
- context += `\`\`\`json\n${JSON.stringify(example, null, 2)}\n\`\`\`\n\n`;
173
- }
140
+ function buildJsonSchemaSection(config) {
141
+ if (!config.jsonSchema || config.outputFormat !== 'json') return '';
174
142
 
175
- context += `CRITICAL RULES:\n`;
176
- context += `- Output ONLY the JSON object - no explanation, no thinking, no preamble\n`;
177
- context += `- Use EXACTLY the enum values specified (case-sensitive)\n`;
178
- context += `- Include ALL required fields\n\n`;
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`;
179
153
  }
180
154
 
181
- // Add sources
182
- for (const source of strategy.sources) {
183
- // Resolve special 'since' values
184
- let sinceTimestamp = source.since;
185
- if (source.since === 'cluster_start') {
186
- sinceTimestamp = cluster.createdAt;
187
- } else if (source.since === 'last_task_end') {
188
- // Use timestamp of last task completion, or cluster start if no tasks completed yet
189
- sinceTimestamp = lastTaskEndTime || cluster.createdAt;
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
+ );
182
+ }
183
+ return parsed;
184
+ }
185
+
186
+ return sinceValue;
187
+ }
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`;
190
198
  }
199
+ context += '\n';
200
+ }
201
+ return context;
202
+ }
191
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);
192
214
  const messages = messageBus.query({
193
215
  cluster_id: cluster.id,
194
216
  topic: source.topic,
@@ -198,134 +220,221 @@ function buildContext({
198
220
  });
199
221
 
200
222
  if (messages.length > 0) {
201
- context += `\n## Messages from topic: ${source.topic}\n\n`;
202
- for (const msg of messages) {
203
- context += `[${new Date(msg.timestamp).toISOString()}] ${msg.sender}:\n`;
204
- if (msg.content?.text) {
205
- context += `${msg.content.text}\n`;
206
- }
207
- if (msg.content?.data) {
208
- context += `Data: ${JSON.stringify(msg.content.data, null, 2)}\n`;
209
- }
210
- context += '\n';
211
- }
223
+ context += formatSourceMessagesSection(source, messages);
212
224
  }
213
225
  }
226
+ return context;
227
+ }
214
228
 
215
- // Add triggering message
216
- context += `\n## Triggering Message\n\n`;
217
- context += `Topic: ${triggeringMessage.topic}\n`;
218
- context += `Sender: ${triggeringMessage.sender}\n`;
219
- if (triggeringMessage.content?.text) {
220
- 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
+ }
221
242
  }
243
+ return cannotValidateCriteria;
244
+ }
222
245
 
223
- // DEFENSIVE TRUNCATION - Prevent context overflow errors
224
- // Strategy: Keep ISSUE_OPENED (original task) + most recent messages
225
- // Truncate from MIDDLE (oldest context messages) if too long
226
- const originalLength = context.length;
246
+ function buildCannotValidateSection(cannotValidateCriteria) {
247
+ if (cannotValidateCriteria.length === 0) return '';
227
248
 
228
- if (originalLength > MAX_CONTEXT_CHARS) {
229
- console.log(
230
- `[Context] Context too large (${originalLength} chars), truncating to prevent overflow...`
231
- );
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
+ }
232
259
 
233
- // Split context into sections
234
- const lines = context.split('\n');
260
+ function buildValidatorSkipSection({ role, messageBus, cluster }) {
261
+ if (role !== 'validator') return '';
235
262
 
236
- // Find critical sections that must be preserved
237
- let issueOpenedStart = -1;
238
- let issueOpenedEnd = -1;
239
- 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
+ });
240
269
 
241
- for (let i = 0; i < lines.length; i++) {
242
- if (lines[i].includes('## Messages from topic: ISSUE_OPENED')) {
243
- issueOpenedStart = i;
244
- }
245
- if (issueOpenedStart !== -1 && issueOpenedEnd === -1 && lines[i].startsWith('## ')) {
246
- issueOpenedEnd = i;
247
- }
248
- if (lines[i].includes('## Triggering Message')) {
249
- triggeringStart = i;
250
- break;
251
- }
252
- }
270
+ const cannotValidateCriteria = collectCannotValidateCriteria(prevValidations);
271
+ return buildCannotValidateSection(cannotValidateCriteria);
272
+ }
253
273
 
254
- // Build truncated context:
255
- // 1. Header (agent info, instructions, output format)
256
- // 2. ISSUE_OPENED message (original task - NEVER truncate)
257
- // 3. Most recent N messages (whatever fits in budget)
258
- // 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
+ }
259
283
 
260
- const headerEnd = issueOpenedStart !== -1 ? issueOpenedStart : triggeringStart;
261
- const header = lines.slice(0, headerEnd).join('\n');
284
+ function findContextSectionIndices(lines) {
285
+ let issueOpenedStart = -1;
286
+ let issueOpenedEnd = -1;
287
+ let triggeringStart = -1;
262
288
 
263
- const issueOpened =
264
- issueOpenedStart !== -1 && issueOpenedEnd !== -1
265
- ? lines.slice(issueOpenedStart, issueOpenedEnd).join('\n')
266
- : '';
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
+ }
267
301
 
268
- const triggeringMsg = lines.slice(triggeringStart).join('\n');
302
+ return { issueOpenedStart, issueOpenedEnd, triggeringStart };
303
+ }
269
304
 
270
- // Calculate remaining budget for recent messages
271
- const fixedSize = header.length + issueOpened.length + triggeringMsg.length;
272
- const budgetForRecent = MAX_CONTEXT_CHARS - fixedSize - 200; // 200 char buffer for markers
305
+ function collectRecentLines(middleLines, budgetForRecent) {
306
+ const recentLines = [];
307
+ let recentSize = 0;
273
308
 
274
- // Collect recent messages (from end backwards until budget exhausted)
275
- const recentLines = [];
276
- let recentSize = 0;
309
+ for (let i = middleLines.length - 1; i >= 0; i--) {
310
+ const line = middleLines[i];
311
+ const lineSize = line.length + 1;
277
312
 
278
- const middleStart = issueOpenedEnd !== -1 ? issueOpenedEnd : headerEnd;
279
- const middleEnd = triggeringStart;
280
- const middleLines = lines.slice(middleStart, middleEnd);
313
+ if (recentSize + lineSize > budgetForRecent) {
314
+ break;
315
+ }
281
316
 
282
- for (let i = middleLines.length - 1; i >= 0; i--) {
283
- const line = middleLines[i];
284
- const lineSize = line.length + 1; // +1 for newline
317
+ recentLines.unshift(line);
318
+ recentSize += lineSize;
319
+ }
285
320
 
286
- if (recentSize + lineSize > budgetForRecent) {
287
- break; // Budget exhausted
288
- }
321
+ return recentLines;
322
+ }
289
323
 
290
- recentLines.unshift(line);
291
- recentSize += lineSize;
292
- }
324
+ function truncateContextIfNeeded(context) {
325
+ const originalLength = context.length;
326
+ if (originalLength <= MAX_CONTEXT_CHARS) {
327
+ return context;
328
+ }
293
329
 
294
- // Assemble truncated context
295
- const parts = [header];
330
+ console.log(
331
+ `[Context] Context too large (${originalLength} chars), truncating to prevent overflow...`
332
+ );
296
333
 
297
- if (issueOpened) {
298
- parts.push(issueOpened);
299
- }
334
+ const lines = context.split('\n');
335
+ const { issueOpenedStart, issueOpenedEnd, triggeringStart } = findContextSectionIndices(lines);
300
336
 
301
- if (recentLines.length < middleLines.length) {
302
- // Some messages were truncated
303
- const truncatedCount = middleLines.length - recentLines.length;
304
- parts.push(
305
- `\n[...${truncatedCount} earlier context messages truncated to prevent overflow...]\n`
306
- );
307
- }
337
+ const headerEnd = issueOpenedStart !== -1 ? issueOpenedStart : triggeringStart;
338
+ const header = lines.slice(0, headerEnd).join('\n');
308
339
 
309
- if (recentLines.length > 0) {
310
- parts.push(recentLines.join('\n'));
311
- }
340
+ const issueOpened =
341
+ issueOpenedStart !== -1 && issueOpenedEnd !== -1
342
+ ? lines.slice(issueOpenedStart, issueOpenedEnd).join('\n')
343
+ : '';
312
344
 
313
- parts.push(triggeringMsg);
345
+ const triggeringMsg = lines.slice(triggeringStart).join('\n');
314
346
 
315
- context = parts.join('\n');
347
+ const fixedSize = header.length + issueOpened.length + triggeringMsg.length;
348
+ const budgetForRecent = MAX_CONTEXT_CHARS - fixedSize - 200;
316
349
 
317
- const truncatedLength = context.length;
318
- console.log(
319
- `[Context] Truncated from ${originalLength} to ${truncatedLength} chars (${Math.round((truncatedLength / originalLength) * 100)}% retained)`
350
+ const middleStart = issueOpenedEnd !== -1 ? issueOpenedEnd : headerEnd;
351
+ const middleEnd = triggeringStart;
352
+ const middleLines = lines.slice(middleStart, middleEnd);
353
+ const recentLines = collectRecentLines(middleLines, budgetForRecent);
354
+
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`
320
363
  );
321
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
+ );
375
+
376
+ return truncatedContext;
377
+ }
322
378
 
323
- // Legacy maxTokens check (for backward compatibility with agent configs)
379
+ function applyLegacyMaxTokens(context, strategy) {
324
380
  const maxTokens = strategy.maxTokens || 100000;
325
381
  const maxChars = maxTokens * 4;
326
382
  if (context.length > maxChars) {
327
- context = context.slice(0, maxChars) + '\n\n[Context truncated...]';
383
+ return context.slice(0, maxChars) + '\n\n[Context truncated...]';
328
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);
329
438
 
330
439
  return context;
331
440
  }