workermill 0.3.1 → 0.3.2

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}`);
96
+ return task;
299
97
  }
300
- function wmCoordinatorLog(message) {
301
- console.log(chalk2.cyan("[coordinator] ") + chalk2.white(message));
302
- info(`[coordinator] ${message}`);
303
- }
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,9 @@ 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
+ let stories = parseStoriesFromText(planText, output);
546
405
  if (stories.length === 0) {
547
- console.log(chalk3.yellow(" \u26A0 Planner didn't produce structured stories, falling back to single story"));
406
+ output.log("system", "Planner didn't produce structured stories, falling back to single story");
548
407
  stories = [{
549
408
  id: "implement",
550
409
  title: userTask.slice(0, 60),
@@ -554,12 +413,11 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
554
413
  }
555
414
  return stories;
556
415
  }
557
- function parseStoriesFromText(text) {
416
+ function parseStoriesFromText(text, output) {
558
417
  const codeBlocks = [...text.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
559
418
  for (const match of codeBlocks) {
560
419
  const stories2 = tryParseStories(match[1].trim());
561
- if (stories2)
562
- return stories2;
420
+ if (stories2) return stories2;
563
421
  }
564
422
  const storiesIdx = text.indexOf('"stories"');
565
423
  if (storiesIdx !== -1) {
@@ -568,8 +426,7 @@ function parseStoriesFromText(text) {
568
426
  const json = extractBalancedJSON(text, braceStart);
569
427
  if (json) {
570
428
  const stories2 = tryParseStories(json);
571
- if (stories2)
572
- return stories2;
429
+ if (stories2) return stories2;
573
430
  }
574
431
  }
575
432
  }
@@ -578,27 +435,23 @@ function parseStoriesFromText(text) {
578
435
  const json = extractBalancedJSON(text, arrayStart);
579
436
  if (json) {
580
437
  const stories2 = tryParseStories(json);
581
- if (stories2)
582
- return stories2;
438
+ if (stories2) return stories2;
583
439
  }
584
440
  }
585
441
  const stories = tryParseStories(text.trim());
586
- if (stories)
587
- return stories;
442
+ if (stories) return stories;
588
443
  const preview = text.slice(0, 500);
589
- console.log(chalk3.dim(` (planner output preview: ${preview}${text.length > 500 ? "..." : ""})`));
444
+ output.log("system", `(planner output preview: ${preview}${text.length > 500 ? "..." : ""})`);
590
445
  return [];
591
446
  }
592
447
  function tryParseStories(text) {
593
448
  try {
594
449
  const parsed = JSON.parse(text);
595
450
  if (Array.isArray(parsed)) {
596
- if (parsed.length > 0 && parsed[0].persona)
597
- return parsed;
451
+ if (parsed.length > 0 && parsed[0].persona) return parsed;
598
452
  }
599
453
  if (parsed && Array.isArray(parsed.stories)) {
600
- if (parsed.stories.length > 0)
601
- return parsed.stories;
454
+ if (parsed.stories.length > 0) return parsed.stories;
602
455
  }
603
456
  } catch {
604
457
  }
@@ -607,8 +460,7 @@ function tryParseStories(text) {
607
460
  function extractBalancedJSON(text, start) {
608
461
  const open = text[start];
609
462
  const close = open === "{" ? "}" : open === "[" ? "]" : null;
610
- if (!close)
611
- return null;
463
+ if (!close) return null;
612
464
  let depth = 0;
613
465
  let inString = false;
614
466
  let escape = false;
@@ -626,10 +478,8 @@ function extractBalancedJSON(text, start) {
626
478
  inString = !inString;
627
479
  continue;
628
480
  }
629
- if (inString)
630
- continue;
631
- if (ch === open)
632
- depth++;
481
+ if (inString) continue;
482
+ if (ch === open) depth++;
633
483
  if (ch === close) {
634
484
  depth--;
635
485
  if (depth === 0) {
@@ -640,36 +490,31 @@ function extractBalancedJSON(text, start) {
640
490
  return null;
641
491
  }
642
492
  function extractScore(text) {
643
- const markerMatch = text.match(/::review_score::(\d+)/);
644
- if (markerMatch)
645
- return parseInt(markerMatch[1], 10);
493
+ const markerMatches = [...text.matchAll(/::review_score::(\d+)/g)];
494
+ if (markerMatches.length > 0) {
495
+ return parseInt(markerMatches[markerMatches.length - 1][1], 10);
496
+ }
646
497
  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
498
+ /\bscore[:\s]+(\d+)\s*\/\s*100/gi,
499
+ /\bscore[:\s]+(\d+)/gi,
500
+ /\brating[:\s]+(\d+)/gi
651
501
  ];
652
502
  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;
503
+ const matches = [...text.matchAll(pattern)];
504
+ if (matches.length > 0) {
505
+ const n = parseInt(matches[matches.length - 1][1], 10);
506
+ if (n >= 0 && n <= 100) return n;
658
507
  }
659
508
  }
660
- if (/\bapprove/i.test(text))
661
- return 85;
662
- if (/\brevis/i.test(text))
663
- return 60;
509
+ if (/\bapprove/i.test(text)) return 85;
510
+ if (/\brevis/i.test(text)) return 60;
664
511
  return 75;
665
512
  }
666
513
  function parseAffectedStories(text) {
667
514
  const storiesMatch = text.match(/AFFECTED_STORIES:\s*\[([^\]]+)\]/i);
668
- if (!storiesMatch)
669
- return null;
515
+ if (!storiesMatch) return null;
670
516
  const stories = storiesMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
671
- if (stories.length === 0)
672
- return null;
517
+ if (stories.length === 0) return null;
673
518
  let reasons = {};
674
519
  const reasonsMatch = text.match(/AFFECTED_REASONS:\s*(\{[\s\S]*?\})/i);
675
520
  if (reasonsMatch) {
@@ -686,7 +531,8 @@ function parseAffectedStories(text) {
686
531
  }
687
532
  return { stories, reasons };
688
533
  }
689
- async function runOrchestration(config, userTask, trustAll, sandboxed = true, agentRl) {
534
+ async function runOrchestration(config, userTask, trustAll, sandboxed, output) {
535
+ userTask = resolveTaskInput(userTask, process.cwd());
690
536
  const costTracker = new CostTracker();
691
537
  const context = {
692
538
  filesCreated: [],
@@ -694,18 +540,15 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
694
540
  decisions: [],
695
541
  learnings: []
696
542
  };
697
- const permissions = new PermissionManager(trustAll);
698
- if (agentRl)
699
- permissions.setReadline(agentRl);
543
+ const sessionAllow = /* @__PURE__ */ new Set();
700
544
  const workingDir = process.cwd();
701
- const plannerStories = await planStories(config, userTask, workingDir, sandboxed);
702
- wmLog("planner", `Plan generated: ${plannerStories.length} stories`);
545
+ const plannerStories = await planStories(config, userTask, workingDir, sandboxed, output);
546
+ output.log("planner", `Plan generated: ${plannerStories.length} stories`);
703
547
  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(", ")})` : ""}`);
548
+ output.log("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
706
549
  });
707
- wmLog("planner", `Plan validated: ${plannerStories.length} stories. Task queued for execution.`);
708
- console.log();
550
+ output.log("planner", `Plan validated: ${plannerStories.length} stories. Task queued for execution.`);
551
+ output.log("system", "");
709
552
  if (config.review?.useCritic) {
710
553
  const critic = loadPersona("critic");
711
554
  if (critic) {
@@ -719,14 +562,14 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
719
562
  criticReadOnly[name] = {
720
563
  ...toolDef,
721
564
  execute: async (input) => {
722
- wmLog("critic", formatToolCall(name, input));
565
+ output.log("critic", formatToolCallDisplay(name, input));
723
566
  const result = await toolDef.execute(input);
724
567
  return result;
725
568
  }
726
569
  };
727
570
  }
728
571
  }
729
- const criticSpinner = ora({ stream: process.stdout, text: chalk3.white("Critic reviewing plan..."), prefixText: " " }).start();
572
+ output.status("Critic reviewing plan...");
730
573
  const criticStream = streamText({
731
574
  model: criticModel,
732
575
  system: critic.systemPrompt,
@@ -742,52 +585,47 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
742
585
  for await (const _chunk of criticStream.textStream) {
743
586
  }
744
587
  const criticText = await criticStream.text;
745
- criticSpinner.stop();
588
+ output.statusDone();
746
589
  const score = extractScore(criticText);
747
- wmLog("critic", `::review_score::${score}`);
748
- wmLog("critic", score >= 80 ? "Plan approved" : "Plan needs revision");
749
- console.log();
590
+ output.log("critic", `::review_score::${score}`);
591
+ output.log("critic", score >= 80 ? "Plan approved" : "Plan needs revision");
592
+ output.log("system", "");
750
593
  }
751
594
  }
752
595
  const sorted = topologicalSort(plannerStories);
753
596
  if (!trustAll) {
754
- let answer = "n";
597
+ let proceed = false;
755
598
  try {
756
- answer = await permissions.askUser(chalk3.dim(" Execute this plan? (y/n): "));
599
+ proceed = await output.confirm("Execute this plan?");
757
600
  } catch {
758
601
  }
759
- if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
760
- console.log(chalk3.dim(" Plan cancelled.\n"));
602
+ if (!proceed) {
603
+ output.log("system", "Plan cancelled.");
761
604
  return;
762
605
  }
763
- console.log();
606
+ output.log("system", "");
764
607
  }
765
608
  for (let i = 0; i < sorted.length; i++) {
766
609
  const story = sorted[i];
767
610
  const persona = loadPersona(story.persona);
768
611
  if (!persona) {
769
- printError(`Unknown persona: ${story.persona}`);
612
+ output.error(`Unknown persona: ${story.persona}`);
770
613
  continue;
771
614
  }
772
- const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config, persona.provider || story.persona);
615
+ const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(
616
+ config,
617
+ persona.provider || story.persona
618
+ );
773
619
  if (apiKey) {
774
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
620
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
775
621
  const envVar = envMap[provider];
776
- if (envVar && !process.env[envVar])
777
- process.env[envVar] = apiKey;
622
+ if (envVar && !process.env[envVar]) process.env[envVar] = apiKey;
778
623
  }
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();
624
+ output.log("system", `--- Story ${i + 1}/${sorted.length} ---`);
625
+ output.coordinatorLog(`Task claimed by orchestrator`);
626
+ output.log(story.persona, `Starting ${story.title}`);
627
+ output.log(story.persona, `Executing story with AIClient (model: ${modelName})...`);
628
+ output.status("");
791
629
  const model = createModel(provider, modelName, host, contextLength);
792
630
  const allTools = createToolDefinitions(workingDir, model, sandboxed);
793
631
  const personaTools = {};
@@ -798,18 +636,17 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
798
636
  personaTools[toolName] = {
799
637
  ...toolDef,
800
638
  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;
639
+ const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
640
+ if (!allowed) return "Tool execution denied by user.";
641
+ const sig = `${toolName}:${JSON.stringify(input)}`;
642
+ const isDuplicate = sig === lastToolCall;
643
+ lastToolCall = sig;
807
644
  if (!isDuplicate) {
808
- spinner.stop();
809
- wmLog(story.persona, formatToolCall(toolName, input));
645
+ output.statusDone();
646
+ output.toolCall(story.persona, toolName, input);
810
647
  }
811
648
  const result = await toolDef.execute(input);
812
- spinner.start();
649
+ output.status("");
813
650
  return result;
814
651
  }
815
652
  };
@@ -872,12 +709,11 @@ ${revisionFeedback}` : ""}`;
872
709
  ...buildOllamaOptions(provider, contextLength),
873
710
  onStepFinish({ text: text2 }) {
874
711
  if (text2) {
875
- spinner.stop();
712
+ output.statusDone();
876
713
  const lines = text2.split("\n").filter((l) => l.trim());
877
714
  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);
715
+ if (line.includes("::decision::") || line.includes("::learning::") || line.includes("::file_created::") || line.includes("::file_modified::")) continue;
716
+ output.log(story.persona, line);
881
717
  }
882
718
  }
883
719
  }
@@ -887,13 +723,13 @@ ${revisionFeedback}` : ""}`;
887
723
  const text = await stream.text;
888
724
  allText = text;
889
725
  const usage = await stream.totalUsage;
890
- spinner.stop();
726
+ output.statusDone();
891
727
  const decisionMatches = text.match(/::decision::(.*?)(?=::\w+::|$)/gs);
892
728
  if (decisionMatches) {
893
729
  for (const m of decisionMatches) {
894
730
  const decision = m.replace("::decision::", "").trim();
895
731
  context.decisions.push(decision);
896
- wmLog(story.persona, decision);
732
+ output.log(story.persona, decision);
897
733
  }
898
734
  }
899
735
  const learningMatches = text.match(/::learning::(.*?)(?=::\w+::|$)/gs);
@@ -917,17 +753,17 @@ ${revisionFeedback}` : ""}`;
917
753
  const inTokens = usage?.inputTokens || 0;
918
754
  const outTokens = usage?.outputTokens || 0;
919
755
  costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
920
- wmLog(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
921
- console.log();
756
+ output.log(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
757
+ output.log("system", "");
922
758
  break;
923
759
  } catch (err) {
924
- spinner.stop();
760
+ output.statusDone();
925
761
  const errMsg = err instanceof Error ? err.message : String(err);
926
762
  if (isTransientError(err) && revision < 2) {
927
- wmLog(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
763
+ output.log(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
928
764
  continue;
929
765
  }
930
- printError(`Story ${i + 1} failed: ${errMsg}`);
766
+ output.error(`Story ${i + 1} failed: ${errMsg}`);
931
767
  break;
932
768
  }
933
769
  }
@@ -935,16 +771,18 @@ ${revisionFeedback}` : ""}`;
935
771
  const maxRevisions = config.review?.maxRevisions ?? 2;
936
772
  const autoRevise = config.review?.autoRevise ?? false;
937
773
  const approvalThreshold = config.review?.approvalThreshold ?? 80;
938
- const reviewer = loadPersona("reviewer");
774
+ const reviewer = loadPersona("tech_lead");
939
775
  if (reviewer) {
940
- const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(config, reviewer.provider || "reviewer");
776
+ const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(
777
+ config,
778
+ "tech_lead"
779
+ );
941
780
  const revApiKey = config.providers[revProvider]?.apiKey;
942
781
  if (revApiKey) {
943
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
782
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
944
783
  const envVar = envMap[revProvider];
945
784
  const key = revApiKey.startsWith("{env:") ? process.env[revApiKey.slice(5, -1)] : revApiKey;
946
- if (envVar && key && !process.env[envVar])
947
- process.env[envVar] = key;
785
+ if (envVar && key && !process.env[envVar]) process.env[envVar] = key;
948
786
  }
949
787
  const reviewModel = createModel(revProvider, revModel, revHost, revCtx);
950
788
  const reviewTools = createToolDefinitions(workingDir, reviewModel, sandboxed);
@@ -955,7 +793,7 @@ ${revisionFeedback}` : ""}`;
955
793
  reviewerTools[toolName] = {
956
794
  ...toolDef,
957
795
  execute: async (input) => {
958
- wmLog("tech_lead", formatToolCall(toolName, input));
796
+ output.log("tech_lead", formatToolCallDisplay(toolName, input));
959
797
  const result = await toolDef.execute(input);
960
798
  return result;
961
799
  }
@@ -965,13 +803,9 @@ ${revisionFeedback}` : ""}`;
965
803
  let previousReviewFeedback = "";
966
804
  for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
967
805
  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();
806
+ output.coordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
807
+ output.log("tech_lead", "Starting agent execution");
808
+ output.status(isRevision ? "Reviewer -- Re-checking after revisions" : "Reviewer -- Checking code quality");
975
809
  try {
976
810
  const previousFeedbackSection = isRevision && previousReviewFeedback ? `## Previous Review Feedback (Review ${reviewRound}/${maxRevisions})
977
811
  This is a revision attempt. The previous code was reviewed and these issues were identified:
@@ -1034,7 +868,7 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
1034
868
  - Only include stories that have ACTUAL implementation issues
1035
869
  - If ALL stories need revision, you may omit AFFECTED_STORIES (all will re-run)
1036
870
  - Be specific in AFFECTED_REASONS so developers know exactly what to fix`;
1037
- let allReviewText = "";
871
+ let reviewerOutput = "";
1038
872
  const reviewStream = streamText({
1039
873
  model: reviewModel,
1040
874
  system: reviewer.systemPrompt,
@@ -1045,95 +879,94 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
1045
879
  ...buildOllamaOptions(revProvider, revCtx),
1046
880
  onStepFinish({ text }) {
1047
881
  if (text) {
1048
- reviewSpinner.stop();
882
+ reviewerOutput += text + "\n";
883
+ output.statusDone();
1049
884
  const lines = text.split("\n").filter((l) => l.trim());
1050
885
  for (const line of lines) {
1051
- if (line.includes("::review_score::") || line.includes("::review_verdict::"))
1052
- continue;
1053
- wmLog("tech_lead", line);
886
+ if (line.includes("::review_score::") || line.includes("::review_verdict::")) continue;
887
+ output.log("tech_lead", line);
1054
888
  }
1055
889
  }
1056
890
  }
1057
891
  });
1058
892
  for await (const _chunk of reviewStream.textStream) {
1059
893
  }
1060
- const finalReviewText = await reviewStream.text;
1061
- const reviewText = finalReviewText && finalReviewText.length > allReviewText.length ? finalReviewText : allReviewText;
894
+ const reviewText = reviewerOutput;
1062
895
  const reviewUsage = await reviewStream.totalUsage;
1063
- reviewSpinner.stop();
896
+ output.statusDone();
1064
897
  const score = extractScore(reviewText);
1065
898
  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)`);
899
+ output.log("tech_lead", `::code_quality_score::${score}`);
900
+ output.log("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
901
+ output.coordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
1069
902
  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;
903
+ output.log("system", "");
904
+ costTracker.addUsage(
905
+ `Reviewer (round ${reviewRound + 1})`,
906
+ revProvider,
907
+ revModel,
908
+ reviewUsage?.inputTokens || 0,
909
+ reviewUsage?.outputTokens || 0
910
+ );
911
+ if (approved) break;
1074
912
  if (reviewRound >= maxRevisions) {
1075
- console.log(chalk3.yellow(` \u26A0 Max review revisions (${maxRevisions}) reached`));
913
+ output.log("system", `Max review revisions (${maxRevisions}) reached`);
1076
914
  break;
1077
915
  }
1078
916
  let shouldRevise = autoRevise;
1079
917
  if (!autoRevise) {
1080
918
  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";
919
+ shouldRevise = await output.confirm(`Revise and re-review? (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)`);
1083
920
  } catch {
1084
921
  shouldRevise = false;
1085
922
  }
1086
923
  } else {
1087
- console.log(chalk3.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
924
+ output.log("system", `Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`);
1088
925
  }
1089
926
  if (!shouldRevise) {
1090
- console.log(chalk3.dim(" Skipping revision, proceeding to commit."));
927
+ output.log("system", "Skipping revision, proceeding to commit.");
1091
928
  break;
1092
929
  }
1093
930
  const affected = parseAffectedStories(reviewText);
1094
931
  const affectedSet = affected ? new Set(affected.stories) : null;
1095
932
  if (affected) {
1096
933
  const selectiveInfo = `stories ${affected.stories.join(", ")}`;
1097
- wmCoordinatorLog(`Selective revision: ${selectiveInfo}`);
934
+ output.coordinatorLog(`Selective revision: ${selectiveInfo}`);
1098
935
  if (Object.keys(affected.reasons).length > 0) {
1099
936
  for (const [idx, reason] of Object.entries(affected.reasons)) {
1100
- wmCoordinatorLog(` Story ${idx}: ${reason}`);
937
+ output.coordinatorLog(` Story ${idx}: ${reason}`);
1101
938
  }
1102
939
  }
1103
940
  } else {
1104
- wmCoordinatorLog("Full revision (all stories)");
941
+ output.coordinatorLog("Full revision (all stories)");
1105
942
  }
1106
- console.log(chalk3.bold("\n \u2500\u2500\u2500 Revision Pass \u2500\u2500\u2500\n"));
943
+ output.log("system", "--- Revision Pass ---");
1107
944
  for (let i = 0; i < sorted.length; i++) {
1108
945
  const story = sorted[i];
1109
946
  if (affectedSet && !affectedSet.has(i + 1)) {
1110
- wmCoordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
947
+ output.coordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
1111
948
  continue;
1112
949
  }
1113
950
  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);
951
+ if (!storyPersona) continue;
952
+ const { provider: sProvider, model: sModel, host: sHost, contextLength: sCtx } = getProviderForPersona(
953
+ config,
954
+ storyPersona.provider || story.persona
955
+ );
1117
956
  if (sProvider) {
1118
957
  const sApiKey = config.providers[sProvider]?.apiKey;
1119
958
  if (sApiKey) {
1120
- const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
959
+ const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
1121
960
  const envVar = envMap[sProvider];
1122
961
  if (envVar && !process.env[envVar]) {
1123
962
  const key = sApiKey.startsWith("{env:") ? process.env[sApiKey.slice(5, -1)] : sApiKey;
1124
- if (key)
1125
- process.env[envVar] = key;
963
+ if (key) process.env[envVar] = key;
1126
964
  }
1127
965
  }
1128
966
  }
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();
967
+ output.coordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
968
+ output.log(story.persona, `Starting revision: ${story.title}`);
969
+ output.status("");
1137
970
  const storyModel = createModel(sProvider, sModel, sHost, sCtx);
1138
971
  const storyAllTools = createToolDefinitions(workingDir, storyModel, sandboxed);
1139
972
  const storyTools = {};
@@ -1143,13 +976,12 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
1143
976
  storyTools[toolName] = {
1144
977
  ...toolDef,
1145
978
  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));
979
+ const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
980
+ if (!allowed) return "Tool execution denied by user.";
981
+ output.statusDone();
982
+ output.log(story.persona, formatToolCallDisplay(toolName, input));
1151
983
  const result = await toolDef.execute(input);
1152
- revSpinner.start();
984
+ output.status("");
1153
985
  return result;
1154
986
  }
1155
987
  };
@@ -1186,12 +1018,11 @@ ${story.description}`,
1186
1018
  ...buildOllamaOptions(sProvider, sCtx),
1187
1019
  onStepFinish({ text }) {
1188
1020
  if (text) {
1189
- revSpinner.stop();
1021
+ output.statusDone();
1190
1022
  const lines = text.split("\n").filter((l) => l.trim());
1191
1023
  for (const line of lines) {
1192
- if (line.includes("::"))
1193
- continue;
1194
- wmLog(story.persona, line);
1024
+ if (line.includes("::")) continue;
1025
+ output.log(story.persona, line);
1195
1026
  }
1196
1027
  }
1197
1028
  }
@@ -1199,80 +1030,86 @@ ${story.description}`,
1199
1030
  for await (const _chunk of revStream.textStream) {
1200
1031
  }
1201
1032
  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!`);
1033
+ output.statusDone();
1034
+ costTracker.addUsage(
1035
+ `${storyPersona.name} (revision)`,
1036
+ sProvider,
1037
+ sModel,
1038
+ revUsage?.inputTokens || 0,
1039
+ revUsage?.outputTokens || 0
1040
+ );
1041
+ output.log(story.persona, `${story.title} \u2014 revision complete!`);
1205
1042
  } catch (err) {
1206
- revSpinner.stop();
1207
- console.log(chalk3.yellow(` \u26A0 Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`));
1043
+ output.statusDone();
1044
+ output.log("system", `Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
1208
1045
  }
1209
1046
  }
1210
- console.log();
1047
+ output.log("system", "");
1211
1048
  } catch (err) {
1212
- reviewSpinner.stop();
1213
- console.log(chalk3.yellow(` \u26A0 Review skipped: ${err instanceof Error ? err.message : String(err)}`));
1214
- console.log();
1049
+ output.statusDone();
1050
+ output.log("system", `Review skipped: ${err instanceof Error ? err.message : String(err)}`);
1051
+ output.log("system", "");
1215
1052
  break;
1216
1053
  }
1217
1054
  }
1218
1055
  }
1219
1056
  try {
1220
- const { execSync: execSync2 } = await import("child_process");
1057
+ const { execSync } = await import("child_process");
1221
1058
  try {
1222
- execSync2("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1059
+ execSync("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1223
1060
  } catch {
1224
- wmCoordinatorLog("Initializing git repository...");
1225
- execSync2("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1226
- const fs2 = await import("fs");
1061
+ output.coordinatorLog("Initializing git repository...");
1062
+ execSync("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1063
+ const fs3 = await import("fs");
1227
1064
  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");
1065
+ if (!fs3.existsSync(gitignorePath)) {
1066
+ fs3.writeFileSync(gitignorePath, "node_modules/\ndist/\n.env\n.workermill/\n*.log\n", "utf-8");
1230
1067
  }
1231
- wmCoordinatorLog("Git repo initialized");
1068
+ output.coordinatorLog("Git repo initialized");
1232
1069
  }
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();
1070
+ const diff = execSync("git diff --stat", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1071
+ const untracked = execSync("git ls-files --others --exclude-standard", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1235
1072
  const hasChanges = diff || untracked;
1236
1073
  if (hasChanges) {
1237
- console.log(chalk3.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
1074
+ output.log("system", "--- Changes ---");
1238
1075
  if (diff) {
1239
- console.log(chalk3.dim(" " + diff.split("\n").join("\n ")));
1076
+ output.log("system", diff);
1240
1077
  }
1241
1078
  if (untracked) {
1242
1079
  const untrackedFiles = untracked.split("\n");
1243
- console.log(chalk3.dim(" New files:"));
1080
+ output.log("system", "New files:");
1244
1081
  for (const f of untrackedFiles) {
1245
- console.log(chalk3.dim(` + ${f}`));
1082
+ output.log("system", ` + ${f}`);
1246
1083
  }
1247
1084
  }
1248
- console.log();
1085
+ output.log("system", "");
1249
1086
  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") {
1087
+ const commitConfirmed = await output.confirm("Commit these changes?");
1088
+ if (commitConfirmed) {
1252
1089
  const filesToStage = [...context.filesCreated, ...context.filesModified].filter(Boolean);
1253
1090
  if (filesToStage.length > 0) {
1254
1091
  for (const f of filesToStage) {
1255
1092
  try {
1256
- execSync2(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
1093
+ execSync(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
1257
1094
  } catch {
1258
1095
  }
1259
1096
  }
1260
1097
  } else {
1261
- execSync2("git add -u", { cwd: workingDir, stdio: "pipe" });
1098
+ execSync("git add -u", { cwd: workingDir, stdio: "pipe" });
1262
1099
  }
1263
1100
  const storyTitles = sorted.map((s) => s.title).join(", ");
1264
1101
  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"));
1102
+ execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
1103
+ output.log("system", "Changes committed");
1267
1104
  }
1268
1105
  }
1269
1106
  }
1270
1107
  } catch (err) {
1271
1108
  }
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();
1109
+ output.log("system", "--- Session Complete ---");
1110
+ output.log("system", "");
1111
+ output.log("system", costTracker.getSummary());
1112
+ output.log("system", "");
1276
1113
  }
1277
1114
  export {
1278
1115
  classifyComplexity,