@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 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 || "",
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.0",
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",