@workermill/agent 0.5.2 → 0.6.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/dist/poller.js +72 -1
- package/dist/spawner.d.ts +48 -0
- package/dist/spawner.js +157 -2
- package/package.json +1 -1
package/dist/poller.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { api } from "./api.js";
|
|
9
9
|
import { planTask } from "./planner.js";
|
|
10
|
-
import { spawnWorker, getActiveCount, getActiveTaskIds, stopTask, } from "./spawner.js";
|
|
10
|
+
import { spawnWorker, spawnManagerWorker, getActiveCount, getActiveTaskIds, stopTask, } from "./spawner.js";
|
|
11
11
|
import { AGENT_VERSION } from "./version.js";
|
|
12
12
|
import { selfUpdate, restartAgent } from "./updater.js";
|
|
13
13
|
// Track tasks currently being planned (to avoid double-dispatching)
|
|
14
14
|
const planningInProgress = new Set();
|
|
15
|
+
// Track manager tasks in progress (to avoid double-dispatching)
|
|
16
|
+
const managerInProgress = new Set();
|
|
15
17
|
// Cached org config
|
|
16
18
|
let orgConfig = null;
|
|
17
19
|
// Flags to avoid spamming update notices every heartbeat
|
|
@@ -61,6 +63,15 @@ async function pollOnce(config) {
|
|
|
61
63
|
await handleQueuedTask(task, config);
|
|
62
64
|
}
|
|
63
65
|
}
|
|
66
|
+
// Handle manager tasks (log analysis, PR review)
|
|
67
|
+
const managerTasks = response.data.managerTasks;
|
|
68
|
+
if (managerTasks && managerTasks.length > 0) {
|
|
69
|
+
for (const mt of managerTasks) {
|
|
70
|
+
if (!managerInProgress.has(mt.id)) {
|
|
71
|
+
await handleManagerTask(mt, config);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
}
|
|
65
76
|
catch (error) {
|
|
66
77
|
const err = error;
|
|
@@ -153,9 +164,18 @@ async function handleQueuedTask(task, config) {
|
|
|
153
164
|
description: task.description,
|
|
154
165
|
jiraIssueKey: task.jiraIssueKey,
|
|
155
166
|
workerModel: task.workerModel,
|
|
167
|
+
workerProvider: task.workerProvider,
|
|
156
168
|
githubRepo: task.githubRepo,
|
|
157
169
|
scmProvider: task.scmProvider,
|
|
158
170
|
skipManagerReview: task.skipManagerReview,
|
|
171
|
+
deploymentEnabled: task.deploymentEnabled,
|
|
172
|
+
improvementEnabled: task.improvementEnabled,
|
|
173
|
+
qualityGateBypass: task.qualityGateBypass,
|
|
174
|
+
standardSdkMode: task.standardSdkMode,
|
|
175
|
+
parentTaskId: task.parentTaskId,
|
|
176
|
+
taskNotes: task.taskNotes,
|
|
177
|
+
githubPrUrl: task.githubPrUrl,
|
|
178
|
+
githubPrNumber: task.githubPrNumber,
|
|
159
179
|
executionPlanV2: task.executionPlanV2,
|
|
160
180
|
jiraFields: task.jiraFields || {},
|
|
161
181
|
};
|
|
@@ -164,6 +184,57 @@ async function handleQueuedTask(task, config) {
|
|
|
164
184
|
// Spawn asynchronously (don't block the poll loop)
|
|
165
185
|
spawnWorker(spawnableTask, config, oc, credentials).catch((err) => console.error(`${ts()} ${chalk.red("✗")} Spawn failed for ${taskLabel}:`, err.message || err));
|
|
166
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Handle a manager task (log analysis or PR review).
|
|
189
|
+
*/
|
|
190
|
+
async function handleManagerTask(task, config) {
|
|
191
|
+
const taskLabel = chalk.cyan(task.id.slice(0, 8));
|
|
192
|
+
managerInProgress.add(task.id);
|
|
193
|
+
// Claim the manager task
|
|
194
|
+
try {
|
|
195
|
+
const claimResponse = await api.post("/api/agent/claim-manager", {
|
|
196
|
+
taskId: task.id,
|
|
197
|
+
agentId: config.agentId,
|
|
198
|
+
action: task.managerAction,
|
|
199
|
+
});
|
|
200
|
+
if (!claimResponse.data.claimed) {
|
|
201
|
+
managerInProgress.delete(task.id);
|
|
202
|
+
return; // Another agent or cloud ECS claimed it
|
|
203
|
+
}
|
|
204
|
+
const claimedTask = claimResponse.data.task;
|
|
205
|
+
const credentials = claimResponse.data.credentials || {};
|
|
206
|
+
const actionLabel = task.managerAction === "analyze_logs"
|
|
207
|
+
? chalk.yellow("LOG ANALYSIS")
|
|
208
|
+
: chalk.yellow("PR REVIEW");
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(`${ts()} ${chalk.magenta("◆ MANAGER")} ${actionLabel} ${taskLabel} ${task.summary.substring(0, 60)}`);
|
|
211
|
+
const managerTask = {
|
|
212
|
+
id: task.id,
|
|
213
|
+
summary: claimedTask?.summary || task.summary,
|
|
214
|
+
description: claimedTask?.description || task.description,
|
|
215
|
+
jiraIssueKey: claimedTask?.jiraIssueKey || task.jiraIssueKey,
|
|
216
|
+
githubRepo: claimedTask?.githubRepo || task.githubRepo,
|
|
217
|
+
scmProvider: claimedTask?.scmProvider || task.scmProvider,
|
|
218
|
+
githubPrUrl: claimedTask?.githubPrUrl || task.githubPrUrl,
|
|
219
|
+
githubPrNumber: claimedTask?.githubPrNumber || task.githubPrNumber,
|
|
220
|
+
managerAction: task.managerAction,
|
|
221
|
+
};
|
|
222
|
+
// Spawn asynchronously (don't block the poll loop)
|
|
223
|
+
spawnManagerWorker(managerTask, config, credentials)
|
|
224
|
+
.then(() => {
|
|
225
|
+
console.log(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.green("✓")} Manager ${task.managerAction} dispatched`);
|
|
226
|
+
})
|
|
227
|
+
.catch((err) => {
|
|
228
|
+
console.error(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.red("✗")} Manager spawn failed:`, err.message || err);
|
|
229
|
+
})
|
|
230
|
+
.finally(() => managerInProgress.delete(task.id));
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
managerInProgress.delete(task.id);
|
|
234
|
+
const error = err;
|
|
235
|
+
console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Failed to claim manager task:`, error.message || String(err));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
167
238
|
/**
|
|
168
239
|
* Start the poll loop.
|
|
169
240
|
*/
|
package/dist/spawner.d.ts
CHANGED
|
@@ -10,17 +10,28 @@
|
|
|
10
10
|
import type { AgentConfig } from "./config.js";
|
|
11
11
|
export interface SpawnableTask {
|
|
12
12
|
id: string;
|
|
13
|
+
orgId?: string;
|
|
13
14
|
summary: string;
|
|
14
15
|
description: string | null;
|
|
15
16
|
jiraIssueKey: string | null;
|
|
16
17
|
workerModel: string;
|
|
17
18
|
workerProvider?: string;
|
|
19
|
+
workerPersona?: string;
|
|
18
20
|
githubRepo: string;
|
|
19
21
|
scmProvider: string;
|
|
20
22
|
skipManagerReview?: boolean;
|
|
21
23
|
executionPlanV2: unknown;
|
|
22
24
|
jiraFields: Record<string, unknown>;
|
|
23
25
|
taskNotes?: string;
|
|
26
|
+
deploymentEnabled?: boolean;
|
|
27
|
+
improvementEnabled?: boolean;
|
|
28
|
+
qualityGateBypass?: boolean;
|
|
29
|
+
standardSdkMode?: boolean;
|
|
30
|
+
parentTaskId?: string;
|
|
31
|
+
githubPrUrl?: string;
|
|
32
|
+
githubPrNumber?: number;
|
|
33
|
+
retryCount?: number;
|
|
34
|
+
pipelineVersion?: string;
|
|
24
35
|
}
|
|
25
36
|
/** Org credentials returned by /api/agent/claim */
|
|
26
37
|
export interface ClaimCredentials {
|
|
@@ -33,12 +44,18 @@ export interface ClaimCredentials {
|
|
|
33
44
|
customerAwsAccessKeyId?: string;
|
|
34
45
|
customerAwsSecretAccessKey?: string;
|
|
35
46
|
customerAwsRegion?: string;
|
|
47
|
+
customerAwsRoleArn?: string;
|
|
48
|
+
customerAwsExternalId?: string;
|
|
36
49
|
issueTrackerProvider?: string;
|
|
37
50
|
bitbucketEmail?: string;
|
|
51
|
+
githubReviewerToken?: string;
|
|
52
|
+
scmBaseUrl?: string;
|
|
53
|
+
ollamaContextWindow?: number;
|
|
38
54
|
anthropicApiKey?: string;
|
|
39
55
|
openaiApiKey?: string;
|
|
40
56
|
googleApiKey?: string;
|
|
41
57
|
ollamaBaseUrl?: string;
|
|
58
|
+
vllmBaseUrl?: string;
|
|
42
59
|
}
|
|
43
60
|
/**
|
|
44
61
|
* Spawn a Docker worker container for a task.
|
|
@@ -60,3 +77,34 @@ export declare function stopTask(taskId: string): void;
|
|
|
60
77
|
* Stop all running containers.
|
|
61
78
|
*/
|
|
62
79
|
export declare function stopAll(): Promise<void>;
|
|
80
|
+
/** Manager task returned by the poll endpoint */
|
|
81
|
+
export interface ManagerTask {
|
|
82
|
+
id: string;
|
|
83
|
+
summary: string;
|
|
84
|
+
description: string | null;
|
|
85
|
+
jiraIssueKey: string | null;
|
|
86
|
+
githubRepo: string;
|
|
87
|
+
scmProvider: string;
|
|
88
|
+
githubPrUrl?: string;
|
|
89
|
+
githubPrNumber?: number;
|
|
90
|
+
managerAction: "analyze_logs" | "review_pr";
|
|
91
|
+
}
|
|
92
|
+
/** Credentials for manager tasks (subset of ClaimCredentials) */
|
|
93
|
+
export interface ManagerCredentials {
|
|
94
|
+
managerProvider?: string;
|
|
95
|
+
managerModelId?: string;
|
|
96
|
+
anthropicApiKey?: string;
|
|
97
|
+
openaiApiKey?: string;
|
|
98
|
+
googleApiKey?: string;
|
|
99
|
+
ollamaBaseUrl?: string;
|
|
100
|
+
jiraBaseUrl?: string;
|
|
101
|
+
jiraEmail?: string;
|
|
102
|
+
jiraApiToken?: string;
|
|
103
|
+
linearApiKey?: string;
|
|
104
|
+
issueTrackerProvider?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Spawn a manager Docker container for PR review or log analysis.
|
|
108
|
+
* Uses the same worker image but overrides the entrypoint to manager-entrypoint.sh.
|
|
109
|
+
*/
|
|
110
|
+
export declare function spawnManagerWorker(task: ManagerTask, config: AgentConfig, credentials?: ManagerCredentials): Promise<void>;
|
package/dist/spawner.js
CHANGED
|
@@ -157,26 +157,34 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
|
|
|
157
157
|
EPIC_MODE: "true",
|
|
158
158
|
EXECUTION_MODE: "local",
|
|
159
159
|
TASK_ID: task.id,
|
|
160
|
-
|
|
160
|
+
ORG_ID: task.orgId || "",
|
|
161
161
|
JIRA_ISSUE_KEY: task.jiraIssueKey || "",
|
|
162
|
+
JIRA_SUMMARY: task.summary || "",
|
|
163
|
+
JIRA_DESCRIPTION: task.description || "",
|
|
162
164
|
TASK_SUMMARY: task.summary || "",
|
|
163
165
|
TASK_DESCRIPTION: task.description || "",
|
|
166
|
+
WORKER_PERSONA: task.workerPersona || "",
|
|
167
|
+
RETRY_NUMBER: String(task.retryCount ?? 0),
|
|
168
|
+
TICKET_KEY: task.jiraIssueKey || "",
|
|
164
169
|
// Cloud API — this is what makes remote agent mode work
|
|
165
170
|
API_BASE_URL: config.apiUrl,
|
|
166
171
|
ORG_API_KEY: config.apiKey,
|
|
167
172
|
// SCM configuration
|
|
168
173
|
SCM_PROVIDER: scmProvider,
|
|
169
174
|
SCM_TOKEN: scmToken,
|
|
175
|
+
SCM_BASE_URL: credentials?.scmBaseUrl || "",
|
|
170
176
|
GITHUB_TOKEN: config.githubToken,
|
|
171
177
|
GH_TOKEN: config.githubToken,
|
|
178
|
+
GITHUB_REVIEWER_TOKEN: credentials?.githubReviewerToken || "",
|
|
172
179
|
BITBUCKET_TOKEN: config.bitbucketToken,
|
|
173
180
|
BITBUCKET_USERNAME: "x-token-auth",
|
|
174
181
|
GITLAB_TOKEN: config.gitlabToken,
|
|
175
182
|
// Target repository
|
|
176
183
|
TARGET_REPO: task.githubRepo || "",
|
|
177
184
|
GITHUB_REPO: task.githubRepo || "",
|
|
178
|
-
// Worker model
|
|
185
|
+
// Worker model (CLAUDE_MODEL is legacy compat for manager entrypoint)
|
|
179
186
|
WORKER_MODEL: task.workerModel || String(orgConfig.defaultWorkerModel || "sonnet"),
|
|
187
|
+
CLAUDE_MODEL: task.workerProvider === "anthropic" ? (task.workerModel || "sonnet") : "sonnet",
|
|
180
188
|
// Jira credentials (from org Secrets Manager via /api/agent/claim)
|
|
181
189
|
JIRA_BASE_URL: credentials?.jiraBaseUrl || "",
|
|
182
190
|
JIRA_EMAIL: credentials?.jiraEmail || "",
|
|
@@ -189,13 +197,43 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
|
|
|
189
197
|
AWS_SECRET_ACCESS_KEY: credentials?.customerAwsSecretAccessKey || "",
|
|
190
198
|
AWS_DEFAULT_REGION: credentials?.customerAwsRegion || "",
|
|
191
199
|
AWS_REGION: credentials?.customerAwsRegion || "",
|
|
200
|
+
// AWS cross-account access (for customer infrastructure deployment)
|
|
201
|
+
CUSTOMER_AWS_ROLE_ARN: credentials?.customerAwsRoleArn || "",
|
|
202
|
+
CUSTOMER_AWS_EXTERNAL_ID: credentials?.customerAwsExternalId || "",
|
|
203
|
+
CUSTOMER_AWS_REGION: credentials?.customerAwsRegion || "",
|
|
192
204
|
// Manager provider and model for tech lead review
|
|
193
205
|
MANAGER_PROVIDER: credentials?.managerProvider || "anthropic",
|
|
194
206
|
MANAGER_MODEL: credentials?.managerModelId || "",
|
|
195
207
|
// Bitbucket email (needed for API calls with API tokens)
|
|
196
208
|
BITBUCKET_EMAIL: credentials?.bitbucketEmail || "",
|
|
209
|
+
// Workflow control flags (match ECS and local spawner logic)
|
|
210
|
+
DEPLOYMENT_ENABLED: task.deploymentEnabled || task.parentTaskId ? "true" : "false",
|
|
211
|
+
PRD_CHILD_TASK: task.parentTaskId ? "true" : "false",
|
|
212
|
+
IMPROVEMENT_ENABLED: task.improvementEnabled ? "true" : "false",
|
|
213
|
+
QUALITY_GATE_BYPASS: task.qualityGateBypass ? "true" : "false",
|
|
214
|
+
STANDARD_SDK_MODE: task.standardSdkMode ? "true" : "false",
|
|
215
|
+
MAX_REVIEW_REVISIONS: String(orgConfig.maxReviewRevisions ?? 3),
|
|
216
|
+
CODEBASE_INDEXING_ENABLED: orgConfig.codebaseIndexingEnabled === true ? "true" : "false",
|
|
217
|
+
// Existing PR info (for deployment-only runs)
|
|
218
|
+
EXISTING_PR_URL: task.githubPrUrl || "",
|
|
219
|
+
EXISTING_PR_NUMBER: task.githubPrNumber ? String(task.githubPrNumber) : "",
|
|
220
|
+
// PRD Orchestration
|
|
221
|
+
PARENT_TASK_ID: task.parentTaskId || "",
|
|
222
|
+
PARENT_JIRA_KEY: task.jiraIssueKey && /-S\d+$/.test(task.jiraIssueKey)
|
|
223
|
+
? task.jiraFields?.parentJiraKey || ""
|
|
224
|
+
: "",
|
|
225
|
+
TARGET_BRANCH: task.jiraFields?.targetBranch || "",
|
|
226
|
+
STORY_BRANCH: task.jiraFields?.storyBranch || "",
|
|
197
227
|
// Task notes from dashboard
|
|
198
228
|
TASK_NOTES: task.taskNotes || "",
|
|
229
|
+
// File targeting from planning agent (cost-first optimization)
|
|
230
|
+
TARGET_FILES: JSON.stringify(task.jiraFields?.targetFiles || []),
|
|
231
|
+
REFERENCE_FILES: JSON.stringify(task.jiraFields?.referenceFiles || []),
|
|
232
|
+
// V2 Pipeline support
|
|
233
|
+
PIPELINE_VERSION: task.jiraFields?.pipelineVersion || task.pipelineVersion || "",
|
|
234
|
+
V2_STEP_INPUT: task.jiraFields?.v2StepInput ? JSON.stringify(task.jiraFields.v2StepInput) : "",
|
|
235
|
+
// Execution mode for supervised/autonomous
|
|
236
|
+
EXECUTION_MODE_SETTING: task.jiraFields?.executionMode || "autonomous",
|
|
199
237
|
// AI provider configuration
|
|
200
238
|
ANTHROPIC_API_KEY: claudeConfigDir ? "" : (credentials?.anthropicApiKey || process.env.ANTHROPIC_API_KEY || ""),
|
|
201
239
|
WORKER_PROVIDER: task.workerProvider || "anthropic",
|
|
@@ -203,6 +241,8 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
|
|
|
203
241
|
GOOGLE_API_KEY: credentials?.googleApiKey || "",
|
|
204
242
|
GOOGLE_GENERATIVE_AI_API_KEY: credentials?.googleApiKey || "",
|
|
205
243
|
OLLAMA_HOST: credentials?.ollamaBaseUrl || "",
|
|
244
|
+
OLLAMA_CONTEXT_WINDOW: credentials?.ollamaContextWindow ? String(credentials.ollamaContextWindow) : "",
|
|
245
|
+
VLLM_BASE_URL: credentials?.vllmBaseUrl || "",
|
|
206
246
|
// Resilience settings from org config
|
|
207
247
|
BLOCKER_MAX_AUTO_RETRIES: String(orgConfig.blockerMaxAutoRetries ?? 3),
|
|
208
248
|
BLOCKER_AUTO_RETRY_ENABLED: orgConfig.blockerAutoRetryEnabled !== false ? "true" : "false",
|
|
@@ -316,3 +356,118 @@ export async function stopAll() {
|
|
|
316
356
|
}
|
|
317
357
|
activeContainers.clear();
|
|
318
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Spawn a manager Docker container for PR review or log analysis.
|
|
361
|
+
* Uses the same worker image but overrides the entrypoint to manager-entrypoint.sh.
|
|
362
|
+
*/
|
|
363
|
+
export async function spawnManagerWorker(task, config, credentials) {
|
|
364
|
+
const taskLabel = chalk.cyan(task.id.slice(0, 8));
|
|
365
|
+
// Check if already running a manager for this task
|
|
366
|
+
const managerKey = `manager-${task.id}`;
|
|
367
|
+
if (activeContainers.has(managerKey)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const containerName = `wm-manager-${task.id.slice(0, 8)}-${Date.now()}`;
|
|
371
|
+
// Mount Claude credentials
|
|
372
|
+
const claudeConfigDir = findClaudeConfigDir();
|
|
373
|
+
const dockerArgs = [
|
|
374
|
+
"run", "--rm",
|
|
375
|
+
"--name", containerName,
|
|
376
|
+
"--memory=4g",
|
|
377
|
+
"--cpus=2",
|
|
378
|
+
];
|
|
379
|
+
if (claudeConfigDir) {
|
|
380
|
+
const dockerClaudeDir = toDockerPath(claudeConfigDir);
|
|
381
|
+
dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
|
|
382
|
+
}
|
|
383
|
+
// Manager-specific env vars (match ECS runManagerTask)
|
|
384
|
+
const scmProvider = (task.scmProvider || "github");
|
|
385
|
+
const scmToken = getScmToken(scmProvider, config);
|
|
386
|
+
const envVars = {
|
|
387
|
+
NODE_OPTIONS: "--max-old-space-size=3072",
|
|
388
|
+
TASK_ID: task.id,
|
|
389
|
+
MANAGER_ACTION: task.managerAction,
|
|
390
|
+
JIRA_ISSUE_KEY: task.jiraIssueKey || "",
|
|
391
|
+
JIRA_SUMMARY: task.summary || "",
|
|
392
|
+
JIRA_DESCRIPTION: task.description || "",
|
|
393
|
+
GITHUB_REPO: task.githubRepo || "",
|
|
394
|
+
PR_URL: task.githubPrUrl || "",
|
|
395
|
+
PR_NUMBER: task.githubPrNumber ? String(task.githubPrNumber) : "",
|
|
396
|
+
// Cloud API
|
|
397
|
+
API_BASE_URL: config.apiUrl,
|
|
398
|
+
ORG_API_KEY: config.apiKey,
|
|
399
|
+
// SCM configuration
|
|
400
|
+
SCM_PROVIDER: scmProvider,
|
|
401
|
+
SCM_TOKEN: scmToken,
|
|
402
|
+
GITHUB_TOKEN: config.githubToken,
|
|
403
|
+
GH_TOKEN: config.githubToken,
|
|
404
|
+
BITBUCKET_TOKEN: config.bitbucketToken,
|
|
405
|
+
BITBUCKET_USERNAME: "x-token-auth",
|
|
406
|
+
GITLAB_TOKEN: config.gitlabToken,
|
|
407
|
+
// Manager provider and model
|
|
408
|
+
MANAGER_PROVIDER: credentials?.managerProvider || "anthropic",
|
|
409
|
+
MANAGER_MODEL: credentials?.managerModelId || "",
|
|
410
|
+
// AI provider API keys
|
|
411
|
+
ANTHROPIC_API_KEY: claudeConfigDir ? "" : (credentials?.anthropicApiKey || process.env.ANTHROPIC_API_KEY || ""),
|
|
412
|
+
OPENAI_API_KEY: credentials?.openaiApiKey || "",
|
|
413
|
+
GOOGLE_API_KEY: credentials?.googleApiKey || "",
|
|
414
|
+
// Jira credentials
|
|
415
|
+
JIRA_BASE_URL: credentials?.jiraBaseUrl || "",
|
|
416
|
+
JIRA_EMAIL: credentials?.jiraEmail || "",
|
|
417
|
+
JIRA_API_TOKEN: credentials?.jiraApiToken || "",
|
|
418
|
+
TICKET_SYSTEM: credentials?.issueTrackerProvider || "jira",
|
|
419
|
+
LINEAR_API_KEY: credentials?.linearApiKey || "",
|
|
420
|
+
};
|
|
421
|
+
// Build -e args, filtering empty values
|
|
422
|
+
for (const [k, v] of Object.entries(envVars)) {
|
|
423
|
+
if (v !== "") {
|
|
424
|
+
dockerArgs.push("-e", `${k}=${v}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Worker image with manager entrypoint override
|
|
428
|
+
const workerImage = config.workerImage || "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
|
|
429
|
+
dockerArgs.push("--entrypoint", "/bin/bash");
|
|
430
|
+
dockerArgs.push(workerImage);
|
|
431
|
+
dockerArgs.push("/app/manager-entrypoint.sh");
|
|
432
|
+
console.log(`${ts()} ${taskLabel} ${chalk.magenta("◆ MANAGER")} Starting ${task.managerAction} container ${chalk.yellow(containerName)}`);
|
|
433
|
+
const proc = spawn("docker", dockerArgs, {
|
|
434
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
435
|
+
detached: false,
|
|
436
|
+
});
|
|
437
|
+
if (!proc.pid) {
|
|
438
|
+
console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Failed to spawn manager container`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const container = {
|
|
442
|
+
taskId: managerKey,
|
|
443
|
+
containerName,
|
|
444
|
+
process: proc,
|
|
445
|
+
startedAt: new Date(),
|
|
446
|
+
status: "running",
|
|
447
|
+
};
|
|
448
|
+
activeContainers.set(managerKey, container);
|
|
449
|
+
proc.stdout?.on("data", (data) => {
|
|
450
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
451
|
+
for (const line of lines) {
|
|
452
|
+
console.log(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.dim(line)}`);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
proc.stderr?.on("data", (data) => {
|
|
456
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
457
|
+
for (const line of lines) {
|
|
458
|
+
console.log(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.red(line)}`);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
proc.on("exit", (code) => {
|
|
462
|
+
container.status = code === 0 ? "completed" : "failed";
|
|
463
|
+
const duration = Math.round((Date.now() - container.startedAt.getTime()) / 1000);
|
|
464
|
+
const icon = code === 0 ? chalk.green("✓") : chalk.red("✗");
|
|
465
|
+
const status = code === 0 ? chalk.green("completed") : chalk.red(`failed (exit ${code})`);
|
|
466
|
+
console.log(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${icon} Manager ${task.managerAction} ${status} ${chalk.dim(`(${duration}s)`)}`);
|
|
467
|
+
setTimeout(() => activeContainers.delete(managerKey), 60_000);
|
|
468
|
+
});
|
|
469
|
+
proc.on("error", (err) => {
|
|
470
|
+
container.status = "failed";
|
|
471
|
+
console.error(`${ts()} ${taskLabel} ${chalk.magenta("MGR")} ${chalk.red("✗")} Container error: ${err.message}`);
|
|
472
|
+
});
|
|
473
|
+
}
|