@workermill/agent 0.3.0 → 0.4.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.
@@ -25,14 +25,23 @@ export async function startCommand(options) {
25
25
  const failing = prereqs.filter((p) => !p.ok);
26
26
  // Auto-pull worker image if it's the only missing prereq
27
27
  const imageMissing = failing.find((p) => p.name === "Worker image");
28
- const otherFailing = failing.filter((p) => p.name !== "Worker image");
29
- if (otherFailing.length > 0) {
28
+ // Claude CLI and auth are soft prerequisites — only needed for Anthropic provider.
29
+ // Non-Anthropic orgs can plan+execute without Claude CLI.
30
+ const softPrereqs = new Set(["Claude CLI", "Claude auth"]);
31
+ const hardFailing = failing.filter((p) => p.name !== "Worker image" && !softPrereqs.has(p.name));
32
+ const softFailing = failing.filter((p) => softPrereqs.has(p.name));
33
+ if (hardFailing.length > 0) {
30
34
  console.log(chalk.red("Prerequisites check failed:"));
31
- for (const p of otherFailing) {
35
+ for (const p of hardFailing) {
32
36
  console.log(chalk.red(` ✗ ${p.name}: ${p.detail}`));
33
37
  }
34
38
  process.exit(1);
35
39
  }
40
+ if (softFailing.length > 0) {
41
+ for (const p of softFailing) {
42
+ console.log(chalk.yellow(` ⚠ ${p.name}: ${p.detail} (required for Anthropic provider)`));
43
+ }
44
+ }
36
45
  if (imageMissing) {
37
46
  console.log(chalk.yellow(` Worker image not found locally. Pulling ${config.workerImage}...`));
38
47
  const { spawnSync } = await import("child_process");
package/dist/config.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface AgentConfig {
16
16
  bitbucketToken: string;
17
17
  gitlabToken: string;
18
18
  workerImage: string;
19
+ teamPlanningEnabled: boolean;
19
20
  }
20
21
  export interface FileConfig {
21
22
  apiUrl: string;
@@ -30,6 +31,7 @@ export interface FileConfig {
30
31
  gitlab: string;
31
32
  };
32
33
  workerImage: string;
34
+ teamPlanningEnabled?: boolean;
33
35
  setupCompletedAt: string;
34
36
  }
35
37
  export declare function getConfigDir(): string;
package/dist/config.js CHANGED
@@ -75,6 +75,7 @@ export function loadConfigFromFile() {
75
75
  bitbucketToken: fc.tokens?.bitbucket || "",
76
76
  gitlabToken: fc.tokens?.gitlab || "",
77
77
  workerImage,
78
+ teamPlanningEnabled: fc.teamPlanningEnabled ?? true,
78
79
  };
79
80
  }
80
81
  /**
@@ -119,6 +120,7 @@ export function loadConfig() {
119
120
  bitbucketToken: process.env.BITBUCKET_TOKEN || "",
120
121
  gitlabToken: process.env.GITLAB_TOKEN || "",
121
122
  workerImage: process.env.WORKER_IMAGE || "workermill-worker:local",
123
+ teamPlanningEnabled: process.env.TEAM_PLANNING_ENABLED !== "false",
122
124
  };
123
125
  }
124
126
  /**
@@ -9,6 +9,7 @@
9
9
  * This ensures remote agent plans get the same quality gates as cloud plans,
10
10
  * even though the planning prompt runs locally via Claude CLI.
11
11
  */
12
+ import { type AIProvider } from "./providers.js";
12
13
  export interface PlannedStory {
13
14
  id: string;
14
15
  title: string;
@@ -76,7 +77,8 @@ export declare function runCriticCli(claudePath: string, model: string, prompt:
76
77
  export declare function formatCriticFeedback(critic: CriticResult): string;
77
78
  /**
78
79
  * Run critic validation on a parsed plan.
80
+ * Routes to Claude CLI (Anthropic) or HTTP API (other providers).
79
81
  * Returns the critic result, or null if critic fails (non-blocking).
80
82
  */
81
- export declare function runCriticValidation(claudePath: string, model: string, prd: string, plan: ExecutionPlan, env: Record<string, string | undefined>, taskLabel: string): Promise<CriticResult | null>;
83
+ export declare function runCriticValidation(claudePath: string, model: string, prd: string, plan: ExecutionPlan, env: Record<string, string | undefined>, taskLabel: string, provider?: AIProvider, providerApiKey?: string): Promise<CriticResult | null>;
82
84
  export { AUTO_APPROVAL_THRESHOLD };
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { spawn } from "child_process";
13
13
  import chalk from "chalk";
14
+ import { generateText } from "./providers.js";
14
15
  // ============================================================================
15
16
  // CONSTANTS
16
17
  // ============================================================================
@@ -245,13 +246,24 @@ function ts() {
245
246
  }
246
247
  /**
247
248
  * Run critic validation on a parsed plan.
249
+ * Routes to Claude CLI (Anthropic) or HTTP API (other providers).
248
250
  * Returns the critic result, or null if critic fails (non-blocking).
249
251
  */
250
- export async function runCriticValidation(claudePath, model, prd, plan, env, taskLabel) {
252
+ export async function runCriticValidation(claudePath, model, prd, plan, env, taskLabel, provider, providerApiKey) {
251
253
  const criticPrompt = buildCriticPrompt(prd, plan);
252
- console.log(`${ts()} ${taskLabel} ${chalk.dim("Running critic validation...")}`);
254
+ const effectiveProvider = provider || "anthropic";
255
+ console.log(`${ts()} ${taskLabel} ${chalk.dim(`Running critic validation (${effectiveProvider})...`)}`);
253
256
  try {
254
- const rawCriticOutput = await runCriticCli(claudePath, model, criticPrompt, env);
257
+ let rawCriticOutput;
258
+ if (effectiveProvider === "anthropic") {
259
+ rawCriticOutput = await runCriticCli(claudePath, model, criticPrompt, env);
260
+ }
261
+ else {
262
+ if (!providerApiKey) {
263
+ throw new Error(`No API key for critic provider "${effectiveProvider}"`);
264
+ }
265
+ rawCriticOutput = await generateText(effectiveProvider, model, criticPrompt, providerApiKey, { maxTokens: 4096, temperature: 0.3, timeoutMs: 180_000 });
266
+ }
255
267
  const result = parseCriticResponse(rawCriticOutput);
256
268
  const statusIcon = result.score >= AUTO_APPROVAL_THRESHOLD
257
269
  ? chalk.green("✓")
package/dist/planner.d.ts CHANGED
@@ -15,10 +15,13 @@
15
15
  * sees the same planning progress as cloud mode.
16
16
  */
17
17
  import { type AgentConfig } from "./config.js";
18
+ import type { ClaimCredentials } from "./spawner.js";
18
19
  export interface PlanningTask {
19
20
  id: string;
20
21
  summary: string;
21
22
  description: string | null;
23
+ githubRepo?: string;
24
+ scmProvider?: string;
22
25
  }
23
26
  /**
24
27
  * Run planning for a task with Planner-Critic validation loop.
@@ -32,4 +35,4 @@ export interface PlanningTask {
32
35
  * 6. If critic rejects: re-run planner with feedback (up to MAX_ITERATIONS)
33
36
  * 7. After MAX_ITERATIONS without approval: fail the task
34
37
  */
35
- export declare function planTask(task: PlanningTask, config: AgentConfig): Promise<boolean>;
38
+ export declare function planTask(task: PlanningTask, config: AgentConfig, credentials?: ClaimCredentials): Promise<boolean>;
package/dist/planner.js CHANGED
@@ -15,10 +15,11 @@
15
15
  * sees the same planning progress as cloud mode.
16
16
  */
17
17
  import chalk from "chalk";
18
- import { spawn } from "child_process";
18
+ import { spawn, execSync } from "child_process";
19
19
  import { findClaudePath } from "./config.js";
20
20
  import { api } from "./api.js";
21
21
  import { parseExecutionPlan, applyFileCap, serializePlan, runCriticValidation, formatCriticFeedback, AUTO_APPROVAL_THRESHOLD, } from "./plan-validator.js";
22
+ import { generateText } from "./providers.js";
22
23
  /** Max Planner-Critic iterations before giving up */
23
24
  const MAX_ITERATIONS = 3;
24
25
  /** Timestamp prefix */
@@ -203,8 +204,8 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
203
204
  clearInterval(progressInterval);
204
205
  clearInterval(sseProgressInterval);
205
206
  proc.kill("SIGTERM");
206
- reject(new Error("Claude CLI timed out after 10 minutes"));
207
- }, 600_000);
207
+ reject(new Error("Claude CLI timed out after 20 minutes"));
208
+ }, 1_200_000);
208
209
  proc.on("exit", (code) => {
209
210
  clearTimeout(timeout);
210
211
  clearInterval(progressInterval);
@@ -228,6 +229,219 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
228
229
  });
229
230
  });
230
231
  }
232
+ /**
233
+ * Resolve the API key for a given provider from claim credentials.
234
+ * For Ollama, returns the base URL instead of an API key.
235
+ */
236
+ function resolveProviderApiKey(provider, credentials) {
237
+ if (!credentials)
238
+ return undefined;
239
+ switch (provider) {
240
+ case "anthropic":
241
+ return credentials.anthropicApiKey;
242
+ case "openai":
243
+ return credentials.openaiApiKey;
244
+ case "google":
245
+ return credentials.googleApiKey;
246
+ case "ollama":
247
+ return credentials.ollamaBaseUrl || "http://localhost:11434";
248
+ default:
249
+ return undefined;
250
+ }
251
+ }
252
+ /**
253
+ * Build a git clone URL with authentication for the given SCM provider.
254
+ */
255
+ function buildCloneUrl(repo, token, scmProvider) {
256
+ switch (scmProvider) {
257
+ case "bitbucket":
258
+ return `https://x-token-auth:${token}@bitbucket.org/${repo}.git`;
259
+ case "gitlab":
260
+ return `https://oauth2:${token}@gitlab.com/${repo}.git`;
261
+ case "github":
262
+ default:
263
+ return `https://x-access-token:${token}@github.com/${repo}.git`;
264
+ }
265
+ }
266
+ /**
267
+ * Clone the target repo to a temp directory for team planning analysis.
268
+ * Returns the path on success, or null on failure (fallback to single-agent).
269
+ */
270
+ async function cloneTargetRepo(repo, token, scmProvider, taskId) {
271
+ const taskLabel = chalk.cyan(taskId.slice(0, 8));
272
+ const tmpDir = `/tmp/workermill-planning-${taskId.slice(0, 8)}-${Date.now()}`;
273
+ try {
274
+ const cloneUrl = buildCloneUrl(repo, token, scmProvider);
275
+ console.log(`${ts()} ${taskLabel} ${chalk.dim("Cloning repo for team planning...")}`);
276
+ execSync(`git clone --depth 1 --single-branch "${cloneUrl}" "${tmpDir}"`, {
277
+ stdio: "ignore",
278
+ timeout: 60_000,
279
+ });
280
+ console.log(`${ts()} ${taskLabel} ${chalk.green("✓")} Repo cloned to ${chalk.dim(tmpDir)}`);
281
+ return tmpDir;
282
+ }
283
+ catch (error) {
284
+ const errMsg = error instanceof Error ? error.message : String(error);
285
+ console.error(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} Clone failed, falling back to single-agent: ${errMsg.substring(0, 100)}`);
286
+ // Cleanup partial clone
287
+ try {
288
+ execSync(`rm -rf "${tmpDir}"`, { stdio: "ignore" });
289
+ }
290
+ catch {
291
+ /* ignore */
292
+ }
293
+ return null;
294
+ }
295
+ }
296
+ /**
297
+ * Run an analyst agent via Claude CLI with tool access to the cloned repo.
298
+ * Returns the analyst's report text, or an empty string on failure.
299
+ */
300
+ function runAnalyst(claudePath, model, prompt, repoPath, env, timeoutMs = 120_000) {
301
+ return new Promise((resolve) => {
302
+ const proc = spawn(claudePath, [
303
+ "-p",
304
+ prompt,
305
+ "--model",
306
+ model,
307
+ "--permission-mode",
308
+ "bypassPermissions",
309
+ "--output-format",
310
+ "stream-json",
311
+ ], {
312
+ cwd: repoPath,
313
+ env,
314
+ stdio: ["pipe", "pipe", "pipe"],
315
+ });
316
+ let resultText = "";
317
+ let fullText = "";
318
+ let lineBuffer = "";
319
+ proc.stdout.on("data", (data) => {
320
+ lineBuffer += data.toString();
321
+ const lines = lineBuffer.split("\n");
322
+ lineBuffer = lines.pop() || "";
323
+ for (const line of lines) {
324
+ const trimmed = line.trim();
325
+ if (!trimmed)
326
+ continue;
327
+ try {
328
+ const event = JSON.parse(trimmed);
329
+ if (event.type === "content_block_delta" && event.delta?.text) {
330
+ fullText += event.delta.text;
331
+ }
332
+ else if (event.type === "result" && event.result) {
333
+ resultText =
334
+ typeof event.result === "string" ? event.result : "";
335
+ }
336
+ }
337
+ catch {
338
+ fullText += trimmed + "\n";
339
+ }
340
+ }
341
+ });
342
+ const timeout = setTimeout(() => {
343
+ proc.kill("SIGTERM");
344
+ resolve(resultText || fullText || "");
345
+ }, timeoutMs);
346
+ proc.on("exit", () => {
347
+ clearTimeout(timeout);
348
+ resolve(resultText || fullText || "");
349
+ });
350
+ proc.on("error", () => {
351
+ clearTimeout(timeout);
352
+ resolve("");
353
+ });
354
+ });
355
+ }
356
+ /** Analyst prompt templates */
357
+ const CODEBASE_ANALYST_PROMPT = `You are analyzing a codebase to help plan a development task.
358
+ Use Glob and Read to explore the repository structure.
359
+ Report:
360
+ 1. Key directories and their purposes
361
+ 2. Frameworks, languages, and patterns used
362
+ 3. Existing test patterns and locations
363
+ 4. CI/CD configuration
364
+ 5. Key configuration files (.env, tsconfig, etc.)
365
+ Keep your report under 2000 words. Focus on facts, not opinions.`;
366
+ function makeRequirementsAnalystPrompt(task) {
367
+ return `Given this task description:
368
+
369
+ Title: ${task.summary}
370
+ ${task.description ? `\nDescription:\n${task.description}` : ""}
371
+
372
+ Analyze the requirements and report:
373
+ 1. Explicit acceptance criteria (what MUST be done)
374
+ 2. Implicit requirements (what's assumed but not stated)
375
+ 3. Ambiguities that could lead to wrong implementation
376
+ 4. Affected components based on the requirement scope
377
+ 5. Suggested personas for each component
378
+ Keep your report under 1500 words.`;
379
+ }
380
+ function makeRiskAssessorPrompt(task) {
381
+ return `You are assessing risks for a development task on this codebase.
382
+ The task: ${task.summary}
383
+ ${task.description ? `\nDescription:\n${task.description}` : ""}
384
+
385
+ Use Grep and Read to check for potential blockers.
386
+ Report:
387
+ 1. Files likely to be modified (search for relevant code)
388
+ 2. Files that are heavily coupled (imports/dependencies)
389
+ 3. Existing tests that may need updating
390
+ 4. Environment/config dependencies
391
+ 5. Migration or deployment considerations
392
+ Keep your report under 1500 words.`;
393
+ }
394
+ /**
395
+ * Run team planning: spawn 3 parallel analyst agents, then synthesize
396
+ * their reports into an enhanced planning prompt for the final planner.
397
+ *
398
+ * Falls back to single-agent planning if anything goes wrong.
399
+ */
400
+ async function runTeamPlanning(task, basePrompt, claudePath, model, env, repoPath, taskId, startTime) {
401
+ const taskLabel = chalk.cyan(taskId.slice(0, 8));
402
+ console.log(`${ts()} ${taskLabel} ${chalk.magenta("◆ Team planning")} — running 3 analysts in parallel...`);
403
+ await postLog(taskId, `${PREFIX} Team planning: running codebase, requirements, and risk analysts in parallel...`);
404
+ await postProgress(taskId, "reading_repo", Math.round((Date.now() - startTime) / 1000), "Running parallel analysis agents...", 0, 0);
405
+ const analysisModel = model.includes("opus") ? "sonnet" : model;
406
+ const [codebaseResult, requirementsResult, riskResult] = await Promise.allSettled([
407
+ runAnalyst(claudePath, analysisModel, CODEBASE_ANALYST_PROMPT, repoPath, env),
408
+ runAnalyst(claudePath, analysisModel, makeRequirementsAnalystPrompt(task), repoPath, env),
409
+ runAnalyst(claudePath, analysisModel, makeRiskAssessorPrompt(task), repoPath, env),
410
+ ]);
411
+ const codebaseReport = codebaseResult.status === "fulfilled" ? codebaseResult.value : "";
412
+ const requirementsReport = requirementsResult.status === "fulfilled" ? requirementsResult.value : "";
413
+ const riskReport = riskResult.status === "fulfilled" ? riskResult.value : "";
414
+ const successCount = [codebaseReport, requirementsReport, riskReport].filter((r) => r.length > 0).length;
415
+ const analysisElapsed = Math.round((Date.now() - startTime) / 1000);
416
+ console.log(`${ts()} ${taskLabel} ${chalk.green("✓")} Analysis complete: ${successCount}/3 reports (${analysisElapsed}s)`);
417
+ await postLog(taskId, `${PREFIX} Team analysis complete: ${successCount}/3 reports in ${formatElapsed(analysisElapsed)}. Synthesizing plan...`);
418
+ await postProgress(taskId, "analyzing", analysisElapsed, "Synthesizing analysis reports...", 0, 0);
419
+ // Build enhanced prompt with analysis reports
420
+ const sections = [];
421
+ if (codebaseReport) {
422
+ sections.push(`## Codebase Analysis (from automated analysis)\n\n${codebaseReport}`);
423
+ }
424
+ if (requirementsReport) {
425
+ sections.push(`## Requirements Analysis\n\n${requirementsReport}`);
426
+ }
427
+ if (riskReport) {
428
+ sections.push(`## Risk Assessment\n\n${riskReport}`);
429
+ }
430
+ if (sections.length === 0) {
431
+ // All analysts failed — fall through to regular planning
432
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} All analysts failed, falling back to single-agent planning`);
433
+ await postLog(taskId, `${PREFIX} All analysis agents failed — falling back to single-agent planning`);
434
+ return runClaudeCli(claudePath, model, basePrompt, env, taskId, startTime);
435
+ }
436
+ const enhancedPrompt = basePrompt +
437
+ "\n\n" +
438
+ sections.join("\n\n") +
439
+ "\n\n" +
440
+ "Use these analyses to produce a more accurate execution plan.\n" +
441
+ "Prefer actual file paths discovered in the codebase analysis over guessed paths.";
442
+ // Run the final synthesizer planner with the enhanced prompt
443
+ return runClaudeCli(claudePath, model, enhancedPrompt, env, taskId, startTime);
444
+ }
231
445
  /**
232
446
  * Run planning for a task with Planner-Critic validation loop.
233
447
  *
@@ -240,7 +454,7 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
240
454
  * 6. If critic rejects: re-run planner with feedback (up to MAX_ITERATIONS)
241
455
  * 7. After MAX_ITERATIONS without approval: fail the task
242
456
  */
243
- export async function planTask(task, config) {
457
+ export async function planTask(task, config, credentials) {
244
458
  const taskLabel = chalk.cyan(task.id.slice(0, 8));
245
459
  console.log(`${ts()} ${taskLabel} Fetching planning prompt...`);
246
460
  await postLog(task.id, `${PREFIX} Fetching planning prompt from cloud API...`);
@@ -248,14 +462,34 @@ export async function planTask(task, config) {
248
462
  const promptResponse = await api.get("/api/agent/planning-prompt", {
249
463
  params: { taskId: task.id },
250
464
  });
251
- const { prompt: basePrompt, model } = promptResponse.data;
465
+ const { prompt: basePrompt, model, provider: planningProvider } = promptResponse.data;
252
466
  const cliModel = model || "sonnet";
467
+ const provider = (planningProvider || "anthropic");
468
+ const isAnthropicPlanning = provider === "anthropic";
253
469
  const claudePath = process.env.CLAUDE_CLI_PATH || findClaudePath() || "claude";
254
470
  const cleanEnv = { ...process.env };
255
471
  delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
472
+ // Resolve provider API key for non-Anthropic planning
473
+ const providerApiKey = resolveProviderApiKey(provider, credentials);
256
474
  const startTime = Date.now();
257
475
  // PRD for critic validation: use task description, fall back to summary
258
476
  const prd = task.description || task.summary;
477
+ // Clone repo for team planning if enabled
478
+ let repoPath = null;
479
+ if (isAnthropicPlanning && config.teamPlanningEnabled && task.githubRepo) {
480
+ const scmProvider = task.scmProvider || "github";
481
+ const scmToken = scmProvider === "bitbucket"
482
+ ? config.bitbucketToken
483
+ : scmProvider === "gitlab"
484
+ ? config.gitlabToken
485
+ : config.githubToken;
486
+ if (scmToken) {
487
+ repoPath = await cloneTargetRepo(task.githubRepo, scmToken, scmProvider, task.id);
488
+ }
489
+ else {
490
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} No SCM token for ${scmProvider}, skipping team planning`);
491
+ }
492
+ }
259
493
  // 2. Planner-Critic iteration loop
260
494
  let currentPrompt = basePrompt;
261
495
  let bestPlan = null;
@@ -263,136 +497,167 @@ export async function planTask(task, config) {
263
497
  // Track critic history across iterations for analytics
264
498
  const criticHistory = [];
265
499
  let totalFileCapTruncations = 0;
266
- for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
267
- const iterLabel = MAX_ITERATIONS > 1 ? ` (attempt ${iteration}/${MAX_ITERATIONS})` : "";
268
- if (iteration > 1) {
269
- console.log(`${ts()} ${taskLabel} Running Claude CLI${iterLabel} ${chalk.dim(`(model: ${chalk.yellow(cliModel)})`)}`);
270
- await postLog(task.id, `${PREFIX} Re-planning${iterLabel} using anthropic/${cliModel}`);
271
- }
272
- else {
273
- console.log(`${ts()} ${taskLabel} Running Claude CLI ${chalk.dim(`(model: ${chalk.yellow(cliModel)})`)}`);
274
- await postLog(task.id, `${PREFIX} Starting planning agent using anthropic/${cliModel}`);
275
- }
276
- // 2a. Run Claude CLI to generate plan
277
- let rawOutput;
278
- try {
279
- rawOutput = await runClaudeCli(claudePath, cliModel, currentPrompt, cleanEnv, task.id, startTime);
280
- }
281
- catch (error) {
500
+ try {
501
+ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
502
+ const iterLabel = MAX_ITERATIONS > 1 ? ` (attempt ${iteration}/${MAX_ITERATIONS})` : "";
503
+ const providerLabel = `${provider}/${cliModel}`;
504
+ if (iteration > 1) {
505
+ console.log(`${ts()} ${taskLabel} Running planner${iterLabel} ${chalk.dim(`(${chalk.yellow(providerLabel)})`)}`);
506
+ await postLog(task.id, `${PREFIX} Re-planning${iterLabel} using ${providerLabel}`);
507
+ }
508
+ else {
509
+ console.log(`${ts()} ${taskLabel} Running planner ${chalk.dim(`(${chalk.yellow(providerLabel)})`)}`);
510
+ await postLog(task.id, `${PREFIX} Starting planning agent using ${providerLabel}`);
511
+ }
512
+ // 2a. Generate plan via Claude CLI (Anthropic) or HTTP API (other providers)
513
+ let rawOutput;
514
+ try {
515
+ if (isAnthropicPlanning && config.teamPlanningEnabled && repoPath && iteration === 1) {
516
+ rawOutput = await runTeamPlanning(task, currentPrompt, claudePath, cliModel, cleanEnv, repoPath, task.id, startTime);
517
+ }
518
+ else if (isAnthropicPlanning) {
519
+ rawOutput = await runClaudeCli(claudePath, cliModel, currentPrompt, cleanEnv, task.id, startTime);
520
+ }
521
+ else {
522
+ if (!providerApiKey) {
523
+ throw new Error(`No API key available for provider "${provider}". Configure it in Settings > Integrations.`);
524
+ }
525
+ const genStart = Math.round((Date.now() - startTime) / 1000);
526
+ await postProgress(task.id, "generating_plan", genStart, "Generating plan via API...", 0, 0);
527
+ rawOutput = await generateText(provider, cliModel, currentPrompt, providerApiKey);
528
+ // Post "validating" phase so the dashboard progress bar transitions correctly
529
+ const genEnd = Math.round((Date.now() - startTime) / 1000);
530
+ await postProgress(task.id, "validating", genEnd, "Validating plan...", rawOutput.length, 0);
531
+ }
532
+ }
533
+ catch (error) {
534
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
535
+ const errMsg = error instanceof Error ? error.message : String(error);
536
+ console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Failed after ${elapsed}s: ${errMsg.substring(0, 100)}`);
537
+ await postLog(task.id, `${PREFIX} Planning failed after ${formatElapsed(elapsed)}: ${errMsg.substring(0, 200)}`, "error", "error");
538
+ return false;
539
+ }
282
540
  const elapsed = Math.round((Date.now() - startTime) / 1000);
283
- const errMsg = error instanceof Error ? error.message : String(error);
284
- console.error(`${ts()} ${taskLabel} ${chalk.red("")} Failed after ${elapsed}s: ${errMsg.substring(0, 100)}`);
285
- await postLog(task.id, `${PREFIX} Planning failed after ${formatElapsed(elapsed)}: ${errMsg.substring(0, 200)}`, "error", "error");
286
- return false;
287
- }
288
- const elapsed = Math.round((Date.now() - startTime) / 1000);
289
- console.log(`${ts()} ${taskLabel} ${chalk.green("✓")} Claude CLI done ${chalk.dim(`(${elapsed}s, ${rawOutput.length} chars)`)}`);
290
- // 2b. Parse plan from raw output
291
- let plan;
292
- try {
293
- plan = parseExecutionPlan(rawOutput);
294
- }
295
- catch (error) {
296
- const errMsg = error instanceof Error ? error.message : String(error);
297
- console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Plan parse failed: ${errMsg.substring(0, 100)}`);
298
- await postLog(task.id, `${PREFIX} Failed to parse execution plan from Claude output: ${errMsg.substring(0, 200)}`, "error", "error");
299
- // If we can't parse the plan, post raw output and let server-side try
300
- return await postRawPlan(task.id, rawOutput, config.agentId, taskLabel, elapsed);
301
- }
302
- // 2c. Apply file cap (max 5 files per story)
303
- const { truncatedCount, details } = applyFileCap(plan);
304
- if (truncatedCount > 0) {
305
- totalFileCapTruncations += truncatedCount;
306
- const msg = `${PREFIX} File cap applied: ${truncatedCount} stories truncated to max 5 targetFiles`;
307
- console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
308
- await postLog(task.id, msg);
309
- for (const detail of details) {
310
- console.log(`${ts()} ${taskLabel} ${chalk.dim(detail)}`);
541
+ const doneLabel = isAnthropicPlanning ? "Claude CLI" : `${provider} API`;
542
+ console.log(`${ts()} ${taskLabel} ${chalk.green("")} ${doneLabel} done ${chalk.dim(`(${elapsed}s, ${rawOutput.length} chars)`)}`);
543
+ // 2b. Parse plan from raw output
544
+ let plan;
545
+ try {
546
+ plan = parseExecutionPlan(rawOutput);
311
547
  }
312
- }
313
- console.log(`${ts()} ${taskLabel} Plan: ${chalk.bold(plan.stories.length)} stories`);
314
- await postLog(task.id, `${PREFIX} Plan generated: ${plan.stories.length} stories (${formatElapsed(elapsed)}). Running critic validation...`);
315
- // 2d. Run critic validation
316
- const criticResult = await runCriticValidation(claudePath, cliModel, prd, plan, cleanEnv, taskLabel);
317
- // Track best plan across iterations
318
- if (criticResult && criticResult.score > bestScore) {
319
- bestPlan = plan;
320
- bestScore = criticResult.score;
321
- }
322
- else if (!criticResult && !bestPlan) {
323
- // Critic failed entirely — use this plan as fallback
324
- bestPlan = plan;
325
- }
326
- // Record critic history for this iteration
327
- if (criticResult) {
328
- criticHistory.push({
329
- iteration,
330
- score: criticResult.score,
331
- approved: criticResult.approved || criticResult.score >= AUTO_APPROVAL_THRESHOLD,
332
- risks: criticResult.risks,
333
- suggestions: criticResult.suggestions,
334
- filesCapApplied: truncatedCount > 0 ? truncatedCount : undefined,
335
- });
336
- }
337
- // 2e. Check critic result
338
- if (!criticResult) {
339
- // Critic failed (timeout, parse error, etc.) — post plan without critic gate
340
- const msg = `${PREFIX} Critic validation failed — posting plan without critic score`;
341
- console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
342
- await postLog(task.id, msg);
343
- const planningDurationMs = Date.now() - startTime;
344
- return await postValidatedPlan(task.id, plan, config.agentId, taskLabel, elapsed, undefined, undefined, criticHistory, totalFileCapTruncations, planningDurationMs, iteration);
345
- }
346
- if (criticResult.approved || criticResult.score >= AUTO_APPROVAL_THRESHOLD) {
347
- // Approved! Post the file-capped plan
348
- const msg = `${PREFIX} Critic approved (score: ${criticResult.score}/100)`;
349
- console.log(`${ts()} ${taskLabel} ${chalk.green("✓")} ${msg}`);
350
- await postLog(task.id, msg);
351
- if (criticResult.risks.length > 0) {
352
- const risksMsg = `${PREFIX} Critic risks (non-blocking): ${criticResult.risks.join("; ")}`;
353
- console.log(`${ts()} ${taskLabel} ${chalk.dim(risksMsg)}`);
354
- await postLog(task.id, risksMsg);
548
+ catch (error) {
549
+ const errMsg = error instanceof Error ? error.message : String(error);
550
+ console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Plan parse failed: ${errMsg.substring(0, 100)}`);
551
+ await postLog(task.id, `${PREFIX} Failed to parse execution plan from Claude output: ${errMsg.substring(0, 200)}`, "error", "error");
552
+ // If we can't parse the plan, post raw output and let server-side try
553
+ return await postRawPlan(task.id, rawOutput, config.agentId, taskLabel, elapsed);
355
554
  }
356
- const planningDurationMs = Date.now() - startTime;
357
- return await postValidatedPlan(task.id, plan, config.agentId, taskLabel, elapsed, criticResult.score, criticResult.risks, criticHistory, totalFileCapTruncations, planningDurationMs, iteration);
358
- }
359
- // 2f. Rejected — append critic feedback for next iteration
360
- if (iteration < MAX_ITERATIONS) {
361
- const feedback = formatCriticFeedback(criticResult);
362
- currentPrompt = basePrompt + "\n\n" + feedback;
363
- const msg = `${PREFIX} Critic rejected (score: ${criticResult.score}/100, threshold: ${AUTO_APPROVAL_THRESHOLD}). Re-planning with feedback...`;
364
- console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
365
- await postLog(task.id, msg);
366
- if (criticResult.risks.length > 0) {
367
- const risksMsg = `${PREFIX} Critic risks: ${criticResult.risks.join("; ")}`;
368
- console.log(`${ts()} ${taskLabel} ${chalk.dim(risksMsg)}`);
369
- await postLog(task.id, risksMsg);
555
+ // 2c. Apply file cap (max 5 files per story)
556
+ const { truncatedCount, details } = applyFileCap(plan);
557
+ if (truncatedCount > 0) {
558
+ totalFileCapTruncations += truncatedCount;
559
+ const msg = `${PREFIX} File cap applied: ${truncatedCount} stories truncated to max 5 targetFiles`;
560
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
561
+ await postLog(task.id, msg);
562
+ for (const detail of details) {
563
+ console.log(`${ts()} ${taskLabel} ${chalk.dim(detail)}`);
564
+ }
565
+ }
566
+ console.log(`${ts()} ${taskLabel} Plan: ${chalk.bold(plan.stories.length)} stories`);
567
+ await postLog(task.id, `${PREFIX} Plan generated: ${plan.stories.length} stories (${formatElapsed(elapsed)}). Running critic validation...`);
568
+ // 2d. Run critic validation
569
+ const criticResult = await runCriticValidation(claudePath, cliModel, prd, plan, cleanEnv, taskLabel, provider, providerApiKey);
570
+ // Track best plan across iterations
571
+ if (criticResult && criticResult.score > bestScore) {
572
+ bestPlan = plan;
573
+ bestScore = criticResult.score;
370
574
  }
371
- if (criticResult.suggestions && criticResult.suggestions.length > 0) {
372
- const sugMsg = `${PREFIX} Critic suggestions: ${criticResult.suggestions.join("; ")}`;
373
- console.log(`${ts()} ${taskLabel} ${chalk.dim(sugMsg)}`);
374
- await postLog(task.id, sugMsg);
575
+ else if (!criticResult && !bestPlan) {
576
+ // Critic failed entirely use this plan as fallback
577
+ bestPlan = plan;
578
+ }
579
+ // Record critic history for this iteration
580
+ if (criticResult) {
581
+ criticHistory.push({
582
+ iteration,
583
+ score: criticResult.score,
584
+ approved: criticResult.approved || criticResult.score >= AUTO_APPROVAL_THRESHOLD,
585
+ risks: criticResult.risks,
586
+ suggestions: criticResult.suggestions,
587
+ filesCapApplied: truncatedCount > 0 ? truncatedCount : undefined,
588
+ });
589
+ }
590
+ // 2e. Check critic result
591
+ if (!criticResult) {
592
+ // Critic failed (timeout, parse error, etc.) — post plan without critic gate
593
+ const msg = `${PREFIX} Critic validation failed — posting plan without critic score`;
594
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
595
+ await postLog(task.id, msg);
596
+ const planningDurationMs = Date.now() - startTime;
597
+ return await postValidatedPlan(task.id, plan, config.agentId, taskLabel, elapsed, undefined, undefined, criticHistory, totalFileCapTruncations, planningDurationMs, iteration);
598
+ }
599
+ if (criticResult.approved || criticResult.score >= AUTO_APPROVAL_THRESHOLD) {
600
+ // Approved! Post the file-capped plan
601
+ const msg = `${PREFIX} Critic approved (score: ${criticResult.score}/100)`;
602
+ console.log(`${ts()} ${taskLabel} ${chalk.green("✓")} ${msg}`);
603
+ await postLog(task.id, msg);
604
+ if (criticResult.risks.length > 0) {
605
+ const risksMsg = `${PREFIX} Critic risks (non-blocking): ${criticResult.risks.join("; ")}`;
606
+ console.log(`${ts()} ${taskLabel} ${chalk.dim(risksMsg)}`);
607
+ await postLog(task.id, risksMsg);
608
+ }
609
+ const planningDurationMs = Date.now() - startTime;
610
+ return await postValidatedPlan(task.id, plan, config.agentId, taskLabel, elapsed, criticResult.score, criticResult.risks, criticHistory, totalFileCapTruncations, planningDurationMs, iteration);
611
+ }
612
+ // 2f. Rejected — append critic feedback for next iteration
613
+ if (iteration < MAX_ITERATIONS) {
614
+ const feedback = formatCriticFeedback(criticResult);
615
+ currentPrompt = basePrompt + "\n\n" + feedback;
616
+ const msg = `${PREFIX} Critic rejected (score: ${criticResult.score}/100, threshold: ${AUTO_APPROVAL_THRESHOLD}). Re-planning with feedback...`;
617
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
618
+ await postLog(task.id, msg);
619
+ if (criticResult.risks.length > 0) {
620
+ const risksMsg = `${PREFIX} Critic risks: ${criticResult.risks.join("; ")}`;
621
+ console.log(`${ts()} ${taskLabel} ${chalk.dim(risksMsg)}`);
622
+ await postLog(task.id, risksMsg);
623
+ }
624
+ if (criticResult.suggestions && criticResult.suggestions.length > 0) {
625
+ const sugMsg = `${PREFIX} Critic suggestions: ${criticResult.suggestions.join("; ")}`;
626
+ console.log(`${ts()} ${taskLabel} ${chalk.dim(sugMsg)}`);
627
+ await postLog(task.id, sugMsg);
628
+ }
629
+ }
630
+ else {
631
+ // Final iteration — rejected
632
+ const msg = `${PREFIX} Critic rejected after ${MAX_ITERATIONS} iterations (best score: ${bestScore}/100, threshold: ${AUTO_APPROVAL_THRESHOLD})`;
633
+ console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} ${msg}`);
634
+ await postLog(task.id, msg, "error", "error");
635
+ if (criticResult.risks.length > 0) {
636
+ const risksMsg = `${PREFIX} Final risks: ${criticResult.risks.join("; ")}`;
637
+ console.error(`${ts()} ${taskLabel} ${risksMsg}`);
638
+ await postLog(task.id, risksMsg, "error", "error");
639
+ }
640
+ if (criticResult.suggestions && criticResult.suggestions.length > 0) {
641
+ const sugMsg = `${PREFIX} Suggestions: ${criticResult.suggestions.join("; ")}`;
642
+ console.error(`${ts()} ${taskLabel} ${sugMsg}`);
643
+ await postLog(task.id, sugMsg, "error", "error");
644
+ }
375
645
  }
376
646
  }
377
- else {
378
- // Final iteration — rejected
379
- const msg = `${PREFIX} Critic rejected after ${MAX_ITERATIONS} iterations (best score: ${bestScore}/100, threshold: ${AUTO_APPROVAL_THRESHOLD})`;
380
- console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} ${msg}`);
381
- await postLog(task.id, msg, "error", "error");
382
- if (criticResult.risks.length > 0) {
383
- const risksMsg = `${PREFIX} Final risks: ${criticResult.risks.join("; ")}`;
384
- console.error(`${ts()} ${taskLabel} ${risksMsg}`);
385
- await postLog(task.id, risksMsg, "error", "error");
647
+ // All iterations exhausted — fail
648
+ return false;
649
+ }
650
+ finally {
651
+ // Cleanup temp clone
652
+ if (repoPath) {
653
+ try {
654
+ execSync(`rm -rf "${repoPath}"`, { stdio: "ignore" });
386
655
  }
387
- if (criticResult.suggestions && criticResult.suggestions.length > 0) {
388
- const sugMsg = `${PREFIX} Suggestions: ${criticResult.suggestions.join("; ")}`;
389
- console.error(`${ts()} ${taskLabel} ${sugMsg}`);
390
- await postLog(task.id, sugMsg, "error", "error");
656
+ catch {
657
+ /* ignore */
391
658
  }
392
659
  }
393
660
  }
394
- // All iterations exhausted — fail
395
- return false;
396
661
  }
397
662
  /**
398
663
  * Post a validated (file-capped) plan to the cloud API.
package/dist/poller.js CHANGED
@@ -7,7 +7,7 @@
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, 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)
@@ -76,7 +76,8 @@ async function pollOnce(config) {
76
76
  * Handle a task in "planning" status.
77
77
  */
78
78
  async function handlePlanningTask(task, config) {
79
- // Claim the task
79
+ // Claim the task (also returns org credentials for provider API keys)
80
+ let credentials;
80
81
  try {
81
82
  const claimResponse = await api.post("/api/agent/claim", {
82
83
  taskId: task.id,
@@ -85,6 +86,7 @@ async function handlePlanningTask(task, config) {
85
86
  if (!claimResponse.data.claimed) {
86
87
  return; // Another agent or cloud orchestrator claimed it
87
88
  }
89
+ credentials = claimResponse.data.credentials;
88
90
  }
89
91
  catch {
90
92
  return;
@@ -94,7 +96,7 @@ async function handlePlanningTask(task, config) {
94
96
  console.log(`${ts()} ${chalk.magenta("◆ PLANNING")} ${taskLabel} ${task.summary.substring(0, 60)}`);
95
97
  planningInProgress.add(task.id);
96
98
  // Run planning asynchronously (don't block the poll loop)
97
- planTask(task, config)
99
+ planTask(task, config, credentials)
98
100
  .then((success) => {
99
101
  if (success) {
100
102
  console.log(`${ts()} ${chalk.green("✓")} Planning complete for ${taskLabel}`);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Lightweight HTTP wrappers for text generation across AI providers.
3
+ *
4
+ * Used for planning and critic stages when the org's provider is not Anthropic.
5
+ * No heavy SDK dependencies — uses the existing axios dependency for HTTP calls.
6
+ * Streaming is not needed (planning/critic are text-only, result matters).
7
+ */
8
+ export type AIProvider = "anthropic" | "openai" | "google" | "ollama";
9
+ export interface GenerateTextOptions {
10
+ maxTokens?: number;
11
+ temperature?: number;
12
+ timeoutMs?: number;
13
+ }
14
+ /**
15
+ * Generate text from any supported AI provider via direct HTTP API calls.
16
+ * Returns the raw text response.
17
+ */
18
+ export declare function generateText(provider: AIProvider, model: string, prompt: string, apiKey: string, options?: GenerateTextOptions): Promise<string>;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Lightweight HTTP wrappers for text generation across AI providers.
3
+ *
4
+ * Used for planning and critic stages when the org's provider is not Anthropic.
5
+ * No heavy SDK dependencies — uses the existing axios dependency for HTTP calls.
6
+ * Streaming is not needed (planning/critic are text-only, result matters).
7
+ */
8
+ import axios from "axios";
9
+ const DEFAULT_MAX_TOKENS = 16384;
10
+ const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes (matches Claude CLI timeout)
11
+ /**
12
+ * Generate text from any supported AI provider via direct HTTP API calls.
13
+ * Returns the raw text response.
14
+ */
15
+ export async function generateText(provider, model, prompt, apiKey, options) {
16
+ const maxTokens = options?.maxTokens ?? DEFAULT_MAX_TOKENS;
17
+ const temperature = options?.temperature ?? 0.7;
18
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
19
+ switch (provider) {
20
+ case "anthropic":
21
+ return generateAnthropic(model, prompt, apiKey, maxTokens, temperature, timeoutMs);
22
+ case "openai":
23
+ return generateOpenAI(model, prompt, apiKey, maxTokens, temperature, timeoutMs);
24
+ case "google":
25
+ return generateGoogle(model, prompt, apiKey, maxTokens, temperature, timeoutMs);
26
+ case "ollama":
27
+ return generateOllama(model, prompt, apiKey, maxTokens, temperature, timeoutMs);
28
+ default:
29
+ throw new Error(`Unsupported AI provider: ${provider}`);
30
+ }
31
+ }
32
+ /**
33
+ * Anthropic Messages API (for orgs with API key, not CLI).
34
+ */
35
+ async function generateAnthropic(model, prompt, apiKey, maxTokens, temperature, timeoutMs) {
36
+ const response = await axios.post("https://api.anthropic.com/v1/messages", {
37
+ model,
38
+ max_tokens: maxTokens,
39
+ temperature,
40
+ messages: [{ role: "user", content: prompt }],
41
+ }, {
42
+ headers: {
43
+ "x-api-key": apiKey,
44
+ "anthropic-version": "2023-06-01",
45
+ "Content-Type": "application/json",
46
+ },
47
+ timeout: timeoutMs,
48
+ });
49
+ const content = response.data?.content;
50
+ if (Array.isArray(content)) {
51
+ return content
52
+ .filter((block) => block.type === "text")
53
+ .map((block) => block.text)
54
+ .join("");
55
+ }
56
+ throw new Error("Unexpected Anthropic API response format");
57
+ }
58
+ /**
59
+ * OpenAI Chat Completions API.
60
+ */
61
+ async function generateOpenAI(model, prompt, apiKey, maxTokens, temperature, timeoutMs) {
62
+ const response = await axios.post("https://api.openai.com/v1/chat/completions", {
63
+ model,
64
+ max_tokens: maxTokens,
65
+ temperature,
66
+ messages: [{ role: "user", content: prompt }],
67
+ }, {
68
+ headers: {
69
+ Authorization: `Bearer ${apiKey}`,
70
+ "Content-Type": "application/json",
71
+ },
72
+ timeout: timeoutMs,
73
+ });
74
+ const message = response.data?.choices?.[0]?.message;
75
+ if (message?.content)
76
+ return message.content;
77
+ throw new Error("Unexpected OpenAI API response format");
78
+ }
79
+ /**
80
+ * Google Generative AI (Gemini) API.
81
+ */
82
+ async function generateGoogle(model, prompt, apiKey, maxTokens, temperature, timeoutMs) {
83
+ const response = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
84
+ contents: [{ parts: [{ text: prompt }] }],
85
+ generationConfig: {
86
+ maxOutputTokens: maxTokens,
87
+ temperature,
88
+ },
89
+ }, {
90
+ headers: { "Content-Type": "application/json" },
91
+ timeout: timeoutMs,
92
+ });
93
+ const candidate = response.data?.candidates?.[0];
94
+ const text = candidate?.content?.parts?.[0]?.text;
95
+ if (text)
96
+ return text;
97
+ throw new Error("Unexpected Google AI API response format");
98
+ }
99
+ /**
100
+ * Ollama Generate API (self-hosted).
101
+ * `apiKey` is used as the Ollama base URL (e.g. "http://localhost:11434").
102
+ */
103
+ async function generateOllama(model, prompt, ollamaHost, _maxTokens, temperature, timeoutMs) {
104
+ const baseUrl = ollamaHost || "http://localhost:11434";
105
+ const response = await axios.post(`${baseUrl}/api/generate`, {
106
+ model,
107
+ prompt,
108
+ stream: false,
109
+ options: { temperature },
110
+ }, {
111
+ headers: { "Content-Type": "application/json" },
112
+ timeout: timeoutMs,
113
+ });
114
+ const text = response.data?.response;
115
+ if (text)
116
+ return text;
117
+ throw new Error("Unexpected Ollama API response format");
118
+ }
package/dist/spawner.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface SpawnableTask {
14
14
  description: string | null;
15
15
  jiraIssueKey: string | null;
16
16
  workerModel: string;
17
+ workerProvider?: string;
17
18
  githubRepo: string;
18
19
  scmProvider: string;
19
20
  skipManagerReview?: boolean;
@@ -34,6 +35,10 @@ export interface ClaimCredentials {
34
35
  customerAwsRegion?: string;
35
36
  issueTrackerProvider?: string;
36
37
  bitbucketEmail?: string;
38
+ anthropicApiKey?: string;
39
+ openaiApiKey?: string;
40
+ googleApiKey?: string;
41
+ ollamaBaseUrl?: string;
37
42
  }
38
43
  /**
39
44
  * Spawn a Docker worker container for a task.
package/dist/spawner.js CHANGED
@@ -132,17 +132,20 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
132
132
  else {
133
133
  dockerArgs.push("--network", "host");
134
134
  }
135
- // Mount Claude credentials
135
+ // Mount Claude credentials (required for Anthropic workers, optional for others)
136
+ const workerProvider = task.workerProvider || "anthropic";
136
137
  const claudeConfigDir = findClaudeConfigDir();
137
- if (!claudeConfigDir) {
138
+ if (!claudeConfigDir && workerProvider === "anthropic") {
138
139
  console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Claude credentials not found. Run 'claude' and complete the sign-in flow.`);
139
140
  return;
140
141
  }
141
- // Copy credentials to a temp dir with relaxed permissions for container access
142
- // (avoids weakening permissions on the user's actual credentials file)
143
- const credFile = path.join(claudeConfigDir, ".credentials.json");
144
- const dockerClaudeDir = toDockerPath(claudeConfigDir);
145
- dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
142
+ if (claudeConfigDir) {
143
+ const dockerClaudeDir = toDockerPath(claudeConfigDir);
144
+ dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
145
+ }
146
+ else {
147
+ console.log(`${ts()} ${taskLabel} ${chalk.dim("Skipping Claude mount (non-Anthropic worker)")}`);
148
+ }
146
149
  // Build environment variables — KEY DIFFERENCE: API_BASE_URL points to cloud
147
150
  const scmProvider = (task.scmProvider || "github");
148
151
  const scmToken = getScmToken(scmProvider, config);
@@ -193,8 +196,13 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
193
196
  BITBUCKET_EMAIL: credentials?.bitbucketEmail || "",
194
197
  // Task notes from dashboard
195
198
  TASK_NOTES: task.taskNotes || "",
196
- // Anthropic API key (if available)
197
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "",
199
+ // AI provider configuration
200
+ ANTHROPIC_API_KEY: credentials?.anthropicApiKey || process.env.ANTHROPIC_API_KEY || "",
201
+ WORKER_PROVIDER: task.workerProvider || "anthropic",
202
+ OPENAI_API_KEY: credentials?.openaiApiKey || "",
203
+ GOOGLE_API_KEY: credentials?.googleApiKey || "",
204
+ GOOGLE_GENERATIVE_AI_API_KEY: credentials?.googleApiKey || "",
205
+ OLLAMA_HOST: credentials?.ollamaBaseUrl || "",
198
206
  // Resilience settings from org config
199
207
  BLOCKER_MAX_AUTO_RETRIES: String(orgConfig.blockerMaxAutoRetries ?? 3),
200
208
  BLOCKER_AUTO_RETRY_ENABLED: orgConfig.blockerAutoRetryEnabled !== false ? "true" : "false",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",