skimpyclaw 0.3.14 → 0.4.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/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
// Code Agent Orchestrator - Team coordination logic
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { getNextCodeAgentId, storeCodeAgentTask, writeCodeAgentTask, getCodeAgent } from './registry.js';
|
|
6
|
-
import { runCodeAgentBackground, runValidation } from './executor.js';
|
|
7
|
-
import { notifyCodeAgentResult } from './utils.js';
|
|
8
|
-
import { parseAgentOutput, formatStructuredContext } from './structured-context.js';
|
|
9
|
-
import { runAgentTurn } from '../agent.js';
|
|
10
|
-
import { startTrace, addEvent, endTrace } from '../audit.js';
|
|
11
|
-
import { toErrorMessage } from '../utils.js';
|
|
12
|
-
import { isGitRepo, commitPendingChanges, createWorktree, mergeWorktree, removeWorktree, cleanupAllWorktrees, } from './worktree.js';
|
|
13
|
-
/**
|
|
14
|
-
* Compute execution waves from dependency info.
|
|
15
|
-
* Returns an array of waves, where each wave is an array of subtask indices that can run in parallel.
|
|
16
|
-
* Throws if there's a cycle in the dependency graph.
|
|
17
|
-
*/
|
|
18
|
-
export function computeWaves(subtasks) {
|
|
19
|
-
const n = subtasks.length;
|
|
20
|
-
const assigned = new Array(n).fill(-1); // wave assignment per subtask
|
|
21
|
-
const waves = [];
|
|
22
|
-
// Topological wave assignment
|
|
23
|
-
let remaining = n;
|
|
24
|
-
let waveIdx = 0;
|
|
25
|
-
while (remaining > 0) {
|
|
26
|
-
const wave = [];
|
|
27
|
-
for (let i = 0; i < n; i++) {
|
|
28
|
-
if (assigned[i] >= 0)
|
|
29
|
-
continue; // already assigned
|
|
30
|
-
// Check if all dependencies are satisfied
|
|
31
|
-
const depsOk = subtasks[i].dependsOn.every(d => assigned[d] >= 0);
|
|
32
|
-
if (depsOk)
|
|
33
|
-
wave.push(i);
|
|
34
|
-
}
|
|
35
|
-
if (wave.length === 0) {
|
|
36
|
-
// Cycle detected — force remaining into current wave
|
|
37
|
-
console.warn('[team] Dependency cycle detected, forcing remaining subtasks into current wave');
|
|
38
|
-
for (let i = 0; i < n; i++) {
|
|
39
|
-
if (assigned[i] < 0) {
|
|
40
|
-
wave.push(i);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
for (const idx of wave) {
|
|
45
|
-
assigned[idx] = waveIdx;
|
|
46
|
-
}
|
|
47
|
-
waves.push(wave);
|
|
48
|
-
remaining -= wave.length;
|
|
49
|
-
waveIdx++;
|
|
50
|
-
}
|
|
51
|
-
return waves;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Gather lightweight codebase context to improve task decomposition.
|
|
55
|
-
* Returns a short summary of the project structure (file tree, package.json scripts).
|
|
56
|
-
* Capped at ~2000 chars to keep the decomposition prompt small.
|
|
57
|
-
*/
|
|
58
|
-
export function gatherCodebaseContext(workdir) {
|
|
59
|
-
const parts = [];
|
|
60
|
-
// Package.json scripts
|
|
61
|
-
try {
|
|
62
|
-
const pkgPath = join(workdir, 'package.json');
|
|
63
|
-
if (existsSync(pkgPath)) {
|
|
64
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
65
|
-
if (pkg.scripts) {
|
|
66
|
-
const scriptNames = Object.keys(pkg.scripts).slice(0, 15).join(', ');
|
|
67
|
-
parts.push(`Scripts: ${scriptNames}`);
|
|
68
|
-
}
|
|
69
|
-
if (pkg.dependencies || pkg.devDependencies) {
|
|
70
|
-
const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).slice(0, 20).join(', ');
|
|
71
|
-
parts.push(`Key deps: ${deps}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch { /* ignore */ }
|
|
76
|
-
// Source file tree (top-level structure)
|
|
77
|
-
try {
|
|
78
|
-
const tree = execSync('find . -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" | grep -v node_modules | grep -v dist | grep -v .test. | sort | head -60', { cwd: workdir, timeout: 5000, encoding: 'utf-8' }).trim();
|
|
79
|
-
if (tree)
|
|
80
|
-
parts.push(`Source files:\n${tree}`);
|
|
81
|
-
}
|
|
82
|
-
catch { /* ignore */ }
|
|
83
|
-
const context = parts.join('\n\n');
|
|
84
|
-
return context.slice(0, 2000);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Use a quick model call to decompose a complex task into N subtasks with optional dependency info.
|
|
88
|
-
* Falls back to numbered subtask splitting on parse error.
|
|
89
|
-
* Falls back to all-independent if dependency info is missing or invalid.
|
|
90
|
-
*/
|
|
91
|
-
export async function decomposeTask(task, teamSize, config, workdir) {
|
|
92
|
-
try {
|
|
93
|
-
// Gather codebase context for smarter decomposition
|
|
94
|
-
const codebaseContext = workdir ? gatherCodebaseContext(workdir) : '';
|
|
95
|
-
const contextBlock = codebaseContext
|
|
96
|
-
? `\n\nProject structure:\n${codebaseContext}\n`
|
|
97
|
-
: '';
|
|
98
|
-
const prompt = `You are a task decomposition expert. Split the following coding task into exactly ${teamSize} independent or dependent subtasks that can be assigned to separate coding agents.
|
|
99
|
-
|
|
100
|
-
Rules:
|
|
101
|
-
- Each subtask should be self-contained with clear file scope
|
|
102
|
-
- Each parallel agent gets its own git worktree (branch), so file overlap is OK but be aware changes are merged after
|
|
103
|
-
- Use dependsOn to order subtasks that must run sequentially (e.g. create interface before implementation)
|
|
104
|
-
- Be specific: mention exact files, functions, and expected changes
|
|
105
|
-
- Return JSON only: {"subtasks":[{"description":"...","dependsOn":[]},...]}
|
|
106
|
-
- Use 0-based indices for dependsOn
|
|
107
|
-
${contextBlock}
|
|
108
|
-
Task: ${task}`;
|
|
109
|
-
const result = await runAgentTurn('main', prompt, config);
|
|
110
|
-
const match = result.match(/\{[\s\S]*"subtasks"[\s\S]*\}/);
|
|
111
|
-
if (match) {
|
|
112
|
-
const parsed = JSON.parse(match[0]);
|
|
113
|
-
if (Array.isArray(parsed.subtasks) && parsed.subtasks.length > 0) {
|
|
114
|
-
// Handle both new format (objects with dependsOn) and legacy format (plain strings)
|
|
115
|
-
const normalized = parsed.subtasks.map((item, _idx) => {
|
|
116
|
-
if (typeof item === 'string') {
|
|
117
|
-
return { description: item, dependsOn: [] };
|
|
118
|
-
}
|
|
119
|
-
if (item && typeof item.description === 'string') {
|
|
120
|
-
const deps = Array.isArray(item.dependsOn)
|
|
121
|
-
? item.dependsOn.filter((d) => typeof d === 'number' && d >= 0 && d < parsed.subtasks.length)
|
|
122
|
-
: [];
|
|
123
|
-
return { description: item.description, dependsOn: deps };
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}).filter((x) => x !== null);
|
|
127
|
-
if (normalized.length > 0) {
|
|
128
|
-
// Remove self-references from dependsOn
|
|
129
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
130
|
-
normalized[i].dependsOn = normalized[i].dependsOn.filter(d => d !== i);
|
|
131
|
-
}
|
|
132
|
-
// Pad or trim to match teamSize
|
|
133
|
-
while (normalized.length < teamSize) {
|
|
134
|
-
normalized.push({ description: `Additional part of: ${task.slice(0, 200)}`, dependsOn: [] });
|
|
135
|
-
}
|
|
136
|
-
return normalized.slice(0, teamSize);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
console.warn('[team] Task decomposition failed, using fallback:', err instanceof Error ? err.message : err);
|
|
143
|
-
}
|
|
144
|
-
// Fallback: numbered subtask splitting (all independent)
|
|
145
|
-
return Array.from({ length: teamSize }, (_, i) => ({
|
|
146
|
-
description: `Part ${i + 1} of ${teamSize}: ${task}`,
|
|
147
|
-
dependsOn: [],
|
|
148
|
-
}));
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Use a quick model call to synthesize results from multiple subtask completions.
|
|
152
|
-
*/
|
|
153
|
-
export async function synthesizeResults(originalTask, results, config, workdir) {
|
|
154
|
-
try {
|
|
155
|
-
const resultSummary = results.map((r, i) => {
|
|
156
|
-
const context = r.output
|
|
157
|
-
? formatStructuredContext(parseAgentOutput(r.output))
|
|
158
|
-
: '';
|
|
159
|
-
return `### Subtask ${i + 1}: ${r.subtask}\nStatus: ${r.status}\n${context}${r.error ? `\nError: ${r.error}` : ''}`;
|
|
160
|
-
}).join('\n\n');
|
|
161
|
-
// Include actual file changes from git for accuracy
|
|
162
|
-
let diffBlock = '';
|
|
163
|
-
if (workdir) {
|
|
164
|
-
try {
|
|
165
|
-
const diffStat = execSync('git diff --stat HEAD 2>/dev/null || git diff --stat 2>/dev/null', {
|
|
166
|
-
cwd: workdir,
|
|
167
|
-
timeout: 5000,
|
|
168
|
-
encoding: 'utf-8',
|
|
169
|
-
}).trim();
|
|
170
|
-
if (diffStat)
|
|
171
|
-
diffBlock = `\n\nActual file changes (git diff --stat):\n${diffStat.slice(0, 2000)}`;
|
|
172
|
-
}
|
|
173
|
-
catch { /* not a git repo or no changes */ }
|
|
174
|
-
}
|
|
175
|
-
const succeeded = results.filter(r => r.status === 'completed').length;
|
|
176
|
-
const failed = results.filter(r => r.status !== 'completed').length;
|
|
177
|
-
const prompt = `You are a results synthesizer. Summarize the results of a multi-agent coding task.
|
|
178
|
-
|
|
179
|
-
Original task: ${originalTask}
|
|
180
|
-
|
|
181
|
-
Results from each agent (${succeeded} succeeded, ${failed} failed):
|
|
182
|
-
${resultSummary}${diffBlock}
|
|
183
|
-
|
|
184
|
-
Provide a concise markdown summary of what was accomplished, what succeeded, and what failed (if anything). Be specific about files changed and outcomes.`;
|
|
185
|
-
return await runAgentTurn('main', prompt, config);
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
// Fallback: mechanical summary
|
|
189
|
-
const succeeded = results.filter(r => r.status === 'completed').length;
|
|
190
|
-
const failed = results.filter(r => r.status !== 'completed').length;
|
|
191
|
-
return `Team completed: ${succeeded}/${results.length} subtasks succeeded${failed > 0 ? `, ${failed} failed` : ''}.\n\n${results.map((r, i) => `${i + 1}. [${r.status}] ${r.subtask}`).join('\n')}`;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Team orchestrator — decomposes task, spawns parallel agents, monitors, synthesizes.
|
|
196
|
-
*/
|
|
197
|
-
export async function runTeamOrchestrator(parentId, task, teamSize, workdir, validate, agent, model, startedAt, context) {
|
|
198
|
-
const parentTask = getCodeAgent(parentId);
|
|
199
|
-
if (!parentTask) {
|
|
200
|
-
throw new Error(`Parent task ${parentId} not found`);
|
|
201
|
-
}
|
|
202
|
-
const traceId = startTrace('code_team');
|
|
203
|
-
addEvent(traceId, {
|
|
204
|
-
type: 'spawn',
|
|
205
|
-
summary: `team-coordinator: ${task.slice(0, 150)}`,
|
|
206
|
-
durationMs: 0,
|
|
207
|
-
detail: { teamSize, workdir, agent, model, validate },
|
|
208
|
-
});
|
|
209
|
-
const configTeamTimeout = context?.fullConfig?.codeAgents?.teamTimeoutMinutes ?? 60;
|
|
210
|
-
const timeoutMinutes = Math.min(configTeamTimeout, 120);
|
|
211
|
-
// Reserve budget for overhead (decompose, synthesize, validation) and distribute rest across waves
|
|
212
|
-
const overheadMinutes = 5;
|
|
213
|
-
const availableForChildren = Math.max(timeoutMinutes - overheadMinutes, timeoutMinutes * 0.7);
|
|
214
|
-
const CANCELLED_MESSAGE = 'Cancelled by user';
|
|
215
|
-
// Worktree state — declared outside try so catch can clean up
|
|
216
|
-
const _useWorktrees = isGitRepo(workdir);
|
|
217
|
-
const _activeWorktrees = new Map();
|
|
218
|
-
try {
|
|
219
|
-
if (getCodeAgent(parentId)?.status === 'cancelled')
|
|
220
|
-
throw new Error(CANCELLED_MESSAGE);
|
|
221
|
-
// Phase 1: Decompose
|
|
222
|
-
parentTask.liveOutput = 'Phase: Decomposing task...';
|
|
223
|
-
writeCodeAgentTask(parentTask);
|
|
224
|
-
const fullConfig = context?.fullConfig;
|
|
225
|
-
if (!fullConfig)
|
|
226
|
-
throw new Error('No config available for task decomposition');
|
|
227
|
-
const subtasks = await decomposeTask(task, teamSize, fullConfig, workdir);
|
|
228
|
-
const waves = computeWaves(subtasks);
|
|
229
|
-
// Distribute timeout across waves (not team size) for better budgeting
|
|
230
|
-
const perChildTimeout = Math.max(5, Math.floor(availableForChildren / waves.length));
|
|
231
|
-
addEvent(traceId, {
|
|
232
|
-
type: 'decompose',
|
|
233
|
-
summary: `Decomposed into ${subtasks.length} subtasks in ${waves.length} wave(s)`,
|
|
234
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
235
|
-
});
|
|
236
|
-
// Phase 2: Create child task entries and schedule waves
|
|
237
|
-
const totalWaves = waves.length;
|
|
238
|
-
parentTask.liveOutput = `Phase: Scheduling ${subtasks.length} agents in ${totalWaves} wave(s)...`;
|
|
239
|
-
writeCodeAgentTask(parentTask);
|
|
240
|
-
// Create all child tasks upfront (pending for later waves)
|
|
241
|
-
const childIds = [];
|
|
242
|
-
const childIdByIndex = []; // subtask index → child id
|
|
243
|
-
for (let i = 0; i < subtasks.length; i++) {
|
|
244
|
-
const childId = getNextCodeAgentId();
|
|
245
|
-
const waveNum = waves.findIndex(w => w.includes(i));
|
|
246
|
-
const childTask = {
|
|
247
|
-
id: childId,
|
|
248
|
-
agent,
|
|
249
|
-
task: subtasks[i].description,
|
|
250
|
-
status: waveNum === 0 ? 'running' : 'pending',
|
|
251
|
-
chatId: context?.chatId,
|
|
252
|
-
startedAt: new Date().toISOString(),
|
|
253
|
-
workdir,
|
|
254
|
-
model,
|
|
255
|
-
parentTaskId: parentId,
|
|
256
|
-
subtask: subtasks[i].description,
|
|
257
|
-
dependsOn: subtasks[i].dependsOn,
|
|
258
|
-
wave: waveNum,
|
|
259
|
-
};
|
|
260
|
-
storeCodeAgentTask(childTask);
|
|
261
|
-
writeCodeAgentTask(childTask);
|
|
262
|
-
childIds.push(childId);
|
|
263
|
-
childIdByIndex.push(childId);
|
|
264
|
-
}
|
|
265
|
-
parentTask.childTaskIds = childIds;
|
|
266
|
-
writeCodeAgentTask(parentTask);
|
|
267
|
-
// Helper: get git diff summary for context passing between waves
|
|
268
|
-
function getGitDiffSummary() {
|
|
269
|
-
try {
|
|
270
|
-
const diff = execSync('git diff --stat HEAD 2>/dev/null || git diff --stat 2>/dev/null', {
|
|
271
|
-
cwd: workdir,
|
|
272
|
-
timeout: 5000,
|
|
273
|
-
encoding: 'utf-8',
|
|
274
|
-
}).trim();
|
|
275
|
-
return diff ? diff.slice(0, 1500) : '';
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
return '';
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// Helper: build task prompt with predecessor context for dependent subtasks
|
|
282
|
-
function buildChildPrompt(subtaskIdx) {
|
|
283
|
-
const sub = subtasks[subtaskIdx];
|
|
284
|
-
if (sub.dependsOn.length === 0)
|
|
285
|
-
return sub.description;
|
|
286
|
-
const contextParts = [];
|
|
287
|
-
for (const depIdx of sub.dependsOn) {
|
|
288
|
-
const depChild = getCodeAgent(childIdByIndex[depIdx]);
|
|
289
|
-
if (depChild && depChild.outputPreview) {
|
|
290
|
-
// Use the full outputPreview (up to 5000 chars) not just the 500-char summary
|
|
291
|
-
const structured = formatStructuredContext(parseAgentOutput(depChild.outputPreview));
|
|
292
|
-
contextParts.push(`- Task "${subtasks[depIdx].description}" [${depChild.status}]:\n${structured}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Include git diff to show what predecessor waves actually changed on disk
|
|
296
|
-
const diffSummary = getGitDiffSummary();
|
|
297
|
-
const diffBlock = diffSummary ? `\nFiles changed so far:\n${diffSummary}\n` : '';
|
|
298
|
-
if (contextParts.length === 0 && !diffBlock)
|
|
299
|
-
return sub.description;
|
|
300
|
-
return `Context from completed prerequisite tasks:\n${contextParts.join('\n')}${diffBlock}\nYour task: ${sub.description}`;
|
|
301
|
-
}
|
|
302
|
-
// Worktree isolation: parallel agents in the same wave get their own worktree
|
|
303
|
-
// so they can't overwrite each other's files. After the wave, branches are
|
|
304
|
-
// merged back sequentially. Falls back to shared workdir if not a git repo.
|
|
305
|
-
const useWorktrees = _useWorktrees;
|
|
306
|
-
const activeWorktrees = _activeWorktrees;
|
|
307
|
-
if (useWorktrees) {
|
|
308
|
-
// Clean up any stale worktrees from previous crashed runs
|
|
309
|
-
cleanupAllWorktrees(workdir);
|
|
310
|
-
addEvent(traceId, {
|
|
311
|
-
type: 'worktree',
|
|
312
|
-
summary: 'Git worktree isolation enabled',
|
|
313
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
console.warn('[code-team] Not a git repo — agents will share workdir (risk of file conflicts)');
|
|
318
|
-
}
|
|
319
|
-
// Phase 3: Execute waves sequentially, tasks within each wave in parallel
|
|
320
|
-
const POLL_INTERVAL = 3000;
|
|
321
|
-
const totalTimeoutMs = timeoutMinutes * 60 * 1000;
|
|
322
|
-
let lastLiveOutput = '';
|
|
323
|
-
for (let waveIdx = 0; waveIdx < totalWaves; waveIdx++) {
|
|
324
|
-
if (getCodeAgent(parentId)?.status === 'cancelled')
|
|
325
|
-
throw new Error(CANCELLED_MESSAGE);
|
|
326
|
-
const waveIndices = waves[waveIdx];
|
|
327
|
-
addEvent(traceId, {
|
|
328
|
-
type: 'wave_start',
|
|
329
|
-
summary: `Starting wave ${waveIdx + 1}/${totalWaves} (${waveIndices.length} tasks)`,
|
|
330
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
331
|
-
});
|
|
332
|
-
// Helper: spawn a single child task in the given workdir
|
|
333
|
-
function spawnChild(childId, prompt, childWorkdir) {
|
|
334
|
-
runCodeAgentBackground(childId, agent, prompt, childWorkdir, false, // per-child validation is handled by the orchestrator below
|
|
335
|
-
{ task: prompt, model, timeout_minutes: perChildTimeout }, new Date(), { skipNotification: true, defaultTimeoutMinutes: perChildTimeout, maxTimeoutMinutes: perChildTimeout, validationCommands: fullConfig?.codeAgents?.validationCommands }).catch((err) => {
|
|
336
|
-
const child = getCodeAgent(childId);
|
|
337
|
-
if (child && child.status === 'running') {
|
|
338
|
-
Object.assign(child, {
|
|
339
|
-
status: 'failed',
|
|
340
|
-
endedAt: new Date().toISOString(),
|
|
341
|
-
error: toErrorMessage(err),
|
|
342
|
-
});
|
|
343
|
-
writeCodeAgentTask(child);
|
|
344
|
-
}
|
|
345
|
-
console.error(`[code-team] Child ${childId} background error:`, err);
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
// Create worktrees for parallel tasks (waves with >1 child)
|
|
349
|
-
const useWorktreeForWave = useWorktrees && waveIndices.length > 1;
|
|
350
|
-
if (useWorktreeForWave) {
|
|
351
|
-
// Commit any pending changes so worktrees branch from a clean state
|
|
352
|
-
commitPendingChanges(workdir, `[skimpyclaw] pre-wave-${waveIdx + 1}`);
|
|
353
|
-
parentTask.liveOutput = `Phase: Creating worktrees for wave ${waveIdx + 1}...`;
|
|
354
|
-
writeCodeAgentTask(parentTask);
|
|
355
|
-
}
|
|
356
|
-
// Spawn all tasks in this wave
|
|
357
|
-
for (const subtaskIdx of waveIndices) {
|
|
358
|
-
const childId = childIdByIndex[subtaskIdx];
|
|
359
|
-
const child = getCodeAgent(childId);
|
|
360
|
-
const prompt = buildChildPrompt(subtaskIdx);
|
|
361
|
-
// Determine workdir for this child
|
|
362
|
-
let childWorkdir = workdir;
|
|
363
|
-
if (useWorktreeForWave) {
|
|
364
|
-
try {
|
|
365
|
-
const wt = createWorktree(workdir, childId);
|
|
366
|
-
activeWorktrees.set(childId, wt);
|
|
367
|
-
childWorkdir = wt.path;
|
|
368
|
-
console.log(`[code-team] Created worktree for ${childId}: ${wt.path} (branch ${wt.branch})`);
|
|
369
|
-
}
|
|
370
|
-
catch (err) {
|
|
371
|
-
console.error(`[code-team] Failed to create worktree for ${childId}, using shared workdir:`, err);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
child.status = 'running';
|
|
375
|
-
child.task = prompt;
|
|
376
|
-
child.workdir = childWorkdir;
|
|
377
|
-
child.startedAt = new Date().toISOString();
|
|
378
|
-
writeCodeAgentTask(child);
|
|
379
|
-
spawnChild(childId, prompt, childWorkdir);
|
|
380
|
-
}
|
|
381
|
-
// Poll until all tasks in this wave complete
|
|
382
|
-
const waveChildIds = waveIndices.map(i => childIdByIndex[i]);
|
|
383
|
-
// Check cancellation after spawning
|
|
384
|
-
if (getCodeAgent(parentId)?.status === 'cancelled') {
|
|
385
|
-
for (const childId of waveChildIds) {
|
|
386
|
-
const child = getCodeAgent(childId);
|
|
387
|
-
if (child && (child.status === 'running' || child.status === 'pending')) {
|
|
388
|
-
Object.assign(child, {
|
|
389
|
-
status: 'cancelled',
|
|
390
|
-
endedAt: new Date().toISOString(),
|
|
391
|
-
error: CANCELLED_MESSAGE,
|
|
392
|
-
});
|
|
393
|
-
writeCodeAgentTask(child);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
throw new Error(CANCELLED_MESSAGE);
|
|
397
|
-
}
|
|
398
|
-
while (true) {
|
|
399
|
-
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
|
|
400
|
-
if (getCodeAgent(parentId)?.status === 'cancelled')
|
|
401
|
-
throw new Error(CANCELLED_MESSAGE);
|
|
402
|
-
const allChildren = childIds.map(id => getCodeAgent(id));
|
|
403
|
-
const waveChildren = waveChildIds.map(id => getCodeAgent(id));
|
|
404
|
-
const waveDone = waveChildren.filter(c => c.status !== 'running' && c.status !== 'validating').length;
|
|
405
|
-
// Build wave-grouped status display
|
|
406
|
-
const waveStatusLines = [];
|
|
407
|
-
for (let w = 0; w < totalWaves; w++) {
|
|
408
|
-
const wIndices = waves[w];
|
|
409
|
-
const wChildren = wIndices.map(i => getCodeAgent(childIdByIndex[i]));
|
|
410
|
-
const wDone = wChildren.every(c => c.status !== 'running' && c.status !== 'validating' && c.status !== 'pending');
|
|
411
|
-
const wRunning = w === waveIdx;
|
|
412
|
-
const wPending = w > waveIdx;
|
|
413
|
-
const wLabel = wDone ? 'done' : wRunning ? 'running' : 'pending';
|
|
414
|
-
const childLines = wChildren.map(c => {
|
|
415
|
-
if (c.status === 'pending')
|
|
416
|
-
return ` ${c.id} [pending]`;
|
|
417
|
-
const elapsed = Math.round((Date.now() - new Date(c.startedAt).getTime()) / 1000);
|
|
418
|
-
const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m${elapsed % 60}s`;
|
|
419
|
-
const preview = (c.subtask || c.task).slice(0, 150);
|
|
420
|
-
return ` ${c.id} [${c.status}] (${elapsedStr}): ${preview}`;
|
|
421
|
-
}).join('\n');
|
|
422
|
-
waveStatusLines.push(`Wave ${w + 1} [${wLabel}]:\n${childLines}`);
|
|
423
|
-
}
|
|
424
|
-
const completedWaves = waves.filter((_, w) => w < waveIdx).length;
|
|
425
|
-
const newLiveOutput = `Phase: Running Wave ${waveIdx + 1}/${totalWaves} (${completedWaves}/${totalWaves} complete)\n${waveStatusLines.join('\n')}`;
|
|
426
|
-
if (newLiveOutput !== lastLiveOutput) {
|
|
427
|
-
parentTask.liveOutput = newLiveOutput;
|
|
428
|
-
writeCodeAgentTask(parentTask);
|
|
429
|
-
lastLiveOutput = newLiveOutput;
|
|
430
|
-
}
|
|
431
|
-
if (waveDone === waveChildren.length)
|
|
432
|
-
break;
|
|
433
|
-
// Check total timeout
|
|
434
|
-
if (Date.now() - startedAt.getTime() > totalTimeoutMs) {
|
|
435
|
-
// Kill all remaining running/pending children
|
|
436
|
-
for (const child of allChildren) {
|
|
437
|
-
if (child.status === 'running' || child.status === 'validating' || child.status === 'pending') {
|
|
438
|
-
Object.assign(child, {
|
|
439
|
-
status: 'timeout',
|
|
440
|
-
endedAt: new Date().toISOString(),
|
|
441
|
-
durationSeconds: Math.round((Date.now() - new Date(child.startedAt).getTime()) / 1000),
|
|
442
|
-
error: 'Parent team timed out',
|
|
443
|
-
});
|
|
444
|
-
writeCodeAgentTask(child);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
// Jump to synthesis
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
// Per-wave validation: run build after each wave to catch breakage early
|
|
452
|
-
if (validate && Date.now() - startedAt.getTime() < totalTimeoutMs) {
|
|
453
|
-
const waveCompleted = waveChildIds.every(id => getCodeAgent(id)?.status === 'completed');
|
|
454
|
-
if (waveCompleted) {
|
|
455
|
-
parentTask.liveOutput = `Phase: Validating wave ${waveIdx + 1}/${totalWaves}...`;
|
|
456
|
-
writeCodeAgentTask(parentTask);
|
|
457
|
-
const { passed, output: valOutput } = await runValidation(workdir, fullConfig?.codeAgents?.validationCommands);
|
|
458
|
-
if (!passed) {
|
|
459
|
-
addEvent(traceId, {
|
|
460
|
-
type: 'wave_validation',
|
|
461
|
-
summary: `Wave ${waveIdx + 1} validation failed — retrying failed children`,
|
|
462
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
463
|
-
});
|
|
464
|
-
// Retry each child in this wave once with the validation error context
|
|
465
|
-
const retryChildIds = [];
|
|
466
|
-
for (const subtaskIdx of waveIndices) {
|
|
467
|
-
const childId = childIdByIndex[subtaskIdx];
|
|
468
|
-
const child = getCodeAgent(childId);
|
|
469
|
-
if (child.retryCount)
|
|
470
|
-
continue; // already retried
|
|
471
|
-
child.retryCount = 1;
|
|
472
|
-
child.status = 'running';
|
|
473
|
-
child.validationOutput = valOutput.slice(0, 4000);
|
|
474
|
-
child.startedAt = new Date().toISOString();
|
|
475
|
-
writeCodeAgentTask(child);
|
|
476
|
-
const retryPrompt = `Fix build/test errors. Your original task: ${subtasks[subtaskIdx].description}\n\nValidation errors:\n${valOutput.slice(0, 4000)}`;
|
|
477
|
-
child.task = retryPrompt;
|
|
478
|
-
writeCodeAgentTask(child);
|
|
479
|
-
spawnChild(childId, retryPrompt, workdir);
|
|
480
|
-
retryChildIds.push(childId);
|
|
481
|
-
}
|
|
482
|
-
// Poll until retries complete
|
|
483
|
-
if (retryChildIds.length > 0) {
|
|
484
|
-
while (true) {
|
|
485
|
-
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
|
|
486
|
-
if (getCodeAgent(parentId)?.status === 'cancelled')
|
|
487
|
-
throw new Error(CANCELLED_MESSAGE);
|
|
488
|
-
const retryDone = retryChildIds.every(id => {
|
|
489
|
-
const c = getCodeAgent(id);
|
|
490
|
-
return c.status !== 'running' && c.status !== 'validating';
|
|
491
|
-
});
|
|
492
|
-
if (retryDone)
|
|
493
|
-
break;
|
|
494
|
-
if (Date.now() - startedAt.getTime() > totalTimeoutMs)
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
addEvent(traceId, {
|
|
498
|
-
type: 'wave_retry_complete',
|
|
499
|
-
summary: `Wave ${waveIdx + 1} retry complete`,
|
|
500
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
addEvent(traceId, {
|
|
506
|
-
type: 'wave_validation',
|
|
507
|
-
summary: `Wave ${waveIdx + 1} validation passed`,
|
|
508
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
// Merge worktree branches back into main branch sequentially
|
|
514
|
-
if (useWorktreeForWave && Date.now() - startedAt.getTime() < totalTimeoutMs) {
|
|
515
|
-
parentTask.liveOutput = `Phase: Merging wave ${waveIdx + 1} results...`;
|
|
516
|
-
writeCodeAgentTask(parentTask);
|
|
517
|
-
const mergeErrors = [];
|
|
518
|
-
for (const subtaskIdx of waveIndices) {
|
|
519
|
-
const childId = childIdByIndex[subtaskIdx];
|
|
520
|
-
const wt = activeWorktrees.get(childId);
|
|
521
|
-
if (!wt)
|
|
522
|
-
continue;
|
|
523
|
-
const child = getCodeAgent(childId);
|
|
524
|
-
if (!child || child.status !== 'completed') {
|
|
525
|
-
// Don't merge failed/timed-out children
|
|
526
|
-
console.log(`[code-team] Skipping merge for ${childId} (status: ${child?.status})`);
|
|
527
|
-
removeWorktree(workdir, childId, wt.branch);
|
|
528
|
-
activeWorktrees.delete(childId);
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
// Commit any uncommitted changes in the worktree before merging
|
|
532
|
-
commitPendingChanges(wt.path, `[skimpyclaw] ${childId}: ${subtasks[subtaskIdx].description.slice(0, 60)}`);
|
|
533
|
-
const { merged, conflict } = mergeWorktree(workdir, wt.branch, childId);
|
|
534
|
-
if (merged) {
|
|
535
|
-
console.log(`[code-team] Merged ${childId} (branch ${wt.branch}) successfully`);
|
|
536
|
-
addEvent(traceId, {
|
|
537
|
-
type: 'merge',
|
|
538
|
-
summary: `Merged ${childId} successfully`,
|
|
539
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
const conflictMsg = `Merge conflict for ${childId}: ${conflict}`;
|
|
544
|
-
console.error(`[code-team] ${conflictMsg}`);
|
|
545
|
-
mergeErrors.push(conflictMsg);
|
|
546
|
-
addEvent(traceId, {
|
|
547
|
-
type: 'merge_conflict',
|
|
548
|
-
summary: conflictMsg.slice(0, 200),
|
|
549
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
// Clean up worktree regardless of merge result
|
|
553
|
-
removeWorktree(workdir, childId, wt.branch);
|
|
554
|
-
activeWorktrees.delete(childId);
|
|
555
|
-
}
|
|
556
|
-
if (mergeErrors.length > 0) {
|
|
557
|
-
const errorMsg = `Merge conflicts in wave ${waveIdx + 1}:\n${mergeErrors.join('\n')}`;
|
|
558
|
-
parentTask.error = (parentTask.error ? parentTask.error + '\n' : '') + errorMsg;
|
|
559
|
-
writeCodeAgentTask(parentTask);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
// If we timed out, don't start more waves
|
|
563
|
-
if (Date.now() - startedAt.getTime() > totalTimeoutMs)
|
|
564
|
-
break;
|
|
565
|
-
}
|
|
566
|
-
// Phase 4: Collect results and synthesize
|
|
567
|
-
if (getCodeAgent(parentId)?.status === 'cancelled')
|
|
568
|
-
throw new Error(CANCELLED_MESSAGE);
|
|
569
|
-
parentTask.liveOutput = 'Phase: Synthesizing results...';
|
|
570
|
-
// Aggregate cost/tokens from all children into parent
|
|
571
|
-
let totalCost = 0;
|
|
572
|
-
let totalInput = 0;
|
|
573
|
-
let totalOutput = 0;
|
|
574
|
-
let hasCostData = false;
|
|
575
|
-
for (const cid of childIds) {
|
|
576
|
-
const child = getCodeAgent(cid);
|
|
577
|
-
if (child?.totalCost != null) {
|
|
578
|
-
totalCost += child.totalCost;
|
|
579
|
-
hasCostData = true;
|
|
580
|
-
}
|
|
581
|
-
if (child?.inputTokens != null)
|
|
582
|
-
totalInput += child.inputTokens;
|
|
583
|
-
if (child?.outputTokens != null)
|
|
584
|
-
totalOutput += child.outputTokens;
|
|
585
|
-
}
|
|
586
|
-
if (hasCostData) {
|
|
587
|
-
parentTask.totalCost = totalCost;
|
|
588
|
-
parentTask.inputTokens = totalInput;
|
|
589
|
-
parentTask.outputTokens = totalOutput;
|
|
590
|
-
}
|
|
591
|
-
writeCodeAgentTask(parentTask);
|
|
592
|
-
const childResults = childIds.map(id => {
|
|
593
|
-
const child = getCodeAgent(id);
|
|
594
|
-
return {
|
|
595
|
-
subtask: child.subtask || child.task,
|
|
596
|
-
status: child.status,
|
|
597
|
-
output: child.outputPreview,
|
|
598
|
-
error: child.error,
|
|
599
|
-
};
|
|
600
|
-
});
|
|
601
|
-
addEvent(traceId, {
|
|
602
|
-
type: 'synthesize',
|
|
603
|
-
summary: `Synthesizing ${childResults.length} results`,
|
|
604
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
605
|
-
});
|
|
606
|
-
const synthesis = await synthesizeResults(task, childResults, fullConfig, workdir);
|
|
607
|
-
parentTask.synthesisResult = synthesis;
|
|
608
|
-
// Phase 5: Validation (once, on the combined result)
|
|
609
|
-
if (validate) {
|
|
610
|
-
parentTask.liveOutput = 'Phase: Validating...';
|
|
611
|
-
parentTask.status = 'validating';
|
|
612
|
-
writeCodeAgentTask(parentTask);
|
|
613
|
-
const { passed, output } = await runValidation(workdir, fullConfig?.codeAgents?.validationCommands);
|
|
614
|
-
const endedAt = new Date();
|
|
615
|
-
const duration = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
|
|
616
|
-
if (!passed) {
|
|
617
|
-
addEvent(traceId, { type: 'validation', summary: 'Team validation failed', durationMs: Date.now() - startedAt.getTime() });
|
|
618
|
-
await endTrace(traceId, 'error');
|
|
619
|
-
Object.assign(parentTask, {
|
|
620
|
-
status: 'failed',
|
|
621
|
-
endedAt: endedAt.toISOString(),
|
|
622
|
-
durationSeconds: duration,
|
|
623
|
-
validationPassed: false,
|
|
624
|
-
validationOutput: output,
|
|
625
|
-
outputPreview: synthesis.slice(0, 5000),
|
|
626
|
-
error: 'Validation failed',
|
|
627
|
-
liveOutput: undefined,
|
|
628
|
-
});
|
|
629
|
-
writeCodeAgentTask(parentTask);
|
|
630
|
-
await notifyCodeAgentResult(parentTask, getCodeAgent);
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
addEvent(traceId, { type: 'validation', summary: 'Team validation passed', durationMs: Date.now() - startedAt.getTime() });
|
|
634
|
-
await endTrace(traceId, 'ok');
|
|
635
|
-
Object.assign(parentTask, {
|
|
636
|
-
status: 'completed',
|
|
637
|
-
endedAt: endedAt.toISOString(),
|
|
638
|
-
durationSeconds: duration,
|
|
639
|
-
validationPassed: true,
|
|
640
|
-
outputPreview: synthesis.slice(0, 5000),
|
|
641
|
-
liveOutput: undefined,
|
|
642
|
-
});
|
|
643
|
-
writeCodeAgentTask(parentTask);
|
|
644
|
-
await notifyCodeAgentResult(parentTask, getCodeAgent);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
// No validation — mark complete
|
|
648
|
-
const endedAt = new Date();
|
|
649
|
-
addEvent(traceId, { type: 'complete', summary: 'Team completed (no validation)', durationMs: Date.now() - startedAt.getTime() });
|
|
650
|
-
await endTrace(traceId, 'ok');
|
|
651
|
-
Object.assign(parentTask, {
|
|
652
|
-
status: 'completed',
|
|
653
|
-
endedAt: endedAt.toISOString(),
|
|
654
|
-
durationSeconds: Math.round((endedAt.getTime() - startedAt.getTime()) / 1000),
|
|
655
|
-
outputPreview: synthesis.slice(0, 5000),
|
|
656
|
-
liveOutput: undefined,
|
|
657
|
-
});
|
|
658
|
-
writeCodeAgentTask(parentTask);
|
|
659
|
-
await notifyCodeAgentResult(parentTask, getCodeAgent);
|
|
660
|
-
}
|
|
661
|
-
catch (err) {
|
|
662
|
-
// Clean up any remaining worktrees
|
|
663
|
-
for (const [childId, wt] of _activeWorktrees) {
|
|
664
|
-
try {
|
|
665
|
-
removeWorktree(workdir, childId, wt.branch);
|
|
666
|
-
}
|
|
667
|
-
catch { /* best effort */ }
|
|
668
|
-
}
|
|
669
|
-
_activeWorktrees.clear();
|
|
670
|
-
if (_useWorktrees) {
|
|
671
|
-
try {
|
|
672
|
-
cleanupAllWorktrees(workdir);
|
|
673
|
-
}
|
|
674
|
-
catch { /* best effort */ }
|
|
675
|
-
}
|
|
676
|
-
const errMsg = toErrorMessage(err);
|
|
677
|
-
addEvent(traceId, { type: 'error', summary: errMsg.slice(0, 200), durationMs: Date.now() - startedAt.getTime() });
|
|
678
|
-
await endTrace(traceId, 'error');
|
|
679
|
-
Object.assign(parentTask, {
|
|
680
|
-
status: errMsg.includes(CANCELLED_MESSAGE) || parentTask.status === 'cancelled'
|
|
681
|
-
? 'cancelled'
|
|
682
|
-
: errMsg.includes('timed out')
|
|
683
|
-
? 'timeout'
|
|
684
|
-
: 'failed',
|
|
685
|
-
endedAt: new Date().toISOString(),
|
|
686
|
-
durationSeconds: Math.round((Date.now() - startedAt.getTime()) / 1000),
|
|
687
|
-
error: errMsg,
|
|
688
|
-
liveOutput: undefined,
|
|
689
|
-
});
|
|
690
|
-
writeCodeAgentTask(parentTask);
|
|
691
|
-
if (parentTask.status !== 'cancelled')
|
|
692
|
-
await notifyCodeAgentResult(parentTask, (id) => getCodeAgent(id) ?? null);
|
|
693
|
-
}
|
|
694
|
-
}
|