workermill 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,23 +5,24 @@ import {
|
|
|
5
5
|
createModel,
|
|
6
6
|
createToolDefinitions,
|
|
7
7
|
getProviderForPersona,
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
init_esm_shims
|
|
9
|
+
} from "./chunk-KL7SFKGG.js";
|
|
10
10
|
|
|
11
|
-
// src/orchestrator.
|
|
12
|
-
|
|
13
|
-
import ora from "ora";
|
|
11
|
+
// src/orchestrator.ts
|
|
12
|
+
init_esm_shims();
|
|
14
13
|
import { streamText, generateObject, generateText, stepCountIs } from "ai";
|
|
15
14
|
import { z } from "zod";
|
|
15
|
+
import fs2 from "fs";
|
|
16
|
+
import path2 from "path";
|
|
16
17
|
|
|
17
|
-
// src/personas.
|
|
18
|
+
// src/personas.ts
|
|
19
|
+
init_esm_shims();
|
|
18
20
|
import fs from "fs";
|
|
19
21
|
import path from "path";
|
|
20
22
|
import os from "os";
|
|
21
23
|
function parsePersonaFile(content) {
|
|
22
24
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
23
|
-
if (!match)
|
|
24
|
-
return null;
|
|
25
|
+
if (!match) return null;
|
|
25
26
|
const frontmatter = match[1];
|
|
26
27
|
const body = match[2].trim();
|
|
27
28
|
const meta = {};
|
|
@@ -36,8 +37,7 @@ function parsePersonaFile(content) {
|
|
|
36
37
|
meta[key] = value;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
if (!meta.name || !meta.slug)
|
|
40
|
-
return null;
|
|
40
|
+
if (!meta.name || !meta.slug) return null;
|
|
41
41
|
return {
|
|
42
42
|
name: meta.name,
|
|
43
43
|
slug: meta.slug,
|
|
@@ -65,8 +65,7 @@ function loadPersona(slug) {
|
|
|
65
65
|
if (fs.existsSync(loc)) {
|
|
66
66
|
const content = fs.readFileSync(loc, "utf-8");
|
|
67
67
|
const persona = parsePersonaFile(content);
|
|
68
|
-
if (persona)
|
|
69
|
-
return persona;
|
|
68
|
+
if (persona) return persona;
|
|
70
69
|
}
|
|
71
70
|
} catch {
|
|
72
71
|
continue;
|
|
@@ -81,234 +80,21 @@ function loadPersona(slug) {
|
|
|
81
80
|
};
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
// src/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{ pattern: /git\s+clean\s+-[a-z]*f/i, label: "git clean" },
|
|
93
|
-
{ pattern: /drop\s+table/i, label: "drop table" },
|
|
94
|
-
{ pattern: /truncate\s+/i, label: "truncate" },
|
|
95
|
-
{ pattern: /DELETE\s+FROM\s+\w+\s*;/i, label: "DELETE without WHERE" },
|
|
96
|
-
{ pattern: /chmod\s+777/i, label: "chmod 777" },
|
|
97
|
-
{ pattern: />(\/dev\/sda|\/dev\/disk)/i, label: "write to disk device" }
|
|
98
|
-
];
|
|
99
|
-
function isDangerous(command) {
|
|
100
|
-
for (const { pattern, label } of DANGEROUS_PATTERNS) {
|
|
101
|
-
if (pattern.test(command))
|
|
102
|
-
return label;
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
var PermissionManager = class {
|
|
107
|
-
sessionAllow = /* @__PURE__ */ new Set();
|
|
108
|
-
trustAll;
|
|
109
|
-
configTrust;
|
|
110
|
-
rl = null;
|
|
111
|
-
cancelCurrentPrompt = null;
|
|
112
|
-
/** True while rl.question() is active — external line handlers must ignore input */
|
|
113
|
-
questionActive = false;
|
|
114
|
-
constructor(trustAll = false, configTrust = []) {
|
|
115
|
-
this.trustAll = trustAll;
|
|
116
|
-
this.configTrust = new Set(configTrust);
|
|
117
|
-
}
|
|
118
|
-
/** Bind to the agent's readline instance so we reuse it for prompts */
|
|
119
|
-
setReadline(rl) {
|
|
120
|
-
this.rl = rl;
|
|
121
|
-
}
|
|
122
|
-
cancelPrompt() {
|
|
123
|
-
if (this.cancelCurrentPrompt) {
|
|
124
|
-
this.cancelCurrentPrompt();
|
|
125
|
-
this.cancelCurrentPrompt = null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
async checkPermission(toolName, toolInput) {
|
|
129
|
-
if (toolName === "bash") {
|
|
130
|
-
const cmd = String(toolInput.command || "");
|
|
131
|
-
const danger = isDangerous(cmd);
|
|
132
|
-
if (danger) {
|
|
133
|
-
if (this.trustAll)
|
|
134
|
-
return true;
|
|
135
|
-
console.log();
|
|
136
|
-
console.log(chalk.red.bold(` \u26A0 DANGEROUS: ${danger}`));
|
|
137
|
-
console.log(chalk.red(` Command: ${cmd}`));
|
|
138
|
-
const answer = await this.askUser(chalk.red(" Are you sure? (yes to confirm): "));
|
|
139
|
-
if (answer.trim().toLowerCase() !== "yes")
|
|
140
|
-
return false;
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (this.trustAll)
|
|
145
|
-
return true;
|
|
146
|
-
if (READ_TOOLS.has(toolName))
|
|
147
|
-
return true;
|
|
148
|
-
if (this.sessionAllow.has(toolName))
|
|
149
|
-
return true;
|
|
150
|
-
if (this.configTrust.has(toolName))
|
|
151
|
-
return true;
|
|
152
|
-
return this.promptUser(toolName, toolInput);
|
|
153
|
-
}
|
|
154
|
-
async promptUser(toolName, toolInput) {
|
|
155
|
-
const display = this.formatToolCall(toolName, toolInput);
|
|
156
|
-
console.log();
|
|
157
|
-
console.log(chalk.cyan(` \u250C\u2500 ${toolName} ${"\u2500".repeat(Math.max(0, 40 - toolName.length))}\u2510`));
|
|
158
|
-
for (const line of display.split("\n")) {
|
|
159
|
-
console.log(chalk.cyan(" \u2502 ") + chalk.white(line));
|
|
160
|
-
}
|
|
161
|
-
console.log(chalk.cyan(` \u2514${"\u2500".repeat(43)}\u2518`));
|
|
162
|
-
const answer = await this.askUser(chalk.dim(" Allow? ") + chalk.white("(y)es / (n)o / (a)lways this tool / (t)rust all: "));
|
|
163
|
-
const choice = answer.trim().toLowerCase();
|
|
164
|
-
if (choice === "t" || choice === "trust") {
|
|
165
|
-
this.trustAll = true;
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
if (choice === "a" || choice === "always") {
|
|
169
|
-
this.sessionAllow.add(toolName);
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
return choice === "y" || choice === "yes";
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Prompt the user with a question. Sets questionActive flag so the
|
|
176
|
-
* agent's line handler knows to ignore this input.
|
|
177
|
-
*/
|
|
178
|
-
askUser(prompt) {
|
|
179
|
-
return new Promise((resolve, reject) => {
|
|
180
|
-
this.cancelCurrentPrompt = () => {
|
|
181
|
-
this.questionActive = false;
|
|
182
|
-
reject(new Error("cancelled"));
|
|
183
|
-
};
|
|
184
|
-
if (this.rl) {
|
|
185
|
-
this.questionActive = true;
|
|
186
|
-
this.rl.resume();
|
|
187
|
-
this.rl.question(prompt, (answer) => {
|
|
188
|
-
this.questionActive = false;
|
|
189
|
-
this.cancelCurrentPrompt = null;
|
|
190
|
-
this.rl.pause();
|
|
191
|
-
resolve(answer);
|
|
192
|
-
});
|
|
193
|
-
} else {
|
|
194
|
-
const questionRl = readline.createInterface({
|
|
195
|
-
input: process.stdin,
|
|
196
|
-
output: process.stdout
|
|
197
|
-
});
|
|
198
|
-
this.questionActive = true;
|
|
199
|
-
questionRl.question(prompt, (answer) => {
|
|
200
|
-
this.questionActive = false;
|
|
201
|
-
this.cancelCurrentPrompt = null;
|
|
202
|
-
questionRl.close();
|
|
203
|
-
resolve(answer);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
formatToolCall(toolName, input) {
|
|
209
|
-
switch (toolName) {
|
|
210
|
-
case "bash":
|
|
211
|
-
return String(input.command || "");
|
|
212
|
-
case "write_file":
|
|
213
|
-
case "edit_file":
|
|
214
|
-
return `${input.path || ""}`;
|
|
215
|
-
case "patch":
|
|
216
|
-
return String(input.patch_text || "").slice(0, 200) + "...";
|
|
217
|
-
case "fetch":
|
|
218
|
-
return String(input.url || "");
|
|
219
|
-
default:
|
|
220
|
-
return JSON.stringify(input, null, 2).slice(0, 200);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
};
|
|
83
|
+
// src/orchestrator.ts
|
|
84
|
+
function resolveTaskInput(task, workingDir) {
|
|
85
|
+
const trimmed = task.trim();
|
|
86
|
+
if (!trimmed.includes(" ") && /\.\w{1,10}$/.test(trimmed)) {
|
|
87
|
+
const fullPath = path2.resolve(workingDir, trimmed);
|
|
88
|
+
try {
|
|
89
|
+
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
90
|
+
return `Implement the following specification from ${trimmed}:
|
|
224
91
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
import { execSync } from "child_process";
|
|
228
|
-
function formatToolCall(toolName, toolInput) {
|
|
229
|
-
let msg = `Tool: ${toolName}`;
|
|
230
|
-
if (toolInput) {
|
|
231
|
-
if (toolInput.file_path)
|
|
232
|
-
msg += ` \u2192 ${toolInput.file_path}`;
|
|
233
|
-
else if (toolInput.path)
|
|
234
|
-
msg += ` \u2192 ${toolInput.path}`;
|
|
235
|
-
else if (toolInput.command)
|
|
236
|
-
msg += ` \u2192 ${String(toolInput.command).substring(0, 500)}`;
|
|
237
|
-
else if (toolInput.pattern)
|
|
238
|
-
msg += ` \u2192 pattern: ${toolInput.pattern}`;
|
|
239
|
-
else {
|
|
240
|
-
const keys = Object.keys(toolInput).slice(0, 3);
|
|
241
|
-
if (keys.length > 0) {
|
|
242
|
-
msg += ` \u2192 ${keys.map((k) => `${k}: ${String(toolInput[k]).substring(0, 200)}`).join(", ")}`;
|
|
243
|
-
}
|
|
92
|
+
${content}`;
|
|
93
|
+
} catch {
|
|
244
94
|
}
|
|
245
95
|
}
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
var PERSONA_EMOJIS = {
|
|
249
|
-
frontend_developer: "\u{1F3A8}",
|
|
250
|
-
// 🎨
|
|
251
|
-
backend_developer: "\u{1F4BB}",
|
|
252
|
-
// 💻
|
|
253
|
-
fullstack_developer: "\u{1F4BB}",
|
|
254
|
-
// 💻 (same as backend)
|
|
255
|
-
devops_engineer: "\u{1F527}",
|
|
256
|
-
// 🔧
|
|
257
|
-
security_engineer: "\u{1F512}",
|
|
258
|
-
// 🔐
|
|
259
|
-
qa_engineer: "\u{1F9EA}",
|
|
260
|
-
// 🧪
|
|
261
|
-
tech_writer: "\u{1F4DD}",
|
|
262
|
-
// 📝
|
|
263
|
-
project_manager: "\u{1F4CB}",
|
|
264
|
-
// 📋
|
|
265
|
-
architect: "\u{1F3D7}\uFE0F",
|
|
266
|
-
// 🏗️
|
|
267
|
-
database_engineer: "\u{1F4CA}",
|
|
268
|
-
// 📊
|
|
269
|
-
data_engineer: "\u{1F4CA}",
|
|
270
|
-
// 📊
|
|
271
|
-
data_ml_engineer: "\u{1F4CA}",
|
|
272
|
-
// 📊
|
|
273
|
-
ml_engineer: "\u{1F4CA}",
|
|
274
|
-
// 📊
|
|
275
|
-
mobile_developer: "\u{1F4F1}",
|
|
276
|
-
// 📱
|
|
277
|
-
tech_lead: "\u{1F451}",
|
|
278
|
-
// 👑
|
|
279
|
-
manager: "\u{1F454}",
|
|
280
|
-
// 👔
|
|
281
|
-
support_agent: "\u{1F4AC}",
|
|
282
|
-
// 💬
|
|
283
|
-
planner: "\u{1F4A1}",
|
|
284
|
-
// 💡 (planning_agent)
|
|
285
|
-
coordinator: "\u{1F3AF}",
|
|
286
|
-
// 🎯
|
|
287
|
-
critic: "\u{1F50D}",
|
|
288
|
-
// 🔍
|
|
289
|
-
reviewer: "\u{1F50D}"
|
|
290
|
-
// 🔍
|
|
291
|
-
};
|
|
292
|
-
function getPersonaEmoji(persona) {
|
|
293
|
-
return PERSONA_EMOJIS[persona] || "\u{1F916}";
|
|
294
|
-
}
|
|
295
|
-
function wmLog(persona, message) {
|
|
296
|
-
const emoji = getPersonaEmoji(persona);
|
|
297
|
-
console.log(chalk2.cyan(`[${emoji} ${persona} \u{1F3E0}] `) + chalk2.white(message));
|
|
298
|
-
info(`[${persona}] ${message}`);
|
|
96
|
+
return task;
|
|
299
97
|
}
|
|
300
|
-
function wmCoordinatorLog(message) {
|
|
301
|
-
console.log(chalk2.cyan("[coordinator] ") + chalk2.white(message));
|
|
302
|
-
info(`[coordinator] ${message}`);
|
|
303
|
-
}
|
|
304
|
-
function printError(message) {
|
|
305
|
-
console.log(chalk2.red(`
|
|
306
|
-
\u2717 ${message}
|
|
307
|
-
`));
|
|
308
|
-
}
|
|
309
|
-
var sessionStartTime = Date.now();
|
|
310
|
-
|
|
311
|
-
// src/orchestrator.js
|
|
312
98
|
var LEARNING_INSTRUCTIONS = `
|
|
313
99
|
|
|
314
100
|
## Reporting Learnings
|
|
@@ -368,21 +154,77 @@ function buildReasoningOptions(provider, modelName) {
|
|
|
368
154
|
}
|
|
369
155
|
}
|
|
370
156
|
function isTransientError(error) {
|
|
371
|
-
if (!error || typeof error !== "object")
|
|
372
|
-
return false;
|
|
157
|
+
if (!error || typeof error !== "object") return false;
|
|
373
158
|
const msg = error instanceof Error ? error.message : String(error);
|
|
374
159
|
if (/status code (502|503|504)|socket hang up|ECONNRESET|ETIMEDOUT|network error|ECONNREFUSED/i.test(msg)) {
|
|
375
160
|
return true;
|
|
376
161
|
}
|
|
377
162
|
return false;
|
|
378
163
|
}
|
|
379
|
-
|
|
164
|
+
var READ_TOOLS = /* @__PURE__ */ new Set(["read_file", "glob", "grep", "ls", "sub_agent"]);
|
|
165
|
+
var DANGEROUS_PATTERNS = [
|
|
166
|
+
{ pattern: /rm\s+(-[a-z]*f|-[a-z]*r|--force|--recursive)/i, label: "recursive/forced delete" },
|
|
167
|
+
{ pattern: /git\s+reset\s+--hard/i, label: "hard reset" },
|
|
168
|
+
{ pattern: /git\s+push\s+.*--force/i, label: "force push" },
|
|
169
|
+
{ pattern: /git\s+clean\s+-[a-z]*f/i, label: "git clean" },
|
|
170
|
+
{ pattern: /drop\s+table/i, label: "drop table" },
|
|
171
|
+
{ pattern: /truncate\s+/i, label: "truncate" },
|
|
172
|
+
{ pattern: /DELETE\s+FROM\s+\w+\s*;/i, label: "DELETE without WHERE" },
|
|
173
|
+
{ pattern: /chmod\s+777/i, label: "chmod 777" },
|
|
174
|
+
{ pattern: />(\/dev\/sda|\/dev\/disk)/i, label: "write to disk device" }
|
|
175
|
+
];
|
|
176
|
+
function isDangerous(command) {
|
|
177
|
+
for (const { pattern, label } of DANGEROUS_PATTERNS) {
|
|
178
|
+
if (pattern.test(command)) return label;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
async function checkToolPermission(toolName, toolInput, trustAll, sessionAllow, output) {
|
|
183
|
+
if (toolName === "bash") {
|
|
184
|
+
const cmd = String(toolInput.command || "");
|
|
185
|
+
const danger = isDangerous(cmd);
|
|
186
|
+
if (danger) {
|
|
187
|
+
if (trustAll) return true;
|
|
188
|
+
output.error(`DANGEROUS: ${danger}`);
|
|
189
|
+
output.error(`Command: ${cmd}`);
|
|
190
|
+
const confirmed2 = await output.confirm("This is a dangerous operation. Are you sure?");
|
|
191
|
+
return confirmed2;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (trustAll) return true;
|
|
195
|
+
if (READ_TOOLS.has(toolName)) return true;
|
|
196
|
+
if (sessionAllow.has(toolName)) return true;
|
|
197
|
+
const display = formatToolCallDisplay(toolName, toolInput);
|
|
198
|
+
output.log("system", `Tool: ${toolName} -- ${display}`);
|
|
199
|
+
const confirmed = await output.confirm(`Allow ${toolName}?`);
|
|
200
|
+
if (confirmed) {
|
|
201
|
+
sessionAllow.add(toolName);
|
|
202
|
+
}
|
|
203
|
+
return confirmed;
|
|
204
|
+
}
|
|
205
|
+
function formatToolCallDisplay(toolName, toolInput) {
|
|
206
|
+
let msg = `Tool: ${toolName}`;
|
|
207
|
+
if (toolInput) {
|
|
208
|
+
if (toolInput.file_path) msg += ` -> ${toolInput.file_path}`;
|
|
209
|
+
else if (toolInput.path) msg += ` -> ${toolInput.path}`;
|
|
210
|
+
else if (toolInput.command) msg += ` -> ${String(toolInput.command).substring(0, 500)}`;
|
|
211
|
+
else if (toolInput.pattern) msg += ` -> pattern: ${toolInput.pattern}`;
|
|
212
|
+
else {
|
|
213
|
+
const keys = Object.keys(toolInput).slice(0, 3);
|
|
214
|
+
if (keys.length > 0) {
|
|
215
|
+
msg += ` -> ${keys.map((k) => `${k}: ${String(toolInput[k]).substring(0, 200)}`).join(", ")}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return msg;
|
|
220
|
+
}
|
|
221
|
+
async function classifyComplexity(config, userInput, output) {
|
|
222
|
+
const resolvedInput = resolveTaskInput(userInput, process.cwd());
|
|
380
223
|
const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config);
|
|
381
224
|
if (apiKey) {
|
|
382
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
225
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
383
226
|
const envVar = envMap[provider];
|
|
384
|
-
if (envVar && !process.env[envVar])
|
|
385
|
-
process.env[envVar] = apiKey;
|
|
227
|
+
if (envVar && !process.env[envVar]) process.env[envVar] = apiKey;
|
|
386
228
|
}
|
|
387
229
|
const model = createModel(provider, modelName, host, contextLength);
|
|
388
230
|
try {
|
|
@@ -395,7 +237,7 @@ async function classifyComplexity(config, userInput) {
|
|
|
395
237
|
prompt: `Analyze this coding task. If it involves multiple distinct concerns that would benefit from different specialist personas (e.g., database + backend + frontend + devops), classify as "multi". If it's a focused task that one developer could handle, classify as "single". Just classify \u2014 do not break down into stories.
|
|
396
238
|
|
|
397
239
|
Task:
|
|
398
|
-
${
|
|
240
|
+
${resolvedInput}`
|
|
399
241
|
});
|
|
400
242
|
return {
|
|
401
243
|
isMulti: result.object.complexity === "multi",
|
|
@@ -407,7 +249,7 @@ ${userInput}`
|
|
|
407
249
|
model,
|
|
408
250
|
prompt: `Is this task "single" (one developer) or "multi" (needs multiple specialists)? Respond with just "single" or "multi" and a brief reason.
|
|
409
251
|
|
|
410
|
-
Task: ${
|
|
252
|
+
Task: ${resolvedInput}`
|
|
411
253
|
});
|
|
412
254
|
const isMulti = /\bmulti\b/i.test(textResult.text);
|
|
413
255
|
return { isMulti, reason: textResult.text.slice(0, 200) };
|
|
@@ -422,42 +264,37 @@ function topologicalSort(stories) {
|
|
|
422
264
|
const result = [];
|
|
423
265
|
const visiting = /* @__PURE__ */ new Set();
|
|
424
266
|
function visit(id) {
|
|
425
|
-
if (visited.has(id))
|
|
426
|
-
return;
|
|
267
|
+
if (visited.has(id)) return;
|
|
427
268
|
if (visiting.has(id)) {
|
|
428
|
-
console.log(chalk3.yellow(` \u26A0 Circular dependency at ${id}, using input order`));
|
|
429
269
|
return;
|
|
430
270
|
}
|
|
431
271
|
visiting.add(id);
|
|
432
272
|
const story = idMap.get(id);
|
|
433
273
|
if (story?.dependsOn) {
|
|
434
274
|
for (const dep of story.dependsOn) {
|
|
435
|
-
if (idMap.has(dep))
|
|
436
|
-
visit(dep);
|
|
275
|
+
if (idMap.has(dep)) visit(dep);
|
|
437
276
|
}
|
|
438
277
|
}
|
|
439
278
|
visiting.delete(id);
|
|
440
279
|
visited.add(id);
|
|
441
|
-
if (story)
|
|
442
|
-
result.push(story);
|
|
280
|
+
if (story) result.push(story);
|
|
443
281
|
}
|
|
444
282
|
for (const story of stories) {
|
|
445
283
|
visit(story.id);
|
|
446
284
|
}
|
|
447
285
|
return result;
|
|
448
286
|
}
|
|
449
|
-
async function planStories(config, userTask, workingDir, sandboxed
|
|
287
|
+
async function planStories(config, userTask, workingDir, sandboxed, output) {
|
|
450
288
|
const planner = loadPersona("planner");
|
|
451
289
|
const { provider: pProvider, model: pModel, host: pHost, contextLength: pCtx } = getProviderForPersona(config, "planner");
|
|
452
290
|
if (pProvider) {
|
|
453
291
|
const pApiKey = config.providers[pProvider]?.apiKey;
|
|
454
292
|
if (pApiKey) {
|
|
455
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
293
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
456
294
|
const envVar = envMap[pProvider];
|
|
457
295
|
if (envVar && !process.env[envVar]) {
|
|
458
296
|
const key = pApiKey.startsWith("{env:") ? process.env[pApiKey.slice(5, -1)] : pApiKey;
|
|
459
|
-
if (key)
|
|
460
|
-
process.env[envVar] = key;
|
|
297
|
+
if (key) process.env[envVar] = key;
|
|
461
298
|
}
|
|
462
299
|
}
|
|
463
300
|
}
|
|
@@ -471,7 +308,7 @@ async function planStories(config, userTask, workingDir, sandboxed = true) {
|
|
|
471
308
|
readOnlyTools[toolName] = {
|
|
472
309
|
...toolDef,
|
|
473
310
|
execute: async (input) => {
|
|
474
|
-
|
|
311
|
+
output.toolCall("planner", toolName, input);
|
|
475
312
|
const result = await toolDef.execute(input);
|
|
476
313
|
return result;
|
|
477
314
|
}
|
|
@@ -479,16 +316,39 @@ async function planStories(config, userTask, workingDir, sandboxed = true) {
|
|
|
479
316
|
}
|
|
480
317
|
}
|
|
481
318
|
}
|
|
319
|
+
const fileRefPattern = /(?:^|\s)([\w./-]+\.(?:md|txt|yaml|yml|json|toml|ts|js|py|go|rs|spec|requirements|prd|plan))\b/gi;
|
|
320
|
+
const referencedFiles = [...new Set([...userTask.matchAll(fileRefPattern)].map((m) => m[1]))];
|
|
321
|
+
let inlinedFileContext = "";
|
|
322
|
+
if (referencedFiles.length > 0) {
|
|
323
|
+
const fs3 = await import("fs");
|
|
324
|
+
const path3 = await import("path");
|
|
325
|
+
for (const ref of referencedFiles) {
|
|
326
|
+
const fullPath = path3.default.resolve(workingDir, ref);
|
|
327
|
+
try {
|
|
328
|
+
const content = fs3.default.readFileSync(fullPath, "utf-8");
|
|
329
|
+
inlinedFileContext += `
|
|
330
|
+
### File: ${ref}
|
|
331
|
+
\`\`\`
|
|
332
|
+
${content}
|
|
333
|
+
\`\`\`
|
|
334
|
+
`;
|
|
335
|
+
output.log("planner", `Read referenced file: ${ref}`);
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
482
340
|
const plannerPrompt = `You are an expert implementation planner. Analyze this task and create a high-quality implementation plan.
|
|
483
341
|
|
|
484
342
|
## Task
|
|
485
343
|
${userTask}
|
|
486
|
-
|
|
344
|
+
${inlinedFileContext ? `
|
|
345
|
+
## Referenced Files
|
|
346
|
+
${inlinedFileContext}` : ""}
|
|
487
347
|
## Working directory
|
|
488
348
|
${workingDir}
|
|
489
349
|
|
|
490
350
|
## Instructions
|
|
491
|
-
1. Use your tools to explore the working directory and understand what exists. Stay within the working directory.
|
|
351
|
+
1. Use your tools to explore the working directory and understand what exists. Stay within the working directory.${referencedFiles.length > 0 ? "\n The referenced files above have been inlined for you. Read any additional files you need for context." : ""}
|
|
492
352
|
2. Design a plan that breaks the task into focused stories, each assigned to a specialist persona.
|
|
493
353
|
3. Each story should be a meaningful unit of work \u2014 not too granular, not too broad.
|
|
494
354
|
4. Quality criteria:
|
|
@@ -514,8 +374,8 @@ Return ONLY a JSON code block with this structure:
|
|
|
514
374
|
\`\`\`
|
|
515
375
|
|
|
516
376
|
Available personas: backend_developer, frontend_developer, devops_engineer, qa_engineer, security_engineer, data_ml_engineer, mobile_developer, tech_writer, tech_lead`;
|
|
517
|
-
|
|
518
|
-
|
|
377
|
+
output.log("planner", `Starting planning agent using ${pModel}`);
|
|
378
|
+
output.log("planner", "Reading repository structure...");
|
|
519
379
|
let planText = "";
|
|
520
380
|
const planStream = streamText({
|
|
521
381
|
model: plannerModel,
|
|
@@ -529,9 +389,8 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
|
|
|
529
389
|
if (text) {
|
|
530
390
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
531
391
|
for (const line of lines) {
|
|
532
|
-
if (line.trim().startsWith("{") || line.trim().startsWith("}") || line.trim().startsWith('"') || line.trim().startsWith("[") || line.trim().startsWith("]") || line.includes("```"))
|
|
533
|
-
|
|
534
|
-
wmLog("planner", line);
|
|
392
|
+
if (line.trim().startsWith("{") || line.trim().startsWith("}") || line.trim().startsWith('"') || line.trim().startsWith("[") || line.trim().startsWith("]") || line.includes("```")) continue;
|
|
393
|
+
output.log("planner", line);
|
|
535
394
|
}
|
|
536
395
|
}
|
|
537
396
|
}
|
|
@@ -542,9 +401,9 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
|
|
|
542
401
|
if (finalText && finalText.length > planText.length) {
|
|
543
402
|
planText = finalText;
|
|
544
403
|
}
|
|
545
|
-
let stories = parseStoriesFromText(planText);
|
|
404
|
+
let stories = parseStoriesFromText(planText, output);
|
|
546
405
|
if (stories.length === 0) {
|
|
547
|
-
|
|
406
|
+
output.log("system", "Planner didn't produce structured stories, falling back to single story");
|
|
548
407
|
stories = [{
|
|
549
408
|
id: "implement",
|
|
550
409
|
title: userTask.slice(0, 60),
|
|
@@ -554,12 +413,11 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
|
|
|
554
413
|
}
|
|
555
414
|
return stories;
|
|
556
415
|
}
|
|
557
|
-
function parseStoriesFromText(text) {
|
|
416
|
+
function parseStoriesFromText(text, output) {
|
|
558
417
|
const codeBlocks = [...text.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
|
|
559
418
|
for (const match of codeBlocks) {
|
|
560
419
|
const stories2 = tryParseStories(match[1].trim());
|
|
561
|
-
if (stories2)
|
|
562
|
-
return stories2;
|
|
420
|
+
if (stories2) return stories2;
|
|
563
421
|
}
|
|
564
422
|
const storiesIdx = text.indexOf('"stories"');
|
|
565
423
|
if (storiesIdx !== -1) {
|
|
@@ -568,8 +426,7 @@ function parseStoriesFromText(text) {
|
|
|
568
426
|
const json = extractBalancedJSON(text, braceStart);
|
|
569
427
|
if (json) {
|
|
570
428
|
const stories2 = tryParseStories(json);
|
|
571
|
-
if (stories2)
|
|
572
|
-
return stories2;
|
|
429
|
+
if (stories2) return stories2;
|
|
573
430
|
}
|
|
574
431
|
}
|
|
575
432
|
}
|
|
@@ -578,27 +435,23 @@ function parseStoriesFromText(text) {
|
|
|
578
435
|
const json = extractBalancedJSON(text, arrayStart);
|
|
579
436
|
if (json) {
|
|
580
437
|
const stories2 = tryParseStories(json);
|
|
581
|
-
if (stories2)
|
|
582
|
-
return stories2;
|
|
438
|
+
if (stories2) return stories2;
|
|
583
439
|
}
|
|
584
440
|
}
|
|
585
441
|
const stories = tryParseStories(text.trim());
|
|
586
|
-
if (stories)
|
|
587
|
-
return stories;
|
|
442
|
+
if (stories) return stories;
|
|
588
443
|
const preview = text.slice(0, 500);
|
|
589
|
-
|
|
444
|
+
output.log("system", `(planner output preview: ${preview}${text.length > 500 ? "..." : ""})`);
|
|
590
445
|
return [];
|
|
591
446
|
}
|
|
592
447
|
function tryParseStories(text) {
|
|
593
448
|
try {
|
|
594
449
|
const parsed = JSON.parse(text);
|
|
595
450
|
if (Array.isArray(parsed)) {
|
|
596
|
-
if (parsed.length > 0 && parsed[0].persona)
|
|
597
|
-
return parsed;
|
|
451
|
+
if (parsed.length > 0 && parsed[0].persona) return parsed;
|
|
598
452
|
}
|
|
599
453
|
if (parsed && Array.isArray(parsed.stories)) {
|
|
600
|
-
if (parsed.stories.length > 0)
|
|
601
|
-
return parsed.stories;
|
|
454
|
+
if (parsed.stories.length > 0) return parsed.stories;
|
|
602
455
|
}
|
|
603
456
|
} catch {
|
|
604
457
|
}
|
|
@@ -607,8 +460,7 @@ function tryParseStories(text) {
|
|
|
607
460
|
function extractBalancedJSON(text, start) {
|
|
608
461
|
const open = text[start];
|
|
609
462
|
const close = open === "{" ? "}" : open === "[" ? "]" : null;
|
|
610
|
-
if (!close)
|
|
611
|
-
return null;
|
|
463
|
+
if (!close) return null;
|
|
612
464
|
let depth = 0;
|
|
613
465
|
let inString = false;
|
|
614
466
|
let escape = false;
|
|
@@ -626,10 +478,8 @@ function extractBalancedJSON(text, start) {
|
|
|
626
478
|
inString = !inString;
|
|
627
479
|
continue;
|
|
628
480
|
}
|
|
629
|
-
if (inString)
|
|
630
|
-
|
|
631
|
-
if (ch === open)
|
|
632
|
-
depth++;
|
|
481
|
+
if (inString) continue;
|
|
482
|
+
if (ch === open) depth++;
|
|
633
483
|
if (ch === close) {
|
|
634
484
|
depth--;
|
|
635
485
|
if (depth === 0) {
|
|
@@ -640,36 +490,31 @@ function extractBalancedJSON(text, start) {
|
|
|
640
490
|
return null;
|
|
641
491
|
}
|
|
642
492
|
function extractScore(text) {
|
|
643
|
-
const
|
|
644
|
-
if (
|
|
645
|
-
return parseInt(
|
|
493
|
+
const markerMatches = [...text.matchAll(/::review_score::(\d+)/g)];
|
|
494
|
+
if (markerMatches.length > 0) {
|
|
495
|
+
return parseInt(markerMatches[markerMatches.length - 1][1], 10);
|
|
496
|
+
}
|
|
646
497
|
const scorePatterns = [
|
|
647
|
-
/\bscore[:\s]+(\d+)\s*\/\s*100/
|
|
648
|
-
/\
|
|
649
|
-
/\
|
|
650
|
-
/\brating[:\s]+(\d+)/i
|
|
498
|
+
/\bscore[:\s]+(\d+)\s*\/\s*100/gi,
|
|
499
|
+
/\bscore[:\s]+(\d+)/gi,
|
|
500
|
+
/\brating[:\s]+(\d+)/gi
|
|
651
501
|
];
|
|
652
502
|
for (const pattern of scorePatterns) {
|
|
653
|
-
const
|
|
654
|
-
if (
|
|
655
|
-
const n = parseInt(
|
|
656
|
-
if (n >= 0 && n <= 100)
|
|
657
|
-
return n;
|
|
503
|
+
const matches = [...text.matchAll(pattern)];
|
|
504
|
+
if (matches.length > 0) {
|
|
505
|
+
const n = parseInt(matches[matches.length - 1][1], 10);
|
|
506
|
+
if (n >= 0 && n <= 100) return n;
|
|
658
507
|
}
|
|
659
508
|
}
|
|
660
|
-
if (/\bapprove/i.test(text))
|
|
661
|
-
|
|
662
|
-
if (/\brevis/i.test(text))
|
|
663
|
-
return 60;
|
|
509
|
+
if (/\bapprove/i.test(text)) return 85;
|
|
510
|
+
if (/\brevis/i.test(text)) return 60;
|
|
664
511
|
return 75;
|
|
665
512
|
}
|
|
666
513
|
function parseAffectedStories(text) {
|
|
667
514
|
const storiesMatch = text.match(/AFFECTED_STORIES:\s*\[([^\]]+)\]/i);
|
|
668
|
-
if (!storiesMatch)
|
|
669
|
-
return null;
|
|
515
|
+
if (!storiesMatch) return null;
|
|
670
516
|
const stories = storiesMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
|
671
|
-
if (stories.length === 0)
|
|
672
|
-
return null;
|
|
517
|
+
if (stories.length === 0) return null;
|
|
673
518
|
let reasons = {};
|
|
674
519
|
const reasonsMatch = text.match(/AFFECTED_REASONS:\s*(\{[\s\S]*?\})/i);
|
|
675
520
|
if (reasonsMatch) {
|
|
@@ -686,7 +531,8 @@ function parseAffectedStories(text) {
|
|
|
686
531
|
}
|
|
687
532
|
return { stories, reasons };
|
|
688
533
|
}
|
|
689
|
-
async function runOrchestration(config, userTask, trustAll, sandboxed
|
|
534
|
+
async function runOrchestration(config, userTask, trustAll, sandboxed, output) {
|
|
535
|
+
userTask = resolveTaskInput(userTask, process.cwd());
|
|
690
536
|
const costTracker = new CostTracker();
|
|
691
537
|
const context = {
|
|
692
538
|
filesCreated: [],
|
|
@@ -694,18 +540,15 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
|
|
|
694
540
|
decisions: [],
|
|
695
541
|
learnings: []
|
|
696
542
|
};
|
|
697
|
-
const
|
|
698
|
-
if (agentRl)
|
|
699
|
-
permissions.setReadline(agentRl);
|
|
543
|
+
const sessionAllow = /* @__PURE__ */ new Set();
|
|
700
544
|
const workingDir = process.cwd();
|
|
701
|
-
const plannerStories = await planStories(config, userTask, workingDir, sandboxed);
|
|
702
|
-
|
|
545
|
+
const plannerStories = await planStories(config, userTask, workingDir, sandboxed, output);
|
|
546
|
+
output.log("planner", `Plan generated: ${plannerStories.length} stories`);
|
|
703
547
|
plannerStories.forEach((s, i) => {
|
|
704
|
-
|
|
705
|
-
wmLog("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
|
|
548
|
+
output.log("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
|
|
706
549
|
});
|
|
707
|
-
|
|
708
|
-
|
|
550
|
+
output.log("planner", `Plan validated: ${plannerStories.length} stories. Task queued for execution.`);
|
|
551
|
+
output.log("system", "");
|
|
709
552
|
if (config.review?.useCritic) {
|
|
710
553
|
const critic = loadPersona("critic");
|
|
711
554
|
if (critic) {
|
|
@@ -719,14 +562,14 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
|
|
|
719
562
|
criticReadOnly[name] = {
|
|
720
563
|
...toolDef,
|
|
721
564
|
execute: async (input) => {
|
|
722
|
-
|
|
565
|
+
output.log("critic", formatToolCallDisplay(name, input));
|
|
723
566
|
const result = await toolDef.execute(input);
|
|
724
567
|
return result;
|
|
725
568
|
}
|
|
726
569
|
};
|
|
727
570
|
}
|
|
728
571
|
}
|
|
729
|
-
|
|
572
|
+
output.status("Critic reviewing plan...");
|
|
730
573
|
const criticStream = streamText({
|
|
731
574
|
model: criticModel,
|
|
732
575
|
system: critic.systemPrompt,
|
|
@@ -742,52 +585,47 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
|
|
|
742
585
|
for await (const _chunk of criticStream.textStream) {
|
|
743
586
|
}
|
|
744
587
|
const criticText = await criticStream.text;
|
|
745
|
-
|
|
588
|
+
output.statusDone();
|
|
746
589
|
const score = extractScore(criticText);
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
590
|
+
output.log("critic", `::review_score::${score}`);
|
|
591
|
+
output.log("critic", score >= 80 ? "Plan approved" : "Plan needs revision");
|
|
592
|
+
output.log("system", "");
|
|
750
593
|
}
|
|
751
594
|
}
|
|
752
595
|
const sorted = topologicalSort(plannerStories);
|
|
753
596
|
if (!trustAll) {
|
|
754
|
-
let
|
|
597
|
+
let proceed = false;
|
|
755
598
|
try {
|
|
756
|
-
|
|
599
|
+
proceed = await output.confirm("Execute this plan?");
|
|
757
600
|
} catch {
|
|
758
601
|
}
|
|
759
|
-
if (
|
|
760
|
-
|
|
602
|
+
if (!proceed) {
|
|
603
|
+
output.log("system", "Plan cancelled.");
|
|
761
604
|
return;
|
|
762
605
|
}
|
|
763
|
-
|
|
606
|
+
output.log("system", "");
|
|
764
607
|
}
|
|
765
608
|
for (let i = 0; i < sorted.length; i++) {
|
|
766
609
|
const story = sorted[i];
|
|
767
610
|
const persona = loadPersona(story.persona);
|
|
768
611
|
if (!persona) {
|
|
769
|
-
|
|
612
|
+
output.error(`Unknown persona: ${story.persona}`);
|
|
770
613
|
continue;
|
|
771
614
|
}
|
|
772
|
-
const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(
|
|
615
|
+
const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(
|
|
616
|
+
config,
|
|
617
|
+
persona.provider || story.persona
|
|
618
|
+
);
|
|
773
619
|
if (apiKey) {
|
|
774
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
620
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
775
621
|
const envVar = envMap[provider];
|
|
776
|
-
if (envVar && !process.env[envVar])
|
|
777
|
-
process.env[envVar] = apiKey;
|
|
622
|
+
if (envVar && !process.env[envVar]) process.env[envVar] = apiKey;
|
|
778
623
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
`)
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
wmLog(story.persona, `Executing story with AIClient (model: ${modelName})...`);
|
|
785
|
-
const spinner = ora({
|
|
786
|
-
stream: process.stdout,
|
|
787
|
-
text: "",
|
|
788
|
-
prefixText: "",
|
|
789
|
-
spinner: "dots"
|
|
790
|
-
}).start();
|
|
624
|
+
output.log("system", `--- Story ${i + 1}/${sorted.length} ---`);
|
|
625
|
+
output.coordinatorLog(`Task claimed by orchestrator`);
|
|
626
|
+
output.log(story.persona, `Starting ${story.title}`);
|
|
627
|
+
output.log(story.persona, `Executing story with AIClient (model: ${modelName})...`);
|
|
628
|
+
output.status("");
|
|
791
629
|
const model = createModel(provider, modelName, host, contextLength);
|
|
792
630
|
const allTools = createToolDefinitions(workingDir, model, sandboxed);
|
|
793
631
|
const personaTools = {};
|
|
@@ -798,18 +636,17 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
|
|
|
798
636
|
personaTools[toolName] = {
|
|
799
637
|
...toolDef,
|
|
800
638
|
execute: async (input) => {
|
|
801
|
-
const allowed = await
|
|
802
|
-
if (!allowed)
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
lastToolCall = callKey;
|
|
639
|
+
const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
|
|
640
|
+
if (!allowed) return "Tool execution denied by user.";
|
|
641
|
+
const sig = `${toolName}:${JSON.stringify(input)}`;
|
|
642
|
+
const isDuplicate = sig === lastToolCall;
|
|
643
|
+
lastToolCall = sig;
|
|
807
644
|
if (!isDuplicate) {
|
|
808
|
-
|
|
809
|
-
|
|
645
|
+
output.statusDone();
|
|
646
|
+
output.toolCall(story.persona, toolName, input);
|
|
810
647
|
}
|
|
811
648
|
const result = await toolDef.execute(input);
|
|
812
|
-
|
|
649
|
+
output.status("");
|
|
813
650
|
return result;
|
|
814
651
|
}
|
|
815
652
|
};
|
|
@@ -872,12 +709,11 @@ ${revisionFeedback}` : ""}`;
|
|
|
872
709
|
...buildOllamaOptions(provider, contextLength),
|
|
873
710
|
onStepFinish({ text: text2 }) {
|
|
874
711
|
if (text2) {
|
|
875
|
-
|
|
712
|
+
output.statusDone();
|
|
876
713
|
const lines = text2.split("\n").filter((l) => l.trim());
|
|
877
714
|
for (const line of lines) {
|
|
878
|
-
if (line.includes("::decision::") || line.includes("::learning::") || line.includes("::file_created::") || line.includes("::file_modified::"))
|
|
879
|
-
|
|
880
|
-
wmLog(story.persona, line);
|
|
715
|
+
if (line.includes("::decision::") || line.includes("::learning::") || line.includes("::file_created::") || line.includes("::file_modified::")) continue;
|
|
716
|
+
output.log(story.persona, line);
|
|
881
717
|
}
|
|
882
718
|
}
|
|
883
719
|
}
|
|
@@ -887,13 +723,13 @@ ${revisionFeedback}` : ""}`;
|
|
|
887
723
|
const text = await stream.text;
|
|
888
724
|
allText = text;
|
|
889
725
|
const usage = await stream.totalUsage;
|
|
890
|
-
|
|
726
|
+
output.statusDone();
|
|
891
727
|
const decisionMatches = text.match(/::decision::(.*?)(?=::\w+::|$)/gs);
|
|
892
728
|
if (decisionMatches) {
|
|
893
729
|
for (const m of decisionMatches) {
|
|
894
730
|
const decision = m.replace("::decision::", "").trim();
|
|
895
731
|
context.decisions.push(decision);
|
|
896
|
-
|
|
732
|
+
output.log(story.persona, decision);
|
|
897
733
|
}
|
|
898
734
|
}
|
|
899
735
|
const learningMatches = text.match(/::learning::(.*?)(?=::\w+::|$)/gs);
|
|
@@ -917,17 +753,17 @@ ${revisionFeedback}` : ""}`;
|
|
|
917
753
|
const inTokens = usage?.inputTokens || 0;
|
|
918
754
|
const outTokens = usage?.outputTokens || 0;
|
|
919
755
|
costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
|
|
920
|
-
|
|
921
|
-
|
|
756
|
+
output.log(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
|
|
757
|
+
output.log("system", "");
|
|
922
758
|
break;
|
|
923
759
|
} catch (err) {
|
|
924
|
-
|
|
760
|
+
output.statusDone();
|
|
925
761
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
926
762
|
if (isTransientError(err) && revision < 2) {
|
|
927
|
-
|
|
763
|
+
output.log(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
|
|
928
764
|
continue;
|
|
929
765
|
}
|
|
930
|
-
|
|
766
|
+
output.error(`Story ${i + 1} failed: ${errMsg}`);
|
|
931
767
|
break;
|
|
932
768
|
}
|
|
933
769
|
}
|
|
@@ -935,16 +771,18 @@ ${revisionFeedback}` : ""}`;
|
|
|
935
771
|
const maxRevisions = config.review?.maxRevisions ?? 2;
|
|
936
772
|
const autoRevise = config.review?.autoRevise ?? false;
|
|
937
773
|
const approvalThreshold = config.review?.approvalThreshold ?? 80;
|
|
938
|
-
const reviewer = loadPersona("
|
|
774
|
+
const reviewer = loadPersona("tech_lead");
|
|
939
775
|
if (reviewer) {
|
|
940
|
-
const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(
|
|
776
|
+
const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(
|
|
777
|
+
config,
|
|
778
|
+
"tech_lead"
|
|
779
|
+
);
|
|
941
780
|
const revApiKey = config.providers[revProvider]?.apiKey;
|
|
942
781
|
if (revApiKey) {
|
|
943
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
782
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
944
783
|
const envVar = envMap[revProvider];
|
|
945
784
|
const key = revApiKey.startsWith("{env:") ? process.env[revApiKey.slice(5, -1)] : revApiKey;
|
|
946
|
-
if (envVar && key && !process.env[envVar])
|
|
947
|
-
process.env[envVar] = key;
|
|
785
|
+
if (envVar && key && !process.env[envVar]) process.env[envVar] = key;
|
|
948
786
|
}
|
|
949
787
|
const reviewModel = createModel(revProvider, revModel, revHost, revCtx);
|
|
950
788
|
const reviewTools = createToolDefinitions(workingDir, reviewModel, sandboxed);
|
|
@@ -955,7 +793,7 @@ ${revisionFeedback}` : ""}`;
|
|
|
955
793
|
reviewerTools[toolName] = {
|
|
956
794
|
...toolDef,
|
|
957
795
|
execute: async (input) => {
|
|
958
|
-
|
|
796
|
+
output.log("tech_lead", formatToolCallDisplay(toolName, input));
|
|
959
797
|
const result = await toolDef.execute(input);
|
|
960
798
|
return result;
|
|
961
799
|
}
|
|
@@ -965,13 +803,9 @@ ${revisionFeedback}` : ""}`;
|
|
|
965
803
|
let previousReviewFeedback = "";
|
|
966
804
|
for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
|
|
967
805
|
const isRevision = reviewRound > 0;
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
stream: process.stdout,
|
|
972
|
-
text: chalk3.white(isRevision ? "Reviewer \u2014 Re-checking after revisions" : "Reviewer \u2014 Checking code quality"),
|
|
973
|
-
prefixText: " "
|
|
974
|
-
}).start();
|
|
806
|
+
output.coordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
|
|
807
|
+
output.log("tech_lead", "Starting agent execution");
|
|
808
|
+
output.status(isRevision ? "Reviewer -- Re-checking after revisions" : "Reviewer -- Checking code quality");
|
|
975
809
|
try {
|
|
976
810
|
const previousFeedbackSection = isRevision && previousReviewFeedback ? `## Previous Review Feedback (Review ${reviewRound}/${maxRevisions})
|
|
977
811
|
This is a revision attempt. The previous code was reviewed and these issues were identified:
|
|
@@ -1034,7 +868,7 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
|
|
|
1034
868
|
- Only include stories that have ACTUAL implementation issues
|
|
1035
869
|
- If ALL stories need revision, you may omit AFFECTED_STORIES (all will re-run)
|
|
1036
870
|
- Be specific in AFFECTED_REASONS so developers know exactly what to fix`;
|
|
1037
|
-
let
|
|
871
|
+
let reviewerOutput = "";
|
|
1038
872
|
const reviewStream = streamText({
|
|
1039
873
|
model: reviewModel,
|
|
1040
874
|
system: reviewer.systemPrompt,
|
|
@@ -1045,95 +879,94 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
|
|
|
1045
879
|
...buildOllamaOptions(revProvider, revCtx),
|
|
1046
880
|
onStepFinish({ text }) {
|
|
1047
881
|
if (text) {
|
|
1048
|
-
|
|
882
|
+
reviewerOutput += text + "\n";
|
|
883
|
+
output.statusDone();
|
|
1049
884
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
1050
885
|
for (const line of lines) {
|
|
1051
|
-
if (line.includes("::review_score::") || line.includes("::review_verdict::"))
|
|
1052
|
-
|
|
1053
|
-
wmLog("tech_lead", line);
|
|
886
|
+
if (line.includes("::review_score::") || line.includes("::review_verdict::")) continue;
|
|
887
|
+
output.log("tech_lead", line);
|
|
1054
888
|
}
|
|
1055
889
|
}
|
|
1056
890
|
}
|
|
1057
891
|
});
|
|
1058
892
|
for await (const _chunk of reviewStream.textStream) {
|
|
1059
893
|
}
|
|
1060
|
-
const
|
|
1061
|
-
const reviewText = finalReviewText && finalReviewText.length > allReviewText.length ? finalReviewText : allReviewText;
|
|
894
|
+
const reviewText = reviewerOutput;
|
|
1062
895
|
const reviewUsage = await reviewStream.totalUsage;
|
|
1063
|
-
|
|
896
|
+
output.statusDone();
|
|
1064
897
|
const score = extractScore(reviewText);
|
|
1065
898
|
const approved = score >= approvalThreshold;
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
899
|
+
output.log("tech_lead", `::code_quality_score::${score}`);
|
|
900
|
+
output.log("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
|
|
901
|
+
output.coordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
|
|
1069
902
|
previousReviewFeedback = reviewText;
|
|
1070
|
-
|
|
1071
|
-
costTracker.addUsage(
|
|
1072
|
-
|
|
1073
|
-
|
|
903
|
+
output.log("system", "");
|
|
904
|
+
costTracker.addUsage(
|
|
905
|
+
`Reviewer (round ${reviewRound + 1})`,
|
|
906
|
+
revProvider,
|
|
907
|
+
revModel,
|
|
908
|
+
reviewUsage?.inputTokens || 0,
|
|
909
|
+
reviewUsage?.outputTokens || 0
|
|
910
|
+
);
|
|
911
|
+
if (approved) break;
|
|
1074
912
|
if (reviewRound >= maxRevisions) {
|
|
1075
|
-
|
|
913
|
+
output.log("system", `Max review revisions (${maxRevisions}) reached`);
|
|
1076
914
|
break;
|
|
1077
915
|
}
|
|
1078
916
|
let shouldRevise = autoRevise;
|
|
1079
917
|
if (!autoRevise) {
|
|
1080
918
|
try {
|
|
1081
|
-
|
|
1082
|
-
shouldRevise = answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
919
|
+
shouldRevise = await output.confirm(`Revise and re-review? (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)`);
|
|
1083
920
|
} catch {
|
|
1084
921
|
shouldRevise = false;
|
|
1085
922
|
}
|
|
1086
923
|
} else {
|
|
1087
|
-
|
|
924
|
+
output.log("system", `Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`);
|
|
1088
925
|
}
|
|
1089
926
|
if (!shouldRevise) {
|
|
1090
|
-
|
|
927
|
+
output.log("system", "Skipping revision, proceeding to commit.");
|
|
1091
928
|
break;
|
|
1092
929
|
}
|
|
1093
930
|
const affected = parseAffectedStories(reviewText);
|
|
1094
931
|
const affectedSet = affected ? new Set(affected.stories) : null;
|
|
1095
932
|
if (affected) {
|
|
1096
933
|
const selectiveInfo = `stories ${affected.stories.join(", ")}`;
|
|
1097
|
-
|
|
934
|
+
output.coordinatorLog(`Selective revision: ${selectiveInfo}`);
|
|
1098
935
|
if (Object.keys(affected.reasons).length > 0) {
|
|
1099
936
|
for (const [idx, reason] of Object.entries(affected.reasons)) {
|
|
1100
|
-
|
|
937
|
+
output.coordinatorLog(` Story ${idx}: ${reason}`);
|
|
1101
938
|
}
|
|
1102
939
|
}
|
|
1103
940
|
} else {
|
|
1104
|
-
|
|
941
|
+
output.coordinatorLog("Full revision (all stories)");
|
|
1105
942
|
}
|
|
1106
|
-
|
|
943
|
+
output.log("system", "--- Revision Pass ---");
|
|
1107
944
|
for (let i = 0; i < sorted.length; i++) {
|
|
1108
945
|
const story = sorted[i];
|
|
1109
946
|
if (affectedSet && !affectedSet.has(i + 1)) {
|
|
1110
|
-
|
|
947
|
+
output.coordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
|
|
1111
948
|
continue;
|
|
1112
949
|
}
|
|
1113
950
|
const storyPersona = loadPersona(story.persona);
|
|
1114
|
-
if (!storyPersona)
|
|
1115
|
-
|
|
1116
|
-
|
|
951
|
+
if (!storyPersona) continue;
|
|
952
|
+
const { provider: sProvider, model: sModel, host: sHost, contextLength: sCtx } = getProviderForPersona(
|
|
953
|
+
config,
|
|
954
|
+
storyPersona.provider || story.persona
|
|
955
|
+
);
|
|
1117
956
|
if (sProvider) {
|
|
1118
957
|
const sApiKey = config.providers[sProvider]?.apiKey;
|
|
1119
958
|
if (sApiKey) {
|
|
1120
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
959
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
1121
960
|
const envVar = envMap[sProvider];
|
|
1122
961
|
if (envVar && !process.env[envVar]) {
|
|
1123
962
|
const key = sApiKey.startsWith("{env:") ? process.env[sApiKey.slice(5, -1)] : sApiKey;
|
|
1124
|
-
if (key)
|
|
1125
|
-
process.env[envVar] = key;
|
|
963
|
+
if (key) process.env[envVar] = key;
|
|
1126
964
|
}
|
|
1127
965
|
}
|
|
1128
966
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
stream: process.stdout,
|
|
1133
|
-
text: "",
|
|
1134
|
-
prefixText: "",
|
|
1135
|
-
spinner: "dots"
|
|
1136
|
-
}).start();
|
|
967
|
+
output.coordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
|
|
968
|
+
output.log(story.persona, `Starting revision: ${story.title}`);
|
|
969
|
+
output.status("");
|
|
1137
970
|
const storyModel = createModel(sProvider, sModel, sHost, sCtx);
|
|
1138
971
|
const storyAllTools = createToolDefinitions(workingDir, storyModel, sandboxed);
|
|
1139
972
|
const storyTools = {};
|
|
@@ -1143,13 +976,12 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
|
|
|
1143
976
|
storyTools[toolName] = {
|
|
1144
977
|
...toolDef,
|
|
1145
978
|
execute: async (input) => {
|
|
1146
|
-
const allowed = await
|
|
1147
|
-
if (!allowed)
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
wmLog(story.persona, formatToolCall(toolName, input));
|
|
979
|
+
const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
|
|
980
|
+
if (!allowed) return "Tool execution denied by user.";
|
|
981
|
+
output.statusDone();
|
|
982
|
+
output.log(story.persona, formatToolCallDisplay(toolName, input));
|
|
1151
983
|
const result = await toolDef.execute(input);
|
|
1152
|
-
|
|
984
|
+
output.status("");
|
|
1153
985
|
return result;
|
|
1154
986
|
}
|
|
1155
987
|
};
|
|
@@ -1186,12 +1018,11 @@ ${story.description}`,
|
|
|
1186
1018
|
...buildOllamaOptions(sProvider, sCtx),
|
|
1187
1019
|
onStepFinish({ text }) {
|
|
1188
1020
|
if (text) {
|
|
1189
|
-
|
|
1021
|
+
output.statusDone();
|
|
1190
1022
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
1191
1023
|
for (const line of lines) {
|
|
1192
|
-
if (line.includes("::"))
|
|
1193
|
-
|
|
1194
|
-
wmLog(story.persona, line);
|
|
1024
|
+
if (line.includes("::")) continue;
|
|
1025
|
+
output.log(story.persona, line);
|
|
1195
1026
|
}
|
|
1196
1027
|
}
|
|
1197
1028
|
}
|
|
@@ -1199,80 +1030,86 @@ ${story.description}`,
|
|
|
1199
1030
|
for await (const _chunk of revStream.textStream) {
|
|
1200
1031
|
}
|
|
1201
1032
|
const revUsage = await revStream.totalUsage;
|
|
1202
|
-
|
|
1203
|
-
costTracker.addUsage(
|
|
1204
|
-
|
|
1033
|
+
output.statusDone();
|
|
1034
|
+
costTracker.addUsage(
|
|
1035
|
+
`${storyPersona.name} (revision)`,
|
|
1036
|
+
sProvider,
|
|
1037
|
+
sModel,
|
|
1038
|
+
revUsage?.inputTokens || 0,
|
|
1039
|
+
revUsage?.outputTokens || 0
|
|
1040
|
+
);
|
|
1041
|
+
output.log(story.persona, `${story.title} \u2014 revision complete!`);
|
|
1205
1042
|
} catch (err) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1043
|
+
output.statusDone();
|
|
1044
|
+
output.log("system", `Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1208
1045
|
}
|
|
1209
1046
|
}
|
|
1210
|
-
|
|
1047
|
+
output.log("system", "");
|
|
1211
1048
|
} catch (err) {
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1049
|
+
output.statusDone();
|
|
1050
|
+
output.log("system", `Review skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
1051
|
+
output.log("system", "");
|
|
1215
1052
|
break;
|
|
1216
1053
|
}
|
|
1217
1054
|
}
|
|
1218
1055
|
}
|
|
1219
1056
|
try {
|
|
1220
|
-
const { execSync
|
|
1057
|
+
const { execSync } = await import("child_process");
|
|
1221
1058
|
try {
|
|
1222
|
-
|
|
1059
|
+
execSync("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
|
|
1223
1060
|
} catch {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const
|
|
1061
|
+
output.coordinatorLog("Initializing git repository...");
|
|
1062
|
+
execSync("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
|
|
1063
|
+
const fs3 = await import("fs");
|
|
1227
1064
|
const gitignorePath = `${workingDir}/.gitignore`;
|
|
1228
|
-
if (!
|
|
1229
|
-
|
|
1065
|
+
if (!fs3.existsSync(gitignorePath)) {
|
|
1066
|
+
fs3.writeFileSync(gitignorePath, "node_modules/\ndist/\n.env\n.workermill/\n*.log\n", "utf-8");
|
|
1230
1067
|
}
|
|
1231
|
-
|
|
1068
|
+
output.coordinatorLog("Git repo initialized");
|
|
1232
1069
|
}
|
|
1233
|
-
const diff =
|
|
1234
|
-
const untracked =
|
|
1070
|
+
const diff = execSync("git diff --stat", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1071
|
+
const untracked = execSync("git ls-files --others --exclude-standard", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1235
1072
|
const hasChanges = diff || untracked;
|
|
1236
1073
|
if (hasChanges) {
|
|
1237
|
-
|
|
1074
|
+
output.log("system", "--- Changes ---");
|
|
1238
1075
|
if (diff) {
|
|
1239
|
-
|
|
1076
|
+
output.log("system", diff);
|
|
1240
1077
|
}
|
|
1241
1078
|
if (untracked) {
|
|
1242
1079
|
const untrackedFiles = untracked.split("\n");
|
|
1243
|
-
|
|
1080
|
+
output.log("system", "New files:");
|
|
1244
1081
|
for (const f of untrackedFiles) {
|
|
1245
|
-
|
|
1082
|
+
output.log("system", ` + ${f}`);
|
|
1246
1083
|
}
|
|
1247
1084
|
}
|
|
1248
|
-
|
|
1085
|
+
output.log("system", "");
|
|
1249
1086
|
if (!trustAll) {
|
|
1250
|
-
const
|
|
1251
|
-
if (
|
|
1087
|
+
const commitConfirmed = await output.confirm("Commit these changes?");
|
|
1088
|
+
if (commitConfirmed) {
|
|
1252
1089
|
const filesToStage = [...context.filesCreated, ...context.filesModified].filter(Boolean);
|
|
1253
1090
|
if (filesToStage.length > 0) {
|
|
1254
1091
|
for (const f of filesToStage) {
|
|
1255
1092
|
try {
|
|
1256
|
-
|
|
1093
|
+
execSync(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
|
|
1257
1094
|
} catch {
|
|
1258
1095
|
}
|
|
1259
1096
|
}
|
|
1260
1097
|
} else {
|
|
1261
|
-
|
|
1098
|
+
execSync("git add -u", { cwd: workingDir, stdio: "pipe" });
|
|
1262
1099
|
}
|
|
1263
1100
|
const storyTitles = sorted.map((s) => s.title).join(", ");
|
|
1264
1101
|
const msg = `feat: ${storyTitles}`.slice(0, 72);
|
|
1265
|
-
|
|
1266
|
-
|
|
1102
|
+
execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
|
|
1103
|
+
output.log("system", "Changes committed");
|
|
1267
1104
|
}
|
|
1268
1105
|
}
|
|
1269
1106
|
}
|
|
1270
1107
|
} catch (err) {
|
|
1271
1108
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1109
|
+
output.log("system", "--- Session Complete ---");
|
|
1110
|
+
output.log("system", "");
|
|
1111
|
+
output.log("system", costTracker.getSummary());
|
|
1112
|
+
output.log("system", "");
|
|
1276
1113
|
}
|
|
1277
1114
|
export {
|
|
1278
1115
|
classifyComplexity,
|