workermill 0.3.1 → 0.3.3

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.
@@ -5,23 +5,24 @@ import {
5
5
  createModel,
6
6
  createToolDefinitions,
7
7
  getProviderForPersona,
8
- info
9
- } from "./chunk-NGQKIYVB.js";
8
+ init_esm_shims
9
+ } from "./chunk-KL7SFKGG.js";
10
10
 
11
- // src/orchestrator.js
12
- import chalk3 from "chalk";
13
- import ora from "ora";
11
+ // src/orchestrator.ts
12
+ init_esm_shims();
14
13
  import { streamText, generateObject, generateText, stepCountIs } from "ai";
15
14
  import { z } from "zod";
15
+ import fs2 from "fs";
16
+ import path2 from "path";
16
17
 
17
- // src/personas.js
18
+ // src/personas.ts
19
+ init_esm_shims();
18
20
  import fs from "fs";
19
21
  import path from "path";
20
22
  import os from "os";
21
23
  function parsePersonaFile(content) {
22
24
  const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
23
- if (!match)
24
- return null;
25
+ if (!match) return null;
25
26
  const frontmatter = match[1];
26
27
  const body = match[2].trim();
27
28
  const meta = {};
@@ -36,8 +37,7 @@ function parsePersonaFile(content) {
36
37
  meta[key] = value;
37
38
  }
38
39
  }
39
- if (!meta.name || !meta.slug)
40
- return null;
40
+ if (!meta.name || !meta.slug) return null;
41
41
  return {
42
42
  name: meta.name,
43
43
  slug: meta.slug,
@@ -65,8 +65,7 @@ function loadPersona(slug) {
65
65
  if (fs.existsSync(loc)) {
66
66
  const content = fs.readFileSync(loc, "utf-8");
67
67
  const persona = parsePersonaFile(content);
68
- if (persona)
69
- return persona;
68
+ if (persona) return persona;
70
69
  }
71
70
  } catch {
72
71
  continue;
@@ -81,234 +80,21 @@ function loadPersona(slug) {
81
80
  };
82
81
  }
83
82
 
84
- // src/permissions.js
85
- import readline from "readline";
86
- import chalk from "chalk";
87
- var READ_TOOLS = /* @__PURE__ */ new Set(["read_file", "glob", "grep", "ls", "sub_agent"]);
88
- var DANGEROUS_PATTERNS = [
89
- { pattern: /rm\s+(-[a-z]*f|-[a-z]*r|--force|--recursive)/i, label: "recursive/forced delete" },
90
- { pattern: /git\s+reset\s+--hard/i, label: "hard reset" },
91
- { pattern: /git\s+push\s+.*--force/i, label: "force push" },
92
- { pattern: /git\s+clean\s+-[a-z]*f/i, label: "git clean" },
93
- { pattern: /drop\s+table/i, label: "drop table" },
94
- { pattern: /truncate\s+/i, label: "truncate" },
95
- { pattern: /DELETE\s+FROM\s+\w+\s*;/i, label: "DELETE without WHERE" },
96
- { pattern: /chmod\s+777/i, label: "chmod 777" },
97
- { pattern: />(\/dev\/sda|\/dev\/disk)/i, label: "write to disk device" }
98
- ];
99
- function isDangerous(command) {
100
- for (const { pattern, label } of DANGEROUS_PATTERNS) {
101
- if (pattern.test(command))
102
- return label;
103
- }
104
- return null;
105
- }
106
- var PermissionManager = class {
107
- sessionAllow = /* @__PURE__ */ new Set();
108
- trustAll;
109
- configTrust;
110
- rl = null;
111
- cancelCurrentPrompt = null;
112
- /** True while rl.question() is active — external line handlers must ignore input */
113
- questionActive = false;
114
- constructor(trustAll = false, configTrust = []) {
115
- this.trustAll = trustAll;
116
- this.configTrust = new Set(configTrust);
117
- }
118
- /** Bind to the agent's readline instance so we reuse it for prompts */
119
- setReadline(rl) {
120
- this.rl = rl;
121
- }
122
- cancelPrompt() {
123
- if (this.cancelCurrentPrompt) {
124
- this.cancelCurrentPrompt();
125
- this.cancelCurrentPrompt = null;
126
- }
127
- }
128
- async checkPermission(toolName, toolInput) {
129
- if (toolName === "bash") {
130
- const cmd = String(toolInput.command || "");
131
- const danger = isDangerous(cmd);
132
- if (danger) {
133
- if (this.trustAll)
134
- return true;
135
- console.log();
136
- console.log(chalk.red.bold(` \u26A0 DANGEROUS: ${danger}`));
137
- console.log(chalk.red(` Command: ${cmd}`));
138
- const answer = await this.askUser(chalk.red(" Are you sure? (yes to confirm): "));
139
- if (answer.trim().toLowerCase() !== "yes")
140
- return false;
141
- return true;
142
- }
143
- }
144
- if (this.trustAll)
145
- return true;
146
- if (READ_TOOLS.has(toolName))
147
- return true;
148
- if (this.sessionAllow.has(toolName))
149
- return true;
150
- if (this.configTrust.has(toolName))
151
- return true;
152
- return this.promptUser(toolName, toolInput);
153
- }
154
- async promptUser(toolName, toolInput) {
155
- const display = this.formatToolCall(toolName, toolInput);
156
- console.log();
157
- console.log(chalk.cyan(` \u250C\u2500 ${toolName} ${"\u2500".repeat(Math.max(0, 40 - toolName.length))}\u2510`));
158
- for (const line of display.split("\n")) {
159
- console.log(chalk.cyan(" \u2502 ") + chalk.white(line));
160
- }
161
- console.log(chalk.cyan(` \u2514${"\u2500".repeat(43)}\u2518`));
162
- const answer = await this.askUser(chalk.dim(" Allow? ") + chalk.white("(y)es / (n)o / (a)lways this tool / (t)rust all: "));
163
- const choice = answer.trim().toLowerCase();
164
- if (choice === "t" || choice === "trust") {
165
- this.trustAll = true;
166
- return true;
167
- }
168
- if (choice === "a" || choice === "always") {
169
- this.sessionAllow.add(toolName);
170
- return true;
171
- }
172
- return choice === "y" || choice === "yes";
173
- }
174
- /**
175
- * Prompt the user with a question. Sets questionActive flag so the
176
- * agent's line handler knows to ignore this input.
177
- */
178
- askUser(prompt) {
179
- return new Promise((resolve, reject) => {
180
- this.cancelCurrentPrompt = () => {
181
- this.questionActive = false;
182
- reject(new Error("cancelled"));
183
- };
184
- if (this.rl) {
185
- this.questionActive = true;
186
- this.rl.resume();
187
- this.rl.question(prompt, (answer) => {
188
- this.questionActive = false;
189
- this.cancelCurrentPrompt = null;
190
- this.rl.pause();
191
- resolve(answer);
192
- });
193
- } else {
194
- const questionRl = readline.createInterface({
195
- input: process.stdin,
196
- output: process.stdout
197
- });
198
- this.questionActive = true;
199
- questionRl.question(prompt, (answer) => {
200
- this.questionActive = false;
201
- this.cancelCurrentPrompt = null;
202
- questionRl.close();
203
- resolve(answer);
204
- });
205
- }
206
- });
207
- }
208
- formatToolCall(toolName, input) {
209
- switch (toolName) {
210
- case "bash":
211
- return String(input.command || "");
212
- case "write_file":
213
- case "edit_file":
214
- return `${input.path || ""}`;
215
- case "patch":
216
- return String(input.patch_text || "").slice(0, 200) + "...";
217
- case "fetch":
218
- return String(input.url || "");
219
- default:
220
- return JSON.stringify(input, null, 2).slice(0, 200);
221
- }
222
- }
223
- };
83
+ // src/orchestrator.ts
84
+ function resolveTaskInput(task, workingDir) {
85
+ const trimmed = task.trim();
86
+ if (!trimmed.includes(" ") && /\.\w{1,10}$/.test(trimmed)) {
87
+ const fullPath = path2.resolve(workingDir, trimmed);
88
+ try {
89
+ const content = fs2.readFileSync(fullPath, "utf-8");
90
+ return `Implement the following specification from ${trimmed}:
224
91
 
225
- // src/tui.js
226
- import chalk2 from "chalk";
227
- import { execSync } from "child_process";
228
- function formatToolCall(toolName, toolInput) {
229
- let msg = `Tool: ${toolName}`;
230
- if (toolInput) {
231
- if (toolInput.file_path)
232
- msg += ` \u2192 ${toolInput.file_path}`;
233
- else if (toolInput.path)
234
- msg += ` \u2192 ${toolInput.path}`;
235
- else if (toolInput.command)
236
- msg += ` \u2192 ${String(toolInput.command).substring(0, 500)}`;
237
- else if (toolInput.pattern)
238
- msg += ` \u2192 pattern: ${toolInput.pattern}`;
239
- else {
240
- const keys = Object.keys(toolInput).slice(0, 3);
241
- if (keys.length > 0) {
242
- msg += ` \u2192 ${keys.map((k) => `${k}: ${String(toolInput[k]).substring(0, 200)}`).join(", ")}`;
243
- }
92
+ ${content}`;
93
+ } catch {
244
94
  }
245
95
  }
246
- return msg;
247
- }
248
- var PERSONA_EMOJIS = {
249
- frontend_developer: "\u{1F3A8}",
250
- // 🎨
251
- backend_developer: "\u{1F4BB}",
252
- // 💻
253
- fullstack_developer: "\u{1F4BB}",
254
- // 💻 (same as backend)
255
- devops_engineer: "\u{1F527}",
256
- // 🔧
257
- security_engineer: "\u{1F512}",
258
- // 🔐
259
- qa_engineer: "\u{1F9EA}",
260
- // 🧪
261
- tech_writer: "\u{1F4DD}",
262
- // 📝
263
- project_manager: "\u{1F4CB}",
264
- // 📋
265
- architect: "\u{1F3D7}\uFE0F",
266
- // 🏗️
267
- database_engineer: "\u{1F4CA}",
268
- // 📊
269
- data_engineer: "\u{1F4CA}",
270
- // 📊
271
- data_ml_engineer: "\u{1F4CA}",
272
- // 📊
273
- ml_engineer: "\u{1F4CA}",
274
- // 📊
275
- mobile_developer: "\u{1F4F1}",
276
- // 📱
277
- tech_lead: "\u{1F451}",
278
- // 👑
279
- manager: "\u{1F454}",
280
- // 👔
281
- support_agent: "\u{1F4AC}",
282
- // 💬
283
- planner: "\u{1F4A1}",
284
- // 💡 (planning_agent)
285
- coordinator: "\u{1F3AF}",
286
- // 🎯
287
- critic: "\u{1F50D}",
288
- // 🔍
289
- reviewer: "\u{1F50D}"
290
- // 🔍
291
- };
292
- function getPersonaEmoji(persona) {
293
- return PERSONA_EMOJIS[persona] || "\u{1F916}";
294
- }
295
- function wmLog(persona, message) {
296
- const emoji = getPersonaEmoji(persona);
297
- console.log(chalk2.cyan(`[${emoji} ${persona} \u{1F3E0}] `) + chalk2.white(message));
298
- info(`[${persona}] ${message}`);
299
- }
300
- function wmCoordinatorLog(message) {
301
- console.log(chalk2.cyan("[coordinator] ") + chalk2.white(message));
302
- info(`[coordinator] ${message}`);
96
+ return task;
303
97
  }
304
- function printError(message) {
305
- console.log(chalk2.red(`
306
- \u2717 ${message}
307
- `));
308
- }
309
- var sessionStartTime = Date.now();
310
-
311
- // src/orchestrator.js
312
98
  var LEARNING_INSTRUCTIONS = `
313
99
 
314
100
  ## Reporting Learnings
@@ -368,21 +154,77 @@ function buildReasoningOptions(provider, modelName) {
368
154
  }
369
155
  }
370
156
  function isTransientError(error) {
371
- if (!error || typeof error !== "object")
372
- return false;
157
+ if (!error || typeof error !== "object") return false;
373
158
  const msg = error instanceof Error ? error.message : String(error);
374
159
  if (/status code (502|503|504)|socket hang up|ECONNRESET|ETIMEDOUT|network error|ECONNREFUSED/i.test(msg)) {
375
160
  return true;
376
161
  }
377
162
  return false;
378
163
  }
379
- async function classifyComplexity(config, userInput) {
164
+ var READ_TOOLS = /* @__PURE__ */ new Set(["read_file", "glob", "grep", "ls", "sub_agent"]);
165
+ var DANGEROUS_PATTERNS = [
166
+ { pattern: /rm\s+(-[a-z]*f|-[a-z]*r|--force|--recursive)/i, label: "recursive/forced delete" },
167
+ { pattern: /git\s+reset\s+--hard/i, label: "hard reset" },
168
+ { pattern: /git\s+push\s+.*--force/i, label: "force push" },
169
+ { pattern: /git\s+clean\s+-[a-z]*f/i, label: "git clean" },
170
+ { pattern: /drop\s+table/i, label: "drop table" },
171
+ { pattern: /truncate\s+/i, label: "truncate" },
172
+ { pattern: /DELETE\s+FROM\s+\w+\s*;/i, label: "DELETE without WHERE" },
173
+ { pattern: /chmod\s+777/i, label: "chmod 777" },
174
+ { pattern: />(\/dev\/sda|\/dev\/disk)/i, label: "write to disk device" }
175
+ ];
176
+ function isDangerous(command) {
177
+ for (const { pattern, label } of DANGEROUS_PATTERNS) {
178
+ if (pattern.test(command)) return label;
179
+ }
180
+ return null;
181
+ }
182
+ async function checkToolPermission(toolName, toolInput, trustAll, sessionAllow, output) {
183
+ if (toolName === "bash") {
184
+ const cmd = String(toolInput.command || "");
185
+ const danger = isDangerous(cmd);
186
+ if (danger) {
187
+ if (trustAll) return true;
188
+ output.error(`DANGEROUS: ${danger}`);
189
+ output.error(`Command: ${cmd}`);
190
+ const confirmed2 = await output.confirm("This is a dangerous operation. Are you sure?");
191
+ return confirmed2;
192
+ }
193
+ }
194
+ if (trustAll) return true;
195
+ if (READ_TOOLS.has(toolName)) return true;
196
+ if (sessionAllow.has(toolName)) return true;
197
+ const display = formatToolCallDisplay(toolName, toolInput);
198
+ output.log("system", `Tool: ${toolName} -- ${display}`);
199
+ const confirmed = await output.confirm(`Allow ${toolName}?`);
200
+ if (confirmed) {
201
+ sessionAllow.add(toolName);
202
+ }
203
+ return confirmed;
204
+ }
205
+ function formatToolCallDisplay(toolName, toolInput) {
206
+ let msg = `Tool: ${toolName}`;
207
+ if (toolInput) {
208
+ if (toolInput.file_path) msg += ` -> ${toolInput.file_path}`;
209
+ else if (toolInput.path) msg += ` -> ${toolInput.path}`;
210
+ else if (toolInput.command) msg += ` -> ${String(toolInput.command).substring(0, 500)}`;
211
+ else if (toolInput.pattern) msg += ` -> pattern: ${toolInput.pattern}`;
212
+ else {
213
+ const keys = Object.keys(toolInput).slice(0, 3);
214
+ if (keys.length > 0) {
215
+ msg += ` -> ${keys.map((k) => `${k}: ${String(toolInput[k]).substring(0, 200)}`).join(", ")}`;
216
+ }
217
+ }
218
+ }
219
+ return msg;
220
+ }
221
+ async function classifyComplexity(config, userInput, output) {
222
+ const resolvedInput = resolveTaskInput(userInput, process.cwd());
380
223
  const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config);
381
224
  if (apiKey) {
382
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
225
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
383
226
  const envVar = envMap[provider];
384
- if (envVar && !process.env[envVar])
385
- process.env[envVar] = apiKey;
227
+ if (envVar && !process.env[envVar]) process.env[envVar] = apiKey;
386
228
  }
387
229
  const model = createModel(provider, modelName, host, contextLength);
388
230
  try {
@@ -395,7 +237,7 @@ async function classifyComplexity(config, userInput) {
395
237
  prompt: `Analyze this coding task. If it involves multiple distinct concerns that would benefit from different specialist personas (e.g., database + backend + frontend + devops), classify as "multi". If it's a focused task that one developer could handle, classify as "single". Just classify \u2014 do not break down into stories.
396
238
 
397
239
  Task:
398
- ${userInput}`
240
+ ${resolvedInput}`
399
241
  });
400
242
  return {
401
243
  isMulti: result.object.complexity === "multi",
@@ -407,7 +249,7 @@ ${userInput}`
407
249
  model,
408
250
  prompt: `Is this task "single" (one developer) or "multi" (needs multiple specialists)? Respond with just "single" or "multi" and a brief reason.
409
251
 
410
- Task: ${userInput}`
252
+ Task: ${resolvedInput}`
411
253
  });
412
254
  const isMulti = /\bmulti\b/i.test(textResult.text);
413
255
  return { isMulti, reason: textResult.text.slice(0, 200) };
@@ -422,42 +264,37 @@ function topologicalSort(stories) {
422
264
  const result = [];
423
265
  const visiting = /* @__PURE__ */ new Set();
424
266
  function visit(id) {
425
- if (visited.has(id))
426
- return;
267
+ if (visited.has(id)) return;
427
268
  if (visiting.has(id)) {
428
- console.log(chalk3.yellow(` \u26A0 Circular dependency at ${id}, using input order`));
429
269
  return;
430
270
  }
431
271
  visiting.add(id);
432
272
  const story = idMap.get(id);
433
273
  if (story?.dependsOn) {
434
274
  for (const dep of story.dependsOn) {
435
- if (idMap.has(dep))
436
- visit(dep);
275
+ if (idMap.has(dep)) visit(dep);
437
276
  }
438
277
  }
439
278
  visiting.delete(id);
440
279
  visited.add(id);
441
- if (story)
442
- result.push(story);
280
+ if (story) result.push(story);
443
281
  }
444
282
  for (const story of stories) {
445
283
  visit(story.id);
446
284
  }
447
285
  return result;
448
286
  }
449
- async function planStories(config, userTask, workingDir, sandboxed = true) {
287
+ async function planStories(config, userTask, workingDir, sandboxed, output) {
450
288
  const planner = loadPersona("planner");
451
289
  const { provider: pProvider, model: pModel, host: pHost, contextLength: pCtx } = getProviderForPersona(config, "planner");
452
290
  if (pProvider) {
453
291
  const pApiKey = config.providers[pProvider]?.apiKey;
454
292
  if (pApiKey) {
455
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
293
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
456
294
  const envVar = envMap[pProvider];
457
295
  if (envVar && !process.env[envVar]) {
458
296
  const key = pApiKey.startsWith("{env:") ? process.env[pApiKey.slice(5, -1)] : pApiKey;
459
- if (key)
460
- process.env[envVar] = key;
297
+ if (key) process.env[envVar] = key;
461
298
  }
462
299
  }
463
300
  }
@@ -471,7 +308,7 @@ async function planStories(config, userTask, workingDir, sandboxed = true) {
471
308
  readOnlyTools[toolName] = {
472
309
  ...toolDef,
473
310
  execute: async (input) => {
474
- wmLog("planner", formatToolCall(toolName, input));
311
+ output.toolCall("planner", toolName, input);
475
312
  const result = await toolDef.execute(input);
476
313
  return result;
477
314
  }
@@ -479,16 +316,39 @@ async function planStories(config, userTask, workingDir, sandboxed = true) {
479
316
  }
480
317
  }
481
318
  }
319
+ const fileRefPattern = /(?:^|\s)([\w./-]+\.(?:md|txt|yaml|yml|json|toml|ts|js|py|go|rs|spec|requirements|prd|plan))\b/gi;
320
+ const referencedFiles = [...new Set([...userTask.matchAll(fileRefPattern)].map((m) => m[1]))];
321
+ let inlinedFileContext = "";
322
+ if (referencedFiles.length > 0) {
323
+ const fs3 = await import("fs");
324
+ const path3 = await import("path");
325
+ for (const ref of referencedFiles) {
326
+ const fullPath = path3.default.resolve(workingDir, ref);
327
+ try {
328
+ const content = fs3.default.readFileSync(fullPath, "utf-8");
329
+ inlinedFileContext += `
330
+ ### File: ${ref}
331
+ \`\`\`
332
+ ${content}
333
+ \`\`\`
334
+ `;
335
+ output.log("planner", `Read referenced file: ${ref}`);
336
+ } catch {
337
+ }
338
+ }
339
+ }
482
340
  const plannerPrompt = `You are an expert implementation planner. Analyze this task and create a high-quality implementation plan.
483
341
 
484
342
  ## Task
485
343
  ${userTask}
486
-
344
+ ${inlinedFileContext ? `
345
+ ## Referenced Files
346
+ ${inlinedFileContext}` : ""}
487
347
  ## Working directory
488
348
  ${workingDir}
489
349
 
490
350
  ## Instructions
491
- 1. Use your tools to explore the working directory and understand what exists. Stay within the working directory.
351
+ 1. Use your tools to explore the working directory and understand what exists. Stay within the working directory.${referencedFiles.length > 0 ? "\n The referenced files above have been inlined for you. Read any additional files you need for context." : ""}
492
352
  2. Design a plan that breaks the task into focused stories, each assigned to a specialist persona.
493
353
  3. Each story should be a meaningful unit of work \u2014 not too granular, not too broad.
494
354
  4. Quality criteria:
@@ -514,8 +374,8 @@ Return ONLY a JSON code block with this structure:
514
374
  \`\`\`
515
375
 
516
376
  Available personas: backend_developer, frontend_developer, devops_engineer, qa_engineer, security_engineer, data_ml_engineer, mobile_developer, tech_writer, tech_lead`;
517
- wmLog("planner", `Starting planning agent using ${pModel}`);
518
- wmLog("planner", "Reading repository structure...");
377
+ output.log("planner", `Starting planning agent using ${pModel}`);
378
+ output.log("planner", "Reading repository structure...");
519
379
  let planText = "";
520
380
  const planStream = streamText({
521
381
  model: plannerModel,
@@ -529,9 +389,8 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
529
389
  if (text) {
530
390
  const lines = text.split("\n").filter((l) => l.trim());
531
391
  for (const line of lines) {
532
- if (line.trim().startsWith("{") || line.trim().startsWith("}") || line.trim().startsWith('"') || line.trim().startsWith("[") || line.trim().startsWith("]") || line.includes("```"))
533
- continue;
534
- wmLog("planner", line);
392
+ if (line.trim().startsWith("{") || line.trim().startsWith("}") || line.trim().startsWith('"') || line.trim().startsWith("[") || line.trim().startsWith("]") || line.includes("```")) continue;
393
+ output.log("planner", line);
535
394
  }
536
395
  }
537
396
  }
@@ -542,9 +401,10 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
542
401
  if (finalText && finalText.length > planText.length) {
543
402
  planText = finalText;
544
403
  }
545
- let stories = parseStoriesFromText(planText);
404
+ const planUsage = await planStream.totalUsage;
405
+ let stories = parseStoriesFromText(planText, output);
546
406
  if (stories.length === 0) {
547
- console.log(chalk3.yellow(" \u26A0 Planner didn't produce structured stories, falling back to single story"));
407
+ output.log("system", "Planner didn't produce structured stories, falling back to single story");
548
408
  stories = [{
549
409
  id: "implement",
550
410
  title: userTask.slice(0, 60),
@@ -552,14 +412,19 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
552
412
  description: userTask
553
413
  }];
554
414
  }
555
- return stories;
415
+ return {
416
+ stories,
417
+ provider: pProvider,
418
+ model: pModel,
419
+ inputTokens: planUsage?.inputTokens || 0,
420
+ outputTokens: planUsage?.outputTokens || 0
421
+ };
556
422
  }
557
- function parseStoriesFromText(text) {
423
+ function parseStoriesFromText(text, output) {
558
424
  const codeBlocks = [...text.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
559
425
  for (const match of codeBlocks) {
560
426
  const stories2 = tryParseStories(match[1].trim());
561
- if (stories2)
562
- return stories2;
427
+ if (stories2) return stories2;
563
428
  }
564
429
  const storiesIdx = text.indexOf('"stories"');
565
430
  if (storiesIdx !== -1) {
@@ -568,8 +433,7 @@ function parseStoriesFromText(text) {
568
433
  const json = extractBalancedJSON(text, braceStart);
569
434
  if (json) {
570
435
  const stories2 = tryParseStories(json);
571
- if (stories2)
572
- return stories2;
436
+ if (stories2) return stories2;
573
437
  }
574
438
  }
575
439
  }
@@ -578,27 +442,23 @@ function parseStoriesFromText(text) {
578
442
  const json = extractBalancedJSON(text, arrayStart);
579
443
  if (json) {
580
444
  const stories2 = tryParseStories(json);
581
- if (stories2)
582
- return stories2;
445
+ if (stories2) return stories2;
583
446
  }
584
447
  }
585
448
  const stories = tryParseStories(text.trim());
586
- if (stories)
587
- return stories;
449
+ if (stories) return stories;
588
450
  const preview = text.slice(0, 500);
589
- console.log(chalk3.dim(` (planner output preview: ${preview}${text.length > 500 ? "..." : ""})`));
451
+ output.log("system", `(planner output preview: ${preview}${text.length > 500 ? "..." : ""})`);
590
452
  return [];
591
453
  }
592
454
  function tryParseStories(text) {
593
455
  try {
594
456
  const parsed = JSON.parse(text);
595
457
  if (Array.isArray(parsed)) {
596
- if (parsed.length > 0 && parsed[0].persona)
597
- return parsed;
458
+ if (parsed.length > 0 && parsed[0].persona) return parsed;
598
459
  }
599
460
  if (parsed && Array.isArray(parsed.stories)) {
600
- if (parsed.stories.length > 0)
601
- return parsed.stories;
461
+ if (parsed.stories.length > 0) return parsed.stories;
602
462
  }
603
463
  } catch {
604
464
  }
@@ -607,8 +467,7 @@ function tryParseStories(text) {
607
467
  function extractBalancedJSON(text, start) {
608
468
  const open = text[start];
609
469
  const close = open === "{" ? "}" : open === "[" ? "]" : null;
610
- if (!close)
611
- return null;
470
+ if (!close) return null;
612
471
  let depth = 0;
613
472
  let inString = false;
614
473
  let escape = false;
@@ -626,10 +485,8 @@ function extractBalancedJSON(text, start) {
626
485
  inString = !inString;
627
486
  continue;
628
487
  }
629
- if (inString)
630
- continue;
631
- if (ch === open)
632
- depth++;
488
+ if (inString) continue;
489
+ if (ch === open) depth++;
633
490
  if (ch === close) {
634
491
  depth--;
635
492
  if (depth === 0) {
@@ -640,36 +497,31 @@ function extractBalancedJSON(text, start) {
640
497
  return null;
641
498
  }
642
499
  function extractScore(text) {
643
- const markerMatch = text.match(/::review_score::(\d+)/);
644
- if (markerMatch)
645
- return parseInt(markerMatch[1], 10);
500
+ const markerMatches = [...text.matchAll(/::review_score::(\d+)/g)];
501
+ if (markerMatches.length > 0) {
502
+ return parseInt(markerMatches[markerMatches.length - 1][1], 10);
503
+ }
646
504
  const scorePatterns = [
647
- /\bscore[:\s]+(\d+)\s*\/\s*100/i,
648
- /\b(\d+)\s*\/\s*100/,
649
- /\bscore[:\s]+(\d+)/i,
650
- /\brating[:\s]+(\d+)/i
505
+ /\bscore[:\s]+(\d+)\s*\/\s*100/gi,
506
+ /\bscore[:\s]+(\d+)/gi,
507
+ /\brating[:\s]+(\d+)/gi
651
508
  ];
652
509
  for (const pattern of scorePatterns) {
653
- const match = text.match(pattern);
654
- if (match) {
655
- const n = parseInt(match[1], 10);
656
- if (n >= 0 && n <= 100)
657
- return n;
510
+ const matches = [...text.matchAll(pattern)];
511
+ if (matches.length > 0) {
512
+ const n = parseInt(matches[matches.length - 1][1], 10);
513
+ if (n >= 0 && n <= 100) return n;
658
514
  }
659
515
  }
660
- if (/\bapprove/i.test(text))
661
- return 85;
662
- if (/\brevis/i.test(text))
663
- return 60;
516
+ if (/\bapprove/i.test(text)) return 85;
517
+ if (/\brevis/i.test(text)) return 60;
664
518
  return 75;
665
519
  }
666
520
  function parseAffectedStories(text) {
667
521
  const storiesMatch = text.match(/AFFECTED_STORIES:\s*\[([^\]]+)\]/i);
668
- if (!storiesMatch)
669
- return null;
522
+ if (!storiesMatch) return null;
670
523
  const stories = storiesMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
671
- if (stories.length === 0)
672
- return null;
524
+ if (stories.length === 0) return null;
673
525
  let reasons = {};
674
526
  const reasonsMatch = text.match(/AFFECTED_REASONS:\s*(\{[\s\S]*?\})/i);
675
527
  if (reasonsMatch) {
@@ -686,7 +538,8 @@ function parseAffectedStories(text) {
686
538
  }
687
539
  return { stories, reasons };
688
540
  }
689
- async function runOrchestration(config, userTask, trustAll, sandboxed = true, agentRl) {
541
+ async function runOrchestration(config, userTask, trustAll, sandboxed, output) {
542
+ userTask = resolveTaskInput(userTask, process.cwd());
690
543
  const costTracker = new CostTracker();
691
544
  const context = {
692
545
  filesCreated: [],
@@ -694,18 +547,18 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
694
547
  decisions: [],
695
548
  learnings: []
696
549
  };
697
- const permissions = new PermissionManager(trustAll);
698
- if (agentRl)
699
- permissions.setReadline(agentRl);
550
+ const sessionAllow = /* @__PURE__ */ new Set();
700
551
  const workingDir = process.cwd();
701
- const plannerStories = await planStories(config, userTask, workingDir, sandboxed);
702
- wmLog("planner", `Plan generated: ${plannerStories.length} stories`);
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());
556
+ output.log("planner", `Plan generated: ${plannerStories.length} stories`);
703
557
  plannerStories.forEach((s, i) => {
704
- const emoji = getPersonaEmoji(s.persona);
705
- wmLog("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
558
+ output.log("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
706
559
  });
707
- wmLog("planner", `Plan validated: ${plannerStories.length} stories. Task queued for execution.`);
708
- console.log();
560
+ output.log("planner", `Plan validated: ${plannerStories.length} stories. Task queued for execution.`);
561
+ output.log("system", "");
709
562
  if (config.review?.useCritic) {
710
563
  const critic = loadPersona("critic");
711
564
  if (critic) {
@@ -719,14 +572,14 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
719
572
  criticReadOnly[name] = {
720
573
  ...toolDef,
721
574
  execute: async (input) => {
722
- wmLog("critic", formatToolCall(name, input));
575
+ output.log("critic", formatToolCallDisplay(name, input));
723
576
  const result = await toolDef.execute(input);
724
577
  return result;
725
578
  }
726
579
  };
727
580
  }
728
581
  }
729
- const criticSpinner = ora({ stream: process.stdout, text: chalk3.white("Critic reviewing plan..."), prefixText: " " }).start();
582
+ output.status("Critic reviewing plan...");
730
583
  const criticStream = streamText({
731
584
  model: criticModel,
732
585
  system: critic.systemPrompt,
@@ -742,52 +595,47 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
742
595
  for await (const _chunk of criticStream.textStream) {
743
596
  }
744
597
  const criticText = await criticStream.text;
745
- criticSpinner.stop();
598
+ output.statusDone();
746
599
  const score = extractScore(criticText);
747
- wmLog("critic", `::review_score::${score}`);
748
- wmLog("critic", score >= 80 ? "Plan approved" : "Plan needs revision");
749
- console.log();
600
+ output.log("critic", `::review_score::${score}`);
601
+ output.log("critic", score >= 80 ? "Plan approved" : "Plan needs revision");
602
+ output.log("system", "");
750
603
  }
751
604
  }
752
605
  const sorted = topologicalSort(plannerStories);
753
606
  if (!trustAll) {
754
- let answer = "n";
607
+ let proceed = false;
755
608
  try {
756
- answer = await permissions.askUser(chalk3.dim(" Execute this plan? (y/n): "));
609
+ proceed = await output.confirm("Execute this plan?");
757
610
  } catch {
758
611
  }
759
- if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
760
- console.log(chalk3.dim(" Plan cancelled.\n"));
612
+ if (!proceed) {
613
+ output.log("system", "Plan cancelled.");
761
614
  return;
762
615
  }
763
- console.log();
616
+ output.log("system", "");
764
617
  }
765
618
  for (let i = 0; i < sorted.length; i++) {
766
619
  const story = sorted[i];
767
620
  const persona = loadPersona(story.persona);
768
621
  if (!persona) {
769
- printError(`Unknown persona: ${story.persona}`);
622
+ output.error(`Unknown persona: ${story.persona}`);
770
623
  continue;
771
624
  }
772
- const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config, persona.provider || story.persona);
625
+ const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(
626
+ config,
627
+ persona.provider || story.persona
628
+ );
773
629
  if (apiKey) {
774
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
630
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
775
631
  const envVar = envMap[provider];
776
- if (envVar && !process.env[envVar])
777
- process.env[envVar] = apiKey;
632
+ if (envVar && !process.env[envVar]) process.env[envVar] = apiKey;
778
633
  }
779
- console.log(chalk3.bold(`
780
- \u2500\u2500\u2500 Story ${i + 1}/${sorted.length} \u2500\u2500\u2500
781
- `));
782
- wmCoordinatorLog(`Task claimed by orchestrator`);
783
- wmLog(story.persona, `Starting ${story.title}`);
784
- wmLog(story.persona, `Executing story with AIClient (model: ${modelName})...`);
785
- const spinner = ora({
786
- stream: process.stdout,
787
- text: "",
788
- prefixText: "",
789
- spinner: "dots"
790
- }).start();
634
+ output.log("system", `--- Story ${i + 1}/${sorted.length} ---`);
635
+ output.coordinatorLog(`Task claimed by orchestrator`);
636
+ output.log(story.persona, `Starting ${story.title}`);
637
+ output.log(story.persona, `Executing story with AIClient (model: ${modelName})...`);
638
+ output.status("");
791
639
  const model = createModel(provider, modelName, host, contextLength);
792
640
  const allTools = createToolDefinitions(workingDir, model, sandboxed);
793
641
  const personaTools = {};
@@ -798,18 +646,17 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
798
646
  personaTools[toolName] = {
799
647
  ...toolDef,
800
648
  execute: async (input) => {
801
- const allowed = await permissions.checkPermission(toolName, input);
802
- if (!allowed)
803
- return "Tool execution denied by user.";
804
- const callKey = `${toolName}:${JSON.stringify(input)}`;
805
- const isDuplicate = callKey === lastToolCall;
806
- lastToolCall = callKey;
649
+ const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
650
+ if (!allowed) return "Tool execution denied by user.";
651
+ const sig = `${toolName}:${JSON.stringify(input)}`;
652
+ const isDuplicate = sig === lastToolCall;
653
+ lastToolCall = sig;
807
654
  if (!isDuplicate) {
808
- spinner.stop();
809
- wmLog(story.persona, formatToolCall(toolName, input));
655
+ output.statusDone();
656
+ output.toolCall(story.persona, toolName, input);
810
657
  }
811
658
  const result = await toolDef.execute(input);
812
- spinner.start();
659
+ output.status("");
813
660
  return result;
814
661
  }
815
662
  };
@@ -872,12 +719,11 @@ ${revisionFeedback}` : ""}`;
872
719
  ...buildOllamaOptions(provider, contextLength),
873
720
  onStepFinish({ text: text2 }) {
874
721
  if (text2) {
875
- spinner.stop();
722
+ output.statusDone();
876
723
  const lines = text2.split("\n").filter((l) => l.trim());
877
724
  for (const line of lines) {
878
- if (line.includes("::decision::") || line.includes("::learning::") || line.includes("::file_created::") || line.includes("::file_modified::"))
879
- continue;
880
- wmLog(story.persona, line);
725
+ if (line.includes("::decision::") || line.includes("::learning::") || line.includes("::file_created::") || line.includes("::file_modified::")) continue;
726
+ output.log(story.persona, line);
881
727
  }
882
728
  }
883
729
  }
@@ -887,13 +733,13 @@ ${revisionFeedback}` : ""}`;
887
733
  const text = await stream.text;
888
734
  allText = text;
889
735
  const usage = await stream.totalUsage;
890
- spinner.stop();
736
+ output.statusDone();
891
737
  const decisionMatches = text.match(/::decision::(.*?)(?=::\w+::|$)/gs);
892
738
  if (decisionMatches) {
893
739
  for (const m of decisionMatches) {
894
740
  const decision = m.replace("::decision::", "").trim();
895
741
  context.decisions.push(decision);
896
- wmLog(story.persona, decision);
742
+ output.log(story.persona, decision);
897
743
  }
898
744
  }
899
745
  const learningMatches = text.match(/::learning::(.*?)(?=::\w+::|$)/gs);
@@ -917,17 +763,18 @@ ${revisionFeedback}` : ""}`;
917
763
  const inTokens = usage?.inputTokens || 0;
918
764
  const outTokens = usage?.outputTokens || 0;
919
765
  costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
920
- wmLog(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
921
- console.log();
766
+ output.updateCost?.(costTracker.getTotalCost());
767
+ output.log(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
768
+ output.log("system", "");
922
769
  break;
923
770
  } catch (err) {
924
- spinner.stop();
771
+ output.statusDone();
925
772
  const errMsg = err instanceof Error ? err.message : String(err);
926
773
  if (isTransientError(err) && revision < 2) {
927
- wmLog(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
774
+ output.log(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
928
775
  continue;
929
776
  }
930
- printError(`Story ${i + 1} failed: ${errMsg}`);
777
+ output.error(`Story ${i + 1} failed: ${errMsg}`);
931
778
  break;
932
779
  }
933
780
  }
@@ -935,16 +782,18 @@ ${revisionFeedback}` : ""}`;
935
782
  const maxRevisions = config.review?.maxRevisions ?? 2;
936
783
  const autoRevise = config.review?.autoRevise ?? false;
937
784
  const approvalThreshold = config.review?.approvalThreshold ?? 80;
938
- const reviewer = loadPersona("reviewer");
785
+ const reviewer = loadPersona("tech_lead");
939
786
  if (reviewer) {
940
- const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(config, reviewer.provider || "reviewer");
787
+ const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(
788
+ config,
789
+ "tech_lead"
790
+ );
941
791
  const revApiKey = config.providers[revProvider]?.apiKey;
942
792
  if (revApiKey) {
943
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
793
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
944
794
  const envVar = envMap[revProvider];
945
795
  const key = revApiKey.startsWith("{env:") ? process.env[revApiKey.slice(5, -1)] : revApiKey;
946
- if (envVar && key && !process.env[envVar])
947
- process.env[envVar] = key;
796
+ if (envVar && key && !process.env[envVar]) process.env[envVar] = key;
948
797
  }
949
798
  const reviewModel = createModel(revProvider, revModel, revHost, revCtx);
950
799
  const reviewTools = createToolDefinitions(workingDir, reviewModel, sandboxed);
@@ -955,7 +804,7 @@ ${revisionFeedback}` : ""}`;
955
804
  reviewerTools[toolName] = {
956
805
  ...toolDef,
957
806
  execute: async (input) => {
958
- wmLog("tech_lead", formatToolCall(toolName, input));
807
+ output.log("tech_lead", formatToolCallDisplay(toolName, input));
959
808
  const result = await toolDef.execute(input);
960
809
  return result;
961
810
  }
@@ -965,13 +814,9 @@ ${revisionFeedback}` : ""}`;
965
814
  let previousReviewFeedback = "";
966
815
  for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
967
816
  const isRevision = reviewRound > 0;
968
- wmCoordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
969
- wmLog("tech_lead", "Starting agent execution");
970
- const reviewSpinner = ora({
971
- stream: process.stdout,
972
- text: chalk3.white(isRevision ? "Reviewer \u2014 Re-checking after revisions" : "Reviewer \u2014 Checking code quality"),
973
- prefixText: " "
974
- }).start();
817
+ output.coordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
818
+ output.log("tech_lead", `Starting agent execution (model: ${revModel})`);
819
+ output.status(isRevision ? "Reviewer -- Re-checking after revisions" : "Reviewer -- Checking code quality");
975
820
  try {
976
821
  const previousFeedbackSection = isRevision && previousReviewFeedback ? `## Previous Review Feedback (Review ${reviewRound}/${maxRevisions})
977
822
  This is a revision attempt. The previous code was reviewed and these issues were identified:
@@ -1034,7 +879,7 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
1034
879
  - Only include stories that have ACTUAL implementation issues
1035
880
  - If ALL stories need revision, you may omit AFFECTED_STORIES (all will re-run)
1036
881
  - Be specific in AFFECTED_REASONS so developers know exactly what to fix`;
1037
- let allReviewText = "";
882
+ let reviewerOutput = "";
1038
883
  const reviewStream = streamText({
1039
884
  model: reviewModel,
1040
885
  system: reviewer.systemPrompt,
@@ -1045,95 +890,95 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
1045
890
  ...buildOllamaOptions(revProvider, revCtx),
1046
891
  onStepFinish({ text }) {
1047
892
  if (text) {
1048
- reviewSpinner.stop();
893
+ reviewerOutput += text + "\n";
894
+ output.statusDone();
1049
895
  const lines = text.split("\n").filter((l) => l.trim());
1050
896
  for (const line of lines) {
1051
- if (line.includes("::review_score::") || line.includes("::review_verdict::"))
1052
- continue;
1053
- wmLog("tech_lead", line);
897
+ if (line.includes("::review_score::") || line.includes("::review_verdict::")) continue;
898
+ output.log("tech_lead", line);
1054
899
  }
1055
900
  }
1056
901
  }
1057
902
  });
1058
903
  for await (const _chunk of reviewStream.textStream) {
1059
904
  }
1060
- const finalReviewText = await reviewStream.text;
1061
- const reviewText = finalReviewText && finalReviewText.length > allReviewText.length ? finalReviewText : allReviewText;
905
+ const reviewText = reviewerOutput;
1062
906
  const reviewUsage = await reviewStream.totalUsage;
1063
- reviewSpinner.stop();
907
+ output.statusDone();
1064
908
  const score = extractScore(reviewText);
1065
909
  const approved = score >= approvalThreshold;
1066
- wmLog("tech_lead", `::code_quality_score::${score}`);
1067
- wmLog("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
1068
- wmCoordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
910
+ output.log("tech_lead", `::code_quality_score::${score}`);
911
+ output.log("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
912
+ output.coordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
1069
913
  previousReviewFeedback = reviewText;
1070
- console.log();
1071
- costTracker.addUsage(`Reviewer (round ${reviewRound + 1})`, revProvider, revModel, reviewUsage?.inputTokens || 0, reviewUsage?.outputTokens || 0);
1072
- if (approved)
1073
- break;
914
+ output.log("system", "");
915
+ costTracker.addUsage(
916
+ `Reviewer (round ${reviewRound + 1})`,
917
+ revProvider,
918
+ revModel,
919
+ reviewUsage?.inputTokens || 0,
920
+ reviewUsage?.outputTokens || 0
921
+ );
922
+ output.updateCost?.(costTracker.getTotalCost());
923
+ if (approved) break;
1074
924
  if (reviewRound >= maxRevisions) {
1075
- console.log(chalk3.yellow(` \u26A0 Max review revisions (${maxRevisions}) reached`));
925
+ output.log("system", `Max review revisions (${maxRevisions}) reached`);
1076
926
  break;
1077
927
  }
1078
928
  let shouldRevise = autoRevise;
1079
929
  if (!autoRevise) {
1080
930
  try {
1081
- const answer = await permissions.askUser(chalk3.dim(" Revise and re-review? ") + chalk3.white(`(y/n, ${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left): `));
1082
- shouldRevise = answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
931
+ shouldRevise = await output.confirm(`Revise and re-review? (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)`);
1083
932
  } catch {
1084
933
  shouldRevise = false;
1085
934
  }
1086
935
  } else {
1087
- console.log(chalk3.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
936
+ output.log("system", `Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`);
1088
937
  }
1089
938
  if (!shouldRevise) {
1090
- console.log(chalk3.dim(" Skipping revision, proceeding to commit."));
939
+ output.log("system", "Skipping revision, proceeding to commit.");
1091
940
  break;
1092
941
  }
1093
942
  const affected = parseAffectedStories(reviewText);
1094
943
  const affectedSet = affected ? new Set(affected.stories) : null;
1095
944
  if (affected) {
1096
945
  const selectiveInfo = `stories ${affected.stories.join(", ")}`;
1097
- wmCoordinatorLog(`Selective revision: ${selectiveInfo}`);
946
+ output.coordinatorLog(`Selective revision: ${selectiveInfo}`);
1098
947
  if (Object.keys(affected.reasons).length > 0) {
1099
948
  for (const [idx, reason] of Object.entries(affected.reasons)) {
1100
- wmCoordinatorLog(` Story ${idx}: ${reason}`);
949
+ output.coordinatorLog(` Story ${idx}: ${reason}`);
1101
950
  }
1102
951
  }
1103
952
  } else {
1104
- wmCoordinatorLog("Full revision (all stories)");
953
+ output.coordinatorLog("Full revision (all stories)");
1105
954
  }
1106
- console.log(chalk3.bold("\n \u2500\u2500\u2500 Revision Pass \u2500\u2500\u2500\n"));
955
+ output.log("system", "--- Revision Pass ---");
1107
956
  for (let i = 0; i < sorted.length; i++) {
1108
957
  const story = sorted[i];
1109
958
  if (affectedSet && !affectedSet.has(i + 1)) {
1110
- wmCoordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
959
+ output.coordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
1111
960
  continue;
1112
961
  }
1113
962
  const storyPersona = loadPersona(story.persona);
1114
- if (!storyPersona)
1115
- continue;
1116
- const { provider: sProvider, model: sModel, host: sHost, contextLength: sCtx } = getProviderForPersona(config, storyPersona.provider || story.persona);
963
+ if (!storyPersona) continue;
964
+ const { provider: sProvider, model: sModel, host: sHost, contextLength: sCtx } = getProviderForPersona(
965
+ config,
966
+ storyPersona.provider || story.persona
967
+ );
1117
968
  if (sProvider) {
1118
969
  const sApiKey = config.providers[sProvider]?.apiKey;
1119
970
  if (sApiKey) {
1120
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
971
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
1121
972
  const envVar = envMap[sProvider];
1122
973
  if (envVar && !process.env[envVar]) {
1123
974
  const key = sApiKey.startsWith("{env:") ? process.env[sApiKey.slice(5, -1)] : sApiKey;
1124
- if (key)
1125
- process.env[envVar] = key;
975
+ if (key) process.env[envVar] = key;
1126
976
  }
1127
977
  }
1128
978
  }
1129
- wmCoordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
1130
- wmLog(story.persona, `Starting revision: ${story.title}`);
1131
- const revSpinner = ora({
1132
- stream: process.stdout,
1133
- text: "",
1134
- prefixText: "",
1135
- spinner: "dots"
1136
- }).start();
979
+ output.coordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
980
+ output.log(story.persona, `Starting revision: ${story.title} (model: ${sModel})`);
981
+ output.status("");
1137
982
  const storyModel = createModel(sProvider, sModel, sHost, sCtx);
1138
983
  const storyAllTools = createToolDefinitions(workingDir, storyModel, sandboxed);
1139
984
  const storyTools = {};
@@ -1143,13 +988,12 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
1143
988
  storyTools[toolName] = {
1144
989
  ...toolDef,
1145
990
  execute: async (input) => {
1146
- const allowed = await permissions.checkPermission(toolName, input);
1147
- if (!allowed)
1148
- return "Tool execution denied by user.";
1149
- revSpinner.stop();
1150
- wmLog(story.persona, formatToolCall(toolName, input));
991
+ const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
992
+ if (!allowed) return "Tool execution denied by user.";
993
+ output.statusDone();
994
+ output.log(story.persona, formatToolCallDisplay(toolName, input));
1151
995
  const result = await toolDef.execute(input);
1152
- revSpinner.start();
996
+ output.status("");
1153
997
  return result;
1154
998
  }
1155
999
  };
@@ -1186,12 +1030,11 @@ ${story.description}`,
1186
1030
  ...buildOllamaOptions(sProvider, sCtx),
1187
1031
  onStepFinish({ text }) {
1188
1032
  if (text) {
1189
- revSpinner.stop();
1033
+ output.statusDone();
1190
1034
  const lines = text.split("\n").filter((l) => l.trim());
1191
1035
  for (const line of lines) {
1192
- if (line.includes("::"))
1193
- continue;
1194
- wmLog(story.persona, line);
1036
+ if (line.includes("::")) continue;
1037
+ output.log(story.persona, line);
1195
1038
  }
1196
1039
  }
1197
1040
  }
@@ -1199,80 +1042,87 @@ ${story.description}`,
1199
1042
  for await (const _chunk of revStream.textStream) {
1200
1043
  }
1201
1044
  const revUsage = await revStream.totalUsage;
1202
- revSpinner.stop();
1203
- costTracker.addUsage(`${storyPersona.name} (revision)`, sProvider, sModel, revUsage?.inputTokens || 0, revUsage?.outputTokens || 0);
1204
- wmLog(story.persona, `${story.title} \u2014 revision complete!`);
1045
+ output.statusDone();
1046
+ costTracker.addUsage(
1047
+ `${storyPersona.name} (revision)`,
1048
+ sProvider,
1049
+ sModel,
1050
+ revUsage?.inputTokens || 0,
1051
+ revUsage?.outputTokens || 0
1052
+ );
1053
+ output.updateCost?.(costTracker.getTotalCost());
1054
+ output.log(story.persona, `${story.title} \u2014 revision complete!`);
1205
1055
  } catch (err) {
1206
- revSpinner.stop();
1207
- console.log(chalk3.yellow(` \u26A0 Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`));
1056
+ output.statusDone();
1057
+ output.log("system", `Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
1208
1058
  }
1209
1059
  }
1210
- console.log();
1060
+ output.log("system", "");
1211
1061
  } catch (err) {
1212
- reviewSpinner.stop();
1213
- console.log(chalk3.yellow(` \u26A0 Review skipped: ${err instanceof Error ? err.message : String(err)}`));
1214
- console.log();
1062
+ output.statusDone();
1063
+ output.log("system", `Review skipped: ${err instanceof Error ? err.message : String(err)}`);
1064
+ output.log("system", "");
1215
1065
  break;
1216
1066
  }
1217
1067
  }
1218
1068
  }
1219
1069
  try {
1220
- const { execSync: execSync2 } = await import("child_process");
1070
+ const { execSync } = await import("child_process");
1221
1071
  try {
1222
- execSync2("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1072
+ execSync("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1223
1073
  } catch {
1224
- wmCoordinatorLog("Initializing git repository...");
1225
- execSync2("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1226
- const fs2 = await import("fs");
1074
+ output.coordinatorLog("Initializing git repository...");
1075
+ execSync("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1076
+ const fs3 = await import("fs");
1227
1077
  const gitignorePath = `${workingDir}/.gitignore`;
1228
- if (!fs2.existsSync(gitignorePath)) {
1229
- fs2.writeFileSync(gitignorePath, "node_modules/\ndist/\n.env\n.workermill/\n*.log\n", "utf-8");
1078
+ if (!fs3.existsSync(gitignorePath)) {
1079
+ fs3.writeFileSync(gitignorePath, "node_modules/\ndist/\n.env\n.workermill/\n*.log\n", "utf-8");
1230
1080
  }
1231
- wmCoordinatorLog("Git repo initialized");
1081
+ output.coordinatorLog("Git repo initialized");
1232
1082
  }
1233
- const diff = execSync2("git diff --stat", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1234
- const untracked = execSync2("git ls-files --others --exclude-standard", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1083
+ const diff = execSync("git diff --stat", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1084
+ const untracked = execSync("git ls-files --others --exclude-standard", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1235
1085
  const hasChanges = diff || untracked;
1236
1086
  if (hasChanges) {
1237
- console.log(chalk3.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
1087
+ output.log("system", "--- Changes ---");
1238
1088
  if (diff) {
1239
- console.log(chalk3.dim(" " + diff.split("\n").join("\n ")));
1089
+ output.log("system", diff);
1240
1090
  }
1241
1091
  if (untracked) {
1242
1092
  const untrackedFiles = untracked.split("\n");
1243
- console.log(chalk3.dim(" New files:"));
1093
+ output.log("system", "New files:");
1244
1094
  for (const f of untrackedFiles) {
1245
- console.log(chalk3.dim(` + ${f}`));
1095
+ output.log("system", ` + ${f}`);
1246
1096
  }
1247
1097
  }
1248
- console.log();
1098
+ output.log("system", "");
1249
1099
  if (!trustAll) {
1250
- const answer = await permissions.askUser(chalk3.dim(" Commit these changes? (y/n): "));
1251
- if (answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes") {
1100
+ const commitConfirmed = await output.confirm("Commit these changes?");
1101
+ if (commitConfirmed) {
1252
1102
  const filesToStage = [...context.filesCreated, ...context.filesModified].filter(Boolean);
1253
1103
  if (filesToStage.length > 0) {
1254
1104
  for (const f of filesToStage) {
1255
1105
  try {
1256
- execSync2(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
1106
+ execSync(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
1257
1107
  } catch {
1258
1108
  }
1259
1109
  }
1260
1110
  } else {
1261
- execSync2("git add -u", { cwd: workingDir, stdio: "pipe" });
1111
+ execSync("git add -u", { cwd: workingDir, stdio: "pipe" });
1262
1112
  }
1263
1113
  const storyTitles = sorted.map((s) => s.title).join(", ");
1264
1114
  const msg = `feat: ${storyTitles}`.slice(0, 72);
1265
- execSync2(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
1266
- console.log(chalk3.green(" \u2713 Changes committed"));
1115
+ execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
1116
+ output.log("system", "Changes committed");
1267
1117
  }
1268
1118
  }
1269
1119
  }
1270
1120
  } catch (err) {
1271
1121
  }
1272
- console.log(chalk3.bold(" \u2500\u2500\u2500 Session Complete \u2500\u2500\u2500"));
1273
- console.log();
1274
- console.log(chalk3.dim(" " + costTracker.getSummary().split("\n").join("\n ")));
1275
- console.log();
1122
+ output.log("system", "--- Session Complete ---");
1123
+ output.log("system", "");
1124
+ output.log("system", costTracker.getSummary());
1125
+ output.log("system", "");
1276
1126
  }
1277
1127
  export {
1278
1128
  classifyComplexity,