workermill 0.3.1 → 0.3.3
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}`);
|
|
299
|
-
}
|
|
300
|
-
function wmCoordinatorLog(message) {
|
|
301
|
-
console.log(chalk2.cyan("[coordinator] ") + chalk2.white(message));
|
|
302
|
-
info(`[coordinator] ${message}`);
|
|
96
|
+
return task;
|
|
303
97
|
}
|
|
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,10 @@ 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
|
-
|
|
404
|
+
const planUsage = await planStream.totalUsage;
|
|
405
|
+
let stories = parseStoriesFromText(planText, output);
|
|
546
406
|
if (stories.length === 0) {
|
|
547
|
-
|
|
407
|
+
output.log("system", "Planner didn't produce structured stories, falling back to single story");
|
|
548
408
|
stories = [{
|
|
549
409
|
id: "implement",
|
|
550
410
|
title: userTask.slice(0, 60),
|
|
@@ -552,14 +412,19 @@ Available personas: backend_developer, frontend_developer, devops_engineer, qa_e
|
|
|
552
412
|
description: userTask
|
|
553
413
|
}];
|
|
554
414
|
}
|
|
555
|
-
return
|
|
415
|
+
return {
|
|
416
|
+
stories,
|
|
417
|
+
provider: pProvider,
|
|
418
|
+
model: pModel,
|
|
419
|
+
inputTokens: planUsage?.inputTokens || 0,
|
|
420
|
+
outputTokens: planUsage?.outputTokens || 0
|
|
421
|
+
};
|
|
556
422
|
}
|
|
557
|
-
function parseStoriesFromText(text) {
|
|
423
|
+
function parseStoriesFromText(text, output) {
|
|
558
424
|
const codeBlocks = [...text.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
|
|
559
425
|
for (const match of codeBlocks) {
|
|
560
426
|
const stories2 = tryParseStories(match[1].trim());
|
|
561
|
-
if (stories2)
|
|
562
|
-
return stories2;
|
|
427
|
+
if (stories2) return stories2;
|
|
563
428
|
}
|
|
564
429
|
const storiesIdx = text.indexOf('"stories"');
|
|
565
430
|
if (storiesIdx !== -1) {
|
|
@@ -568,8 +433,7 @@ function parseStoriesFromText(text) {
|
|
|
568
433
|
const json = extractBalancedJSON(text, braceStart);
|
|
569
434
|
if (json) {
|
|
570
435
|
const stories2 = tryParseStories(json);
|
|
571
|
-
if (stories2)
|
|
572
|
-
return stories2;
|
|
436
|
+
if (stories2) return stories2;
|
|
573
437
|
}
|
|
574
438
|
}
|
|
575
439
|
}
|
|
@@ -578,27 +442,23 @@ function parseStoriesFromText(text) {
|
|
|
578
442
|
const json = extractBalancedJSON(text, arrayStart);
|
|
579
443
|
if (json) {
|
|
580
444
|
const stories2 = tryParseStories(json);
|
|
581
|
-
if (stories2)
|
|
582
|
-
return stories2;
|
|
445
|
+
if (stories2) return stories2;
|
|
583
446
|
}
|
|
584
447
|
}
|
|
585
448
|
const stories = tryParseStories(text.trim());
|
|
586
|
-
if (stories)
|
|
587
|
-
return stories;
|
|
449
|
+
if (stories) return stories;
|
|
588
450
|
const preview = text.slice(0, 500);
|
|
589
|
-
|
|
451
|
+
output.log("system", `(planner output preview: ${preview}${text.length > 500 ? "..." : ""})`);
|
|
590
452
|
return [];
|
|
591
453
|
}
|
|
592
454
|
function tryParseStories(text) {
|
|
593
455
|
try {
|
|
594
456
|
const parsed = JSON.parse(text);
|
|
595
457
|
if (Array.isArray(parsed)) {
|
|
596
|
-
if (parsed.length > 0 && parsed[0].persona)
|
|
597
|
-
return parsed;
|
|
458
|
+
if (parsed.length > 0 && parsed[0].persona) return parsed;
|
|
598
459
|
}
|
|
599
460
|
if (parsed && Array.isArray(parsed.stories)) {
|
|
600
|
-
if (parsed.stories.length > 0)
|
|
601
|
-
return parsed.stories;
|
|
461
|
+
if (parsed.stories.length > 0) return parsed.stories;
|
|
602
462
|
}
|
|
603
463
|
} catch {
|
|
604
464
|
}
|
|
@@ -607,8 +467,7 @@ function tryParseStories(text) {
|
|
|
607
467
|
function extractBalancedJSON(text, start) {
|
|
608
468
|
const open = text[start];
|
|
609
469
|
const close = open === "{" ? "}" : open === "[" ? "]" : null;
|
|
610
|
-
if (!close)
|
|
611
|
-
return null;
|
|
470
|
+
if (!close) return null;
|
|
612
471
|
let depth = 0;
|
|
613
472
|
let inString = false;
|
|
614
473
|
let escape = false;
|
|
@@ -626,10 +485,8 @@ function extractBalancedJSON(text, start) {
|
|
|
626
485
|
inString = !inString;
|
|
627
486
|
continue;
|
|
628
487
|
}
|
|
629
|
-
if (inString)
|
|
630
|
-
|
|
631
|
-
if (ch === open)
|
|
632
|
-
depth++;
|
|
488
|
+
if (inString) continue;
|
|
489
|
+
if (ch === open) depth++;
|
|
633
490
|
if (ch === close) {
|
|
634
491
|
depth--;
|
|
635
492
|
if (depth === 0) {
|
|
@@ -640,36 +497,31 @@ function extractBalancedJSON(text, start) {
|
|
|
640
497
|
return null;
|
|
641
498
|
}
|
|
642
499
|
function extractScore(text) {
|
|
643
|
-
const
|
|
644
|
-
if (
|
|
645
|
-
return parseInt(
|
|
500
|
+
const markerMatches = [...text.matchAll(/::review_score::(\d+)/g)];
|
|
501
|
+
if (markerMatches.length > 0) {
|
|
502
|
+
return parseInt(markerMatches[markerMatches.length - 1][1], 10);
|
|
503
|
+
}
|
|
646
504
|
const scorePatterns = [
|
|
647
|
-
/\bscore[:\s]+(\d+)\s*\/\s*100/
|
|
648
|
-
/\
|
|
649
|
-
/\
|
|
650
|
-
/\brating[:\s]+(\d+)/i
|
|
505
|
+
/\bscore[:\s]+(\d+)\s*\/\s*100/gi,
|
|
506
|
+
/\bscore[:\s]+(\d+)/gi,
|
|
507
|
+
/\brating[:\s]+(\d+)/gi
|
|
651
508
|
];
|
|
652
509
|
for (const pattern of scorePatterns) {
|
|
653
|
-
const
|
|
654
|
-
if (
|
|
655
|
-
const n = parseInt(
|
|
656
|
-
if (n >= 0 && n <= 100)
|
|
657
|
-
return n;
|
|
510
|
+
const matches = [...text.matchAll(pattern)];
|
|
511
|
+
if (matches.length > 0) {
|
|
512
|
+
const n = parseInt(matches[matches.length - 1][1], 10);
|
|
513
|
+
if (n >= 0 && n <= 100) return n;
|
|
658
514
|
}
|
|
659
515
|
}
|
|
660
|
-
if (/\bapprove/i.test(text))
|
|
661
|
-
|
|
662
|
-
if (/\brevis/i.test(text))
|
|
663
|
-
return 60;
|
|
516
|
+
if (/\bapprove/i.test(text)) return 85;
|
|
517
|
+
if (/\brevis/i.test(text)) return 60;
|
|
664
518
|
return 75;
|
|
665
519
|
}
|
|
666
520
|
function parseAffectedStories(text) {
|
|
667
521
|
const storiesMatch = text.match(/AFFECTED_STORIES:\s*\[([^\]]+)\]/i);
|
|
668
|
-
if (!storiesMatch)
|
|
669
|
-
return null;
|
|
522
|
+
if (!storiesMatch) return null;
|
|
670
523
|
const stories = storiesMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
|
671
|
-
if (stories.length === 0)
|
|
672
|
-
return null;
|
|
524
|
+
if (stories.length === 0) return null;
|
|
673
525
|
let reasons = {};
|
|
674
526
|
const reasonsMatch = text.match(/AFFECTED_REASONS:\s*(\{[\s\S]*?\})/i);
|
|
675
527
|
if (reasonsMatch) {
|
|
@@ -686,7 +538,8 @@ function parseAffectedStories(text) {
|
|
|
686
538
|
}
|
|
687
539
|
return { stories, reasons };
|
|
688
540
|
}
|
|
689
|
-
async function runOrchestration(config, userTask, trustAll, sandboxed
|
|
541
|
+
async function runOrchestration(config, userTask, trustAll, sandboxed, output) {
|
|
542
|
+
userTask = resolveTaskInput(userTask, process.cwd());
|
|
690
543
|
const costTracker = new CostTracker();
|
|
691
544
|
const context = {
|
|
692
545
|
filesCreated: [],
|
|
@@ -694,18 +547,18 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
|
|
|
694
547
|
decisions: [],
|
|
695
548
|
learnings: []
|
|
696
549
|
};
|
|
697
|
-
const
|
|
698
|
-
if (agentRl)
|
|
699
|
-
permissions.setReadline(agentRl);
|
|
550
|
+
const sessionAllow = /* @__PURE__ */ new Set();
|
|
700
551
|
const workingDir = process.cwd();
|
|
701
|
-
const
|
|
702
|
-
|
|
552
|
+
const planResult = await planStories(config, userTask, workingDir, sandboxed, output);
|
|
553
|
+
const plannerStories = planResult.stories;
|
|
554
|
+
costTracker.addUsage("Planner", planResult.provider, planResult.model, planResult.inputTokens, planResult.outputTokens);
|
|
555
|
+
output.updateCost?.(costTracker.getTotalCost());
|
|
556
|
+
output.log("planner", `Plan generated: ${plannerStories.length} stories`);
|
|
703
557
|
plannerStories.forEach((s, i) => {
|
|
704
|
-
|
|
705
|
-
wmLog("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
|
|
558
|
+
output.log("planner", `Step ${i + 1}: [${s.persona}] ${s.title}${s.dependsOn?.length ? ` (after: ${s.dependsOn.join(", ")})` : ""}`);
|
|
706
559
|
});
|
|
707
|
-
|
|
708
|
-
|
|
560
|
+
output.log("planner", `Plan validated: ${plannerStories.length} stories. Task queued for execution.`);
|
|
561
|
+
output.log("system", "");
|
|
709
562
|
if (config.review?.useCritic) {
|
|
710
563
|
const critic = loadPersona("critic");
|
|
711
564
|
if (critic) {
|
|
@@ -719,14 +572,14 @@ async function runOrchestration(config, userTask, trustAll, sandboxed = true, ag
|
|
|
719
572
|
criticReadOnly[name] = {
|
|
720
573
|
...toolDef,
|
|
721
574
|
execute: async (input) => {
|
|
722
|
-
|
|
575
|
+
output.log("critic", formatToolCallDisplay(name, input));
|
|
723
576
|
const result = await toolDef.execute(input);
|
|
724
577
|
return result;
|
|
725
578
|
}
|
|
726
579
|
};
|
|
727
580
|
}
|
|
728
581
|
}
|
|
729
|
-
|
|
582
|
+
output.status("Critic reviewing plan...");
|
|
730
583
|
const criticStream = streamText({
|
|
731
584
|
model: criticModel,
|
|
732
585
|
system: critic.systemPrompt,
|
|
@@ -742,52 +595,47 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
|
|
|
742
595
|
for await (const _chunk of criticStream.textStream) {
|
|
743
596
|
}
|
|
744
597
|
const criticText = await criticStream.text;
|
|
745
|
-
|
|
598
|
+
output.statusDone();
|
|
746
599
|
const score = extractScore(criticText);
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
600
|
+
output.log("critic", `::review_score::${score}`);
|
|
601
|
+
output.log("critic", score >= 80 ? "Plan approved" : "Plan needs revision");
|
|
602
|
+
output.log("system", "");
|
|
750
603
|
}
|
|
751
604
|
}
|
|
752
605
|
const sorted = topologicalSort(plannerStories);
|
|
753
606
|
if (!trustAll) {
|
|
754
|
-
let
|
|
607
|
+
let proceed = false;
|
|
755
608
|
try {
|
|
756
|
-
|
|
609
|
+
proceed = await output.confirm("Execute this plan?");
|
|
757
610
|
} catch {
|
|
758
611
|
}
|
|
759
|
-
if (
|
|
760
|
-
|
|
612
|
+
if (!proceed) {
|
|
613
|
+
output.log("system", "Plan cancelled.");
|
|
761
614
|
return;
|
|
762
615
|
}
|
|
763
|
-
|
|
616
|
+
output.log("system", "");
|
|
764
617
|
}
|
|
765
618
|
for (let i = 0; i < sorted.length; i++) {
|
|
766
619
|
const story = sorted[i];
|
|
767
620
|
const persona = loadPersona(story.persona);
|
|
768
621
|
if (!persona) {
|
|
769
|
-
|
|
622
|
+
output.error(`Unknown persona: ${story.persona}`);
|
|
770
623
|
continue;
|
|
771
624
|
}
|
|
772
|
-
const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(
|
|
625
|
+
const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(
|
|
626
|
+
config,
|
|
627
|
+
persona.provider || story.persona
|
|
628
|
+
);
|
|
773
629
|
if (apiKey) {
|
|
774
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
630
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
775
631
|
const envVar = envMap[provider];
|
|
776
|
-
if (envVar && !process.env[envVar])
|
|
777
|
-
process.env[envVar] = apiKey;
|
|
632
|
+
if (envVar && !process.env[envVar]) process.env[envVar] = apiKey;
|
|
778
633
|
}
|
|
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();
|
|
634
|
+
output.log("system", `--- Story ${i + 1}/${sorted.length} ---`);
|
|
635
|
+
output.coordinatorLog(`Task claimed by orchestrator`);
|
|
636
|
+
output.log(story.persona, `Starting ${story.title}`);
|
|
637
|
+
output.log(story.persona, `Executing story with AIClient (model: ${modelName})...`);
|
|
638
|
+
output.status("");
|
|
791
639
|
const model = createModel(provider, modelName, host, contextLength);
|
|
792
640
|
const allTools = createToolDefinitions(workingDir, model, sandboxed);
|
|
793
641
|
const personaTools = {};
|
|
@@ -798,18 +646,17 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
|
|
|
798
646
|
personaTools[toolName] = {
|
|
799
647
|
...toolDef,
|
|
800
648
|
execute: async (input) => {
|
|
801
|
-
const allowed = await
|
|
802
|
-
if (!allowed)
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
lastToolCall = callKey;
|
|
649
|
+
const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
|
|
650
|
+
if (!allowed) return "Tool execution denied by user.";
|
|
651
|
+
const sig = `${toolName}:${JSON.stringify(input)}`;
|
|
652
|
+
const isDuplicate = sig === lastToolCall;
|
|
653
|
+
lastToolCall = sig;
|
|
807
654
|
if (!isDuplicate) {
|
|
808
|
-
|
|
809
|
-
|
|
655
|
+
output.statusDone();
|
|
656
|
+
output.toolCall(story.persona, toolName, input);
|
|
810
657
|
}
|
|
811
658
|
const result = await toolDef.execute(input);
|
|
812
|
-
|
|
659
|
+
output.status("");
|
|
813
660
|
return result;
|
|
814
661
|
}
|
|
815
662
|
};
|
|
@@ -872,12 +719,11 @@ ${revisionFeedback}` : ""}`;
|
|
|
872
719
|
...buildOllamaOptions(provider, contextLength),
|
|
873
720
|
onStepFinish({ text: text2 }) {
|
|
874
721
|
if (text2) {
|
|
875
|
-
|
|
722
|
+
output.statusDone();
|
|
876
723
|
const lines = text2.split("\n").filter((l) => l.trim());
|
|
877
724
|
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);
|
|
725
|
+
if (line.includes("::decision::") || line.includes("::learning::") || line.includes("::file_created::") || line.includes("::file_modified::")) continue;
|
|
726
|
+
output.log(story.persona, line);
|
|
881
727
|
}
|
|
882
728
|
}
|
|
883
729
|
}
|
|
@@ -887,13 +733,13 @@ ${revisionFeedback}` : ""}`;
|
|
|
887
733
|
const text = await stream.text;
|
|
888
734
|
allText = text;
|
|
889
735
|
const usage = await stream.totalUsage;
|
|
890
|
-
|
|
736
|
+
output.statusDone();
|
|
891
737
|
const decisionMatches = text.match(/::decision::(.*?)(?=::\w+::|$)/gs);
|
|
892
738
|
if (decisionMatches) {
|
|
893
739
|
for (const m of decisionMatches) {
|
|
894
740
|
const decision = m.replace("::decision::", "").trim();
|
|
895
741
|
context.decisions.push(decision);
|
|
896
|
-
|
|
742
|
+
output.log(story.persona, decision);
|
|
897
743
|
}
|
|
898
744
|
}
|
|
899
745
|
const learningMatches = text.match(/::learning::(.*?)(?=::\w+::|$)/gs);
|
|
@@ -917,17 +763,18 @@ ${revisionFeedback}` : ""}`;
|
|
|
917
763
|
const inTokens = usage?.inputTokens || 0;
|
|
918
764
|
const outTokens = usage?.outputTokens || 0;
|
|
919
765
|
costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
|
|
920
|
-
|
|
921
|
-
|
|
766
|
+
output.updateCost?.(costTracker.getTotalCost());
|
|
767
|
+
output.log(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
|
|
768
|
+
output.log("system", "");
|
|
922
769
|
break;
|
|
923
770
|
} catch (err) {
|
|
924
|
-
|
|
771
|
+
output.statusDone();
|
|
925
772
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
926
773
|
if (isTransientError(err) && revision < 2) {
|
|
927
|
-
|
|
774
|
+
output.log(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
|
|
928
775
|
continue;
|
|
929
776
|
}
|
|
930
|
-
|
|
777
|
+
output.error(`Story ${i + 1} failed: ${errMsg}`);
|
|
931
778
|
break;
|
|
932
779
|
}
|
|
933
780
|
}
|
|
@@ -935,16 +782,18 @@ ${revisionFeedback}` : ""}`;
|
|
|
935
782
|
const maxRevisions = config.review?.maxRevisions ?? 2;
|
|
936
783
|
const autoRevise = config.review?.autoRevise ?? false;
|
|
937
784
|
const approvalThreshold = config.review?.approvalThreshold ?? 80;
|
|
938
|
-
const reviewer = loadPersona("
|
|
785
|
+
const reviewer = loadPersona("tech_lead");
|
|
939
786
|
if (reviewer) {
|
|
940
|
-
const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(
|
|
787
|
+
const { provider: revProvider, model: revModel, host: revHost, contextLength: revCtx } = getProviderForPersona(
|
|
788
|
+
config,
|
|
789
|
+
"tech_lead"
|
|
790
|
+
);
|
|
941
791
|
const revApiKey = config.providers[revProvider]?.apiKey;
|
|
942
792
|
if (revApiKey) {
|
|
943
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
793
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
944
794
|
const envVar = envMap[revProvider];
|
|
945
795
|
const key = revApiKey.startsWith("{env:") ? process.env[revApiKey.slice(5, -1)] : revApiKey;
|
|
946
|
-
if (envVar && key && !process.env[envVar])
|
|
947
|
-
process.env[envVar] = key;
|
|
796
|
+
if (envVar && key && !process.env[envVar]) process.env[envVar] = key;
|
|
948
797
|
}
|
|
949
798
|
const reviewModel = createModel(revProvider, revModel, revHost, revCtx);
|
|
950
799
|
const reviewTools = createToolDefinitions(workingDir, reviewModel, sandboxed);
|
|
@@ -955,7 +804,7 @@ ${revisionFeedback}` : ""}`;
|
|
|
955
804
|
reviewerTools[toolName] = {
|
|
956
805
|
...toolDef,
|
|
957
806
|
execute: async (input) => {
|
|
958
|
-
|
|
807
|
+
output.log("tech_lead", formatToolCallDisplay(toolName, input));
|
|
959
808
|
const result = await toolDef.execute(input);
|
|
960
809
|
return result;
|
|
961
810
|
}
|
|
@@ -965,13 +814,9 @@ ${revisionFeedback}` : ""}`;
|
|
|
965
814
|
let previousReviewFeedback = "";
|
|
966
815
|
for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
|
|
967
816
|
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();
|
|
817
|
+
output.coordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
|
|
818
|
+
output.log("tech_lead", `Starting agent execution (model: ${revModel})`);
|
|
819
|
+
output.status(isRevision ? "Reviewer -- Re-checking after revisions" : "Reviewer -- Checking code quality");
|
|
975
820
|
try {
|
|
976
821
|
const previousFeedbackSection = isRevision && previousReviewFeedback ? `## Previous Review Feedback (Review ${reviewRound}/${maxRevisions})
|
|
977
822
|
This is a revision attempt. The previous code was reviewed and these issues were identified:
|
|
@@ -1034,7 +879,7 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
|
|
|
1034
879
|
- Only include stories that have ACTUAL implementation issues
|
|
1035
880
|
- If ALL stories need revision, you may omit AFFECTED_STORIES (all will re-run)
|
|
1036
881
|
- Be specific in AFFECTED_REASONS so developers know exactly what to fix`;
|
|
1037
|
-
let
|
|
882
|
+
let reviewerOutput = "";
|
|
1038
883
|
const reviewStream = streamText({
|
|
1039
884
|
model: reviewModel,
|
|
1040
885
|
system: reviewer.systemPrompt,
|
|
@@ -1045,95 +890,95 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
|
|
|
1045
890
|
...buildOllamaOptions(revProvider, revCtx),
|
|
1046
891
|
onStepFinish({ text }) {
|
|
1047
892
|
if (text) {
|
|
1048
|
-
|
|
893
|
+
reviewerOutput += text + "\n";
|
|
894
|
+
output.statusDone();
|
|
1049
895
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
1050
896
|
for (const line of lines) {
|
|
1051
|
-
if (line.includes("::review_score::") || line.includes("::review_verdict::"))
|
|
1052
|
-
|
|
1053
|
-
wmLog("tech_lead", line);
|
|
897
|
+
if (line.includes("::review_score::") || line.includes("::review_verdict::")) continue;
|
|
898
|
+
output.log("tech_lead", line);
|
|
1054
899
|
}
|
|
1055
900
|
}
|
|
1056
901
|
}
|
|
1057
902
|
});
|
|
1058
903
|
for await (const _chunk of reviewStream.textStream) {
|
|
1059
904
|
}
|
|
1060
|
-
const
|
|
1061
|
-
const reviewText = finalReviewText && finalReviewText.length > allReviewText.length ? finalReviewText : allReviewText;
|
|
905
|
+
const reviewText = reviewerOutput;
|
|
1062
906
|
const reviewUsage = await reviewStream.totalUsage;
|
|
1063
|
-
|
|
907
|
+
output.statusDone();
|
|
1064
908
|
const score = extractScore(reviewText);
|
|
1065
909
|
const approved = score >= approvalThreshold;
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
910
|
+
output.log("tech_lead", `::code_quality_score::${score}`);
|
|
911
|
+
output.log("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
|
|
912
|
+
output.coordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
|
|
1069
913
|
previousReviewFeedback = reviewText;
|
|
1070
|
-
|
|
1071
|
-
costTracker.addUsage(
|
|
1072
|
-
|
|
1073
|
-
|
|
914
|
+
output.log("system", "");
|
|
915
|
+
costTracker.addUsage(
|
|
916
|
+
`Reviewer (round ${reviewRound + 1})`,
|
|
917
|
+
revProvider,
|
|
918
|
+
revModel,
|
|
919
|
+
reviewUsage?.inputTokens || 0,
|
|
920
|
+
reviewUsage?.outputTokens || 0
|
|
921
|
+
);
|
|
922
|
+
output.updateCost?.(costTracker.getTotalCost());
|
|
923
|
+
if (approved) break;
|
|
1074
924
|
if (reviewRound >= maxRevisions) {
|
|
1075
|
-
|
|
925
|
+
output.log("system", `Max review revisions (${maxRevisions}) reached`);
|
|
1076
926
|
break;
|
|
1077
927
|
}
|
|
1078
928
|
let shouldRevise = autoRevise;
|
|
1079
929
|
if (!autoRevise) {
|
|
1080
930
|
try {
|
|
1081
|
-
|
|
1082
|
-
shouldRevise = answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
931
|
+
shouldRevise = await output.confirm(`Revise and re-review? (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)`);
|
|
1083
932
|
} catch {
|
|
1084
933
|
shouldRevise = false;
|
|
1085
934
|
}
|
|
1086
935
|
} else {
|
|
1087
|
-
|
|
936
|
+
output.log("system", `Auto-revising (${maxRevisions - reviewRound} attempt${maxRevisions - reviewRound > 1 ? "s" : ""} left)...`);
|
|
1088
937
|
}
|
|
1089
938
|
if (!shouldRevise) {
|
|
1090
|
-
|
|
939
|
+
output.log("system", "Skipping revision, proceeding to commit.");
|
|
1091
940
|
break;
|
|
1092
941
|
}
|
|
1093
942
|
const affected = parseAffectedStories(reviewText);
|
|
1094
943
|
const affectedSet = affected ? new Set(affected.stories) : null;
|
|
1095
944
|
if (affected) {
|
|
1096
945
|
const selectiveInfo = `stories ${affected.stories.join(", ")}`;
|
|
1097
|
-
|
|
946
|
+
output.coordinatorLog(`Selective revision: ${selectiveInfo}`);
|
|
1098
947
|
if (Object.keys(affected.reasons).length > 0) {
|
|
1099
948
|
for (const [idx, reason] of Object.entries(affected.reasons)) {
|
|
1100
|
-
|
|
949
|
+
output.coordinatorLog(` Story ${idx}: ${reason}`);
|
|
1101
950
|
}
|
|
1102
951
|
}
|
|
1103
952
|
} else {
|
|
1104
|
-
|
|
953
|
+
output.coordinatorLog("Full revision (all stories)");
|
|
1105
954
|
}
|
|
1106
|
-
|
|
955
|
+
output.log("system", "--- Revision Pass ---");
|
|
1107
956
|
for (let i = 0; i < sorted.length; i++) {
|
|
1108
957
|
const story = sorted[i];
|
|
1109
958
|
if (affectedSet && !affectedSet.has(i + 1)) {
|
|
1110
|
-
|
|
959
|
+
output.coordinatorLog(`Skipping story ${i + 1}/${sorted.length} \u2014 not affected`);
|
|
1111
960
|
continue;
|
|
1112
961
|
}
|
|
1113
962
|
const storyPersona = loadPersona(story.persona);
|
|
1114
|
-
if (!storyPersona)
|
|
1115
|
-
|
|
1116
|
-
|
|
963
|
+
if (!storyPersona) continue;
|
|
964
|
+
const { provider: sProvider, model: sModel, host: sHost, contextLength: sCtx } = getProviderForPersona(
|
|
965
|
+
config,
|
|
966
|
+
storyPersona.provider || story.persona
|
|
967
|
+
);
|
|
1117
968
|
if (sProvider) {
|
|
1118
969
|
const sApiKey = config.providers[sProvider]?.apiKey;
|
|
1119
970
|
if (sApiKey) {
|
|
1120
|
-
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "
|
|
971
|
+
const envMap = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" };
|
|
1121
972
|
const envVar = envMap[sProvider];
|
|
1122
973
|
if (envVar && !process.env[envVar]) {
|
|
1123
974
|
const key = sApiKey.startsWith("{env:") ? process.env[sApiKey.slice(5, -1)] : sApiKey;
|
|
1124
|
-
if (key)
|
|
1125
|
-
process.env[envVar] = key;
|
|
975
|
+
if (key) process.env[envVar] = key;
|
|
1126
976
|
}
|
|
1127
977
|
}
|
|
1128
978
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
stream: process.stdout,
|
|
1133
|
-
text: "",
|
|
1134
|
-
prefixText: "",
|
|
1135
|
-
spinner: "dots"
|
|
1136
|
-
}).start();
|
|
979
|
+
output.coordinatorLog(`Revision pass for story ${i + 1}/${sorted.length}`);
|
|
980
|
+
output.log(story.persona, `Starting revision: ${story.title} (model: ${sModel})`);
|
|
981
|
+
output.status("");
|
|
1137
982
|
const storyModel = createModel(sProvider, sModel, sHost, sCtx);
|
|
1138
983
|
const storyAllTools = createToolDefinitions(workingDir, storyModel, sandboxed);
|
|
1139
984
|
const storyTools = {};
|
|
@@ -1143,13 +988,12 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
|
|
|
1143
988
|
storyTools[toolName] = {
|
|
1144
989
|
...toolDef,
|
|
1145
990
|
execute: async (input) => {
|
|
1146
|
-
const allowed = await
|
|
1147
|
-
if (!allowed)
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
wmLog(story.persona, formatToolCall(toolName, input));
|
|
991
|
+
const allowed = await checkToolPermission(toolName, input, trustAll, sessionAllow, output);
|
|
992
|
+
if (!allowed) return "Tool execution denied by user.";
|
|
993
|
+
output.statusDone();
|
|
994
|
+
output.log(story.persona, formatToolCallDisplay(toolName, input));
|
|
1151
995
|
const result = await toolDef.execute(input);
|
|
1152
|
-
|
|
996
|
+
output.status("");
|
|
1153
997
|
return result;
|
|
1154
998
|
}
|
|
1155
999
|
};
|
|
@@ -1186,12 +1030,11 @@ ${story.description}`,
|
|
|
1186
1030
|
...buildOllamaOptions(sProvider, sCtx),
|
|
1187
1031
|
onStepFinish({ text }) {
|
|
1188
1032
|
if (text) {
|
|
1189
|
-
|
|
1033
|
+
output.statusDone();
|
|
1190
1034
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
1191
1035
|
for (const line of lines) {
|
|
1192
|
-
if (line.includes("::"))
|
|
1193
|
-
|
|
1194
|
-
wmLog(story.persona, line);
|
|
1036
|
+
if (line.includes("::")) continue;
|
|
1037
|
+
output.log(story.persona, line);
|
|
1195
1038
|
}
|
|
1196
1039
|
}
|
|
1197
1040
|
}
|
|
@@ -1199,80 +1042,87 @@ ${story.description}`,
|
|
|
1199
1042
|
for await (const _chunk of revStream.textStream) {
|
|
1200
1043
|
}
|
|
1201
1044
|
const revUsage = await revStream.totalUsage;
|
|
1202
|
-
|
|
1203
|
-
costTracker.addUsage(
|
|
1204
|
-
|
|
1045
|
+
output.statusDone();
|
|
1046
|
+
costTracker.addUsage(
|
|
1047
|
+
`${storyPersona.name} (revision)`,
|
|
1048
|
+
sProvider,
|
|
1049
|
+
sModel,
|
|
1050
|
+
revUsage?.inputTokens || 0,
|
|
1051
|
+
revUsage?.outputTokens || 0
|
|
1052
|
+
);
|
|
1053
|
+
output.updateCost?.(costTracker.getTotalCost());
|
|
1054
|
+
output.log(story.persona, `${story.title} \u2014 revision complete!`);
|
|
1205
1055
|
} catch (err) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1056
|
+
output.statusDone();
|
|
1057
|
+
output.log("system", `Revision failed for story ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1208
1058
|
}
|
|
1209
1059
|
}
|
|
1210
|
-
|
|
1060
|
+
output.log("system", "");
|
|
1211
1061
|
} catch (err) {
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1062
|
+
output.statusDone();
|
|
1063
|
+
output.log("system", `Review skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
1064
|
+
output.log("system", "");
|
|
1215
1065
|
break;
|
|
1216
1066
|
}
|
|
1217
1067
|
}
|
|
1218
1068
|
}
|
|
1219
1069
|
try {
|
|
1220
|
-
const { execSync
|
|
1070
|
+
const { execSync } = await import("child_process");
|
|
1221
1071
|
try {
|
|
1222
|
-
|
|
1072
|
+
execSync("git rev-parse --git-dir", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
|
|
1223
1073
|
} catch {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const
|
|
1074
|
+
output.coordinatorLog("Initializing git repository...");
|
|
1075
|
+
execSync("git init", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" });
|
|
1076
|
+
const fs3 = await import("fs");
|
|
1227
1077
|
const gitignorePath = `${workingDir}/.gitignore`;
|
|
1228
|
-
if (!
|
|
1229
|
-
|
|
1078
|
+
if (!fs3.existsSync(gitignorePath)) {
|
|
1079
|
+
fs3.writeFileSync(gitignorePath, "node_modules/\ndist/\n.env\n.workermill/\n*.log\n", "utf-8");
|
|
1230
1080
|
}
|
|
1231
|
-
|
|
1081
|
+
output.coordinatorLog("Git repo initialized");
|
|
1232
1082
|
}
|
|
1233
|
-
const diff =
|
|
1234
|
-
const untracked =
|
|
1083
|
+
const diff = execSync("git diff --stat", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1084
|
+
const untracked = execSync("git ls-files --others --exclude-standard", { cwd: workingDir, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1235
1085
|
const hasChanges = diff || untracked;
|
|
1236
1086
|
if (hasChanges) {
|
|
1237
|
-
|
|
1087
|
+
output.log("system", "--- Changes ---");
|
|
1238
1088
|
if (diff) {
|
|
1239
|
-
|
|
1089
|
+
output.log("system", diff);
|
|
1240
1090
|
}
|
|
1241
1091
|
if (untracked) {
|
|
1242
1092
|
const untrackedFiles = untracked.split("\n");
|
|
1243
|
-
|
|
1093
|
+
output.log("system", "New files:");
|
|
1244
1094
|
for (const f of untrackedFiles) {
|
|
1245
|
-
|
|
1095
|
+
output.log("system", ` + ${f}`);
|
|
1246
1096
|
}
|
|
1247
1097
|
}
|
|
1248
|
-
|
|
1098
|
+
output.log("system", "");
|
|
1249
1099
|
if (!trustAll) {
|
|
1250
|
-
const
|
|
1251
|
-
if (
|
|
1100
|
+
const commitConfirmed = await output.confirm("Commit these changes?");
|
|
1101
|
+
if (commitConfirmed) {
|
|
1252
1102
|
const filesToStage = [...context.filesCreated, ...context.filesModified].filter(Boolean);
|
|
1253
1103
|
if (filesToStage.length > 0) {
|
|
1254
1104
|
for (const f of filesToStage) {
|
|
1255
1105
|
try {
|
|
1256
|
-
|
|
1106
|
+
execSync(`git add "${f}"`, { cwd: workingDir, stdio: "pipe" });
|
|
1257
1107
|
} catch {
|
|
1258
1108
|
}
|
|
1259
1109
|
}
|
|
1260
1110
|
} else {
|
|
1261
|
-
|
|
1111
|
+
execSync("git add -u", { cwd: workingDir, stdio: "pipe" });
|
|
1262
1112
|
}
|
|
1263
1113
|
const storyTitles = sorted.map((s) => s.title).join(", ");
|
|
1264
1114
|
const msg = `feat: ${storyTitles}`.slice(0, 72);
|
|
1265
|
-
|
|
1266
|
-
|
|
1115
|
+
execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: workingDir, stdio: "pipe" });
|
|
1116
|
+
output.log("system", "Changes committed");
|
|
1267
1117
|
}
|
|
1268
1118
|
}
|
|
1269
1119
|
}
|
|
1270
1120
|
} catch (err) {
|
|
1271
1121
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1122
|
+
output.log("system", "--- Session Complete ---");
|
|
1123
|
+
output.log("system", "");
|
|
1124
|
+
output.log("system", costTracker.getSummary());
|
|
1125
|
+
output.log("system", "");
|
|
1276
1126
|
}
|
|
1277
1127
|
export {
|
|
1278
1128
|
classifyComplexity,
|