daedalus-cli 3.63.1 → 3.64.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.
- package/CHANGELOG.md +20 -1
- package/dist/agents/orchestrator.d.ts +3 -6
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +39 -843
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/agents/subagent-runner.d.ts +18 -0
- package/dist/agents/subagent-runner.d.ts.map +1 -0
- package/dist/agents/subagent-runner.js +231 -0
- package/dist/agents/subagent-runner.js.map +1 -0
- package/dist/agents/subagent-runner.test.d.ts +2 -0
- package/dist/agents/subagent-runner.test.d.ts.map +1 -0
- package/dist/agents/subagent-runner.test.js +74 -0
- package/dist/agents/subagent-runner.test.js.map +1 -0
- package/dist/agents/task-delegator.d.ts +28 -0
- package/dist/agents/task-delegator.d.ts.map +1 -0
- package/dist/agents/task-delegator.js +542 -0
- package/dist/agents/task-delegator.js.map +1 -0
- package/dist/agents/task-delegator.test.d.ts +2 -0
- package/dist/agents/task-delegator.test.d.ts.map +1 -0
- package/dist/agents/task-delegator.test.js +83 -0
- package/dist/agents/task-delegator.test.js.map +1 -0
- package/dist/indexing/indexer.d.ts +1 -1
- package/dist/indexing/indexer.d.ts.map +1 -1
- package/dist/indexing/indexer.js +112 -89
- package/dist/indexing/indexer.js.map +1 -1
- package/dist/indexing/indexer.test.js +58 -2
- package/dist/indexing/indexer.test.js.map +1 -1
- package/dist/router/fallback.test.js +67 -0
- package/dist/router/fallback.test.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +23 -2
- package/dist/router/index.js.map +1 -1
- package/dist/types.d.ts +17 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.test.js +19 -0
- package/dist/types.test.js.map +1 -1
- 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 {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
info.push(
|
|
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(
|
|
63
|
+
info.push(`React (v${pkg.dependencies.react})`);
|
|
89
64
|
}
|
|
90
65
|
if (pkg.dependencies.vue)
|
|
91
|
-
info.push(
|
|
92
|
-
if (pkg.dependencies.
|
|
93
|
-
info.push(
|
|
94
|
-
if (pkg.dependencies
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
parts.push(`Scripts: ${scripts}`);
|
|
76
|
+
if (info.length > 0)
|
|
77
|
+
parts.push(`Framework/Stack: ${info.join(', ')}`);
|
|
108
78
|
}
|
|
109
|
-
catch { /*
|
|
79
|
+
catch { /* no package.json or invalid JSON */ }
|
|
110
80
|
try {
|
|
111
|
-
const entries = fs.readdirSync(cwd).filter(
|
|
112
|
-
|
|
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 { /*
|
|
120
|
-
return parts.
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
781
|
+
return this.taskDelegator.getSubAgentRunner().executeOpenAIToolCalls(toolCalls);
|
|
1586
782
|
}
|
|
1587
783
|
synthesize(goal) {
|
|
1588
784
|
if (this.toolContext.abortSignal.aborted) {
|