@workermill/agent 0.8.5 → 0.8.9

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/dist/spawner.js DELETED
@@ -1,523 +0,0 @@
1
- /**
2
- * Remote Agent Spawner
3
- *
4
- * Spawns Docker worker containers that talk directly to the cloud WorkerMill API.
5
- * Extracted from api/src/services/local-epic-spawner.ts with key differences:
6
- * - API_BASE_URL points to cloud (https://workermill.com)
7
- * - ORG_API_KEY is the real org API key
8
- * - Container logs stream to cloud dashboard via SSE
9
- */
10
- import chalk from "chalk";
11
- import { spawn, execSync } from "child_process";
12
- import * as path from "path";
13
- import * as fs from "fs";
14
- import * as os from "os";
15
- /** Timestamp prefix */
16
- function ts() {
17
- return chalk.dim(new Date().toLocaleTimeString());
18
- }
19
- // Track active containers
20
- const activeContainers = new Map();
21
- /**
22
- * Detect if running in WSL.
23
- */
24
- function detectWSL() {
25
- if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
26
- return true;
27
- try {
28
- const procVersion = fs.readFileSync("/proc/version", "utf-8");
29
- return procVersion.toLowerCase().includes("microsoft");
30
- }
31
- catch {
32
- return false;
33
- }
34
- }
35
- const isWSL = detectWSL();
36
- const isDockerDesktop = isWSL || process.platform === "darwin" || process.platform === "win32";
37
- /**
38
- * Convert WSL paths to Windows paths for Docker volume mounts.
39
- */
40
- function toDockerPath(unixPath) {
41
- if (!isWSL)
42
- return unixPath;
43
- const match = unixPath.match(/^\/mnt\/([a-zA-Z])\/(.*)$/);
44
- if (match)
45
- return `${match[1].toUpperCase()}:/${match[2]}`;
46
- return unixPath;
47
- }
48
- /**
49
- * Find Claude config directory (handles WSL).
50
- */
51
- function findClaudeConfigDir() {
52
- const standardDir = path.join(os.homedir(), ".claude");
53
- if (fs.existsSync(standardDir))
54
- return standardDir;
55
- if (isWSL) {
56
- const windowsUsersDir = "/mnt/c/Users";
57
- if (fs.existsSync(windowsUsersDir)) {
58
- try {
59
- for (const user of fs.readdirSync(windowsUsersDir)) {
60
- if (["Public", "Default", "Default User", "All Users"].includes(user))
61
- continue;
62
- const claudeDir = path.join(windowsUsersDir, user, ".claude");
63
- if (fs.existsSync(claudeDir))
64
- return claudeDir;
65
- }
66
- }
67
- catch { /* ignore */ }
68
- }
69
- }
70
- return null;
71
- }
72
- /**
73
- * Get SCM token based on provider.
74
- */
75
- function getScmToken(scmProvider, config) {
76
- switch (scmProvider) {
77
- case "bitbucket": return config.bitbucketToken;
78
- case "gitlab": return config.gitlabToken;
79
- default: return config.githubToken;
80
- }
81
- }
82
- /** Check if a task has the self-review label (works across Jira, GitHub, GitLab, Linear) */
83
- function hasSelfReviewLabel(task) {
84
- const fields = task.jiraFields;
85
- if (!fields)
86
- return false;
87
- // Jira: labels is a string array at top level
88
- const jiraLabels = fields.labels;
89
- if (Array.isArray(jiraLabels) && jiraLabels.some((l) => typeof l === "string" && l.toLowerCase() === "self-review"))
90
- return true;
91
- // GitHub/GitLab: labels are in nested issue object with {name: string} shape
92
- const issue = fields.issue;
93
- const issueLabels = issue?.labels;
94
- if (Array.isArray(issueLabels) && issueLabels.some((l) => {
95
- if (typeof l === "string")
96
- return l.toLowerCase() === "self-review";
97
- if (l && typeof l === "object" && "name" in l)
98
- return (l.name || "").toLowerCase() === "self-review";
99
- return false;
100
- }))
101
- return true;
102
- return false;
103
- }
104
- /**
105
- * Spawn a Docker worker container for a task.
106
- */
107
- export async function spawnWorker(task, config, orgConfig, credentials) {
108
- const taskLabel = chalk.cyan(task.id.slice(0, 8));
109
- if (activeContainers.has(task.id)) {
110
- console.log(`${ts()} ${taskLabel} ${chalk.dim("Already running, skipping")}`);
111
- return;
112
- }
113
- const containerName = `workermill-${task.id.slice(0, 8)}`;
114
- // Build Docker run arguments
115
- const dockerArgs = ["run", "--rm", "--pull", "always", "--name", containerName];
116
- // Resource limits — 6GB memory with swap for overflow.
117
- // NODE_OPTIONS caps V8 heap at 2GB; the extra room is for git, npm, Claude CLI subprocesses.
118
- const totalRamGB = Math.round(os.totalmem() / (1024 * 1024 * 1024));
119
- if (totalRamGB <= 16) {
120
- dockerArgs.push("--memory", "6g", "--memory-swap", "10g", "--cpus", "2");
121
- }
122
- else if (totalRamGB <= 32) {
123
- dockerArgs.push("--memory", "6g", "--memory-swap", "12g", "--cpus", "4");
124
- }
125
- else {
126
- dockerArgs.push("--memory", "6g", "--memory-swap", "12g", "--cpus", "4");
127
- }
128
- // Network mode
129
- if (isDockerDesktop) {
130
- dockerArgs.push("--add-host=host.docker.internal:host-gateway");
131
- }
132
- else {
133
- dockerArgs.push("--network", "host");
134
- }
135
- // Mount Claude credentials (required for Anthropic workers, optional for others)
136
- const workerProvider = task.workerProvider || "anthropic";
137
- const claudeConfigDir = findClaudeConfigDir();
138
- if (!claudeConfigDir && workerProvider === "anthropic") {
139
- console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Claude credentials not found. Run 'claude' and complete the sign-in flow.`);
140
- return;
141
- }
142
- if (claudeConfigDir) {
143
- // Ensure credentials file is readable AND writable inside container.
144
- // Claude CLI creates .credentials.json with 600 permissions, but the container
145
- // runs as UID 1001 (worker) while the host user is UID 1000. Without this chmod,
146
- // the mounted file is unreadable inside the container → "Invalid API key" errors.
147
- const credFile = path.join(claudeConfigDir, ".credentials.json");
148
- try {
149
- fs.chmodSync(credFile, 0o666);
150
- }
151
- catch {
152
- // Ignore - file may not exist yet
153
- }
154
- const dockerClaudeDir = toDockerPath(claudeConfigDir);
155
- dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
156
- }
157
- else {
158
- console.log(`${ts()} ${taskLabel} ${chalk.dim("Skipping Claude mount (non-Anthropic worker)")}`);
159
- }
160
- // Build environment variables — KEY DIFFERENCE: API_BASE_URL points to cloud
161
- const scmProvider = (task.scmProvider || "github");
162
- const scmToken = getScmToken(scmProvider, config);
163
- const envVars = {
164
- // Cap V8 heap to 3GB — forces aggressive GC instead of bloating to fill container.
165
- // Each Claude CLI subprocess inherits this, preventing unbounded heap growth.
166
- // Container has 6GB total; this leaves room for git, npm, and OS overhead.
167
- NODE_OPTIONS: "--max-old-space-size=3072",
168
- EPIC_MODE: "true",
169
- EXECUTION_MODE: "local",
170
- TASK_ID: task.id,
171
- ORG_ID: task.orgId || "",
172
- JIRA_ISSUE_KEY: task.jiraIssueKey || "",
173
- JIRA_SUMMARY: task.summary || "",
174
- JIRA_DESCRIPTION: task.description || "",
175
- TASK_SUMMARY: task.summary || "",
176
- TASK_DESCRIPTION: task.description || "",
177
- WORKER_PERSONA: task.workerPersona || "",
178
- RETRY_NUMBER: String(task.retryCount ?? 0),
179
- TICKET_KEY: task.jiraIssueKey || "",
180
- // Cloud API — this is what makes remote agent mode work
181
- API_BASE_URL: config.apiUrl,
182
- ORG_API_KEY: config.apiKey,
183
- // SCM configuration
184
- SCM_PROVIDER: scmProvider,
185
- SCM_TOKEN: scmToken,
186
- SCM_BASE_URL: credentials?.scmBaseUrl || "",
187
- GITHUB_TOKEN: config.githubToken,
188
- GH_TOKEN: config.githubToken,
189
- GITHUB_REVIEWER_TOKEN: credentials?.githubReviewerToken || "",
190
- BITBUCKET_TOKEN: config.bitbucketToken,
191
- BITBUCKET_USERNAME: "x-token-auth",
192
- GITLAB_TOKEN: config.gitlabToken,
193
- // Target repository
194
- TARGET_REPO: task.githubRepo || "",
195
- GITHUB_REPO: task.githubRepo || "",
196
- // Worker model — comes from task or org settings, no hardcoded fallbacks
197
- WORKER_MODEL: task.workerModel || String(orgConfig.defaultWorkerModel || ""),
198
- CLAUDE_MODEL: task.workerModel || String(orgConfig.defaultWorkerModel || ""),
199
- // Jira credentials (from org Secrets Manager via /api/agent/claim)
200
- JIRA_BASE_URL: credentials?.jiraBaseUrl || "",
201
- JIRA_EMAIL: credentials?.jiraEmail || "",
202
- JIRA_API_TOKEN: credentials?.jiraApiToken || "",
203
- // Issue tracker system (jira, linear, github-issues)
204
- TICKET_SYSTEM: credentials?.issueTrackerProvider || "jira",
205
- LINEAR_API_KEY: credentials?.linearApiKey || "",
206
- // AWS credentials (from org Secrets Manager for workers that deploy infrastructure)
207
- AWS_ACCESS_KEY_ID: credentials?.customerAwsAccessKeyId || "",
208
- AWS_SECRET_ACCESS_KEY: credentials?.customerAwsSecretAccessKey || "",
209
- AWS_DEFAULT_REGION: credentials?.customerAwsRegion || "",
210
- AWS_REGION: credentials?.customerAwsRegion || "",
211
- // AWS cross-account access (for customer infrastructure deployment)
212
- CUSTOMER_AWS_ROLE_ARN: credentials?.customerAwsRoleArn || "",
213
- CUSTOMER_AWS_EXTERNAL_ID: credentials?.customerAwsExternalId || "",
214
- CUSTOMER_AWS_REGION: credentials?.customerAwsRegion || "",
215
- // Manager provider and model for tech lead review
216
- MANAGER_PROVIDER: credentials?.managerProvider || "anthropic",
217
- MANAGER_MODEL: credentials?.managerModelId || "",
218
- // Bitbucket email (needed for API calls with API tokens)
219
- BITBUCKET_EMAIL: credentials?.bitbucketEmail || "",
220
- // Workflow control flags (match ECS and local spawner logic)
221
- DEPLOYMENT_ENABLED: task.deploymentEnabled || task.parentTaskId ? "true" : "false",
222
- PRD_CHILD_TASK: task.parentTaskId ? "true" : "false",
223
- IMPROVEMENT_ENABLED: task.improvementEnabled ? "true" : "false",
224
- QUALITY_GATE_BYPASS: task.qualityGateBypass ? "true" : "false",
225
- STANDARD_SDK_MODE: task.standardSdkMode ? "true" : "false",
226
- MAX_REVIEW_REVISIONS: String(orgConfig.maxReviewRevisions ?? 3),
227
- CODEBASE_INDEXING_ENABLED: orgConfig.codebaseIndexingEnabled === true ? "true" : "false",
228
- // Existing PR info (for deployment-only runs)
229
- EXISTING_PR_URL: task.githubPrUrl || "",
230
- EXISTING_PR_NUMBER: task.githubPrNumber ? String(task.githubPrNumber) : "",
231
- // PRD Orchestration
232
- PARENT_TASK_ID: task.parentTaskId || task.id,
233
- PARENT_JIRA_KEY: task.jiraIssueKey && /-S\d+$/.test(task.jiraIssueKey)
234
- ? task.jiraFields?.parentJiraKey || ""
235
- : "",
236
- TARGET_BRANCH: task.jiraFields?.targetBranch || "",
237
- STORY_BRANCH: task.jiraFields?.storyBranch || "",
238
- // Task notes from dashboard
239
- TASK_NOTES: task.taskNotes || "",
240
- // File targeting from planning agent (cost-first optimization)
241
- TARGET_FILES: JSON.stringify(task.jiraFields?.targetFiles || []),
242
- REFERENCE_FILES: JSON.stringify(task.jiraFields?.referenceFiles || []),
243
- // V2 Pipeline support
244
- PIPELINE_VERSION: task.jiraFields?.pipelineVersion || task.pipelineVersion || "",
245
- V2_STEP_INPUT: task.jiraFields?.v2StepInput ? JSON.stringify(task.jiraFields.v2StepInput) : "",
246
- // Execution mode for supervised/autonomous
247
- EXECUTION_MODE_SETTING: task.jiraFields?.executionMode || "autonomous",
248
- // AI provider configuration
249
- ANTHROPIC_API_KEY: claudeConfigDir ? "" : (credentials?.anthropicApiKey || process.env.ANTHROPIC_API_KEY || ""),
250
- WORKER_PROVIDER: task.workerProvider || "anthropic",
251
- OPENAI_API_KEY: credentials?.openaiApiKey || "",
252
- GOOGLE_API_KEY: credentials?.googleApiKey || "",
253
- GOOGLE_GENERATIVE_AI_API_KEY: credentials?.googleApiKey || "",
254
- OLLAMA_HOST: credentials?.ollamaBaseUrl || "",
255
- OLLAMA_CONTEXT_WINDOW: credentials?.ollamaContextWindow ? String(credentials.ollamaContextWindow) : "",
256
- VLLM_BASE_URL: credentials?.vllmBaseUrl || "",
257
- // Resilience settings from org config
258
- BLOCKER_MAX_AUTO_RETRIES: String(orgConfig.blockerMaxAutoRetries ?? 3),
259
- BLOCKER_AUTO_RETRY_ENABLED: orgConfig.blockerAutoRetryEnabled !== false ? "true" : "false",
260
- PUSH_AFTER_COMMIT: orgConfig.pushAfterCommit !== false ? "true" : "false",
261
- GRACEFUL_SHUTDOWN_ENABLED: orgConfig.gracefulShutdownEnabled !== false ? "true" : "false",
262
- MAX_PARALLEL_EXPERTS: String(orgConfig.maxParallelExperts ?? 4),
263
- REVIEW_ENABLED: task.skipManagerReview === false ? "true" : "false",
264
- SELF_REVIEW_ENABLED: hasSelfReviewLabel(task) || (orgConfig.selfReviewEnabled !== false) ? "true" : "false",
265
- };
266
- // Build -e args, filtering empty values
267
- for (const [k, v] of Object.entries(envVars)) {
268
- if (v !== "") {
269
- dockerArgs.push("-e", `${k}=${v}`);
270
- }
271
- }
272
- // Worker image (configurable: Docker Hub for CLI users, local for bin/remote-agent)
273
- dockerArgs.push(config.workerImage || "public.ecr.aws/a7k5r0v0/workermill-worker:latest");
274
- const reviewEnabled = task.skipManagerReview === false;
275
- console.log(`${ts()} ${taskLabel} ${chalk.dim("Starting container")} ${chalk.yellow(containerName)}`);
276
- console.log(`${ts()} ${taskLabel} ${chalk.dim(` skipManagerReview=${task.skipManagerReview} → REVIEW_ENABLED=${reviewEnabled}`)}`);
277
- console.log(`${ts()} ${taskLabel} ${chalk.dim(` model=${task.workerModel} repo=${task.githubRepo}`)}`);
278
- console.log(`${ts()} ${taskLabel} ${chalk.dim(` totalRamGB=${totalRamGB} docker args:`)} ${dockerArgs.slice(0, 10).join(" ")}`);
279
- // Spawn Docker container
280
- const proc = spawn("docker", dockerArgs, {
281
- stdio: ["ignore", "pipe", "pipe"],
282
- detached: false,
283
- });
284
- if (!proc.pid) {
285
- console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Failed to spawn container`);
286
- return;
287
- }
288
- const container = {
289
- taskId: task.id,
290
- containerName,
291
- process: proc,
292
- startedAt: new Date(),
293
- status: "running",
294
- resultEmitted: false,
295
- };
296
- activeContainers.set(task.id, container);
297
- // Stream stdout/stderr to console (logs go to cloud via container's own HTTP calls)
298
- proc.stdout?.on("data", (data) => {
299
- const lines = data.toString().split("\n").filter((l) => l.trim());
300
- for (const line of lines) {
301
- console.log(`${ts()} ${taskLabel} ${chalk.dim(line)}`);
302
- // Track if worker emitted ::result:: marker (means it called worker-complete itself)
303
- if (line.includes("::result::")) {
304
- container.resultEmitted = true;
305
- }
306
- }
307
- });
308
- proc.stderr?.on("data", (data) => {
309
- const lines = data.toString().split("\n").filter((l) => l.trim());
310
- for (const line of lines) {
311
- console.log(`${ts()} ${taskLabel} ${chalk.red(line)}`);
312
- }
313
- });
314
- // Handle exit
315
- proc.on("exit", (code) => {
316
- container.status = code === 0 ? "completed" : "failed";
317
- const duration = Math.round((Date.now() - container.startedAt.getTime()) / 1000);
318
- const icon = code === 0 ? chalk.green("✓") : chalk.red("✗");
319
- const status = code === 0 ? chalk.green("completed") : chalk.red(`failed (exit ${code})`);
320
- console.log(`${ts()} ${taskLabel} ${icon} Container ${status} ${chalk.dim(`(${duration}s)`)}`);
321
- // Safety net: if worker didn't emit ::result::, it may have died without calling worker-complete.
322
- // Wait briefly for any in-flight API calls, then POST a fallback completion.
323
- if (!container.resultEmitted) {
324
- console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} No ::result:: marker seen — posting fallback completion in 15s`);
325
- setTimeout(async () => {
326
- try {
327
- const fallbackResult = code === 0 ? "completed" : "failed";
328
- const errorMsg = code !== 0 ? `Worker container exited with code ${code} without reporting completion` : undefined;
329
- const resp = await fetch(`${config.apiUrl}/api/tasks/${task.id}/worker-complete`, {
330
- method: "POST",
331
- headers: {
332
- "Content-Type": "application/json",
333
- "x-api-key": config.apiKey,
334
- },
335
- body: JSON.stringify({
336
- exitCode: code ?? 1,
337
- result: fallbackResult,
338
- errorMessage: errorMsg,
339
- }),
340
- });
341
- const data = await resp.json();
342
- if (data.status === "ignored") {
343
- console.log(`${ts()} ${taskLabel} ${chalk.dim("Fallback completion ignored (task already transitioned)")}`);
344
- }
345
- else {
346
- console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} Fallback completion applied: ${fallbackResult}`);
347
- }
348
- }
349
- catch (err) {
350
- console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Fallback completion failed:`, err instanceof Error ? err.message : err);
351
- }
352
- }, 15_000);
353
- }
354
- // Clean up after delay (extended to allow fallback completion to finish)
355
- setTimeout(() => activeContainers.delete(task.id), 90_000);
356
- });
357
- proc.on("error", (err) => {
358
- container.status = "failed";
359
- console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Container error: ${err.message}`);
360
- });
361
- }
362
- /**
363
- * Get count of actively running containers.
364
- */
365
- export function getActiveCount() {
366
- return Array.from(activeContainers.values()).filter((c) => c.status === "running").length;
367
- }
368
- /**
369
- * Get IDs of all active tasks.
370
- */
371
- export function getActiveTaskIds() {
372
- return Array.from(activeContainers.values())
373
- .filter((c) => c.status === "running")
374
- .map((c) => c.taskId);
375
- }
376
- /**
377
- * Stop a specific task's container by task ID.
378
- */
379
- export function stopTask(taskId) {
380
- const container = activeContainers.get(taskId);
381
- if (!container || container.status !== "running")
382
- return;
383
- const taskLabel = chalk.cyan(taskId.slice(0, 8));
384
- console.log(`${ts()} ${taskLabel} ${chalk.red("■")} Stopping container (cancelled by dashboard)`);
385
- try {
386
- execSync(`docker stop ${container.containerName}`, { stdio: "ignore", timeout: 15_000 });
387
- container.status = "completed";
388
- }
389
- catch { /* may have already exited */ }
390
- activeContainers.delete(taskId);
391
- }
392
- /**
393
- * Stop all running containers.
394
- */
395
- export async function stopAll() {
396
- console.log(`${ts()} ${chalk.dim(`Stopping ${activeContainers.size} containers...`)}`);
397
- for (const [, container] of activeContainers) {
398
- if (container.status === "running") {
399
- try {
400
- execSync(`docker stop ${container.containerName}`, { stdio: "ignore", timeout: 15_000 });
401
- container.status = "completed";
402
- }
403
- catch { /* may have already exited */ }
404
- }
405
- }
406
- activeContainers.clear();
407
- }
408
- /**
409
- * Spawn a manager Docker container for PR review or log analysis.
410
- * Uses the same worker image but overrides the entrypoint to manager-entrypoint.sh.
411
- */
412
- export async function spawnManagerWorker(task, config, credentials) {
413
- const taskLabel = chalk.cyan(task.id.slice(0, 8));
414
- // Check if already running a manager for this task
415
- const managerKey = `manager-${task.id}`;
416
- if (activeContainers.has(managerKey)) {
417
- return;
418
- }
419
- const containerName = `wm-manager-${task.id.slice(0, 8)}-${Date.now()}`;
420
- // Mount Claude credentials
421
- const claudeConfigDir = findClaudeConfigDir();
422
- const dockerArgs = [
423
- "run", "--rm",
424
- "--name", containerName,
425
- "--memory=4g",
426
- "--cpus=2",
427
- ];
428
- if (claudeConfigDir) {
429
- const dockerClaudeDir = toDockerPath(claudeConfigDir);
430
- dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
431
- }
432
- // Manager-specific env vars (match ECS runManagerTask)
433
- const scmProvider = (task.scmProvider || "github");
434
- const scmToken = getScmToken(scmProvider, config);
435
- const envVars = {
436
- NODE_OPTIONS: "--max-old-space-size=3072",
437
- TASK_ID: task.id,
438
- MANAGER_ACTION: task.managerAction,
439
- JIRA_ISSUE_KEY: task.jiraIssueKey || "",
440
- JIRA_SUMMARY: task.summary || "",
441
- JIRA_DESCRIPTION: task.description || "",
442
- GITHUB_REPO: task.githubRepo || "",
443
- PR_URL: task.githubPrUrl || "",
444
- PR_NUMBER: task.githubPrNumber ? String(task.githubPrNumber) : "",
445
- // Cloud API
446
- API_BASE_URL: config.apiUrl,
447
- ORG_API_KEY: config.apiKey,
448
- // SCM configuration
449
- SCM_PROVIDER: scmProvider,
450
- SCM_TOKEN: scmToken,
451
- GITHUB_TOKEN: config.githubToken,
452
- GH_TOKEN: config.githubToken,
453
- BITBUCKET_TOKEN: config.bitbucketToken,
454
- BITBUCKET_USERNAME: "x-token-auth",
455
- GITLAB_TOKEN: config.gitlabToken,
456
- // Manager provider and model
457
- MANAGER_PROVIDER: credentials?.managerProvider || "anthropic",
458
- MANAGER_MODEL: credentials?.managerModelId || "",
459
- // AI provider API keys
460
- ANTHROPIC_API_KEY: claudeConfigDir ? "" : (credentials?.anthropicApiKey || process.env.ANTHROPIC_API_KEY || ""),
461
- OPENAI_API_KEY: credentials?.openaiApiKey || "",
462
- GOOGLE_API_KEY: credentials?.googleApiKey || "",
463
- // Jira credentials
464
- JIRA_BASE_URL: credentials?.jiraBaseUrl || "",
465
- JIRA_EMAIL: credentials?.jiraEmail || "",
466
- JIRA_API_TOKEN: credentials?.jiraApiToken || "",
467
- TICKET_SYSTEM: credentials?.issueTrackerProvider || "jira",
468
- LINEAR_API_KEY: credentials?.linearApiKey || "",
469
- };
470
- // Build -e args, filtering empty values
471
- for (const [k, v] of Object.entries(envVars)) {
472
- if (v !== "") {
473
- dockerArgs.push("-e", `${k}=${v}`);
474
- }
475
- }
476
- // Worker image with manager entrypoint override
477
- const workerImage = config.workerImage || "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
478
- dockerArgs.push("--entrypoint", "/bin/bash");
479
- dockerArgs.push(workerImage);
480
- dockerArgs.push("/app/manager-entrypoint.sh");
481
- console.log(`${ts()} ${taskLabel} ${chalk.magenta("◆ MANAGER")} Starting ${task.managerAction} container ${chalk.yellow(containerName)}`);
482
- const proc = spawn("docker", dockerArgs, {
483
- stdio: ["ignore", "pipe", "pipe"],
484
- detached: false,
485
- });
486
- if (!proc.pid) {
487
- console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Failed to spawn manager container`);
488
- return;
489
- }
490
- const container = {
491
- taskId: managerKey,
492
- containerName,
493
- process: proc,
494
- startedAt: new Date(),
495
- status: "running",
496
- resultEmitted: false,
497
- };
498
- activeContainers.set(managerKey, container);
499
- proc.stdout?.on("data", (data) => {
500
- const lines = data.toString().split("\n").filter((l) => l.trim());
501
- for (const line of lines) {
502
- console.log(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.dim(line)}`);
503
- }
504
- });
505
- proc.stderr?.on("data", (data) => {
506
- const lines = data.toString().split("\n").filter((l) => l.trim());
507
- for (const line of lines) {
508
- console.log(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.red(line)}`);
509
- }
510
- });
511
- proc.on("exit", (code) => {
512
- container.status = code === 0 ? "completed" : "failed";
513
- const duration = Math.round((Date.now() - container.startedAt.getTime()) / 1000);
514
- const icon = code === 0 ? chalk.green("✓") : chalk.red("✗");
515
- const status = code === 0 ? chalk.green("completed") : chalk.red(`failed (exit ${code})`);
516
- console.log(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${icon} Manager ${task.managerAction} ${status} ${chalk.dim(`(${duration}s)`)}`);
517
- setTimeout(() => activeContainers.delete(managerKey), 60_000);
518
- });
519
- proc.on("error", (err) => {
520
- container.status = "failed";
521
- console.error(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.red("✗")} Container error: ${err.message}`);
522
- });
523
- }
package/dist/updater.d.ts DELETED
@@ -1,8 +0,0 @@
1
- /**
2
- * Run `npm install -g @workermill/agent@latest` and return whether it succeeded.
3
- */
4
- export declare function selfUpdate(): Promise<boolean>;
5
- /**
6
- * Restart the current process by spawning a new one and exiting.
7
- */
8
- export declare function restartAgent(): never;
package/dist/updater.js DELETED
@@ -1,40 +0,0 @@
1
- import { spawn } from "child_process";
2
- import chalk from "chalk";
3
- /**
4
- * Run `npm install -g @workermill/agent@latest` and return whether it succeeded.
5
- */
6
- export async function selfUpdate() {
7
- console.log(chalk.cyan(" Updating @workermill/agent..."));
8
- return new Promise((resolve) => {
9
- const child = spawn("npm", ["install", "-g", "@workermill/agent@latest"], {
10
- stdio: "inherit",
11
- shell: true,
12
- });
13
- child.on("error", (err) => {
14
- console.error(chalk.red(` Update failed: ${err.message}`));
15
- resolve(false);
16
- });
17
- child.on("close", (code) => {
18
- if (code === 0) {
19
- console.log(chalk.green(" Update successful."));
20
- resolve(true);
21
- }
22
- else {
23
- console.error(chalk.red(` Update failed with exit code ${code}`));
24
- resolve(false);
25
- }
26
- });
27
- });
28
- }
29
- /**
30
- * Restart the current process by spawning a new one and exiting.
31
- */
32
- export function restartAgent() {
33
- console.log(chalk.cyan(" Restarting agent..."));
34
- const child = spawn(process.argv[0], process.argv.slice(1), {
35
- stdio: "inherit",
36
- detached: true,
37
- });
38
- child.unref();
39
- process.exit(0);
40
- }
package/dist/version.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const AGENT_VERSION: string;
package/dist/version.js DELETED
@@ -1,4 +0,0 @@
1
- import { createRequire } from "module";
2
- const require = createRequire(import.meta.url);
3
- const pkg = require("../package.json");
4
- export const AGENT_VERSION = pkg.version;