@workermill/agent 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -183,8 +183,8 @@ export function runCriticCli(claudePath, model, prompt, env) {
183
183
  });
184
184
  const timeout = setTimeout(() => {
185
185
  proc.kill("SIGTERM");
186
- reject(new Error("Critic CLI timed out after 3 minutes"));
187
- }, 180_000);
186
+ reject(new Error("Critic CLI timed out after 10 minutes"));
187
+ }, 600_000);
188
188
  proc.on("exit", (code) => {
189
189
  clearTimeout(timeout);
190
190
  if (code !== 0) {
@@ -262,7 +262,7 @@ export async function runCriticValidation(claudePath, model, prd, plan, env, tas
262
262
  if (!providerApiKey) {
263
263
  throw new Error(`No API key for critic provider "${effectiveProvider}"`);
264
264
  }
265
- rawCriticOutput = await generateText(effectiveProvider, model, criticPrompt, providerApiKey, { maxTokens: 4096, temperature: 0.3, timeoutMs: 180_000 });
265
+ rawCriticOutput = await generateText(effectiveProvider, model, criticPrompt, providerApiKey, { maxTokens: 4096, temperature: 0.3, timeoutMs: 600_000 });
266
266
  }
267
267
  const result = parseCriticResponse(rawCriticOutput);
268
268
  const statusIcon = result.score >= AUTO_APPROVAL_THRESHOLD
package/dist/planner.js CHANGED
@@ -456,7 +456,15 @@ Keep your report under 1500 words. Only report facts you verified with tools.`;
456
456
  *
457
457
  * Falls back to single-agent planning if anything goes wrong.
458
458
  */
459
- async function runTeamPlanning(task, basePrompt, claudePath, model, env, repoPath, taskId, startTime) {
459
+ /**
460
+ * Run team analysis: spawn 3 parallel analyst agents once, then return
461
+ * an enhanced prompt with their reports appended. Returns null if all
462
+ * analysts fail (caller should fall back to basePrompt).
463
+ *
464
+ * This runs ONCE before the planner-critic loop — analyst prompts don't
465
+ * include critic feedback, so re-running them on iteration 2+ is waste.
466
+ */
467
+ async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPath, taskId, startTime) {
460
468
  const taskLabel = chalk.cyan(taskId.slice(0, 8));
461
469
  console.log(`${ts()} ${taskLabel} ${chalk.magenta("◆ Team planning")} — running 3 analysts in parallel...`);
462
470
  await postLog(taskId, `${PREFIX} Team planning: running codebase, requirements, and risk analysts in parallel...`);
@@ -497,7 +505,7 @@ async function runTeamPlanning(task, basePrompt, claudePath, model, env, repoPat
497
505
  if (attempt === MAX_TEAM_RETRIES) {
498
506
  console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} All analysts failed after ${MAX_TEAM_RETRIES} attempts, falling back to single-agent planning`);
499
507
  await postLog(taskId, `${PREFIX} All analysis agents failed after ${MAX_TEAM_RETRIES} attempts — falling back to single-agent planning`);
500
- return runClaudeCli(claudePath, model, basePrompt, env, taskId, startTime);
508
+ return null;
501
509
  }
502
510
  }
503
511
  // Build enhanced prompt with analysis reports
@@ -511,14 +519,12 @@ async function runTeamPlanning(task, basePrompt, claudePath, model, env, repoPat
511
519
  if (riskReport) {
512
520
  sections.push(`## Risk Assessment\n\n${riskReport}`);
513
521
  }
514
- const enhancedPrompt = basePrompt +
522
+ return (basePrompt +
515
523
  "\n\n" +
516
524
  sections.join("\n\n") +
517
525
  "\n\n" +
518
526
  "Use these analyses to produce a more accurate execution plan.\n" +
519
- "Prefer actual file paths discovered in the codebase analysis over guessed paths.";
520
- // Run the final synthesizer planner with the enhanced prompt
521
- return runClaudeCli(claudePath, model, enhancedPrompt, env, taskId, startTime);
527
+ "Prefer actual file paths discovered in the codebase analysis over guessed paths.");
522
528
  }
523
529
  /**
524
530
  * Run planning for a task with Planner-Critic validation loop.
@@ -552,8 +558,11 @@ export async function planTask(task, config, credentials) {
552
558
  const startTime = Date.now();
553
559
  // PRD for critic validation: use task description, fall back to summary
554
560
  const prd = task.description || task.summary;
555
- // Clone repo for team planning if enabled
561
+ // Run team analysis ONCE before the planner-critic loop.
562
+ // Analyst prompts don't include critic feedback, so re-running them
563
+ // on iteration 2+ wastes compute (they'd produce the same reports).
556
564
  let repoPath = null;
565
+ let enhancedBasePrompt = basePrompt;
557
566
  if (isAnthropicPlanning && config.teamPlanningEnabled && task.githubRepo) {
558
567
  const scmProvider = task.scmProvider || "github";
559
568
  const scmToken = scmProvider === "bitbucket"
@@ -567,9 +576,18 @@ export async function planTask(task, config, credentials) {
567
576
  else {
568
577
  console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} No SCM token for ${scmProvider}, skipping team planning`);
569
578
  }
579
+ if (repoPath) {
580
+ const analysisResult = await runTeamAnalysis(task, basePrompt, claudePath, cliModel, cleanEnv, repoPath, task.id, startTime);
581
+ if (analysisResult) {
582
+ enhancedBasePrompt = analysisResult;
583
+ }
584
+ // else: all analysts failed, fall back to basePrompt
585
+ }
570
586
  }
571
587
  // 2. Planner-Critic iteration loop
572
- let currentPrompt = basePrompt;
588
+ // Use enhancedBasePrompt (with analyst reports) as the base for all iterations.
589
+ // Critic feedback gets appended on re-plan, but analyst reports are fixed.
590
+ let currentPrompt = enhancedBasePrompt;
573
591
  let bestPlan = null;
574
592
  let bestScore = 0;
575
593
  // Track critic history across iterations for analytics
@@ -590,10 +608,7 @@ export async function planTask(task, config, credentials) {
590
608
  // 2a. Generate plan via Claude CLI (Anthropic) or HTTP API (other providers)
591
609
  let rawOutput;
592
610
  try {
593
- if (isAnthropicPlanning && config.teamPlanningEnabled && repoPath) {
594
- rawOutput = await runTeamPlanning(task, currentPrompt, claudePath, cliModel, cleanEnv, repoPath, task.id, startTime);
595
- }
596
- else if (isAnthropicPlanning) {
611
+ if (isAnthropicPlanning) {
597
612
  rawOutput = await runClaudeCli(claudePath, cliModel, currentPrompt, cleanEnv, task.id, startTime);
598
613
  }
599
614
  else {
@@ -690,7 +705,7 @@ export async function planTask(task, config, credentials) {
690
705
  // 2f. Rejected — append critic feedback for next iteration
691
706
  if (iteration < MAX_ITERATIONS) {
692
707
  const feedback = formatCriticFeedback(criticResult);
693
- currentPrompt = basePrompt + "\n\n" + feedback;
708
+ currentPrompt = enhancedBasePrompt + "\n\n" + feedback;
694
709
  const msg = `${PREFIX} Critic rejected (score: ${criticResult.score}/100, threshold: ${AUTO_APPROVAL_THRESHOLD}). Re-planning with feedback...`;
695
710
  console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
696
711
  await postLog(task.id, msg);
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
- PARENT_TASK_ID: task.id,
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 || task.id,
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "WorkerMill Remote Agent - Run AI workers locally with your Claude Max subscription",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",