daedalus-cli 3.63.2 → 3.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/dist/agents/orchestrator.d.ts +3 -6
  3. package/dist/agents/orchestrator.d.ts.map +1 -1
  4. package/dist/agents/orchestrator.js +39 -843
  5. package/dist/agents/orchestrator.js.map +1 -1
  6. package/dist/agents/subagent-runner.d.ts +18 -0
  7. package/dist/agents/subagent-runner.d.ts.map +1 -0
  8. package/dist/agents/subagent-runner.js +231 -0
  9. package/dist/agents/subagent-runner.js.map +1 -0
  10. package/dist/agents/subagent-runner.test.d.ts +2 -0
  11. package/dist/agents/subagent-runner.test.d.ts.map +1 -0
  12. package/dist/agents/subagent-runner.test.js +74 -0
  13. package/dist/agents/subagent-runner.test.js.map +1 -0
  14. package/dist/agents/task-delegator.d.ts +28 -0
  15. package/dist/agents/task-delegator.d.ts.map +1 -0
  16. package/dist/agents/task-delegator.js +542 -0
  17. package/dist/agents/task-delegator.js.map +1 -0
  18. package/dist/agents/task-delegator.test.d.ts +2 -0
  19. package/dist/agents/task-delegator.test.d.ts.map +1 -0
  20. package/dist/agents/task-delegator.test.js +83 -0
  21. package/dist/agents/task-delegator.test.js.map +1 -0
  22. package/dist/agents/turn-guards.d.ts +41 -0
  23. package/dist/agents/turn-guards.d.ts.map +1 -0
  24. package/dist/agents/turn-guards.js +325 -0
  25. package/dist/agents/turn-guards.js.map +1 -0
  26. package/dist/agents/turn-guards.test.d.ts +2 -0
  27. package/dist/agents/turn-guards.test.d.ts.map +1 -0
  28. package/dist/agents/turn-guards.test.js +123 -0
  29. package/dist/agents/turn-guards.test.js.map +1 -0
  30. package/dist/commands/agents.d.ts +5 -28
  31. package/dist/commands/agents.d.ts.map +1 -1
  32. package/dist/commands/agents.js +11 -1021
  33. package/dist/commands/agents.js.map +1 -1
  34. package/dist/commands/autopilot.d.ts +30 -0
  35. package/dist/commands/autopilot.d.ts.map +1 -0
  36. package/dist/commands/autopilot.js +372 -0
  37. package/dist/commands/autopilot.js.map +1 -0
  38. package/dist/commands/hunt.d.ts +3 -0
  39. package/dist/commands/hunt.d.ts.map +1 -0
  40. package/dist/commands/hunt.js +181 -0
  41. package/dist/commands/hunt.js.map +1 -0
  42. package/dist/commands/mcp.d.ts +3 -0
  43. package/dist/commands/mcp.d.ts.map +1 -0
  44. package/dist/commands/mcp.js +339 -0
  45. package/dist/commands/mcp.js.map +1 -0
  46. package/dist/commands/onboard.d.ts +3 -0
  47. package/dist/commands/onboard.d.ts.map +1 -0
  48. package/dist/commands/onboard.js +109 -0
  49. package/dist/commands/onboard.js.map +1 -0
  50. package/dist/config/index.d.ts.map +1 -1
  51. package/dist/config/index.js +2 -7
  52. package/dist/config/index.js.map +1 -1
  53. package/dist/indexing/indexer.d.ts +1 -1
  54. package/dist/indexing/indexer.d.ts.map +1 -1
  55. package/dist/indexing/indexer.js +112 -89
  56. package/dist/indexing/indexer.js.map +1 -1
  57. package/dist/indexing/indexer.test.js +58 -2
  58. package/dist/indexing/indexer.test.js.map +1 -1
  59. package/dist/indexing/watcher.js +1 -1
  60. package/dist/indexing/watcher.js.map +1 -1
  61. package/dist/indexing/watcher.test.js +53 -16
  62. package/dist/indexing/watcher.test.js.map +1 -1
  63. package/dist/model.d.ts.map +1 -1
  64. package/dist/model.js +32 -426
  65. package/dist/model.js.map +1 -1
  66. package/dist/router/fallback.test.js +67 -0
  67. package/dist/router/fallback.test.js.map +1 -1
  68. package/dist/router/index.d.ts.map +1 -1
  69. package/dist/router/index.js +23 -2
  70. package/dist/router/index.js.map +1 -1
  71. package/dist/session/manager.d.ts +2 -2
  72. package/dist/session/manager.d.ts.map +1 -1
  73. package/dist/session/manager.js +9 -6
  74. package/dist/session/manager.js.map +1 -1
  75. package/dist/session/manager.test.js +5 -0
  76. package/dist/session/manager.test.js.map +1 -1
  77. package/dist/session/sqlite.d.ts +4 -0
  78. package/dist/session/sqlite.d.ts.map +1 -1
  79. package/dist/session/sqlite.js +17 -0
  80. package/dist/session/sqlite.js.map +1 -1
  81. package/dist/session/sqlite.test.js +10 -1
  82. package/dist/session/sqlite.test.js.map +1 -1
  83. package/dist/types.d.ts +17 -5
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/types.test.js +19 -0
  86. package/dist/types.test.js.map +1 -1
  87. package/package.json +2 -2
@@ -3,35 +3,30 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import readline from 'readline';
5
5
  import { BUILTIN_TOOLS } from '../tools/definitions.js';
6
- import { getResolvedShellType } from '../tools/builtin/terminal.js';
7
6
  import { mcpRegistry } from '../tools/mcp/registry.js';
8
- import { executeToolCalls } from '../tools/executor.js';
9
7
  import { getAgentRole, filterToolsForRole, roleLabel, resolveRoleKey } from './roles.js';
10
- import { VALID_AGENT_ROLES } from '../tools/builtin/handoff.js';
11
8
  import { messageText } from '../types.js';
12
9
  import pc from 'picocolors';
13
10
  import { DaedalusSpinner } from '../tools/daedalus-spinner.js';
14
11
  import { errMessage } from '../utils/errors.js';
15
- import { parseTextToolCalls } from '../formatting.js';
16
- import { filterValidTasks, validateTasks, cleanTaskText, cleanPlanOutput, truncateGoal, extractFilePaths, buildDependencyGraph, groupIndependent, planNamesTestFiles, isUnnecessaryConfigTask, extractRequirements, getFrameworkGuidance, orphanedModuleWarning, } from './orchestrator-validation.js';
17
- import { isDeclaredError, verifyArtifacts, verifyArtifactsThoroughly, checkPlaceholders, fillPlaceholders, buildCleanSummary, isBuildErrorRelated, generateBuildErrorHint, runBuildVerification, attemptRepair, rollbackTaskPatches, verifySpecAssertions, isRealFile, } from './orchestrator-verification.js';
18
- import { generateSpecContract, loadSpecContract, formatSpecForPromptSafe } from './spec.js';
19
- import { SigmaMemEngine } from '../session/sigma-mem.js';
20
- import { getSigmaMemories } from '../session/sqlite.js';
12
+ import { filterValidTasks, validateTasks, cleanTaskText, cleanPlanOutput, truncateGoal, extractFilePaths, buildDependencyGraph, groupIndependent, isUnnecessaryConfigTask, getFrameworkGuidance, } from './orchestrator-validation.js';
13
+ import { runBuildVerification, isRealFile, } from './orchestrator-verification.js';
14
+ import { generateSpecContract } from './spec.js';
21
15
  import { maskSecrets } from '../security/secret-detector.js';
22
- // Simplified placeholder regexes for common auto-fill tokens only
23
- // Simplified HTML placeholder regex for same tokens inside comments
16
+ import { TaskDelegator } from './task-delegator.js';
24
17
  export class Orchestrator {
25
18
  router;
26
19
  messages;
27
20
  toolContext;
28
- // Per-task tool context for the currently running sub-agent. allowTestEdits
29
- // is derived from the task goal (see runAgent) so a parent goal that merely
30
- // mentions "tests" does not disarm the test-suite lock for every sub-agent.
31
- subContext;
32
21
  sessionManager;
33
22
  modelOverride;
34
- results = [];
23
+ taskDelegator;
24
+ get results() {
25
+ return this.taskDelegator.results;
26
+ }
27
+ set results(val) {
28
+ this.taskDelegator.results = val;
29
+ }
35
30
  MAX_INITIAL_TASKS = 12;
36
31
  MAX_TOTAL_TASKS = 20;
37
32
  REPLAN_INTERVAL = 2;
@@ -41,33 +36,14 @@ export class Orchestrator {
41
36
  this.toolContext = toolContext;
42
37
  this.sessionManager = sessionManager;
43
38
  this.modelOverride = modelOverride;
39
+ this.taskDelegator = new TaskDelegator(this.router, this.toolContext, this.sessionManager, this.modelOverride, undefined, {
40
+ createPlan: (g, p) => this.createPlan(g, p),
41
+ parseDelegationTasks: (p, g) => this.parseDelegationTasks(p, g),
42
+ printTaskList: (t, f) => this.printTaskList(t, f),
43
+ });
44
44
  }
45
45
  async retryApiCall(fn, label, maxRetries = 2) {
46
- let lastErr;
47
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
48
- try {
49
- return await fn();
50
- }
51
- catch (err) {
52
- lastErr = err instanceof Error ? err : new Error(String(err));
53
- const msg = String(lastErr.message || lastErr);
54
- // If 400 Bad Request (e.g. model not in catalog) or 413 (payload too large), do not retry
55
- if (msg.includes('400') || msg.includes('not in the catalog') || msg.includes('413') || msg.includes('request entity too large')) {
56
- throw lastErr;
57
- }
58
- if (attempt < maxRetries) {
59
- // Check for explicit 429 backoff retry seconds (e.g. "Retry in 29s")
60
- const rateLimitMatch = msg.match(/Retry in (\d+)s/i);
61
- let delay = Math.min(1000 * Math.pow(2, attempt), 8000);
62
- if (rateLimitMatch && rateLimitMatch[1]) {
63
- delay = (parseInt(rateLimitMatch[1], 10) + 1) * 1000;
64
- }
65
- console.log(pc.yellow(`\n[API Backoff] ${label} failed — ${msg.split('\n')[0]}. Waiting ${Math.round(delay / 1000)}s before retry (attempt ${attempt + 1}/${maxRetries})...`));
66
- await new Promise(r => setTimeout(r, delay));
67
- }
68
- }
69
- }
70
- throw lastErr;
46
+ return this.taskDelegator.getSubAgentRunner().retryApiCall(fn, label, maxRetries);
71
47
  }
72
48
  async discoverProjectContext() {
73
49
  const cwd = this.toolContext.projectRoot || process.cwd();
@@ -78,46 +54,35 @@ export class Orchestrator {
78
54
  if (pkg.dependencies) {
79
55
  if (pkg.dependencies.next) {
80
56
  const rawVer = pkg.dependencies.next;
81
- // Strip semver range operators (^, ~, >=, <=, >, <) before parsing
82
57
  const cleaned = rawVer.replace(/^[^0-9]*/, '');
83
58
  const major = parseInt(cleaned.split('.')[0]);
84
- const label = Number.isFinite(major) ? `Next.js ${major} (React, SSR)` : 'Next.js (React, SSR)';
85
- info.push(label);
59
+ const isAppRouter = fs.existsSync(path.join(cwd, 'app'));
60
+ info.push(`Next.js (v${rawVer}${major >= 13 ? (isAppRouter ? ' App Router' : ' Pages Router') : ''})`);
86
61
  }
87
62
  else if (pkg.dependencies.react) {
88
- info.push('React');
63
+ info.push(`React (v${pkg.dependencies.react})`);
89
64
  }
90
65
  if (pkg.dependencies.vue)
91
- info.push('Vue');
92
- if (pkg.dependencies.express)
93
- info.push('Express');
94
- if (pkg.dependencies['@angular/core'])
95
- info.push('Angular');
96
- }
97
- if (pkg.devDependencies) {
98
- if (pkg.devDependencies.vite)
99
- info.push('Vite');
100
- if (pkg.devDependencies.typescript)
101
- info.push('TypeScript');
102
- if (pkg.devDependencies.tailwindcss)
66
+ info.push(`Vue (v${pkg.dependencies.vue})`);
67
+ if (pkg.dependencies.svelte)
68
+ info.push(`Svelte (v${pkg.dependencies.svelte})`);
69
+ if (pkg.dependencies.tailwindcss || pkg.devDependencies?.tailwindcss)
103
70
  info.push('Tailwind CSS');
71
+ if (pkg.dependencies['@prisma/client'] || pkg.devDependencies?.prisma)
72
+ info.push('Prisma ORM');
73
+ if (pkg.dependencies.drizzle || pkg.devDependencies?.['drizzle-orm'])
74
+ info.push('Drizzle ORM');
104
75
  }
105
- const scripts = pkg.scripts ? Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).join(', ') : 'none';
106
- parts.push(`Framework: ${info.join(' | ') || 'unknown'}`);
107
- parts.push(`Scripts: ${scripts}`);
76
+ if (info.length > 0)
77
+ parts.push(`Framework/Stack: ${info.join(', ')}`);
108
78
  }
109
- catch { /* not a node project */ }
79
+ catch { /* no package.json or invalid JSON */ }
110
80
  try {
111
- const entries = fs.readdirSync(cwd).filter(e => !e.startsWith('.') && e !== 'node_modules' && e !== 'ffmpeg');
112
- const dirs = entries.filter(e => fs.statSync(path.join(cwd, e)).isDirectory());
113
- const files = entries.filter(e => !fs.statSync(path.join(cwd, e)).isDirectory());
114
- if (dirs.length > 0)
115
- parts.push(`Directories: ${dirs.join(', ')}`);
116
- if (files.length > 0)
117
- parts.push(`Key files: ${files.join(', ')}`);
81
+ const entries = fs.readdirSync(cwd).filter(f => !f.startsWith('.') && f !== 'node_modules' && f !== 'dist');
82
+ parts.push(`Key directories: ${entries.slice(0, 8).join(', ')}`);
118
83
  }
119
- catch { /* not readable */ }
120
- return parts.length > 0 ? parts.join('\n') : '';
84
+ catch { /* unreadable directory */ }
85
+ return parts.join('\n');
121
86
  }
122
87
  async run(goal) {
123
88
  this.results = [];
@@ -138,7 +103,6 @@ export class Orchestrator {
138
103
  return 'Orchestration stopped by user';
139
104
  }
140
105
  let tasks = this.parseDelegationTasks(plan, goal);
141
- // Cap initial tasks — re-plan if the planner gets carried away
142
106
  if (tasks.length > this.MAX_INITIAL_TASKS) {
143
107
  console.log(pc.yellow(`\nPlan has ${tasks.length} steps (max ${this.MAX_INITIAL_TASKS}). Asking planner to simplify...`));
144
108
  plan = await this.createPlan(goal, projectContext, `Simplify to at most ${this.MAX_INITIAL_TASKS} focused steps. Merge related steps. Each step must produce real output.`);
@@ -153,7 +117,6 @@ export class Orchestrator {
153
117
  if (tasks.length === 0) {
154
118
  return `Orchestration failed: planning produced no executable tasks for goal: ${goal}. The planner returned an empty or unparseable plan (delegate-to lines must name a known agent role, e.g. "delegate to Hephaestus: ...").`;
155
119
  }
156
- // Pre-Flight Codebase Audit: Check if workspace has pre-existing compilation/build errors
157
120
  const preFlight = await runBuildVerification(this.toolContext, 0);
158
121
  const isNoInputsErr = preFlight.errorLogs && (preFlight.errorLogs.includes('TS18003') || preFlight.errorLogs.includes('No inputs were found'));
159
122
  if (!preFlight.success && preFlight.errorLogs && !isNoInputsErr) {
@@ -191,7 +154,6 @@ export class Orchestrator {
191
154
  async resume(goal, planText, tasks, startIndex, previousResults) {
192
155
  this.results = [...previousResults];
193
156
  const projectContext = await this.discoverProjectContext();
194
- // Reset abort signal so resume works after a pause/crash
195
157
  Object.defineProperty(this.toolContext.abortSignal, 'aborted', { value: false, writable: true });
196
158
  tasks.forEach((t, idx) => {
197
159
  if (idx < startIndex) {
@@ -316,7 +278,6 @@ export class Orchestrator {
316
278
  continue;
317
279
  planText = content;
318
280
  }
319
- // Validate the plan
320
281
  const testTasks = this.parseDelegationTasks(planText || `- delegate to ${roleLabel('coder')}: ${goal}`, goal);
321
282
  const validationError = validateTasks(testTasks, goal, this.toolContext.projectRoot);
322
283
  if (!validationError) {
@@ -365,14 +326,6 @@ export class Orchestrator {
365
326
  return tasks.join('\n');
366
327
  }
367
328
  }
368
- // If explicit target files are listed in the goal, split into file-focused tasks in fallback mode.
369
- // Match paths like src/foo.ts, tests/foo.test.ts, public/index.html. Require a real extension and
370
- // stop at whitespace/punctuation so "...in src/server.ts." yields "src/server.ts", not "src/server.ts.".
371
- // IMPORTANT: references inside constraint phrases ("do not modify src/server.ts", "don't touch
372
- // src/server.ts", "without changing src/server.ts", "leave src/server.ts alone", "existing
373
- // endpoints in src/server.ts") must NOT become create/update targets — otherwise the planner spawns
374
- // a mutation task for a file the user explicitly wants left untouched, which collides with the real
375
- // file and confuses the coder. Strip those negative clauses first.
376
329
  const constraintRe = /(?:do\s+not\s+modify|don'?t\s+(?:touch|modify|change)|without\s+(?:changing|modifying)|leave\s+\S*\s+alone|existing\s+(?:endpoints?|files?|routes?)\s+(?:in|at)|keep\s+\S*\s+unchanged)[^.;]*?(?:src|tests|public|app|pages)\/[a-zA-Z0-9_\-]+\.[a-zA-Z0-9]+/gi;
377
330
  const constraintPaths = new Set((goal.match(constraintRe) || [])
378
331
  .map((m) => (m.match(/(?:src|tests|public|app|pages)\/[a-zA-Z0-9_\-]+\.[a-zA-Z0-9]+/) || [])[0])
@@ -421,7 +374,6 @@ export class Orchestrator {
421
374
  const running = tasks.filter(t => t.status === 'in_progress').length;
422
375
  const failed = tasks.filter(t => t.status === 'failed').length;
423
376
  const total = tasks.length;
424
- // Only print full task list on initial plan, re-plan, or forced summary
425
377
  if (!forceFull && (running > 0 || completed > 0)) {
426
378
  const activeTask = tasks.find(t => t.status === 'in_progress');
427
379
  const activeText = activeTask ? ` | Active: [${roleLabel(activeTask.role)}] ${activeTask.goal.slice(0, 50)}...` : '';
@@ -461,19 +413,17 @@ export class Orchestrator {
461
413
  const isPending = t.status === undefined || t.status === 'pending';
462
414
  if (!isPending)
463
415
  continue;
464
- // Dependencies satisfied = all dependency goals are completed
465
416
  if (t.dependencies && t.dependencies.length > 0) {
466
417
  const allDone = t.dependencies.every(dep => tasks.some(other => other.goal === dep && (other.status === 'completed' || other.status === 'skipped')));
467
418
  if (!allDone)
468
419
  continue;
469
420
  }
470
421
  batch.push(t);
471
- // In auto-approve, gather at most 2 concurrent tasks to prevent API rate-limit storms
472
422
  if (process.env.DAEDALUS_AUTO_APPROVE === 'true' && batch.length < 2) {
473
423
  continue;
474
424
  }
475
425
  else {
476
- break; // max 2 concurrent or sequential
426
+ break;
477
427
  }
478
428
  }
479
429
  return batch;
@@ -483,7 +433,6 @@ export class Orchestrator {
483
433
  this.printTaskList(tasks);
484
434
  await this.delegateTask(task, tasks, originalGoal, projectContext);
485
435
  this.printTaskList(tasks);
486
- // Handle task failure with auto-retry in auto-approve mode
487
436
  if (task.status === 'failed') {
488
437
  console.log(`\n${pc.bold(pc.red('--- Task Failure Checkpoint ---'))}`);
489
438
  console.log(`${pc.red('[ERROR] Task failed:')} ${task.role} - ${task.goal}`);
@@ -558,13 +507,11 @@ export class Orchestrator {
558
507
  }
559
508
  async executePlan(plan, tasks, startIndex = 0, originalGoal, projectContext) {
560
509
  let lastReplanCount = 0;
561
- // Build dependency graph from file paths
562
510
  buildDependencyGraph(tasks);
563
511
  for (let i = startIndex; i < tasks.length; /* increment inside */) {
564
512
  if (this.toolContext.abortSignal.aborted) {
565
513
  break;
566
514
  }
567
- // Hard cap — stop adding new tasks past the limit
568
515
  if (i >= this.MAX_TOTAL_TASKS && tasks.filter(t => t.status === 'pending').length > 0) {
569
516
  console.log(pc.yellow(`\nReached task limit (${this.MAX_TOTAL_TASKS}). Halting new task generation.`));
570
517
  while (tasks.length > this.MAX_TOTAL_TASKS) {
@@ -577,12 +524,10 @@ export class Orchestrator {
577
524
  break;
578
525
  }
579
526
  const task = tasks[i];
580
- // Skip already completed/skipped tasks
581
527
  if (task.status === 'completed' || task.status === 'skipped') {
582
528
  i++;
583
529
  continue;
584
530
  }
585
- // Skip unnecessary config tasks for file-based routing frameworks
586
531
  if (isUnnecessaryConfigTask(task, projectContext)) {
587
532
  console.log(pc.yellow(`\nSkipping task ${i + 1}: Next.js uses file-based routing — no config changes needed`));
588
533
  task.status = 'skipped';
@@ -590,14 +535,11 @@ export class Orchestrator {
590
535
  i++;
591
536
  continue;
592
537
  }
593
- // Get the next batch of runnable tasks
594
538
  const batch = this.getNextBatch(tasks, i);
595
539
  if (batch.length === 0) {
596
- // Deadlock or no pending tasks — advance past completed/skipped
597
540
  i++;
598
541
  continue;
599
542
  }
600
- // In auto-approve mode, run independent tasks concurrently
601
543
  if (process.env.DAEDALUS_AUTO_APPROVE === 'true') {
602
544
  const groups = groupIndependent(batch);
603
545
  for (const group of groups) {
@@ -605,32 +547,26 @@ export class Orchestrator {
605
547
  }
606
548
  }
607
549
  else {
608
- // Interactive mode: sequential only
609
550
  for (const t of batch) {
610
551
  await this.executeSingleTask(t, tasks, originalGoal, projectContext);
611
552
  }
612
553
  }
613
- // Advance past all tasks that were just processed (no longer pending)
614
554
  while (i < tasks.length && tasks[i].status !== 'pending') {
615
555
  i++;
616
556
  }
617
- // Save state after completing tasks
618
557
  if (this.sessionManager) {
619
558
  this.sessionManager.saveState('orchestrate_task_index', i);
620
559
  this.sessionManager.saveState('orchestrate_results', this.results);
621
560
  }
622
- // Re-plan checkpoint: after every REPLAN_INTERVAL completed tasks, re-evaluate
623
561
  const completedCount = tasks.filter(t => t.status === 'completed').length;
624
562
  if (completedCount > 0 && completedCount - lastReplanCount >= this.REPLAN_INTERVAL) {
625
563
  const hasPending = tasks.some(t => t.status === 'pending' || t.status === 'in_progress');
626
564
  if (hasPending && originalGoal) {
627
565
  lastReplanCount = completedCount;
628
566
  await this.replanRemaining(tasks, originalGoal, projectContext);
629
- // Rebuild dependency graph after replan
630
567
  buildDependencyGraph(tasks);
631
568
  }
632
569
  }
633
- // Interactive checkpoint: ask user before next task
634
570
  if (process.env.DAEDALUS_AUTO_APPROVE !== 'true' && i < tasks.length) {
635
571
  const prevTask = tasks[i - 1];
636
572
  if (prevTask && prevTask.status === 'completed') {
@@ -671,11 +607,6 @@ export class Orchestrator {
671
607
  }
672
608
  }
673
609
  }
674
- // Guard against phantom success: if no task was ever delegated/executed
675
- // (all skipped, or the plan collapsed to zero runnable tasks), surface a
676
- // failure instead of letting run() print "Orchestration Complete" with no
677
- // artifacts. Observed in the wild: a fallback plan parsed to tasks but the
678
- // execution loop produced nothing, yet the run reported success.
679
610
  const executed = tasks.filter(t => t.status === 'completed' || t.status === 'in_progress').length;
680
611
  const anyDelegated = (this.results?.length ?? 0) > 0;
681
612
  if (executed === 0 && !anyDelegated) {
@@ -699,19 +630,16 @@ export class Orchestrator {
699
630
  const paths = extractFilePaths(r.summary);
700
631
  return paths;
701
632
  });
702
- // Save original pending tasks as fallback in case replan fails
703
633
  const originalPending = tasks
704
634
  .filter(t => t.status === 'pending')
705
635
  .map(t => ({ ...t }));
706
636
  console.log(pc.cyan(`\n[RE-PLAN] ${pending.length} task(s) remaining. Re-evaluating based on completed work...`));
707
637
  const subPlan = await this.createPlan(`Original goal: ${originalGoal}\n\nCompleted so far:\n${summary}\n\nFiles already written: ${completedFiles.length > 0 ? completedFiles.join(', ') : '(none)'}\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. Do NOT repeat tasks that are already done. Do NOT re-create files that already exist. Each remaining step must produce real output that has not been created yet.`, projectContext);
708
- // Remove old pending tasks
709
638
  for (let i = tasks.length - 1; i >= 0; i--) {
710
639
  if (tasks[i].status === 'pending') {
711
640
  tasks.splice(i, 1);
712
641
  }
713
642
  }
714
- // Add new tasks from re-plan, dropping duplicates against completed work
715
643
  let newTasks = this.parseDelegationTasks(subPlan, originalGoal);
716
644
  newTasks = newTasks.filter(nt => {
717
645
  if (done.length === 0 || nt.role !== 'coder')
@@ -719,14 +647,11 @@ export class Orchestrator {
719
647
  const newPaths = extractFilePaths(nt.goal).map(p => p.toLowerCase());
720
648
  if (newPaths.length === 0)
721
649
  return true;
722
- // Only drop task if ALL mentioned files are REAL files on disk (>100 bytes, no placeholder comment shells)
723
650
  const root = this.toolContext.projectRoot || process.cwd();
724
651
  const allExist = newPaths.every(p => isRealFile(path.resolve(root, p)));
725
652
  return !allExist;
726
653
  });
727
- // Enforce task cap and filter out non-actionable tasks
728
654
  newTasks = filterValidTasks(newTasks).slice(0, this.MAX_INITIAL_TASKS);
729
- // Fallback: if replan produced no valid tasks, restore original pending tasks
730
655
  if (newTasks.length === 0 && originalPending.length > 0) {
731
656
  for (const t of originalPending) {
732
657
  t.status = 'pending';
@@ -777,7 +702,6 @@ export class Orchestrator {
777
702
  return 'debugger';
778
703
  return 'coder';
779
704
  };
780
- // Primary: explicit "delegate to" / role-prefixed lines
781
705
  for (const line of lines) {
782
706
  const trimmedLine = line.trim();
783
707
  if (/^\s*tools?\s*used\b/i.test(trimmedLine))
@@ -797,7 +721,6 @@ export class Orchestrator {
797
721
  currentGoal = goalPart.trim();
798
722
  }
799
723
  else if (currentRole && trimmedLine) {
800
- // Only merge continuation lines that look like task detail, not standalone commentary
801
724
  const isCommentary = trimmedLine.length > 60 && /^[A-Z]/.test(trimmedLine) && !/^(and|or|with|using|that|which|to|for|in|on|at)\b/i.test(trimmedLine);
802
725
  if (!isCommentary) {
803
726
  currentGoal += ' ' + trimmedLine;
@@ -807,7 +730,6 @@ export class Orchestrator {
807
730
  if (currentRole && currentGoal) {
808
731
  pushTask(currentRole, currentGoal, baseCtx, 0);
809
732
  }
810
- // Fallback: plain numbered/bulleted list without explicit roles
811
733
  if (tasks.length === 0) {
812
734
  for (const line of lines) {
813
735
  const trimmed = line.trim();
@@ -822,7 +744,6 @@ export class Orchestrator {
822
744
  pushTask(role, body, baseCtx, 0);
823
745
  }
824
746
  else if (trimmed.length > 10) {
825
- // Only accept unformatted lines that contain an action verb
826
747
  const hasActionVerb = /\b(create|write|build|implement|update|add|fix|generate|install|setup|configure|refactor|move|delete|rename)\b/i.test(trimmed);
827
748
  if (hasActionVerb) {
828
749
  pushTask('coder', trimmed, baseCtx, 0);
@@ -831,7 +752,6 @@ export class Orchestrator {
831
752
  }
832
753
  }
833
754
  if (tasks.length === 0) {
834
- // Auto-extract multiple file mentions if present in goal (e.g. public/index.html, src/server.ts)
835
755
  const fileMatches = Array.from(goal.matchAll(/([A-Za-z0-9_\-/\\]+\.[a-zA-Z0-9]+)/g)).map(m => m[1]);
836
756
  const uniqueFiles = Array.from(new Set(fileMatches)).filter(f => !f.endsWith('.md') && !f.endsWith('.json'));
837
757
  if (uniqueFiles.length > 1) {
@@ -851,738 +771,14 @@ export class Orchestrator {
851
771
  }
852
772
  return tasks;
853
773
  }
854
- findStyleReference(taskGoal) {
855
- // Extract target directory from goal
856
- let dir = '';
857
- const fileMatch = taskGoal.match(/([A-Za-z0-9_\-/\\]+\.[a-zA-Z0-9]+)/);
858
- if (fileMatch) {
859
- dir = path.dirname(fileMatch[1].replace(/\\/g, '/'));
860
- }
861
- else {
862
- const dirMatch = taskGoal.match(/(?:in|at|to|under|inside)\s+(?:the\s+)?([A-Za-z0-9_\-/\\]{2,})(?:\s+(?:directory|folder|path))?/i);
863
- if (dirMatch) {
864
- dir = dirMatch[1].replace(/\\/g, '/').replace(/\/+$/, '');
865
- }
866
- }
867
- const checkDirForReference = (searchDir) => {
868
- if (!searchDir || !fs.existsSync(searchDir) || searchDir === '.')
869
- return null;
870
- let entries;
871
- try {
872
- entries = fs.readdirSync(searchDir);
873
- }
874
- catch {
875
- return null;
876
- }
877
- const candidates = entries
878
- .filter(f => !f.startsWith('.') && !f.includes('.test.') && !f.includes('.spec.') && !f.startsWith('__'))
879
- .sort();
880
- const target = candidates.find(f => /\.(tsx?|jsx?|vue|svelte)$/i.test(f))
881
- || candidates.find(f => /\.(css|scss|less)$/i.test(f))
882
- || candidates[0];
883
- if (!target)
884
- return null;
885
- const fullPath = path.join(searchDir, target);
886
- if (fs.statSync(fullPath).isDirectory())
887
- return null;
888
- try {
889
- let content = fs.readFileSync(fullPath, 'utf8');
890
- const lines = content.split('\n');
891
- if (lines.length > 80) {
892
- content = lines.slice(0, 80).join('\n') + '\n... (truncated)';
893
- }
894
- const isAppRouter = fs.existsSync(path.join(this.toolContext.projectRoot || process.cwd(), 'app'));
895
- const antiPatterns = [
896
- /legacyBehavior/,
897
- /^\s*<[A-Za-z]/m,
898
- ];
899
- if (isAppRouter) {
900
- antiPatterns.push(/import\s+React\s+from\s+['"]react['"]/);
901
- }
902
- if (antiPatterns.some(re => re.test(content)))
903
- return null;
904
- return { fullPath, content };
905
- }
906
- catch {
907
- return null;
908
- }
909
- };
910
- // 1. Try target directory
911
- let ref = checkDirForReference(dir);
912
- if (ref) {
913
- return `\nExisting file in ${dir}/ (use as a style reference for structure and import order only):\n--- ${ref.fullPath} ---\n${ref.content}\n--- end ---`;
914
- }
915
- // 2. Try parent directory if target directory doesn't have reference
916
- if (dir && dir !== '.' && dir !== '') {
917
- const parentDir = path.dirname(dir);
918
- ref = checkDirForReference(parentDir);
919
- if (ref) {
920
- return `\nExisting file in sibling/parent ${parentDir}/ (use as a style reference for structure and import order only):\n--- ${ref.fullPath} ---\n${ref.content}\n--- end ---`;
921
- }
922
- }
923
- // 3. Try common directories in the project
924
- const commonDirs = ['src/components', 'components', 'src/pages', 'pages', 'app', 'src', 'lib'];
925
- for (const commonDir of commonDirs) {
926
- const fullCommonDir = path.join(this.toolContext.projectRoot || process.cwd(), commonDir);
927
- ref = checkDirForReference(fullCommonDir);
928
- if (ref) {
929
- return `\nExisting file in ${commonDir}/ (use as a style reference for structure and import order only):\n--- ${ref.fullPath} ---\n${ref.content}\n--- end ---`;
930
- }
931
- }
932
- return null;
933
- }
934
- discoverDesignTokens() {
935
- const cwd = this.toolContext.projectRoot || process.cwd();
936
- let tokens = '';
937
- // 1. Try to find tailwind config
938
- const twConfigs = ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs'];
939
- for (const configName of twConfigs) {
940
- const fullPath = path.join(cwd, configName);
941
- if (fs.existsSync(fullPath)) {
942
- try {
943
- const content = fs.readFileSync(fullPath, 'utf8');
944
- const themeMatch = content.match(/theme\s*:\s*\{[\s\S]*?\}/);
945
- if (themeMatch) {
946
- tokens += `\nTailwind Theme Configuration (from ${configName}):\n${themeMatch[0]}\n`;
947
- }
948
- else {
949
- const lines = content.split('\n').slice(0, 40).join('\n');
950
- tokens += `\nTailwind Configuration Snippet (from ${configName}):\n${lines}\n`;
951
- }
952
- break;
953
- }
954
- catch { /* ignore */ }
955
- }
956
- }
957
- // 2. Try to find CSS custom properties in global CSS files
958
- const commonCssDirs = ['src', 'app', 'styles', 'src/styles', '.'];
959
- const cssFileNames = ['globals.css', 'global.css', 'index.css', 'app.css', 'main.css'];
960
- for (const dirName of commonCssDirs) {
961
- for (const fileName of cssFileNames) {
962
- const fullPath = path.join(cwd, dirName, fileName);
963
- if (fs.existsSync(fullPath)) {
964
- try {
965
- const content = fs.readFileSync(fullPath, 'utf8');
966
- const rootMatches = content.match(/:root\s*\{[\s\S]*?\}/g);
967
- if (rootMatches && rootMatches.length > 0) {
968
- tokens += `\nDesign Tokens / CSS Variables (from ${dirName}/${fileName}):\n${rootMatches.join('\n')}\n`;
969
- }
970
- break;
971
- }
972
- catch { /* ignore */ }
973
- }
974
- }
975
- if (tokens)
976
- break;
977
- }
978
- return tokens;
979
- }
980
- pickMemoryCategory(task) {
981
- const goal = task.goal;
982
- if (task.role === 'debugger' || /fix|debug|repair|resolve/i.test(goal))
983
- return 'fix_resolution';
984
- if (task.role === 'reviewer' || /verify|test|review|validate|inspect/i.test(goal))
985
- return 'build_rule';
986
- if (task.role === 'planner' || /spec|contract|interface/i.test(goal))
987
- return 'schema_contract';
988
- return 'code_pattern';
989
- }
990
- getTaskRelatedSigmaIds(db, activeIds, task) {
991
- if (activeIds.length === 0)
992
- return [];
993
- const active = new Set(activeIds);
994
- const paths = extractFilePaths(task.goal).map(p => p.toLowerCase());
995
- const keywords = task.goal
996
- .split(/[^a-zA-Z0-9]+/)
997
- .map(k => k.toLowerCase())
998
- .filter(k => k.length >= 3);
999
- const hasOverlap = (tags) => {
1000
- for (const tag of tags) {
1001
- const t = tag.toLowerCase();
1002
- if (paths.some(p => p.includes(t) || t.includes(p)))
1003
- return true;
1004
- if (keywords.some(k => k.includes(t) || t.includes(k)))
1005
- return true;
1006
- }
1007
- return false;
1008
- };
1009
- const related = new Set();
1010
- for (const row of getSigmaMemories(db, 0, 200)) {
1011
- if (!active.has(row.id))
1012
- continue;
1013
- let tags = [];
1014
- try {
1015
- const parsed = JSON.parse(row.tags);
1016
- if (Array.isArray(parsed))
1017
- tags = parsed.map(String);
1018
- }
1019
- catch { /* unparseable tags */ }
1020
- if (hasOverlap(tags))
1021
- related.add(row.id);
1022
- }
1023
- return activeIds.filter(id => related.has(id));
1024
- }
1025
774
  async delegateTask(task, tasks, goal, projectContext) {
1026
- const role = getAgentRole(task.role);
1027
- console.log(`\n[SPAWN] Delegating to ${roleLabel(role.name)}: ${task.goal}`);
1028
- const tools = filterToolsForRole([...BUILTIN_TOOLS, ...mcpRegistry.getToolDefinitions()], task.role);
1029
- const historyStartIndex = this.toolContext.patchHistory?.length || 0;
1030
- // Inject user metadata so the agent has real values instead of guessing
1031
- const currentDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
1032
- let enrichedContext = `Current date: ${currentDate}\n`;
1033
- const projectRoot = this.toolContext.projectRoot || this.sessionManager?.projectRoot || process.cwd();
1034
- const specContract = loadSpecContract(projectRoot);
1035
- if (specContract) {
1036
- // Use the staleness-aware formatter: a spec whose referenced files don't exist is a
1037
- // PLAN, not current code state. Injecting it as authoritative makes the agent report
1038
- // the spec's intended design as real "findings" (see hallucinated helmet/TODO claims).
1039
- enrichedContext += `\n${formatSpecForPromptSafe(specContract, projectRoot)}\n`;
1040
- }
1041
- // Surface the ACTUAL shell the terminal tool runs in. The static terminal tool
1042
- // description says "bash syntax", but on Windows the model frequently overrides that
1043
- // and emits PowerShell/cmd syntax ($null, Select-String, { } blocks) which the bash
1044
- // (git-bash/MSYS) shell rejects — a retry → circuit-breaker → model-upgrade spiral.
1045
- // Stating the resolved shell explicitly stops the model from guessing wrong.
1046
- const shellType = getResolvedShellType();
1047
- if (shellType === 'bash') {
1048
- enrichedContext += '\n[SHELL] Terminal commands run in BASH (git-bash/MSYS) on this Windows host. Use bash syntax ONLY — NOT PowerShell/cmd. Specifically: use "$" for variables (never "$null"), avoid "Select-String"/"Where-Object", and never use PowerShell "{ ... }" script blocks. Example: ls -la dir || echo "missing".\n';
1049
- }
1050
- else if (shellType === 'powershell') {
1051
- enrichedContext += '\n[SHELL] Terminal commands run in POWERSHELL. Use PowerShell syntax ($, $null, Select-String are valid). Avoid bash-only constructs like "2>/dev/null" (use "2>$null").\n';
1052
- }
1053
- else {
1054
- enrichedContext += '\n[SHELL] Terminal commands run in CMD (Windows command prompt). Use cmd.exe syntax (not bash, not PowerShell). Use "dir", "2>nul", "if exist".\n';
1055
- }
1056
- // Build systemExtra with project context — system prompt is more authoritative than user message
1057
- const frameworkBlock = projectContext
1058
- ? `Follow the project framework conventions (e.g., Next.js pages go under pages/, Vue components under components/).\n`
1059
- : '';
1060
- // Surface relevant past lessons as context (cap to most-used to save tokens)
1061
- const lessons = this.sessionManager ? this.sessionManager.getFailureLessons(task.role) : [];
1062
- const topLesson = lessons.length > 0 ? lessons.sort((a, b) => ((b.used_count || 0) - (a.used_count || 0)))[0] : null;
1063
- if (topLesson) {
1064
- enrichedContext += `[LESSON] Previously failed on: "${topLesson.error_snippet}" -> resolution: ${topLesson.resolution} (occurred ${topLesson.used_count}x)\n`;
1065
- }
1066
- // Inject an existing file from the target dir as a style reference only for non-trivial tasks
1067
- const styleRef = this.findStyleReference(task.goal);
1068
- if (styleRef) {
1069
- enrichedContext += styleRef;
1070
- }
1071
- // Discover and inject design tokens if coder task
1072
- if (task.role === 'coder') {
1073
- const designTokens = this.discoverDesignTokens();
1074
- if (designTokens) {
1075
- enrichedContext += designTokens;
1076
- }
1077
- }
1078
- // Extract explicit requirements from the task goal only
1079
- const taskReqs = extractRequirements(task.goal);
1080
- if (taskReqs.length > 0) {
1081
- enrichedContext += `\nRequirements:\n${taskReqs.slice(0, 4).map(r => ` - ${r}`).join('\n')}\n`;
1082
- }
1083
- // NO FILLER CONTENT — the requirements above must be implemented with real content
1084
- if (taskReqs.length > 0 && task.role === 'coder') {
1085
- enrichedContext += `\nCRITICAL: You MUST implement every requirement above with real, specific content. Do NOT use generic filler like "Welcome to our platform", "We provide services", "Learn more about us", or placeholder text. Each requirement needs actual concrete content that a real business would publish.\n`;
1086
- }
1087
- // Extract explicit file paths from the goal and inject a concise scope boundary
1088
- const scopePaths = extractFilePaths(task.goal);
1089
- if (scopePaths.length > 0) {
1090
- enrichedContext += `\nSCOPE: only touch ${scopePaths.join(', ')}. Do NOT create a parallel module (e.g. a './foo/index.ts' beside an existing 'src/foo.ts') or expand into an unrelated refactor — if you find broken/duplicate adjacent code, fix the existing file in place.`;
1091
- }
1092
- // Warn if the task targets an existing route/module file that nothing imports.
1093
- // Edits to an orphaned module are silent no-ops at runtime (the app never loads it).
1094
- const orphanWarn = this.toolContext.projectRoot
1095
- ? orphanedModuleWarning(task.goal, this.toolContext.projectRoot)
1096
- : null;
1097
- if (orphanWarn) {
1098
- enrichedContext += `\n${orphanWarn}`;
1099
- }
1100
- enrichedContext += `\n${frameworkBlock}${task.context}`;
1101
- const frameworkRules = getFrameworkGuidance(projectContext, this.toolContext.projectRoot);
1102
- let sigmaMemBlock = '';
1103
- let activeSigmaMemoryIds = [];
1104
- const sigmaDb = SigmaMemEngine.resolveProjectMemDb(this.sessionManager, this.toolContext.projectRoot);
1105
- if (sigmaDb) {
1106
- const sigmaRes = SigmaMemEngine.getPromptContext(sigmaDb, task.role, 0.60, 5, extractFilePaths(task.goal));
1107
- sigmaMemBlock = sigmaRes.prompt;
1108
- activeSigmaMemoryIds = sigmaRes.activeMemoryIds;
1109
- SigmaMemEngine.markMemoriesUsed(sigmaDb, activeSigmaMemoryIds);
1110
- }
1111
- const systemExtra = `Project context:\n${projectContext || '(none discovered)'}${frameworkRules}${sigmaMemBlock}\n`;
1112
- // Prepend a terse override reminder so the rules land in the user message too,
1113
- // which some models weight more heavily than the system prompt extension.
1114
- if (frameworkRules && task.role === 'coder') {
1115
- enrichedContext = `IMPORTANT: The CODING RULES in your system context are mandatory and override any patterns you observe in style reference files or your training data.\n\n` + enrichedContext;
1116
- }
1117
- let result = await this.runAgent(role, task.goal, enrichedContext, tools, systemExtra);
1118
- // Lightweight ensemble: in ensemble mode, run a second coder at higher temp and pick the best
1119
- if (process.env.DAEDALUS_ENSEMBLE === 'true' && task.role === 'coder' && !this.toolContext.abortSignal.aborted) {
1120
- const firstPatches = this.toolContext.patchHistory?.slice(historyStartIndex) || [];
1121
- const firstCount = firstPatches.length;
1122
- const secondRole = { ...role, temperature: 0.5 };
1123
- const secondResult = await this.runAgent(secondRole, task.goal, enrichedContext, tools, systemExtra);
1124
- const secondPatches = this.toolContext.patchHistory?.slice(historyStartIndex) || [];
1125
- const secondCount = secondPatches.length;
1126
- if (secondCount > firstCount) {
1127
- // Second candidate produced more artifacts — use its result
1128
- result = secondResult;
1129
- }
1130
- else {
1131
- // First candidate was better — revert second candidate's patches
1132
- if (this.toolContext.patchHistory) {
1133
- this.toolContext.patchHistory.length = historyStartIndex + firstCount;
1134
- }
1135
- }
1136
- }
1137
- if (this.toolContext.abortSignal.aborted) {
1138
- this.results.push({
1139
- role: task.role,
1140
- goal: task.goal,
1141
- summary: 'Task aborted by user',
1142
- success: false,
1143
- });
1144
- task.status = 'failed';
1145
- task.error = 'Task aborted by user';
1146
- return;
1147
- }
1148
- const MAX_TURNS_SIGNAL = 'Agent reached max turns';
1149
- const PATCH_ABORT_PREFIX = 'Agent aborted: too many patch failures';
1150
- if (result.startsWith(PATCH_ABORT_PREFIX)) {
1151
- task.status = 'failed';
1152
- task.error = result.split('\n')[0];
1153
- if (task.role === 'coder' || task.role === 'debugger') {
1154
- await rollbackTaskPatches(this.toolContext, historyStartIndex);
1155
- }
1156
- this.results.push({ role: task.role, goal: task.goal, summary: result, success: false });
1157
- console.log(`[${pc.red('FAILED')}] ${role.name}: ${task.error}`);
1158
- return;
1159
- }
1160
- if (result === MAX_TURNS_SIGNAL) {
1161
- const partialWork = (this.toolContext.patchHistory?.length ?? 0) > historyStartIndex;
1162
- const depth = task.splitDepth ?? 0;
1163
- if (partialWork && depth < 3 && tasks) {
1164
- console.log(`\n${pc.yellow('Task exceeded turn limit with partial progress.')} Splitting remaining work (depth ${depth + 1})...`);
1165
- const newPatches = this.toolContext.patchHistory?.slice(historyStartIndex) || [];
1166
- const filesDone = [...new Set(newPatches.map(p => p.filePath).filter(Boolean))];
1167
- task.status = 'completed';
1168
- this.results.push({
1169
- role: task.role,
1170
- goal: task.goal,
1171
- summary: 'Partially completed — splitting remaining work into sub-tasks',
1172
- success: true,
1173
- });
1174
- const doneCtx = filesDone.length > 0 ? `\nPartially completed files: ${filesDone.join(', ')}` : '';
1175
- 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.`, projectContext);
1176
- const subTasks = this.parseDelegationTasks(subPlan, goal || task.goal);
1177
- const currentIndex = (tasks || []).indexOf(task);
1178
- const doneBeforeSplit = (tasks || []).filter((t, idx) => t.status === 'completed' && idx < currentIndex);
1179
- const deduped = subTasks.filter(st => {
1180
- if (doneBeforeSplit.length === 0 || st.role !== 'coder')
1181
- return true;
1182
- const newPaths = extractFilePaths(st.goal).map(p => p.toLowerCase());
1183
- if (newPaths.length === 0)
1184
- return true;
1185
- return !doneBeforeSplit.some(d => {
1186
- if (d.role !== 'coder')
1187
- return false;
1188
- const donePaths = extractFilePaths(d.goal).map(p => p.toLowerCase());
1189
- return donePaths.some(dp => newPaths.includes(dp));
1190
- });
1191
- });
1192
- const inheritCtx = filesDone.length > 0
1193
- ? `Original goal: ${task.goal}\nProject root: ${this.toolContext.projectRoot || 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.`
1194
- : `Original goal: ${task.goal}\nProject root: ${this.toolContext.projectRoot || 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.`;
1195
- for (const st of deduped) {
1196
- st.status = 'pending';
1197
- st.splitDepth = depth + 1;
1198
- st.context = `${inheritCtx}\n\n${st.context}`;
1199
- tasks.push(st);
1200
- }
1201
- this.printTaskList(tasks);
1202
- return;
1203
- }
1204
- task.status = 'failed';
1205
- task.error = partialWork
1206
- ? `Task still too large after ${depth} splits — manual review needed`
1207
- : 'Task too large and no work completed';
1208
- if (task.role === 'coder' || task.role === 'debugger') {
1209
- await rollbackTaskPatches(this.toolContext, historyStartIndex);
1210
- }
1211
- this.results.push({
1212
- role: task.role,
1213
- goal: task.goal,
1214
- summary: task.error,
1215
- success: false,
1216
- });
1217
- return;
1218
- }
1219
- let verified = await verifyArtifacts(this.toolContext, task.role, task.goal, result, historyStartIndex);
1220
- let evidence = '';
1221
- let placeholderSites = [];
1222
- let checkLogs = '';
1223
- if (verified) {
1224
- placeholderSites = await checkPlaceholders(this.toolContext, historyStartIndex);
1225
- if (placeholderSites.length > 0) {
1226
- console.log(pc.yellow(`\nFound ${placeholderSites.length} placeholder(s) in written files`));
1227
- // Auto-fill trivial placeholders like [Year], [Your Name]
1228
- const filled = await fillPlaceholders(this.toolContext, historyStartIndex);
1229
- if (filled > 0) {
1230
- console.log(pc.green(` Auto-filled ${filled} trivial placeholder(s) (year, name, etc.)`));
1231
- }
1232
- // Re-check for remaining (structural) placeholders
1233
- placeholderSites = await checkPlaceholders(this.toolContext, historyStartIndex);
1234
- if (placeholderSites.length === 0) {
1235
- verified = true;
1236
- }
1237
- else {
1238
- verified = false;
1239
- }
1240
- }
1241
- }
1242
- if (verified && (task.role === 'coder' || task.role === 'debugger') && (this.toolContext.patchHistory?.length ?? 0) > historyStartIndex) {
1243
- const checkResult = await runBuildVerification(this.toolContext, historyStartIndex);
1244
- if (!checkResult.success) {
1245
- const modifiedFiles = this.toolContext.patchHistory.slice(historyStartIndex).map(p => p.filePath);
1246
- const isRelated = isBuildErrorRelated(checkResult.errorLogs || '', modifiedFiles, this.toolContext.projectRoot);
1247
- if (isRelated) {
1248
- verified = false;
1249
- checkLogs = (checkResult.errorLogs || 'Build check failed') + generateBuildErrorHint(checkResult.errorLogs || '');
1250
- }
1251
- else {
1252
- console.log(pc.yellow(`\n[VERIFY] Build check failed, but errors appear to be in unrelated files. Ignoring build failure for this task.`));
1253
- }
1254
- }
1255
- if (verified) {
1256
- const specResult = await verifySpecAssertions(this.toolContext.projectRoot || process.cwd());
1257
- if (!specResult.success) {
1258
- console.log(pc.yellow(`\n[SpecFirst] Spec contract assertion check failed!`));
1259
- verified = false;
1260
- checkLogs = (specResult.errorLogs || 'Spec contract check failed');
1261
- }
1262
- }
1263
- }
1264
- if (!verified) {
1265
- let repairCtx = task.context;
1266
- if (placeholderSites.length > 0) {
1267
- const siteList = placeholderSites.map(s => ` - ${s}`).join('\n');
1268
- 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.`;
1269
- }
1270
- if (checkLogs) {
1271
- repairCtx += `\n\nPrevious attempt failed build/compilation verification. The check failed with the following error output:\n\`\`\`\n${checkLogs}\n\`\`\`\nPlease fix the build/compilation errors listed above.`;
1272
- }
1273
- const repaired = await attemptRepair({ toolContext: this.toolContext, runAgent: (role, goal, context, tools) => this.runAgent(role, goal, context, tools) }, task, {
1274
- summary: result,
1275
- }, repairCtx, historyStartIndex);
1276
- result = repaired.summary;
1277
- verified = repaired.success;
1278
- evidence = repaired.evidence || '';
1279
- if (verified) {
1280
- const stillPlaceholders = await checkPlaceholders(this.toolContext, historyStartIndex);
1281
- if (stillPlaceholders.length > 0) {
1282
- // Try auto-fill one more time after repair
1283
- const filled = await fillPlaceholders(this.toolContext, historyStartIndex);
1284
- if (filled > 0)
1285
- console.log(pc.green(` Auto-filled ${filled} remaining trivial placeholder(s)`));
1286
- const remain = await checkPlaceholders(this.toolContext, historyStartIndex);
1287
- if (remain.length > 0) {
1288
- verified = false;
1289
- evidence = `Placeholders remain: ${remain.join('; ')}`;
1290
- }
1291
- }
1292
- }
1293
- }
1294
- const resultForCheck = result.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
1295
- const success = verified && !isDeclaredError(resultForCheck) && verifyArtifactsThoroughly(this.toolContext, task.role, task.goal, resultForCheck, historyStartIndex);
1296
- if (success) {
1297
- const clean = buildCleanSummary(this.toolContext, task, result, historyStartIndex);
1298
- if (clean)
1299
- result = clean;
1300
- if (this.sessionManager?.projectMemDb) {
1301
- const related = this.getTaskRelatedSigmaIds(this.sessionManager.projectMemDb, activeSigmaMemoryIds, task);
1302
- SigmaMemEngine.rewardSuccessfulPass(this.sessionManager.projectMemDb, related.length > 0 ? related : activeSigmaMemoryIds);
1303
- SigmaMemEngine.recordVerifiedKnowledge(this.sessionManager.projectMemDb, {
1304
- agentRole: task.role,
1305
- category: this.pickMemoryCategory(task),
1306
- tags: extractFilePaths(task.goal),
1307
- summary: cleanTaskText(task.goal),
1308
- content: result.slice(0, 300),
1309
- });
1310
- }
1311
- }
1312
- task.status = success ? 'completed' : 'failed';
1313
- if (!success) {
1314
- task.error = resultForCheck.split('\n')[0] || result.split('\n')[0] || 'Unknown failure';
1315
- if (this.sessionManager?.projectMemDb) {
1316
- const related = this.getTaskRelatedSigmaIds(this.sessionManager.projectMemDb, activeSigmaMemoryIds, task);
1317
- if (related.length > 0) {
1318
- SigmaMemEngine.penalizeFailedAttempt(this.sessionManager.projectMemDb, related, task.error?.slice(0, 280));
1319
- }
1320
- }
1321
- // Rollback patches made during this task to keep codebase clean
1322
- if (task.role === 'coder' || task.role === 'debugger') {
1323
- await rollbackTaskPatches(this.toolContext, historyStartIndex);
1324
- }
1325
- // Log failure as a lesson for self-improvement
1326
- if (this.sessionManager) {
1327
- try {
1328
- this.sessionManager.saveFailureLesson({
1329
- task_role: task.role,
1330
- goal_keywords: task.goal.split(' ').slice(0, 5).join(' '),
1331
- error_snippet: task.error.slice(0, 200),
1332
- resolution: 'Pending — retry may succeed with different approach',
1333
- });
1334
- }
1335
- catch { /* session not available */ }
1336
- }
1337
- }
1338
- this.results.push({
1339
- role: task.role,
1340
- goal: task.goal,
1341
- summary: result,
1342
- success,
1343
- ...(evidence ? { evidence } : {}),
1344
- });
1345
- if (success) {
1346
- console.log(`[${pc.green('OK')}] ${role.name} completed`);
1347
- }
1348
- else {
1349
- console.log(`[${pc.red('FAILED')}] ${role.name}: ${task.error || 'verification failed'}`);
1350
- }
1351
- // Post-task review if task touched files — now BLOCKING for coder tasks
1352
- if (success && this.sessionManager && task.role !== 'reviewer') {
1353
- try {
1354
- const reviewerRole = getAgentRole('reviewer');
1355
- if (reviewerRole && !reviewerRole.canDelegate) {
1356
- const touchedFiles = (this.toolContext.patchHistory || [])
1357
- .filter((h) => h.filePath)
1358
- .map((h) => h.filePath);
1359
- const fileList = touchedFiles.length > 0
1360
- ? `\nFILES_TOUCHED: ${touchedFiles.join(', ')}`
1361
- : '';
1362
- const truncatedResult = result.length > 6000 ? result.slice(0, 6000) + '\n...[result truncated for review]' : result;
1363
- const reviewContext = `TASK: ${task.goal}\n\nAgent result:\n${truncatedResult}${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.`;
1364
- const reviewTools = filterToolsForRole([...BUILTIN_TOOLS, ...mcpRegistry.getToolDefinitions()], 'reviewer');
1365
- const review = await this.runAgent(reviewerRole, `Review files from task: ${task.goal}`, reviewContext, reviewTools);
1366
- // Parse reviewer verdict
1367
- const statusMatch = review.match(/STATUS:\s*(PASS|NEEDS_FIX|STOP)/i);
1368
- const verdict = statusMatch?.[1]?.toUpperCase() || 'PASS';
1369
- // BLOCKING: if reviewer found issues on a coder task, trigger a repair pass
1370
- if ((verdict === 'NEEDS_FIX' || verdict === 'STOP') && (task.role === 'coder' || task.role === 'debugger')) {
1371
- console.log(pc.yellow(`\n[REVIEWER] Found issues — triggering repair pass...`));
1372
- const findingsMatch = review.match(/FINDINGS:([\s\S]*?)(?:RECOMMENDATION:|$)/i);
1373
- const findings = findingsMatch?.[1]?.trim() || review;
1374
- const repairGoal = `Fix the following reviewer findings in the files you just wrote for task: "${task.goal}"\n\nFINDINGS:\n${findings}\n\nApply targeted fixes only. Do not change unrelated code.`;
1375
- const coderRole = getAgentRole('coder');
1376
- if (coderRole) {
1377
- const repairTools = filterToolsForRole([...BUILTIN_TOOLS, ...mcpRegistry.getToolDefinitions()], 'coder');
1378
- await this.runAgent(coderRole, repairGoal, reviewContext, repairTools);
1379
- console.log(pc.green(`[REPAIR] Repair pass complete.`));
1380
- }
1381
- }
1382
- // Update project status from review
1383
- try {
1384
- const buildStatus = /build.*pass|no errors|pass/i.test(review) ? 'passing' : 'needs_attention';
1385
- this.sessionManager.saveProjectStatus({
1386
- build_status: buildStatus,
1387
- test_status: /test.*pass|no failures/i.test(review) ? 'passing' : 'unknown',
1388
- key_concerns: review.split('\n').filter(l => /ERROR|FAIL|STOP|needs_fix/i.test(l)).slice(0, 3).join('; '),
1389
- last_reviewed_at: Date.now(),
1390
- });
1391
- }
1392
- catch { /* status save failed, non-critical */ }
1393
- }
1394
- }
1395
- catch { /* review failed, non-critical */ }
1396
- }
775
+ return this.taskDelegator.delegateTask(task, tasks, goal, projectContext);
1397
776
  }
1398
777
  async runAgent(role, goal, context, tools, systemExtra) {
1399
- const currentDateStr = new Date().toLocaleString();
1400
- // Build the system prompt for a given active role. Reused when a
1401
- // handoff_task mutates subContext.agentRole mid-run so the new role's
1402
- // system prompt and tool set actually take effect on subsequent turns.
1403
- const buildSystemPrompt = (activeRole) => {
1404
- let prompt = `${activeRole.systemPrompt}\n\n## CURRENT TIME\nThe current date and local time is: ${currentDateStr}.\n`;
1405
- const projectRoot = this.toolContext.projectRoot || this.sessionManager?.projectRoot;
1406
- if (projectRoot) {
1407
- const filesToCheck = ['CLAUDE.md', '.cursorrules', '.daedalusrules', 'DAEDALUS.md'];
1408
- let rules = '';
1409
- for (const file of filesToCheck) {
1410
- const fullPath = path.join(projectRoot, file);
1411
- if (fs.existsSync(fullPath)) {
1412
- try {
1413
- const content = fs.readFileSync(fullPath, 'utf8').trim();
1414
- if (content) {
1415
- rules += `\n### Rules from ${file}:\n${content}\n`;
1416
- }
1417
- }
1418
- catch {
1419
- // Ignore unreadable rule file
1420
- }
1421
- }
1422
- }
1423
- if (rules) {
1424
- prompt += `\n## PROJECT-SPECIFIC GUIDELINES\n${rules}`;
1425
- }
1426
- }
1427
- if (systemExtra) {
1428
- prompt += `\n${systemExtra}\n`;
1429
- }
1430
- const cv = this.subContext?.contextVariables;
1431
- if (cv && Object.keys(cv).length > 0) {
1432
- prompt += `\n## SHARED CONTEXT VARIABLES\nThe following state bag is shared across turns and handoffs. Honor it in your work:\n${JSON.stringify(cv, null, 2)}`;
1433
- }
1434
- return prompt;
1435
- };
1436
- // Mutable active role — a handoff_task call can transfer control to another role.
1437
- let currentRole = role;
1438
- const dynamicSystemPrompt = buildSystemPrompt(currentRole);
1439
- const messages = [
1440
- { role: 'system', content: dynamicSystemPrompt },
1441
- { role: 'user', content: `${context}\n\nTask: ${goal}` },
1442
- ];
1443
- // Derive test-suite write permission from THIS task's goal, not the parent
1444
- // autopilot goal. The planner must EXPLICITLY name a test file as a
1445
- // deliverable for this task (spec contract) — a goal that merely mentions
1446
- // "tests" must not disarm the lock (that is how an empty test file slipped
1447
- // through on an autonomous run). Live user approval (testApprovalGranted)
1448
- // still wins for the session.
1449
- const taskTestIntent = planNamesTestFiles(goal);
1450
- this.subContext = {
1451
- ...this.toolContext,
1452
- allowTestEdits: this.toolContext.testApprovalGranted ? true : taskTestIntent,
1453
- };
1454
- // Re-filter the tool set for the (possibly handoff-switched) active role,
1455
- // preserving any extra tools the caller passed in (e.g. MCP definitions).
1456
- let activeTools = filterToolsForRole(tools, currentRole.name);
1457
- let turns = 0;
1458
- let maxTurns = currentRole.maxTurns ?? 10;
1459
- const patchFailures = new Map();
1460
- const taskStartHistoryLength = this.toolContext.patchHistory?.length || 0;
1461
- let idleReadTurn = -1;
1462
- while (turns < maxTurns) {
1463
- if (this.toolContext.abortSignal.aborted) {
1464
- return 'Agent execution aborted by user';
1465
- }
1466
- const agentSpinner = new DaedalusSpinner({ text: `${roleLabel(currentRole.name)} running (turn ${turns + 1})`, color: (s) => pc.cyan(s) });
1467
- agentSpinner.start();
1468
- let completion;
1469
- const isLastTurn = turns === maxTurns - 1;
1470
- const currentTools = isLastTurn ? undefined : (activeTools.length > 0 ? activeTools : undefined);
1471
- const currentToolChoice = isLastTurn ? undefined : ((currentRole.name === 'coder' || currentRole.name === 'debugger') && turns === 0 ? 'required' : 'auto');
1472
- try {
1473
- completion = await this.retryApiCall(() => this.router.chat.completions.create({
1474
- model: this.modelOverride || 'auto',
1475
- complexity: this.modelOverride ? undefined : 'complex',
1476
- messages,
1477
- temperature: currentRole.temperature ?? 0.1,
1478
- tools: currentTools,
1479
- tool_choice: currentToolChoice,
1480
- }), `${currentRole.name} API call`);
1481
- }
1482
- finally {
1483
- agentSpinner.stop();
1484
- }
1485
- if (!completion || !completion.choices || completion.choices.length === 0) {
1486
- return 'Agent completed without response';
1487
- }
1488
- const message = completion.choices[0].message;
1489
- messages.push(message);
1490
- let effectiveToolCalls = message.tool_calls || [];
1491
- if (!effectiveToolCalls.length && message.content) {
1492
- const parsed = parseTextToolCalls(messageText(message.content));
1493
- if (parsed.length > 0) {
1494
- effectiveToolCalls = parsed;
1495
- }
1496
- }
1497
- if (effectiveToolCalls.length > 0) {
1498
- const results = await executeToolCalls(effectiveToolCalls.map((tc) => ({
1499
- id: tc.id,
1500
- type: 'function',
1501
- function: { name: tc.function.name, arguments: tc.function.arguments },
1502
- })), this.subContext);
1503
- // Dynamic handoff: handoff_task mutated subContext.agentRole. If it
1504
- // switched to a different valid role, re-role this runAgent instance so
1505
- // subsequent turns run with the new agent's system prompt + tool set.
1506
- const switchedRole = this.subContext?.agentRole;
1507
- if (switchedRole && switchedRole !== currentRole.name && VALID_AGENT_ROLES.includes(switchedRole)) {
1508
- const nextRole = getAgentRole(switchedRole);
1509
- currentRole = nextRole;
1510
- maxTurns = currentRole.maxTurns ?? 10;
1511
- // Re-filter from the FULL tool set (not the caller-filtered `tools` param,
1512
- // which is scoped to the original role) so the new role gains its own tools.
1513
- activeTools = filterToolsForRole([...BUILTIN_TOOLS, ...mcpRegistry.getToolDefinitions()], currentRole.name);
1514
- messages[0] = { role: 'system', content: buildSystemPrompt(currentRole) };
1515
- console.log(pc.magenta(`\n[HANDOFF] ${switchedRole} agent took over the execution turn`));
1516
- }
1517
- // Track patch failures per file to break retry spirals
1518
- let hadPatchFailure = false;
1519
- let patchFailureFile;
1520
- for (const result of results) {
1521
- if (/patch.*Syntax error introduced|error TS\d+/.test(result.content || '')) {
1522
- hadPatchFailure = true;
1523
- const fileMatch = (result.content || '').match(/src\/([^\s(]+)/);
1524
- patchFailureFile = fileMatch ? fileMatch[1] : undefined;
1525
- }
1526
- }
1527
- if (hadPatchFailure && patchFailureFile) {
1528
- const prev = patchFailures.get(patchFailureFile) || 0;
1529
- patchFailures.set(patchFailureFile, prev + 1);
1530
- if (prev + 1 >= 3) {
1531
- return `Agent aborted: too many patch failures on ${patchFailureFile}.\nLast error from patch tool: ${results.find(r => /patch.*Syntax error/.test(r.content || ''))?.content || 'unknown'}\nFix the TypeScript error in that file before retrying.`;
1532
- }
1533
- }
1534
- else if (!hadPatchFailure) {
1535
- for (const [file] of Array.from(patchFailures)) {
1536
- patchFailures.set(file, 0);
1537
- }
1538
- }
1539
- for (const result of results) {
1540
- let rawContent = typeof result.content === 'string' ? result.content : JSON.stringify(result.content);
1541
- if (!result.success && result.error) {
1542
- rawContent = `${rawContent}\n\n[Tool Error] ${result.error}`;
1543
- }
1544
- const cappedContent = rawContent.length > 8000
1545
- ? rawContent.slice(0, 8000) + '\n...[content truncated to prevent oversized request]'
1546
- : rawContent;
1547
- messages.push({
1548
- role: 'tool',
1549
- content: maskSecrets(cappedContent),
1550
- tool_call_id: result.toolCallId,
1551
- });
1552
- }
1553
- // Early-exit: after artifacts exist, if agent spends 2+ turns on read-only tools, it's done
1554
- const hasArtifacts = this.toolContext.patchHistory && this.toolContext.patchHistory.length > taskStartHistoryLength;
1555
- const hasArtifactTool = effectiveToolCalls.some((tc) => /^(write_file|patch|terminal)$/i.test(tc.function.name));
1556
- if (hasArtifacts && hasArtifactTool) {
1557
- idleReadTurn = -1;
1558
- }
1559
- else if (hasArtifacts && !hasArtifactTool) {
1560
- if (idleReadTurn === -1)
1561
- idleReadTurn = turns;
1562
- else if (turns - idleReadTurn >= 3) {
1563
- return 'Agent completed';
1564
- }
1565
- }
1566
- turns++;
1567
- continue;
1568
- }
1569
- // No tool calls on this turn
1570
- const responseText = messageText(message.content);
1571
- // If tools were provided but the model refused to use them, give it a firm nudge
1572
- 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)) {
1573
- messages.push({
1574
- role: 'user',
1575
- 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.',
1576
- });
1577
- turns++;
1578
- continue;
1579
- }
1580
- return responseText || 'Agent completed without response';
1581
- }
1582
- return `Agent reached max turns${this.toolContext.maxTurnsCause ? ` (cause: ${this.toolContext.maxTurnsCause})` : ''}`;
778
+ return this.taskDelegator.getSubAgentRunner().runAgent(role, goal, context, tools, systemExtra);
1583
779
  }
1584
780
  async executeOpenAIToolCalls(toolCalls) {
1585
- return executeToolCalls(toolCalls, this.subContext ?? this.toolContext);
781
+ return this.taskDelegator.getSubAgentRunner().executeOpenAIToolCalls(toolCalls);
1586
782
  }
1587
783
  synthesize(goal) {
1588
784
  if (this.toolContext.abortSignal.aborted) {