workermill 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,14 @@
1
1
  import {
2
2
  CostTracker,
3
- PermissionManager,
3
+ __dirname,
4
+ buildOllamaOptions,
4
5
  createModel,
5
6
  createToolDefinitions,
6
- getPersonaEmoji,
7
- getProviderForPersona,
8
- printError,
9
- printToolCall,
10
- printToolResult,
11
- wmCoordinatorLog,
12
- wmLog,
13
- wmLogPrefix
14
- } from "./chunk-3KIFXIBC.js";
15
- import {
16
- __dirname
17
- } from "./chunk-2NTK7H4W.js";
7
+ getProviderForPersona
8
+ } from "./chunk-VC6VNVEY.js";
18
9
 
19
10
  // src/orchestrator.js
20
- import chalk from "chalk";
11
+ import chalk3 from "chalk";
21
12
  import ora from "ora";
22
13
  import { streamText, generateObject, generateText, stepCountIs } from "ai";
23
14
  import { z } from "zod";
@@ -58,10 +49,14 @@ function parsePersonaFile(content) {
58
49
  }
59
50
  function loadPersona(slug) {
60
51
  const locations = [
52
+ // Project-level persona overrides
61
53
  path.join(process.cwd(), ".workermill", "personas", `${slug}.md`),
54
+ // User-level persona overrides
62
55
  path.join(os.homedir(), ".workermill", "personas", `${slug}.md`),
56
+ // Bundled with the npm package (cli/personas/)
57
+ path.join(import.meta.dirname || __dirname, "../personas", `${slug}.md`),
58
+ // Dev mode — resolve from monorepo
63
59
  path.join(import.meta.dirname || __dirname, "../../packages/engine/src/personas", `${slug}.md`),
64
- // Also try relative to the repo root
65
60
  path.join(process.cwd(), "packages/engine/src/personas", `${slug}.md`)
66
61
  ];
67
62
  for (const loc of locations) {
@@ -85,16 +80,341 @@ function loadPersona(slug) {
85
80
  };
86
81
  }
87
82
 
83
+ // src/permissions.js
84
+ import readline from "readline";
85
+ import chalk from "chalk";
86
+ var READ_TOOLS = /* @__PURE__ */ new Set(["read_file", "glob", "grep", "ls", "sub_agent"]);
87
+ var DANGEROUS_PATTERNS = [
88
+ { pattern: /rm\s+(-[a-z]*f|-[a-z]*r|--force|--recursive)/i, label: "recursive/forced delete" },
89
+ { pattern: /git\s+reset\s+--hard/i, label: "hard reset" },
90
+ { pattern: /git\s+push\s+.*--force/i, label: "force push" },
91
+ { pattern: /git\s+clean\s+-[a-z]*f/i, label: "git clean" },
92
+ { pattern: /drop\s+table/i, label: "drop table" },
93
+ { pattern: /truncate\s+/i, label: "truncate" },
94
+ { pattern: /DELETE\s+FROM\s+\w+\s*;/i, label: "DELETE without WHERE" },
95
+ { pattern: /chmod\s+777/i, label: "chmod 777" },
96
+ { pattern: />(\/dev\/sda|\/dev\/disk)/i, label: "write to disk device" }
97
+ ];
98
+ function isDangerous(command) {
99
+ for (const { pattern, label } of DANGEROUS_PATTERNS) {
100
+ if (pattern.test(command))
101
+ return label;
102
+ }
103
+ return null;
104
+ }
105
+ var PermissionManager = class {
106
+ sessionAllow = /* @__PURE__ */ new Set();
107
+ trustAll;
108
+ configTrust;
109
+ rl = null;
110
+ cancelCurrentPrompt = null;
111
+ /** True while rl.question() is active — external line handlers must ignore input */
112
+ questionActive = false;
113
+ constructor(trustAll = false, configTrust = []) {
114
+ this.trustAll = trustAll;
115
+ this.configTrust = new Set(configTrust);
116
+ }
117
+ /** Bind to the agent's readline instance so we reuse it for prompts */
118
+ setReadline(rl) {
119
+ this.rl = rl;
120
+ }
121
+ cancelPrompt() {
122
+ if (this.cancelCurrentPrompt) {
123
+ this.cancelCurrentPrompt();
124
+ this.cancelCurrentPrompt = null;
125
+ }
126
+ }
127
+ async checkPermission(toolName, toolInput) {
128
+ if (toolName === "bash") {
129
+ const cmd = String(toolInput.command || "");
130
+ const danger = isDangerous(cmd);
131
+ if (danger) {
132
+ if (this.trustAll)
133
+ return true;
134
+ console.log();
135
+ console.log(chalk.red.bold(` \u26A0 DANGEROUS: ${danger}`));
136
+ console.log(chalk.red(` Command: ${cmd}`));
137
+ const answer = await this.askUser(chalk.red(" Are you sure? (yes to confirm): "));
138
+ if (answer.trim().toLowerCase() !== "yes")
139
+ return false;
140
+ return true;
141
+ }
142
+ }
143
+ if (this.trustAll)
144
+ return true;
145
+ if (READ_TOOLS.has(toolName))
146
+ return true;
147
+ if (this.sessionAllow.has(toolName))
148
+ return true;
149
+ if (this.configTrust.has(toolName))
150
+ return true;
151
+ return this.promptUser(toolName, toolInput);
152
+ }
153
+ async promptUser(toolName, toolInput) {
154
+ const display = this.formatToolCall(toolName, toolInput);
155
+ console.log();
156
+ console.log(chalk.cyan(` \u250C\u2500 ${toolName} ${"\u2500".repeat(Math.max(0, 40 - toolName.length))}\u2510`));
157
+ for (const line of display.split("\n")) {
158
+ console.log(chalk.cyan(" \u2502 ") + chalk.white(line));
159
+ }
160
+ console.log(chalk.cyan(` \u2514${"\u2500".repeat(43)}\u2518`));
161
+ const answer = await this.askUser(chalk.dim(" Allow? ") + chalk.white("(y)es / (n)o / (a)lways this tool / (t)rust all: "));
162
+ const choice = answer.trim().toLowerCase();
163
+ if (choice === "t" || choice === "trust") {
164
+ this.trustAll = true;
165
+ return true;
166
+ }
167
+ if (choice === "a" || choice === "always") {
168
+ this.sessionAllow.add(toolName);
169
+ return true;
170
+ }
171
+ return choice === "y" || choice === "yes";
172
+ }
173
+ /**
174
+ * Prompt the user with a question. Sets questionActive flag so the
175
+ * agent's line handler knows to ignore this input.
176
+ */
177
+ askUser(prompt) {
178
+ return new Promise((resolve, reject) => {
179
+ this.cancelCurrentPrompt = () => {
180
+ this.questionActive = false;
181
+ reject(new Error("cancelled"));
182
+ };
183
+ if (this.rl) {
184
+ this.questionActive = true;
185
+ this.rl.resume();
186
+ this.rl.question(prompt, (answer) => {
187
+ this.questionActive = false;
188
+ this.cancelCurrentPrompt = null;
189
+ this.rl.pause();
190
+ resolve(answer);
191
+ });
192
+ } else {
193
+ const questionRl = readline.createInterface({
194
+ input: process.stdin,
195
+ output: process.stdout
196
+ });
197
+ this.questionActive = true;
198
+ questionRl.question(prompt, (answer) => {
199
+ this.questionActive = false;
200
+ this.cancelCurrentPrompt = null;
201
+ questionRl.close();
202
+ resolve(answer);
203
+ });
204
+ }
205
+ });
206
+ }
207
+ formatToolCall(toolName, input) {
208
+ switch (toolName) {
209
+ case "bash":
210
+ return String(input.command || "");
211
+ case "write_file":
212
+ case "edit_file":
213
+ return `${input.path || ""}`;
214
+ case "patch":
215
+ return String(input.patch_text || "").slice(0, 200) + "...";
216
+ case "fetch":
217
+ return String(input.url || "");
218
+ default:
219
+ return JSON.stringify(input, null, 2).slice(0, 200);
220
+ }
221
+ }
222
+ };
223
+
224
+ // src/tui.js
225
+ import chalk2 from "chalk";
226
+ import { execSync } from "child_process";
227
+
228
+ // src/logger.js
229
+ import fs2 from "fs";
230
+ import path2 from "path";
231
+ var LOG_DIR = path2.join(process.cwd(), ".workermill");
232
+ var LOG_FILE = path2.join(LOG_DIR, "cli.log");
233
+ var logStream = null;
234
+ function ensureLogDir() {
235
+ if (!fs2.existsSync(LOG_DIR)) {
236
+ fs2.mkdirSync(LOG_DIR, { recursive: true });
237
+ }
238
+ }
239
+ function getStream() {
240
+ if (!logStream) {
241
+ ensureLogDir();
242
+ logStream = fs2.createWriteStream(LOG_FILE, { flags: "a" });
243
+ }
244
+ return logStream;
245
+ }
246
+ function timestamp() {
247
+ return (/* @__PURE__ */ new Date()).toISOString();
248
+ }
249
+ function log(level, message, data) {
250
+ const entry = data ? `[${timestamp()}] ${level}: ${message} ${JSON.stringify(data)}` : `[${timestamp()}] ${level}: ${message}`;
251
+ getStream().write(entry + "\n");
252
+ }
253
+ function info(message, data) {
254
+ log("INFO", message, data);
255
+ }
256
+
257
+ // src/tui.js
258
+ function formatToolCall(toolName, toolInput) {
259
+ let msg = `Tool: ${toolName}`;
260
+ if (toolInput) {
261
+ if (toolInput.file_path)
262
+ msg += ` \u2192 ${toolInput.file_path}`;
263
+ else if (toolInput.path)
264
+ msg += ` \u2192 ${toolInput.path}`;
265
+ else if (toolInput.command)
266
+ msg += ` \u2192 ${String(toolInput.command).substring(0, 500)}`;
267
+ else if (toolInput.pattern)
268
+ msg += ` \u2192 pattern: ${toolInput.pattern}`;
269
+ else {
270
+ const keys = Object.keys(toolInput).slice(0, 3);
271
+ if (keys.length > 0) {
272
+ msg += ` \u2192 ${keys.map((k) => `${k}: ${String(toolInput[k]).substring(0, 200)}`).join(", ")}`;
273
+ }
274
+ }
275
+ }
276
+ return msg;
277
+ }
278
+ var PERSONA_EMOJIS = {
279
+ frontend_developer: "\u{1F3A8}",
280
+ // 🎨
281
+ backend_developer: "\u{1F4BB}",
282
+ // 💻
283
+ fullstack_developer: "\u{1F4BB}",
284
+ // 💻 (same as backend)
285
+ devops_engineer: "\u{1F527}",
286
+ // 🔧
287
+ security_engineer: "\u{1F512}",
288
+ // 🔐
289
+ qa_engineer: "\u{1F9EA}",
290
+ // 🧪
291
+ tech_writer: "\u{1F4DD}",
292
+ // 📝
293
+ project_manager: "\u{1F4CB}",
294
+ // 📋
295
+ architect: "\u{1F3D7}\uFE0F",
296
+ // 🏗️
297
+ database_engineer: "\u{1F4CA}",
298
+ // 📊
299
+ data_engineer: "\u{1F4CA}",
300
+ // 📊
301
+ data_ml_engineer: "\u{1F4CA}",
302
+ // 📊
303
+ ml_engineer: "\u{1F4CA}",
304
+ // 📊
305
+ mobile_developer: "\u{1F4F1}",
306
+ // 📱
307
+ tech_lead: "\u{1F451}",
308
+ // 👑
309
+ manager: "\u{1F454}",
310
+ // 👔
311
+ support_agent: "\u{1F4AC}",
312
+ // 💬
313
+ planner: "\u{1F4A1}",
314
+ // 💡 (planning_agent)
315
+ coordinator: "\u{1F3AF}",
316
+ // 🎯
317
+ critic: "\u{1F50D}",
318
+ // 🔍
319
+ reviewer: "\u{1F50D}"
320
+ // 🔍
321
+ };
322
+ function getPersonaEmoji(persona) {
323
+ return PERSONA_EMOJIS[persona] || "\u{1F916}";
324
+ }
325
+ function wmLog(persona, message) {
326
+ const emoji = getPersonaEmoji(persona);
327
+ console.log(chalk2.cyan(`[${emoji} ${persona} \u{1F3E0}] `) + chalk2.white(message));
328
+ info(`[${persona}] ${message}`);
329
+ }
330
+ function wmCoordinatorLog(message) {
331
+ console.log(chalk2.cyan("[coordinator] ") + chalk2.white(message));
332
+ info(`[coordinator] ${message}`);
333
+ }
334
+ function printError(message) {
335
+ console.log(chalk2.red(`
336
+ \u2717 ${message}
337
+ `));
338
+ }
339
+ var sessionStartTime = Date.now();
340
+
88
341
  // src/orchestrator.js
342
+ var LEARNING_INSTRUCTIONS = `
343
+
344
+ ## Reporting Learnings
345
+
346
+ When you discover something specific and actionable about this codebase, emit a learning marker:
347
+
348
+ \`\`\`
349
+ ::learning::The test suite requires DATABASE_URL env var or tests silently pass without running
350
+ ::learning::New API routes must be registered in backend/src/routes/index.ts or they won't load
351
+ \`\`\`
352
+
353
+ **Emit a learning when you discover:**
354
+ - A non-obvious requirement (specific env vars, config files, build steps)
355
+ - A codebase convention not documented elsewhere (naming patterns, file organization)
356
+ - A gotcha you had to work around (unexpected failures, ordering dependencies)
357
+ - Files that must be modified together (route + model + migration + test)
358
+
359
+ **Do NOT emit generic advice** like "write tests" or "handle errors properly."
360
+ Include file paths, commands, and exact details. Only emit when you genuinely discover something non-obvious.
361
+ `;
362
+ var DOCKER_INSTRUCTIONS = `
363
+
364
+ ## Development Environment
365
+
366
+ If this task requires databases, caches, or other services, use Docker to run real instances instead of mocking them. Do NOT mock or stub external services.
367
+
368
+ ### Common Services
369
+ - PostgreSQL: \`docker run -d --rm -p 5432:5432 -e POSTGRES_PASSWORD=test --name postgres-test postgres:16-alpine\`
370
+ - Redis: \`docker run -d --rm -p 6379:6379 --name redis-test redis:7-alpine\`
371
+ - MongoDB: \`docker run -d --rm -p 27017:27017 --name mongo-test mongo:7\`
372
+ - MySQL: \`docker run -d --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=test --name mysql-test mysql:8\`
373
+ - If the project has a \`docker-compose.yml\`, use \`docker compose up -d\`
374
+
375
+ Tests that pass against mocks but fail against real services are worthless.
376
+
377
+ ### CI/CD \u2014 Always add service containers
378
+ When creating GitHub Actions CI workflows that run tests requiring databases, add \`services:\` blocks so CI has real instances. Match your local Docker setup with CI service containers.
379
+ `;
380
+ var VERSION_TRUST = `
381
+
382
+ ## Technology Versions \u2014 Trust the Spec
383
+
384
+ If the ticket, PRD, or task description specifies a dependency version, USE THAT VERSION. Do NOT downgrade or "fix" versions you don't recognize \u2014 your training data has a cutoff and newer releases exist. Trust the spec over your knowledge.
385
+ `;
386
+ function buildReasoningOptions(provider, modelName) {
387
+ switch (provider) {
388
+ case "openai":
389
+ return { providerOptions: { openai: { reasoningSummary: "detailed" } } };
390
+ case "google":
391
+ case "gemini":
392
+ if (modelName && modelName.includes("gemini-3")) {
393
+ return { providerOptions: { google: { thinkingConfig: { thinkingLevel: "high", includeThoughts: true } } } };
394
+ }
395
+ return { providerOptions: { google: { thinkingConfig: { thinkingBudget: 8192, includeThoughts: true } } } };
396
+ default:
397
+ return {};
398
+ }
399
+ }
400
+ function isTransientError(error) {
401
+ if (!error || typeof error !== "object")
402
+ return false;
403
+ const msg = error instanceof Error ? error.message : String(error);
404
+ if (/status code (502|503|504)|socket hang up|ECONNRESET|ETIMEDOUT|network error|ECONNREFUSED/i.test(msg)) {
405
+ return true;
406
+ }
407
+ return false;
408
+ }
89
409
  async function classifyComplexity(config, userInput) {
90
- const { provider, model: modelName, apiKey, host } = getProviderForPersona(config);
410
+ const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config);
91
411
  if (apiKey) {
92
412
  const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
93
413
  const envVar = envMap[provider];
94
414
  if (envVar && !process.env[envVar])
95
415
  process.env[envVar] = apiKey;
96
416
  }
97
- const model = createModel(provider, modelName, host);
417
+ const model = createModel(provider, modelName, host, contextLength);
98
418
  try {
99
419
  const result = await generateObject({
100
420
  model,
@@ -135,7 +455,7 @@ function topologicalSort(stories) {
135
455
  if (visited.has(id))
136
456
  return;
137
457
  if (visiting.has(id)) {
138
- console.log(chalk.yellow(` \u26A0 Circular dependency at ${id}, using input order`));
458
+ console.log(chalk3.yellow(` \u26A0 Circular dependency at ${id}, using input order`));
139
459
  return;
140
460
  }
141
461
  visiting.add(id);
@@ -158,7 +478,7 @@ function topologicalSort(stories) {
158
478
  }
159
479
  async function planStories(config, userTask, workingDir, sandboxed = true) {
160
480
  const planner = loadPersona("planner");
161
- const { provider: pProvider, model: pModel, host: pHost } = getProviderForPersona(config, "planner");
481
+ const { provider: pProvider, model: pModel, host: pHost, contextLength: pCtx } = getProviderForPersona(config, "planner");
162
482
  if (pProvider) {
163
483
  const pApiKey = config.providers[pProvider]?.apiKey;
164
484
  if (pApiKey) {
@@ -171,13 +491,21 @@ async function planStories(config, userTask, workingDir, sandboxed = true) {
171
491
  }
172
492
  }
173
493
  }
174
- const plannerModel = createModel(pProvider, pModel, pHost);
494
+ const plannerModel = createModel(pProvider, pModel, pHost, pCtx);
175
495
  const plannerTools = createToolDefinitions(workingDir, plannerModel, sandboxed);
176
496
  const readOnlyTools = {};
177
497
  if (planner) {
178
498
  for (const toolName of planner.tools) {
179
- if (plannerTools[toolName]) {
180
- readOnlyTools[toolName] = plannerTools[toolName];
499
+ const toolDef = plannerTools[toolName];
500
+ if (toolDef) {
501
+ readOnlyTools[toolName] = {
502
+ ...toolDef,
503
+ execute: async (input) => {
504
+ wmLog("planner", formatToolCall(toolName, input));
505
+ const result = await toolDef.execute(input);
506
+ return result;
507
+ }
508
+ };
181
509
  }
182
510
  }
183
511
  }
@@ -215,56 +543,42 @@ Return ONLY a JSON code block with this structure:
215
543
  }
216
544
  \`\`\`
217
545
 
218
- Available personas: architect, backend_developer, frontend_developer, fullstack_developer, devops_engineer, qa_engineer, security_engineer, database_engineer, mobile_developer, data_engineer, ml_engineer`;
546
+ Available personas: backend_developer, frontend_developer, devops_engineer, qa_engineer, security_engineer, data_ml_engineer, mobile_developer, tech_writer, tech_lead`;
219
547
  wmLog("planner", `Starting planning agent using ${pModel}`);
220
548
  wmLog("planner", "Reading repository structure...");
549
+ let planText = "";
221
550
  const planStream = streamText({
222
551
  model: plannerModel,
223
552
  system: planner?.systemPrompt || "You are an implementation planner.",
224
553
  prompt: plannerPrompt,
225
554
  tools: readOnlyTools,
226
555
  stopWhen: stepCountIs(100),
227
- abortSignal: AbortSignal.timeout(3 * 60 * 1e3)
228
- });
229
- let planText = "";
230
- const planPrefix = wmLogPrefix("planner");
231
- let planNeedsPrefix = true;
232
- let inJsonBlock = false;
233
- for await (const chunk of planStream.textStream) {
234
- if (chunk) {
235
- planText += chunk;
236
- if (chunk.includes("```json")) {
237
- inJsonBlock = true;
238
- continue;
239
- }
240
- if (chunk.includes("```") && inJsonBlock) {
241
- inJsonBlock = false;
242
- continue;
243
- }
244
- if (inJsonBlock)
245
- continue;
246
- if (planNeedsPrefix) {
247
- process.stdout.write(planPrefix);
248
- planNeedsPrefix = false;
556
+ timeout: { totalMs: 3 * 60 * 1e3, chunkMs: 12e4 },
557
+ ...buildOllamaOptions(pProvider, pCtx),
558
+ onStepFinish({ text }) {
559
+ if (text) {
560
+ const lines = text.split("\n").filter((l) => l.trim());
561
+ for (const line of lines) {
562
+ if (line.trim().startsWith("{") || line.trim().startsWith("}") || line.trim().startsWith('"') || line.trim().startsWith("[") || line.trim().startsWith("]") || line.includes("```"))
563
+ continue;
564
+ wmLog("planner", line);
565
+ }
249
566
  }
250
- process.stdout.write(chalk.white(chunk));
251
- if (chunk.endsWith("\n"))
252
- planNeedsPrefix = true;
253
567
  }
568
+ });
569
+ for await (const _chunk of planStream.textStream) {
254
570
  }
255
- if (!planNeedsPrefix)
256
- process.stdout.write("\n");
257
571
  const finalText = await planStream.text;
258
572
  if (finalText && finalText.length > planText.length) {
259
573
  planText = finalText;
260
574
  }
261
575
  let stories = parseStoriesFromText(planText);
262
576
  if (stories.length === 0) {
263
- console.log(chalk.yellow(" \u26A0 Planner didn't produce structured stories, falling back to single story"));
577
+ console.log(chalk3.yellow(" \u26A0 Planner didn't produce structured stories, falling back to single story"));
264
578
  stories = [{
265
579
  id: "implement",
266
580
  title: userTask.slice(0, 60),
267
- persona: "fullstack_developer",
581
+ persona: "backend_developer",
268
582
  description: userTask
269
583
  }];
270
584
  }
@@ -302,7 +616,7 @@ function parseStoriesFromText(text) {
302
616
  if (stories)
303
617
  return stories;
304
618
  const preview = text.slice(0, 500);
305
- console.log(chalk.dim(` (planner output preview: ${preview}${text.length > 500 ? "..." : ""})`));
619
+ console.log(chalk3.dim(` (planner output preview: ${preview}${text.length > 500 ? "..." : ""})`));
306
620
  return [];
307
621
  }
308
622
  function tryParseStories(text) {
@@ -379,6 +693,29 @@ function extractScore(text) {
379
693
  return 60;
380
694
  return 75;
381
695
  }
696
+ function parseAffectedStories(text) {
697
+ const storiesMatch = text.match(/AFFECTED_STORIES:\s*\[([^\]]+)\]/i);
698
+ if (!storiesMatch)
699
+ return null;
700
+ const stories = storiesMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
701
+ if (stories.length === 0)
702
+ return null;
703
+ let reasons = {};
704
+ const reasonsMatch = text.match(/AFFECTED_REASONS:\s*(\{[\s\S]*?\})/i);
705
+ if (reasonsMatch) {
706
+ try {
707
+ const parsed = JSON.parse(reasonsMatch[1]);
708
+ for (const [key, value] of Object.entries(parsed)) {
709
+ const storyIndex = parseInt(key, 10);
710
+ if (!isNaN(storyIndex) && typeof value === "string") {
711
+ reasons[storyIndex] = value;
712
+ }
713
+ }
714
+ } catch {
715
+ }
716
+ }
717
+ return { stories, reasons };
718
+ }
382
719
  async function runOrchestration(config, userTask, trustAll, sandboxed = true, agentRl) {
383
720
  const costTracker = new CostTracker();
384
721
  const context = {
@@ -402,16 +739,24 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
402
739
  if (config.review?.useCritic) {
403
740
  const critic = loadPersona("critic");
404
741
  if (critic) {
405
- const { provider: cProvider, model: cModel, host: cHost } = getProviderForPersona(config, "critic");
406
- const criticModel = createModel(cProvider, cModel, cHost);
742
+ const { provider: cProvider, model: cModel, host: cHost, contextLength: cCtx } = getProviderForPersona(config, "critic");
743
+ const criticModel = createModel(cProvider, cModel, cHost, cCtx);
407
744
  const criticTools = createToolDefinitions(workingDir, criticModel, sandboxed);
408
745
  const criticReadOnly = {};
409
746
  for (const name of critic.tools) {
410
- if (criticTools[name]) {
411
- criticReadOnly[name] = criticTools[name];
747
+ const toolDef = criticTools[name];
748
+ if (toolDef) {
749
+ criticReadOnly[name] = {
750
+ ...toolDef,
751
+ execute: async (input) => {
752
+ wmLog("critic", formatToolCall(name, input));
753
+ const result = await toolDef.execute(input);
754
+ return result;
755
+ }
756
+ };
412
757
  }
413
758
  }
414
- const criticSpinner = ora({ stream: process.stdout, text: chalk.white("Critic reviewing plan..."), prefixText: " " }).start();
759
+ const criticSpinner = ora({ stream: process.stdout, text: chalk3.white("Critic reviewing plan..."), prefixText: " " }).start();
415
760
  const criticStream = streamText({
416
761
  model: criticModel,
417
762
  system: critic.systemPrompt,
@@ -421,7 +766,8 @@ Stories:
421
766
  ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.description}`).join("\n")}`,
422
767
  tools: criticReadOnly,
423
768
  stopWhen: stepCountIs(100),
424
- abortSignal: AbortSignal.timeout(3 * 60 * 1e3)
769
+ timeout: { totalMs: 3 * 60 * 1e3, chunkMs: 12e4 },
770
+ ...buildOllamaOptions(cProvider, cCtx)
425
771
  });
426
772
  for await (const _chunk of criticStream.textStream) {
427
773
  }
@@ -437,11 +783,11 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
437
783
  if (!trustAll) {
438
784
  let answer = "n";
439
785
  try {
440
- answer = await permissions.askUser(chalk.dim(" Execute this plan? (y/n): "));
786
+ answer = await permissions.askUser(chalk3.dim(" Execute this plan? (y/n): "));
441
787
  } catch {
442
788
  }
443
789
  if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
444
- console.log(chalk.dim(" Plan cancelled.\n"));
790
+ console.log(chalk3.dim(" Plan cancelled.\n"));
445
791
  return;
446
792
  }
447
793
  console.log();
@@ -453,13 +799,16 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
453
799
  printError(`Unknown persona: ${story.persona}`);
454
800
  continue;
455
801
  }
456
- const { provider, model: modelName, apiKey, host } = getProviderForPersona(config, persona.provider || story.persona);
802
+ const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config, persona.provider || story.persona);
457
803
  if (apiKey) {
458
804
  const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
459
805
  const envVar = envMap[provider];
460
806
  if (envVar && !process.env[envVar])
461
807
  process.env[envVar] = apiKey;
462
808
  }
809
+ console.log(chalk3.bold(`
810
+ \u2500\u2500\u2500 Story ${i + 1}/${sorted.length} \u2500\u2500\u2500
811
+ `));
463
812
  wmCoordinatorLog(`Task claimed by orchestrator`);
464
813
  wmLog(story.persona, `Starting ${story.title}`);
465
814
  wmLog(story.persona, `Executing story with AIClient (model: ${modelName})...`);
@@ -469,7 +818,7 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
469
818
  prefixText: "",
470
819
  spinner: "dots"
471
820
  }).start();
472
- const model = createModel(provider, modelName, host);
821
+ const model = createModel(provider, modelName, host, contextLength);
473
822
  const allTools = createToolDefinitions(workingDir, model, sandboxed);
474
823
  const personaTools = {};
475
824
  let lastToolCall = "";
@@ -487,13 +836,9 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
487
836
  lastToolCall = callKey;
488
837
  if (!isDuplicate) {
489
838
  spinner.stop();
490
- wmLog(story.persona, `Tool: ${toolName}`);
839
+ wmLog(story.persona, formatToolCall(toolName, input));
491
840
  }
492
841
  const result = await toolDef.execute(input);
493
- const resultStr = typeof result === "string" ? result : JSON.stringify(result);
494
- if (!isDuplicate) {
495
- printToolResult(toolName, resultStr);
496
- }
497
842
  spinner.start();
498
843
  return result;
499
844
  }
@@ -525,6 +870,12 @@ Working directory: ${workingDir}
525
870
 
526
871
  Your task: ${story.description}
527
872
 
873
+ ## Communication Style
874
+
875
+ Write in a professional, direct tone. Do NOT open messages with filler words or pleasantries like "Perfect!", "Great!", "Awesome!", "Sure!", "Absolutely!", or similar. Start with the substance \u2014 what you did, what you found, or what you need. Be concise and informative. Do NOT repeat what you said in previous steps \u2014 each response should add new information only.
876
+
877
+ When summarizing your work at the end, describe decisions in plain language. The internal DEC-xxx markers are parsed by the system automatically \u2014 your summary should restate decisions in readable form.
878
+
528
879
  ## Critical rules
529
880
  - NEVER start long-running processes (dev servers, watch modes, npm start, npm run dev, nodemon, tsc --watch, webpack serve, etc.). These block execution indefinitely.
530
881
  - NEVER run interactive commands that wait for user input.
@@ -532,45 +883,39 @@ Your task: ${story.description}
532
883
  - If you need to verify a server works, check that the code compiles or run a quick test \u2014 do NOT start the actual server.
533
884
 
534
885
  When you make a decision that affects other parts of the system, include ::decision:: markers in your output.
535
- When you learn something useful, include ::learning:: markers.
536
886
  When you create a file, include ::file_created::path markers.
537
- When you modify a file, include ::file_modified::path markers.${revisionFeedback ? `
887
+ When you modify a file, include ::file_modified::path markers.
888
+ ${LEARNING_INSTRUCTIONS}${DOCKER_INSTRUCTIONS}${VERSION_TRUST}${revisionFeedback ? `
538
889
 
539
890
  ## Revision requested
540
891
  ${revisionFeedback}` : ""}`;
541
892
  try {
893
+ let allText = "";
542
894
  const stream = streamText({
543
895
  model,
544
896
  system: systemPrompt,
545
897
  prompt: story.description,
546
898
  tools: personaTools,
547
899
  stopWhen: stepCountIs(100),
548
- abortSignal: AbortSignal.timeout(10 * 60 * 1e3)
549
- });
550
- let allText = "";
551
- const storyPrefix = wmLogPrefix(story.persona);
552
- let needsPrefix = true;
553
- for await (const chunk of stream.textStream) {
554
- if (chunk) {
555
- allText += chunk;
556
- if (chunk.includes("::decision::") || chunk.includes("::learning::") || chunk.includes("::file_created::") || chunk.includes("::file_modified::"))
557
- continue;
558
- spinner.stop();
559
- if (needsPrefix) {
560
- process.stdout.write(storyPrefix);
561
- needsPrefix = false;
562
- }
563
- process.stdout.write(chalk.white(chunk));
564
- if (chunk.endsWith("\n")) {
565
- needsPrefix = true;
900
+ timeout: { totalMs: 10 * 60 * 1e3, chunkMs: 12e4 },
901
+ ...buildReasoningOptions(provider, modelName),
902
+ ...buildOllamaOptions(provider, contextLength),
903
+ onStepFinish({ text: text2 }) {
904
+ if (text2) {
905
+ spinner.stop();
906
+ const lines = text2.split("\n").filter((l) => l.trim());
907
+ for (const line of lines) {
908
+ if (line.includes("::decision::") || line.includes("::learning::") || line.includes("::file_created::") || line.includes("::file_modified::"))
909
+ continue;
910
+ wmLog(story.persona, line);
911
+ }
566
912
  }
567
913
  }
914
+ });
915
+ for await (const _chunk of stream.textStream) {
568
916
  }
569
- if (!needsPrefix) {
570
- process.stdout.write("\n");
571
- }
572
- const finalStreamText = await stream.text;
573
- const text = finalStreamText && finalStreamText.length > allText.length ? finalStreamText : allText;
917
+ const text = await stream.text;
918
+ allText = text;
574
919
  const usage = await stream.totalUsage;
575
920
  spinner.stop();
576
921
  const decisionMatches = text.match(/::decision::(.*?)(?=::\w+::|$)/gs);
@@ -602,12 +947,17 @@ ${revisionFeedback}` : ""}`;
602
947
  const inTokens = usage?.inputTokens || 0;
603
948
  const outTokens = usage?.outputTokens || 0;
604
949
  costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
605
- wmLog(story.persona, `${story.title} \u2014 completed!`);
950
+ wmLog(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
606
951
  console.log();
607
952
  break;
608
953
  } catch (err) {
609
954
  spinner.stop();
610
- printError(`Story ${i + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
955
+ const errMsg = err instanceof Error ? err.message : String(err);
956
+ if (isTransientError(err) && revision < 2) {
957
+ wmLog(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
958
+ continue;
959
+ }
960
+ printError(`Story ${i + 1} failed: ${errMsg}`);
611
961
  break;
612
962
  }
613
963
  }
@@ -617,7 +967,7 @@ ${revisionFeedback}` : ""}`;
617
967
  const approvalThreshold = config.review?.approvalThreshold ?? 80;
618
968
  const reviewer = loadPersona("reviewer");
619
969
  if (reviewer) {
620
- const { provider: revProvider, model: revModel, host: revHost } = getProviderForPersona(config, reviewer.provider || "reviewer");
970
+ const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(config, reviewer.provider || "reviewer");
621
971
  const revApiKey = config.providers[revProvider]?.apiKey;
622
972
  if (revApiKey) {
623
973
  const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
@@ -626,68 +976,117 @@ ${revisionFeedback}` : ""}`;
626
976
  if (envVar && key && !process.env[envVar])
627
977
  process.env[envVar] = key;
628
978
  }
629
- const reviewModel = createModel(revProvider, revModel, revHost);
979
+ const reviewModel = createModel(revProvider, revModel, revHost, revCtx);
630
980
  const reviewTools = createToolDefinitions(workingDir, reviewModel, sandboxed);
631
981
  const reviewerTools = {};
632
982
  for (const toolName of reviewer.tools) {
633
- if (reviewTools[toolName]) {
634
- reviewerTools[toolName] = reviewTools[toolName];
983
+ const toolDef = reviewTools[toolName];
984
+ if (toolDef) {
985
+ reviewerTools[toolName] = {
986
+ ...toolDef,
987
+ execute: async (input) => {
988
+ wmLog("tech_lead", formatToolCall(toolName, input));
989
+ const result = await toolDef.execute(input);
990
+ return result;
991
+ }
992
+ };
635
993
  }
636
994
  }
995
+ let previousReviewFeedback = "";
637
996
  for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
638
997
  const isRevision = reviewRound > 0;
639
998
  wmCoordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
640
999
  wmLog("tech_lead", "Starting agent execution");
641
1000
  const reviewSpinner = ora({
642
1001
  stream: process.stdout,
643
- text: chalk.white(isRevision ? "Reviewer \u2014 Re-checking after revisions" : "Reviewer \u2014 Checking code quality"),
1002
+ text: chalk3.white(isRevision ? "Reviewer \u2014 Re-checking after revisions" : "Reviewer \u2014 Checking code quality"),
644
1003
  prefixText: " "
645
1004
  }).start();
646
1005
  try {
647
- const reviewPrompt = `Review the changes made by the following experts:
1006
+ const previousFeedbackSection = isRevision && previousReviewFeedback ? `## Previous Review Feedback (Review ${reviewRound}/${maxRevisions})
1007
+ This is a revision attempt. The previous code was reviewed and these issues were identified:
1008
+
1009
+ ${previousReviewFeedback}
1010
+
1011
+ **IMPORTANT: Check if ALL issues above have been addressed, not just some of them.**
1012
+ - The developer was instructed to fix every item
1013
+ - If ANY issue remains unaddressed, request another revision
1014
+ - Be specific about which items are still outstanding
1015
+
1016
+ ---
1017
+
1018
+ ` : "";
1019
+ const storySummaryRows = sorted.map((s, idx) => {
1020
+ const files = [...context.filesCreated, ...context.filesModified].slice(0, 3).join(", ") || "(none)";
1021
+ return `| ${idx + 1} | ${s.persona} | ${s.title} | ${files} |`;
1022
+ }).join("\n");
1023
+ const reviewPrompt = `${previousFeedbackSection}## Original Task
1024
+
1025
+ ${userTask}
1026
+
1027
+ ## Story Summary
1028
+
1029
+ | # | Persona | Title | Files |
1030
+ |---|---------|-------|-------|
1031
+ ${storySummaryRows}
648
1032
 
649
- ${sorted.map((s, idx) => `${idx + 1}. ${s.persona}: ${s.title} \u2014 ${s.description}`).join("\n")}
1033
+ ## Changes Made
650
1034
 
651
1035
  Files created: ${context.filesCreated.join(", ") || "none"}
652
1036
  Files modified: ${context.filesModified.join(", ") || "none"}
1037
+ ${context.decisions.length > 0 ? `
1038
+ Decisions made:
1039
+ ${context.decisions.map((d) => `- ${d}`).join("\n")}` : ""}
1040
+
1041
+ ## Review Instructions
1042
+
1043
+ Use read_file, glob, grep, and git tools to examine the actual code. Check:
1044
+ - Does the code correctly implement the original task requirements?
1045
+ - Are there bugs, logic errors, or security issues?
1046
+ - Does the code follow existing project conventions?
1047
+ - Is error handling appropriate?
1048
+ - Are there missing pieces from the task requirements?
653
1049
 
654
- Use the read_file, glob, and grep tools to examine the actual changes. Look for:
655
- - Bugs or logic errors
656
- - Missing error handling
657
- - Security issues
658
- - Code that doesn't follow project conventions
659
- - Missing tests
1050
+ Use \`git diff\` or read individual files to see the actual changes.
660
1051
 
661
1052
  Provide a review with a quality score (0-100) using ::review_score:: marker and a verdict using ::review_verdict::approved or ::review_verdict::needs_revision.
662
- If there are issues, be specific about which files and what needs to change.`;
1053
+
1054
+ ### For REVISION_NEEDED Decisions - Specify Affected Stories
1055
+
1056
+ When requesting revision, you MUST specify which stories need changes. Use the story numbers from the Story Summary table above.
1057
+
1058
+ \`\`\`
1059
+ AFFECTED_STORIES: [2, 3]
1060
+ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Frontend form has no validation"}
1061
+ \`\`\`
1062
+
1063
+ **Guidelines:**
1064
+ - Only include stories that have ACTUAL implementation issues
1065
+ - If ALL stories need revision, you may omit AFFECTED_STORIES (all will re-run)
1066
+ - Be specific in AFFECTED_REASONS so developers know exactly what to fix`;
1067
+ let allReviewText = "";
663
1068
  const reviewStream = streamText({
664
1069
  model: reviewModel,
665
1070
  system: reviewer.systemPrompt,
666
1071
  prompt: reviewPrompt,
667
1072
  tools: reviewerTools,
668
1073
  stopWhen: stepCountIs(100),
669
- abortSignal: AbortSignal.timeout(5 * 60 * 1e3)
670
- });
671
- let allReviewText = "";
672
- const revPrefix = wmLogPrefix("tech_lead");
673
- let revNeedsPrefix = true;
674
- for await (const chunk of reviewStream.textStream) {
675
- if (chunk) {
676
- allReviewText += chunk;
677
- if (chunk.includes("::review_score::") || chunk.includes("::review_verdict::"))
678
- continue;
679
- reviewSpinner.stop();
680
- if (revNeedsPrefix) {
681
- process.stdout.write(revPrefix);
682
- revNeedsPrefix = false;
1074
+ timeout: { totalMs: 5 * 60 * 1e3, chunkMs: 12e4 },
1075
+ ...buildOllamaOptions(revProvider, revCtx),
1076
+ onStepFinish({ text }) {
1077
+ if (text) {
1078
+ reviewSpinner.stop();
1079
+ const lines = text.split("\n").filter((l) => l.trim());
1080
+ for (const line of lines) {
1081
+ if (line.includes("::review_score::") || line.includes("::review_verdict::"))
1082
+ continue;
1083
+ wmLog("tech_lead", line);
1084
+ }
683
1085
  }
684
- process.stdout.write(chalk.white(chunk));
685
- if (chunk.endsWith("\n"))
686
- revNeedsPrefix = true;
687
1086
  }
1087
+ });
1088
+ for await (const _chunk of reviewStream.textStream) {
688
1089
  }
689
- if (!revNeedsPrefix)
690
- process.stdout.write("\n");
691
1090
  const finalReviewText = await reviewStream.text;
692
1091
  const reviewText = finalReviewText && finalReviewText.length > allReviewText.length ? finalReviewText : allReviewText;
693
1092
  const reviewUsage = await reviewStream.totalUsage;
@@ -697,36 +1096,54 @@ If there are issues, be specific about which files and what needs to change.`;
697
1096
  wmLog("tech_lead", `::code_quality_score::${score}`);
698
1097
  wmLog("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
699
1098
  wmCoordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
1099
+ previousReviewFeedback = reviewText;
700
1100
  console.log();
701
1101
  costTracker.addUsage(`Reviewer (round ${reviewRound + 1})`, revProvider, revModel, reviewUsage?.inputTokens || 0, reviewUsage?.outputTokens || 0);
702
1102
  if (approved)
703
1103
  break;
704
1104
  if (reviewRound >= maxRevisions) {
705
- console.log(chalk.yellow(` \u26A0 Max review revisions (${maxRevisions}) reached`));
1105
+ console.log(chalk3.yellow(` \u26A0 Max review revisions (${maxRevisions}) reached`));
706
1106
  break;
707
1107
  }
708
1108
  let shouldRevise = autoRevise;
709
1109
  if (!autoRevise) {
710
1110
  try {
711
- const answer = await permissions.askUser(chalk.dim(" Revise and re-review? ") + chalk.white(`(y/n, ${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left): `));
1111
+ const answer = await permissions.askUser(chalk3.dim(" Revise and re-review? ") + chalk3.white(`(y/n, ${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left): `));
712
1112
  shouldRevise = answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
713
1113
  } catch {
714
1114
  shouldRevise = false;
715
1115
  }
716
1116
  } else {
717
- console.log(chalk.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
1117
+ console.log(chalk3.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
718
1118
  }
719
1119
  if (!shouldRevise) {
720
- console.log(chalk.dim(" Skipping revision, proceeding to commit."));
1120
+ console.log(chalk3.dim(" Skipping revision, proceeding to commit."));
721
1121
  break;
722
1122
  }
723
- console.log(chalk.bold("\n \u2500\u2500\u2500 Revision Pass \u2500\u2500\u2500\n"));
1123
+ const affected = parseAffectedStories(reviewText);
1124
+ const affectedSet = affected ? new Set(affected.stories) : null;
1125
+ if (affected) {
1126
+ const selectiveInfo = `stories ${affected.stories.join(", ")}`;
1127
+ wmCoordinatorLog(`Selective revision: ${selectiveInfo}`);
1128
+ if (Object.keys(affected.reasons).length > 0) {
1129
+ for (const [idx, reason] of Object.entries(affected.reasons)) {
1130
+ wmCoordinatorLog(` Story ${idx}: ${reason}`);
1131
+ }
1132
+ }
1133
+ } else {
1134
+ wmCoordinatorLog("Full revision (all stories)");
1135
+ }
1136
+ console.log(chalk3.bold("\n \u2500\u2500\u2500 Revision Pass \u2500\u2500\u2500\n"));
724
1137
  for (let i = 0; i < sorted.length; i++) {
725
1138
  const story = sorted[i];
1139
+ if (affectedSet && !affectedSet.has(i + 1)) {
1140
+ wmCoordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
1141
+ continue;
1142
+ }
726
1143
  const storyPersona = loadPersona(story.persona);
727
1144
  if (!storyPersona)
728
1145
  continue;
729
- const { provider: sProvider, model: sModel, host: sHost } = getProviderForPersona(config, storyPersona.provider || story.persona);
1146
+ const { provider: sProvider, model: sModel, host: sHost, contextLength: sCtx } = getProviderForPersona(config, storyPersona.provider || story.persona);
730
1147
  if (sProvider) {
731
1148
  const sApiKey = config.providers[sProvider]?.apiKey;
732
1149
  if (sApiKey) {
@@ -739,12 +1156,15 @@ If there are issues, be specific about which files and what needs to change.`;
739
1156
  }
740
1157
  }
741
1158
  }
1159
+ wmCoordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
1160
+ wmLog(story.persona, `Starting revision: ${story.title}`);
742
1161
  const revSpinner = ora({
743
1162
  stream: process.stdout,
744
- text: chalk.white(`Revising ${i + 1}/${sorted.length} \u2014 ${storyPersona.name} \u2014 ${story.title}`),
745
- prefixText: " "
1163
+ text: "",
1164
+ prefixText: "",
1165
+ spinner: "dots"
746
1166
  }).start();
747
- const storyModel = createModel(sProvider, sModel, sHost);
1167
+ const storyModel = createModel(sProvider, sModel, sHost, sCtx);
748
1168
  const storyAllTools = createToolDefinitions(workingDir, storyModel, sandboxed);
749
1169
  const storyTools = {};
750
1170
  for (const toolName of storyPersona.tools) {
@@ -757,10 +1177,8 @@ If there are issues, be specific about which files and what needs to change.`;
757
1177
  if (!allowed)
758
1178
  return "Tool execution denied by user.";
759
1179
  revSpinner.stop();
760
- printToolCall(toolName, input);
1180
+ wmLog(story.persona, formatToolCall(toolName, input));
761
1181
  const result = await toolDef.execute(input);
762
- const resultStr = typeof result === "string" ? result : JSON.stringify(result);
763
- printToolResult(toolName, resultStr);
764
1182
  revSpinner.start();
765
1183
  return result;
766
1184
  }
@@ -771,6 +1189,10 @@ If there are issues, be specific about which files and what needs to change.`;
771
1189
 
772
1190
  Working directory: ${workingDir}
773
1191
 
1192
+ ## Communication Style
1193
+
1194
+ Write in a professional, direct tone. Do NOT open messages with filler words or pleasantries like "Perfect!", "Great!", "Awesome!", "Sure!", "Absolutely!", or similar. Start with the substance \u2014 what you did, what you found, or what you need. Be concise and informative. Do NOT repeat what you said in previous steps \u2014 each response should add new information only.
1195
+
774
1196
  ## Critical rules
775
1197
  - NEVER start long-running processes (dev servers, watch modes, npm start, npm run dev, nodemon, tsc --watch, etc.)
776
1198
  - NEVER run interactive commands that wait for user input
@@ -789,7 +1211,20 @@ Your task: Address the reviewer's feedback for "${story.title}". Fix the specifi
789
1211
  ${story.description}`,
790
1212
  tools: storyTools,
791
1213
  stopWhen: stepCountIs(100),
792
- abortSignal: AbortSignal.timeout(5 * 60 * 1e3)
1214
+ timeout: { totalMs: 5 * 60 * 1e3, chunkMs: 12e4 },
1215
+ ...buildReasoningOptions(sProvider, sModel),
1216
+ ...buildOllamaOptions(sProvider, sCtx),
1217
+ onStepFinish({ text }) {
1218
+ if (text) {
1219
+ revSpinner.stop();
1220
+ const lines = text.split("\n").filter((l) => l.trim());
1221
+ for (const line of lines) {
1222
+ if (line.includes("::"))
1223
+ continue;
1224
+ wmLog(story.persona, line);
1225
+ }
1226
+ }
1227
+ }
793
1228
  });
794
1229
  for await (const _chunk of revStream.textStream) {
795
1230
  }
@@ -799,74 +1234,74 @@ ${story.description}`,
799
1234
  wmLog(story.persona, `${story.title} \u2014 revision complete!`);
800
1235
  } catch (err) {
801
1236
  revSpinner.stop();
802
- console.log(chalk.yellow(` \u26A0 Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`));
1237
+ console.log(chalk3.yellow(` \u26A0 Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`));
803
1238
  }
804
1239
  }
805
1240
  console.log();
806
1241
  } catch (err) {
807
1242
  reviewSpinner.stop();
808
- console.log(chalk.yellow(` \u26A0 Review skipped: ${err instanceof Error ? err.message : String(err)}`));
1243
+ console.log(chalk3.yellow(` \u26A0 Review skipped: ${err instanceof Error ? err.message : String(err)}`));
809
1244
  console.log();
810
1245
  break;
811
1246
  }
812
1247
  }
813
1248
  }
814
1249
  try {
815
- const { execSync } = await import("child_process");
1250
+ const { execSync: execSync2 } = await import("child_process");
816
1251
  try {
817
- execSync("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1252
+ execSync2("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
818
1253
  } catch {
819
1254
  wmCoordinatorLog("Initializing git repository...");
820
- execSync("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
821
- const fs2 = await import("fs");
1255
+ execSync2("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1256
+ const fs3 = await import("fs");
822
1257
  const gitignorePath = `${workingDir}/.gitignore`;
823
- if (!fs2.existsSync(gitignorePath)) {
824
- fs2.writeFileSync(gitignorePath, "node_modules/\ndist/\n.env\n.workermill/\n*.log\n", "utf-8");
1258
+ if (!fs3.existsSync(gitignorePath)) {
1259
+ fs3.writeFileSync(gitignorePath, "node_modules/\ndist/\n.env\n.workermill/\n*.log\n", "utf-8");
825
1260
  }
826
1261
  wmCoordinatorLog("Git repo initialized");
827
1262
  }
828
- const diff = execSync("git diff --stat", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
829
- const untracked = execSync("git ls-files --others --exclude-standard", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1263
+ const diff = execSync2("git diff --stat", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
1264
+ const untracked = execSync2("git ls-files --others --exclude-standard", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
830
1265
  const hasChanges = diff || untracked;
831
1266
  if (hasChanges) {
832
- console.log(chalk.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
1267
+ console.log(chalk3.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
833
1268
  if (diff) {
834
- console.log(chalk.dim(" " + diff.split("\n").join("\n ")));
1269
+ console.log(chalk3.dim(" " + diff.split("\n").join("\n ")));
835
1270
  }
836
1271
  if (untracked) {
837
1272
  const untrackedFiles = untracked.split("\n");
838
- console.log(chalk.dim(" New files:"));
1273
+ console.log(chalk3.dim(" New files:"));
839
1274
  for (const f of untrackedFiles) {
840
- console.log(chalk.dim(` + ${f}`));
1275
+ console.log(chalk3.dim(` + ${f}`));
841
1276
  }
842
1277
  }
843
1278
  console.log();
844
1279
  if (!trustAll) {
845
- const answer = await permissions.askUser(chalk.dim(" Commit these changes? (y/n): "));
1280
+ const answer = await permissions.askUser(chalk3.dim(" Commit these changes? (y/n): "));
846
1281
  if (answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes") {
847
1282
  const filesToStage = [...context.filesCreated, ...context.filesModified].filter(Boolean);
848
1283
  if (filesToStage.length > 0) {
849
1284
  for (const f of filesToStage) {
850
1285
  try {
851
- execSync(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
1286
+ execSync2(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
852
1287
  } catch {
853
1288
  }
854
1289
  }
855
1290
  } else {
856
- execSync("git add -u", { cwd: workingDir, stdio: "pipe" });
1291
+ execSync2("git add -u", { cwd: workingDir, stdio: "pipe" });
857
1292
  }
858
1293
  const storyTitles = sorted.map((s) => s.title).join(", ");
859
1294
  const msg = `feat: ${storyTitles}`.slice(0, 72);
860
- execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
861
- console.log(chalk.green(" \u2713 Changes committed"));
1295
+ execSync2(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
1296
+ console.log(chalk3.green(" \u2713 Changes committed"));
862
1297
  }
863
1298
  }
864
1299
  }
865
1300
  } catch (err) {
866
1301
  }
867
- console.log(chalk.bold(" \u2500\u2500\u2500 Session Complete \u2500\u2500\u2500"));
1302
+ console.log(chalk3.bold(" \u2500\u2500\u2500 Session Complete \u2500\u2500\u2500"));
868
1303
  console.log();
869
- console.log(chalk.dim(" " + costTracker.getSummary().split("\n").join("\n ")));
1304
+ console.log(chalk3.dim(" " + costTracker.getSummary().split("\n").join("\n ")));
870
1305
  console.log();
871
1306
  }
872
1307
  export {