@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/cli.js +31 -64
- package/dist/index.js +23 -123
- package/package.json +3 -2
- package/dist/ai-sdk-generate.d.ts +0 -33
- package/dist/ai-sdk-generate.js +0 -160
- package/dist/api.d.ts +0 -13
- package/dist/api.js +0 -29
- package/dist/cli.d.ts +0 -8
- package/dist/commands/logs.d.ts +0 -9
- package/dist/commands/logs.js +0 -52
- package/dist/commands/pull.d.ts +0 -4
- package/dist/commands/pull.js +0 -35
- package/dist/commands/setup.d.ts +0 -11
- package/dist/commands/setup.js +0 -396
- package/dist/commands/start.d.ts +0 -11
- package/dist/commands/start.js +0 -152
- package/dist/commands/status.d.ts +0 -6
- package/dist/commands/status.js +0 -86
- package/dist/commands/stop.d.ts +0 -6
- package/dist/commands/stop.js +0 -61
- package/dist/commands/update.d.ts +0 -1
- package/dist/commands/update.js +0 -20
- package/dist/config.d.ts +0 -77
- package/dist/config.js +0 -284
- package/dist/index.d.ts +0 -14
- package/dist/plan-validator.d.ts +0 -104
- package/dist/plan-validator.js +0 -436
- package/dist/planner.d.ts +0 -40
- package/dist/planner.js +0 -792
- package/dist/poller.d.ts +0 -20
- package/dist/poller.js +0 -346
- package/dist/providers.d.ts +0 -18
- package/dist/providers.js +0 -118
- package/dist/spawner.d.ts +0 -110
- package/dist/spawner.js +0 -523
- package/dist/updater.d.ts +0 -8
- package/dist/updater.js +0 -40
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -4
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