workermill 0.3.2 → 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.
package/dist/index.js CHANGED
@@ -24,101 +24,312 @@ import readline from "readline";
24
24
  import { execSync } from "child_process";
25
25
  import chalk from "chalk";
26
26
  var PROVIDERS = [
27
- { name: "ollama", display: "Ollama (local, no API key needed)", needsKey: false, defaultModel: "qwen3-coder:30b" },
28
- { name: "anthropic", display: "Anthropic (Claude)", needsKey: true, defaultModel: "claude-sonnet-4-6", envVar: "ANTHROPIC_API_KEY" },
29
- { name: "openai", display: "OpenAI (GPT)", needsKey: true, defaultModel: "gpt-5.4", envVar: "OPENAI_API_KEY" },
30
- { name: "google", display: "Google (Gemini)", needsKey: true, defaultModel: "gemini-3.1-pro", envVar: "GOOGLE_GENERATIVE_AI_API_KEY" }
27
+ {
28
+ name: "ollama",
29
+ display: "Ollama (local, no API key)",
30
+ needsKey: false,
31
+ models: [
32
+ { id: "qwen3-coder:30b", label: "Qwen 3 Coder 30B (recommended)" },
33
+ { id: "qwen2.5-coder:32b", label: "Qwen 2.5 Coder 32B" }
34
+ ]
35
+ },
36
+ {
37
+ name: "anthropic",
38
+ display: "Anthropic (Claude)",
39
+ needsKey: true,
40
+ envVar: "ANTHROPIC_API_KEY",
41
+ models: [
42
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (recommended)" },
43
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6 (most powerful)" }
44
+ ]
45
+ },
46
+ {
47
+ name: "openai",
48
+ display: "OpenAI",
49
+ needsKey: true,
50
+ envVar: "OPENAI_API_KEY",
51
+ models: [
52
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex (built for code)" },
53
+ { id: "gpt-5", label: "GPT-5 (general flagship)" }
54
+ ]
55
+ },
56
+ {
57
+ name: "google",
58
+ display: "Google (Gemini)",
59
+ needsKey: true,
60
+ envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
61
+ models: [
62
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro (most powerful)" },
63
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash (fast, good value)" }
64
+ ]
65
+ }
31
66
  ];
32
67
  function ask(rl, question) {
33
68
  return new Promise((resolve) => rl.question(question, resolve));
34
69
  }
35
- async function runSetup() {
36
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
- console.log();
38
- console.log(chalk.bold(" WorkerMill CLI") + chalk.dim(" \u2014 AI coding agent"));
39
- console.log();
40
- console.log(chalk.dim(" No provider configured. Let's set one up."));
41
- console.log();
42
- console.log(" Provider:");
43
- PROVIDERS.forEach((p, i) => {
70
+ async function pickProvider(rl, label, providers) {
71
+ console.log(` ${chalk.bold(label)}:`);
72
+ providers.forEach((p, i) => {
44
73
  console.log(` ${chalk.cyan(`${i + 1}`)}. ${p.display}`);
45
74
  });
46
75
  console.log();
47
- const choiceStr = await ask(rl, chalk.dim(" Choose (1-4): "));
76
+ const choiceStr = await ask(rl, chalk.dim(` Choose (1-${providers.length}): `));
48
77
  const choice = parseInt(choiceStr.trim(), 10) - 1;
49
- const selected = PROVIDERS[choice] || PROVIDERS[0];
78
+ const selected = providers[choice] || providers[0];
79
+ console.log(chalk.dim(` \u2192 ${selected.display}`));
80
+ return selected;
81
+ }
82
+ async function pickModel(rl, provider) {
50
83
  console.log();
51
- console.log(chalk.dim(` Selected: ${selected.display}`));
52
- const providerConfig = { model: selected.defaultModel };
53
- if (selected.needsKey) {
54
- const envValue = selected.envVar ? process.env[selected.envVar] : void 0;
55
- if (envValue) {
56
- console.log(chalk.green(` \u2713 Found ${selected.envVar} in environment`));
57
- providerConfig.apiKey = `{env:${selected.envVar}}`;
58
- } else {
59
- const key = await ask(rl, chalk.dim(` API key: `));
60
- providerConfig.apiKey = key.trim();
84
+ provider.models.forEach((m, i) => {
85
+ console.log(` ${chalk.cyan(`${i + 1}`)}. ${m.label}`);
86
+ });
87
+ console.log(` ${chalk.cyan(`${provider.models.length + 1}`)}. Custom model`);
88
+ console.log();
89
+ const choiceStr = await ask(rl, chalk.dim(` Choose (1-${provider.models.length + 1}): `));
90
+ const choice = parseInt(choiceStr.trim(), 10) - 1;
91
+ if (choice >= 0 && choice < provider.models.length) {
92
+ const model2 = provider.models[choice].id;
93
+ console.log(chalk.dim(` \u2192 ${model2}`));
94
+ return model2;
95
+ }
96
+ const custom = await ask(rl, chalk.dim(" Model name: "));
97
+ const model = custom.trim() || provider.models[0].id;
98
+ console.log(chalk.dim(` \u2192 ${model}`));
99
+ return model;
100
+ }
101
+ function maskKey(key) {
102
+ if (key.length <= 12) return "\u2022".repeat(key.length);
103
+ return key.slice(0, 6) + "\u2022".repeat(Math.min(key.length - 10, 30)) + key.slice(-4);
104
+ }
105
+ async function getApiKey(rl, provider, existingKeys) {
106
+ if (!provider.needsKey) return void 0;
107
+ if (existingKeys.has(provider.name)) {
108
+ console.log(chalk.green(` \u2713 Reusing ${provider.display} API key from earlier`));
109
+ return existingKeys.get(provider.name);
110
+ }
111
+ const envValue = provider.envVar ? process.env[provider.envVar] : void 0;
112
+ if (envValue) {
113
+ console.log(chalk.green(` \u2713 Found ${provider.envVar} in environment`));
114
+ const key2 = `{env:${provider.envVar}}`;
115
+ existingKeys.set(provider.name, key2);
116
+ return key2;
117
+ }
118
+ const key = await readKeyMasked(rl, chalk.dim(` ${provider.display} API key: `));
119
+ const trimmed = key.trim();
120
+ existingKeys.set(provider.name, trimmed);
121
+ return trimmed;
122
+ }
123
+ function readKeyMasked(rl, prompt) {
124
+ return new Promise((resolve) => {
125
+ let buffer = "";
126
+ let revealed = false;
127
+ process.stdout.write(prompt);
128
+ const wasRaw = process.stdin.isRaw;
129
+ if (process.stdin.isTTY) {
130
+ process.stdin.setRawMode(true);
61
131
  }
132
+ process.stdin.resume();
133
+ const redraw = () => {
134
+ process.stdout.write(`\r\x1B[K${prompt}`);
135
+ if (revealed) {
136
+ process.stdout.write(buffer);
137
+ } else {
138
+ process.stdout.write(maskKey(buffer));
139
+ }
140
+ };
141
+ const onData = (data) => {
142
+ const str = data.toString();
143
+ for (const ch of str) {
144
+ const code = ch.charCodeAt(0);
145
+ if (code === 13 || code === 10) {
146
+ process.stdin.removeListener("data", onData);
147
+ if (process.stdin.isTTY) {
148
+ process.stdin.setRawMode(wasRaw ?? false);
149
+ }
150
+ process.stdout.write(`\r\x1B[K${prompt}`);
151
+ if (buffer.length > 0) {
152
+ process.stdout.write(chalk.green(maskKey(buffer)));
153
+ }
154
+ process.stdout.write("\n");
155
+ resolve(buffer);
156
+ return;
157
+ }
158
+ if (code === 9) {
159
+ revealed = !revealed;
160
+ redraw();
161
+ continue;
162
+ }
163
+ if (code === 3) {
164
+ process.stdin.removeListener("data", onData);
165
+ if (process.stdin.isTTY) {
166
+ process.stdin.setRawMode(wasRaw ?? false);
167
+ }
168
+ process.stdout.write("\n");
169
+ resolve("");
170
+ return;
171
+ }
172
+ if (code === 127 || code === 8) {
173
+ if (buffer.length > 0) {
174
+ buffer = buffer.slice(0, -1);
175
+ redraw();
176
+ }
177
+ continue;
178
+ }
179
+ if (code === 21) {
180
+ buffer = "";
181
+ redraw();
182
+ continue;
183
+ }
184
+ if (code >= 32) {
185
+ buffer += ch;
186
+ redraw();
187
+ }
188
+ }
189
+ };
190
+ process.stdin.on("data", onData);
191
+ });
192
+ }
193
+ async function configureOllama(providerConfig) {
194
+ const hostsToTry = ["http://localhost:11434"];
195
+ try {
196
+ const gateway = execSync("ip route show default 2>/dev/null | awk '{print $3}'", { encoding: "utf-8" }).trim();
197
+ if (gateway) hostsToTry.push(`http://${gateway}:11434`);
198
+ } catch {
199
+ }
200
+ if (process.env.OLLAMA_HOST) {
201
+ const envHost = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`;
202
+ hostsToTry.unshift(envHost);
62
203
  }
63
- if (selected.name === "ollama") {
64
- const hostsToTry = ["http://localhost:11434"];
204
+ providerConfig.contextLength = 65536;
205
+ for (const host of hostsToTry) {
65
206
  try {
66
- const gateway = execSync("ip route show default 2>/dev/null | awk '{print $3}'", { encoding: "utf-8" }).trim();
67
- if (gateway) {
68
- hostsToTry.push(`http://${gateway}:11434`);
69
- }
70
- } catch {
71
- }
72
- if (process.env.OLLAMA_HOST) {
73
- const envHost = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`;
74
- hostsToTry.unshift(envHost);
75
- }
76
- let connectedHost = null;
77
- let models = [];
78
- for (const host of hostsToTry) {
79
- try {
80
- const controller = new AbortController();
81
- const timeout = setTimeout(() => controller.abort(), 3e3);
82
- const response = await globalThis.fetch(`${host}/api/tags`, { signal: controller.signal });
83
- clearTimeout(timeout);
84
- if (response.ok) {
85
- const data = await response.json();
86
- connectedHost = host;
87
- models = data.models || [];
88
- break;
207
+ const controller = new AbortController();
208
+ const timeout = setTimeout(() => controller.abort(), 3e3);
209
+ const response = await globalThis.fetch(`${host}/api/tags`, { signal: controller.signal });
210
+ clearTimeout(timeout);
211
+ if (response.ok) {
212
+ const data = await response.json();
213
+ providerConfig.host = host;
214
+ console.log(chalk.green(` \u2713 Connected to Ollama at ${host}`));
215
+ const models = data.models || [];
216
+ if (models.length > 0) {
217
+ console.log(chalk.dim(` Available: ${models.map((m) => m.name).join(", ")}`));
89
218
  }
90
- } catch {
91
- continue;
219
+ return;
92
220
  }
221
+ } catch {
222
+ continue;
93
223
  }
94
- providerConfig.contextLength = 65536;
95
- if (connectedHost) {
96
- providerConfig.host = connectedHost;
97
- console.log(chalk.green(` \u2713 Connected to Ollama at ${connectedHost}`));
98
- console.log(chalk.dim(` Context window: ${providerConfig.contextLength.toLocaleString()} tokens`));
99
- if (models.length > 0) {
100
- console.log(chalk.dim(` Available models: ${models.map((m) => m.name).join(", ")}`));
101
- }
102
- } else {
103
- providerConfig.host = "http://localhost:11434";
104
- console.log(chalk.yellow(" \u26A0 Could not connect to Ollama. Make sure it's running."));
105
- console.log(chalk.dim(" Tried: " + hostsToTry.join(", ")));
224
+ }
225
+ providerConfig.host = "http://localhost:11434";
226
+ console.log(chalk.yellow(" \u26A0 Could not connect to Ollama. Make sure it's running."));
227
+ }
228
+ async function runSetup() {
229
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
230
+ const apiKeys = /* @__PURE__ */ new Map();
231
+ console.log();
232
+ console.log(chalk.bold(" WorkerMill CLI") + chalk.dim(" \u2014 AI coding agent"));
233
+ console.log();
234
+ console.log(chalk.dim(" WorkerMill uses three roles. Each can use a different provider."));
235
+ console.log(chalk.dim(" Workers build code, the planner designs the plan, the reviewer checks quality."));
236
+ console.log();
237
+ console.log(chalk.hex("#D77757").bold(" \u2460 Workers") + chalk.dim(" \u2014 expert personas that write code"));
238
+ console.log();
239
+ const workerProvider = await pickProvider(rl, "Provider for workers", PROVIDERS);
240
+ const workerModel = await pickModel(rl, workerProvider);
241
+ const workerKey = await getApiKey(rl, workerProvider, apiKeys);
242
+ const workerConfig = { model: workerModel };
243
+ if (workerKey) workerConfig.apiKey = workerKey;
244
+ if (workerProvider.name === "ollama") await configureOllama(workerConfig);
245
+ console.log();
246
+ console.log(chalk.hex("#D77757").bold(" \u2461 Planner") + chalk.dim(" \u2014 plans stories and validates the approach"));
247
+ console.log();
248
+ const sameForPlanner = await ask(rl, chalk.dim(` Same as workers (${workerProvider.display} / ${workerModel})? [Y/n] `));
249
+ let plannerProviderName;
250
+ let plannerModel;
251
+ if (sameForPlanner.trim().toLowerCase() === "n") {
252
+ const plannerProvider = await pickProvider(rl, "Provider for planner", PROVIDERS);
253
+ plannerModel = await pickModel(rl, plannerProvider);
254
+ const plannerKey = await getApiKey(rl, plannerProvider, apiKeys);
255
+ plannerProviderName = plannerProvider.name;
256
+ if (plannerKey && plannerProvider.name !== workerProvider.name) {
106
257
  }
258
+ } else {
259
+ plannerProviderName = workerProvider.name;
260
+ plannerModel = workerModel;
261
+ console.log(chalk.dim(` \u2192 ${workerProvider.display} / ${workerModel}`));
107
262
  }
108
263
  console.log();
109
- const modelOverride = await ask(rl, chalk.dim(` Model (press Enter for ${selected.defaultModel}): `));
110
- if (modelOverride.trim()) {
111
- providerConfig.model = modelOverride.trim();
264
+ console.log(chalk.hex("#D77757").bold(" \u2462 Reviewer") + chalk.dim(" \u2014 tech lead that reviews code quality"));
265
+ console.log();
266
+ const sameForReviewer = await ask(rl, chalk.dim(` Same as workers (${workerProvider.display} / ${workerModel})? [Y/n] `));
267
+ let reviewerProviderName;
268
+ let reviewerModel;
269
+ if (sameForReviewer.trim().toLowerCase() === "n") {
270
+ const reviewerProvider = await pickProvider(rl, "Provider for reviewer", PROVIDERS);
271
+ reviewerModel = await pickModel(rl, reviewerProvider);
272
+ const reviewerKey = await getApiKey(rl, reviewerProvider, apiKeys);
273
+ reviewerProviderName = reviewerProvider.name;
274
+ if (reviewerKey && reviewerProvider.name !== workerProvider.name) {
275
+ }
276
+ } else {
277
+ reviewerProviderName = workerProvider.name;
278
+ reviewerModel = workerModel;
279
+ console.log(chalk.dim(` \u2192 ${workerProvider.display} / ${workerModel}`));
112
280
  }
113
281
  rl.close();
282
+ const providers = {
283
+ [workerProvider.name]: workerConfig
284
+ };
285
+ if (plannerProviderName !== workerProvider.name && !providers[plannerProviderName]) {
286
+ const pProvider = PROVIDERS.find((p) => p.name === plannerProviderName);
287
+ const cfg = { model: plannerModel };
288
+ const key = apiKeys.get(plannerProviderName);
289
+ if (key) cfg.apiKey = key;
290
+ if (pProvider.name === "ollama") await configureOllama(cfg);
291
+ providers[plannerProviderName] = cfg;
292
+ }
293
+ if (reviewerProviderName !== workerProvider.name && !providers[reviewerProviderName]) {
294
+ const rProvider = PROVIDERS.find((p) => p.name === reviewerProviderName);
295
+ const cfg = { model: reviewerModel };
296
+ const key = apiKeys.get(reviewerProviderName);
297
+ if (key) cfg.apiKey = key;
298
+ if (rProvider.name === "ollama") await configureOllama(cfg);
299
+ providers[reviewerProviderName] = cfg;
300
+ }
301
+ const routing = {};
302
+ if (plannerProviderName !== workerProvider.name) {
303
+ routing.planner = plannerProviderName;
304
+ routing.critic = plannerProviderName;
305
+ }
306
+ if (reviewerProviderName !== workerProvider.name) {
307
+ routing.tech_lead = reviewerProviderName;
308
+ }
309
+ if (plannerProviderName === workerProvider.name && plannerModel !== workerModel) {
310
+ const altKey = `${plannerProviderName}_planner`;
311
+ providers[altKey] = { ...providers[plannerProviderName], model: plannerModel };
312
+ routing.planner = altKey;
313
+ routing.critic = altKey;
314
+ }
315
+ if (reviewerProviderName === workerProvider.name && reviewerModel !== workerModel) {
316
+ const altKey = `${reviewerProviderName}_reviewer`;
317
+ providers[altKey] = { ...providers[reviewerProviderName], model: reviewerModel };
318
+ routing.tech_lead = altKey;
319
+ }
114
320
  const config = {
115
- providers: { [selected.name]: providerConfig },
116
- default: selected.name
321
+ providers,
322
+ default: workerProvider.name,
323
+ ...Object.keys(routing).length > 0 ? { routing } : {}
117
324
  };
118
325
  saveConfig(config);
119
326
  console.log();
120
327
  console.log(chalk.green(" \u2713 Config saved to ~/.workermill/cli.json"));
121
328
  console.log();
329
+ console.log(chalk.dim(" Workers: ") + `${workerProvider.name}/${workerModel}`);
330
+ console.log(chalk.dim(" Planner: ") + `${plannerProviderName}/${plannerModel}`);
331
+ console.log(chalk.dim(" Reviewer: ") + `${reviewerProviderName}/${reviewerModel}`);
332
+ console.log();
122
333
  return config;
123
334
  }
124
335
 
@@ -743,7 +954,8 @@ function useAgent(options) {
743
954
  setTrustAll,
744
955
  setPlanMode,
745
956
  addSystemMessage,
746
- addUserMessage
957
+ addUserMessage,
958
+ setCost
747
959
  };
748
960
  }
749
961
 
@@ -797,7 +1009,7 @@ var PERSONA_EMOJIS = {
797
1009
  function getEmoji(persona) {
798
1010
  return PERSONA_EMOJIS[persona] || "\u{1F916}";
799
1011
  }
800
- function useOrchestrator(addMessage2) {
1012
+ function useOrchestrator(addMessage2, setCost) {
801
1013
  const [running, setRunning] = useState2(false);
802
1014
  const [statusMessage, setStatusMessage] = useState2("");
803
1015
  const [confirmRequest, setConfirmRequest] = useState2(null);
@@ -816,7 +1028,7 @@ function useOrchestrator(addMessage2) {
816
1028
  setRunning(false);
817
1029
  return;
818
1030
  }
819
- const { classifyComplexity, runOrchestration } = await import("./orchestrator-A2CDVJAS.js");
1031
+ const { classifyComplexity, runOrchestration } = await import("./orchestrator-VRGIOUHT.js");
820
1032
  const output = {
821
1033
  log(persona, message) {
822
1034
  const emoji = getEmoji(persona);
@@ -880,6 +1092,9 @@ function useOrchestrator(addMessage2) {
880
1092
  addMessage2(
881
1093
  `[${emoji} ${persona}] \u2193 ${toolName}${detail ? " " + detail : ""}`
882
1094
  );
1095
+ },
1096
+ updateCost(cost) {
1097
+ setCost?.(cost);
883
1098
  }
884
1099
  };
885
1100
  addMessage2("Analyzing task complexity\u2026");
@@ -1368,11 +1583,6 @@ function PermissionPrompt({ request }) {
1368
1583
  init_esm_shims();
1369
1584
  import { Box as Box4, Text as Text4, useStdout } from "ink";
1370
1585
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1371
- function formatTokens(n) {
1372
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1373
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
1374
- return String(n);
1375
- }
1376
1586
  function formatCost(c) {
1377
1587
  if (c < 0.01) return "$0.00";
1378
1588
  return `$${c.toFixed(2)}`;
@@ -1399,7 +1609,8 @@ function StatusBar(props) {
1399
1609
  }
1400
1610
  const bgColor = theme.subtleDark;
1401
1611
  const modelStr = ` ${props.provider}/${props.model} `;
1402
- const tokenStr = ` ${formatTokens(props.tokens)} `;
1612
+ const pct = props.maxContext > 0 ? Math.round(usage * 100) : 0;
1613
+ const tokenStr = ` ${pct}% `;
1403
1614
  const costStr = formatCost(props.cost);
1404
1615
  const branchStr = props.gitBranch ? ` git:(${props.gitBranch})` : "";
1405
1616
  const cwdStr = props.cwd ? ` ${props.cwd}` : "";
@@ -1679,7 +1890,7 @@ function Root(props) {
1679
1890
  },
1680
1891
  [agent]
1681
1892
  );
1682
- const orchestrator = useOrchestrator(addOrchestratorMessage);
1893
+ const orchestrator = useOrchestrator(addOrchestratorMessage, agent.setCost);
1683
1894
  const buildStarted = useRef3(false);
1684
1895
  useEffect2(() => {
1685
1896
  if (props.initialBuildTask && !buildStarted.current) {
@@ -1997,7 +2208,7 @@ function printWelcome(provider, model, workingDir) {
1997
2208
  console.log(dim(" Type ") + white("/help") + dim(" for all commands."));
1998
2209
  console.log();
1999
2210
  }
2000
- var VERSION = "0.3.2";
2211
+ var VERSION = "0.4.0";
2001
2212
  function addSharedOptions(cmd) {
2002
2213
  return cmd.option("--provider <provider>", "Override default provider").option("--model <model>", "Override model").option("--trust", "Skip all tool permission prompts").option("--full-disk", "Allow tools to access files outside working directory");
2003
2214
  }
@@ -401,6 +401,7 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
401
401
  if (finalText && finalText.length > planText.length) {
402
402
  planText = finalText;
403
403
  }
404
+ const planUsage = await planStream.totalUsage;
404
405
  let stories = parseStoriesFromText(planText, output);
405
406
  if (stories.length === 0) {
406
407
  output.log("system", "Planner didn't produce structured stories, falling back to single story");
@@ -411,7 +412,13 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
411
412
  description: userTask
412
413
  }];
413
414
  }
414
- return stories;
415
+ return {
416
+ stories,
417
+ provider: pProvider,
418
+ model: pModel,
419
+ inputTokens: planUsage?.inputTokens || 0,
420
+ outputTokens: planUsage?.outputTokens || 0
421
+ };
415
422
  }
416
423
  function parseStoriesFromText(text, output) {
417
424
  const codeBlocks = [...text.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
@@ -542,7 +549,10 @@ async function runOrchestration(config, userTask, trustAll, sandboxed, output) {
542
549
  };
543
550
  const sessionAllow = /* @__PURE__ */ new Set();
544
551
  const workingDir = process.cwd();
545
- const plannerStories = await planStories(config, userTask, workingDir, sandboxed, output);
552
+ const planResult = await planStories(config, userTask, workingDir, sandboxed, output);
553
+ const plannerStories = planResult.stories;
554
+ costTracker.addUsage("Planner", planResult.provider, planResult.model, planResult.inputTokens, planResult.outputTokens);
555
+ output.updateCost?.(costTracker.getTotalCost());
546
556
  output.log("planner", `Plan generated: ${plannerStories.length} stories`);
547
557
  plannerStories.forEach((s, i) => {
548
558
  output.log("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
@@ -753,6 +763,7 @@ ${revisionFeedback}` : ""}`;
753
763
  const inTokens = usage?.inputTokens || 0;
754
764
  const outTokens = usage?.outputTokens || 0;
755
765
  costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
766
+ output.updateCost?.(costTracker.getTotalCost());
756
767
  output.log(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
757
768
  output.log("system", "");
758
769
  break;
@@ -804,7 +815,7 @@ ${revisionFeedback}` : ""}`;
804
815
  for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
805
816
  const isRevision = reviewRound > 0;
806
817
  output.coordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
807
- output.log("tech_lead", "Starting agent execution");
818
+ output.log("tech_lead", `Starting agent execution (model: ${revModel})`);
808
819
  output.status(isRevision ? "Reviewer -- Re-checking after revisions" : "Reviewer -- Checking code quality");
809
820
  try {
810
821
  const previousFeedbackSection = isRevision && previousReviewFeedback ? `## Previous Review Feedback (Review ${reviewRound}/${maxRevisions})
@@ -908,6 +919,7 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
908
919
  reviewUsage?.inputTokens || 0,
909
920
  reviewUsage?.outputTokens || 0
910
921
  );
922
+ output.updateCost?.(costTracker.getTotalCost());
911
923
  if (approved) break;
912
924
  if (reviewRound >= maxRevisions) {
913
925
  output.log("system", `Max review revisions (${maxRevisions}) reached`);
@@ -965,7 +977,7 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
965
977
  }
966
978
  }
967
979
  output.coordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
968
- output.log(story.persona, `Starting revision: ${story.title}`);
980
+ output.log(story.persona, `Starting revision: ${story.title} (model: ${sModel})`);
969
981
  output.status("");
970
982
  const storyModel = createModel(sProvider, sModel, sHost, sCtx);
971
983
  const storyAllTools = createToolDefinitions(workingDir, storyModel, sandboxed);
@@ -1038,6 +1050,7 @@ ${story.description}`,
1038
1050
  revUsage?.inputTokens || 0,
1039
1051
  revUsage?.outputTokens || 0
1040
1052
  );
1053
+ output.updateCost?.(costTracker.getTotalCost());
1041
1054
  output.log(story.persona, `${story.title} \u2014 revision complete!`);
1042
1055
  } catch (err) {
1043
1056
  output.statusDone();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workermill",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "AI coding agent with multi-expert orchestration. Works with any LLM provider.",
5
5
  "type": "module",
6
6
  "bin": {