@covibes/zeroshot 5.4.0 → 5.4.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.
- package/LICENSE +1 -1
- package/README.md +8 -465
- package/cli/index.js +101 -5231
- package/package.json +16 -130
- package/CHANGELOG.md +0 -537
- package/cli/commands/providers.js +0 -149
- package/cli/lib/first-run.js +0 -211
- package/cli/lib/update-checker.js +0 -281
- package/cli/message-formatter-utils.js +0 -75
- package/cli/message-formatters-normal.js +0 -275
- package/cli/message-formatters-watch.js +0 -181
- package/cluster-templates/base-templates/debug-workflow.json +0 -375
- package/cluster-templates/base-templates/full-workflow.json +0 -581
- package/cluster-templates/base-templates/single-worker.json +0 -63
- package/cluster-templates/base-templates/worker-validator.json +0 -208
- package/cluster-templates/conductor-bootstrap.json +0 -124
- package/docker/zeroshot-cluster/Dockerfile +0 -152
- package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
- package/lib/completion.js +0 -174
- package/lib/docker-config.js +0 -220
- package/lib/git-remote-utils.js +0 -165
- package/lib/id-detector.js +0 -56
- package/lib/provider-defaults.js +0 -62
- package/lib/provider-detection.js +0 -59
- package/lib/provider-names.js +0 -57
- package/lib/settings/claude-auth.js +0 -78
- package/lib/settings.js +0 -503
- package/lib/stream-json-parser.js +0 -10
- package/scripts/fix-node-pty-permissions.js +0 -75
- package/scripts/record-demo.sh +0 -279
- package/scripts/setup-merge-queue.sh +0 -170
- package/scripts/test-install.sh +0 -40
- package/scripts/validate-templates.js +0 -100
- package/src/agent/agent-config.js +0 -246
- package/src/agent/agent-context-builder.js +0 -444
- package/src/agent/agent-hook-executor.js +0 -599
- package/src/agent/agent-lifecycle.js +0 -743
- package/src/agent/agent-stuck-detector.js +0 -256
- package/src/agent/agent-task-executor.js +0 -1690
- package/src/agent/agent-trigger-evaluator.js +0 -67
- package/src/agent/output-extraction.js +0 -217
- package/src/agent/output-reformatter.js +0 -175
- package/src/agent/schema-utils.js +0 -146
- package/src/agent-wrapper.js +0 -579
- package/src/agents/git-pusher-template.js +0 -285
- package/src/attach/attach-client.js +0 -438
- package/src/attach/attach-server.js +0 -543
- package/src/attach/index.js +0 -35
- package/src/attach/protocol.js +0 -220
- package/src/attach/ring-buffer.js +0 -121
- package/src/attach/socket-discovery.js +0 -267
- package/src/claude-task-runner.js +0 -574
- package/src/config-router.js +0 -80
- package/src/config-validator.js +0 -1925
- package/src/copy-worker.js +0 -43
- package/src/input-helpers.js +0 -65
- package/src/isolation-manager.js +0 -1471
- package/src/issue-providers/README.md +0 -305
- package/src/issue-providers/azure-devops-provider.js +0 -273
- package/src/issue-providers/base-provider.js +0 -232
- package/src/issue-providers/github-provider.js +0 -179
- package/src/issue-providers/gitlab-provider.js +0 -241
- package/src/issue-providers/index.js +0 -196
- package/src/issue-providers/jira-provider.js +0 -239
- package/src/ledger.js +0 -631
- package/src/lib/safe-exec.js +0 -88
- package/src/logic-engine.js +0 -223
- package/src/message-bus-bridge.js +0 -139
- package/src/message-bus.js +0 -249
- package/src/name-generator.js +0 -232
- package/src/orchestrator.js +0 -2815
- package/src/preflight.js +0 -639
- package/src/process-metrics.js +0 -608
- package/src/providers/anthropic/cli-builder.js +0 -73
- package/src/providers/anthropic/index.js +0 -204
- package/src/providers/anthropic/models.js +0 -23
- package/src/providers/anthropic/output-parser.js +0 -177
- package/src/providers/base-provider.js +0 -251
- package/src/providers/capabilities.js +0 -60
- package/src/providers/google/cli-builder.js +0 -55
- package/src/providers/google/index.js +0 -116
- package/src/providers/google/models.js +0 -24
- package/src/providers/google/output-parser.js +0 -101
- package/src/providers/index.js +0 -91
- package/src/providers/openai/cli-builder.js +0 -133
- package/src/providers/openai/index.js +0 -136
- package/src/providers/openai/models.js +0 -21
- package/src/providers/openai/output-parser.js +0 -143
- package/src/providers/opencode/cli-builder.js +0 -42
- package/src/providers/opencode/index.js +0 -103
- package/src/providers/opencode/models.js +0 -55
- package/src/providers/opencode/output-parser.js +0 -122
- package/src/schemas/sub-cluster.js +0 -185
- package/src/status-footer.js +0 -1026
- package/src/sub-cluster-wrapper.js +0 -601
- package/src/task-runner.js +0 -30
- package/src/template-resolver.js +0 -372
- package/src/tui/CHANGES.txt +0 -133
- package/src/tui/LAYOUT.md +0 -261
- package/src/tui/README.txt +0 -192
- package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
- package/src/tui/data-poller.js +0 -349
- package/src/tui/demo.js +0 -208
- package/src/tui/formatters.js +0 -129
- package/src/tui/index.js +0 -193
- package/src/tui/keybindings.js +0 -324
- package/src/tui/layout.js +0 -334
- package/src/tui/renderer.js +0 -184
- package/task-lib/attachable-watcher.js +0 -309
- package/task-lib/claude-recovery.js +0 -156
- package/task-lib/commands/clean.js +0 -50
- package/task-lib/commands/episodes.js +0 -105
- package/task-lib/commands/get-log-path.js +0 -23
- package/task-lib/commands/kill.js +0 -32
- package/task-lib/commands/list.js +0 -105
- package/task-lib/commands/logs.js +0 -453
- package/task-lib/commands/resume.js +0 -42
- package/task-lib/commands/run.js +0 -57
- package/task-lib/commands/schedule.js +0 -105
- package/task-lib/commands/scheduler-cmd.js +0 -96
- package/task-lib/commands/schedules.js +0 -148
- package/task-lib/commands/status.js +0 -44
- package/task-lib/commands/unschedule.js +0 -16
- package/task-lib/completion.js +0 -9
- package/task-lib/config.js +0 -8
- package/task-lib/name-generator.js +0 -230
- package/task-lib/package.json +0 -3
- package/task-lib/runner.js +0 -201
- package/task-lib/scheduler.js +0 -252
- package/task-lib/store.js +0 -529
- package/task-lib/tui/formatters.js +0 -215
- package/task-lib/tui/index.js +0 -197
- package/task-lib/tui/layout.js +0 -111
- package/task-lib/tui/renderer.js +0 -129
- package/task-lib/tui.js +0 -425
- package/task-lib/watcher.js +0 -226
|
@@ -1,1690 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* AgentTaskExecutor - Claude CLI spawning and monitoring
|
|
4
|
-
*
|
|
5
|
-
* Provides:
|
|
6
|
-
* - Claude CLI task spawning (normal and isolated modes)
|
|
7
|
-
* - Log streaming and real-time output broadcasting
|
|
8
|
-
* - Task lifecycle management (wait, kill)
|
|
9
|
-
* - Output parsing and validation
|
|
10
|
-
* - Vibe-specific Claude config with AskUserQuestion blocked
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { spawn } = require('child_process');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const os = require('os');
|
|
17
|
-
const { exec, execSync } = require('../lib/safe-exec'); // Enforces timeouts - prevents infinite hangs
|
|
18
|
-
const { getProvider, parseChunkWithProvider } = require('../providers');
|
|
19
|
-
const { getTask } = require('../../task-lib/store.js');
|
|
20
|
-
const { loadSettings } = require('../../lib/settings.js');
|
|
21
|
-
const { resolveClaudeAuth } = require('../../lib/settings/claude-auth.js');
|
|
22
|
-
|
|
23
|
-
// Schema utilities for normalizing LLM output
|
|
24
|
-
const { normalizeEnumValues } = require('./schema-utils');
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Build Claude-specific environment variables for task spawning
|
|
28
|
-
* Consolidates auth resolution and model mapping logic used by both isolated and non-isolated modes
|
|
29
|
-
* @param {Object} modelSpec - Model specification from agent
|
|
30
|
-
* @param {Object} [options] - Options
|
|
31
|
-
* @param {boolean} [options.includeAuth=true] - Include auth env vars (false for isolated mode where IsolationManager handles auth)
|
|
32
|
-
* @returns {Object} Environment variables to merge into spawn env
|
|
33
|
-
*/
|
|
34
|
-
function buildClaudeEnv(modelSpec, options = {}) {
|
|
35
|
-
const { includeAuth = true } = options;
|
|
36
|
-
const env = {};
|
|
37
|
-
|
|
38
|
-
if (includeAuth) {
|
|
39
|
-
const settings = loadSettings();
|
|
40
|
-
const authEnv = resolveClaudeAuth(settings);
|
|
41
|
-
Object.assign(env, authEnv);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (modelSpec?.model) {
|
|
45
|
-
env.ANTHROPIC_MODEL = modelSpec.model;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Activate AskUserQuestion blocking hook (see hooks/block-ask-user-question.py)
|
|
49
|
-
env.ZEROSHOT_BLOCK_ASK_USER = '1';
|
|
50
|
-
|
|
51
|
-
return env;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Validate and sanitize error messages.
|
|
56
|
-
* Detects TypeScript type annotations that may have leaked into error storage.
|
|
57
|
-
*
|
|
58
|
-
* @param {string|null} error - Error message to validate
|
|
59
|
-
* @returns {string|null} Sanitized error or original if valid
|
|
60
|
-
*/
|
|
61
|
-
function sanitizeErrorMessage(error) {
|
|
62
|
-
if (!error) return null;
|
|
63
|
-
|
|
64
|
-
// Patterns that look like TypeScript type annotations (not real error messages)
|
|
65
|
-
const typeAnnotationPatterns = [
|
|
66
|
-
/^string\s*\|\s*null$/i,
|
|
67
|
-
/^number\s*\|\s*undefined$/i,
|
|
68
|
-
/^boolean\s*\|\s*null$/i,
|
|
69
|
-
/^any$/i,
|
|
70
|
-
/^unknown$/i,
|
|
71
|
-
/^void$/i,
|
|
72
|
-
/^never$/i,
|
|
73
|
-
/^[A-Z][a-zA-Z]*\s*\|\s*(?:null|undefined)$/, // e.g., "Error | null"
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
const trimmedError = error.trim();
|
|
77
|
-
|
|
78
|
-
// Check if it's a union type like "string | number | boolean" (ReDoS-safe approach)
|
|
79
|
-
const unionParts = trimmedError.split(/\s*\|\s*/);
|
|
80
|
-
const isUnionType = unionParts.length > 1 && unionParts.every((p) => /^[a-z]+$/i.test(p));
|
|
81
|
-
|
|
82
|
-
for (const pattern of typeAnnotationPatterns) {
|
|
83
|
-
if (pattern.test(trimmedError) || isUnionType) {
|
|
84
|
-
console.warn(
|
|
85
|
-
`[agent-task-executor] WARNING: Error message looks like a TypeScript type annotation: "${error}". ` +
|
|
86
|
-
`This indicates corrupted data. Replacing with generic error.`
|
|
87
|
-
);
|
|
88
|
-
return `Task failed with corrupted error data (original: "${error}")`;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return error;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Extract error context from task output.
|
|
97
|
-
* Shared by both isolated and non-isolated modes.
|
|
98
|
-
*
|
|
99
|
-
* @param {Object} params - Extraction parameters
|
|
100
|
-
* @param {string} params.output - Full task output
|
|
101
|
-
* @param {string} [params.statusOutput] - Status command output (non-isolated only)
|
|
102
|
-
* @param {string} params.taskId - Task ID for error messages
|
|
103
|
-
* @param {boolean} [params.isNotFound=false] - True if task was not found
|
|
104
|
-
* @returns {string|null} Sanitized error context or null if extraction failed
|
|
105
|
-
*/
|
|
106
|
-
function extractErrorContext({ output, statusOutput, taskId, isNotFound = false }) {
|
|
107
|
-
// Task not found - explicit error
|
|
108
|
-
if (isNotFound) {
|
|
109
|
-
return sanitizeErrorMessage(`Task ${taskId} not found (may have crashed or been killed)`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Try status output first (only available in non-isolated mode)
|
|
113
|
-
if (statusOutput) {
|
|
114
|
-
const statusErrorMatch = statusOutput.match(/Error:\s*(.+)/);
|
|
115
|
-
if (statusErrorMatch) {
|
|
116
|
-
return sanitizeErrorMessage(statusErrorMatch[1].trim());
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// KNOWN CLAUDE CODE LIMITATIONS - detect and provide actionable guidance
|
|
121
|
-
const fullOutput = output || '';
|
|
122
|
-
|
|
123
|
-
// 256KB file limit error
|
|
124
|
-
if (fullOutput.includes('exceeds maximum allowed size') || fullOutput.includes('256KB')) {
|
|
125
|
-
return sanitizeErrorMessage(
|
|
126
|
-
`FILE TOO LARGE (Claude Code 256KB limit). ` +
|
|
127
|
-
`Use offset and limit parameters when reading large files. ` +
|
|
128
|
-
`Example: Read tool with offset=0, limit=1000 to read first 1000 lines.`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Streaming mode error (interactive tools in non-interactive mode)
|
|
133
|
-
if (fullOutput.includes('only prompt commands are supported in streaming mode')) {
|
|
134
|
-
return sanitizeErrorMessage(
|
|
135
|
-
`STREAMING MODE ERROR: Agent tried to use interactive tools in streaming mode. ` +
|
|
136
|
-
`This usually happens with AskUserQuestion or interactive prompts. ` +
|
|
137
|
-
`Zeroshot agents must run non-interactively.`
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// NEVER TRUNCATE OUTPUT - truncation corrupts structured JSON and causes false "crash" status
|
|
142
|
-
// If output is too verbose, that's a prompt problem - fix the prompts, not the data
|
|
143
|
-
const trimmedOutput = (output || '').trim();
|
|
144
|
-
if (!trimmedOutput) {
|
|
145
|
-
return sanitizeErrorMessage(
|
|
146
|
-
'Task failed with no output (check if task was interrupted or timed out)'
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Try to extract structured JSON from output first - it may contain the actual result
|
|
151
|
-
// even if the task was marked as "failed" due to timeout/stale status
|
|
152
|
-
try {
|
|
153
|
-
const { extractJsonFromOutput } = require('./output-extraction');
|
|
154
|
-
const extracted = extractJsonFromOutput(trimmedOutput);
|
|
155
|
-
if (extracted && typeof extracted === 'object') {
|
|
156
|
-
// If we found valid JSON, return it as the error context
|
|
157
|
-
// This preserves the actual agent output for downstream processing
|
|
158
|
-
return JSON.stringify(extracted);
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
// Extraction failed, fall through to error pattern matching
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Extract non-JSON lines only (JSON lines contain "is_error": true which falsely matches)
|
|
165
|
-
const nonJsonLines = trimmedOutput
|
|
166
|
-
.split('\n')
|
|
167
|
-
.filter((line) => {
|
|
168
|
-
const trimmed = line.trim();
|
|
169
|
-
// Skip JSON objects and JSON-like content
|
|
170
|
-
return trimmed && !trimmed.startsWith('{') && !trimmed.startsWith('"');
|
|
171
|
-
})
|
|
172
|
-
.join('\n');
|
|
173
|
-
|
|
174
|
-
// Common error patterns - match against non-JSON content
|
|
175
|
-
const textToSearch = nonJsonLines || trimmedOutput;
|
|
176
|
-
const errorPatterns = [
|
|
177
|
-
/Error:\s*(.+)/i,
|
|
178
|
-
/error:\s*(.+)/i,
|
|
179
|
-
/failed:\s*(.+)/i,
|
|
180
|
-
/Exception:\s*(.+)/i,
|
|
181
|
-
/panic:\s*(.+)/i,
|
|
182
|
-
];
|
|
183
|
-
|
|
184
|
-
for (const pattern of errorPatterns) {
|
|
185
|
-
const match = textToSearch.match(pattern);
|
|
186
|
-
if (match) {
|
|
187
|
-
// Don't truncate - let the full error message through
|
|
188
|
-
return sanitizeErrorMessage(match[1]);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// No pattern matched - return full output (no truncation)
|
|
193
|
-
// If this is too long, the solution is to make agents output less, not to corrupt data
|
|
194
|
-
return sanitizeErrorMessage(`Task failed. Output: ${trimmedOutput}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Track if we've already ensured the AskUserQuestion hook is installed
|
|
198
|
-
let askUserQuestionHookInstalled = false;
|
|
199
|
-
|
|
200
|
-
// Track if we've already ensured the dangerous git hook is installed
|
|
201
|
-
let dangerousGitHookInstalled = false;
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Extract token usage from NDJSON output.
|
|
205
|
-
* Looks for the 'result' event line which contains usage data.
|
|
206
|
-
*
|
|
207
|
-
* @param {string} output - Full NDJSON output from Claude CLI
|
|
208
|
-
* @returns {Object|null} Token usage data or null if not found
|
|
209
|
-
*/
|
|
210
|
-
function extractTokenUsage(output, providerName = 'claude') {
|
|
211
|
-
if (!output) return null;
|
|
212
|
-
|
|
213
|
-
const provider = getProvider(providerName);
|
|
214
|
-
const events = parseChunkWithProvider(provider, output);
|
|
215
|
-
const resultEvent = events.find((event) => event.type === 'result');
|
|
216
|
-
|
|
217
|
-
if (!resultEvent) {
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
inputTokens: resultEvent.inputTokens || 0,
|
|
223
|
-
outputTokens: resultEvent.outputTokens || 0,
|
|
224
|
-
cacheReadInputTokens: resultEvent.cacheReadInputTokens || 0,
|
|
225
|
-
cacheCreationInputTokens: resultEvent.cacheCreationInputTokens || 0,
|
|
226
|
-
totalCostUsd: resultEvent.cost || null,
|
|
227
|
-
durationMs: resultEvent.duration || null,
|
|
228
|
-
modelUsage: resultEvent.modelUsage || null,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Ensure the AskUserQuestion blocking hook is installed in user's Claude config.
|
|
234
|
-
* This adds defense-in-depth by blocking the tool at the Claude CLI level.
|
|
235
|
-
* Modifies ~/.claude/settings.json and copies hook script to ~/.claude/hooks/
|
|
236
|
-
*
|
|
237
|
-
* Safe to call multiple times - only modifies config once per process.
|
|
238
|
-
*/
|
|
239
|
-
function ensureAskUserQuestionHook() {
|
|
240
|
-
if (askUserQuestionHookInstalled) {
|
|
241
|
-
return; // Already installed this session
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const userClaudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
245
|
-
const hooksDir = path.join(userClaudeDir, 'hooks');
|
|
246
|
-
const settingsPath = path.join(userClaudeDir, 'settings.json');
|
|
247
|
-
const hookScriptName = 'block-ask-user-question.py';
|
|
248
|
-
const hookScriptDst = path.join(hooksDir, hookScriptName);
|
|
249
|
-
|
|
250
|
-
// Ensure hooks directory exists
|
|
251
|
-
if (!fs.existsSync(hooksDir)) {
|
|
252
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Copy hook script if not present or outdated
|
|
256
|
-
const hookScriptSrc = path.join(__dirname, '..', '..', 'hooks', hookScriptName);
|
|
257
|
-
if (fs.existsSync(hookScriptSrc)) {
|
|
258
|
-
// Always copy to ensure latest version
|
|
259
|
-
fs.copyFileSync(hookScriptSrc, hookScriptDst);
|
|
260
|
-
fs.chmodSync(hookScriptDst, 0o755);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Read existing settings or create new
|
|
264
|
-
let settings = {};
|
|
265
|
-
if (fs.existsSync(settingsPath)) {
|
|
266
|
-
try {
|
|
267
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
268
|
-
} catch (e) {
|
|
269
|
-
console.warn(`[AgentTaskExecutor] Could not parse settings.json, creating new: ${e.message}`);
|
|
270
|
-
settings = {};
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Ensure hooks structure exists
|
|
275
|
-
if (!settings.hooks) {
|
|
276
|
-
settings.hooks = {};
|
|
277
|
-
}
|
|
278
|
-
if (!settings.hooks.PreToolUse) {
|
|
279
|
-
settings.hooks.PreToolUse = [];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Check if AskUserQuestion hook already exists
|
|
283
|
-
const hasHook = settings.hooks.PreToolUse.some(
|
|
284
|
-
(entry) =>
|
|
285
|
-
entry.matcher === 'AskUserQuestion' ||
|
|
286
|
-
(entry.hooks && entry.hooks.some((h) => h.command && h.command.includes(hookScriptName)))
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
if (!hasHook) {
|
|
290
|
-
// Add the hook
|
|
291
|
-
settings.hooks.PreToolUse.push({
|
|
292
|
-
matcher: 'AskUserQuestion',
|
|
293
|
-
hooks: [
|
|
294
|
-
{
|
|
295
|
-
type: 'command',
|
|
296
|
-
command: hookScriptDst,
|
|
297
|
-
},
|
|
298
|
-
],
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// Write updated settings
|
|
302
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
303
|
-
console.log(`[AgentTaskExecutor] Installed AskUserQuestion blocking hook in ${settingsPath}`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
askUserQuestionHookInstalled = true;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Ensure the dangerous git blocking hook is installed in user's Claude config.
|
|
311
|
-
* This blocks dangerous git commands like stash, checkout --, reset --hard, etc.
|
|
312
|
-
* Modifies ~/.claude/settings.json and copies hook script to ~/.claude/hooks/
|
|
313
|
-
*
|
|
314
|
-
* Only used in worktree mode - Docker isolation mode has its own git-safe.sh wrapper.
|
|
315
|
-
* Safe to call multiple times - only modifies config once per process.
|
|
316
|
-
*/
|
|
317
|
-
function ensureDangerousGitHook() {
|
|
318
|
-
if (dangerousGitHookInstalled) {
|
|
319
|
-
return; // Already installed this session
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const userClaudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
323
|
-
const hooksDir = path.join(userClaudeDir, 'hooks');
|
|
324
|
-
const settingsPath = path.join(userClaudeDir, 'settings.json');
|
|
325
|
-
const hookScriptName = 'block-dangerous-git.py';
|
|
326
|
-
const hookScriptDst = path.join(hooksDir, hookScriptName);
|
|
327
|
-
|
|
328
|
-
// Ensure hooks directory exists
|
|
329
|
-
if (!fs.existsSync(hooksDir)) {
|
|
330
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Copy hook script if not present or outdated
|
|
334
|
-
const hookScriptSrc = path.join(__dirname, '..', '..', 'hooks', hookScriptName);
|
|
335
|
-
if (fs.existsSync(hookScriptSrc)) {
|
|
336
|
-
// Always copy to ensure latest version
|
|
337
|
-
fs.copyFileSync(hookScriptSrc, hookScriptDst);
|
|
338
|
-
fs.chmodSync(hookScriptDst, 0o755);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Read existing settings or create new
|
|
342
|
-
let settings = {};
|
|
343
|
-
if (fs.existsSync(settingsPath)) {
|
|
344
|
-
try {
|
|
345
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
346
|
-
} catch (e) {
|
|
347
|
-
console.warn(`[AgentTaskExecutor] Could not parse settings.json, creating new: ${e.message}`);
|
|
348
|
-
settings = {};
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Ensure hooks structure exists
|
|
353
|
-
if (!settings.hooks) {
|
|
354
|
-
settings.hooks = {};
|
|
355
|
-
}
|
|
356
|
-
if (!settings.hooks.PreToolUse) {
|
|
357
|
-
settings.hooks.PreToolUse = [];
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Check if dangerous git hook already exists
|
|
361
|
-
const hasHook = settings.hooks.PreToolUse.some(
|
|
362
|
-
(entry) =>
|
|
363
|
-
entry.matcher === 'Bash' &&
|
|
364
|
-
entry.hooks &&
|
|
365
|
-
entry.hooks.some((h) => h.command && h.command.includes(hookScriptName))
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
if (!hasHook) {
|
|
369
|
-
// Add the hook - matches Bash tool to check for dangerous git commands
|
|
370
|
-
settings.hooks.PreToolUse.push({
|
|
371
|
-
matcher: 'Bash',
|
|
372
|
-
hooks: [
|
|
373
|
-
{
|
|
374
|
-
type: 'command',
|
|
375
|
-
command: hookScriptDst,
|
|
376
|
-
},
|
|
377
|
-
],
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// Write updated settings
|
|
381
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
382
|
-
console.log(`[AgentTaskExecutor] Installed dangerous git blocking hook in ${settingsPath}`);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
dangerousGitHookInstalled = true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Spawn claude-zeroshots process and stream output via message bus
|
|
390
|
-
* @param {Object} agent - Agent instance
|
|
391
|
-
* @param {String} context - Context to pass to Claude
|
|
392
|
-
* @returns {Promise<Object>} Result object { success, output, error }
|
|
393
|
-
*/
|
|
394
|
-
async function spawnClaudeTask(agent, context) {
|
|
395
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
396
|
-
const modelSpec = resolveAgentModelSpec(agent);
|
|
397
|
-
|
|
398
|
-
const ctPath = getClaudeTasksPath();
|
|
399
|
-
const cwd = agent.config.cwd || process.cwd();
|
|
400
|
-
|
|
401
|
-
// Build zeroshot task run args.
|
|
402
|
-
// CRITICAL: Default to strict schema validation to prevent cluster crashes from parse failures
|
|
403
|
-
// strictSchema=true uses Claude CLI's native --json-schema enforcement (no streaming but guaranteed structure)
|
|
404
|
-
// strictSchema=false uses stream-json with post-run validation (live logs but fragile)
|
|
405
|
-
const { desiredOutputFormat, runOutputFormat } = resolveOutputFormatConfig(agent);
|
|
406
|
-
const args = buildTaskRunArgs({
|
|
407
|
-
agent,
|
|
408
|
-
providerName,
|
|
409
|
-
modelSpec,
|
|
410
|
-
runOutputFormat,
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// NOTE: maxRetries is handled by the agent wrapper's internal retry loop,
|
|
414
|
-
// not passed to the CLI. See _handleTrigger() for retry logic.
|
|
415
|
-
|
|
416
|
-
maybeLogStreamJsonNotice(agent, runOutputFormat);
|
|
417
|
-
|
|
418
|
-
// If schema enforcement is desired but we had to run stream-json for live logs,
|
|
419
|
-
// add explicit output instructions so the model still knows the required shape.
|
|
420
|
-
const finalContext = buildFinalContext({
|
|
421
|
-
agent,
|
|
422
|
-
context,
|
|
423
|
-
desiredOutputFormat,
|
|
424
|
-
runOutputFormat,
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
args.push(finalContext);
|
|
428
|
-
|
|
429
|
-
// MOCK SUPPORT: Use injected mock function if provided
|
|
430
|
-
if (agent.mockSpawnFn) {
|
|
431
|
-
return agent.mockSpawnFn(args, { context });
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// SAFETY: Fail hard if testMode=true but no mock (should be caught in constructor)
|
|
435
|
-
if (agent.testMode) {
|
|
436
|
-
throw new Error(
|
|
437
|
-
`AgentWrapper: testMode=true but attempting real Claude API call for agent '${agent.id}'. ` +
|
|
438
|
-
`This is a bug - mock should be set in constructor.`
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ISOLATION MODE: Run inside Docker container
|
|
443
|
-
if (agent.isolation?.enabled) {
|
|
444
|
-
return spawnClaudeTaskIsolated(agent, context);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// NON-ISOLATION MODE: For Claude, use user's existing Claude config
|
|
448
|
-
// AskUserQuestion blocking handled via:
|
|
449
|
-
// 1. Prompt injection (see agent-context-builder)
|
|
450
|
-
// 2. PreToolUse hook (defense-in-depth) - activated by ZEROSHOT_BLOCK_ASK_USER env var
|
|
451
|
-
ensureProviderHooks(agent, providerName);
|
|
452
|
-
const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec);
|
|
453
|
-
const taskId = await spawnTaskProcess({
|
|
454
|
-
agent,
|
|
455
|
-
ctPath,
|
|
456
|
-
args,
|
|
457
|
-
cwd,
|
|
458
|
-
spawnEnv,
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
agent._log(`📋 Agent ${agent.id}: Following zeroshot logs for ${taskId}`);
|
|
462
|
-
|
|
463
|
-
// Wait for task to be registered in zeroshot storage (race condition fix)
|
|
464
|
-
await waitForTaskReady(agent, taskId);
|
|
465
|
-
|
|
466
|
-
// CRITICAL: Poll for REAL process PID from task store
|
|
467
|
-
// The watcher spawns the actual CLI and writes PID to SQLite asynchronously.
|
|
468
|
-
// We must poll because the watcher runs in a forked process.
|
|
469
|
-
const MAX_PID_POLLS = 30; // 3 seconds max
|
|
470
|
-
const PID_POLL_DELAY = 100;
|
|
471
|
-
let realPid = null;
|
|
472
|
-
|
|
473
|
-
for (let i = 0; i < MAX_PID_POLLS; i++) {
|
|
474
|
-
const taskInfo = getTask(taskId);
|
|
475
|
-
if (taskInfo?.pid) {
|
|
476
|
-
realPid = taskInfo.pid;
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
await new Promise((r) => setTimeout(r, PID_POLL_DELAY));
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (realPid) {
|
|
483
|
-
agent.processPid = realPid;
|
|
484
|
-
agent._publishLifecycle('PROCESS_SPAWNED', { pid: realPid });
|
|
485
|
-
agent._log(`📋 Agent ${agent.id}: Process PID: ${realPid}`);
|
|
486
|
-
} else {
|
|
487
|
-
agent._log(`⚠️ Agent ${agent.id}: PID not available (task may use non-standard watcher)`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Now follow the logs and stream output
|
|
491
|
-
return followClaudeTaskLogs(agent, taskId);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function resolveAgentModelSpec(agent) {
|
|
495
|
-
return agent._resolveModelSpec ? agent._resolveModelSpec() : { model: agent._selectModel() };
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function resolveOutputFormatConfig(agent) {
|
|
499
|
-
// CRITICAL: Default to strict schema validation to prevent cluster crashes from parse failures
|
|
500
|
-
// strictSchema=true uses Claude CLI's native --json-schema enforcement (no streaming but guaranteed structure)
|
|
501
|
-
// strictSchema=false uses stream-json with post-run validation (live logs but fragile)
|
|
502
|
-
const desiredOutputFormat = agent.config.outputFormat || 'json';
|
|
503
|
-
const strictSchema = agent.config.strictSchema !== false; // DEFAULT TO TRUE
|
|
504
|
-
const runOutputFormat =
|
|
505
|
-
agent.config.jsonSchema && desiredOutputFormat === 'json' && !strictSchema
|
|
506
|
-
? 'stream-json'
|
|
507
|
-
: desiredOutputFormat;
|
|
508
|
-
|
|
509
|
-
return { desiredOutputFormat, strictSchema, runOutputFormat };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function buildTaskRunArgs({ agent, providerName, modelSpec, runOutputFormat }) {
|
|
513
|
-
const args = ['task', 'run', '--output-format', runOutputFormat, '--provider', providerName];
|
|
514
|
-
|
|
515
|
-
if (modelSpec?.model) {
|
|
516
|
-
args.push('--model', modelSpec.model);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (modelSpec?.reasoningEffort) {
|
|
520
|
-
args.push('--reasoning-effort', modelSpec.reasoningEffort);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Add verification mode flag if configured
|
|
524
|
-
if (agent.config.verificationMode) {
|
|
525
|
-
args.push('-v');
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Add JSON schema if specified in agent config.
|
|
529
|
-
// If we are running stream-json for live logs (strictSchema=false), do NOT pass schema to CLI.
|
|
530
|
-
if (agent.config.jsonSchema && runOutputFormat === 'json') {
|
|
531
|
-
const schema = JSON.stringify(agent.config.jsonSchema);
|
|
532
|
-
args.push('--json-schema', schema);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return args;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function maybeLogStreamJsonNotice(agent, runOutputFormat) {
|
|
539
|
-
if (agent.config.jsonSchema && runOutputFormat !== 'json' && !agent.quiet) {
|
|
540
|
-
agent._log(
|
|
541
|
-
`[Agent ${agent.id}] jsonSchema configured; running stream-json for live logs (strictSchema=false). Schema will be validated after completion.`
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function buildFinalContext({ agent, context, desiredOutputFormat, runOutputFormat }) {
|
|
547
|
-
if (
|
|
548
|
-
agent.config.jsonSchema &&
|
|
549
|
-
desiredOutputFormat === 'json' &&
|
|
550
|
-
runOutputFormat === 'stream-json'
|
|
551
|
-
) {
|
|
552
|
-
return (
|
|
553
|
-
context +
|
|
554
|
-
`\n\n## Output Format (REQUIRED)\n\nReturn a JSON object that matches this schema exactly.\n\nSchema:\n\`\`\`json\n${JSON.stringify(
|
|
555
|
-
agent.config.jsonSchema,
|
|
556
|
-
null,
|
|
557
|
-
2
|
|
558
|
-
)}\n\`\`\`\n`
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return context;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function ensureProviderHooks(agent, providerName) {
|
|
566
|
-
if (providerName !== 'claude') {
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
ensureAskUserQuestionHook();
|
|
571
|
-
|
|
572
|
-
// WORKTREE MODE: Install git safety hook (blocks dangerous git commands)
|
|
573
|
-
if (agent.worktree?.enabled) {
|
|
574
|
-
ensureDangerousGitHook();
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function buildSpawnEnv(agent, providerName, modelSpec) {
|
|
579
|
-
const spawnEnv = { ...process.env };
|
|
580
|
-
|
|
581
|
-
if (providerName === 'claude') {
|
|
582
|
-
Object.assign(spawnEnv, buildClaudeEnv(modelSpec));
|
|
583
|
-
|
|
584
|
-
// WORKTREE MODE: Activate git safety hook via environment variable
|
|
585
|
-
if (agent.worktree?.enabled) {
|
|
586
|
-
spawnEnv.ZEROSHOT_WORKTREE = '1';
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return spawnEnv;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function parseTaskIdFromOutput(stdout) {
|
|
594
|
-
const match = stdout.match(/Task spawned: ((?:task-)?[a-z]+-[a-z]+-[a-z0-9]+)/);
|
|
595
|
-
return match ? match[1] : null;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv }) {
|
|
599
|
-
// Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it
|
|
600
|
-
const SPAWN_TIMEOUT_MS = 30000; // 30 seconds to spawn task
|
|
601
|
-
|
|
602
|
-
return new Promise((resolve, reject) => {
|
|
603
|
-
const proc = spawn(ctPath, args, {
|
|
604
|
-
cwd,
|
|
605
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
606
|
-
env: spawnEnv,
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
// NOTE: Don't emit PROCESS_SPAWNED here - proc.pid is a wrapper that exits immediately.
|
|
610
|
-
// Real PID comes from task store after watcher spawns the actual CLI process.
|
|
611
|
-
// PROCESS_SPAWNED is emitted in spawnClaudeTask after waitForTaskReady + PID polling.
|
|
612
|
-
|
|
613
|
-
let stdout = '';
|
|
614
|
-
let stderr = '';
|
|
615
|
-
let resolved = false;
|
|
616
|
-
|
|
617
|
-
// CRITICAL: Timeout to prevent infinite hang if provider CLI hangs
|
|
618
|
-
const spawnTimeout = setTimeout(() => {
|
|
619
|
-
if (resolved) return;
|
|
620
|
-
resolved = true;
|
|
621
|
-
proc.kill('SIGKILL');
|
|
622
|
-
reject(
|
|
623
|
-
new Error(
|
|
624
|
-
`Spawn timeout after ${SPAWN_TIMEOUT_MS / 1000}s - provider CLI hung. ` +
|
|
625
|
-
`stdout: ${stdout.slice(-500)}, stderr: ${stderr.slice(-500)}`
|
|
626
|
-
)
|
|
627
|
-
);
|
|
628
|
-
}, SPAWN_TIMEOUT_MS);
|
|
629
|
-
|
|
630
|
-
proc.stdout.on('data', (data) => {
|
|
631
|
-
stdout += data.toString();
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
proc.stderr.on('data', (data) => {
|
|
635
|
-
stderr += data.toString();
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
proc.on('close', (code, signal) => {
|
|
639
|
-
clearTimeout(spawnTimeout);
|
|
640
|
-
if (resolved) return;
|
|
641
|
-
resolved = true;
|
|
642
|
-
// Handle process killed by signal (e.g., SIGTERM, SIGKILL, SIGSTOP)
|
|
643
|
-
if (signal) {
|
|
644
|
-
reject(new Error(`Process killed by signal ${signal}${stderr ? `: ${stderr}` : ''}`));
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (code === 0) {
|
|
649
|
-
// Parse task ID from output: "✓ Task spawned: xxx-yyy-nn"
|
|
650
|
-
// Format: <adjective>-<noun>-<digits> (may or may not have task- prefix)
|
|
651
|
-
const spawnedTaskId = parseTaskIdFromOutput(stdout);
|
|
652
|
-
if (spawnedTaskId) {
|
|
653
|
-
agent.currentTaskId = spawnedTaskId; // Track for resume capability
|
|
654
|
-
agent._publishLifecycle('TASK_ID_ASSIGNED', {
|
|
655
|
-
pid: agent.processPid,
|
|
656
|
-
taskId: spawnedTaskId,
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// Start liveness monitoring
|
|
660
|
-
if (agent.enableLivenessCheck) {
|
|
661
|
-
agent.lastOutputTime = Date.now(); // Initialize to spawn time
|
|
662
|
-
agent._startLivenessCheck();
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
resolve(spawnedTaskId);
|
|
666
|
-
} else {
|
|
667
|
-
reject(new Error(`Could not parse task ID from output: ${stdout}`));
|
|
668
|
-
}
|
|
669
|
-
} else {
|
|
670
|
-
reject(new Error(`zeroshot task run failed with code ${code}: ${stderr}`));
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
proc.on('error', (error) => {
|
|
675
|
-
clearTimeout(spawnTimeout);
|
|
676
|
-
if (resolved) return;
|
|
677
|
-
resolved = true;
|
|
678
|
-
reject(error);
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Wait for task to be registered in ct storage
|
|
685
|
-
* @param {Object} agent - Agent instance
|
|
686
|
-
* @param {String} taskId - Task ID to wait for
|
|
687
|
-
* @param {Number} maxRetries - Max retries (default 10)
|
|
688
|
-
* @param {Number} delayMs - Delay between retries (default 200)
|
|
689
|
-
* @returns {Promise<void>}
|
|
690
|
-
*/
|
|
691
|
-
async function waitForTaskReady(agent, taskId, maxRetries = 10, delayMs = 200) {
|
|
692
|
-
const ctPath = getClaudeTasksPath();
|
|
693
|
-
|
|
694
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
695
|
-
let exists = false;
|
|
696
|
-
try {
|
|
697
|
-
const { stdout } = await exec(`${ctPath} status ${taskId}`, { timeout: 5000 });
|
|
698
|
-
exists = !stdout.includes('Task not found');
|
|
699
|
-
} catch {
|
|
700
|
-
// Timeout or error - task not ready yet
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (exists) return;
|
|
704
|
-
|
|
705
|
-
// Wait before retry
|
|
706
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// FAIL FAST: Task not found after retries = unrecoverable error
|
|
710
|
-
// Continuing with a non-existent task causes 30s of pointless polling then crash
|
|
711
|
-
throw new Error(
|
|
712
|
-
`Task ${taskId} not found after ${maxRetries} retries (${maxRetries * delayMs}ms). ` +
|
|
713
|
-
`Task spawn may have failed silently. Check zeroshot task run output.`
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const MAX_STATUS_FAILURES = 30;
|
|
718
|
-
|
|
719
|
-
function createLogFollowState() {
|
|
720
|
-
return {
|
|
721
|
-
output: '',
|
|
722
|
-
logFilePath: null,
|
|
723
|
-
lastSize: 0,
|
|
724
|
-
pollInterval: null,
|
|
725
|
-
statusCheckInterval: null,
|
|
726
|
-
resolved: false,
|
|
727
|
-
lineBuffer: '',
|
|
728
|
-
consecutiveExecFailures: 0,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function lookupLogFilePath(ctPath, taskId) {
|
|
733
|
-
try {
|
|
734
|
-
return execSync(`${ctPath} get-log-path ${taskId}`, {
|
|
735
|
-
encoding: 'utf-8',
|
|
736
|
-
timeout: 5000,
|
|
737
|
-
}).trim();
|
|
738
|
-
} catch {
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function parseTimestampedLine(line) {
|
|
744
|
-
let timestamp = Date.now();
|
|
745
|
-
let content = line.replace(/\r$/, '');
|
|
746
|
-
|
|
747
|
-
const timestampMatch = content.match(/^\[(\d{13})\](.*)$/);
|
|
748
|
-
if (timestampMatch) {
|
|
749
|
-
timestamp = parseInt(timestampMatch[1], 10);
|
|
750
|
-
content = timestampMatch[2];
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return { timestamp, content };
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function shouldSkipLogLine(content) {
|
|
757
|
-
return (
|
|
758
|
-
content.startsWith('===') ||
|
|
759
|
-
content.startsWith('Finished:') ||
|
|
760
|
-
content.startsWith('Exit code:') ||
|
|
761
|
-
(content.includes('"type":"system"') && content.includes('"subtype":"init"'))
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function isValidJsonLine(content) {
|
|
766
|
-
if (!content.trim().startsWith('{')) {
|
|
767
|
-
return false;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
try {
|
|
771
|
-
JSON.parse(content);
|
|
772
|
-
return true;
|
|
773
|
-
} catch {
|
|
774
|
-
return false;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function broadcastAgentLine({ agent, providerName, state, line }) {
|
|
779
|
-
if (!line.trim()) return;
|
|
780
|
-
|
|
781
|
-
const { timestamp, content } = parseTimestampedLine(line);
|
|
782
|
-
if (shouldSkipLogLine(content)) {
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const isValidJson = isValidJsonLine(content);
|
|
787
|
-
state.output += content + '\n';
|
|
788
|
-
|
|
789
|
-
agent.lastOutputTime = Date.now();
|
|
790
|
-
|
|
791
|
-
agent._publish({
|
|
792
|
-
topic: 'AGENT_OUTPUT',
|
|
793
|
-
receiver: 'broadcast',
|
|
794
|
-
timestamp,
|
|
795
|
-
content: {
|
|
796
|
-
text: content,
|
|
797
|
-
data: {
|
|
798
|
-
type: isValidJson ? 'json' : 'text',
|
|
799
|
-
line: content,
|
|
800
|
-
agent: agent.id,
|
|
801
|
-
role: agent.role,
|
|
802
|
-
iteration: agent.iteration,
|
|
803
|
-
provider: providerName,
|
|
804
|
-
},
|
|
805
|
-
},
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function appendContentToBuffer(state, content, onLine) {
|
|
810
|
-
state.lineBuffer += content;
|
|
811
|
-
const lines = state.lineBuffer.split('\n');
|
|
812
|
-
|
|
813
|
-
for (let i = 0; i < lines.length - 1; i++) {
|
|
814
|
-
onLine(lines[i]);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
state.lineBuffer = lines[lines.length - 1];
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function pollLogFileForUpdates({ agent, fsModule, ctPath, taskId, state, onNewContent }) {
|
|
821
|
-
if (!state.logFilePath) {
|
|
822
|
-
const logFilePath = lookupLogFilePath(ctPath, taskId);
|
|
823
|
-
if (!logFilePath) {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
state.logFilePath = logFilePath;
|
|
827
|
-
agent._log(`📋 Agent ${agent.id}: Found log file: ${logFilePath}`);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (!fsModule.existsSync(state.logFilePath)) {
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
try {
|
|
835
|
-
const stats = fsModule.statSync(state.logFilePath);
|
|
836
|
-
const currentSize = stats.size;
|
|
837
|
-
|
|
838
|
-
if (currentSize > state.lastSize) {
|
|
839
|
-
const fd = fsModule.openSync(state.logFilePath, 'r');
|
|
840
|
-
const buffer = Buffer.alloc(currentSize - state.lastSize);
|
|
841
|
-
fsModule.readSync(fd, buffer, 0, buffer.length, state.lastSize);
|
|
842
|
-
fsModule.closeSync(fd);
|
|
843
|
-
|
|
844
|
-
onNewContent(buffer.toString('utf-8'));
|
|
845
|
-
state.lastSize = currentSize;
|
|
846
|
-
}
|
|
847
|
-
} catch (err) {
|
|
848
|
-
const error = /** @type {Error} */ (err);
|
|
849
|
-
console.warn(`⚠️ Agent ${agent.id}: Error reading log: ${error.message}`);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function stripAnsiCodes(value) {
|
|
854
|
-
const ansiPattern = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
|
|
855
|
-
return value.replace(ansiPattern, '');
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function parseStatusFlags(cleanStdout) {
|
|
859
|
-
return {
|
|
860
|
-
isCompleted: /Status:\s+completed/i.test(cleanStdout),
|
|
861
|
-
isFailed: /Status:\s+failed/i.test(cleanStdout),
|
|
862
|
-
isStale: /Status:\s+stale/i.test(cleanStdout),
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function determineStaleSuccess({ agent, output, providerName, taskId }) {
|
|
867
|
-
if (!output) {
|
|
868
|
-
return false;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const hasStructuredOutput = /"structured_output"\s*:/.test(output);
|
|
872
|
-
const hasSuccessResult = /"subtype"\s*:\s*"success"/.test(output);
|
|
873
|
-
let hasParsedOutput = false;
|
|
874
|
-
|
|
875
|
-
try {
|
|
876
|
-
const { extractJsonFromOutput } = require('./output-extraction');
|
|
877
|
-
hasParsedOutput = !!extractJsonFromOutput(output, providerName);
|
|
878
|
-
} catch {
|
|
879
|
-
// Ignore extraction errors - fallback to other signals
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const success = hasStructuredOutput || hasSuccessResult || hasParsedOutput;
|
|
883
|
-
if (!agent.quiet) {
|
|
884
|
-
agent._log(
|
|
885
|
-
`[Agent ${agent.id}] Task ${taskId} is stale - recovered as ${success ? 'SUCCESS' : 'FAILURE'} based on output analysis`
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
return success;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function finalizeLogFollow(agent, state) {
|
|
893
|
-
if (state.pollInterval) {
|
|
894
|
-
clearInterval(state.pollInterval);
|
|
895
|
-
}
|
|
896
|
-
if (state.statusCheckInterval) {
|
|
897
|
-
clearInterval(state.statusCheckInterval);
|
|
898
|
-
}
|
|
899
|
-
agent.currentTask = null;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function handleStatusExecError({ agent, state, ctPath, taskId, error, stderr, resolve }) {
|
|
903
|
-
if (!error) {
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
state.consecutiveExecFailures++;
|
|
908
|
-
if (state.consecutiveExecFailures < MAX_STATUS_FAILURES) {
|
|
909
|
-
return true;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
console.error(
|
|
913
|
-
`[Agent ${agent.id}] ⚠️ Status polling failed ${MAX_STATUS_FAILURES} times consecutively! STOPPING.`
|
|
914
|
-
);
|
|
915
|
-
console.error(` Command: ${ctPath} status ${taskId}`);
|
|
916
|
-
console.error(` Error: ${error.message}`);
|
|
917
|
-
console.error(` Stderr: ${stderr || 'none'}`);
|
|
918
|
-
console.error(` This may indicate zeroshot is not in PATH or task storage is corrupted.`);
|
|
919
|
-
|
|
920
|
-
if (!state.resolved) {
|
|
921
|
-
state.resolved = true;
|
|
922
|
-
finalizeLogFollow(agent, state);
|
|
923
|
-
|
|
924
|
-
agent._publish({
|
|
925
|
-
topic: 'AGENT_ERROR',
|
|
926
|
-
receiver: 'broadcast',
|
|
927
|
-
content: {
|
|
928
|
-
text: `Task ${taskId} polling failed after ${MAX_STATUS_FAILURES} consecutive failures`,
|
|
929
|
-
data: {
|
|
930
|
-
taskId,
|
|
931
|
-
error: 'polling_timeout',
|
|
932
|
-
attempts: state.consecutiveExecFailures,
|
|
933
|
-
role: agent.role,
|
|
934
|
-
iteration: agent.iteration,
|
|
935
|
-
},
|
|
936
|
-
},
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
resolve({
|
|
940
|
-
success: false,
|
|
941
|
-
output: state.output,
|
|
942
|
-
error: `Status polling failed ${MAX_STATUS_FAILURES} times - task may not exist`,
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
return true;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function handleStatusCompletion({
|
|
950
|
-
agent,
|
|
951
|
-
taskId,
|
|
952
|
-
providerName,
|
|
953
|
-
state,
|
|
954
|
-
stdout,
|
|
955
|
-
pollLogFile,
|
|
956
|
-
resolve,
|
|
957
|
-
}) {
|
|
958
|
-
const cleanStdout = stripAnsiCodes(stdout);
|
|
959
|
-
const { isCompleted, isFailed, isStale } = parseStatusFlags(cleanStdout);
|
|
960
|
-
|
|
961
|
-
if (!isCompleted && !isFailed && !isStale) {
|
|
962
|
-
return false;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
pollLogFile();
|
|
966
|
-
|
|
967
|
-
let success = isCompleted;
|
|
968
|
-
if (isStale) {
|
|
969
|
-
success = determineStaleSuccess({ agent, output: state.output, providerName, taskId });
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
setTimeout(() => {
|
|
973
|
-
if (state.resolved) return;
|
|
974
|
-
state.resolved = true;
|
|
975
|
-
|
|
976
|
-
finalizeLogFollow(agent, state);
|
|
977
|
-
|
|
978
|
-
const errorContext = !success
|
|
979
|
-
? extractErrorContext({ output: state.output, statusOutput: stdout, taskId })
|
|
980
|
-
: null;
|
|
981
|
-
|
|
982
|
-
resolve({
|
|
983
|
-
success,
|
|
984
|
-
output: state.output,
|
|
985
|
-
error: errorContext,
|
|
986
|
-
tokenUsage: extractTokenUsage(state.output, providerName),
|
|
987
|
-
});
|
|
988
|
-
}, 500);
|
|
989
|
-
|
|
990
|
-
return true;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function buildKillHandler({ agent, state, providerName, resolve }) {
|
|
994
|
-
return {
|
|
995
|
-
kill: (reason = 'Task killed') => {
|
|
996
|
-
if (state.resolved) return;
|
|
997
|
-
state.resolved = true;
|
|
998
|
-
finalizeLogFollow(agent, state);
|
|
999
|
-
agent._stopLivenessCheck();
|
|
1000
|
-
resolve({
|
|
1001
|
-
success: false,
|
|
1002
|
-
output: state.output,
|
|
1003
|
-
error: reason,
|
|
1004
|
-
tokenUsage: extractTokenUsage(state.output, providerName),
|
|
1005
|
-
});
|
|
1006
|
-
},
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function createLogFollower({ agent, taskId, fsModule, ctPath, providerName }) {
|
|
1011
|
-
return new Promise((resolve) => {
|
|
1012
|
-
const state = createLogFollowState();
|
|
1013
|
-
|
|
1014
|
-
state.logFilePath = lookupLogFilePath(ctPath, taskId);
|
|
1015
|
-
if (state.logFilePath) {
|
|
1016
|
-
agent._log(`📋 Agent ${agent.id}: Following ct logs for ${taskId}`);
|
|
1017
|
-
} else {
|
|
1018
|
-
agent._log(`⏳ Agent ${agent.id}: Waiting for log file...`);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
const broadcastLine = (line) => broadcastAgentLine({ agent, providerName, state, line });
|
|
1022
|
-
const processNewContent = (content) => appendContentToBuffer(state, content, broadcastLine);
|
|
1023
|
-
const pollLogFile = () =>
|
|
1024
|
-
pollLogFileForUpdates({
|
|
1025
|
-
agent,
|
|
1026
|
-
fsModule,
|
|
1027
|
-
ctPath,
|
|
1028
|
-
taskId,
|
|
1029
|
-
state,
|
|
1030
|
-
onNewContent: processNewContent,
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
state.pollInterval = setInterval(pollLogFile, 300);
|
|
1034
|
-
|
|
1035
|
-
state.statusCheckInterval = setInterval(() => {
|
|
1036
|
-
exec(`${ctPath} status ${taskId}`, { timeout: 5000 }, (error, stdout, stderr) => {
|
|
1037
|
-
if (state.resolved) return;
|
|
1038
|
-
|
|
1039
|
-
if (handleStatusExecError({ agent, state, ctPath, taskId, error, stderr, resolve })) {
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
state.consecutiveExecFailures = 0;
|
|
1044
|
-
handleStatusCompletion({
|
|
1045
|
-
agent,
|
|
1046
|
-
taskId,
|
|
1047
|
-
providerName,
|
|
1048
|
-
state,
|
|
1049
|
-
stdout,
|
|
1050
|
-
pollLogFile,
|
|
1051
|
-
resolve,
|
|
1052
|
-
});
|
|
1053
|
-
});
|
|
1054
|
-
}, 1000);
|
|
1055
|
-
|
|
1056
|
-
agent.currentTask = buildKillHandler({ agent, state, providerName, resolve });
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Follow claude-zeroshots logs until completion, streaming to message bus
|
|
1062
|
-
* Reads log file directly for reliable streaming
|
|
1063
|
-
* @param {Object} agent - Agent instance
|
|
1064
|
-
* @param {String} taskId - Task ID to follow
|
|
1065
|
-
* @returns {Promise<Object>} Result object { success, output, error }
|
|
1066
|
-
*/
|
|
1067
|
-
function followClaudeTaskLogs(agent, taskId) {
|
|
1068
|
-
const fsModule = require('fs');
|
|
1069
|
-
const ctPath = getClaudeTasksPath();
|
|
1070
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
1071
|
-
|
|
1072
|
-
return createLogFollower({ agent, taskId, fsModule, ctPath, providerName });
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/**
|
|
1076
|
-
* Get path to claude-zeroshots executable
|
|
1077
|
-
* @returns {String} Path to zeroshot command
|
|
1078
|
-
*/
|
|
1079
|
-
function getClaudeTasksPath() {
|
|
1080
|
-
// Use zeroshot command (unified CLI)
|
|
1081
|
-
return 'zeroshot'; // Assumes zeroshot is installed globally
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
/**
|
|
1085
|
-
* Spawn claude-zeroshots inside Docker container (isolation mode)
|
|
1086
|
-
* Runs Claude CLI inside the container for full isolation
|
|
1087
|
-
* @param {Object} agent - Agent instance
|
|
1088
|
-
* @param {String} context - Context to pass to Claude
|
|
1089
|
-
* @returns {Promise<Object>} Result object { success, output, error }
|
|
1090
|
-
*/
|
|
1091
|
-
async function spawnClaudeTaskIsolated(agent, context) {
|
|
1092
|
-
const { manager, clusterId } = agent.isolation;
|
|
1093
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
1094
|
-
const modelSpec = agent._resolveModelSpec
|
|
1095
|
-
? agent._resolveModelSpec()
|
|
1096
|
-
: { model: agent._selectModel() };
|
|
1097
|
-
|
|
1098
|
-
agent._log(`📦 Agent ${agent.id}: Running task in isolated container using zeroshot task run...`);
|
|
1099
|
-
|
|
1100
|
-
// Build zeroshot task run command (same infrastructure as non-isolation mode)
|
|
1101
|
-
// CRITICAL: Default to strict schema validation to prevent cluster crashes from parse failures
|
|
1102
|
-
const desiredOutputFormat = agent.config.outputFormat || 'json';
|
|
1103
|
-
const strictSchema = agent.config.strictSchema !== false; // DEFAULT TO TRUE
|
|
1104
|
-
const runOutputFormat =
|
|
1105
|
-
agent.config.jsonSchema && desiredOutputFormat === 'json' && !strictSchema
|
|
1106
|
-
? 'stream-json'
|
|
1107
|
-
: desiredOutputFormat;
|
|
1108
|
-
|
|
1109
|
-
const command = [
|
|
1110
|
-
'zeroshot',
|
|
1111
|
-
'task',
|
|
1112
|
-
'run',
|
|
1113
|
-
'--output-format',
|
|
1114
|
-
runOutputFormat,
|
|
1115
|
-
'--provider',
|
|
1116
|
-
providerName,
|
|
1117
|
-
];
|
|
1118
|
-
|
|
1119
|
-
if (modelSpec?.model) {
|
|
1120
|
-
command.push('--model', modelSpec.model);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
if (modelSpec?.reasoningEffort) {
|
|
1124
|
-
command.push('--reasoning-effort', modelSpec.reasoningEffort);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Add verification mode flag if configured
|
|
1128
|
-
if (agent.config.verificationMode) {
|
|
1129
|
-
command.push('-v');
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Add JSON schema if specified in agent config
|
|
1133
|
-
// If we are running stream-json for live logs (strictSchema=false), do NOT pass schema to CLI
|
|
1134
|
-
if (agent.config.jsonSchema) {
|
|
1135
|
-
if (runOutputFormat === 'json') {
|
|
1136
|
-
// strictSchema=true OR no schema conflict: pass schema to CLI for native enforcement
|
|
1137
|
-
const schema = JSON.stringify(agent.config.jsonSchema);
|
|
1138
|
-
command.push('--json-schema', schema);
|
|
1139
|
-
} else if (!agent.quiet) {
|
|
1140
|
-
agent._log(
|
|
1141
|
-
`[Agent ${agent.id}] jsonSchema configured; running stream-json for live logs (strictSchema=false). Schema will be validated after completion.`
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Add explicit output instructions when we run stream-json for a jsonSchema agent
|
|
1147
|
-
let finalContext = context;
|
|
1148
|
-
if (
|
|
1149
|
-
agent.config.jsonSchema &&
|
|
1150
|
-
desiredOutputFormat === 'json' &&
|
|
1151
|
-
runOutputFormat === 'stream-json'
|
|
1152
|
-
) {
|
|
1153
|
-
finalContext += `\n\n## Output Format (REQUIRED)\n\nReturn a JSON object that matches this schema exactly.\n\nSchema:\n\`\`\`json\n${JSON.stringify(
|
|
1154
|
-
agent.config.jsonSchema,
|
|
1155
|
-
null,
|
|
1156
|
-
2
|
|
1157
|
-
)}\n\`\`\`\n`;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
command.push(finalContext);
|
|
1161
|
-
|
|
1162
|
-
// STEP 1: Spawn task and extract task ID (same as non-isolated mode)
|
|
1163
|
-
// Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it
|
|
1164
|
-
const SPAWN_TIMEOUT_MS = 30000; // 30 seconds to spawn task
|
|
1165
|
-
// Note: Auth env vars are injected by IsolationManager, we only need model mapping here
|
|
1166
|
-
const isolatedEnv =
|
|
1167
|
-
providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {};
|
|
1168
|
-
|
|
1169
|
-
const taskId = await new Promise((resolve, reject) => {
|
|
1170
|
-
const proc = manager.spawnInContainer(clusterId, command, {
|
|
1171
|
-
env: isolatedEnv,
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
// Track PID for resource monitoring
|
|
1175
|
-
agent.processPid = proc.pid;
|
|
1176
|
-
agent._publishLifecycle('PROCESS_SPAWNED', { pid: proc.pid });
|
|
1177
|
-
|
|
1178
|
-
let stdout = '';
|
|
1179
|
-
let stderr = '';
|
|
1180
|
-
let resolved = false;
|
|
1181
|
-
|
|
1182
|
-
// CRITICAL: Timeout to prevent infinite hang if provider CLI hangs
|
|
1183
|
-
const spawnTimeout = setTimeout(() => {
|
|
1184
|
-
if (resolved) return;
|
|
1185
|
-
resolved = true;
|
|
1186
|
-
proc.kill('SIGKILL');
|
|
1187
|
-
reject(
|
|
1188
|
-
new Error(
|
|
1189
|
-
`Spawn timeout after ${SPAWN_TIMEOUT_MS / 1000}s - provider CLI hung. ` +
|
|
1190
|
-
`stdout: ${stdout.slice(-500)}, stderr: ${stderr.slice(-500)}`
|
|
1191
|
-
)
|
|
1192
|
-
);
|
|
1193
|
-
}, SPAWN_TIMEOUT_MS);
|
|
1194
|
-
|
|
1195
|
-
proc.stdout.on('data', (data) => {
|
|
1196
|
-
stdout += data.toString();
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
proc.stderr.on('data', (data) => {
|
|
1200
|
-
stderr += data.toString();
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
proc.on('close', (code, signal) => {
|
|
1204
|
-
clearTimeout(spawnTimeout);
|
|
1205
|
-
if (resolved) return;
|
|
1206
|
-
resolved = true;
|
|
1207
|
-
// Handle process killed by signal
|
|
1208
|
-
if (signal) {
|
|
1209
|
-
reject(new Error(`Process killed by signal ${signal}${stderr ? `: ${stderr}` : ''}`));
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
if (code === 0) {
|
|
1214
|
-
// Parse task ID from output: "✓ Task spawned: xxx-yyy-nn"
|
|
1215
|
-
const match = stdout.match(/Task spawned: ((?:task-)?[a-z]+-[a-z]+-[a-z0-9]+)/);
|
|
1216
|
-
if (match) {
|
|
1217
|
-
const spawnedTaskId = match[1];
|
|
1218
|
-
agent.currentTaskId = spawnedTaskId; // Track for resume capability
|
|
1219
|
-
agent._publishLifecycle('TASK_ID_ASSIGNED', {
|
|
1220
|
-
pid: agent.processPid,
|
|
1221
|
-
taskId: spawnedTaskId,
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
// Start liveness monitoring
|
|
1225
|
-
if (agent.enableLivenessCheck) {
|
|
1226
|
-
agent.lastOutputTime = Date.now(); // Initialize to spawn time
|
|
1227
|
-
agent._startLivenessCheck();
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
resolve(spawnedTaskId);
|
|
1231
|
-
} else {
|
|
1232
|
-
reject(new Error(`Could not parse task ID from output: ${stdout}`));
|
|
1233
|
-
}
|
|
1234
|
-
} else {
|
|
1235
|
-
reject(new Error(`zeroshot task run failed with code ${code}: ${stderr}`));
|
|
1236
|
-
}
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
proc.on('error', (error) => {
|
|
1240
|
-
clearTimeout(spawnTimeout);
|
|
1241
|
-
if (resolved) return;
|
|
1242
|
-
resolved = true;
|
|
1243
|
-
reject(error);
|
|
1244
|
-
});
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
agent._log(`📋 Agent ${agent.id}: Following zeroshot logs for ${taskId} in container...`);
|
|
1248
|
-
|
|
1249
|
-
// STEP 2: Follow the task's log file inside container (NOT the spawn stdout!)
|
|
1250
|
-
return followClaudeTaskLogsIsolated(agent, taskId);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
/**
|
|
1254
|
-
* Follow task logs inside Docker container (isolated mode)
|
|
1255
|
-
* Reads task log file inside container and streams JSON lines to message bus
|
|
1256
|
-
* @param {Object} agent - Agent instance with isolation context
|
|
1257
|
-
* @param {String} taskId - Task ID to follow
|
|
1258
|
-
* @returns {Promise<Object>} Result object
|
|
1259
|
-
* @private
|
|
1260
|
-
*/
|
|
1261
|
-
/**
|
|
1262
|
-
* Follow Claude task logs in isolated container using persistent tail -f stream
|
|
1263
|
-
* Issue #23: Persistent log streaming instead of polling (10-20% latency reduction)
|
|
1264
|
-
*
|
|
1265
|
-
* OLD APPROACH (removed):
|
|
1266
|
-
* - Polled every 500ms with 2-3 docker exec calls per poll
|
|
1267
|
-
* - Each docker exec = ~100-200ms overhead
|
|
1268
|
-
* - Total: 300-400ms latency per poll cycle
|
|
1269
|
-
*
|
|
1270
|
-
* NEW APPROACH:
|
|
1271
|
-
* - Single persistent `tail -f` stream via spawnInContainer()
|
|
1272
|
-
* - Lines arrive in real-time as they're written
|
|
1273
|
-
* - Status checks reduced to every 2 seconds (not every poll)
|
|
1274
|
-
* - Result: 10-20% overall latency reduction
|
|
1275
|
-
*/
|
|
1276
|
-
function createIsolatedLogState() {
|
|
1277
|
-
return {
|
|
1278
|
-
taskExited: false,
|
|
1279
|
-
fullOutput: '',
|
|
1280
|
-
tailProcess: null,
|
|
1281
|
-
statusCheckInterval: null,
|
|
1282
|
-
lineBuffer: '',
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function buildIsolatedCleanup(state) {
|
|
1287
|
-
return () => {
|
|
1288
|
-
if (state.tailProcess) {
|
|
1289
|
-
try {
|
|
1290
|
-
state.tailProcess.kill('SIGTERM');
|
|
1291
|
-
} catch {
|
|
1292
|
-
// Ignore - process may already be dead
|
|
1293
|
-
}
|
|
1294
|
-
state.tailProcess = null;
|
|
1295
|
-
}
|
|
1296
|
-
if (state.statusCheckInterval) {
|
|
1297
|
-
clearInterval(state.statusCheckInterval);
|
|
1298
|
-
state.statusCheckInterval = null;
|
|
1299
|
-
}
|
|
1300
|
-
};
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function broadcastIsolatedLine({ agent, providerName, taskId, line }) {
|
|
1304
|
-
const timestampMatch = line.match(/^\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]\s*(.*)$/);
|
|
1305
|
-
const timestamp = timestampMatch ? new Date(timestampMatch[1]).getTime() : Date.now();
|
|
1306
|
-
const content = timestampMatch ? timestampMatch[2] : line;
|
|
1307
|
-
|
|
1308
|
-
agent.messageBus.publish({
|
|
1309
|
-
cluster_id: agent.cluster.id,
|
|
1310
|
-
topic: 'AGENT_OUTPUT',
|
|
1311
|
-
sender: agent.id,
|
|
1312
|
-
content: {
|
|
1313
|
-
data: {
|
|
1314
|
-
line: content,
|
|
1315
|
-
taskId,
|
|
1316
|
-
iteration: agent.iteration,
|
|
1317
|
-
provider: providerName,
|
|
1318
|
-
},
|
|
1319
|
-
},
|
|
1320
|
-
timestamp,
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
agent.lastOutputTime = Date.now();
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
function appendIsolatedContent(state, content, onLine) {
|
|
1327
|
-
state.lineBuffer += content;
|
|
1328
|
-
const lines = state.lineBuffer.split('\n');
|
|
1329
|
-
|
|
1330
|
-
for (let i = 0; i < lines.length - 1; i++) {
|
|
1331
|
-
if (lines[i].trim()) {
|
|
1332
|
-
onLine(lines[i]);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
state.lineBuffer = lines[lines.length - 1];
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
function startIsolatedTail({ agent, manager, clusterId, logFilePath, state, onLine }) {
|
|
1340
|
-
state.tailProcess = manager.spawnInContainer(clusterId, [
|
|
1341
|
-
'sh',
|
|
1342
|
-
'-c',
|
|
1343
|
-
`while [ ! -f "${logFilePath}" ]; do sleep 0.1; done; tail -F -n +1 "${logFilePath}"`,
|
|
1344
|
-
]);
|
|
1345
|
-
|
|
1346
|
-
state.tailProcess.stdout.on('data', (data) => {
|
|
1347
|
-
const chunk = data.toString();
|
|
1348
|
-
state.fullOutput += chunk;
|
|
1349
|
-
appendIsolatedContent(state, chunk, onLine);
|
|
1350
|
-
});
|
|
1351
|
-
|
|
1352
|
-
state.tailProcess.stderr.on('data', (data) => {
|
|
1353
|
-
const msg = data.toString().trim();
|
|
1354
|
-
if (msg && !msg.includes('file truncated')) {
|
|
1355
|
-
agent._log(`[${agent.id}] tail stderr: ${msg}`);
|
|
1356
|
-
}
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
state.tailProcess.on('close', (exitCode) => {
|
|
1360
|
-
if (!state.taskExited) {
|
|
1361
|
-
agent._log(`[${agent.id}] tail process exited with code ${exitCode}`);
|
|
1362
|
-
}
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
state.tailProcess.on('error', (err) => {
|
|
1366
|
-
agent._log(`[${agent.id}] tail process error: ${err.message}`);
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
async function checkIsolatedStatus({
|
|
1371
|
-
agent,
|
|
1372
|
-
manager,
|
|
1373
|
-
clusterId,
|
|
1374
|
-
logFilePath,
|
|
1375
|
-
taskId,
|
|
1376
|
-
providerName,
|
|
1377
|
-
state,
|
|
1378
|
-
cleanup,
|
|
1379
|
-
resolve,
|
|
1380
|
-
onLine,
|
|
1381
|
-
}) {
|
|
1382
|
-
if (state.taskExited) return;
|
|
1383
|
-
|
|
1384
|
-
const statusResult = await manager.execInContainer(clusterId, [
|
|
1385
|
-
'sh',
|
|
1386
|
-
'-c',
|
|
1387
|
-
`zeroshot status ${taskId} 2>/dev/null || echo "not_found"`,
|
|
1388
|
-
]);
|
|
1389
|
-
|
|
1390
|
-
const statusOutput = statusResult.stdout;
|
|
1391
|
-
const isSuccess = /Status:\s+completed/i.test(statusOutput);
|
|
1392
|
-
const isError = /Status:\s+failed/i.test(statusOutput);
|
|
1393
|
-
const isNotFound = statusOutput.includes('not_found');
|
|
1394
|
-
|
|
1395
|
-
if (!isSuccess && !isError && !isNotFound) {
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
state.taskExited = true;
|
|
1400
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
1401
|
-
|
|
1402
|
-
const finalReadResult = await manager.execInContainer(clusterId, [
|
|
1403
|
-
'sh',
|
|
1404
|
-
'-c',
|
|
1405
|
-
`cat "${logFilePath}" 2>/dev/null || echo ""`,
|
|
1406
|
-
]);
|
|
1407
|
-
|
|
1408
|
-
if (finalReadResult.code === 0 && finalReadResult.stdout) {
|
|
1409
|
-
state.fullOutput = finalReadResult.stdout;
|
|
1410
|
-
const remainingLines = state.fullOutput.split('\n');
|
|
1411
|
-
for (const line of remainingLines) {
|
|
1412
|
-
if (line.trim()) {
|
|
1413
|
-
onLine(line);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
cleanup();
|
|
1419
|
-
|
|
1420
|
-
const success = isSuccess && !isError;
|
|
1421
|
-
const errorContext = !success
|
|
1422
|
-
? extractErrorContext({ output: state.fullOutput, taskId, isNotFound })
|
|
1423
|
-
: null;
|
|
1424
|
-
const parsedResult = await agent._parseResultOutput(state.fullOutput);
|
|
1425
|
-
|
|
1426
|
-
resolve({
|
|
1427
|
-
success,
|
|
1428
|
-
output: state.fullOutput,
|
|
1429
|
-
taskId,
|
|
1430
|
-
result: parsedResult,
|
|
1431
|
-
error: errorContext,
|
|
1432
|
-
tokenUsage: extractTokenUsage(state.fullOutput, providerName),
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function startIsolatedStatusChecks({
|
|
1437
|
-
agent,
|
|
1438
|
-
manager,
|
|
1439
|
-
clusterId,
|
|
1440
|
-
logFilePath,
|
|
1441
|
-
taskId,
|
|
1442
|
-
providerName,
|
|
1443
|
-
state,
|
|
1444
|
-
cleanup,
|
|
1445
|
-
resolve,
|
|
1446
|
-
onLine,
|
|
1447
|
-
}) {
|
|
1448
|
-
state.statusCheckInterval = setInterval(() => {
|
|
1449
|
-
checkIsolatedStatus({
|
|
1450
|
-
agent,
|
|
1451
|
-
manager,
|
|
1452
|
-
clusterId,
|
|
1453
|
-
logFilePath,
|
|
1454
|
-
taskId,
|
|
1455
|
-
providerName,
|
|
1456
|
-
state,
|
|
1457
|
-
cleanup,
|
|
1458
|
-
resolve,
|
|
1459
|
-
onLine,
|
|
1460
|
-
}).catch((statusErr) => {
|
|
1461
|
-
agent._log(`[${agent.id}] Status check error (will retry): ${statusErr.message}`);
|
|
1462
|
-
});
|
|
1463
|
-
}, 2000);
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
1467
|
-
const { isolation } = agent;
|
|
1468
|
-
if (!isolation?.manager) {
|
|
1469
|
-
throw new Error('followClaudeTaskLogsIsolated: isolation manager not found');
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const manager = isolation.manager;
|
|
1473
|
-
const clusterId = isolation.clusterId;
|
|
1474
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
1475
|
-
|
|
1476
|
-
return new Promise((resolve, reject) => {
|
|
1477
|
-
const state = createIsolatedLogState();
|
|
1478
|
-
const cleanup = buildIsolatedCleanup(state);
|
|
1479
|
-
const onLine = (line) => broadcastIsolatedLine({ agent, providerName, taskId, line });
|
|
1480
|
-
|
|
1481
|
-
manager
|
|
1482
|
-
.execInContainer(clusterId, ['sh', '-c', `zeroshot get-log-path ${taskId}`])
|
|
1483
|
-
.then(({ stdout, stderr, code }) => {
|
|
1484
|
-
if (code !== 0) {
|
|
1485
|
-
cleanup();
|
|
1486
|
-
return reject(
|
|
1487
|
-
new Error(`Failed to get log path for ${taskId} inside container: ${stderr || stdout}`)
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
const logFilePath = stdout.trim();
|
|
1492
|
-
if (!logFilePath) {
|
|
1493
|
-
cleanup();
|
|
1494
|
-
return reject(new Error(`Empty log path returned for ${taskId}`));
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
agent._log(`[${agent.id}] Following isolated task logs (streaming): ${logFilePath}`);
|
|
1498
|
-
|
|
1499
|
-
startIsolatedTail({
|
|
1500
|
-
agent,
|
|
1501
|
-
manager,
|
|
1502
|
-
clusterId,
|
|
1503
|
-
logFilePath,
|
|
1504
|
-
state,
|
|
1505
|
-
onLine,
|
|
1506
|
-
});
|
|
1507
|
-
|
|
1508
|
-
startIsolatedStatusChecks({
|
|
1509
|
-
agent,
|
|
1510
|
-
manager,
|
|
1511
|
-
clusterId,
|
|
1512
|
-
logFilePath,
|
|
1513
|
-
taskId,
|
|
1514
|
-
providerName,
|
|
1515
|
-
state,
|
|
1516
|
-
cleanup,
|
|
1517
|
-
resolve,
|
|
1518
|
-
onLine,
|
|
1519
|
-
});
|
|
1520
|
-
|
|
1521
|
-
if (agent.timeout > 0) {
|
|
1522
|
-
setTimeout(() => {
|
|
1523
|
-
if (!state.taskExited) {
|
|
1524
|
-
cleanup();
|
|
1525
|
-
reject(new Error(`Task ${taskId} timeout after ${agent.timeout}ms (isolated mode)`));
|
|
1526
|
-
}
|
|
1527
|
-
}, agent.timeout);
|
|
1528
|
-
}
|
|
1529
|
-
})
|
|
1530
|
-
.catch((err) => {
|
|
1531
|
-
cleanup();
|
|
1532
|
-
reject(err);
|
|
1533
|
-
});
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
/**
|
|
1538
|
-
* Parse agent output to extract structured result data
|
|
1539
|
-
* GENERIC - returns whatever structured output the agent provides
|
|
1540
|
-
* Works with any agent schema (planner, validator, worker, etc.)
|
|
1541
|
-
*
|
|
1542
|
-
* Uses clean extraction pipeline from output-extraction.js
|
|
1543
|
-
* Falls back to reformatting if extraction fails and schema is available
|
|
1544
|
-
*
|
|
1545
|
-
* @param {Object} agent - Agent instance
|
|
1546
|
-
* @param {String} output - Raw output from agent
|
|
1547
|
-
* @returns {Promise<Object>} Parsed result data
|
|
1548
|
-
*/
|
|
1549
|
-
async function parseResultOutput(agent, output) {
|
|
1550
|
-
// Empty or error outputs = FAIL
|
|
1551
|
-
if (!output || output.includes('Task not found') || output.includes('Process terminated')) {
|
|
1552
|
-
throw new Error('Task execution failed - no output');
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
1556
|
-
const { extractJsonFromOutput } = require('./output-extraction');
|
|
1557
|
-
|
|
1558
|
-
// Use clean extraction pipeline
|
|
1559
|
-
let parsed = extractJsonFromOutput(output, providerName);
|
|
1560
|
-
|
|
1561
|
-
// If extraction failed but we have a schema, attempt reformatting
|
|
1562
|
-
if (!parsed && agent.config.jsonSchema) {
|
|
1563
|
-
const { reformatOutput } = require('./output-reformatter');
|
|
1564
|
-
|
|
1565
|
-
try {
|
|
1566
|
-
parsed = await reformatOutput({
|
|
1567
|
-
rawOutput: output,
|
|
1568
|
-
schema: agent.config.jsonSchema,
|
|
1569
|
-
providerName,
|
|
1570
|
-
onAttempt: (attempt, lastError) => {
|
|
1571
|
-
if (lastError) {
|
|
1572
|
-
console.warn(`[Agent ${agent.id}] Reformat attempt ${attempt}: ${lastError}`);
|
|
1573
|
-
} else {
|
|
1574
|
-
console.warn(
|
|
1575
|
-
`[Agent ${agent.id}] JSON extraction failed, reformatting (attempt ${attempt})...`
|
|
1576
|
-
);
|
|
1577
|
-
}
|
|
1578
|
-
},
|
|
1579
|
-
});
|
|
1580
|
-
} catch (reformatError) {
|
|
1581
|
-
// Reformatting failed - fall through to error below
|
|
1582
|
-
console.error(`[Agent ${agent.id}] Reformatting failed: ${reformatError.message}`);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
if (!parsed) {
|
|
1587
|
-
const trimmedOutput = output.trim();
|
|
1588
|
-
console.error(`\n${'='.repeat(80)}`);
|
|
1589
|
-
console.error(`🔴 AGENT OUTPUT MISSING REQUIRED JSON BLOCK`);
|
|
1590
|
-
console.error(`${'='.repeat(80)}`);
|
|
1591
|
-
console.error(`Agent: ${agent.id}, Role: ${agent.role}, Provider: ${providerName}`);
|
|
1592
|
-
console.error(`Output (last 500 chars): ${trimmedOutput.slice(-500)}`);
|
|
1593
|
-
console.error(`${'='.repeat(80)}\n`);
|
|
1594
|
-
throw new Error(`Agent ${agent.id} output missing required JSON block`);
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// If a JSON schema is configured, validate parsed output locally.
|
|
1598
|
-
// This preserves schema enforcement even when we run stream-json for live logs.
|
|
1599
|
-
// IMPORTANT: For non-validator agents we warn but do not fail the cluster.
|
|
1600
|
-
if (agent.config.jsonSchema) {
|
|
1601
|
-
// Normalize enum values BEFORE validation (handles case mismatches, common variations)
|
|
1602
|
-
// This is provider-agnostic - works for Claude CLI, Gemini, Codex, etc.
|
|
1603
|
-
normalizeEnumValues(parsed, agent.config.jsonSchema);
|
|
1604
|
-
|
|
1605
|
-
const Ajv = require('ajv');
|
|
1606
|
-
const ajv = new Ajv({
|
|
1607
|
-
allErrors: true,
|
|
1608
|
-
strict: false,
|
|
1609
|
-
coerceTypes: false, // STRICT: Reject type mismatches (e.g., null instead of array)
|
|
1610
|
-
useDefaults: true,
|
|
1611
|
-
removeAdditional: true,
|
|
1612
|
-
});
|
|
1613
|
-
const validate = ajv.compile(agent.config.jsonSchema);
|
|
1614
|
-
const valid = validate(parsed);
|
|
1615
|
-
if (!valid) {
|
|
1616
|
-
const errorList = (validate.errors || [])
|
|
1617
|
-
.slice(0, 5)
|
|
1618
|
-
.map((e) => `${e.instancePath || e.schemaPath} ${e.message}`)
|
|
1619
|
-
.join('; ');
|
|
1620
|
-
const msg =
|
|
1621
|
-
`Agent ${agent.id} output failed JSON schema validation: ` +
|
|
1622
|
-
(errorList || 'unknown schema error');
|
|
1623
|
-
|
|
1624
|
-
// Validators stay strict (they already have auto-approval fallback on crash).
|
|
1625
|
-
if (agent.role === 'validator') {
|
|
1626
|
-
throw new Error(msg);
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// Non-validators: emit warning and continue with best-effort parsed data.
|
|
1630
|
-
console.warn(`⚠️ ${msg}`);
|
|
1631
|
-
agent._publish({
|
|
1632
|
-
topic: 'AGENT_SCHEMA_WARNING',
|
|
1633
|
-
receiver: 'broadcast',
|
|
1634
|
-
content: {
|
|
1635
|
-
text: msg,
|
|
1636
|
-
data: {
|
|
1637
|
-
agent: agent.id,
|
|
1638
|
-
role: agent.role,
|
|
1639
|
-
iteration: agent.iteration,
|
|
1640
|
-
errors: validate.errors || [],
|
|
1641
|
-
},
|
|
1642
|
-
},
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
// Return whatever the agent produced - no hardcoded field requirements
|
|
1648
|
-
// Template substitution will validate that required fields exist
|
|
1649
|
-
return parsed;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
/**
|
|
1653
|
-
* Kill current task
|
|
1654
|
-
* @param {Object} agent - Agent instance
|
|
1655
|
-
*/
|
|
1656
|
-
function killTask(agent) {
|
|
1657
|
-
if (agent.currentTask) {
|
|
1658
|
-
// currentTask may be either a ChildProcess or our custom { kill } object
|
|
1659
|
-
if (typeof agent.currentTask.kill === 'function') {
|
|
1660
|
-
agent.currentTask.kill('SIGTERM');
|
|
1661
|
-
}
|
|
1662
|
-
agent.currentTask = null;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// Also kill the underlying zeroshot task if we have a task ID
|
|
1666
|
-
// This ensures the task process is stopped, not just our polling intervals
|
|
1667
|
-
if (agent.currentTaskId) {
|
|
1668
|
-
const ctPath = getClaudeTasksPath();
|
|
1669
|
-
exec(`${ctPath} task kill ${agent.currentTaskId}`, { timeout: 10000 }, (error) => {
|
|
1670
|
-
if (error) {
|
|
1671
|
-
// Task may have already completed or been killed, ignore errors
|
|
1672
|
-
agent._log(`Note: Could not kill task ${agent.currentTaskId}: ${error.message}`);
|
|
1673
|
-
} else {
|
|
1674
|
-
agent._log(`Killed task ${agent.currentTaskId}`);
|
|
1675
|
-
}
|
|
1676
|
-
});
|
|
1677
|
-
agent.currentTaskId = null;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
module.exports = {
|
|
1682
|
-
ensureAskUserQuestionHook,
|
|
1683
|
-
spawnClaudeTask,
|
|
1684
|
-
followClaudeTaskLogs,
|
|
1685
|
-
waitForTaskReady,
|
|
1686
|
-
spawnClaudeTaskIsolated,
|
|
1687
|
-
getClaudeTasksPath,
|
|
1688
|
-
parseResultOutput,
|
|
1689
|
-
killTask,
|
|
1690
|
-
};
|