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.
- package/README.md +24 -5
- package/dist/{chunk-3KIFXIBC.js → chunk-NGQKIYVB.js} +253 -469
- package/dist/index.js +1762 -687
- package/dist/{orchestrator-NMTZUS23.js → orchestrator-2M4BCHQR.js} +563 -158
- package/package.json +7 -1
- package/personas/architect.md +51 -0
- package/personas/backend_developer.md +51 -0
- package/personas/critic.md +65 -16
- package/personas/data_ml_engineer.md +83 -0
- package/personas/devops_engineer.md +51 -0
- package/personas/frontend_developer.md +51 -0
- package/personas/mobile_developer.md +51 -0
- package/personas/planner.md +105 -16
- package/personas/qa_engineer.md +51 -0
- package/personas/security_engineer.md +51 -0
- package/personas/tech_lead.md +125 -0
- package/personas/tech_writer.md +51 -0
- package/dist/chunk-2NTK7H4W.js +0 -10
- package/dist/chunk-LVCJZJJH.js +0 -29
- package/dist/terminal-ILMO7Z3P.js +0 -17
- package/personas/data_engineer.md +0 -32
- package/personas/ml_engineer.md +0 -32
- package/personas/reviewer.md +0 -30
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CostTracker,
|
|
3
|
-
|
|
3
|
+
__dirname,
|
|
4
|
+
buildOllamaOptions,
|
|
4
5
|
createModel,
|
|
5
6
|
createToolDefinitions,
|
|
6
|
-
getPersonaEmoji,
|
|
7
7
|
getProviderForPersona,
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
180
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|
|
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(
|
|
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: "
|
|
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(
|
|
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
|
-
|
|
411
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
655
|
-
-
|
|
656
|
-
-
|
|
657
|
-
-
|
|
658
|
-
-
|
|
659
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1087
|
+
console.log(chalk3.dim(` Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`));
|
|
718
1088
|
}
|
|
719
1089
|
if (!shouldRevise) {
|
|
720
|
-
console.log(
|
|
1090
|
+
console.log(chalk3.dim(" Skipping revision, proceeding to commit."));
|
|
721
1091
|
break;
|
|
722
1092
|
}
|
|
723
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1222
|
+
execSync2("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
|
|
818
1223
|
} catch {
|
|
819
1224
|
wmCoordinatorLog("Initializing git repository...");
|
|
820
|
-
|
|
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 =
|
|
829
|
-
const untracked =
|
|
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(
|
|
1237
|
+
console.log(chalk3.bold(" \u2500\u2500\u2500 Changes \u2500\u2500\u2500"));
|
|
833
1238
|
if (diff) {
|
|
834
|
-
console.log(
|
|
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(
|
|
1243
|
+
console.log(chalk3.dim(" New files:"));
|
|
839
1244
|
for (const f of untrackedFiles) {
|
|
840
|
-
console.log(
|
|
1245
|
+
console.log(chalk3.dim(` + ${f}`));
|
|
841
1246
|
}
|
|
842
1247
|
}
|
|
843
1248
|
console.log();
|
|
844
1249
|
if (!trustAll) {
|
|
845
|
-
const answer = await permissions.askUser(
|
|
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
|
-
|
|
1256
|
+
execSync2(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
|
|
852
1257
|
} catch {
|
|
853
1258
|
}
|
|
854
1259
|
}
|
|
855
1260
|
} else {
|
|
856
|
-
|
|
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
|
-
|
|
861
|
-
console.log(
|
|
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(
|
|
1272
|
+
console.log(chalk3.bold(" \u2500\u2500\u2500 Session Complete \u2500\u2500\u2500"));
|
|
868
1273
|
console.log();
|
|
869
|
-
console.log(
|
|
1274
|
+
console.log(chalk3.dim(" " + costTracker.getSummary().split("\n").join("\n ")));
|
|
870
1275
|
console.log();
|
|
871
1276
|
}
|
|
872
1277
|
export {
|