@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
@@ -100,6 +100,23 @@ function truncate(str, maxLen) {
100
100
  * @returns {object|null} Parsed event with type, text, toolName, error
101
101
  */
102
102
  function parseEvent(line) {
103
+ const { trimmed, timestamp } = extractTimestamp(line);
104
+
105
+ // Keep non-JSON lines as-is
106
+ if (!trimmed.startsWith('{')) {
107
+ return trimmed ? { type: 'raw', text: trimmed, timestamp } : null;
108
+ }
109
+
110
+ // Parse JSON events
111
+ const event = safeParseEvent(trimmed);
112
+ if (!event) {
113
+ return null;
114
+ }
115
+
116
+ return parseStreamEvent(event, timestamp);
117
+ }
118
+
119
+ function extractTimestamp(line) {
103
120
  let trimmed = line.trim();
104
121
 
105
122
  // Strip timestamp prefix if present: [1234567890]{...} -> {...}
@@ -110,57 +127,89 @@ function parseEvent(line) {
110
127
  trimmed = timestampMatch[2];
111
128
  }
112
129
 
113
- // Keep non-JSON lines as-is
114
- if (!trimmed.startsWith('{')) {
115
- return trimmed ? { type: 'raw', text: trimmed, timestamp } : null;
116
- }
130
+ return { trimmed, timestamp };
131
+ }
117
132
 
118
- // Parse JSON events
133
+ function safeParseEvent(trimmed) {
119
134
  try {
120
- const event = JSON.parse(trimmed);
121
-
122
- // Text delta
123
- if (event.type === 'stream_event' && event.event?.type === 'content_block_delta') {
124
- return {
125
- type: 'text',
126
- text: event.event?.delta?.text || '',
127
- timestamp,
128
- };
129
- }
130
- // Tool use
131
- else if (event.type === 'stream_event' && event.event?.type === 'content_block_start') {
132
- const block = event.event?.content_block;
133
- if (block?.type === 'tool_use' && block?.name) {
134
- return {
135
- type: 'tool',
136
- toolName: block.name,
137
- timestamp,
138
- };
139
- }
140
- }
141
- // Assistant message
142
- else if (event.type === 'assistant' && event.message?.content) {
143
- let text = '';
144
- for (const content of event.message.content) {
145
- if (content.type === 'text') {
146
- text += content.text;
147
- }
148
- }
149
- return text ? { type: 'text', text, timestamp } : null;
150
- }
151
- // Error
152
- else if (event.type === 'result' && event.is_error) {
153
- return {
154
- type: 'error',
155
- text: event.result || 'Unknown error',
156
- timestamp,
157
- };
158
- }
135
+ return JSON.parse(trimmed);
159
136
  } catch {
160
- // Parse error - skip
137
+ return null;
138
+ }
139
+ }
140
+
141
+ function parseStreamEvent(event, timestamp) {
142
+ if (event.type === 'stream_event') {
143
+ return parseStreamDelta(event, timestamp);
144
+ }
145
+
146
+ if (event.type === 'assistant') {
147
+ return parseAssistantMessage(event, timestamp);
148
+ }
149
+
150
+ if (event.type === 'result') {
151
+ return parseResultEvent(event, timestamp);
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ function parseStreamDelta(event, timestamp) {
158
+ const eventType = event.event?.type;
159
+
160
+ if (eventType === 'content_block_delta') {
161
+ return {
162
+ type: 'text',
163
+ text: event.event?.delta?.text || '',
164
+ timestamp,
165
+ };
166
+ }
167
+
168
+ if (eventType === 'content_block_start') {
169
+ return parseToolUseEvent(event.event?.content_block, timestamp);
170
+ }
171
+
172
+ return null;
173
+ }
174
+
175
+ function parseToolUseEvent(block, timestamp) {
176
+ if (block?.type === 'tool_use' && block?.name) {
177
+ return {
178
+ type: 'tool',
179
+ toolName: block.name,
180
+ timestamp,
181
+ };
161
182
  }
162
183
 
163
184
  return null;
164
185
  }
165
186
 
187
+ function parseAssistantMessage(event, timestamp) {
188
+ const contentBlocks = event.message?.content;
189
+ if (!Array.isArray(contentBlocks)) {
190
+ return null;
191
+ }
192
+
193
+ let text = '';
194
+ for (const content of contentBlocks) {
195
+ if (content.type === 'text') {
196
+ text += content.text;
197
+ }
198
+ }
199
+
200
+ return text ? { type: 'text', text, timestamp } : null;
201
+ }
202
+
203
+ function parseResultEvent(event, timestamp) {
204
+ if (!event.is_error) {
205
+ return null;
206
+ }
207
+
208
+ return {
209
+ type: 'error',
210
+ text: event.result || 'Unknown error',
211
+ timestamp,
212
+ };
213
+ }
214
+
166
215
  export { formatTimestamp, formatBytes, formatCPU, stateIcon, truncate, parseEvent };
@@ -23,6 +23,18 @@ class Renderer {
23
23
  this.screen = screen;
24
24
  }
25
25
 
26
+ /**
27
+ * Format task prompt for display (truncate if needed)
28
+ * @param {string|undefined} prompt - Task prompt
29
+ * @returns {string} Formatted prompt or empty string
30
+ * @private
31
+ */
32
+ _formatTaskPrompt(prompt) {
33
+ if (!prompt) return '';
34
+ const truncated = prompt.length > 80 ? prompt.substring(0, 80) + '...' : prompt;
35
+ return `{bold}Task:{/bold} {gray-fg}${truncated}{/}`;
36
+ }
37
+
26
38
  /**
27
39
  * Render task info box
28
40
  * @param {object} taskInfo - Task metadata
@@ -40,9 +52,7 @@ class Renderer {
40
52
  `${icon} {bold}Status:{/bold} {white-fg}${taskInfo.status || 'unknown'}{/}`,
41
53
  `{bold}Runtime:{/bold} {white-fg}${runtime}{/}`,
42
54
  `{bold}CPU:{/bold} {white-fg}${cpu}{/} {bold}Memory:{/bold} {white-fg}${memory}{/}`,
43
- taskInfo.prompt
44
- ? `{bold}Task:{/bold} {gray-fg}${taskInfo.prompt.substring(0, 80)}${taskInfo.prompt.length > 80 ? '...' : ''}{/}`
45
- : '',
55
+ this._formatTaskPrompt(taskInfo.prompt),
46
56
  ]
47
57
  .filter(Boolean)
48
58
  .join(' ');
package/task-lib/tui.js CHANGED
@@ -31,38 +31,80 @@ function parseLogLine(line) {
31
31
  }
32
32
 
33
33
  // Parse JSON and extract relevant info
34
+ const event = safeParseEvent(trimmed);
35
+ if (!event) {
36
+ return '';
37
+ }
38
+
39
+ return extractEventText(event);
40
+ }
41
+
42
+ function safeParseEvent(trimmed) {
34
43
  try {
35
- const event = JSON.parse(trimmed);
44
+ return JSON.parse(trimmed);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
36
49
 
37
- // Extract text from content_block_delta
38
- if (event.type === 'stream_event' && event.event?.type === 'content_block_delta') {
39
- return event.event?.delta?.text || '';
40
- }
41
- // Extract tool use info
42
- else if (event.type === 'stream_event' && event.event?.type === 'content_block_start') {
43
- const block = event.event?.content_block;
44
- if (block?.type === 'tool_use' && block?.name) {
45
- return `\n[Tool: ${block.name}]\n`;
46
- }
47
- }
48
- // Extract assistant messages
49
- else if (event.type === 'assistant' && event.message?.content) {
50
- let output = '';
51
- for (const content of event.message.content) {
52
- if (content.type === 'text') {
53
- output += content.text;
54
- }
55
- }
56
- return output;
57
- }
58
- // Extract final result
59
- else if (event.type === 'result') {
60
- if (event.is_error) {
61
- return `\n[ERROR] ${event.result || 'Unknown error'}\n`;
62
- }
50
+ function extractEventText(event) {
51
+ if (event.type === 'stream_event') {
52
+ return extractStreamEventText(event);
53
+ }
54
+
55
+ if (event.type === 'assistant') {
56
+ return extractAssistantText(event);
57
+ }
58
+
59
+ if (event.type === 'result') {
60
+ return extractResultText(event);
61
+ }
62
+
63
+ return '';
64
+ }
65
+
66
+ function extractStreamEventText(event) {
67
+ const eventType = event.event?.type;
68
+
69
+ if (eventType === 'content_block_delta') {
70
+ return event.event?.delta?.text || '';
71
+ }
72
+
73
+ if (eventType === 'content_block_start') {
74
+ return extractToolUseText(event.event?.content_block);
75
+ }
76
+
77
+ return '';
78
+ }
79
+
80
+ function extractToolUseText(block) {
81
+ if (block?.type === 'tool_use' && block?.name) {
82
+ return `\n[Tool: ${block.name}]\n`;
83
+ }
84
+
85
+ return '';
86
+ }
87
+
88
+ function extractAssistantText(event) {
89
+ const contentBlocks = event.message?.content;
90
+
91
+ if (!Array.isArray(contentBlocks)) {
92
+ return '';
93
+ }
94
+
95
+ let output = '';
96
+ for (const content of contentBlocks) {
97
+ if (content.type === 'text') {
98
+ output += content.text;
63
99
  }
64
- } catch {
65
- // Not JSON or parse error - skip
100
+ }
101
+
102
+ return output;
103
+ }
104
+
105
+ function extractResultText(event) {
106
+ if (event.is_error) {
107
+ return `\n[ERROR] ${event.result || 'Unknown error'}\n`;
66
108
  }
67
109
 
68
110
  return '';
@@ -260,8 +302,8 @@ class TaskTUI {
260
302
  const tasks = loadTasks();
261
303
  this.tasks = Object.values(tasks);
262
304
 
263
- // Sort by creation date, newest first
264
- this.tasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
305
+ // Sort by creation date, oldest first (chronological)
306
+ this.tasks.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
265
307
 
266
308
  // Verify running status
267
309
  for (const task of this.tasks) {
@@ -334,7 +376,6 @@ class TaskTUI {
334
376
  }
335
377
 
336
378
  // Strip ANSI codes for clean display
337
- // eslint-disable-next-line no-control-regex
338
379
  content = content.replace(/\x1b\[[0-9;]*m/g, '');
339
380
  } catch (error) {
340
381
  content = `Error reading log: ${error.message}`;
@@ -1,24 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Watcher process - spawns and monitors a claude process
4
+ * Watcher process - spawns and monitors a CLI process
5
5
  * Runs detached from parent, updates task status on completion
6
- *
7
- * Uses regular spawn (not PTY) - Claude CLI with --print is non-interactive
8
- * PTY causes EIO errors when processes are killed/OOM'd
9
6
  */
10
7
 
11
8
  import { spawn } from 'child_process';
12
9
  import { appendFileSync } from 'fs';
13
- import { dirname } from 'path';
14
- import { fileURLToPath } from 'url';
15
10
  import { updateTask } from './store.js';
11
+ import { detectStreamingModeError, recoverStructuredOutput } from './claude-recovery.js';
16
12
  import { createRequire } from 'module';
17
13
 
18
14
  const require = createRequire(import.meta.url);
19
- const { getClaudeCommand } = require('../lib/settings.js');
20
-
21
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const { normalizeProviderName } = require('../lib/provider-names');
22
16
 
23
17
  const [, , taskId, cwd, logFile, argsJson, configJson] = process.argv;
24
18
  const args = JSON.parse(argsJson);
@@ -28,142 +22,205 @@ function log(msg) {
28
22
  appendFileSync(logFile, msg);
29
23
  }
30
24
 
31
- // Build environment - inherit user's auth method (API key or subscription)
32
- const env = { ...process.env };
33
-
34
- // Add model flag - priority: config.model > ANTHROPIC_MODEL env var
35
- const claudeArgs = [...args];
36
- const model = config.model || env.ANTHROPIC_MODEL;
37
- if (model && !claudeArgs.includes('--model')) {
38
- claudeArgs.unshift('--model', model);
39
- }
25
+ const providerName = normalizeProviderName(config.provider || 'claude');
26
+ const enableRecovery = providerName === 'claude';
40
27
 
41
- // Get configured Claude command (supports custom commands like 'ccr code')
42
- const { command: claudeCommand, args: claudeExtraArgs } = getClaudeCommand();
43
- const finalArgs = [...claudeExtraArgs, ...claudeArgs];
28
+ const env = { ...process.env, ...(config.env || {}) };
29
+ const command = config.command || 'claude';
30
+ const finalArgs = [...args];
44
31
 
45
- // Spawn claude using regular child_process (not PTY)
46
- // --print mode is non-interactive, PTY adds overhead and causes EIO on OOM
47
- const child = spawn(claudeCommand, finalArgs, {
32
+ const child = spawn(command, finalArgs, {
48
33
  cwd,
49
34
  env,
50
35
  stdio: ['ignore', 'pipe', 'pipe'],
51
36
  });
52
37
 
53
- // Update task with PID
54
38
  updateTask(taskId, { pid: child.pid });
55
39
 
56
- // For JSON schema output with silent mode, capture ONLY the structured_output JSON
57
40
  const silentJsonMode =
58
- config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput;
41
+ config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput && enableRecovery;
42
+
59
43
  let finalResultJson = null;
44
+ let streamingModeError = null;
60
45
 
61
- // Buffer for incomplete lines (need complete lines to add timestamps)
62
46
  let stdoutBuffer = '';
47
+ let stderrBuffer = '';
48
+
49
+ function splitBufferLines(buffer, chunk) {
50
+ const nextBuffer = buffer + chunk;
51
+ const lines = nextBuffer.split('\n');
52
+ const remaining = lines.pop() || '';
53
+ return { lines, remaining };
54
+ }
55
+
56
+ function captureStreamingError(line, timestamp) {
57
+ if (!enableRecovery) {
58
+ return false;
59
+ }
60
+
61
+ const detectedError = detectStreamingModeError(line);
62
+ if (!detectedError) {
63
+ return false;
64
+ }
65
+
66
+ streamingModeError = { ...detectedError, timestamp };
67
+ return true;
68
+ }
69
+
70
+ function maybeCaptureStructuredOutput(line) {
71
+ try {
72
+ const json = JSON.parse(line);
73
+ if (json.structured_output) {
74
+ finalResultJson = line;
75
+ }
76
+ } catch {
77
+ // Not JSON, skip
78
+ }
79
+ }
80
+
81
+ function handleSilentJsonLines(lines, timestamp) {
82
+ for (const line of lines) {
83
+ if (!line.trim()) continue;
84
+ if (captureStreamingError(line, timestamp)) {
85
+ continue;
86
+ }
87
+ maybeCaptureStructuredOutput(line);
88
+ }
89
+ }
90
+
91
+ function handleStreamingLines(lines, timestamp) {
92
+ for (const line of lines) {
93
+ if (captureStreamingError(line, timestamp)) {
94
+ continue;
95
+ }
96
+ log(`[${timestamp}]${line}\n`);
97
+ }
98
+ }
99
+
100
+ function flushStdoutBuffer(timestamp) {
101
+ if (!stdoutBuffer.trim()) {
102
+ return;
103
+ }
104
+
105
+ if (!enableRecovery) {
106
+ if (!silentJsonMode) {
107
+ log(`[${timestamp}]${stdoutBuffer}\n`);
108
+ }
109
+ return;
110
+ }
111
+
112
+ if (captureStreamingError(stdoutBuffer, timestamp)) {
113
+ return;
114
+ }
115
+
116
+ if (silentJsonMode) {
117
+ maybeCaptureStructuredOutput(stdoutBuffer);
118
+ return;
119
+ }
120
+
121
+ log(`[${timestamp}]${stdoutBuffer}\n`);
122
+ }
123
+
124
+ function flushStderrBuffer(timestamp) {
125
+ if (stderrBuffer.trim()) {
126
+ log(`[${timestamp}]${stderrBuffer}\n`);
127
+ }
128
+ }
129
+
130
+ function attemptRecovery(code, timestamp) {
131
+ if (!(enableRecovery && code !== 0 && streamingModeError?.sessionId)) {
132
+ return null;
133
+ }
134
+
135
+ const recovered = recoverStructuredOutput(streamingModeError.sessionId);
136
+ if (recovered?.payload) {
137
+ const recoveredLine = JSON.stringify(recovered.payload);
138
+ if (silentJsonMode) {
139
+ finalResultJson = recoveredLine;
140
+ } else {
141
+ log(`[${timestamp}]${recoveredLine}\n`);
142
+ }
143
+ } else if (streamingModeError.line) {
144
+ if (silentJsonMode) {
145
+ log(streamingModeError.line + '\n');
146
+ } else {
147
+ log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
148
+ }
149
+ }
150
+
151
+ return recovered;
152
+ }
153
+
154
+ function writeCompletionFooter(code, signal) {
155
+ if (config.outputFormat === 'json') {
156
+ return;
157
+ }
158
+
159
+ log(`\n${'='.repeat(50)}\n`);
160
+ log(`Finished: ${new Date().toISOString()}\n`);
161
+ log(`Exit code: ${code}, Signal: ${signal}\n`);
162
+ }
63
163
 
64
- // Process stdout data
65
- // CRITICAL: Prepend timestamp to each line for real-time tracking in cluster
66
- // Format: [1733301234567]{json...} - consumers parse timestamp for accurate timing
67
164
  child.stdout.on('data', (data) => {
68
165
  const chunk = data.toString();
69
166
  const timestamp = Date.now();
70
167
 
168
+ const { lines, remaining } = splitBufferLines(stdoutBuffer, chunk);
169
+ stdoutBuffer = remaining;
170
+
71
171
  if (silentJsonMode) {
72
- // Parse each line to find the one with structured_output
73
- stdoutBuffer += chunk;
74
- const lines = stdoutBuffer.split('\n');
75
- stdoutBuffer = lines.pop() || ''; // Keep incomplete line in buffer
76
-
77
- for (const line of lines) {
78
- if (!line.trim()) continue;
79
- try {
80
- const json = JSON.parse(line);
81
- if (json.structured_output) {
82
- finalResultJson = line;
83
- }
84
- } catch {
85
- // Not JSON or incomplete, skip
86
- }
87
- }
172
+ handleSilentJsonLines(lines, timestamp);
88
173
  } else {
89
- // Normal mode - stream with timestamps on each complete line
90
- stdoutBuffer += chunk;
91
- const lines = stdoutBuffer.split('\n');
92
- stdoutBuffer = lines.pop() || ''; // Keep incomplete line in buffer
93
-
94
- for (const line of lines) {
95
- // Timestamp each line: [epochMs]originalContent
96
- log(`[${timestamp}]${line}\n`);
97
- }
174
+ handleStreamingLines(lines, timestamp);
98
175
  }
99
176
  });
100
177
 
101
- // Buffer for stderr incomplete lines
102
- let stderrBuffer = '';
103
-
104
- // Stream stderr to log with timestamps
105
178
  child.stderr.on('data', (data) => {
106
179
  const chunk = data.toString();
107
180
  const timestamp = Date.now();
108
181
 
109
- stderrBuffer += chunk;
110
- const lines = stderrBuffer.split('\n');
111
- stderrBuffer = lines.pop() || '';
182
+ const { lines, remaining } = splitBufferLines(stderrBuffer, chunk);
183
+ stderrBuffer = remaining;
112
184
 
113
185
  for (const line of lines) {
114
186
  log(`[${timestamp}]${line}\n`);
115
187
  }
116
188
  });
117
189
 
118
- // Handle process exit
119
- child.on('close', (code, signal) => {
190
+ child.on('close', async (code, signal) => {
120
191
  const timestamp = Date.now();
121
192
 
122
- // Flush any remaining buffered stdout
123
- if (stdoutBuffer.trim()) {
124
- if (silentJsonMode) {
125
- try {
126
- const json = JSON.parse(stdoutBuffer);
127
- if (json.structured_output) {
128
- finalResultJson = stdoutBuffer;
129
- }
130
- } catch {
131
- // Not valid JSON
132
- }
133
- } else {
134
- log(`[${timestamp}]${stdoutBuffer}\n`);
135
- }
136
- }
193
+ flushStdoutBuffer(timestamp);
194
+ flushStderrBuffer(timestamp);
137
195
 
138
- // Flush any remaining buffered stderr
139
- if (stderrBuffer.trim()) {
140
- log(`[${timestamp}]${stderrBuffer}\n`);
141
- }
196
+ const recovered = attemptRecovery(code, timestamp);
142
197
 
143
- // In silent JSON mode, log ONLY the final structured_output JSON
144
198
  if (silentJsonMode && finalResultJson) {
145
199
  log(finalResultJson + '\n');
146
200
  }
147
201
 
148
- // Skip footer for pure JSON output
149
- if (config.outputFormat !== 'json') {
150
- log(`\n${'='.repeat(50)}\n`);
151
- log(`Finished: ${new Date().toISOString()}\n`);
152
- log(`Exit code: ${code}, Signal: ${signal}\n`);
202
+ writeCompletionFooter(code, signal);
203
+
204
+ const resolvedCode = recovered?.payload ? 0 : code;
205
+ const status = resolvedCode === 0 ? 'completed' : 'failed';
206
+ try {
207
+ await updateTask(taskId, {
208
+ status,
209
+ exitCode: resolvedCode,
210
+ error: resolvedCode !== 0 && signal ? `Killed by ${signal}` : null,
211
+ });
212
+ } catch (updateError) {
213
+ log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
153
214
  }
154
-
155
- // Simple status: completed if exit 0, failed otherwise
156
- const status = code === 0 ? 'completed' : 'failed';
157
- updateTask(taskId, {
158
- status,
159
- exitCode: code,
160
- error: signal ? `Killed by ${signal}` : null,
161
- });
162
215
  process.exit(0);
163
216
  });
164
217
 
165
- child.on('error', (err) => {
218
+ child.on('error', async (err) => {
166
219
  log(`\nError: ${err.message}\n`);
167
- updateTask(taskId, { status: 'failed', error: err.message });
220
+ try {
221
+ await updateTask(taskId, { status: 'failed', error: err.message });
222
+ } catch (updateError) {
223
+ log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
224
+ }
168
225
  process.exit(1);
169
226
  });
@@ -1,20 +0,0 @@
1
- {
2
- "id": "git-pusher",
3
- "role": "completion-detector",
4
- "model": "sonnet",
5
- "triggers": [
6
- {
7
- "topic": "VALIDATION_RESULT",
8
- "logic": {
9
- "engine": "javascript",
10
- "script": "const validators = cluster.getAgentsByRole('validator'); const lastPush = ledger.findLast({ topic: 'IMPLEMENTATION_READY' }); if (!lastPush) return false; if (validators.length === 0) return true; const results = ledger.query({ topic: 'VALIDATION_RESULT', since: lastPush.timestamp }); if (results.length < validators.length) return false; const allApproved = results.every(r => r.content?.data?.approved === 'true' || r.content?.data?.approved === true); if (!allApproved) return false; const hasRealEvidence = results.every(r => { const criteria = r.content?.data?.criteriaResults || []; return criteria.every(c => { return c.evidence?.command && typeof c.evidence?.exitCode === 'number' && c.evidence?.output?.length > 10; }); }); return hasRealEvidence;"
11
- },
12
- "action": "execute_task"
13
- }
14
- ],
15
- "prompt": "\ud83d\udea8 CRITICAL: ALL VALIDATORS APPROVED. YOU MUST CREATE A PR AND GET IT MERGED. DO NOT STOP UNTIL THE PR IS MERGED. \ud83d\udea8\n\n## MANDATORY STEPS - EXECUTE EACH ONE IN ORDER - DO NOT SKIP ANY STEP\n\n### STEP 1: Stage ALL changes (MANDATORY)\n```bash\ngit add -A\n```\nRun this command. Do not skip it.\n\n### STEP 2: Check what's staged\n```bash\ngit status\n```\nRun this. If nothing to commit, output JSON with pr_url: null and stop.\n\n### STEP 3: Commit the changes (MANDATORY if there are changes)\n```bash\ngit commit -m \"feat: implement #{{issue_number}} - {{issue_title}}\"\n```\nRun this command. Do not skip it.\n\n### STEP 4: Push to origin (MANDATORY)\n```bash\ngit push -u origin HEAD\n```\nRun this. If it fails, check the error and fix it.\n\n\u26a0\ufe0f AFTER PUSH YOU ARE NOT DONE! CONTINUE TO STEP 5! \u26a0\ufe0f\n\n### STEP 5: CREATE THE PR (MANDATORY - YOU MUST RUN THIS COMMAND)\n```bash\ngh pr create --title \"feat: {{issue_title}}\" --body \"Closes #{{issue_number}}\"\n```\n\ud83d\udea8 YOU MUST RUN `gh pr create`! Outputting a link is NOT creating a PR! \ud83d\udea8\nThe push output shows a \"Create a pull request\" link - IGNORE IT.\nYou MUST run the `gh pr create` command above. Save the actual PR URL from the output.\n\n\u26a0\ufe0f AFTER PR CREATION YOU ARE NOT DONE! CONTINUE TO STEP 6! \u26a0\ufe0f\n\n### STEP 6: MERGE THE PR (MANDATORY - THIS IS NOT OPTIONAL)\n```bash\ngh pr merge --merge --auto\n```\nThis sets auto-merge. If it fails (e.g., no auto-merge enabled), try:\n```bash\ngh pr merge --merge\n```\n\n\ud83d\udea8 IF MERGE FAILS DUE TO CONFLICTS - YOU MUST RESOLVE THEM:\na) Pull latest main and rebase:\n ```bash\n git fetch origin main\n git rebase origin/main\n ```\nb) If conflicts appear - RESOLVE THEM IMMEDIATELY:\n - Read the conflicting files\n - Make intelligent decisions about what code to keep\n - Edit the files to resolve conflicts\n - `git add <resolved-files>`\n - `git rebase --continue`\nc) Force push the resolved branch:\n ```bash\n git push --force-with-lease\n ```\nd) Retry merge:\n ```bash\n gh pr merge --merge\n ```\n\nREPEAT UNTIL MERGED. DO NOT GIVE UP. DO NOT SKIP. THE PR MUST BE MERGED.\nIf merge is blocked by CI, wait and retry. If blocked by reviews, set auto-merge.\n\n## CRITICAL RULES\n- Execute EVERY step in order (1, 2, 3, 4, 5, 6)\n- Do NOT skip git add -A\n- Do NOT skip git commit\n- Do NOT skip gh pr create - THE TASK IS NOT DONE UNTIL PR EXISTS\n- Do NOT skip gh pr merge - THE TASK IS NOT DONE UNTIL PR IS MERGED\n- If push fails, debug and fix it\n- If PR creation fails, debug and fix it\n- If merge fails, debug and fix it\n- DO NOT OUTPUT JSON UNTIL PR IS ACTUALLY MERGED\n- A link from git push is NOT a PR - you must run gh pr create\n\n## Final Output\nONLY after the PR is MERGED, output:\n```json\n{\"pr_url\": \"https://github.com/owner/repo/pull/123\", \"pr_number\": 123, \"merged\": true}\n```\n\nIf truly no changes exist, output:\n```json\n{\"pr_url\": null, \"pr_number\": null, \"merged\": false}\n```",
16
- "output": {
17
- "topic": "PR_CREATED",
18
- "publishAfter": "CLUSTER_COMPLETE"
19
- }
20
- }