@astroanywhere/agent 0.1.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.
- package/LICENSE +76 -0
- package/README.md +178 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +401 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +9 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/mcp.d.ts +16 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +19 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/setup.d.ts +20 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +585 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/start.d.ts +16 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +638 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +63 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +5 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +85 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/execution/direct-strategy.d.ts +18 -0
- package/dist/execution/direct-strategy.d.ts.map +1 -0
- package/dist/execution/direct-strategy.js +156 -0
- package/dist/execution/direct-strategy.js.map +1 -0
- package/dist/execution/docker-strategy.d.ts +26 -0
- package/dist/execution/docker-strategy.d.ts.map +1 -0
- package/dist/execution/docker-strategy.js +222 -0
- package/dist/execution/docker-strategy.js.map +1 -0
- package/dist/execution/index.d.ts +14 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +13 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/kubernetes-exec-strategy.d.ts +23 -0
- package/dist/execution/kubernetes-exec-strategy.d.ts.map +1 -0
- package/dist/execution/kubernetes-exec-strategy.js +232 -0
- package/dist/execution/kubernetes-exec-strategy.js.map +1 -0
- package/dist/execution/registry.d.ts +41 -0
- package/dist/execution/registry.d.ts.map +1 -0
- package/dist/execution/registry.js +84 -0
- package/dist/execution/registry.js.map +1 -0
- package/dist/execution/slurm-strategy.d.ts +22 -0
- package/dist/execution/slurm-strategy.d.ts.map +1 -0
- package/dist/execution/slurm-strategy.js +219 -0
- package/dist/execution/slurm-strategy.js.map +1 -0
- package/dist/execution/types.d.ts +72 -0
- package/dist/execution/types.d.ts.map +1 -0
- package/dist/execution/types.js +10 -0
- package/dist/execution/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +35 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +126 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/config.d.ts +174 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +399 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/copy-worktree.d.ts +73 -0
- package/dist/lib/copy-worktree.d.ts.map +1 -0
- package/dist/lib/copy-worktree.js +374 -0
- package/dist/lib/copy-worktree.js.map +1 -0
- package/dist/lib/git-pr.d.ts +63 -0
- package/dist/lib/git-pr.d.ts.map +1 -0
- package/dist/lib/git-pr.js +224 -0
- package/dist/lib/git-pr.js.map +1 -0
- package/dist/lib/hardware-id.d.ts +25 -0
- package/dist/lib/hardware-id.d.ts.map +1 -0
- package/dist/lib/hardware-id.js +186 -0
- package/dist/lib/hardware-id.js.map +1 -0
- package/dist/lib/hpc-context.d.ts +35 -0
- package/dist/lib/hpc-context.d.ts.map +1 -0
- package/dist/lib/hpc-context.js +167 -0
- package/dist/lib/hpc-context.js.map +1 -0
- package/dist/lib/prompt-templates.d.ts +195 -0
- package/dist/lib/prompt-templates.d.ts.map +1 -0
- package/dist/lib/prompt-templates.js +353 -0
- package/dist/lib/prompt-templates.js.map +1 -0
- package/dist/lib/providers.d.ts +27 -0
- package/dist/lib/providers.d.ts.map +1 -0
- package/dist/lib/providers.js +372 -0
- package/dist/lib/providers.js.map +1 -0
- package/dist/lib/repo-context.d.ts +18 -0
- package/dist/lib/repo-context.d.ts.map +1 -0
- package/dist/lib/repo-context.js +61 -0
- package/dist/lib/repo-context.js.map +1 -0
- package/dist/lib/repo-utils.d.ts +35 -0
- package/dist/lib/repo-utils.d.ts.map +1 -0
- package/dist/lib/repo-utils.js +222 -0
- package/dist/lib/repo-utils.js.map +1 -0
- package/dist/lib/resources.d.ts +17 -0
- package/dist/lib/resources.d.ts.map +1 -0
- package/dist/lib/resources.js +227 -0
- package/dist/lib/resources.js.map +1 -0
- package/dist/lib/slurm-detect.d.ts +15 -0
- package/dist/lib/slurm-detect.d.ts.map +1 -0
- package/dist/lib/slurm-detect.js +148 -0
- package/dist/lib/slurm-detect.js.map +1 -0
- package/dist/lib/slurm-executor.d.ts +70 -0
- package/dist/lib/slurm-executor.d.ts.map +1 -0
- package/dist/lib/slurm-executor.js +402 -0
- package/dist/lib/slurm-executor.js.map +1 -0
- package/dist/lib/slurm-job-monitor.d.ts +52 -0
- package/dist/lib/slurm-job-monitor.d.ts.map +1 -0
- package/dist/lib/slurm-job-monitor.js +212 -0
- package/dist/lib/slurm-job-monitor.js.map +1 -0
- package/dist/lib/ssh-discovery.d.ts +17 -0
- package/dist/lib/ssh-discovery.d.ts.map +1 -0
- package/dist/lib/ssh-discovery.js +287 -0
- package/dist/lib/ssh-discovery.js.map +1 -0
- package/dist/lib/ssh-installer.d.ts +69 -0
- package/dist/lib/ssh-installer.d.ts.map +1 -0
- package/dist/lib/ssh-installer.js +230 -0
- package/dist/lib/ssh-installer.js.map +1 -0
- package/dist/lib/streaming-prompt.d.ts +48 -0
- package/dist/lib/streaming-prompt.d.ts.map +1 -0
- package/dist/lib/streaming-prompt.js +91 -0
- package/dist/lib/streaming-prompt.js.map +1 -0
- package/dist/lib/task-executor.d.ts +114 -0
- package/dist/lib/task-executor.d.ts.map +1 -0
- package/dist/lib/task-executor.js +753 -0
- package/dist/lib/task-executor.js.map +1 -0
- package/dist/lib/websocket-client.d.ts +200 -0
- package/dist/lib/websocket-client.d.ts.map +1 -0
- package/dist/lib/websocket-client.js +781 -0
- package/dist/lib/websocket-client.js.map +1 -0
- package/dist/lib/workdir-safety.d.ts +63 -0
- package/dist/lib/workdir-safety.d.ts.map +1 -0
- package/dist/lib/workdir-safety.js +247 -0
- package/dist/lib/workdir-safety.js.map +1 -0
- package/dist/lib/worktree-include.d.ts +14 -0
- package/dist/lib/worktree-include.d.ts.map +1 -0
- package/dist/lib/worktree-include.js +68 -0
- package/dist/lib/worktree-include.js.map +1 -0
- package/dist/lib/worktree-setup.d.ts +18 -0
- package/dist/lib/worktree-setup.d.ts.map +1 -0
- package/dist/lib/worktree-setup.js +60 -0
- package/dist/lib/worktree-setup.js.map +1 -0
- package/dist/lib/worktree.d.ts +37 -0
- package/dist/lib/worktree.d.ts.map +1 -0
- package/dist/lib/worktree.js +411 -0
- package/dist/lib/worktree.js.map +1 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +45 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +153 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session-bridge.d.ts +87 -0
- package/dist/mcp/session-bridge.d.ts.map +1 -0
- package/dist/mcp/session-bridge.js +317 -0
- package/dist/mcp/session-bridge.js.map +1 -0
- package/dist/mcp/tools.d.ts +70 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +234 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +197 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +16 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/base-adapter.d.ts +56 -0
- package/dist/providers/base-adapter.d.ts.map +1 -0
- package/dist/providers/base-adapter.js +5 -0
- package/dist/providers/base-adapter.js.map +1 -0
- package/dist/providers/claude-code-adapter.d.ts +27 -0
- package/dist/providers/claude-code-adapter.d.ts.map +1 -0
- package/dist/providers/claude-code-adapter.js +298 -0
- package/dist/providers/claude-code-adapter.js.map +1 -0
- package/dist/providers/claude-sdk-adapter.d.ts +60 -0
- package/dist/providers/claude-sdk-adapter.d.ts.map +1 -0
- package/dist/providers/claude-sdk-adapter.js +632 -0
- package/dist/providers/claude-sdk-adapter.js.map +1 -0
- package/dist/providers/codex-adapter.d.ts +21 -0
- package/dist/providers/codex-adapter.d.ts.map +1 -0
- package/dist/providers/codex-adapter.js +197 -0
- package/dist/providers/codex-adapter.js.map +1 -0
- package/dist/providers/index.d.ts +26 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +58 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/slurm-adapter.d.ts +26 -0
- package/dist/providers/slurm-adapter.d.ts.map +1 -0
- package/dist/providers/slurm-adapter.js +146 -0
- package/dist/providers/slurm-adapter.js.map +1 -0
- package/dist/types.d.ts +592 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task executor with multiplexing support
|
|
3
|
+
*
|
|
4
|
+
* Handles concurrent task execution, routing to providers,
|
|
5
|
+
* and streaming output back over WebSocket
|
|
6
|
+
*/
|
|
7
|
+
import { createProviderAdapter } from '../providers/index.js';
|
|
8
|
+
import { ClaudeSdkAdapter } from '../providers/claude-sdk-adapter.js';
|
|
9
|
+
import { SlurmJobMonitor } from './slurm-job-monitor.js';
|
|
10
|
+
import { createWorktree } from './worktree.js';
|
|
11
|
+
import { pushAndCreatePR } from './git-pr.js';
|
|
12
|
+
import { checkWorkdirSafety, isGitAvailable, createSandbox, WorkdirSafetyTier, } from './workdir-safety.js';
|
|
13
|
+
export class TaskExecutor {
|
|
14
|
+
wsClient;
|
|
15
|
+
runningTasks = new Map();
|
|
16
|
+
taskQueue = [];
|
|
17
|
+
maxConcurrentTasks;
|
|
18
|
+
defaultTimeout;
|
|
19
|
+
adapters = new Map();
|
|
20
|
+
useWorktree;
|
|
21
|
+
worktreeRoot;
|
|
22
|
+
preserveWorktrees;
|
|
23
|
+
jobMonitor;
|
|
24
|
+
allowNonGit;
|
|
25
|
+
useSandbox;
|
|
26
|
+
maxSandboxSize;
|
|
27
|
+
gitAvailable = false;
|
|
28
|
+
// Safety tracking
|
|
29
|
+
tasksByDirectory = new Map(); // workdir -> taskIds
|
|
30
|
+
pendingSafetyChecks = new Map(); // taskId -> pending check
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.wsClient = options.wsClient;
|
|
33
|
+
this.maxConcurrentTasks = options.maxConcurrentTasks ?? 4;
|
|
34
|
+
this.defaultTimeout = options.defaultTimeout ?? 3600000; // 1 hour
|
|
35
|
+
this.useWorktree = options.useWorktree ?? true;
|
|
36
|
+
this.worktreeRoot = options.worktreeRoot;
|
|
37
|
+
this.preserveWorktrees = options.preserveWorktrees ?? false;
|
|
38
|
+
this.allowNonGit = options.allowNonGit ?? false;
|
|
39
|
+
this.useSandbox = options.useSandbox ?? false;
|
|
40
|
+
this.maxSandboxSize = options.maxSandboxSize ?? 100 * 1024 * 1024; // 100MB
|
|
41
|
+
this.jobMonitor = new SlurmJobMonitor(options.wsClient);
|
|
42
|
+
// Check git availability on startup
|
|
43
|
+
isGitAvailable().then((available) => {
|
|
44
|
+
this.gitAvailable = available;
|
|
45
|
+
console.log(`[executor] Git ${available ? 'available' : 'not available'}`);
|
|
46
|
+
}).catch(() => {
|
|
47
|
+
this.gitAvailable = false;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Submit a task for execution (with safety checks)
|
|
52
|
+
*/
|
|
53
|
+
async submitTask(task) {
|
|
54
|
+
const normalizedTask = {
|
|
55
|
+
...task,
|
|
56
|
+
workingDirectory: resolveWorkingDirectory(task.workingDirectory),
|
|
57
|
+
};
|
|
58
|
+
// Perform safety check
|
|
59
|
+
const safetyCheck = await this.performSafetyCheck(normalizedTask);
|
|
60
|
+
// Handle safety tiers
|
|
61
|
+
if (safetyCheck.tier === WorkdirSafetyTier.UNSAFE) {
|
|
62
|
+
// BLOCK: unsafe conditions
|
|
63
|
+
this.wsClient.sendTaskResult({
|
|
64
|
+
taskId: normalizedTask.id,
|
|
65
|
+
status: 'failed',
|
|
66
|
+
error: safetyCheck.blockReason,
|
|
67
|
+
completedAt: new Date().toISOString(),
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (safetyCheck.tier === WorkdirSafetyTier.RISKY && !this.allowNonGit) {
|
|
72
|
+
// PROMPT: risky conditions require user decision
|
|
73
|
+
await this.requestSafetyDecision(normalizedTask, safetyCheck);
|
|
74
|
+
// Execution will continue when decision is received
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (safetyCheck.tier === WorkdirSafetyTier.GUARDED) {
|
|
78
|
+
// WARN: inform user but continue
|
|
79
|
+
this.wsClient.sendTaskStatus(normalizedTask.id, 'queued', 0, safetyCheck.warning);
|
|
80
|
+
}
|
|
81
|
+
// Track task by directory
|
|
82
|
+
this.trackTaskDirectory(normalizedTask);
|
|
83
|
+
// Check if we can run immediately
|
|
84
|
+
if (this.runningTasks.size < this.maxConcurrentTasks) {
|
|
85
|
+
await this.executeTask(normalizedTask, this.useSandbox);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Queue the task
|
|
89
|
+
this.taskQueue.push(normalizedTask);
|
|
90
|
+
this.wsClient.sendTaskStatus(normalizedTask.id, 'queued', 0, 'Waiting for available slot');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handle safety decision from user (via server)
|
|
95
|
+
*/
|
|
96
|
+
async handleSafetyDecision(taskId, decision) {
|
|
97
|
+
const pending = this.pendingSafetyChecks.get(taskId);
|
|
98
|
+
if (!pending) {
|
|
99
|
+
console.warn(`[executor] Safety decision for ${taskId} but no pending check`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.pendingSafetyChecks.delete(taskId);
|
|
103
|
+
pending.resolveDecision(decision);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Cancel a running or queued task
|
|
107
|
+
*/
|
|
108
|
+
cancelTask(taskId) {
|
|
109
|
+
// Check running tasks
|
|
110
|
+
const running = this.runningTasks.get(taskId);
|
|
111
|
+
if (running) {
|
|
112
|
+
running.abortController.abort();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// Check queue
|
|
116
|
+
const queueIndex = this.taskQueue.findIndex((t) => t.id === taskId);
|
|
117
|
+
if (queueIndex >= 0) {
|
|
118
|
+
this.taskQueue.splice(queueIndex, 1);
|
|
119
|
+
this.wsClient.sendTaskResult({
|
|
120
|
+
taskId,
|
|
121
|
+
status: 'cancelled',
|
|
122
|
+
completedAt: new Date().toISOString(),
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get current task counts
|
|
130
|
+
*/
|
|
131
|
+
getTaskCounts() {
|
|
132
|
+
return {
|
|
133
|
+
running: this.runningTasks.size,
|
|
134
|
+
queued: this.taskQueue.length,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Cancel all tasks and clear queue
|
|
139
|
+
*/
|
|
140
|
+
cancelAll() {
|
|
141
|
+
// Cancel all running tasks
|
|
142
|
+
for (const [taskId, running] of this.runningTasks) {
|
|
143
|
+
running.abortController.abort();
|
|
144
|
+
this.wsClient.sendTaskResult({
|
|
145
|
+
taskId,
|
|
146
|
+
status: 'cancelled',
|
|
147
|
+
completedAt: new Date().toISOString(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
this.runningTasks.clear();
|
|
151
|
+
// Clear queue
|
|
152
|
+
for (const task of this.taskQueue) {
|
|
153
|
+
this.wsClient.sendTaskResult({
|
|
154
|
+
taskId: task.id,
|
|
155
|
+
status: 'cancelled',
|
|
156
|
+
completedAt: new Date().toISOString(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
this.taskQueue = [];
|
|
160
|
+
// Stop job monitor
|
|
161
|
+
this.jobMonitor.stop();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Update max concurrent tasks
|
|
165
|
+
*/
|
|
166
|
+
setMaxConcurrentTasks(max) {
|
|
167
|
+
this.maxConcurrentTasks = max;
|
|
168
|
+
// Process queue in case we can now run more tasks
|
|
169
|
+
this.processQueue();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Steer a running task by injecting a message into the agent session.
|
|
173
|
+
* When interrupt=true, interrupts the current turn before injecting.
|
|
174
|
+
*
|
|
175
|
+
* For completed tasks with a preserved session, this triggers a full
|
|
176
|
+
* resume via the SDK's `resume` option for post-completion follow-up.
|
|
177
|
+
*/
|
|
178
|
+
async steerTask(taskId, message, interrupt = false) {
|
|
179
|
+
const running = this.runningTasks.get(taskId);
|
|
180
|
+
if (running) {
|
|
181
|
+
// Task is still running — inject message into the live session
|
|
182
|
+
if (running.adapter instanceof ClaudeSdkAdapter && typeof running.adapter.injectMessage === 'function') {
|
|
183
|
+
const injected = await running.adapter.injectMessage(taskId, message, interrupt);
|
|
184
|
+
if (injected) {
|
|
185
|
+
return { accepted: true };
|
|
186
|
+
}
|
|
187
|
+
return { accepted: false, reason: 'Failed to inject message into running session' };
|
|
188
|
+
}
|
|
189
|
+
return { accepted: false, reason: 'Provider does not support mid-execution steering' };
|
|
190
|
+
}
|
|
191
|
+
// Task is not running — check if we have a preserved session for resume
|
|
192
|
+
const adapter = this.findAdapterWithSession(taskId);
|
|
193
|
+
if (adapter) {
|
|
194
|
+
const context = adapter.getTaskContext(taskId);
|
|
195
|
+
if (context) {
|
|
196
|
+
// Launch a resume as a new "task" execution
|
|
197
|
+
this.resumeCompletedTask(taskId, message, adapter, context);
|
|
198
|
+
return { accepted: true };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { accepted: false, reason: 'Task not found or session expired' };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Find the ClaudeSdkAdapter that has a preserved session for the given task.
|
|
205
|
+
*/
|
|
206
|
+
findAdapterWithSession(taskId) {
|
|
207
|
+
for (const adapter of this.adapters.values()) {
|
|
208
|
+
if (adapter instanceof ClaudeSdkAdapter) {
|
|
209
|
+
const context = adapter.getTaskContext(taskId);
|
|
210
|
+
if (context)
|
|
211
|
+
return adapter;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resume a completed task session for post-completion steering.
|
|
218
|
+
* Runs in the background, streaming output back through the WebSocket.
|
|
219
|
+
*/
|
|
220
|
+
async resumeCompletedTask(taskId, message, adapter, context) {
|
|
221
|
+
const abortController = new AbortController();
|
|
222
|
+
let textSequence = 0;
|
|
223
|
+
const stream = {
|
|
224
|
+
stdout: (data) => {
|
|
225
|
+
this.wsClient.sendTaskOutput(taskId, 'stdout', data, 0);
|
|
226
|
+
},
|
|
227
|
+
stderr: (data) => {
|
|
228
|
+
this.wsClient.sendTaskOutput(taskId, 'stderr', data, 0);
|
|
229
|
+
},
|
|
230
|
+
status: (status, progress, statusMessage) => {
|
|
231
|
+
this.wsClient.sendTaskStatus(taskId, status, progress, statusMessage);
|
|
232
|
+
},
|
|
233
|
+
toolTrace: (toolName, toolInput, toolResult, success) => {
|
|
234
|
+
this.wsClient.sendToolTrace(taskId, toolName, toolInput, toolResult, success);
|
|
235
|
+
},
|
|
236
|
+
text: (data) => {
|
|
237
|
+
this.wsClient.sendTaskText(taskId, data, textSequence++);
|
|
238
|
+
},
|
|
239
|
+
toolUse: (toolName, toolInput) => {
|
|
240
|
+
this.wsClient.sendTaskToolUse(taskId, toolName, toolInput);
|
|
241
|
+
},
|
|
242
|
+
toolResult: (toolName, result, success) => {
|
|
243
|
+
this.wsClient.sendTaskToolResult(taskId, toolName, result, success);
|
|
244
|
+
},
|
|
245
|
+
fileChange: (path, action, linesAdded, linesRemoved, diff) => {
|
|
246
|
+
this.wsClient.sendTaskFileChange(taskId, path, action, linesAdded, linesRemoved, diff);
|
|
247
|
+
},
|
|
248
|
+
sessionInit: (sessionId, model) => {
|
|
249
|
+
this.wsClient.sendTaskSessionInit(taskId, sessionId, model);
|
|
250
|
+
},
|
|
251
|
+
approvalRequest: async (question, options) => {
|
|
252
|
+
return this.wsClient.sendApprovalRequest(taskId, question, options);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
this.wsClient.sendTaskStatus(taskId, 'running', 0, 'Resuming session...');
|
|
257
|
+
const result = await adapter.resumeTask(taskId, message, context.workingDirectory ?? process.cwd(), context.sessionId, stream, abortController.signal);
|
|
258
|
+
this.wsClient.sendTaskResult({
|
|
259
|
+
taskId,
|
|
260
|
+
status: result.success ? 'completed' : 'failed',
|
|
261
|
+
output: result.output,
|
|
262
|
+
error: result.error,
|
|
263
|
+
completedAt: new Date().toISOString(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
268
|
+
this.wsClient.sendTaskResult({
|
|
269
|
+
taskId,
|
|
270
|
+
status: 'failed',
|
|
271
|
+
error: `Resume failed: ${errorMsg}`,
|
|
272
|
+
completedAt: new Date().toISOString(),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Private Methods
|
|
278
|
+
// ============================================================================
|
|
279
|
+
/**
|
|
280
|
+
* Perform safety check on working directory
|
|
281
|
+
*/
|
|
282
|
+
async performSafetyCheck(task) {
|
|
283
|
+
const activeTasksInDir = this.getActiveTasksInDirectory(task.workingDirectory);
|
|
284
|
+
return await checkWorkdirSafety(task.workingDirectory, activeTasksInDir, this.gitAvailable);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Request safety decision from user
|
|
288
|
+
*/
|
|
289
|
+
async requestSafetyDecision(task, safetyCheck) {
|
|
290
|
+
// Send safety prompt to server (which forwards to UI)
|
|
291
|
+
const options = [
|
|
292
|
+
{ id: 'proceed', label: 'Continue anyway', description: 'Execute in non-git directory at your own risk' },
|
|
293
|
+
{ id: 'init-git', label: 'Initialize git first', description: 'Create git repository before execution' },
|
|
294
|
+
{ id: 'sandbox', label: 'Use sandbox mode', description: 'Work on a copy, review changes before applying' },
|
|
295
|
+
{ id: 'cancel', label: 'Cancel task', description: 'Do not execute this task' },
|
|
296
|
+
];
|
|
297
|
+
// Create promise that will be resolved when decision arrives, with a 5-minute timeout
|
|
298
|
+
const SAFETY_DECISION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
299
|
+
const decisionPromise = new Promise((resolve) => {
|
|
300
|
+
this.pendingSafetyChecks.set(task.id, {
|
|
301
|
+
task,
|
|
302
|
+
safetyResult: safetyCheck,
|
|
303
|
+
resolveDecision: resolve,
|
|
304
|
+
});
|
|
305
|
+
// Auto-cancel if no decision within timeout
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
if (this.pendingSafetyChecks.has(task.id)) {
|
|
308
|
+
console.warn(`[executor] Safety decision for task ${task.id} timed out after ${SAFETY_DECISION_TIMEOUT_MS / 1000}s, auto-cancelling`);
|
|
309
|
+
this.pendingSafetyChecks.delete(task.id);
|
|
310
|
+
resolve('cancel');
|
|
311
|
+
}
|
|
312
|
+
}, SAFETY_DECISION_TIMEOUT_MS).unref();
|
|
313
|
+
});
|
|
314
|
+
// Send prompt
|
|
315
|
+
this.wsClient.sendSafetyPrompt(task.id, safetyCheck.tier, safetyCheck.warning, options);
|
|
316
|
+
// Wait for decision
|
|
317
|
+
const decision = await decisionPromise;
|
|
318
|
+
// Handle decision
|
|
319
|
+
switch (decision) {
|
|
320
|
+
case 'proceed':
|
|
321
|
+
// Mark as allowed and continue
|
|
322
|
+
this.trackTaskDirectory(task);
|
|
323
|
+
if (this.runningTasks.size < this.maxConcurrentTasks) {
|
|
324
|
+
await this.executeTask(task, false);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
this.taskQueue.push(task);
|
|
328
|
+
this.wsClient.sendTaskStatus(task.id, 'queued', 0, 'Waiting for available slot');
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
case 'sandbox':
|
|
332
|
+
// Execute in sandbox mode
|
|
333
|
+
this.trackTaskDirectory(task);
|
|
334
|
+
if (this.runningTasks.size < this.maxConcurrentTasks) {
|
|
335
|
+
await this.executeTask(task, true); // Force sandbox mode
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.taskQueue.push(task);
|
|
339
|
+
this.wsClient.sendTaskStatus(task.id, 'queued', 0, 'Waiting for available slot (sandbox)');
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
case 'init-git':
|
|
343
|
+
// Initialize git and retry
|
|
344
|
+
try {
|
|
345
|
+
await this.initializeGit(task.workingDirectory);
|
|
346
|
+
this.wsClient.sendTaskStatus(task.id, 'queued', 0, 'Git initialized, proceeding...');
|
|
347
|
+
this.trackTaskDirectory(task);
|
|
348
|
+
if (this.runningTasks.size < this.maxConcurrentTasks) {
|
|
349
|
+
await this.executeTask(task, false);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
this.taskQueue.push(task);
|
|
353
|
+
this.wsClient.sendTaskStatus(task.id, 'queued', 0, 'Waiting for available slot');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
this.wsClient.sendTaskResult({
|
|
358
|
+
taskId: task.id,
|
|
359
|
+
status: 'failed',
|
|
360
|
+
error: `Failed to initialize git: ${error instanceof Error ? error.message : String(error)}`,
|
|
361
|
+
completedAt: new Date().toISOString(),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
case 'cancel':
|
|
366
|
+
// Cancel the task
|
|
367
|
+
this.wsClient.sendTaskResult({
|
|
368
|
+
taskId: task.id,
|
|
369
|
+
status: 'cancelled',
|
|
370
|
+
completedAt: new Date().toISOString(),
|
|
371
|
+
});
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Initialize git repository in a directory
|
|
377
|
+
*/
|
|
378
|
+
async initializeGit(workdir) {
|
|
379
|
+
const { execFile } = await import('node:child_process');
|
|
380
|
+
const { promisify } = await import('node:util');
|
|
381
|
+
const execFileAsync = promisify(execFile);
|
|
382
|
+
await execFileAsync('git', ['init'], { cwd: workdir, timeout: 10_000 });
|
|
383
|
+
await execFileAsync('git', ['add', '.'], { cwd: workdir, timeout: 10_000 });
|
|
384
|
+
await execFileAsync('git', ['commit', '-m', 'Initial commit'], { cwd: workdir, timeout: 10_000 });
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Track task by directory for parallel execution safety
|
|
388
|
+
*/
|
|
389
|
+
trackTaskDirectory(task) {
|
|
390
|
+
const tasks = this.tasksByDirectory.get(task.workingDirectory) || new Set();
|
|
391
|
+
tasks.add(task.id);
|
|
392
|
+
this.tasksByDirectory.set(task.workingDirectory, tasks);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Untrack task from directory
|
|
396
|
+
*/
|
|
397
|
+
untrackTaskDirectory(task) {
|
|
398
|
+
const tasks = this.tasksByDirectory.get(task.workingDirectory);
|
|
399
|
+
if (tasks) {
|
|
400
|
+
tasks.delete(task.id);
|
|
401
|
+
if (tasks.size === 0) {
|
|
402
|
+
this.tasksByDirectory.delete(task.workingDirectory);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get count of active tasks in a directory
|
|
408
|
+
*/
|
|
409
|
+
getActiveTasksInDirectory(workdir) {
|
|
410
|
+
const tasks = this.tasksByDirectory.get(workdir);
|
|
411
|
+
return tasks ? tasks.size : 0;
|
|
412
|
+
}
|
|
413
|
+
async executeTask(task, useSandbox = false) {
|
|
414
|
+
console.log(`[executor] Task ${task.id}: workingDirectory=${task.workingDirectory} sandbox=${useSandbox}`);
|
|
415
|
+
// Setup sandbox if requested
|
|
416
|
+
let sandbox;
|
|
417
|
+
let workdir = task.workingDirectory;
|
|
418
|
+
if (useSandbox) {
|
|
419
|
+
try {
|
|
420
|
+
this.wsClient.sendTaskStatus(task.id, 'running', 0, 'Creating sandbox...');
|
|
421
|
+
sandbox = await createSandbox({
|
|
422
|
+
workdir: task.workingDirectory,
|
|
423
|
+
taskId: task.id,
|
|
424
|
+
maxSize: this.maxSandboxSize,
|
|
425
|
+
});
|
|
426
|
+
workdir = sandbox.sandboxPath;
|
|
427
|
+
console.log(`[executor] Task ${task.id}: sandbox created at ${sandbox.sandboxPath}`);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
431
|
+
console.error(`[executor] Task ${task.id}: sandbox creation failed: ${errorMsg}`);
|
|
432
|
+
this.wsClient.sendTaskResult({
|
|
433
|
+
taskId: task.id,
|
|
434
|
+
status: 'failed',
|
|
435
|
+
error: `Sandbox creation failed: ${errorMsg}`,
|
|
436
|
+
completedAt: new Date().toISOString(),
|
|
437
|
+
});
|
|
438
|
+
this.untrackTaskDirectory(task);
|
|
439
|
+
this.processQueue();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const normalizedTask = { ...task, workingDirectory: workdir };
|
|
444
|
+
// Get or create adapter for this provider
|
|
445
|
+
const adapter = await this.getAdapter(normalizedTask.provider);
|
|
446
|
+
if (!adapter) {
|
|
447
|
+
console.error(`[executor] Task ${task.id}: provider ${normalizedTask.provider} not available`);
|
|
448
|
+
this.wsClient.sendTaskResult({
|
|
449
|
+
taskId: normalizedTask.id,
|
|
450
|
+
status: 'failed',
|
|
451
|
+
error: `Provider ${normalizedTask.provider} not available`,
|
|
452
|
+
completedAt: new Date().toISOString(),
|
|
453
|
+
});
|
|
454
|
+
if (sandbox)
|
|
455
|
+
await sandbox.cleanup();
|
|
456
|
+
this.untrackTaskDirectory(task);
|
|
457
|
+
this.processQueue();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
console.log(`[executor] Task ${task.id}: adapter ${adapter.name} (${adapter.type}) ready`);
|
|
461
|
+
const abortController = new AbortController();
|
|
462
|
+
const outputSequence = { stdout: 0, stderr: 0 };
|
|
463
|
+
// Create stream handlers before workspace prep so setup output is captured
|
|
464
|
+
let textSequence = 0;
|
|
465
|
+
const stream = {
|
|
466
|
+
stdout: (data) => {
|
|
467
|
+
this.wsClient.sendTaskOutput(normalizedTask.id, 'stdout', data, outputSequence.stdout++);
|
|
468
|
+
},
|
|
469
|
+
stderr: (data) => {
|
|
470
|
+
this.wsClient.sendTaskOutput(normalizedTask.id, 'stderr', data, outputSequence.stderr++);
|
|
471
|
+
},
|
|
472
|
+
status: (status, progress, message) => {
|
|
473
|
+
this.wsClient.sendTaskStatus(normalizedTask.id, status, progress, message);
|
|
474
|
+
},
|
|
475
|
+
toolTrace: (toolName, toolInput, toolResult, success) => {
|
|
476
|
+
this.wsClient.sendToolTrace(normalizedTask.id, toolName, toolInput, toolResult, success);
|
|
477
|
+
},
|
|
478
|
+
text: (data) => {
|
|
479
|
+
this.wsClient.sendTaskText(normalizedTask.id, data, textSequence++);
|
|
480
|
+
},
|
|
481
|
+
toolUse: (toolName, toolInput) => {
|
|
482
|
+
this.wsClient.sendTaskToolUse(normalizedTask.id, toolName, toolInput);
|
|
483
|
+
},
|
|
484
|
+
toolResult: (toolName, result, success) => {
|
|
485
|
+
this.wsClient.sendTaskToolResult(normalizedTask.id, toolName, result, success);
|
|
486
|
+
},
|
|
487
|
+
fileChange: (path, action, linesAdded, linesRemoved, diff) => {
|
|
488
|
+
this.wsClient.sendTaskFileChange(normalizedTask.id, path, action, linesAdded, linesRemoved, diff);
|
|
489
|
+
},
|
|
490
|
+
sessionInit: (sessionId, model) => {
|
|
491
|
+
this.wsClient.sendTaskSessionInit(normalizedTask.id, sessionId, model);
|
|
492
|
+
},
|
|
493
|
+
approvalRequest: async (question, options) => {
|
|
494
|
+
return this.wsClient.sendApprovalRequest(normalizedTask.id, question, options);
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
const runningTask = {
|
|
498
|
+
task: normalizedTask,
|
|
499
|
+
abortController,
|
|
500
|
+
adapter,
|
|
501
|
+
outputSequence,
|
|
502
|
+
sandbox,
|
|
503
|
+
};
|
|
504
|
+
this.runningTasks.set(normalizedTask.id, runningTask);
|
|
505
|
+
this.wsClient.addActiveTask(normalizedTask.id);
|
|
506
|
+
const prepared = await this.prepareTaskWorkspace(normalizedTask, stream);
|
|
507
|
+
const taskWithWorkspace = { ...normalizedTask, workingDirectory: prepared.workingDirectory };
|
|
508
|
+
runningTask.task = taskWithWorkspace;
|
|
509
|
+
console.log(`[executor] Task ${task.id}: workspace prepared, cwd=${prepared.workingDirectory}`);
|
|
510
|
+
// Execute with timeout
|
|
511
|
+
const timeout = task.timeout ?? this.defaultTimeout;
|
|
512
|
+
const timeoutId = setTimeout(() => {
|
|
513
|
+
abortController.abort();
|
|
514
|
+
}, timeout);
|
|
515
|
+
let keepBranch = false;
|
|
516
|
+
try {
|
|
517
|
+
// Notify task started
|
|
518
|
+
this.wsClient.sendTaskStatus(task.id, 'running', 0, 'Starting');
|
|
519
|
+
console.log(`[executor] Task ${task.id}: executing with ${adapter.name}...`);
|
|
520
|
+
// Execute the task
|
|
521
|
+
const result = await adapter.execute(taskWithWorkspace, stream, abortController.signal);
|
|
522
|
+
// Delivery-mode-aware result handling
|
|
523
|
+
const deliveryMode = task.deliveryMode ?? 'pr';
|
|
524
|
+
if (prepared.branchName && (result.status === 'completed' || !result.error)) {
|
|
525
|
+
try {
|
|
526
|
+
if (deliveryMode === 'direct') {
|
|
527
|
+
// No git delivery — files modified in-place
|
|
528
|
+
console.log(`[executor] Task ${task.id}: direct mode, skipping git delivery`);
|
|
529
|
+
}
|
|
530
|
+
else if (deliveryMode === 'copy') {
|
|
531
|
+
// Copy mode: worktree preserved, no git operations
|
|
532
|
+
console.log(`[executor] Task ${task.id}: copy mode, worktree preserved at ${prepared.workingDirectory}`);
|
|
533
|
+
}
|
|
534
|
+
else if (deliveryMode === 'branch') {
|
|
535
|
+
// Auto-commit but don't push — branch stays local
|
|
536
|
+
console.log(`[executor] Task ${task.id}: branch mode, committing locally`);
|
|
537
|
+
result.branchName = prepared.branchName;
|
|
538
|
+
keepBranch = true;
|
|
539
|
+
}
|
|
540
|
+
else if (deliveryMode === 'push') {
|
|
541
|
+
// Push branch to remote, but don't create a PR — user creates PR manually
|
|
542
|
+
console.log(`[executor] Task ${task.id}: push mode, pushing branch ${prepared.branchName}`);
|
|
543
|
+
const prResult = await pushAndCreatePR(prepared.workingDirectory, {
|
|
544
|
+
branchName: prepared.branchName,
|
|
545
|
+
taskTitle: task.prompt.slice(0, 100),
|
|
546
|
+
taskDescription: task.prompt.slice(0, 500),
|
|
547
|
+
skipPR: true,
|
|
548
|
+
});
|
|
549
|
+
result.branchName = prResult.branchName;
|
|
550
|
+
if (prResult.pushed) {
|
|
551
|
+
keepBranch = true;
|
|
552
|
+
console.log(`[executor] Task ${task.id}: branch pushed (${prepared.branchName})`);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
console.log(`[executor] Task ${task.id}: no changes to push`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
// 'pr' — push + create PR (existing behavior)
|
|
560
|
+
console.log(`[executor] Task ${task.id}: pr mode, attempting PR creation for branch ${prepared.branchName}`);
|
|
561
|
+
const prResult = await pushAndCreatePR(prepared.workingDirectory, {
|
|
562
|
+
branchName: prepared.branchName,
|
|
563
|
+
taskTitle: task.prompt.slice(0, 100),
|
|
564
|
+
taskDescription: task.prompt.slice(0, 500),
|
|
565
|
+
});
|
|
566
|
+
result.branchName = prResult.branchName;
|
|
567
|
+
if (prResult.prUrl) {
|
|
568
|
+
result.prUrl = prResult.prUrl;
|
|
569
|
+
result.prNumber = prResult.prNumber;
|
|
570
|
+
keepBranch = true;
|
|
571
|
+
console.log(`[executor] Task ${task.id}: PR created at ${prResult.prUrl}`);
|
|
572
|
+
}
|
|
573
|
+
else if (prResult.pushed) {
|
|
574
|
+
keepBranch = true;
|
|
575
|
+
console.log(`[executor] Task ${task.id}: branch pushed, no PR created (gh not available?)`);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
console.log(`[executor] Task ${task.id}: no changes to push`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (prError) {
|
|
583
|
+
const prMsg = prError instanceof Error ? prError.message : String(prError);
|
|
584
|
+
console.warn(`[executor] Task ${task.id}: delivery (${deliveryMode}) failed: ${prMsg}`);
|
|
585
|
+
// Non-fatal: still report the task result without PR info
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
console.log(`[executor] Task ${task.id}: completed with status=${result.status}${result.error ? ` error=${result.error}` : ''}`);
|
|
589
|
+
// Check if there are tracked Slurm jobs still running for this task.
|
|
590
|
+
// If so, don't send the final result yet — let the job monitor handle it.
|
|
591
|
+
const pendingJobs = this.jobMonitor.getJobsForExecution(task.id);
|
|
592
|
+
if (pendingJobs.length > 0) {
|
|
593
|
+
console.log(`[executor] Task ${task.id}: ${pendingJobs.length} Slurm job(s) still tracked, deferring completion`);
|
|
594
|
+
this.wsClient.sendTaskStatus(task.id, 'running', 80, `Waiting for ${pendingJobs.length} Slurm job(s): ${pendingJobs.join(', ')}`);
|
|
595
|
+
// Don't send final result — the SlurmJobMonitor will send it when jobs finish
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
// Send final result
|
|
599
|
+
this.wsClient.sendTaskResult(result);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
// Unexpected error during execution
|
|
604
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
605
|
+
console.error(`[executor] Task ${task.id}: execution error: ${errorMsg}`);
|
|
606
|
+
this.wsClient.sendTaskResult({
|
|
607
|
+
taskId: task.id,
|
|
608
|
+
status: 'failed',
|
|
609
|
+
error: errorMsg,
|
|
610
|
+
completedAt: new Date().toISOString(),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
clearTimeout(timeoutId);
|
|
615
|
+
// Cleanup worktree — but preserve it if a branch was pushed/PR created,
|
|
616
|
+
// so the worktree remains available for post-execution steering or inspection.
|
|
617
|
+
// The server will trigger explicit cleanup when the task is fully completed.
|
|
618
|
+
if (this.preserveWorktrees || keepBranch) {
|
|
619
|
+
console.log(`[executor] Task ${task.id}: worktree preserved (${this.preserveWorktrees ? 'debug mode' : 'branch pushed'})`);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
await prepared.cleanup({ keepBranch: false });
|
|
623
|
+
}
|
|
624
|
+
// Sandbox: copy back results then cleanup
|
|
625
|
+
if (sandbox) {
|
|
626
|
+
try {
|
|
627
|
+
console.log(`[executor] Task ${task.id}: copying back sandbox results`);
|
|
628
|
+
await sandbox.copyBack();
|
|
629
|
+
}
|
|
630
|
+
catch (copyBackErr) {
|
|
631
|
+
const msg = copyBackErr instanceof Error ? copyBackErr.message : String(copyBackErr);
|
|
632
|
+
console.error(`[executor] Task ${task.id}: sandbox copyBack failed: ${msg}`);
|
|
633
|
+
}
|
|
634
|
+
console.log(`[executor] Task ${task.id}: cleaning up sandbox`);
|
|
635
|
+
await sandbox.cleanup();
|
|
636
|
+
}
|
|
637
|
+
// Untrack task from directory
|
|
638
|
+
this.untrackTaskDirectory(task);
|
|
639
|
+
this.runningTasks.delete(task.id);
|
|
640
|
+
this.wsClient.removeActiveTask(task.id);
|
|
641
|
+
this.processQueue();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async prepareTaskWorkspace(task, stream) {
|
|
645
|
+
if (!this.useWorktree) {
|
|
646
|
+
console.warn(`[executor] Task ${task.id}: worktree disabled, using raw workdir: ${task.workingDirectory}`);
|
|
647
|
+
return { workingDirectory: task.workingDirectory, cleanup: async () => { } };
|
|
648
|
+
}
|
|
649
|
+
// Direct delivery mode: skip worktree, work in-place
|
|
650
|
+
if (task.deliveryMode === 'direct') {
|
|
651
|
+
console.log(`[executor] Task ${task.id}: direct delivery mode, using raw workdir: ${task.workingDirectory}`);
|
|
652
|
+
return { workingDirectory: task.workingDirectory, cleanup: async () => { } };
|
|
653
|
+
}
|
|
654
|
+
// Copy delivery mode: copy project to worktree dir (non-git)
|
|
655
|
+
if (task.deliveryMode === 'copy') {
|
|
656
|
+
try {
|
|
657
|
+
const agentDirName = task.agentDir ?? '.astro';
|
|
658
|
+
if (task.worktreeStrategy === 'reference') {
|
|
659
|
+
const { createReferenceWorktree } = await import('./copy-worktree.js');
|
|
660
|
+
const ref = await createReferenceWorktree(task.workingDirectory, agentDirName, task.id);
|
|
661
|
+
console.log(`[executor] Task ${task.id}: reference worktree at ${ref.worktreePath}`);
|
|
662
|
+
return { workingDirectory: ref.worktreePath, cleanup: async () => { await ref.cleanup(); } };
|
|
663
|
+
}
|
|
664
|
+
const { createCopyWorktree } = await import('./copy-worktree.js');
|
|
665
|
+
const copy = await createCopyWorktree(task.workingDirectory, agentDirName, task.id);
|
|
666
|
+
console.log(`[executor] Task ${task.id}: copy worktree at ${copy.worktreePath}`);
|
|
667
|
+
return { workingDirectory: copy.worktreePath, cleanup: async () => { await copy.cleanup(); } };
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
671
|
+
console.error(`[executor] Task ${task.id}: copy worktree failed: ${errorMsg}, using raw workdir`);
|
|
672
|
+
return { workingDirectory: task.workingDirectory, cleanup: async () => { } };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
console.log(`[executor] Task ${task.id}: creating worktree for workdir=${task.workingDirectory}`);
|
|
677
|
+
const worktree = await createWorktree({
|
|
678
|
+
workingDirectory: task.workingDirectory,
|
|
679
|
+
taskId: task.id,
|
|
680
|
+
rootOverride: this.worktreeRoot,
|
|
681
|
+
projectId: task.projectId,
|
|
682
|
+
nodeId: task.planNodeId,
|
|
683
|
+
agentDir: task.agentDir,
|
|
684
|
+
stdout: stream.stdout,
|
|
685
|
+
stderr: stream.stderr,
|
|
686
|
+
});
|
|
687
|
+
if (!worktree) {
|
|
688
|
+
console.warn(`[executor] Task ${task.id}: createWorktree returned null, falling back to raw workdir: ${task.workingDirectory}`);
|
|
689
|
+
return { workingDirectory: task.workingDirectory, cleanup: async () => { } };
|
|
690
|
+
}
|
|
691
|
+
console.log(`[executor] Task ${task.id}: worktree created at ${worktree.workingDirectory} (branch: ${worktree.branchName})`);
|
|
692
|
+
return worktree;
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
696
|
+
console.error(`[executor] Task ${task.id}: worktree creation FAILED: ${errorMsg} — falling back to raw workdir: ${task.workingDirectory}`);
|
|
697
|
+
this.wsClient.sendTaskStatus(task.id, 'running', 0, `Worktree setup failed: ${errorMsg}`);
|
|
698
|
+
return { workingDirectory: task.workingDirectory, cleanup: async () => { } };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async getAdapter(type) {
|
|
702
|
+
// Check cache
|
|
703
|
+
const cached = this.adapters.get(type);
|
|
704
|
+
if (cached) {
|
|
705
|
+
return cached;
|
|
706
|
+
}
|
|
707
|
+
// Create new adapter
|
|
708
|
+
const adapter = createProviderAdapter(type);
|
|
709
|
+
if (!adapter) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
// Wire up job monitor for HPC-aware adapters
|
|
713
|
+
if (adapter instanceof ClaudeSdkAdapter) {
|
|
714
|
+
adapter.setJobMonitor(this.jobMonitor);
|
|
715
|
+
}
|
|
716
|
+
// Check availability
|
|
717
|
+
const available = await adapter.isAvailable();
|
|
718
|
+
if (!available) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
// Cache and return
|
|
722
|
+
this.adapters.set(type, adapter);
|
|
723
|
+
return adapter;
|
|
724
|
+
}
|
|
725
|
+
processQueue() {
|
|
726
|
+
while (this.runningTasks.size < this.maxConcurrentTasks && this.taskQueue.length > 0) {
|
|
727
|
+
const task = this.taskQueue.shift();
|
|
728
|
+
if (task) {
|
|
729
|
+
this.executeTask(task).catch((err) => {
|
|
730
|
+
console.error(`[executor] Queued task ${task.id} failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Resolve a working directory value.
|
|
738
|
+
* - Empty/missing → error (must be explicitly set to prevent operating on the agent runner's own repo)
|
|
739
|
+
* - Git URL → error (repo setup should have resolved this to a local path)
|
|
740
|
+
* - Otherwise → return as-is
|
|
741
|
+
*/
|
|
742
|
+
function resolveWorkingDirectory(value) {
|
|
743
|
+
if (!value) {
|
|
744
|
+
throw new Error('workingDirectory is required but was not provided. ' +
|
|
745
|
+
'Configure a project directory or repository before dispatching tasks.');
|
|
746
|
+
}
|
|
747
|
+
const isGitUrl = value.startsWith('http://') || value.startsWith('https://') || value.startsWith('git@');
|
|
748
|
+
if (isGitUrl) {
|
|
749
|
+
throw new Error(`workingDirectory is still a git URL at dispatch time. Run repo setup first.`);
|
|
750
|
+
}
|
|
751
|
+
return value;
|
|
752
|
+
}
|
|
753
|
+
//# sourceMappingURL=task-executor.js.map
|