workermill 0.1.9 → 0.3.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,15 @@
1
1
  import {
2
2
  CostTracker,
3
- PermissionManager,
3
+ __dirname,
4
+ buildOllamaOptions,
4
5
  createModel,
5
6
  createToolDefinitions,
6
- getPersonaEmoji,
7
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";
8
+ info
9
+ } from "./chunk-NGQKIYVB.js";
18
10
 
19
11
  // src/orchestrator.js
20
- import chalk from "chalk";
12
+ import chalk3 from "chalk";
21
13
  import ora from "ora";
22
14
  import { streamText, generateObject, generateText, stepCountIs } from "ai";
23
15
  import { z } from "zod";
@@ -58,10 +50,14 @@ function parsePersonaFile(content) {
58
50
  }
59
51
  function loadPersona(slug) {
60
52
  const locations = [
53
+ // Project-level persona overrides
61
54
  path.join(process.cwd(), ".workermill", "personas", `${slug}.md`),
55
+ // User-level persona overrides
62
56
  path.join(os.homedir(), ".workermill", "personas", `${slug}.md`),
57
+ // Bundled with the npm package (cli/personas/)
58
+ path.join(import.meta.dirname || __dirname, "../personas", `${slug}.md`),
59
+ // Dev mode — resolve from monorepo
63
60
  path.join(import.meta.dirname || __dirname, "../../packages/engine/src/personas", `${slug}.md`),
64
- // Also try relative to the repo root
65
61
  path.join(process.cwd(), "packages/engine/src/personas", `${slug}.md`)
66
62
  ];
67
63
  for (const loc of locations) {
@@ -85,16 +81,310 @@ function loadPersona(slug) {
85
81
  };
86
82
  }
87
83
 
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
+ };
224
+
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
+ }
244
+ }
245
+ }
246
+ return msg;
247
+ }
248
+ var PERSONA_EMOJIS = {
249
+ frontend_developer: "\u{1F3A8}",
250
+ // 🎨
251
+ backend_developer: "\u{1F4BB}",
252
+ // 💻
253
+ fullstack_developer: "\u{1F4BB}",
254
+ // 💻 (same as backend)
255
+ devops_engineer: "\u{1F527}",
256
+ // 🔧
257
+ security_engineer: "\u{1F512}",
258
+ // 🔐
259
+ qa_engineer: "\u{1F9EA}",
260
+ // 🧪
261
+ tech_writer: "\u{1F4DD}",
262
+ // 📝
263
+ project_manager: "\u{1F4CB}",
264
+ // 📋
265
+ architect: "\u{1F3D7}\uFE0F",
266
+ // 🏗️
267
+ database_engineer: "\u{1F4CA}",
268
+ // 📊
269
+ data_engineer: "\u{1F4CA}",
270
+ // 📊
271
+ data_ml_engineer: "\u{1F4CA}",
272
+ // 📊
273
+ ml_engineer: "\u{1F4CA}",
274
+ // 📊
275
+ mobile_developer: "\u{1F4F1}",
276
+ // 📱
277
+ tech_lead: "\u{1F451}",
278
+ // 👑
279
+ manager: "\u{1F454}",
280
+ // 👔
281
+ support_agent: "\u{1F4AC}",
282
+ // 💬
283
+ planner: "\u{1F4A1}",
284
+ // 💡 (planning_agent)
285
+ coordinator: "\u{1F3AF}",
286
+ // 🎯
287
+ critic: "\u{1F50D}",
288
+ // 🔍
289
+ reviewer: "\u{1F50D}"
290
+ // 🔍
291
+ };
292
+ function getPersonaEmoji(persona) {
293
+ return PERSONA_EMOJIS[persona] || "\u{1F916}";
294
+ }
295
+ function wmLog(persona, message) {
296
+ const emoji = getPersonaEmoji(persona);
297
+ console.log(chalk2.cyan(`[${emoji} ${persona} \u{1F3E0}] `) + chalk2.white(message));
298
+ info(`[${persona}] ${message}`);
299
+ }
300
+ function wmCoordinatorLog(message) {
301
+ console.log(chalk2.cyan("[coordinator] ") + chalk2.white(message));
302
+ info(`[coordinator] ${message}`);
303
+ }
304
+ function printError(message) {
305
+ console.log(chalk2.red(`
306
+ \u2717 ${message}
307
+ `));
308
+ }
309
+ var sessionStartTime = Date.now();
310
+
88
311
  // src/orchestrator.js
312
+ var LEARNING_INSTRUCTIONS = `
313
+
314
+ ## Reporting Learnings
315
+
316
+ When you discover something specific and actionable about this codebase, emit a learning marker:
317
+
318
+ \`\`\`
319
+ ::learning::The test suite requires DATABASE_URL env var or tests silently pass without running
320
+ ::learning::New API routes must be registered in backend/src/routes/index.ts or they won't load
321
+ \`\`\`
322
+
323
+ **Emit a learning when you discover:**
324
+ - A non-obvious requirement (specific env vars, config files, build steps)
325
+ - A codebase convention not documented elsewhere (naming patterns, file organization)
326
+ - A gotcha you had to work around (unexpected failures, ordering dependencies)
327
+ - Files that must be modified together (route + model + migration + test)
328
+
329
+ **Do NOT emit generic advice** like "write tests" or "handle errors properly."
330
+ Include file paths, commands, and exact details. Only emit when you genuinely discover something non-obvious.
331
+ `;
332
+ var DOCKER_INSTRUCTIONS = `
333
+
334
+ ## Development Environment
335
+
336
+ 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.
337
+
338
+ ### Common Services
339
+ - PostgreSQL: \`docker run -d --rm -p 5432:5432 -e POSTGRES_PASSWORD=test --name postgres-test postgres:16-alpine\`
340
+ - Redis: \`docker run -d --rm -p 6379:6379 --name redis-test redis:7-alpine\`
341
+ - MongoDB: \`docker run -d --rm -p 27017:27017 --name mongo-test mongo:7\`
342
+ - MySQL: \`docker run -d --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=test --name mysql-test mysql:8\`
343
+ - If the project has a \`docker-compose.yml\`, use \`docker compose up -d\`
344
+
345
+ Tests that pass against mocks but fail against real services are worthless.
346
+
347
+ ### CI/CD \u2014 Always add service containers
348
+ 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.
349
+ `;
350
+ var VERSION_TRUST = `
351
+
352
+ ## Technology Versions \u2014 Trust the Spec
353
+
354
+ 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.
355
+ `;
356
+ function buildReasoningOptions(provider, modelName) {
357
+ switch (provider) {
358
+ case "openai":
359
+ return { providerOptions: { openai: { reasoningSummary: "detailed" } } };
360
+ case "google":
361
+ case "gemini":
362
+ if (modelName && modelName.includes("gemini-3")) {
363
+ return { providerOptions: { google: { thinkingConfig: { thinkingLevel: "high", includeThoughts: true } } } };
364
+ }
365
+ return { providerOptions: { google: { thinkingConfig: { thinkingBudget: 8192, includeThoughts: true } } } };
366
+ default:
367
+ return {};
368
+ }
369
+ }
370
+ function isTransientError(error) {
371
+ if (!error || typeof error !== "object")
372
+ return false;
373
+ const msg = error instanceof Error ? error.message : String(error);
374
+ if (/status code (502|503|504)|socket hang up|ECONNRESET|ETIMEDOUT|network error|ECONNREFUSED/i.test(msg)) {
375
+ return true;
376
+ }
377
+ return false;
378
+ }
89
379
  async function classifyComplexity(config, userInput) {
90
- const { provider, model: modelName, apiKey, host } = getProviderForPersona(config);
380
+ const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config);
91
381
  if (apiKey) {
92
382
  const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
93
383
  const envVar = envMap[provider];
94
384
  if (envVar && !process.env[envVar])
95
385
  process.env[envVar] = apiKey;
96
386
  }
97
- const model = createModel(provider, modelName, host);
387
+ const model = createModel(provider, modelName, host, contextLength);
98
388
  try {
99
389
  const result = await generateObject({
100
390
  model,
@@ -135,7 +425,7 @@ function topologicalSort(stories) {
135
425
  if (visited.has(id))
136
426
  return;
137
427
  if (visiting.has(id)) {
138
- console.log(chalk.yellow(` \u26A0 Circular dependency at ${id}, using input order`));
428
+ console.log(chalk3.yellow(` \u26A0 Circular dependency at ${id}, using input order`));
139
429
  return;
140
430
  }
141
431
  visiting.add(id);
@@ -158,7 +448,7 @@ function topologicalSort(stories) {
158
448
  }
159
449
  async function planStories(config, userTask, workingDir, sandboxed = true) {
160
450
  const planner = loadPersona("planner");
161
- const { provider: pProvider, model: pModel, host: pHost } = getProviderForPersona(config, "planner");
451
+ const { provider: pProvider, model: pModel, host: pHost, contextLength: pCtx } = getProviderForPersona(config, "planner");
162
452
  if (pProvider) {
163
453
  const pApiKey = config.providers[pProvider]?.apiKey;
164
454
  if (pApiKey) {
@@ -171,13 +461,21 @@ async function planStories(config, userTask, workingDir, sandboxed = true) {
171
461
  }
172
462
  }
173
463
  }
174
- const plannerModel = createModel(pProvider, pModel, pHost);
464
+ const plannerModel = createModel(pProvider, pModel, pHost, pCtx);
175
465
  const plannerTools = createToolDefinitions(workingDir, plannerModel, sandboxed);
176
466
  const readOnlyTools = {};
177
467
  if (planner) {
178
468
  for (const toolName of planner.tools) {
179
- if (plannerTools[toolName]) {
180
- readOnlyTools[toolName] = plannerTools[toolName];
469
+ const toolDef = plannerTools[toolName];
470
+ if (toolDef) {
471
+ readOnlyTools[toolName] = {
472
+ ...toolDef,
473
+ execute: async (input) => {
474
+ wmLog("planner", formatToolCall(toolName, input));
475
+ const result = await toolDef.execute(input);
476
+ return result;
477
+ }
478
+ };
181
479
  }
182
480
  }
183
481
  }
@@ -215,56 +513,42 @@ Return ONLY a JSON code block with this structure:
215
513
  }
216
514
  \`\`\`
217
515
 
218
- Available personas: architect, backend_developer, frontend_developer, fullstack_developer, devops_engineer, qa_engineer, security_engineer, database_engineer, mobile_developer, data_engineer, ml_engineer`;
516
+ Available personas: backend_developer, frontend_developer, devops_engineer, qa_engineer, security_engineer, data_ml_engineer, mobile_developer, tech_writer, tech_lead`;
219
517
  wmLog("planner", `Starting planning agent using ${pModel}`);
220
518
  wmLog("planner", "Reading repository structure...");
519
+ let planText = "";
221
520
  const planStream = streamText({
222
521
  model: plannerModel,
223
522
  system: planner?.systemPrompt || "You are an implementation planner.",
224
523
  prompt: plannerPrompt,
225
524
  tools: readOnlyTools,
226
525
  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;
526
+ timeout: { totalMs: 3 * 60 * 1e3, chunkMs: 12e4 },
527
+ ...buildOllamaOptions(pProvider, pCtx),
528
+ onStepFinish({ text }) {
529
+ if (text) {
530
+ const lines = text.split("\n").filter((l) => l.trim());
531
+ 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);
535
+ }
249
536
  }
250
- process.stdout.write(chalk.white(chunk));
251
- if (chunk.endsWith("\n"))
252
- planNeedsPrefix = true;
253
537
  }
538
+ });
539
+ for await (const _chunk of planStream.textStream) {
254
540
  }
255
- if (!planNeedsPrefix)
256
- process.stdout.write("\n");
257
541
  const finalText = await planStream.text;
258
542
  if (finalText && finalText.length > planText.length) {
259
543
  planText = finalText;
260
544
  }
261
545
  let stories = parseStoriesFromText(planText);
262
546
  if (stories.length === 0) {
263
- console.log(chalk.yellow(" \u26A0 Planner didn't produce structured stories, falling back to single story"));
547
+ console.log(chalk3.yellow(" \u26A0 Planner didn't produce structured stories, falling back to single story"));
264
548
  stories = [{
265
549
  id: "implement",
266
550
  title: userTask.slice(0, 60),
267
- persona: "fullstack_developer",
551
+ persona: "backend_developer",
268
552
  description: userTask
269
553
  }];
270
554
  }
@@ -302,7 +586,7 @@ function parseStoriesFromText(text) {
302
586
  if (stories)
303
587
  return stories;
304
588
  const preview = text.slice(0, 500);
305
- console.log(chalk.dim(` (planner output preview: ${preview}${text.length > 500 ? "..." : ""})`));
589
+ console.log(chalk3.dim(` (planner output preview: ${preview}${text.length > 500 ? "..." : ""})`));
306
590
  return [];
307
591
  }
308
592
  function tryParseStories(text) {
@@ -379,6 +663,29 @@ function extractScore(text) {
379
663
  return 60;
380
664
  return 75;
381
665
  }
666
+ function parseAffectedStories(text) {
667
+ const storiesMatch = text.match(/AFFECTED_STORIES:\s*\[([^\]]+)\]/i);
668
+ if (!storiesMatch)
669
+ return null;
670
+ const stories = storiesMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
671
+ if (stories.length === 0)
672
+ return null;
673
+ let reasons = {};
674
+ const reasonsMatch = text.match(/AFFECTED_REASONS:\s*(\{[\s\S]*?\})/i);
675
+ if (reasonsMatch) {
676
+ try {
677
+ const parsed = JSON.parse(reasonsMatch[1]);
678
+ for (const [key, value] of Object.entries(parsed)) {
679
+ const storyIndex = parseInt(key, 10);
680
+ if (!isNaN(storyIndex) && typeof value === "string") {
681
+ reasons[storyIndex] = value;
682
+ }
683
+ }
684
+ } catch {
685
+ }
686
+ }
687
+ return { stories, reasons };
688
+ }
382
689
  async function runOrchestration(config, userTask, trustAll, sandboxed = true, agentRl) {
383
690
  const costTracker = new CostTracker();
384
691
  const context = {
@@ -402,16 +709,24 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
402
709
  if (config.review?.useCritic) {
403
710
  const critic = loadPersona("critic");
404
711
  if (critic) {
405
- const { provider: cProvider, model: cModel, host: cHost } = getProviderForPersona(config, "critic");
406
- const criticModel = createModel(cProvider, cModel, cHost);
712
+ const { provider: cProvider, model: cModel, host: cHost, contextLength: cCtx } = getProviderForPersona(config, "critic");
713
+ const criticModel = createModel(cProvider, cModel, cHost, cCtx);
407
714
  const criticTools = createToolDefinitions(workingDir, criticModel, sandboxed);
408
715
  const criticReadOnly = {};
409
716
  for (const name of critic.tools) {
410
- if (criticTools[name]) {
411
- criticReadOnly[name] = criticTools[name];
717
+ const toolDef = criticTools[name];
718
+ if (toolDef) {
719
+ criticReadOnly[name] = {
720
+ ...toolDef,
721
+ execute: async (input) => {
722
+ wmLog("critic", formatToolCall(name, input));
723
+ const result = await toolDef.execute(input);
724
+ return result;
725
+ }
726
+ };
412
727
  }
413
728
  }
414
- const criticSpinner = ora({ stream: process.stdout, text: chalk.white("Critic reviewing plan..."), prefixText: " " }).start();
729
+ const criticSpinner = ora({ stream: process.stdout, text: chalk3.white("Critic reviewing plan..."), prefixText: " " }).start();
415
730
  const criticStream = streamText({
416
731
  model: criticModel,
417
732
  system: critic.systemPrompt,
@@ -421,7 +736,8 @@ Stories:
421
736
  ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.description}`).join("\n")}`,
422
737
  tools: criticReadOnly,
423
738
  stopWhen: stepCountIs(100),
424
- abortSignal: AbortSignal.timeout(3 * 60 * 1e3)
739
+ timeout: { totalMs: 3 * 60 * 1e3, chunkMs: 12e4 },
740
+ ...buildOllamaOptions(cProvider, cCtx)
425
741
  });
426
742
  for await (const _chunk of criticStream.textStream) {
427
743
  }
@@ -437,11 +753,11 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
437
753
  if (!trustAll) {
438
754
  let answer = "n";
439
755
  try {
440
- answer = await permissions.askUser(chalk.dim(" Execute this plan? (y/n): "));
756
+ answer = await permissions.askUser(chalk3.dim(" Execute this plan? (y/n): "));
441
757
  } catch {
442
758
  }
443
759
  if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
444
- console.log(chalk.dim(" Plan cancelled.\n"));
760
+ console.log(chalk3.dim(" Plan cancelled.\n"));
445
761
  return;
446
762
  }
447
763
  console.log();
@@ -453,13 +769,16 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
453
769
  printError(`Unknown persona: ${story.persona}`);
454
770
  continue;
455
771
  }
456
- const { provider, model: modelName, apiKey, host } = getProviderForPersona(config, persona.provider || story.persona);
772
+ const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config, persona.provider || story.persona);
457
773
  if (apiKey) {
458
774
  const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
459
775
  const envVar = envMap[provider];
460
776
  if (envVar && !process.env[envVar])
461
777
  process.env[envVar] = apiKey;
462
778
  }
779
+ console.log(chalk3.bold(`
780
+ \u2500\u2500\u2500 Story ${i + 1}/${sorted.length} \u2500\u2500\u2500
781
+ `));
463
782
  wmCoordinatorLog(`Task claimed by orchestrator`);
464
783
  wmLog(story.persona, `Starting ${story.title}`);
465
784
  wmLog(story.persona, `Executing story with AIClient (model: ${modelName})...`);
@@ -469,7 +788,7 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
469
788
  prefixText: "",
470
789
  spinner: "dots"
471
790
  }).start();
472
- const model = createModel(provider, modelName, host);
791
+ const model = createModel(provider, modelName, host, contextLength);
473
792
  const allTools = createToolDefinitions(workingDir, model, sandboxed);
474
793
  const personaTools = {};
475
794
  let lastToolCall = "";
@@ -487,13 +806,9 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
487
806
  lastToolCall = callKey;
488
807
  if (!isDuplicate) {
489
808
  spinner.stop();
490
- wmLog(story.persona, `Tool: ${toolName}`);
809
+ wmLog(story.persona, formatToolCall(toolName, input));
491
810
  }
492
811
  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
812
  spinner.start();
498
813
  return result;
499
814
  }
@@ -525,6 +840,12 @@ Working directory: ${workingDir}
525
840
 
526
841
  Your task: ${story.description}
527
842
 
843
+ ## Communication Style
844
+
845
+ 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.
846
+
847
+ 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.
848
+
528
849
  ## Critical rules
529
850
  - 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
851
  - NEVER run interactive commands that wait for user input.
@@ -532,45 +853,39 @@ Your task: ${story.description}
532
853
  - 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
854
 
534
855
  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
856
  When you create a file, include ::file_created::path markers.
537
- When you modify a file, include ::file_modified::path markers.${revisionFeedback ? `
857
+ When you modify a file, include ::file_modified::path markers.
858
+ ${LEARNING_INSTRUCTIONS}${DOCKER_INSTRUCTIONS}${VERSION_TRUST}${revisionFeedback ? `
538
859
 
539
860
  ## Revision requested
540
861
  ${revisionFeedback}` : ""}`;
541
862
  try {
863
+ let allText = "";
542
864
  const stream = streamText({
543
865
  model,
544
866
  system: systemPrompt,
545
867
  prompt: story.description,
546
868
  tools: personaTools,
547
869
  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;
870
+ timeout: { totalMs: 10 * 60 * 1e3, chunkMs: 12e4 },
871
+ ...buildReasoningOptions(provider, modelName),
872
+ ...buildOllamaOptions(provider, contextLength),
873
+ onStepFinish({ text: text2 }) {
874
+ if (text2) {
875
+ spinner.stop();
876
+ const lines = text2.split("\n").filter((l) => l.trim());
877
+ 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);
881
+ }
566
882
  }
567
883
  }
884
+ });
885
+ for await (const _chunk of stream.textStream) {
568
886
  }
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;
887
+ const text = await stream.text;
888
+ allText = text;
574
889
  const usage = await stream.totalUsage;
575
890
  spinner.stop();
576
891
  const decisionMatches = text.match(/::decision::(.*?)(?=::\w+::|$)/gs);
@@ -602,12 +917,17 @@ ${revisionFeedback}` : ""}`;
602
917
  const inTokens = usage?.inputTokens || 0;
603
918
  const outTokens = usage?.outputTokens || 0;
604
919
  costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
605
- wmLog(story.persona, `${story.title} \u2014 completed!`);
920
+ wmLog(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
606
921
  console.log();
607
922
  break;
608
923
  } catch (err) {
609
924
  spinner.stop();
610
- printError(`Story ${i + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
925
+ const errMsg = err instanceof Error ? err.message : String(err);
926
+ if (isTransientError(err) && revision < 2) {
927
+ wmLog(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
928
+ continue;
929
+ }
930
+ printError(`Story ${i + 1} failed: ${errMsg}`);
611
931
  break;
612
932
  }
613
933
  }
@@ -617,7 +937,7 @@ ${revisionFeedback}` : ""}`;
617
937
  const approvalThreshold = config.review?.approvalThreshold ?? 80;
618
938
  const reviewer = loadPersona("reviewer");
619
939
  if (reviewer) {
620
- const { provider: revProvider, model: revModel, host: revHost } = getProviderForPersona(config, reviewer.provider || "reviewer");
940
+ const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(config, reviewer.provider || "reviewer");
621
941
  const revApiKey = config.providers[revProvider]?.apiKey;
622
942
  if (revApiKey) {
623
943
  const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_API_KEY" };
@@ -626,68 +946,117 @@ ${revisionFeedback}` : ""}`;
626
946
  if (envVar && key && !process.env[envVar])
627
947
  process.env[envVar] = key;
628
948
  }
629
- const reviewModel = createModel(revProvider, revModel, revHost);
949
+ const reviewModel = createModel(revProvider, revModel, revHost, revCtx);
630
950
  const reviewTools = createToolDefinitions(workingDir, reviewModel, sandboxed);
631
951
  const reviewerTools = {};
632
952
  for (const toolName of reviewer.tools) {
633
- if (reviewTools[toolName]) {
634
- reviewerTools[toolName] = reviewTools[toolName];
953
+ const toolDef = reviewTools[toolName];
954
+ if (toolDef) {
955
+ reviewerTools[toolName] = {
956
+ ...toolDef,
957
+ execute: async (input) => {
958
+ wmLog("tech_lead", formatToolCall(toolName, input));
959
+ const result = await toolDef.execute(input);
960
+ return result;
961
+ }
962
+ };
635
963
  }
636
964
  }
965
+ let previousReviewFeedback = "";
637
966
  for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
638
967
  const isRevision = reviewRound > 0;
639
968
  wmCoordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
640
969
  wmLog("tech_lead", "Starting agent execution");
641
970
  const reviewSpinner = ora({
642
971
  stream: process.stdout,
643
- text: chalk.white(isRevision ? "Reviewer \u2014 Re-checking after revisions" : "Reviewer \u2014 Checking code quality"),
972
+ text: chalk3.white(isRevision ? "Reviewer \u2014 Re-checking after revisions" : "Reviewer \u2014 Checking code quality"),
644
973
  prefixText: " "
645
974
  }).start();
646
975
  try {
647
- const reviewPrompt = `Review the changes made by the following experts:
976
+ const previousFeedbackSection = isRevision && previousReviewFeedback ? `## Previous Review Feedback (Review ${reviewRound}/${maxRevisions})
977
+ This is a revision attempt. The previous code was reviewed and these issues were identified:
978
+
979
+ ${previousReviewFeedback}
980
+
981
+ **IMPORTANT: Check if ALL issues above have been addressed, not just some of them.**
982
+ - The developer was instructed to fix every item
983
+ - If ANY issue remains unaddressed, request another revision
984
+ - Be specific about which items are still outstanding
985
+
986
+ ---
987
+
988
+ ` : "";
989
+ const storySummaryRows = sorted.map((s, idx) => {
990
+ const files = [...context.filesCreated, ...context.filesModified].slice(0, 3).join(", ") || "(none)";
991
+ return `| ${idx + 1} | ${s.persona} | ${s.title} | ${files} |`;
992
+ }).join("\n");
993
+ const reviewPrompt = `${previousFeedbackSection}## Original Task
994
+
995
+ ${userTask}
996
+
997
+ ## Story Summary
648
998
 
649
- ${sorted.map((s, idx) => `${idx + 1}. ${s.persona}: ${s.title} \u2014 ${s.description}`).join("\n")}
999
+ | # | Persona | Title | Files |
1000
+ |---|---------|-------|-------|
1001
+ ${storySummaryRows}
1002
+
1003
+ ## Changes Made
650
1004
 
651
1005
  Files created: ${context.filesCreated.join(", ") || "none"}
652
1006
  Files modified: ${context.filesModified.join(", ") || "none"}
1007
+ ${context.decisions.length > 0 ? `
1008
+ Decisions made:
1009
+ ${context.decisions.map((d) => `- ${d}`).join("\n")}` : ""}
1010
+
1011
+ ## Review Instructions
653
1012
 
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
1013
+ Use read_file, glob, grep, and git tools to examine the actual code. Check:
1014
+ - Does the code correctly implement the original task requirements?
1015
+ - Are there bugs, logic errors, or security issues?
1016
+ - Does the code follow existing project conventions?
1017
+ - Is error handling appropriate?
1018
+ - Are there missing pieces from the task requirements?
1019
+
1020
+ Use \`git diff\` or read individual files to see the actual changes.
660
1021
 
661
1022
  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.`;
1023
+
1024
+ ### For REVISION_NEEDED Decisions - Specify Affected Stories
1025
+
1026
+ When requesting revision, you MUST specify which stories need changes. Use the story numbers from the Story Summary table above.
1027
+
1028
+ \`\`\`
1029
+ AFFECTED_STORIES: [2, 3]
1030
+ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Frontend form has no validation"}
1031
+ \`\`\`
1032
+
1033
+ **Guidelines:**
1034
+ - Only include stories that have ACTUAL implementation issues
1035
+ - If ALL stories need revision, you may omit AFFECTED_STORIES (all will re-run)
1036
+ - Be specific in AFFECTED_REASONS so developers know exactly what to fix`;
1037
+ let allReviewText = "";
663
1038
  const reviewStream = streamText({
664
1039
  model: reviewModel,
665
1040
  system: reviewer.systemPrompt,
666
1041
  prompt: reviewPrompt,
667
1042
  tools: reviewerTools,
668
1043
  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;
1044
+ timeout: { totalMs: 5 * 60 * 1e3, chunkMs: 12e4 },
1045
+ ...buildOllamaOptions(revProvider, revCtx),
1046
+ onStepFinish({ text }) {
1047
+ if (text) {
1048
+ reviewSpinner.stop();
1049
+ const lines = text.split("\n").filter((l) => l.trim());
1050
+ for (const line of lines) {
1051
+ if (line.includes("::review_score::") || line.includes("::review_verdict::"))
1052
+ continue;
1053
+ wmLog("tech_lead", line);
1054
+ }
683
1055
  }
684
- process.stdout.write(chalk.white(chunk));
685
- if (chunk.endsWith("\n"))
686
- revNeedsPrefix = true;
687
1056
  }
1057
+ });
1058
+ for await (const _chunk of reviewStream.textStream) {
688
1059
  }
689
- if (!revNeedsPrefix)
690
- process.stdout.write("\n");
691
1060
  const finalReviewText = await reviewStream.text;
692
1061
  const reviewText = finalReviewText && finalReviewText.length > allReviewText.length ? finalReviewText : allReviewText;
693
1062
  const reviewUsage = await reviewStream.totalUsage;
@@ -697,36 +1066,54 @@ If there are issues, be specific about which files and what needs to change.`;
697
1066
  wmLog("tech_lead", `::code_quality_score::${score}`);
698
1067
  wmLog("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
699
1068
  wmCoordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
1069
+ previousReviewFeedback = reviewText;
700
1070
  console.log();
701
1071
  costTracker.addUsage(`Reviewer (round ${reviewRound + 1})`, revProvider, revModel, reviewUsage?.inputTokens || 0, reviewUsage?.outputTokens || 0);
702
1072
  if (approved)
703
1073
  break;
704
1074
  if (reviewRound >= maxRevisions) {
705
- console.log(chalk.yellow(` \u26A0 Max review revisions (${maxRevisions}) reached`));
1075
+ console.log(chalk3.yellow(` \u26A0 Max review revisions (${maxRevisions}) reached`));
706
1076
  break;
707
1077
  }
708
1078
  let shouldRevise = autoRevise;
709
1079
  if (!autoRevise) {
710
1080
  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): `));
1081
+ const answer = await permissions.askUser(chalk3.dim(" Revise and re-review? ") + chalk3.white(`(y/n, ${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left): `));
712
1082
  shouldRevise = answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
713
1083
  } catch {
714
1084
  shouldRevise = false;
715
1085
  }
716
1086
  } else {
717
- console.log(chalk.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
1087
+ console.log(chalk3.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
718
1088
  }
719
1089
  if (!shouldRevise) {
720
- console.log(chalk.dim(" Skipping revision, proceeding to commit."));
1090
+ console.log(chalk3.dim(" Skipping revision, proceeding to commit."));
721
1091
  break;
722
1092
  }
723
- console.log(chalk.bold("\n \u2500\u2500\u2500 Revision Pass \u2500\u2500\u2500\n"));
1093
+ const affected = parseAffectedStories(reviewText);
1094
+ const affectedSet = affected ? new Set(affected.stories) : null;
1095
+ if (affected) {
1096
+ const selectiveInfo = `stories ${affected.stories.join(", ")}`;
1097
+ wmCoordinatorLog(`Selective revision: ${selectiveInfo}`);
1098
+ if (Object.keys(affected.reasons).length > 0) {
1099
+ for (const [idx, reason] of Object.entries(affected.reasons)) {
1100
+ wmCoordinatorLog(` Story ${idx}: ${reason}`);
1101
+ }
1102
+ }
1103
+ } else {
1104
+ wmCoordinatorLog("Full revision (all stories)");
1105
+ }
1106
+ console.log(chalk3.bold("\n \u2500\u2500\u2500 Revision Pass \u2500\u2500\u2500\n"));
724
1107
  for (let i = 0; i < sorted.length; i++) {
725
1108
  const story = sorted[i];
1109
+ if (affectedSet && !affectedSet.has(i + 1)) {
1110
+ wmCoordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
1111
+ continue;
1112
+ }
726
1113
  const storyPersona = loadPersona(story.persona);
727
1114
  if (!storyPersona)
728
1115
  continue;
729
- const { provider: sProvider, model: sModel, host: sHost } = getProviderForPersona(config, storyPersona.provider || story.persona);
1116
+ const { provider: sProvider, model: sModel, host: sHost, contextLength: sCtx } = getProviderForPersona(config, storyPersona.provider || story.persona);
730
1117
  if (sProvider) {
731
1118
  const sApiKey = config.providers[sProvider]?.apiKey;
732
1119
  if (sApiKey) {
@@ -739,12 +1126,15 @@ If there are issues, be specific about which files and what needs to change.`;
739
1126
  }
740
1127
  }
741
1128
  }
1129
+ wmCoordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
1130
+ wmLog(story.persona, `Starting revision: ${story.title}`);
742
1131
  const revSpinner = ora({
743
1132
  stream: process.stdout,
744
- text: chalk.white(`Revising ${i + 1}/${sorted.length} \u2014 ${storyPersona.name} \u2014 ${story.title}`),
745
- prefixText: " "
1133
+ text: "",
1134
+ prefixText: "",
1135
+ spinner: "dots"
746
1136
  }).start();
747
- const storyModel = createModel(sProvider, sModel, sHost);
1137
+ const storyModel = createModel(sProvider, sModel, sHost, sCtx);
748
1138
  const storyAllTools = createToolDefinitions(workingDir, storyModel, sandboxed);
749
1139
  const storyTools = {};
750
1140
  for (const toolName of storyPersona.tools) {
@@ -757,10 +1147,8 @@ If there are issues, be specific about which files and what needs to change.`;
757
1147
  if (!allowed)
758
1148
  return "Tool execution denied by user.";
759
1149
  revSpinner.stop();
760
- printToolCall(toolName, input);
1150
+ wmLog(story.persona, formatToolCall(toolName, input));
761
1151
  const result = await toolDef.execute(input);
762
- const resultStr = typeof result === "string" ? result : JSON.stringify(result);
763
- printToolResult(toolName, resultStr);
764
1152
  revSpinner.start();
765
1153
  return result;
766
1154
  }
@@ -771,6 +1159,10 @@ If there are issues, be specific about which files and what needs to change.`;
771
1159
 
772
1160
  Working directory: ${workingDir}
773
1161
 
1162
+ ## Communication Style
1163
+
1164
+ 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.
1165
+
774
1166
  ## Critical rules
775
1167
  - NEVER start long-running processes (dev servers, watch modes, npm start, npm run dev, nodemon, tsc --watch, etc.)
776
1168
  - NEVER run interactive commands that wait for user input
@@ -789,7 +1181,20 @@ Your task: Address the reviewer's feedback for "${story.title}". Fix the specifi
789
1181
  ${story.description}`,
790
1182
  tools: storyTools,
791
1183
  stopWhen: stepCountIs(100),
792
- abortSignal: AbortSignal.timeout(5 * 60 * 1e3)
1184
+ timeout: { totalMs: 5 * 60 * 1e3, chunkMs: 12e4 },
1185
+ ...buildReasoningOptions(sProvider, sModel),
1186
+ ...buildOllamaOptions(sProvider, sCtx),
1187
+ onStepFinish({ text }) {
1188
+ if (text) {
1189
+ revSpinner.stop();
1190
+ const lines = text.split("\n").filter((l) => l.trim());
1191
+ for (const line of lines) {
1192
+ if (line.includes("::"))
1193
+ continue;
1194
+ wmLog(story.persona, line);
1195
+ }
1196
+ }
1197
+ }
793
1198
  });
794
1199
  for await (const _chunk of revStream.textStream) {
795
1200
  }
@@ -799,25 +1204,25 @@ ${story.description}`,
799
1204
  wmLog(story.persona, `${story.title} \u2014 revision complete!`);
800
1205
  } catch (err) {
801
1206
  revSpinner.stop();
802
- console.log(chalk.yellow(` \u26A0 Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`));
1207
+ console.log(chalk3.yellow(` \u26A0 Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`));
803
1208
  }
804
1209
  }
805
1210
  console.log();
806
1211
  } catch (err) {
807
1212
  reviewSpinner.stop();
808
- console.log(chalk.yellow(` \u26A0 Review skipped: ${err instanceof Error ? err.message : String(err)}`));
1213
+ console.log(chalk3.yellow(` \u26A0 Review skipped: ${err instanceof Error ? err.message : String(err)}`));
809
1214
  console.log();
810
1215
  break;
811
1216
  }
812
1217
  }
813
1218
  }
814
1219
  try {
815
- const { execSync } = await import("child_process");
1220
+ const { execSync: execSync2 } = await import("child_process");
816
1221
  try {
817
- execSync("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1222
+ execSync2("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
818
1223
  } catch {
819
1224
  wmCoordinatorLog("Initializing git repository...");
820
- execSync("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
1225
+ execSync2("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
821
1226
  const fs2 = await import("fs");
822
1227
  const gitignorePath = `${workingDir}/.gitignore`;
823
1228
  if (!fs2.existsSync(gitignorePath)) {
@@ -825,48 +1230,48 @@ ${story.description}`,
825
1230
  }
826
1231
  wmCoordinatorLog("Git repo initialized");
827
1232
  }
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();
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();
830
1235
  const hasChanges = diff || untracked;
831
1236
  if (hasChanges) {
832
- console.log(chalk.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
1237
+ console.log(chalk3.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
833
1238
  if (diff) {
834
- console.log(chalk.dim(" " + diff.split("\n").join("\n ")));
1239
+ console.log(chalk3.dim(" " + diff.split("\n").join("\n ")));
835
1240
  }
836
1241
  if (untracked) {
837
1242
  const untrackedFiles = untracked.split("\n");
838
- console.log(chalk.dim(" New files:"));
1243
+ console.log(chalk3.dim(" New files:"));
839
1244
  for (const f of untrackedFiles) {
840
- console.log(chalk.dim(` + ${f}`));
1245
+ console.log(chalk3.dim(` + ${f}`));
841
1246
  }
842
1247
  }
843
1248
  console.log();
844
1249
  if (!trustAll) {
845
- const answer = await permissions.askUser(chalk.dim(" Commit these changes? (y/n): "));
1250
+ const answer = await permissions.askUser(chalk3.dim(" Commit these changes? (y/n): "));
846
1251
  if (answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes") {
847
1252
  const filesToStage = [...context.filesCreated, ...context.filesModified].filter(Boolean);
848
1253
  if (filesToStage.length > 0) {
849
1254
  for (const f of filesToStage) {
850
1255
  try {
851
- execSync(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
1256
+ execSync2(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
852
1257
  } catch {
853
1258
  }
854
1259
  }
855
1260
  } else {
856
- execSync("git add -u", { cwd: workingDir, stdio: "pipe" });
1261
+ execSync2("git add -u", { cwd: workingDir, stdio: "pipe" });
857
1262
  }
858
1263
  const storyTitles = sorted.map((s) => s.title).join(", ");
859
1264
  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"));
1265
+ execSync2(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
1266
+ console.log(chalk3.green(" \u2713 Changes committed"));
862
1267
  }
863
1268
  }
864
1269
  }
865
1270
  } catch (err) {
866
1271
  }
867
- console.log(chalk.bold(" \u2500\u2500\u2500 Session Complete \u2500\u2500\u2500"));
1272
+ console.log(chalk3.bold(" \u2500\u2500\u2500 Session Complete \u2500\u2500\u2500"));
868
1273
  console.log();
869
- console.log(chalk.dim(" " + costTracker.getSummary().split("\n").join("\n ")));
1274
+ console.log(chalk3.dim(" " + costTracker.getSummary().split("\n").join("\n ")));
870
1275
  console.log();
871
1276
  }
872
1277
  export {