@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.
Files changed (203) hide show
  1. package/LICENSE +76 -0
  2. package/README.md +178 -0
  3. package/dist/cli.d.ts +15 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +401 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/index.d.ts +9 -0
  8. package/dist/commands/index.d.ts.map +1 -0
  9. package/dist/commands/index.js +9 -0
  10. package/dist/commands/index.js.map +1 -0
  11. package/dist/commands/mcp.d.ts +16 -0
  12. package/dist/commands/mcp.d.ts.map +1 -0
  13. package/dist/commands/mcp.js +19 -0
  14. package/dist/commands/mcp.js.map +1 -0
  15. package/dist/commands/setup.d.ts +20 -0
  16. package/dist/commands/setup.d.ts.map +1 -0
  17. package/dist/commands/setup.js +585 -0
  18. package/dist/commands/setup.js.map +1 -0
  19. package/dist/commands/start.d.ts +16 -0
  20. package/dist/commands/start.d.ts.map +1 -0
  21. package/dist/commands/start.js +638 -0
  22. package/dist/commands/start.js.map +1 -0
  23. package/dist/commands/status.d.ts +5 -0
  24. package/dist/commands/status.d.ts.map +1 -0
  25. package/dist/commands/status.js +63 -0
  26. package/dist/commands/status.js.map +1 -0
  27. package/dist/commands/stop.d.ts +5 -0
  28. package/dist/commands/stop.d.ts.map +1 -0
  29. package/dist/commands/stop.js +85 -0
  30. package/dist/commands/stop.js.map +1 -0
  31. package/dist/execution/direct-strategy.d.ts +18 -0
  32. package/dist/execution/direct-strategy.d.ts.map +1 -0
  33. package/dist/execution/direct-strategy.js +156 -0
  34. package/dist/execution/direct-strategy.js.map +1 -0
  35. package/dist/execution/docker-strategy.d.ts +26 -0
  36. package/dist/execution/docker-strategy.d.ts.map +1 -0
  37. package/dist/execution/docker-strategy.js +222 -0
  38. package/dist/execution/docker-strategy.js.map +1 -0
  39. package/dist/execution/index.d.ts +14 -0
  40. package/dist/execution/index.d.ts.map +1 -0
  41. package/dist/execution/index.js +13 -0
  42. package/dist/execution/index.js.map +1 -0
  43. package/dist/execution/kubernetes-exec-strategy.d.ts +23 -0
  44. package/dist/execution/kubernetes-exec-strategy.d.ts.map +1 -0
  45. package/dist/execution/kubernetes-exec-strategy.js +232 -0
  46. package/dist/execution/kubernetes-exec-strategy.js.map +1 -0
  47. package/dist/execution/registry.d.ts +41 -0
  48. package/dist/execution/registry.d.ts.map +1 -0
  49. package/dist/execution/registry.js +84 -0
  50. package/dist/execution/registry.js.map +1 -0
  51. package/dist/execution/slurm-strategy.d.ts +22 -0
  52. package/dist/execution/slurm-strategy.d.ts.map +1 -0
  53. package/dist/execution/slurm-strategy.js +219 -0
  54. package/dist/execution/slurm-strategy.js.map +1 -0
  55. package/dist/execution/types.d.ts +72 -0
  56. package/dist/execution/types.d.ts.map +1 -0
  57. package/dist/execution/types.js +10 -0
  58. package/dist/execution/types.js.map +1 -0
  59. package/dist/index.d.ts +22 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +22 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/lib/api-client.d.ts +35 -0
  64. package/dist/lib/api-client.d.ts.map +1 -0
  65. package/dist/lib/api-client.js +126 -0
  66. package/dist/lib/api-client.js.map +1 -0
  67. package/dist/lib/config.d.ts +174 -0
  68. package/dist/lib/config.d.ts.map +1 -0
  69. package/dist/lib/config.js +399 -0
  70. package/dist/lib/config.js.map +1 -0
  71. package/dist/lib/copy-worktree.d.ts +73 -0
  72. package/dist/lib/copy-worktree.d.ts.map +1 -0
  73. package/dist/lib/copy-worktree.js +374 -0
  74. package/dist/lib/copy-worktree.js.map +1 -0
  75. package/dist/lib/git-pr.d.ts +63 -0
  76. package/dist/lib/git-pr.d.ts.map +1 -0
  77. package/dist/lib/git-pr.js +224 -0
  78. package/dist/lib/git-pr.js.map +1 -0
  79. package/dist/lib/hardware-id.d.ts +25 -0
  80. package/dist/lib/hardware-id.d.ts.map +1 -0
  81. package/dist/lib/hardware-id.js +186 -0
  82. package/dist/lib/hardware-id.js.map +1 -0
  83. package/dist/lib/hpc-context.d.ts +35 -0
  84. package/dist/lib/hpc-context.d.ts.map +1 -0
  85. package/dist/lib/hpc-context.js +167 -0
  86. package/dist/lib/hpc-context.js.map +1 -0
  87. package/dist/lib/prompt-templates.d.ts +195 -0
  88. package/dist/lib/prompt-templates.d.ts.map +1 -0
  89. package/dist/lib/prompt-templates.js +353 -0
  90. package/dist/lib/prompt-templates.js.map +1 -0
  91. package/dist/lib/providers.d.ts +27 -0
  92. package/dist/lib/providers.d.ts.map +1 -0
  93. package/dist/lib/providers.js +372 -0
  94. package/dist/lib/providers.js.map +1 -0
  95. package/dist/lib/repo-context.d.ts +18 -0
  96. package/dist/lib/repo-context.d.ts.map +1 -0
  97. package/dist/lib/repo-context.js +61 -0
  98. package/dist/lib/repo-context.js.map +1 -0
  99. package/dist/lib/repo-utils.d.ts +35 -0
  100. package/dist/lib/repo-utils.d.ts.map +1 -0
  101. package/dist/lib/repo-utils.js +222 -0
  102. package/dist/lib/repo-utils.js.map +1 -0
  103. package/dist/lib/resources.d.ts +17 -0
  104. package/dist/lib/resources.d.ts.map +1 -0
  105. package/dist/lib/resources.js +227 -0
  106. package/dist/lib/resources.js.map +1 -0
  107. package/dist/lib/slurm-detect.d.ts +15 -0
  108. package/dist/lib/slurm-detect.d.ts.map +1 -0
  109. package/dist/lib/slurm-detect.js +148 -0
  110. package/dist/lib/slurm-detect.js.map +1 -0
  111. package/dist/lib/slurm-executor.d.ts +70 -0
  112. package/dist/lib/slurm-executor.d.ts.map +1 -0
  113. package/dist/lib/slurm-executor.js +402 -0
  114. package/dist/lib/slurm-executor.js.map +1 -0
  115. package/dist/lib/slurm-job-monitor.d.ts +52 -0
  116. package/dist/lib/slurm-job-monitor.d.ts.map +1 -0
  117. package/dist/lib/slurm-job-monitor.js +212 -0
  118. package/dist/lib/slurm-job-monitor.js.map +1 -0
  119. package/dist/lib/ssh-discovery.d.ts +17 -0
  120. package/dist/lib/ssh-discovery.d.ts.map +1 -0
  121. package/dist/lib/ssh-discovery.js +287 -0
  122. package/dist/lib/ssh-discovery.js.map +1 -0
  123. package/dist/lib/ssh-installer.d.ts +69 -0
  124. package/dist/lib/ssh-installer.d.ts.map +1 -0
  125. package/dist/lib/ssh-installer.js +230 -0
  126. package/dist/lib/ssh-installer.js.map +1 -0
  127. package/dist/lib/streaming-prompt.d.ts +48 -0
  128. package/dist/lib/streaming-prompt.d.ts.map +1 -0
  129. package/dist/lib/streaming-prompt.js +91 -0
  130. package/dist/lib/streaming-prompt.js.map +1 -0
  131. package/dist/lib/task-executor.d.ts +114 -0
  132. package/dist/lib/task-executor.d.ts.map +1 -0
  133. package/dist/lib/task-executor.js +753 -0
  134. package/dist/lib/task-executor.js.map +1 -0
  135. package/dist/lib/websocket-client.d.ts +200 -0
  136. package/dist/lib/websocket-client.d.ts.map +1 -0
  137. package/dist/lib/websocket-client.js +781 -0
  138. package/dist/lib/websocket-client.js.map +1 -0
  139. package/dist/lib/workdir-safety.d.ts +63 -0
  140. package/dist/lib/workdir-safety.d.ts.map +1 -0
  141. package/dist/lib/workdir-safety.js +247 -0
  142. package/dist/lib/workdir-safety.js.map +1 -0
  143. package/dist/lib/worktree-include.d.ts +14 -0
  144. package/dist/lib/worktree-include.d.ts.map +1 -0
  145. package/dist/lib/worktree-include.js +68 -0
  146. package/dist/lib/worktree-include.js.map +1 -0
  147. package/dist/lib/worktree-setup.d.ts +18 -0
  148. package/dist/lib/worktree-setup.d.ts.map +1 -0
  149. package/dist/lib/worktree-setup.js +60 -0
  150. package/dist/lib/worktree-setup.js.map +1 -0
  151. package/dist/lib/worktree.d.ts +37 -0
  152. package/dist/lib/worktree.d.ts.map +1 -0
  153. package/dist/lib/worktree.js +411 -0
  154. package/dist/lib/worktree.js.map +1 -0
  155. package/dist/mcp/index.d.ts +8 -0
  156. package/dist/mcp/index.d.ts.map +1 -0
  157. package/dist/mcp/index.js +8 -0
  158. package/dist/mcp/index.js.map +1 -0
  159. package/dist/mcp/server.d.ts +45 -0
  160. package/dist/mcp/server.d.ts.map +1 -0
  161. package/dist/mcp/server.js +153 -0
  162. package/dist/mcp/server.js.map +1 -0
  163. package/dist/mcp/session-bridge.d.ts +87 -0
  164. package/dist/mcp/session-bridge.d.ts.map +1 -0
  165. package/dist/mcp/session-bridge.js +317 -0
  166. package/dist/mcp/session-bridge.js.map +1 -0
  167. package/dist/mcp/tools.d.ts +70 -0
  168. package/dist/mcp/tools.d.ts.map +1 -0
  169. package/dist/mcp/tools.js +234 -0
  170. package/dist/mcp/tools.js.map +1 -0
  171. package/dist/mcp/types.d.ts +197 -0
  172. package/dist/mcp/types.d.ts.map +1 -0
  173. package/dist/mcp/types.js +16 -0
  174. package/dist/mcp/types.js.map +1 -0
  175. package/dist/providers/base-adapter.d.ts +56 -0
  176. package/dist/providers/base-adapter.d.ts.map +1 -0
  177. package/dist/providers/base-adapter.js +5 -0
  178. package/dist/providers/base-adapter.js.map +1 -0
  179. package/dist/providers/claude-code-adapter.d.ts +27 -0
  180. package/dist/providers/claude-code-adapter.d.ts.map +1 -0
  181. package/dist/providers/claude-code-adapter.js +298 -0
  182. package/dist/providers/claude-code-adapter.js.map +1 -0
  183. package/dist/providers/claude-sdk-adapter.d.ts +60 -0
  184. package/dist/providers/claude-sdk-adapter.d.ts.map +1 -0
  185. package/dist/providers/claude-sdk-adapter.js +632 -0
  186. package/dist/providers/claude-sdk-adapter.js.map +1 -0
  187. package/dist/providers/codex-adapter.d.ts +21 -0
  188. package/dist/providers/codex-adapter.d.ts.map +1 -0
  189. package/dist/providers/codex-adapter.js +197 -0
  190. package/dist/providers/codex-adapter.js.map +1 -0
  191. package/dist/providers/index.d.ts +26 -0
  192. package/dist/providers/index.d.ts.map +1 -0
  193. package/dist/providers/index.js +58 -0
  194. package/dist/providers/index.js.map +1 -0
  195. package/dist/providers/slurm-adapter.d.ts +26 -0
  196. package/dist/providers/slurm-adapter.d.ts.map +1 -0
  197. package/dist/providers/slurm-adapter.js +146 -0
  198. package/dist/providers/slurm-adapter.js.map +1 -0
  199. package/dist/types.d.ts +592 -0
  200. package/dist/types.d.ts.map +1 -0
  201. package/dist/types.js +5 -0
  202. package/dist/types.js.map +1 -0
  203. 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