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.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /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
- }