daedalus-cli 1.22.1 → 1.22.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.
@@ -1,30 +1,94 @@
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 plan = await this.createPlan(goal);
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
- const tasks = this.parseDelegationTasks(plan);
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 = this.parseDelegationTasks(plan, goal);
91
+ }
28
92
  if (this.sessionManager) {
29
93
  this.sessionManager.saveState('orchestrate_plan', tasks);
30
94
  this.sessionManager.saveState('orchestrate_goal', goal);
@@ -32,7 +96,7 @@ export class Orchestrator {
32
96
  this.sessionManager.saveState('orchestrate_results', []);
33
97
  this.sessionManager.saveState('orchestrate_plan_text', plan);
34
98
  }
35
- await this.executePlan(plan, tasks, 0);
99
+ await this.executePlan(plan, tasks, 0, goal, projectContext);
36
100
  }
37
101
  catch (err) {
38
102
  return `Orchestration failed: ${err.message}`;
@@ -48,6 +112,7 @@ export class Orchestrator {
48
112
  }
49
113
  async resume(goal, planText, tasks, startIndex, previousResults) {
50
114
  this.results = [...previousResults];
115
+ const projectContext = await this.discoverProjectContext();
51
116
  tasks.forEach((t, idx) => {
52
117
  if (idx < startIndex) {
53
118
  t.status = 'completed';
@@ -60,7 +125,7 @@ export class Orchestrator {
60
125
  if (this.toolContext.abortSignal.aborted) {
61
126
  return 'Orchestration stopped by user';
62
127
  }
63
- await this.executePlan(planText, tasks, startIndex);
128
+ await this.executePlan(planText, tasks, startIndex, goal, projectContext);
64
129
  }
65
130
  catch (err) {
66
131
  return `Orchestration failed: ${err.message}`;
@@ -74,62 +139,74 @@ export class Orchestrator {
74
139
  }
75
140
  return this.synthesize(goal);
76
141
  }
77
- async createPlan(goal) {
142
+ async createPlan(goal, projectContext) {
78
143
  const plannerRole = getAgentRole('planner');
79
144
  const tools = filterToolsForRole(BUILTIN_TOOLS, 'planner');
80
145
  const systemPrompt = plannerRole.systemPrompt;
81
- const messages = [
82
- { role: 'system', content: systemPrompt },
83
- { role: 'user', content: `Create a plan for: ${goal}\n\nContext:\n${this.toolContext.activeFiles.size > 0 ? 'Files in context: ' + Array.from(this.toolContext.activeFiles.values()).join(', ') : 'No files in context'}` },
84
- ];
85
- const planSpinner = new DaedalusSpinner({ text: 'planner generating plan', color: (s) => pc.cyan(s) });
86
- planSpinner.start();
87
- let completion;
88
- try {
89
- completion = await this.router.chat.completions.create({
90
- model: 'auto',
91
- messages,
92
- temperature: plannerRole.temperature ?? 0.2,
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;
146
+ const REFUSAL_RE = /sorry|can'?t|cannot|don'?t have|not (able|capable)|lack(|ing) (the )?(necessary |required )?(tools|capabilities)|unable|apologize/i;
147
+ let attempts = 0;
148
+ const maxAttempts = 2;
149
+ while (attempts < maxAttempts) {
150
+ attempts++;
151
+ const messages = [
152
+ { 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.' : '') },
153
+ { 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(', ') : ''}` },
154
+ ];
155
+ const planSpinner = new DaedalusSpinner({ text: `planner generating plan`, color: (s) => pc.cyan(s) });
156
+ planSpinner.start();
157
+ let completion;
118
158
  try {
119
- followUp = await this.router.chat.completions.create({
159
+ completion = await this.router.chat.completions.create({
120
160
  model: 'auto',
121
161
  messages,
122
162
  temperature: plannerRole.temperature ?? 0.2,
123
163
  tools,
124
- tool_choice: 'none', // No more tool calls — just produce the plan text
164
+ tool_choice: 'auto',
125
165
  });
126
166
  }
127
167
  finally {
128
- finalizeSpinner.stop();
168
+ planSpinner.stop();
169
+ }
170
+ const assistantMessage = completion.choices[0].message;
171
+ const toolCalls = assistantMessage.tool_calls;
172
+ if (toolCalls && toolCalls.length > 0) {
173
+ messages.push(assistantMessage);
174
+ const results = await this.executeOpenAIToolCalls(toolCalls);
175
+ for (const result of results) {
176
+ messages.push({
177
+ role: 'tool',
178
+ content: result.content,
179
+ tool_call_id: result.toolCallId,
180
+ });
181
+ }
182
+ const finalizeSpinner = new DaedalusSpinner({ text: 'planner finalizing plan', color: (s) => pc.cyan(s) });
183
+ finalizeSpinner.start();
184
+ let followUp;
185
+ try {
186
+ followUp = await this.router.chat.completions.create({
187
+ model: 'auto',
188
+ messages,
189
+ temperature: plannerRole.temperature ?? 0.2,
190
+ tools,
191
+ tool_choice: 'none',
192
+ });
193
+ }
194
+ finally {
195
+ finalizeSpinner.stop();
196
+ }
197
+ const content = (followUp.choices[0].message).content || '';
198
+ if (content && attempts < maxAttempts && REFUSAL_RE.test(content)) {
199
+ continue;
200
+ }
201
+ return content || `- delegate to coder: ${goal}`;
202
+ }
203
+ const content = assistantMessage.content || '';
204
+ if (content && attempts < maxAttempts && REFUSAL_RE.test(content)) {
205
+ continue;
129
206
  }
130
- return (followUp.choices[0].message).content || 'No plan generated';
207
+ return content || `- delegate to coder: ${goal}`;
131
208
  }
132
- return assistantMessage.content || 'No plan generated';
209
+ return `- delegate to coder: ${goal}`;
133
210
  }
134
211
  formatGoal(goal, indentLength, width = 80) {
135
212
  const words = goal.split(/\s+/);
@@ -175,20 +252,49 @@ export class Orchestrator {
175
252
  });
176
253
  console.log(pc.bold(pc.cyan('--------------------------------')));
177
254
  }
178
- async executePlan(plan, tasks, startIndex = 0) {
255
+ async executePlan(plan, tasks, startIndex = 0, originalGoal, projectContext) {
256
+ let lastReplanCount = 0;
179
257
  for (let i = startIndex; i < tasks.length; i++) {
180
258
  if (this.toolContext.abortSignal.aborted) {
181
259
  break;
182
260
  }
261
+ // Hard cap — stop adding new tasks past the limit
262
+ if (i >= this.MAX_TOTAL_TASKS && tasks.filter(t => t.status === 'pending').length > 0) {
263
+ console.log(pc.yellow(`\n[HALT] Reached ${this.MAX_TOTAL_TASKS} task limit. Halting new task generation.`));
264
+ while (tasks.length > this.MAX_TOTAL_TASKS) {
265
+ const removed = tasks.pop();
266
+ if (removed) {
267
+ removed.status = 'skipped';
268
+ removed.error = 'Reached task limit';
269
+ }
270
+ }
271
+ break;
272
+ }
183
273
  const task = tasks[i];
274
+ // Skip unnecessary config tasks for file-based routing frameworks
275
+ if (Orchestrator.isUnnecessaryConfigTask(task, projectContext)) {
276
+ console.log(pc.yellow(`\n[S] Task ${i + 1}: Skipped — Next.js uses file-based routing, no config changes needed`));
277
+ task.status = 'skipped';
278
+ task.error = 'Unnecessary config task for file-based routing framework';
279
+ continue;
280
+ }
184
281
  task.status = 'in_progress';
185
282
  this.printTaskList(tasks);
186
- await this.delegateTask(task);
283
+ await this.delegateTask(task, tasks, originalGoal, projectContext);
187
284
  this.printTaskList(tasks);
188
285
  if (this.sessionManager) {
189
286
  this.sessionManager.saveState('orchestrate_task_index', i + 1);
190
287
  this.sessionManager.saveState('orchestrate_results', this.results);
191
288
  }
289
+ // Re-plan checkpoint: after every REPLAN_INTERVAL completed tasks, re-evaluate
290
+ const completedCount = tasks.filter(t => t.status === 'completed').length;
291
+ if (completedCount > 0 && completedCount - lastReplanCount >= this.REPLAN_INTERVAL) {
292
+ const hasPending = tasks.some(t => t.status === 'pending' || t.status === 'in_progress');
293
+ if (hasPending && originalGoal) {
294
+ lastReplanCount = completedCount;
295
+ await this.replanRemaining(tasks, originalGoal, projectContext);
296
+ }
297
+ }
192
298
  if (task.status === 'failed') {
193
299
  console.log(`\n${pc.bold(pc.red('--- Task Failure Checkpoint ---'))}`);
194
300
  console.log(`${pc.red('[ERROR] Task failed:')} ${task.role} - ${task.goal}`);
@@ -198,6 +304,22 @@ export class Orchestrator {
198
304
  rl.question(p, (ans) => { rl.close(); resolve(ans); });
199
305
  });
200
306
  });
307
+ if (process.env.DAEDALUS_AUTO_APPROVE === 'true') {
308
+ console.log(pc.cyan(`\n[auto] Retrying task: ${task.goal}`));
309
+ task.status = 'in_progress';
310
+ task.error = undefined;
311
+ this.printTaskList(tasks);
312
+ this.results.pop();
313
+ await this.delegateTask(task, undefined, undefined, projectContext);
314
+ this.printTaskList(tasks);
315
+ if (task.status !== 'failed') {
316
+ continue;
317
+ }
318
+ console.log(pc.yellow(`\n[auto] Skipping failed task after retry: ${task.goal}`));
319
+ task.status = 'skipped';
320
+ this.printTaskList(tasks);
321
+ continue;
322
+ }
201
323
  let resolved = false;
202
324
  while (!resolved) {
203
325
  const answer = await ask(`\nTask failed. Choose action: [r]etry / [e]dit / [s]kip / [a]bort: `);
@@ -208,7 +330,7 @@ export class Orchestrator {
208
330
  task.error = undefined;
209
331
  this.printTaskList(tasks);
210
332
  this.results.pop();
211
- await this.delegateTask(task);
333
+ await this.delegateTask(task, undefined, undefined, projectContext);
212
334
  this.printTaskList(tasks);
213
335
  if (task.status !== 'failed') {
214
336
  resolved = true;
@@ -222,7 +344,7 @@ export class Orchestrator {
222
344
  task.error = undefined;
223
345
  this.printTaskList(tasks);
224
346
  this.results.pop();
225
- await this.delegateTask(task);
347
+ await this.delegateTask(task, undefined, undefined, projectContext);
226
348
  this.printTaskList(tasks);
227
349
  if (task.status !== 'failed') {
228
350
  resolved = true;
@@ -287,19 +409,72 @@ export class Orchestrator {
287
409
  }
288
410
  }
289
411
  }
290
- parseDelegationTasks(plan) {
412
+ async replanRemaining(tasks, originalGoal, projectContext) {
413
+ const done = tasks.filter(t => t.status === 'completed');
414
+ const pending = tasks.filter(t => t.status === 'pending');
415
+ if (pending.length === 0)
416
+ return;
417
+ const summary = done.map(t => {
418
+ const r = this.results.find(rr => rr.goal === t.goal && rr.role === t.role);
419
+ return r ? `[✓] [${t.role}] ${t.goal} → ${r.summary.split('\n')[0]}` : `[✓] [${t.role}] ${t.goal}`;
420
+ }).join('\n');
421
+ const remainingList = pending.map(t => `[ ] [${t.role}] ${t.goal}`).join('\n');
422
+ console.log(pc.cyan(`\n[RE-PLAN] ${pending.length} task(s) remaining. Re-evaluating based on completed work...`));
423
+ 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);
424
+ // Remove old pending tasks
425
+ for (let i = tasks.length - 1; i >= 0; i--) {
426
+ if (tasks[i].status === 'pending') {
427
+ tasks.splice(i, 1);
428
+ }
429
+ }
430
+ // Add new tasks from re-plan
431
+ const newTasks = this.parseDelegationTasks(subPlan, originalGoal);
432
+ for (const nt of newTasks) {
433
+ nt.splitDepth = 0;
434
+ tasks.push(nt);
435
+ }
436
+ this.printTaskList(tasks);
437
+ }
438
+ static stripCodeBlocks(text) {
439
+ // Strip triple-backtick code fences (language identifier + code block)
440
+ let result = text.replace(/```[\s\S]*?```/g, '').trim();
441
+ // Unwrap inline backticks — keep the content (e.g. `src/pages/about.tsx` stays)
442
+ result = result.replace(/`([^`]+)`/g, '$1').trim();
443
+ return result;
444
+ }
445
+ truncateGoal(text) {
446
+ if (text.length <= 200)
447
+ return text;
448
+ return text.slice(0, 197) + '...';
449
+ }
450
+ parseDelegationTasks(plan, goal) {
291
451
  const tasks = [];
452
+ const seenGoals = new Set();
292
453
  const activeFilesText = this.toolContext.activeFiles.size > 0
293
454
  ? `Files in context: ${Array.from(this.toolContext.activeFiles.values()).join(', ')}`
294
455
  : '';
456
+ const originalPaths = Orchestrator.extractFilePaths(goal);
457
+ const pathsBlock = originalPaths.length > 0
458
+ ? `\nOriginal goal file paths (MUST preserve in subtask):\n${originalPaths.map(p => ` - ${p}`).join('\n')}\n`
459
+ : '';
460
+ const goalBlock = `Original goal: ${goal}\n`;
461
+ const baseCtx = activeFilesText + goalBlock + pathsBlock;
295
462
  const lines = plan.split('\n');
296
463
  let currentRole = '';
297
464
  let currentGoal = '';
465
+ const pushTask = (role, goalText, ctx, depth) => {
466
+ const clean = Orchestrator.stripCodeBlocks(goalText) || goalText;
467
+ const goalKey = clean.trim().toLowerCase().replace(/\s+/g, ' ');
468
+ if (seenGoals.has(goalKey))
469
+ return;
470
+ seenGoals.add(goalKey);
471
+ tasks.push({ goal: this.truncateGoal(clean || goalText), context: ctx, role, status: 'pending', splitDepth: depth });
472
+ };
298
473
  for (const line of lines) {
299
474
  const roleMatch = line.match(/^\s*(?:-|\*|\d+\.)?\s*(?:delegate to|assign to|have|assign|role|agent:)?\s*(planner|coder|reviewer|debugger|researcher)\b/i);
300
475
  if (roleMatch) {
301
476
  if (currentRole && currentGoal) {
302
- tasks.push({ goal: currentGoal, context: activeFilesText, role: currentRole, status: 'pending' });
477
+ pushTask(currentRole, currentGoal, baseCtx, 0);
303
478
  }
304
479
  currentRole = roleMatch[1].toLowerCase();
305
480
  const matchIndex = line.indexOf(roleMatch[0]);
@@ -312,14 +487,15 @@ export class Orchestrator {
312
487
  }
313
488
  }
314
489
  if (currentRole && currentGoal) {
315
- tasks.push({ goal: currentGoal, context: activeFilesText, role: currentRole, status: 'pending' });
490
+ pushTask(currentRole, currentGoal, baseCtx, 0);
316
491
  }
317
492
  if (tasks.length === 0) {
318
493
  tasks.push({
319
- goal: plan,
320
- context: `Files in context: ${Array.from(this.toolContext.activeFiles.values()).join(', ')}`,
494
+ goal: this.truncateGoal(goal),
495
+ context: baseCtx,
321
496
  role: 'coder',
322
- status: 'pending'
497
+ status: 'pending',
498
+ splitDepth: 0,
323
499
  });
324
500
  }
325
501
  return tasks;
@@ -379,7 +555,72 @@ export class Orchestrator {
379
555
  return true;
380
556
  return false;
381
557
  }
382
- async attemptRepair(task, previous) {
558
+ async checkPlaceholders(historyStartIndex) {
559
+ const history = this.toolContext.patchHistory || [];
560
+ const placeholders = [];
561
+ for (let i = historyStartIndex; i < history.length; i++) {
562
+ const entry = history[i];
563
+ if (!entry.filePath)
564
+ continue;
565
+ try {
566
+ const content = fs.readFileSync(entry.filePath, 'utf8');
567
+ const lines = content.split('\n');
568
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
569
+ const bracketMatch = lines[lineIdx].match(PLACEHOLDER_RE);
570
+ if (bracketMatch) {
571
+ placeholders.push(`${entry.filePath}:${lineIdx + 1} — ${bracketMatch[0].trim()}`);
572
+ }
573
+ const htmlMatch = lines[lineIdx].match(HTML_PLACEHOLDER_RE);
574
+ if (htmlMatch) {
575
+ placeholders.push(`${entry.filePath}:${lineIdx + 1} — HTML comment placeholder`);
576
+ }
577
+ }
578
+ }
579
+ catch {
580
+ // file might have been deleted — skip
581
+ }
582
+ }
583
+ return placeholders;
584
+ }
585
+ async fillPlaceholders(historyStartIndex) {
586
+ const history = this.toolContext.patchHistory || [];
587
+ let filled = 0;
588
+ const year = new Date().getFullYear().toString();
589
+ const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
590
+ let userName;
591
+ try {
592
+ userName = os.userInfo().username;
593
+ }
594
+ catch {
595
+ userName = 'user';
596
+ }
597
+ for (let i = historyStartIndex; i < history.length; i++) {
598
+ const entry = history[i];
599
+ if (!entry.filePath)
600
+ continue;
601
+ try {
602
+ const content = fs.readFileSync(entry.filePath, 'utf8');
603
+ const original = content;
604
+ let newContent = content
605
+ // Year/date — universally guessable
606
+ .replace(/\[(?:YEAR|Year|year|YYYY|yyyy)\]/g, year)
607
+ .replace(/\[(?:DATE|Date|date|TODAY|Today|today)\]/g, today)
608
+ // Name/author/owner — use system account name
609
+ .replace(/\[(?:YOUR\s+NAME|Your\s+Name|your\s+name|FULLNAME|Fullname|fullname|AUTHOR|Author|author|USERNAME|Username|username|OWNER|Owner|owner)\]/g, userName);
610
+ if (newContent !== content) {
611
+ fs.writeFileSync(entry.filePath, newContent, 'utf8');
612
+ const bracketCount = (content.match(/\[/g) || []).length;
613
+ const newBracketCount = (newContent.match(/\[/g) || []).length;
614
+ filled += bracketCount - newBracketCount;
615
+ }
616
+ }
617
+ catch {
618
+ // skip
619
+ }
620
+ }
621
+ return filled;
622
+ }
623
+ async attemptRepair(task, previous, customContext) {
383
624
  const role = getAgentRole(task.role);
384
625
  const tools = filterToolsForRole(BUILTIN_TOOLS, task.role);
385
626
  const maxRetries = 2;
@@ -391,7 +632,8 @@ export class Orchestrator {
391
632
  }
392
633
  attempt++;
393
634
  console.log(`\n[REPAIR] Attempt ${attempt}/${maxRetries} to repair task: ${task.goal}`);
394
- const repairContext = `${task.context}\n\nPrevious attempt failed verification. Output was:\n${currentSummary}\n\nPlease retry and ensure you actually write the required files/artifacts.`;
635
+ const baseCtx = customContext || task.context;
636
+ const repairContext = `${baseCtx}\n\nPrevious attempt failed verification. Output was:\n${currentSummary}\n\nPlease retry and ensure you actually write the required files/artifacts.`;
395
637
  const historyStartIndex = this.toolContext.patchHistory?.length || 0;
396
638
  const result = await this.runAgent(role, task.goal, repairContext, tools);
397
639
  if (this.toolContext.abortSignal.aborted) {
@@ -417,19 +659,180 @@ export class Orchestrator {
417
659
  return true;
418
660
  return !result.toLowerCase().includes('implemented the full');
419
661
  }
420
- async delegateTask(task) {
662
+ buildCleanSummary(task, result, historyStartIndex) {
663
+ const history = this.toolContext.patchHistory || [];
664
+ const newPatches = [];
665
+ for (let i = historyStartIndex; i < history.length; i++) {
666
+ newPatches.push(history[i]);
667
+ }
668
+ if (newPatches.length === 0 || result.split(/\s+/).length < 30)
669
+ return null;
670
+ const files = [...new Set(newPatches.map(p => p.filePath).filter(Boolean))];
671
+ if (files.length === 0)
672
+ return null;
673
+ return `Completed: ${task.goal} — Files: ${files.join(', ')}`;
674
+ }
675
+ static isUnnecessaryConfigTask(task, projectContext) {
676
+ if (!projectContext)
677
+ return false;
678
+ const goal = task.goal.toLowerCase();
679
+ const isNextJs = /\bNext\.js\b/i.test(projectContext);
680
+ const isVue = /\b(Vue|Nuxt)\b/i.test(projectContext);
681
+ if (isNextJs && /\bnext\.config\b/i.test(goal))
682
+ return true;
683
+ if (isVue && /\b(vue|nuxt)\.config\b/i.test(goal))
684
+ return true;
685
+ return false;
686
+ }
687
+ static getFrameworkGuidance(projectContext) {
688
+ if (!projectContext)
689
+ return '';
690
+ const ctx = projectContext;
691
+ if (/\bNext\.js\b/i.test(ctx)) {
692
+ 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.\n';
693
+ }
694
+ if (/\b(Vue|Nuxt)\b/i.test(ctx)) {
695
+ 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';
696
+ }
697
+ if (/\bExpress\b/i.test(ctx)) {
698
+ return '\n\nFRAMEWORK NOTE: Express requires explicit route handlers. New endpoints must be added to the server/ or routes/ directory.\n';
699
+ }
700
+ return '';
701
+ }
702
+ static extractRequirements(text) {
703
+ if (!text)
704
+ return [];
705
+ const reqs = [];
706
+ const m = text.match(/(?:with|including|containing|that\s+(?:has|includes|contains|features?)|featuring)\s+(.+)/i);
707
+ if (!m)
708
+ return reqs;
709
+ const items = m[1]
710
+ .split(/\s*(?:,\s*|\sand\s|\s*&)\s*/)
711
+ .map(s => s.replace(/^(?:an?\s+|the\s+)/i, '').replace(/\.$/, '').trim())
712
+ .filter(Boolean);
713
+ const filler = new Set(['the following content', 'the specified content', 'appropriate content', 'content', 'your code']);
714
+ for (const item of items) {
715
+ const lower = item.toLowerCase();
716
+ if (!filler.has(lower) && lower.length > 2) {
717
+ reqs.push(item);
718
+ }
719
+ }
720
+ return reqs;
721
+ }
722
+ static extractFilePaths(text) {
723
+ if (!text)
724
+ return [];
725
+ const paths = [];
726
+ 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;
727
+ let m;
728
+ while ((m = re.exec(text)) !== null) {
729
+ const p = m[1].replace(/\\/g, '/');
730
+ if (!p.startsWith('http') && !p.startsWith('node_modules') && p.length < 200) {
731
+ paths.push(p);
732
+ }
733
+ }
734
+ return [...new Set(paths)];
735
+ }
736
+ findStyleReference(taskGoal) {
737
+ // Extract target directory from goal
738
+ let dir = '';
739
+ // Try file path first: "src/pages/about.tsx" → "src/pages"
740
+ const fileMatch = taskGoal.match(/([A-Za-z0-9_\-/\\]+\.[a-zA-Z0-9]+)/);
741
+ if (fileMatch) {
742
+ dir = path.dirname(fileMatch[1].replace(/\\/g, '/'));
743
+ }
744
+ else {
745
+ // Try explicit directory: "in src/components/" or "at pages/"
746
+ const dirMatch = taskGoal.match(/(?:in|at|to|under|inside)\s+(?:the\s+)?([A-Za-z0-9_\-/\\]{2,})(?:\s+(?:directory|folder|path))?/i);
747
+ if (dirMatch) {
748
+ dir = dirMatch[1].replace(/\\/g, '/').replace(/\/+$/, '');
749
+ }
750
+ }
751
+ if (!dir || !fs.existsSync(dir) || dir === '.' || dir === '')
752
+ return null;
753
+ // Pick a non-test, non-hidden file as style reference
754
+ let entries;
755
+ try {
756
+ entries = fs.readdirSync(dir);
757
+ }
758
+ catch {
759
+ return null;
760
+ }
761
+ const candidates = entries
762
+ .filter(f => !f.startsWith('.') && !f.includes('.test.') && !f.includes('.spec.') && !f.startsWith('__'))
763
+ .sort();
764
+ const target = candidates.find(f => /\.(tsx?|jsx?|vue|svelte)$/i.test(f))
765
+ || candidates.find(f => /\.(css|scss|less)$/i.test(f))
766
+ || candidates[0];
767
+ if (!target)
768
+ return null;
769
+ const fullPath = path.join(dir, target);
770
+ let content;
771
+ try {
772
+ content = fs.readFileSync(fullPath, 'utf8');
773
+ const lines = content.split('\n');
774
+ if (lines.length > 50) {
775
+ content = lines.slice(0, 50).join('\n') + '\n... (truncated)';
776
+ }
777
+ }
778
+ catch {
779
+ return null;
780
+ }
781
+ return `\nExisting file in ${dir}/ (use as a style reference):\n--- ${fullPath} ---\n${content}\n--- end ---`;
782
+ }
783
+ async delegateTask(task, tasks, goal, projectContext) {
421
784
  const role = getAgentRole(task.role);
422
785
  console.log(`\n[SPAWN] Delegating to ${role.name}: ${task.goal}`);
423
786
  const tools = filterToolsForRole(BUILTIN_TOOLS, task.role);
424
787
  const historyStartIndex = this.toolContext.patchHistory?.length || 0;
788
+ // Inject user metadata so the agent has real values instead of guessing
789
+ const currentDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
790
+ let userName;
791
+ try {
792
+ userName = os.userInfo().username;
793
+ }
794
+ catch {
795
+ userName = 'user';
796
+ }
797
+ const profile = loadProfile();
798
+ let enrichedContext = `Current date: ${currentDate}\n`;
799
+ if (profile.name) {
800
+ enrichedContext += `User name: ${profile.name}\n`;
801
+ }
802
+ else {
803
+ enrichedContext += `System user: ${userName}\n`;
804
+ enrichedContext += `(Set up your profile with /profile to customize your name, style, and preferences)\n`;
805
+ }
806
+ // Build systemExtra with project context — system prompt is more authoritative than user message
807
+ let systemExtra = '';
808
+ if (projectContext) {
809
+ 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.`;
810
+ }
425
811
  // Surface relevant past lessons as context
426
812
  const lessons = this.sessionManager ? this.sessionManager.getFailureLessons(task.role) : [];
427
- let enrichedContext = task.context;
813
+ enrichedContext += `\n${task.context}`;
428
814
  if (lessons.length > 0) {
429
815
  const lessonBlock = lessons.map(l => `[LESSON] Previously failed on: "${l.error_snippet}" -> resolution: ${l.resolution} (occurred ${l.used_count}x)`).join('\n');
430
- enrichedContext = `${lessonBlock}\n\n${task.context}`;
816
+ enrichedContext = `${lessonBlock}\n\n${enrichedContext}`;
431
817
  }
432
- let result = await this.runAgent(role, task.goal, enrichedContext, tools);
818
+ // Inject an existing file from the target dir as a style reference
819
+ const styleRef = this.findStyleReference(task.goal);
820
+ if (styleRef) {
821
+ enrichedContext += styleRef;
822
+ }
823
+ // Extract explicit requirements from task and original goal as a checklist
824
+ const taskReqs = Orchestrator.extractRequirements(task.goal);
825
+ const goalReqs = Orchestrator.extractRequirements(goal || '');
826
+ const allReqs = [...new Set([...taskReqs, ...goalReqs])];
827
+ if (allReqs.length > 0) {
828
+ enrichedContext += `\n\nRequirements (must implement each):\n${allReqs.map(r => ` - ${r}`).join('\n')}\n`;
829
+ }
830
+ // Extract explicit file paths from the goal and inject a scope boundary
831
+ const scopePaths = Orchestrator.extractFilePaths(task.goal);
832
+ if (scopePaths.length > 0) {
833
+ 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`;
834
+ }
835
+ let result = await this.runAgent(role, task.goal, enrichedContext, tools, systemExtra);
433
836
  if (this.toolContext.abortSignal.aborted) {
434
837
  this.results.push({
435
838
  role: task.role,
@@ -441,20 +844,117 @@ export class Orchestrator {
441
844
  task.error = 'Task aborted by user';
442
845
  return;
443
846
  }
847
+ // Detect max turns — task too large, split remaining work into sub-tasks
848
+ const MAX_TURNS_SIGNAL = 'Agent reached max turns';
849
+ if (result === MAX_TURNS_SIGNAL) {
850
+ const partialWork = (this.toolContext.patchHistory?.length ?? 0) > historyStartIndex;
851
+ const depth = task.splitDepth ?? 0;
852
+ if (partialWork && depth < 3 && tasks) {
853
+ console.log(`\n${pc.yellow('[TASK TOO LARGE]')} Agent hit max turns with partial progress. Re-planned remaining work (depth ${depth + 1})...`);
854
+ // Clean up partial files — the exhausted agent left debris
855
+ const newPatches = this.toolContext.patchHistory?.slice(historyStartIndex) || [];
856
+ const filesDone = [...new Set(newPatches.map(p => p.filePath).filter(Boolean))];
857
+ for (const fp of filesDone) {
858
+ try {
859
+ if (fp && fs.existsSync(fp)) {
860
+ fs.unlinkSync(fp);
861
+ console.log(pc.gray(` Removed partial: ${path.basename(fp)}`));
862
+ }
863
+ }
864
+ catch { /* race */ }
865
+ }
866
+ task.status = 'completed';
867
+ this.results.push({
868
+ role: task.role,
869
+ goal: task.goal,
870
+ summary: 'Partially completed — splitting remaining work into sub-tasks',
871
+ success: true,
872
+ });
873
+ const doneCtx = filesDone.length > 0 ? `\nPartially completed files: ${filesDone.join(', ')}` : '';
874
+ 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.`);
875
+ const subTasks = this.parseDelegationTasks(subPlan, goal || task.goal);
876
+ const inheritCtx = filesDone.length > 0
877
+ ? `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.`
878
+ : `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.`;
879
+ for (const st of subTasks) {
880
+ st.status = 'pending';
881
+ st.splitDepth = depth + 1;
882
+ st.context = `${inheritCtx}\n\n${st.context}`;
883
+ tasks.push(st);
884
+ }
885
+ this.printTaskList(tasks);
886
+ return;
887
+ }
888
+ task.status = 'failed';
889
+ task.error = partialWork
890
+ ? `Task still too large after ${depth} splits — manual review needed`
891
+ : 'Task too large and no work completed';
892
+ this.results.push({
893
+ role: task.role,
894
+ goal: task.goal,
895
+ summary: task.error,
896
+ success: false,
897
+ });
898
+ return;
899
+ }
444
900
  let verified = await this.verifyArtifacts(task.role, task.goal, result, historyStartIndex);
445
901
  let evidence = '';
902
+ let placeholderSites = [];
903
+ if (verified) {
904
+ placeholderSites = await this.checkPlaceholders(historyStartIndex);
905
+ if (placeholderSites.length > 0) {
906
+ console.log(pc.yellow(`\n[PLACEHOLDER] Found ${placeholderSites.length} placeholder(s) in written files`));
907
+ // Auto-fill trivial placeholders like [Year], [Your Name]
908
+ const filled = await this.fillPlaceholders(historyStartIndex);
909
+ if (filled > 0) {
910
+ console.log(pc.green(` Auto-filled ${filled} trivial placeholder(s) (year, name, etc.)`));
911
+ }
912
+ // Re-check for remaining (structural) placeholders
913
+ placeholderSites = await this.checkPlaceholders(historyStartIndex);
914
+ if (placeholderSites.length === 0) {
915
+ verified = true;
916
+ }
917
+ else {
918
+ verified = false;
919
+ }
920
+ }
921
+ }
446
922
  if (!verified) {
923
+ let repairCtx = task.context;
924
+ if (placeholderSites.length > 0) {
925
+ const siteList = placeholderSites.map(s => ` - ${s}`).join('\n');
926
+ 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.`;
927
+ }
447
928
  const repaired = await this.attemptRepair(task, {
448
929
  role: task.role,
449
930
  goal: task.goal,
450
931
  summary: result,
451
932
  success: false,
452
- });
933
+ }, repairCtx);
453
934
  result = repaired.summary;
454
935
  verified = repaired.success;
455
936
  evidence = repaired.evidence || '';
937
+ if (verified) {
938
+ const stillPlaceholders = await this.checkPlaceholders(historyStartIndex);
939
+ if (stillPlaceholders.length > 0) {
940
+ // Try auto-fill one more time after repair
941
+ const filled = await this.fillPlaceholders(historyStartIndex);
942
+ if (filled > 0)
943
+ console.log(pc.green(` Auto-filled ${filled} remaining trivial placeholder(s)`));
944
+ const remain = await this.checkPlaceholders(historyStartIndex);
945
+ if (remain.length > 0) {
946
+ verified = false;
947
+ evidence = `Placeholders remain: ${remain.join('; ')}`;
948
+ }
949
+ }
950
+ }
456
951
  }
457
952
  const success = verified && !this.isDeclaredError(result) && this.verifyArtifactsThoroughly(task.role, task.goal, result);
953
+ if (success) {
954
+ const clean = this.buildCleanSummary(task, result, historyStartIndex);
955
+ if (clean)
956
+ result = clean;
957
+ }
458
958
  task.status = success ? 'completed' : 'failed';
459
959
  if (!success) {
460
960
  task.error = result.split('\n')[0] || 'Unknown failure';
@@ -484,7 +984,13 @@ export class Orchestrator {
484
984
  try {
485
985
  const reviewerRole = getAgentRole('reviewer');
486
986
  if (reviewerRole && !reviewerRole.canDelegate) {
487
- const reviewContext = `TASK: ${task.goal}\n\nAgent result:\n${result}\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.`;
987
+ const touchedFiles = (this.toolContext.patchHistory || [])
988
+ .filter((h) => h.filePath)
989
+ .map((h) => h.filePath);
990
+ const fileList = touchedFiles.length > 0
991
+ ? `\nFILES_TOUCHED: ${touchedFiles.join(', ')}`
992
+ : '';
993
+ 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.`;
488
994
  const reviewTools = filterToolsForRole(BUILTIN_TOOLS, 'reviewer');
489
995
  const review = await this.runAgent(reviewerRole, `Review files from task: ${task.goal}`, reviewContext, reviewTools);
490
996
  // Update project status from review
@@ -503,9 +1009,12 @@ export class Orchestrator {
503
1009
  catch { /* review failed, non-critical */ }
504
1010
  }
505
1011
  }
506
- async runAgent(role, goal, context, tools) {
1012
+ async runAgent(role, goal, context, tools, systemExtra) {
507
1013
  const currentDateStr = new Date().toLocaleString();
508
- const dynamicSystemPrompt = `${role.systemPrompt}\n\n## CURRENT TIME\nThe current date and local time is: ${currentDateStr}.\n`;
1014
+ let dynamicSystemPrompt = `${role.systemPrompt}\n\n## CURRENT TIME\nThe current date and local time is: ${currentDateStr}.\n`;
1015
+ if (systemExtra) {
1016
+ dynamicSystemPrompt += `\n${systemExtra}\n`;
1017
+ }
509
1018
  const messages = [
510
1019
  { role: 'system', content: dynamicSystemPrompt },
511
1020
  { role: 'user', content: `${context}\n\nTask: ${goal}` },
@@ -575,8 +1084,18 @@ export class Orchestrator {
575
1084
  turns++;
576
1085
  continue;
577
1086
  }
578
- // No tool calls - agent is done
579
- return message.content || 'Agent completed without response';
1087
+ // No tool calls on this turn
1088
+ const responseText = message.content || '';
1089
+ // If tools were provided but the model refused to use them, give it a firm nudge
1090
+ if (tools.length > 0 && turns === 0 && /sorry|can'?t|cannot|don'?t have|not (able|capable)|lack(|ing) (the )?(necessary |required )?(tools|capabilities)|unable|apologize/i.test(responseText)) {
1091
+ messages.push({
1092
+ role: 'user',
1093
+ content: 'You have tools available to complete this task. Use read_file, write_file, search_files, terminal, and other tools as needed. Do not apologize or refuse — just use the tools to accomplish the task.',
1094
+ });
1095
+ turns++;
1096
+ continue;
1097
+ }
1098
+ return responseText || 'Agent completed without response';
580
1099
  }
581
1100
  return 'Agent reached max turns';
582
1101
  }