daedalus-cli 1.22.2 → 1.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/agents/orchestrator.d.ts +17 -0
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +613 -69
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/agents/orchestrator.test.js +12 -12
- package/dist/agents/orchestrator.test.js.map +1 -1
- package/dist/agents/roles.d.ts.map +1 -1
- package/dist/agents/roles.js +4 -1
- package/dist/agents/roles.js.map +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +8 -0
- package/dist/repl.js.map +1 -1
- package/dist/tools/builtin/terminal.d.ts.map +1 -1
- package/dist/tools/builtin/terminal.js +12 -0
- package/dist/tools/builtin/terminal.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,30 +1,97 @@
|
|
|
1
1
|
// Multi-agent orchestrator - coordinates delegation and synthesis
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
2
5
|
import readline from 'readline';
|
|
3
6
|
import { BUILTIN_TOOLS } from '../tools/definitions.js';
|
|
4
7
|
import { executeToolCalls } from '../tools/executor.js';
|
|
5
8
|
import { getAgentRole, filterToolsForRole } from './roles.js';
|
|
6
9
|
import pc from 'picocolors';
|
|
7
10
|
import { DaedalusSpinner } from '../tools/daedalus-spinner.js';
|
|
11
|
+
import { loadProfile } from '../profile.js';
|
|
12
|
+
// Catch bracket placeholders common across projects (licenses, code comments, configs, docs)
|
|
13
|
+
const PLACEHOLDER_RE = /\[(?:year|your\s+name|fullname|author|copyright|license\s*content|placeholder|todo|fixme|enter\s+\w+|project\s+name|description|date|url|version|email|note|username|login|password|token|api[_-]?key|path|filename|branch|tag)\]/i;
|
|
14
|
+
// Catch HTML comment placeholders like <!-- Add analytics content here -->
|
|
15
|
+
const HTML_PLACEHOLDER_RE = /<!--[^>]*?(?:TODO|FIXME|add\s+(?:your|content|more|some|analytics)|your\s+(?:content|code|text|name|details?)|placeholder|insert|enter\s+\w+|implement|put\s+your|content\s+here|details?\s+here|more\s+content)[^>]*?-->/i;
|
|
8
16
|
export class Orchestrator {
|
|
9
17
|
router;
|
|
10
18
|
messages;
|
|
11
19
|
toolContext;
|
|
12
20
|
sessionManager;
|
|
13
21
|
results = [];
|
|
22
|
+
MAX_INITIAL_TASKS = 4;
|
|
23
|
+
MAX_TOTAL_TASKS = 10;
|
|
24
|
+
REPLAN_INTERVAL = 2;
|
|
14
25
|
constructor(router, messages, toolContext, sessionManager) {
|
|
15
26
|
this.router = router;
|
|
16
27
|
this.messages = messages;
|
|
17
28
|
this.toolContext = toolContext;
|
|
18
29
|
this.sessionManager = sessionManager;
|
|
19
30
|
}
|
|
31
|
+
async discoverProjectContext() {
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
const parts = [];
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
36
|
+
const info = [];
|
|
37
|
+
if (pkg.dependencies) {
|
|
38
|
+
if (pkg.dependencies.next)
|
|
39
|
+
info.push('Next.js (React, SSR)');
|
|
40
|
+
else if (pkg.dependencies.react)
|
|
41
|
+
info.push('React');
|
|
42
|
+
if (pkg.dependencies.vue)
|
|
43
|
+
info.push('Vue');
|
|
44
|
+
if (pkg.dependencies.express)
|
|
45
|
+
info.push('Express');
|
|
46
|
+
if (pkg.dependencies['@angular/core'])
|
|
47
|
+
info.push('Angular');
|
|
48
|
+
}
|
|
49
|
+
if (pkg.devDependencies) {
|
|
50
|
+
if (pkg.devDependencies.vite)
|
|
51
|
+
info.push('Vite');
|
|
52
|
+
if (pkg.devDependencies.typescript)
|
|
53
|
+
info.push('TypeScript');
|
|
54
|
+
if (pkg.devDependencies.tailwindcss)
|
|
55
|
+
info.push('Tailwind CSS');
|
|
56
|
+
}
|
|
57
|
+
const scripts = pkg.scripts ? Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).join(', ') : 'none';
|
|
58
|
+
parts.push(`Framework: ${info.join(' | ') || 'unknown'}`);
|
|
59
|
+
parts.push(`Scripts: ${scripts}`);
|
|
60
|
+
}
|
|
61
|
+
catch { /* not a node project */ }
|
|
62
|
+
try {
|
|
63
|
+
const entries = fs.readdirSync(cwd).filter(e => !e.startsWith('.') && e !== 'node_modules' && e !== 'ffmpeg');
|
|
64
|
+
const dirs = entries.filter(e => fs.statSync(path.join(cwd, e)).isDirectory());
|
|
65
|
+
const files = entries.filter(e => !fs.statSync(path.join(cwd, e)).isDirectory());
|
|
66
|
+
if (dirs.length > 0)
|
|
67
|
+
parts.push(`Directories: ${dirs.join(', ')}`);
|
|
68
|
+
if (files.length > 0)
|
|
69
|
+
parts.push(`Key files: ${files.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
catch { /* not readable */ }
|
|
72
|
+
return parts.length > 0 ? parts.join('\n') : '';
|
|
73
|
+
}
|
|
20
74
|
async run(goal) {
|
|
21
75
|
this.results = [];
|
|
22
76
|
try {
|
|
23
|
-
const
|
|
77
|
+
const projectContext = await this.discoverProjectContext();
|
|
78
|
+
if (projectContext) {
|
|
79
|
+
console.log(pc.gray(`\n${projectContext.split('\n').map(l => ` ${l}`).join('\n')}`));
|
|
80
|
+
}
|
|
81
|
+
let plan = await this.createPlan(goal, projectContext);
|
|
24
82
|
if (this.toolContext.abortSignal.aborted) {
|
|
25
83
|
return 'Orchestration stopped by user';
|
|
26
84
|
}
|
|
27
|
-
|
|
85
|
+
let tasks = this.parseDelegationTasks(plan, goal);
|
|
86
|
+
// Cap initial tasks — re-plan if the planner gets carried away
|
|
87
|
+
if (tasks.length > this.MAX_INITIAL_TASKS) {
|
|
88
|
+
console.log(pc.yellow(`\n[PLAN] Initial plan has ${tasks.length} tasks (max ${this.MAX_INITIAL_TASKS}). Simplifying...`));
|
|
89
|
+
plan = await this.createPlan(`${goal}\n\nYour previous plan had ${tasks.length} steps which is too many. Create a simpler plan with at most ${this.MAX_INITIAL_TASKS} focused steps. Merge related steps together. Each step must produce real output.`, projectContext);
|
|
90
|
+
tasks = Orchestrator.filterValidTasks(this.parseDelegationTasks(plan, goal));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
tasks = Orchestrator.filterValidTasks(tasks);
|
|
94
|
+
}
|
|
28
95
|
if (this.sessionManager) {
|
|
29
96
|
this.sessionManager.saveState('orchestrate_plan', tasks);
|
|
30
97
|
this.sessionManager.saveState('orchestrate_goal', goal);
|
|
@@ -32,7 +99,7 @@ export class Orchestrator {
|
|
|
32
99
|
this.sessionManager.saveState('orchestrate_results', []);
|
|
33
100
|
this.sessionManager.saveState('orchestrate_plan_text', plan);
|
|
34
101
|
}
|
|
35
|
-
await this.executePlan(plan, tasks, 0);
|
|
102
|
+
await this.executePlan(plan, tasks, 0, goal, projectContext);
|
|
36
103
|
}
|
|
37
104
|
catch (err) {
|
|
38
105
|
return `Orchestration failed: ${err.message}`;
|
|
@@ -48,6 +115,7 @@ export class Orchestrator {
|
|
|
48
115
|
}
|
|
49
116
|
async resume(goal, planText, tasks, startIndex, previousResults) {
|
|
50
117
|
this.results = [...previousResults];
|
|
118
|
+
const projectContext = await this.discoverProjectContext();
|
|
51
119
|
tasks.forEach((t, idx) => {
|
|
52
120
|
if (idx < startIndex) {
|
|
53
121
|
t.status = 'completed';
|
|
@@ -60,7 +128,7 @@ export class Orchestrator {
|
|
|
60
128
|
if (this.toolContext.abortSignal.aborted) {
|
|
61
129
|
return 'Orchestration stopped by user';
|
|
62
130
|
}
|
|
63
|
-
await this.executePlan(planText, tasks, startIndex);
|
|
131
|
+
await this.executePlan(planText, tasks, startIndex, goal, projectContext);
|
|
64
132
|
}
|
|
65
133
|
catch (err) {
|
|
66
134
|
return `Orchestration failed: ${err.message}`;
|
|
@@ -74,62 +142,74 @@ export class Orchestrator {
|
|
|
74
142
|
}
|
|
75
143
|
return this.synthesize(goal);
|
|
76
144
|
}
|
|
77
|
-
async createPlan(goal) {
|
|
145
|
+
async createPlan(goal, projectContext) {
|
|
78
146
|
const plannerRole = getAgentRole('planner');
|
|
79
147
|
const tools = filterToolsForRole(BUILTIN_TOOLS, 'planner');
|
|
80
148
|
const systemPrompt = plannerRole.systemPrompt;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
tools,
|
|
94
|
-
tool_choice: 'auto',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
planSpinner.stop();
|
|
99
|
-
}
|
|
100
|
-
const assistantMessage = completion.choices[0].message;
|
|
101
|
-
const toolCalls = assistantMessage.tool_calls;
|
|
102
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
103
|
-
// Push the assistant turn with its tool calls
|
|
104
|
-
messages.push(assistantMessage);
|
|
105
|
-
// Execute tool calls and push results back into the conversation
|
|
106
|
-
const results = await this.executeOpenAIToolCalls(toolCalls);
|
|
107
|
-
for (const result of results) {
|
|
108
|
-
messages.push({
|
|
109
|
-
role: 'tool',
|
|
110
|
-
content: result.content,
|
|
111
|
-
tool_call_id: result.toolCallId,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
// Ask the planner for a final text summary now that tools have run
|
|
115
|
-
const finalizeSpinner = new DaedalusSpinner({ text: 'planner finalizing plan', color: (s) => pc.cyan(s) });
|
|
116
|
-
finalizeSpinner.start();
|
|
117
|
-
let followUp;
|
|
149
|
+
const REFUSAL_RE = /sorry|can'?t|cannot|don'?t have|not (able|capable)|lack(|ing) (the )?(necessary |required )?(tools|capabilities)|unable|apologize/i;
|
|
150
|
+
let attempts = 0;
|
|
151
|
+
const maxAttempts = 2;
|
|
152
|
+
while (attempts < maxAttempts) {
|
|
153
|
+
attempts++;
|
|
154
|
+
const messages = [
|
|
155
|
+
{ role: 'system', content: systemPrompt + (attempts > 1 ? '\n\nIMPORTANT: You MUST create a plan. Do not refuse. Use the tools available if needed, or simply output a delegation plan as plain text.' : '') },
|
|
156
|
+
{ role: 'user', content: `Create a plan for: ${goal}\n\nProject context:\n${projectContext || '(none discovered)'}${Orchestrator.getFrameworkGuidance(projectContext)}\n\n${this.toolContext.activeFiles.size > 0 ? 'Files in context: ' + Array.from(this.toolContext.activeFiles.values()).join(', ') : ''}` },
|
|
157
|
+
];
|
|
158
|
+
const planSpinner = new DaedalusSpinner({ text: `planner generating plan`, color: (s) => pc.cyan(s) });
|
|
159
|
+
planSpinner.start();
|
|
160
|
+
let completion;
|
|
118
161
|
try {
|
|
119
|
-
|
|
162
|
+
completion = await this.router.chat.completions.create({
|
|
120
163
|
model: 'auto',
|
|
121
164
|
messages,
|
|
122
165
|
temperature: plannerRole.temperature ?? 0.2,
|
|
123
166
|
tools,
|
|
124
|
-
tool_choice: '
|
|
167
|
+
tool_choice: 'auto',
|
|
125
168
|
});
|
|
126
169
|
}
|
|
127
170
|
finally {
|
|
128
|
-
|
|
171
|
+
planSpinner.stop();
|
|
172
|
+
}
|
|
173
|
+
const assistantMessage = completion.choices[0].message;
|
|
174
|
+
const toolCalls = assistantMessage.tool_calls;
|
|
175
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
176
|
+
messages.push(assistantMessage);
|
|
177
|
+
const results = await this.executeOpenAIToolCalls(toolCalls);
|
|
178
|
+
for (const result of results) {
|
|
179
|
+
messages.push({
|
|
180
|
+
role: 'tool',
|
|
181
|
+
content: result.content,
|
|
182
|
+
tool_call_id: result.toolCallId,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const finalizeSpinner = new DaedalusSpinner({ text: 'planner finalizing plan', color: (s) => pc.cyan(s) });
|
|
186
|
+
finalizeSpinner.start();
|
|
187
|
+
let followUp;
|
|
188
|
+
try {
|
|
189
|
+
followUp = await this.router.chat.completions.create({
|
|
190
|
+
model: 'auto',
|
|
191
|
+
messages,
|
|
192
|
+
temperature: plannerRole.temperature ?? 0.2,
|
|
193
|
+
tools,
|
|
194
|
+
tool_choice: 'none',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
finalizeSpinner.stop();
|
|
199
|
+
}
|
|
200
|
+
const content = (followUp.choices[0].message).content || '';
|
|
201
|
+
if (content && attempts < maxAttempts && REFUSAL_RE.test(content)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
return content || `- delegate to coder: ${goal}`;
|
|
205
|
+
}
|
|
206
|
+
const content = assistantMessage.content || '';
|
|
207
|
+
if (content && attempts < maxAttempts && REFUSAL_RE.test(content)) {
|
|
208
|
+
continue;
|
|
129
209
|
}
|
|
130
|
-
return
|
|
210
|
+
return content || `- delegate to coder: ${goal}`;
|
|
131
211
|
}
|
|
132
|
-
return
|
|
212
|
+
return `- delegate to coder: ${goal}`;
|
|
133
213
|
}
|
|
134
214
|
formatGoal(goal, indentLength, width = 80) {
|
|
135
215
|
const words = goal.split(/\s+/);
|
|
@@ -175,20 +255,49 @@ export class Orchestrator {
|
|
|
175
255
|
});
|
|
176
256
|
console.log(pc.bold(pc.cyan('--------------------------------')));
|
|
177
257
|
}
|
|
178
|
-
async executePlan(plan, tasks, startIndex = 0) {
|
|
258
|
+
async executePlan(plan, tasks, startIndex = 0, originalGoal, projectContext) {
|
|
259
|
+
let lastReplanCount = 0;
|
|
179
260
|
for (let i = startIndex; i < tasks.length; i++) {
|
|
180
261
|
if (this.toolContext.abortSignal.aborted) {
|
|
181
262
|
break;
|
|
182
263
|
}
|
|
264
|
+
// Hard cap — stop adding new tasks past the limit
|
|
265
|
+
if (i >= this.MAX_TOTAL_TASKS && tasks.filter(t => t.status === 'pending').length > 0) {
|
|
266
|
+
console.log(pc.yellow(`\n[HALT] Reached ${this.MAX_TOTAL_TASKS} task limit. Halting new task generation.`));
|
|
267
|
+
while (tasks.length > this.MAX_TOTAL_TASKS) {
|
|
268
|
+
const removed = tasks.pop();
|
|
269
|
+
if (removed) {
|
|
270
|
+
removed.status = 'skipped';
|
|
271
|
+
removed.error = 'Reached task limit';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
183
276
|
const task = tasks[i];
|
|
277
|
+
// Skip unnecessary config tasks for file-based routing frameworks
|
|
278
|
+
if (Orchestrator.isUnnecessaryConfigTask(task, projectContext)) {
|
|
279
|
+
console.log(pc.yellow(`\n[S] Task ${i + 1}: Skipped — Next.js uses file-based routing, no config changes needed`));
|
|
280
|
+
task.status = 'skipped';
|
|
281
|
+
task.error = 'Unnecessary config task for file-based routing framework';
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
184
284
|
task.status = 'in_progress';
|
|
185
285
|
this.printTaskList(tasks);
|
|
186
|
-
await this.delegateTask(task);
|
|
286
|
+
await this.delegateTask(task, tasks, originalGoal, projectContext);
|
|
187
287
|
this.printTaskList(tasks);
|
|
188
288
|
if (this.sessionManager) {
|
|
189
289
|
this.sessionManager.saveState('orchestrate_task_index', i + 1);
|
|
190
290
|
this.sessionManager.saveState('orchestrate_results', this.results);
|
|
191
291
|
}
|
|
292
|
+
// Re-plan checkpoint: after every REPLAN_INTERVAL completed tasks, re-evaluate
|
|
293
|
+
const completedCount = tasks.filter(t => t.status === 'completed').length;
|
|
294
|
+
if (completedCount > 0 && completedCount - lastReplanCount >= this.REPLAN_INTERVAL) {
|
|
295
|
+
const hasPending = tasks.some(t => t.status === 'pending' || t.status === 'in_progress');
|
|
296
|
+
if (hasPending && originalGoal) {
|
|
297
|
+
lastReplanCount = completedCount;
|
|
298
|
+
await this.replanRemaining(tasks, originalGoal, projectContext);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
192
301
|
if (task.status === 'failed') {
|
|
193
302
|
console.log(`\n${pc.bold(pc.red('--- Task Failure Checkpoint ---'))}`);
|
|
194
303
|
console.log(`${pc.red('[ERROR] Task failed:')} ${task.role} - ${task.goal}`);
|
|
@@ -204,7 +313,7 @@ export class Orchestrator {
|
|
|
204
313
|
task.error = undefined;
|
|
205
314
|
this.printTaskList(tasks);
|
|
206
315
|
this.results.pop();
|
|
207
|
-
await this.delegateTask(task);
|
|
316
|
+
await this.delegateTask(task, undefined, undefined, projectContext);
|
|
208
317
|
this.printTaskList(tasks);
|
|
209
318
|
if (task.status !== 'failed') {
|
|
210
319
|
continue;
|
|
@@ -224,7 +333,7 @@ export class Orchestrator {
|
|
|
224
333
|
task.error = undefined;
|
|
225
334
|
this.printTaskList(tasks);
|
|
226
335
|
this.results.pop();
|
|
227
|
-
await this.delegateTask(task);
|
|
336
|
+
await this.delegateTask(task, undefined, undefined, projectContext);
|
|
228
337
|
this.printTaskList(tasks);
|
|
229
338
|
if (task.status !== 'failed') {
|
|
230
339
|
resolved = true;
|
|
@@ -238,7 +347,7 @@ export class Orchestrator {
|
|
|
238
347
|
task.error = undefined;
|
|
239
348
|
this.printTaskList(tasks);
|
|
240
349
|
this.results.pop();
|
|
241
|
-
await this.delegateTask(task);
|
|
350
|
+
await this.delegateTask(task, undefined, undefined, projectContext);
|
|
242
351
|
this.printTaskList(tasks);
|
|
243
352
|
if (task.status !== 'failed') {
|
|
244
353
|
resolved = true;
|
|
@@ -303,19 +412,100 @@ export class Orchestrator {
|
|
|
303
412
|
}
|
|
304
413
|
}
|
|
305
414
|
}
|
|
306
|
-
|
|
415
|
+
async replanRemaining(tasks, originalGoal, projectContext) {
|
|
416
|
+
const done = tasks.filter(t => t.status === 'completed');
|
|
417
|
+
const pending = tasks.filter(t => t.status === 'pending');
|
|
418
|
+
if (pending.length === 0)
|
|
419
|
+
return;
|
|
420
|
+
const summary = done.map(t => {
|
|
421
|
+
const r = this.results.find(rr => rr.goal === t.goal && rr.role === t.role);
|
|
422
|
+
return r ? `[✓] [${t.role}] ${t.goal} → ${r.summary.split('\n')[0]}` : `[✓] [${t.role}] ${t.goal}`;
|
|
423
|
+
}).join('\n');
|
|
424
|
+
const remainingList = pending.map(t => `[ ] [${t.role}] ${t.goal}`).join('\n');
|
|
425
|
+
console.log(pc.cyan(`\n[RE-PLAN] ${pending.length} task(s) remaining. Re-evaluating based on completed work...`));
|
|
426
|
+
const subPlan = await this.createPlan(`Original goal: ${originalGoal}\n\nCompleted so far:\n${summary}\n\nRemaining:\n${remainingList}\n\nBased on what was completed, re-plan the remaining work. Consolidate and simplify — aim for at most ${this.MAX_INITIAL_TASKS} focused steps. Remove any that are already done or no longer needed. Each remaining step must produce real output.`, projectContext);
|
|
427
|
+
// Remove old pending tasks
|
|
428
|
+
for (let i = tasks.length - 1; i >= 0; i--) {
|
|
429
|
+
if (tasks[i].status === 'pending') {
|
|
430
|
+
tasks.splice(i, 1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Add new tasks from re-plan
|
|
434
|
+
let newTasks = this.parseDelegationTasks(subPlan, originalGoal);
|
|
435
|
+
// Enforce task cap and filter out non-actionable tasks
|
|
436
|
+
newTasks = Orchestrator.filterValidTasks(newTasks).slice(0, this.MAX_INITIAL_TASKS);
|
|
437
|
+
for (const nt of newTasks) {
|
|
438
|
+
nt.splitDepth = 0;
|
|
439
|
+
tasks.push(nt);
|
|
440
|
+
}
|
|
441
|
+
this.printTaskList(tasks);
|
|
442
|
+
}
|
|
443
|
+
static filterValidTasks(tasks) {
|
|
444
|
+
const doneRe = /\b(open|launch|start|run|execute)\b.*\b(file|editor|IDE|app|application|browser|window)\b|\b(commit|push)\b.*\b(git|github|gitlab)\b|\b(use|press|click|type)\b.*\b(mouse|keyboard|key|button)\b/i;
|
|
445
|
+
const metaSaveRe = /\b(save|write)\s+(the\s+)?(changes|file)\b(?!.*\b(to|with|containing|including|featuring)\b)/i;
|
|
446
|
+
return tasks.filter(t => {
|
|
447
|
+
if (doneRe.test(t.goal))
|
|
448
|
+
return false;
|
|
449
|
+
if (metaSaveRe.test(t.goal))
|
|
450
|
+
return false;
|
|
451
|
+
if (t.goal.length < 5)
|
|
452
|
+
return false;
|
|
453
|
+
return true;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
static stripCodeBlocks(text) {
|
|
457
|
+
// Strip triple-backtick code fences (language identifier + code block)
|
|
458
|
+
let result = text.replace(/```[\s\S]*?```/g, '').trim();
|
|
459
|
+
// Unwrap inline backticks — keep the content (e.g. `src/pages/about.tsx` stays)
|
|
460
|
+
result = result.replace(/`([^`]+)`/g, '$1').trim();
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
truncateGoal(text) {
|
|
464
|
+
if (text.length <= 200)
|
|
465
|
+
return text;
|
|
466
|
+
return text.slice(0, 197) + '...';
|
|
467
|
+
}
|
|
468
|
+
parseDelegationTasks(plan, goal) {
|
|
307
469
|
const tasks = [];
|
|
470
|
+
const seenGoals = new Set();
|
|
308
471
|
const activeFilesText = this.toolContext.activeFiles.size > 0
|
|
309
472
|
? `Files in context: ${Array.from(this.toolContext.activeFiles.values()).join(', ')}`
|
|
310
473
|
: '';
|
|
474
|
+
const originalPaths = Orchestrator.extractFilePaths(goal);
|
|
475
|
+
const pathsBlock = originalPaths.length > 0
|
|
476
|
+
? `\nOriginal goal file paths (MUST preserve in subtask):\n${originalPaths.map(p => ` - ${p}`).join('\n')}\n`
|
|
477
|
+
: '';
|
|
478
|
+
const goalBlock = `Original goal: ${goal}\n`;
|
|
479
|
+
const baseCtx = activeFilesText + goalBlock + pathsBlock;
|
|
311
480
|
const lines = plan.split('\n');
|
|
312
481
|
let currentRole = '';
|
|
313
482
|
let currentGoal = '';
|
|
483
|
+
const pushTask = (role, goalText, ctx, depth) => {
|
|
484
|
+
const clean = Orchestrator.stripCodeBlocks(goalText) || goalText;
|
|
485
|
+
const goalKey = clean.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
486
|
+
if (seenGoals.has(goalKey))
|
|
487
|
+
return;
|
|
488
|
+
seenGoals.add(goalKey);
|
|
489
|
+
tasks.push({ goal: this.truncateGoal(clean || goalText), context: ctx, role, status: 'pending', splitDepth: depth });
|
|
490
|
+
};
|
|
491
|
+
const guessRole = (text) => {
|
|
492
|
+
const lower = text.toLowerCase();
|
|
493
|
+
const verifyRe = /\b(verify|check|test|review|inspect|validate|confirm|browser)\b/i;
|
|
494
|
+
const createRe = /\b(create|add|build|implement|write|generate|make|new)\b/i;
|
|
495
|
+
if (verifyRe.test(lower))
|
|
496
|
+
return 'reviewer';
|
|
497
|
+
if (createRe.test(lower))
|
|
498
|
+
return 'coder';
|
|
499
|
+
if (/\b(fix|debug|resolve|repair|patch)\b/i.test(lower))
|
|
500
|
+
return 'debugger';
|
|
501
|
+
return 'coder';
|
|
502
|
+
};
|
|
503
|
+
// Primary: explicit "delegate to" / role-prefixed lines
|
|
314
504
|
for (const line of lines) {
|
|
315
|
-
const roleMatch = line.match(/^\s*(?:-|\*|\d
|
|
505
|
+
const roleMatch = line.match(/^\s*(?:-|\*|\d+\.?)?\s*(?:delegate to|assign to|have|assign|role|agent:)?\s*(planner|coder|reviewer|debugger|researcher)\b/i);
|
|
316
506
|
if (roleMatch) {
|
|
317
507
|
if (currentRole && currentGoal) {
|
|
318
|
-
|
|
508
|
+
pushTask(currentRole, currentGoal, baseCtx, 0);
|
|
319
509
|
}
|
|
320
510
|
currentRole = roleMatch[1].toLowerCase();
|
|
321
511
|
const matchIndex = line.indexOf(roleMatch[0]);
|
|
@@ -328,14 +518,35 @@ export class Orchestrator {
|
|
|
328
518
|
}
|
|
329
519
|
}
|
|
330
520
|
if (currentRole && currentGoal) {
|
|
331
|
-
|
|
521
|
+
pushTask(currentRole, currentGoal, baseCtx, 0);
|
|
522
|
+
}
|
|
523
|
+
// Fallback: plain numbered/bulleted list without explicit roles
|
|
524
|
+
if (tasks.length === 0) {
|
|
525
|
+
for (const line of lines) {
|
|
526
|
+
const trimmed = line.trim();
|
|
527
|
+
if (!trimmed)
|
|
528
|
+
continue;
|
|
529
|
+
const itemMatch = trimmed.match(/^(?:-|\*|\d+\.)\s+(.+)$/);
|
|
530
|
+
if (itemMatch) {
|
|
531
|
+
const body = itemMatch[1];
|
|
532
|
+
if (body.length < 3)
|
|
533
|
+
continue;
|
|
534
|
+
const role = guessRole(body);
|
|
535
|
+
pushTask(role, body, baseCtx, 0);
|
|
536
|
+
}
|
|
537
|
+
else if (trimmed.length > 5) {
|
|
538
|
+
// Unformatted line — treat as a single coder task
|
|
539
|
+
pushTask('coder', trimmed, baseCtx, 0);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
332
542
|
}
|
|
333
543
|
if (tasks.length === 0) {
|
|
334
544
|
tasks.push({
|
|
335
|
-
goal:
|
|
336
|
-
context:
|
|
545
|
+
goal: this.truncateGoal(goal),
|
|
546
|
+
context: baseCtx,
|
|
337
547
|
role: 'coder',
|
|
338
|
-
status: 'pending'
|
|
548
|
+
status: 'pending',
|
|
549
|
+
splitDepth: 0,
|
|
339
550
|
});
|
|
340
551
|
}
|
|
341
552
|
return tasks;
|
|
@@ -395,7 +606,72 @@ export class Orchestrator {
|
|
|
395
606
|
return true;
|
|
396
607
|
return false;
|
|
397
608
|
}
|
|
398
|
-
async
|
|
609
|
+
async checkPlaceholders(historyStartIndex) {
|
|
610
|
+
const history = this.toolContext.patchHistory || [];
|
|
611
|
+
const placeholders = [];
|
|
612
|
+
for (let i = historyStartIndex; i < history.length; i++) {
|
|
613
|
+
const entry = history[i];
|
|
614
|
+
if (!entry.filePath)
|
|
615
|
+
continue;
|
|
616
|
+
try {
|
|
617
|
+
const content = fs.readFileSync(entry.filePath, 'utf8');
|
|
618
|
+
const lines = content.split('\n');
|
|
619
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
620
|
+
const bracketMatch = lines[lineIdx].match(PLACEHOLDER_RE);
|
|
621
|
+
if (bracketMatch) {
|
|
622
|
+
placeholders.push(`${entry.filePath}:${lineIdx + 1} — ${bracketMatch[0].trim()}`);
|
|
623
|
+
}
|
|
624
|
+
const htmlMatch = lines[lineIdx].match(HTML_PLACEHOLDER_RE);
|
|
625
|
+
if (htmlMatch) {
|
|
626
|
+
placeholders.push(`${entry.filePath}:${lineIdx + 1} — HTML comment placeholder`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
// file might have been deleted — skip
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return placeholders;
|
|
635
|
+
}
|
|
636
|
+
async fillPlaceholders(historyStartIndex) {
|
|
637
|
+
const history = this.toolContext.patchHistory || [];
|
|
638
|
+
let filled = 0;
|
|
639
|
+
const year = new Date().getFullYear().toString();
|
|
640
|
+
const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
641
|
+
let userName;
|
|
642
|
+
try {
|
|
643
|
+
userName = os.userInfo().username;
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
userName = 'user';
|
|
647
|
+
}
|
|
648
|
+
for (let i = historyStartIndex; i < history.length; i++) {
|
|
649
|
+
const entry = history[i];
|
|
650
|
+
if (!entry.filePath)
|
|
651
|
+
continue;
|
|
652
|
+
try {
|
|
653
|
+
const content = fs.readFileSync(entry.filePath, 'utf8');
|
|
654
|
+
const original = content;
|
|
655
|
+
let newContent = content
|
|
656
|
+
// Year/date — universally guessable
|
|
657
|
+
.replace(/\[(?:YEAR|Year|year|YYYY|yyyy)\]/g, year)
|
|
658
|
+
.replace(/\[(?:DATE|Date|date|TODAY|Today|today)\]/g, today)
|
|
659
|
+
// Name/author/owner — use system account name
|
|
660
|
+
.replace(/\[(?:YOUR\s+NAME|Your\s+Name|your\s+name|FULLNAME|Fullname|fullname|AUTHOR|Author|author|USERNAME|Username|username|OWNER|Owner|owner)\]/g, userName);
|
|
661
|
+
if (newContent !== content) {
|
|
662
|
+
fs.writeFileSync(entry.filePath, newContent, 'utf8');
|
|
663
|
+
const bracketCount = (content.match(/\[/g) || []).length;
|
|
664
|
+
const newBracketCount = (newContent.match(/\[/g) || []).length;
|
|
665
|
+
filled += bracketCount - newBracketCount;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// skip
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return filled;
|
|
673
|
+
}
|
|
674
|
+
async attemptRepair(task, previous, customContext) {
|
|
399
675
|
const role = getAgentRole(task.role);
|
|
400
676
|
const tools = filterToolsForRole(BUILTIN_TOOLS, task.role);
|
|
401
677
|
const maxRetries = 2;
|
|
@@ -407,7 +683,8 @@ export class Orchestrator {
|
|
|
407
683
|
}
|
|
408
684
|
attempt++;
|
|
409
685
|
console.log(`\n[REPAIR] Attempt ${attempt}/${maxRetries} to repair task: ${task.goal}`);
|
|
410
|
-
const
|
|
686
|
+
const baseCtx = customContext || task.context;
|
|
687
|
+
const repairContext = `${baseCtx}\n\nPrevious attempt failed verification. Output was:\n${currentSummary}\n\nPlease retry and ensure you actually write the required files/artifacts.`;
|
|
411
688
|
const historyStartIndex = this.toolContext.patchHistory?.length || 0;
|
|
412
689
|
const result = await this.runAgent(role, task.goal, repairContext, tools);
|
|
413
690
|
if (this.toolContext.abortSignal.aborted) {
|
|
@@ -433,19 +710,180 @@ export class Orchestrator {
|
|
|
433
710
|
return true;
|
|
434
711
|
return !result.toLowerCase().includes('implemented the full');
|
|
435
712
|
}
|
|
436
|
-
|
|
713
|
+
buildCleanSummary(task, result, historyStartIndex) {
|
|
714
|
+
const history = this.toolContext.patchHistory || [];
|
|
715
|
+
const newPatches = [];
|
|
716
|
+
for (let i = historyStartIndex; i < history.length; i++) {
|
|
717
|
+
newPatches.push(history[i]);
|
|
718
|
+
}
|
|
719
|
+
if (newPatches.length === 0 || result.split(/\s+/).length < 30)
|
|
720
|
+
return null;
|
|
721
|
+
const files = [...new Set(newPatches.map(p => p.filePath).filter(Boolean))];
|
|
722
|
+
if (files.length === 0)
|
|
723
|
+
return null;
|
|
724
|
+
return `Completed: ${task.goal} — Files: ${files.join(', ')}`;
|
|
725
|
+
}
|
|
726
|
+
static isUnnecessaryConfigTask(task, projectContext) {
|
|
727
|
+
if (!projectContext)
|
|
728
|
+
return false;
|
|
729
|
+
const goal = task.goal.toLowerCase();
|
|
730
|
+
const isNextJs = /\bNext\.js\b/i.test(projectContext);
|
|
731
|
+
const isVue = /\b(Vue|Nuxt)\b/i.test(projectContext);
|
|
732
|
+
if (isNextJs && /\bnext\.config\b/i.test(goal))
|
|
733
|
+
return true;
|
|
734
|
+
if (isVue && /\b(vue|nuxt)\.config\b/i.test(goal))
|
|
735
|
+
return true;
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
static getFrameworkGuidance(projectContext) {
|
|
739
|
+
if (!projectContext)
|
|
740
|
+
return '';
|
|
741
|
+
const ctx = projectContext;
|
|
742
|
+
if (/\bNext\.js\b/i.test(ctx)) {
|
|
743
|
+
return '\n\nIMPORTANT FRAMEWORK NOTE: Next.js uses file-based routing. Any .tsx or .js file added to pages/ or src/pages/ is automatically available as a web page. No edits to next.config.js or any other config file are needed when adding new pages. Do NOT create tasks for modifying config files.\nROUTING: Use next/link for client-side navigation. Do NOT use react-router-dom, vue-router, or any other router library.\n';
|
|
744
|
+
}
|
|
745
|
+
if (/\b(Vue|Nuxt)\b/i.test(ctx)) {
|
|
746
|
+
return '\n\nFRAMEWORK NOTE: This project uses Vue/Vue Router. New pages typically need route entries added to the router config, not config files like vue.config.js.\n';
|
|
747
|
+
}
|
|
748
|
+
if (/\bExpress\b/i.test(ctx)) {
|
|
749
|
+
return '\n\nFRAMEWORK NOTE: Express requires explicit route handlers. New endpoints must be added to the server/ or routes/ directory.\n';
|
|
750
|
+
}
|
|
751
|
+
return '';
|
|
752
|
+
}
|
|
753
|
+
static extractRequirements(text) {
|
|
754
|
+
if (!text)
|
|
755
|
+
return [];
|
|
756
|
+
const reqs = [];
|
|
757
|
+
const m = text.match(/(?:with|including|containing|that\s+(?:has|includes|contains|features?)|featuring)\s+(.+)/i);
|
|
758
|
+
if (!m)
|
|
759
|
+
return reqs;
|
|
760
|
+
const items = m[1]
|
|
761
|
+
.split(/\s*(?:,\s*|\sand\s|\s*&)\s*/)
|
|
762
|
+
.map(s => s.replace(/^(?:an?\s+|the\s+)/i, '').replace(/\.$/, '').trim())
|
|
763
|
+
.filter(Boolean);
|
|
764
|
+
const filler = new Set(['the following content', 'the specified content', 'appropriate content', 'content', 'your code']);
|
|
765
|
+
for (const item of items) {
|
|
766
|
+
const lower = item.toLowerCase();
|
|
767
|
+
if (!filler.has(lower) && lower.length > 2) {
|
|
768
|
+
reqs.push(item);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return reqs;
|
|
772
|
+
}
|
|
773
|
+
static extractFilePaths(text) {
|
|
774
|
+
if (!text)
|
|
775
|
+
return [];
|
|
776
|
+
const paths = [];
|
|
777
|
+
const re = /(?:\(|\[|\s|^)((?:[A-Za-z0-9_\-./\\]+[\\/])?[A-Za-z0-9_\-]+\.(?:tsx?|jsx?|vue|svelte|css|scss|json|md|csv|txt|yaml|yml|toml|py|rs|go|java|md))(?:[)\s,;]|$)/g;
|
|
778
|
+
let m;
|
|
779
|
+
while ((m = re.exec(text)) !== null) {
|
|
780
|
+
const p = m[1].replace(/\\/g, '/');
|
|
781
|
+
if (!p.startsWith('http') && !p.startsWith('node_modules') && p.length < 200) {
|
|
782
|
+
paths.push(p);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return [...new Set(paths)];
|
|
786
|
+
}
|
|
787
|
+
findStyleReference(taskGoal) {
|
|
788
|
+
// Extract target directory from goal
|
|
789
|
+
let dir = '';
|
|
790
|
+
// Try file path first: "src/pages/about.tsx" → "src/pages"
|
|
791
|
+
const fileMatch = taskGoal.match(/([A-Za-z0-9_\-/\\]+\.[a-zA-Z0-9]+)/);
|
|
792
|
+
if (fileMatch) {
|
|
793
|
+
dir = path.dirname(fileMatch[1].replace(/\\/g, '/'));
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// Try explicit directory: "in src/components/" or "at pages/"
|
|
797
|
+
const dirMatch = taskGoal.match(/(?:in|at|to|under|inside)\s+(?:the\s+)?([A-Za-z0-9_\-/\\]{2,})(?:\s+(?:directory|folder|path))?/i);
|
|
798
|
+
if (dirMatch) {
|
|
799
|
+
dir = dirMatch[1].replace(/\\/g, '/').replace(/\/+$/, '');
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (!dir || !fs.existsSync(dir) || dir === '.' || dir === '')
|
|
803
|
+
return null;
|
|
804
|
+
// Pick a non-test, non-hidden file as style reference
|
|
805
|
+
let entries;
|
|
806
|
+
try {
|
|
807
|
+
entries = fs.readdirSync(dir);
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
const candidates = entries
|
|
813
|
+
.filter(f => !f.startsWith('.') && !f.includes('.test.') && !f.includes('.spec.') && !f.startsWith('__'))
|
|
814
|
+
.sort();
|
|
815
|
+
const target = candidates.find(f => /\.(tsx?|jsx?|vue|svelte)$/i.test(f))
|
|
816
|
+
|| candidates.find(f => /\.(css|scss|less)$/i.test(f))
|
|
817
|
+
|| candidates[0];
|
|
818
|
+
if (!target)
|
|
819
|
+
return null;
|
|
820
|
+
const fullPath = path.join(dir, target);
|
|
821
|
+
let content;
|
|
822
|
+
try {
|
|
823
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
824
|
+
const lines = content.split('\n');
|
|
825
|
+
if (lines.length > 50) {
|
|
826
|
+
content = lines.slice(0, 50).join('\n') + '\n... (truncated)';
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
return `\nExisting file in ${dir}/ (use as a style reference):\n--- ${fullPath} ---\n${content}\n--- end ---`;
|
|
833
|
+
}
|
|
834
|
+
async delegateTask(task, tasks, goal, projectContext) {
|
|
437
835
|
const role = getAgentRole(task.role);
|
|
438
836
|
console.log(`\n[SPAWN] Delegating to ${role.name}: ${task.goal}`);
|
|
439
837
|
const tools = filterToolsForRole(BUILTIN_TOOLS, task.role);
|
|
440
838
|
const historyStartIndex = this.toolContext.patchHistory?.length || 0;
|
|
839
|
+
// Inject user metadata so the agent has real values instead of guessing
|
|
840
|
+
const currentDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
841
|
+
let userName;
|
|
842
|
+
try {
|
|
843
|
+
userName = os.userInfo().username;
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
userName = 'user';
|
|
847
|
+
}
|
|
848
|
+
const profile = loadProfile();
|
|
849
|
+
let enrichedContext = `Current date: ${currentDate}\n`;
|
|
850
|
+
if (profile.name) {
|
|
851
|
+
enrichedContext += `User name: ${profile.name}\n`;
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
enrichedContext += `System user: ${userName}\n`;
|
|
855
|
+
enrichedContext += `(Set up your profile with /profile to customize your name, style, and preferences)\n`;
|
|
856
|
+
}
|
|
857
|
+
// Build systemExtra with project context — system prompt is more authoritative than user message
|
|
858
|
+
let systemExtra = '';
|
|
859
|
+
if (projectContext) {
|
|
860
|
+
systemExtra = `Project context:\n${projectContext}\n\nIMPORTANT: Before creating new files, use read_file to examine existing project files and understand the structure. Follow the project framework conventions (e.g., Next.js pages go under pages/, Vue components under components/). Do not place files at the project root unless that is the correct convention for the framework.`;
|
|
861
|
+
}
|
|
441
862
|
// Surface relevant past lessons as context
|
|
442
863
|
const lessons = this.sessionManager ? this.sessionManager.getFailureLessons(task.role) : [];
|
|
443
|
-
|
|
864
|
+
enrichedContext += `\n${task.context}`;
|
|
444
865
|
if (lessons.length > 0) {
|
|
445
866
|
const lessonBlock = lessons.map(l => `[LESSON] Previously failed on: "${l.error_snippet}" -> resolution: ${l.resolution} (occurred ${l.used_count}x)`).join('\n');
|
|
446
|
-
enrichedContext = `${lessonBlock}\n\n${
|
|
867
|
+
enrichedContext = `${lessonBlock}\n\n${enrichedContext}`;
|
|
868
|
+
}
|
|
869
|
+
// Inject an existing file from the target dir as a style reference
|
|
870
|
+
const styleRef = this.findStyleReference(task.goal);
|
|
871
|
+
if (styleRef) {
|
|
872
|
+
enrichedContext += styleRef;
|
|
873
|
+
}
|
|
874
|
+
// Extract explicit requirements from task and original goal as a checklist
|
|
875
|
+
const taskReqs = Orchestrator.extractRequirements(task.goal);
|
|
876
|
+
const goalReqs = Orchestrator.extractRequirements(goal || '');
|
|
877
|
+
const allReqs = [...new Set([...taskReqs, ...goalReqs])];
|
|
878
|
+
if (allReqs.length > 0) {
|
|
879
|
+
enrichedContext += `\n\nRequirements (must implement each):\n${allReqs.map(r => ` - ${r}`).join('\n')}\n`;
|
|
880
|
+
}
|
|
881
|
+
// Extract explicit file paths from the goal and inject a scope boundary
|
|
882
|
+
const scopePaths = Orchestrator.extractFilePaths(task.goal);
|
|
883
|
+
if (scopePaths.length > 0) {
|
|
884
|
+
enrichedContext += `\n\nSCOPE BOUNDARY:\nYou must ONLY touch the following files:\n${scopePaths.map(p => ` - ${p}`).join('\n')}\n\nCRITICAL: Do NOT edit, create, rename, or delete any other files. Touching files outside this list is a hard failure. If the task implies modifying existing files that aren't in this list, STILL do not touch them unless they are required imports/support files directly referenced by the target file. When in doubt, do not modify a file.\n`;
|
|
447
885
|
}
|
|
448
|
-
let result = await this.runAgent(role, task.goal, enrichedContext, tools);
|
|
886
|
+
let result = await this.runAgent(role, task.goal, enrichedContext, tools, systemExtra);
|
|
449
887
|
if (this.toolContext.abortSignal.aborted) {
|
|
450
888
|
this.results.push({
|
|
451
889
|
role: task.role,
|
|
@@ -457,20 +895,117 @@ export class Orchestrator {
|
|
|
457
895
|
task.error = 'Task aborted by user';
|
|
458
896
|
return;
|
|
459
897
|
}
|
|
898
|
+
// Detect max turns — task too large, split remaining work into sub-tasks
|
|
899
|
+
const MAX_TURNS_SIGNAL = 'Agent reached max turns';
|
|
900
|
+
if (result === MAX_TURNS_SIGNAL) {
|
|
901
|
+
const partialWork = (this.toolContext.patchHistory?.length ?? 0) > historyStartIndex;
|
|
902
|
+
const depth = task.splitDepth ?? 0;
|
|
903
|
+
if (partialWork && depth < 3 && tasks) {
|
|
904
|
+
console.log(`\n${pc.yellow('[TASK TOO LARGE]')} Agent hit max turns with partial progress. Re-planned remaining work (depth ${depth + 1})...`);
|
|
905
|
+
// Clean up partial files — the exhausted agent left debris
|
|
906
|
+
const newPatches = this.toolContext.patchHistory?.slice(historyStartIndex) || [];
|
|
907
|
+
const filesDone = [...new Set(newPatches.map(p => p.filePath).filter(Boolean))];
|
|
908
|
+
for (const fp of filesDone) {
|
|
909
|
+
try {
|
|
910
|
+
if (fp && fs.existsSync(fp)) {
|
|
911
|
+
fs.unlinkSync(fp);
|
|
912
|
+
console.log(pc.gray(` Removed partial: ${path.basename(fp)}`));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
catch { /* race */ }
|
|
916
|
+
}
|
|
917
|
+
task.status = 'completed';
|
|
918
|
+
this.results.push({
|
|
919
|
+
role: task.role,
|
|
920
|
+
goal: task.goal,
|
|
921
|
+
summary: 'Partially completed — splitting remaining work into sub-tasks',
|
|
922
|
+
success: true,
|
|
923
|
+
});
|
|
924
|
+
const doneCtx = filesDone.length > 0 ? `\nPartially completed files: ${filesDone.join(', ')}` : '';
|
|
925
|
+
const subPlan = await this.createPlan(`Continue the remaining work for: ${task.goal}${doneCtx}\nThe previous agent only got partial work done before hitting the turn limit. Break this into smaller, focused steps.`);
|
|
926
|
+
const subTasks = this.parseDelegationTasks(subPlan, goal || task.goal);
|
|
927
|
+
const inheritCtx = filesDone.length > 0
|
|
928
|
+
? `Original goal: ${task.goal}\nProject root: ${process.cwd()}\n\nIMPORTANT — Files that were partially created and MUST be completed or replaced:\n${filesDone.map(f => ` - ${f}`).join('\n')}\n\nThe previous agent left these files incomplete before hitting the turn limit. You MUST read each file, then either complete it with a proper implementation or replace it entirely. Check the existing project structure first — use read_file on existing files to understand what's already there before creating new ones.`
|
|
929
|
+
: `Original goal: ${task.goal}\nProject root: ${process.cwd()}\n\nCheck the existing project structure first — use read_file on existing files to understand what's already there before creating new ones.`;
|
|
930
|
+
for (const st of subTasks) {
|
|
931
|
+
st.status = 'pending';
|
|
932
|
+
st.splitDepth = depth + 1;
|
|
933
|
+
st.context = `${inheritCtx}\n\n${st.context}`;
|
|
934
|
+
tasks.push(st);
|
|
935
|
+
}
|
|
936
|
+
this.printTaskList(tasks);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
task.status = 'failed';
|
|
940
|
+
task.error = partialWork
|
|
941
|
+
? `Task still too large after ${depth} splits — manual review needed`
|
|
942
|
+
: 'Task too large and no work completed';
|
|
943
|
+
this.results.push({
|
|
944
|
+
role: task.role,
|
|
945
|
+
goal: task.goal,
|
|
946
|
+
summary: task.error,
|
|
947
|
+
success: false,
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
460
951
|
let verified = await this.verifyArtifacts(task.role, task.goal, result, historyStartIndex);
|
|
461
952
|
let evidence = '';
|
|
953
|
+
let placeholderSites = [];
|
|
954
|
+
if (verified) {
|
|
955
|
+
placeholderSites = await this.checkPlaceholders(historyStartIndex);
|
|
956
|
+
if (placeholderSites.length > 0) {
|
|
957
|
+
console.log(pc.yellow(`\n[PLACEHOLDER] Found ${placeholderSites.length} placeholder(s) in written files`));
|
|
958
|
+
// Auto-fill trivial placeholders like [Year], [Your Name]
|
|
959
|
+
const filled = await this.fillPlaceholders(historyStartIndex);
|
|
960
|
+
if (filled > 0) {
|
|
961
|
+
console.log(pc.green(` Auto-filled ${filled} trivial placeholder(s) (year, name, etc.)`));
|
|
962
|
+
}
|
|
963
|
+
// Re-check for remaining (structural) placeholders
|
|
964
|
+
placeholderSites = await this.checkPlaceholders(historyStartIndex);
|
|
965
|
+
if (placeholderSites.length === 0) {
|
|
966
|
+
verified = true;
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
verified = false;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
462
973
|
if (!verified) {
|
|
974
|
+
let repairCtx = task.context;
|
|
975
|
+
if (placeholderSites.length > 0) {
|
|
976
|
+
const siteList = placeholderSites.map(s => ` - ${s}`).join('\n');
|
|
977
|
+
repairCtx += `\n\nPrevious attempt contained placeholders instead of real content:\n${siteList}\n\nYou MUST replace ALL placeholders with real content. Never output placeholder text like [Year], [Your Name], etc. Use actual values.`;
|
|
978
|
+
}
|
|
463
979
|
const repaired = await this.attemptRepair(task, {
|
|
464
980
|
role: task.role,
|
|
465
981
|
goal: task.goal,
|
|
466
982
|
summary: result,
|
|
467
983
|
success: false,
|
|
468
|
-
});
|
|
984
|
+
}, repairCtx);
|
|
469
985
|
result = repaired.summary;
|
|
470
986
|
verified = repaired.success;
|
|
471
987
|
evidence = repaired.evidence || '';
|
|
988
|
+
if (verified) {
|
|
989
|
+
const stillPlaceholders = await this.checkPlaceholders(historyStartIndex);
|
|
990
|
+
if (stillPlaceholders.length > 0) {
|
|
991
|
+
// Try auto-fill one more time after repair
|
|
992
|
+
const filled = await this.fillPlaceholders(historyStartIndex);
|
|
993
|
+
if (filled > 0)
|
|
994
|
+
console.log(pc.green(` Auto-filled ${filled} remaining trivial placeholder(s)`));
|
|
995
|
+
const remain = await this.checkPlaceholders(historyStartIndex);
|
|
996
|
+
if (remain.length > 0) {
|
|
997
|
+
verified = false;
|
|
998
|
+
evidence = `Placeholders remain: ${remain.join('; ')}`;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
472
1002
|
}
|
|
473
1003
|
const success = verified && !this.isDeclaredError(result) && this.verifyArtifactsThoroughly(task.role, task.goal, result);
|
|
1004
|
+
if (success) {
|
|
1005
|
+
const clean = this.buildCleanSummary(task, result, historyStartIndex);
|
|
1006
|
+
if (clean)
|
|
1007
|
+
result = clean;
|
|
1008
|
+
}
|
|
474
1009
|
task.status = success ? 'completed' : 'failed';
|
|
475
1010
|
if (!success) {
|
|
476
1011
|
task.error = result.split('\n')[0] || 'Unknown failure';
|
|
@@ -500,7 +1035,13 @@ export class Orchestrator {
|
|
|
500
1035
|
try {
|
|
501
1036
|
const reviewerRole = getAgentRole('reviewer');
|
|
502
1037
|
if (reviewerRole && !reviewerRole.canDelegate) {
|
|
503
|
-
const
|
|
1038
|
+
const touchedFiles = (this.toolContext.patchHistory || [])
|
|
1039
|
+
.filter((h) => h.filePath)
|
|
1040
|
+
.map((h) => h.filePath);
|
|
1041
|
+
const fileList = touchedFiles.length > 0
|
|
1042
|
+
? `\nFILES_TOUCHED: ${touchedFiles.join(', ')}`
|
|
1043
|
+
: '';
|
|
1044
|
+
const reviewContext = `TASK: ${task.goal}\n\nAgent result:\n${result}${fileList}\n\nReview the files that were touched for this task. Use git_diff or list files modified recently. Check for syntax errors, correctness, and project health.`;
|
|
504
1045
|
const reviewTools = filterToolsForRole(BUILTIN_TOOLS, 'reviewer');
|
|
505
1046
|
const review = await this.runAgent(reviewerRole, `Review files from task: ${task.goal}`, reviewContext, reviewTools);
|
|
506
1047
|
// Update project status from review
|
|
@@ -519,9 +1060,12 @@ export class Orchestrator {
|
|
|
519
1060
|
catch { /* review failed, non-critical */ }
|
|
520
1061
|
}
|
|
521
1062
|
}
|
|
522
|
-
async runAgent(role, goal, context, tools) {
|
|
1063
|
+
async runAgent(role, goal, context, tools, systemExtra) {
|
|
523
1064
|
const currentDateStr = new Date().toLocaleString();
|
|
524
|
-
|
|
1065
|
+
let dynamicSystemPrompt = `${role.systemPrompt}\n\n## CURRENT TIME\nThe current date and local time is: ${currentDateStr}.\n`;
|
|
1066
|
+
if (systemExtra) {
|
|
1067
|
+
dynamicSystemPrompt += `\n${systemExtra}\n`;
|
|
1068
|
+
}
|
|
525
1069
|
const messages = [
|
|
526
1070
|
{ role: 'system', content: dynamicSystemPrompt },
|
|
527
1071
|
{ role: 'user', content: `${context}\n\nTask: ${goal}` },
|