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