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.
- package/README.md +14 -5
- package/dist/{chunk-3KIFXIBC.js → chunk-VC6VNVEY.js} +214 -468
- package/dist/index.js +1707 -690
- package/dist/{orchestrator-NMTZUS23.js → orchestrator-5I7BGPC7.js} +597 -162
- package/package.json +7 -1
- package/personas/planner.md +1 -1
- package/dist/chunk-2NTK7H4W.js +0 -10
- package/dist/chunk-LVCJZJJH.js +0 -29
- package/dist/terminal-ILMO7Z3P.js +0 -17
- package/personas/ml_engineer.md +0 -32
- /package/personas/{data_engineer.md → data_ml_engineer.md} +0 -0
- /package/personas/{reviewer.md → tech_lead.md} +0 -0
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CostTracker,
|
|
3
|
-
|
|
3
|
+
__dirname,
|
|
4
|
+
buildOllamaOptions,
|
|
4
5
|
createModel,
|
|
5
6
|
createToolDefinitions,
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
180
|
-
|
|
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:
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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(
|
|
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: "
|
|
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(
|
|
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
|
-
|
|
411
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1117
|
+
console.log(chalk3.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
|
|
718
1118
|
}
|
|
719
1119
|
if (!shouldRevise) {
|
|
720
|
-
console.log(
|
|
1120
|
+
console.log(chalk3.dim(" Skipping revision, proceeding to commit."));
|
|
721
1121
|
break;
|
|
722
1122
|
}
|
|
723
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1252
|
+
execSync2("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
|
|
818
1253
|
} catch {
|
|
819
1254
|
wmCoordinatorLog("Initializing git repository...");
|
|
820
|
-
|
|
821
|
-
const
|
|
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 (!
|
|
824
|
-
|
|
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 =
|
|
829
|
-
const untracked =
|
|
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(
|
|
1267
|
+
console.log(chalk3.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
|
|
833
1268
|
if (diff) {
|
|
834
|
-
console.log(
|
|
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(
|
|
1273
|
+
console.log(chalk3.dim(" New files:"));
|
|
839
1274
|
for (const f of untrackedFiles) {
|
|
840
|
-
console.log(
|
|
1275
|
+
console.log(chalk3.dim(` + ${f}`));
|
|
841
1276
|
}
|
|
842
1277
|
}
|
|
843
1278
|
console.log();
|
|
844
1279
|
if (!trustAll) {
|
|
845
|
-
const answer = await permissions.askUser(
|
|
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
|
-
|
|
1286
|
+
execSync2(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
|
|
852
1287
|
} catch {
|
|
853
1288
|
}
|
|
854
1289
|
}
|
|
855
1290
|
} else {
|
|
856
|
-
|
|
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
|
-
|
|
861
|
-
console.log(
|
|
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(
|
|
1302
|
+
console.log(chalk3.bold(" \u2500\u2500\u2500 Session Complete \u2500\u2500\u2500"));
|
|
868
1303
|
console.log();
|
|
869
|
-
console.log(
|
|
1304
|
+
console.log(chalk3.dim(" " + costTracker.getSummary().split("\n").join("\n ")));
|
|
870
1305
|
console.log();
|
|
871
1306
|
}
|
|
872
1307
|
export {
|