create-walle 0.9.3 → 0.9.4
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 +2 -1
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +5 -1
- package/template/claude-task-manager/public/css/walle.css +317 -0
- package/template/claude-task-manager/public/index.html +404 -101
- package/template/claude-task-manager/public/js/walle.js +1256 -86
- package/template/claude-task-manager/server.js +189 -14
- package/template/docs/site/api/README.md +146 -0
- package/template/docs/site/skills/README.md +99 -5
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +54 -0
- package/template/wall-e/api-walle.js +452 -3
- package/template/wall-e/brain.js +45 -1
- package/template/wall-e/channels/telegram-channel.js +96 -0
- package/template/wall-e/chat.js +61 -2
- package/template/wall-e/coding-context.js +252 -0
- package/template/wall-e/coding-orchestrator.js +625 -0
- package/template/wall-e/coding-review.js +189 -0
- package/template/wall-e/core-tasks.js +12 -3
- package/template/wall-e/deploy.sh +4 -4
- package/template/wall-e/fly.toml +2 -2
- package/template/wall-e/package.json +4 -1
- package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
- package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
- package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
- package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
- package/template/wall-e/skills/_templates/manual-action.md +19 -0
- package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
- package/template/wall-e/skills/_templates/script-runner.md +21 -0
- package/template/wall-e/skills/claude-code-reader.js +16 -4
- package/template/wall-e/skills/skill-executor.js +23 -1
- package/template/wall-e/skills/skill-validator.js +73 -0
- package/template/wall-e/tests/brain.test.js +3 -3
- package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
- package/template/wall-e/tests/coding-context.test.js +212 -0
- package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
- package/template/wall-e/tests/coding-review.test.js +141 -0
- package/template/claude-task-manager/package-lock.json +0 -1607
- package/template/claude-task-manager/tests/test-ai-search.js +0 -61
- package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
- package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
- package/template/claude-task-manager/tests/test-features-v2.js +0 -127
- package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
- package/template/claude-task-manager/tests/test-insights.js +0 -124
- package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
- package/template/claude-task-manager/tests/test-permissions.js +0 -122
- package/template/claude-task-manager/tests/test-pin.js +0 -51
- package/template/claude-task-manager/tests/test-prompts.js +0 -164
- package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
- package/template/claude-task-manager/tests/test-review.js +0 -104
- package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
- package/template/claude-task-manager/tests/test-send-final.js +0 -30
- package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
- package/template/claude-task-manager/tests/test-send-integration.js +0 -107
- package/template/claude-task-manager/tests/test-send-visual.js +0 -34
- package/template/claude-task-manager/tests/test-session-create.js +0 -147
- package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
- package/template/claude-task-manager/tests/test-url-hash.js +0 -68
- package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
- package/template/claude-task-manager/tests/test-ux-review.js +0 -130
- package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
- package/template/claude-task-manager/tests/test-zoom.js +0 -92
- package/template/claude-task-manager/tests/test-zoom2.js +0 -67
- package/template/docs/openclaw-vs-walle-comparison.md +0 -103
- package/template/docs/ux-improvement-plan.md +0 -84
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
- package/template/wall-e/package-lock.json +0 -533
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const { spawn, execFile } = require('node:child_process');
|
|
6
|
+
const { promisify } = require('node:util');
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
const codingContext = require('./coding-context');
|
|
11
|
+
const codingReview = require('./coding-review');
|
|
12
|
+
|
|
13
|
+
const MAX_CUMULATIVE_CONTEXT = 4000;
|
|
14
|
+
const MAX_DIFF_SIZE = 50 * 1024; // 50KB
|
|
15
|
+
|
|
16
|
+
// ─── Pure/unit-testable functions ────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns default config object.
|
|
20
|
+
*/
|
|
21
|
+
function getDefaultConfig() {
|
|
22
|
+
return {
|
|
23
|
+
delivery: 'commit',
|
|
24
|
+
max_retries: 3,
|
|
25
|
+
max_subtasks: 15,
|
|
26
|
+
guardrail_threshold: 10,
|
|
27
|
+
subtask_timeout_ms: 300000,
|
|
28
|
+
subtask_budget_usd: 0.50,
|
|
29
|
+
total_budget_usd: 5.00,
|
|
30
|
+
test_command: 'auto',
|
|
31
|
+
review: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the prompt sent to Claude for planning.
|
|
37
|
+
*/
|
|
38
|
+
function buildPlanningPrompt(request, context, options = {}) {
|
|
39
|
+
const maxSubtasks = options.max_subtasks || getDefaultConfig().max_subtasks;
|
|
40
|
+
|
|
41
|
+
const sections = [];
|
|
42
|
+
|
|
43
|
+
sections.push(`You are a senior software engineer planning a coding task. Break the request into subtasks that can each be executed as an independent Claude Code session.
|
|
44
|
+
|
|
45
|
+
## Request
|
|
46
|
+
${request}`);
|
|
47
|
+
|
|
48
|
+
if (context.claudeMd) {
|
|
49
|
+
sections.push(`## Project Conventions (CLAUDE.md)
|
|
50
|
+
${context.claudeMd}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (context.fileTree) {
|
|
54
|
+
sections.push(`## File Tree
|
|
55
|
+
${context.fileTree}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (context.gitLog) {
|
|
59
|
+
sections.push(`## Recent Commits
|
|
60
|
+
${context.gitLog}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (context.relevantFiles && Object.keys(context.relevantFiles).length > 0) {
|
|
64
|
+
const fileEntries = Object.entries(context.relevantFiles)
|
|
65
|
+
.map(([name, content]) => `### ${name}\n\`\`\`\n${content}\n\`\`\``)
|
|
66
|
+
.join('\n\n');
|
|
67
|
+
sections.push(`## Relevant Source Files
|
|
68
|
+
${fileEntries}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (context.brainMemories && context.brainMemories.length > 0) {
|
|
72
|
+
const memories = context.brainMemories
|
|
73
|
+
.map(m => `- ${m.content || JSON.stringify(m)}`)
|
|
74
|
+
.join('\n');
|
|
75
|
+
sections.push(`## Past Decisions (from memory)
|
|
76
|
+
${memories}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (context.mcpResults && context.mcpResults.length > 0) {
|
|
80
|
+
const results = context.mcpResults
|
|
81
|
+
.map(r => `- [${r.tool}]: ${typeof r.result === 'string' ? r.result : JSON.stringify(r.result)}`)
|
|
82
|
+
.join('\n');
|
|
83
|
+
sections.push(`## Related Specs (MCP)
|
|
84
|
+
${results}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (context.testCommand) {
|
|
88
|
+
sections.push(`## Test Command
|
|
89
|
+
${context.testCommand}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
sections.push(`## Instructions
|
|
93
|
+
Output JSON with the following structure (no other text outside the JSON):
|
|
94
|
+
{
|
|
95
|
+
"subtasks": [
|
|
96
|
+
{ "title": "Short title", "prompt": "Detailed prompt for this subtask" }
|
|
97
|
+
],
|
|
98
|
+
"branch_name": "type/short-description",
|
|
99
|
+
"estimated_scope": "small|medium|large"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Rules:
|
|
103
|
+
- Maximum ${maxSubtasks} subtasks
|
|
104
|
+
- Each subtask should be independently executable
|
|
105
|
+
- Order subtasks so dependencies come first
|
|
106
|
+
- Include test-writing subtasks where appropriate
|
|
107
|
+
- branch_name should follow conventional naming (feat/, fix/, refactor/, etc.)`);
|
|
108
|
+
|
|
109
|
+
return sections.join('\n\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extracts plan JSON from Claude's text output.
|
|
114
|
+
*/
|
|
115
|
+
function parsePlan(output) {
|
|
116
|
+
if (!output) {
|
|
117
|
+
throw new Error('Failed to parse plan: empty output');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Try to find JSON with subtasks array
|
|
121
|
+
// First try: look for ```json ... ``` blocks
|
|
122
|
+
const fencedMatch = output.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
123
|
+
if (fencedMatch) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(fencedMatch[1]);
|
|
126
|
+
if (Array.isArray(parsed.subtasks) && parsed.subtasks.length > 0) {
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// fall through to other methods
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Second try: find JSON by locating the opening brace before "subtasks"
|
|
135
|
+
// and the matching closing brace (balanced brace counting)
|
|
136
|
+
const subtasksIdx = output.indexOf('"subtasks"');
|
|
137
|
+
if (subtasksIdx !== -1) {
|
|
138
|
+
let start = output.lastIndexOf('{', subtasksIdx);
|
|
139
|
+
while (start >= 0) {
|
|
140
|
+
// Find matching closing brace
|
|
141
|
+
let depth = 0;
|
|
142
|
+
let end = -1;
|
|
143
|
+
for (let i = start; i < output.length; i++) {
|
|
144
|
+
if (output[i] === '{') depth++;
|
|
145
|
+
else if (output[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
146
|
+
}
|
|
147
|
+
if (end > start) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(output.slice(start, end + 1));
|
|
150
|
+
if (Array.isArray(parsed.subtasks) && parsed.subtasks.length > 0) {
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Not valid from here
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
start = output.lastIndexOf('{', start - 1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error('Failed to parse plan: no valid JSON with non-empty subtasks array found');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Builds prompt for executing a single subtask.
|
|
166
|
+
*/
|
|
167
|
+
function buildSubtaskPrompt(subtask, cumulativeContext, claudeMd) {
|
|
168
|
+
const sections = [];
|
|
169
|
+
|
|
170
|
+
if (claudeMd) {
|
|
171
|
+
sections.push(`## Project Conventions (CLAUDE.md)
|
|
172
|
+
${claudeMd}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (cumulativeContext) {
|
|
176
|
+
sections.push(`## Context from Previous Steps
|
|
177
|
+
${cumulativeContext}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
sections.push(`## Current Subtask: ${subtask.title}
|
|
181
|
+
${subtask.prompt}`);
|
|
182
|
+
|
|
183
|
+
sections.push(`## Instructions
|
|
184
|
+
- Implement the subtask described above
|
|
185
|
+
- Follow project conventions
|
|
186
|
+
- Write clean, well-documented code
|
|
187
|
+
- If tests are relevant, ensure they pass`);
|
|
188
|
+
|
|
189
|
+
return sections.join('\n\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Writes state object to JSON file.
|
|
194
|
+
*/
|
|
195
|
+
function writeCheckpoint(filePath, state) {
|
|
196
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf8');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reads and parses JSON state file.
|
|
201
|
+
*/
|
|
202
|
+
function readCheckpoint(filePath) {
|
|
203
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
204
|
+
return JSON.parse(raw);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Formats a report object as human-readable string.
|
|
209
|
+
*/
|
|
210
|
+
function formatReport(report) {
|
|
211
|
+
const lines = [];
|
|
212
|
+
|
|
213
|
+
lines.push(`Status: ${report.success ? 'SUCCESS' : 'FAILED'}`);
|
|
214
|
+
|
|
215
|
+
if (report.branch) {
|
|
216
|
+
lines.push(`Branch: ${report.branch}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (report.commit) {
|
|
220
|
+
lines.push(`Commit: ${report.commit}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (report.error) {
|
|
224
|
+
lines.push(`Error: ${report.error}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (report.concerns && report.concerns.length > 0) {
|
|
228
|
+
lines.push('Concerns:');
|
|
229
|
+
for (const concern of report.concerns) {
|
|
230
|
+
lines.push(` - ${concern}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (report.summary) {
|
|
235
|
+
lines.push(`Summary: ${report.summary}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Integration functions ───────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Spawns `claude -p <prompt>` as a headless session.
|
|
245
|
+
*/
|
|
246
|
+
function runHeadless(prompt, { cwd, sessionId, timeoutMs, budgetUsd } = {}) {
|
|
247
|
+
const sid = sessionId || crypto.randomUUID();
|
|
248
|
+
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
const args = [
|
|
251
|
+
'-p', prompt,
|
|
252
|
+
'--output-format', 'json',
|
|
253
|
+
'--max-turns', '50',
|
|
254
|
+
'--session-id', sid,
|
|
255
|
+
];
|
|
256
|
+
if (budgetUsd) {
|
|
257
|
+
args.push('--max-budget-usd', String(budgetUsd));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let proc;
|
|
261
|
+
try {
|
|
262
|
+
proc = spawn('claude', args, {
|
|
263
|
+
cwd: cwd || process.cwd(),
|
|
264
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
265
|
+
timeout: timeoutMs || 300000,
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return resolve({
|
|
269
|
+
success: false,
|
|
270
|
+
output: '',
|
|
271
|
+
stderr: err.message,
|
|
272
|
+
sessionId: sid,
|
|
273
|
+
exitCode: -1,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const MAX_OUTPUT = 512 * 1024; // 512KB cap
|
|
278
|
+
let stdout = '';
|
|
279
|
+
let stderr = '';
|
|
280
|
+
|
|
281
|
+
proc.stdout.on('data', (chunk) => {
|
|
282
|
+
if (stdout.length < MAX_OUTPUT) stdout += chunk;
|
|
283
|
+
});
|
|
284
|
+
proc.stderr.on('data', (chunk) => {
|
|
285
|
+
if (stderr.length < MAX_OUTPUT) stderr += chunk;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
proc.on('error', (err) => {
|
|
289
|
+
resolve({
|
|
290
|
+
success: false,
|
|
291
|
+
output: stdout,
|
|
292
|
+
stderr: err.message,
|
|
293
|
+
sessionId: sid,
|
|
294
|
+
exitCode: -1,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
proc.on('close', (code) => {
|
|
299
|
+
resolve({
|
|
300
|
+
success: code === 0,
|
|
301
|
+
output: stdout,
|
|
302
|
+
stderr,
|
|
303
|
+
sessionId: sid,
|
|
304
|
+
exitCode: code,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Runs test command. Returns { success, output }.
|
|
312
|
+
*/
|
|
313
|
+
async function runTests(cwd, testCommand) {
|
|
314
|
+
try {
|
|
315
|
+
const { stdout, stderr } = await execFileAsync(testCommand, [], {
|
|
316
|
+
cwd,
|
|
317
|
+
shell: true,
|
|
318
|
+
timeout: 120000,
|
|
319
|
+
});
|
|
320
|
+
return { success: true, output: (stdout || '') + (stderr || '') };
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return { success: false, output: ((e.stdout || '') + (e.stderr || '')) };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Gets diff for review. Checks unstaged, staged, and committed. Cap at 50KB.
|
|
328
|
+
*/
|
|
329
|
+
async function getGitDiff(cwd) {
|
|
330
|
+
// Try unstaged
|
|
331
|
+
try {
|
|
332
|
+
const { stdout } = await execFileAsync('git', ['diff'], { cwd });
|
|
333
|
+
if (stdout.trim()) return stdout.slice(0, MAX_DIFF_SIZE);
|
|
334
|
+
} catch { /* ignore */ }
|
|
335
|
+
|
|
336
|
+
// Try staged
|
|
337
|
+
try {
|
|
338
|
+
const { stdout } = await execFileAsync('git', ['diff', '--cached'], { cwd });
|
|
339
|
+
if (stdout.trim()) return stdout.slice(0, MAX_DIFF_SIZE);
|
|
340
|
+
} catch { /* ignore */ }
|
|
341
|
+
|
|
342
|
+
// Try committed
|
|
343
|
+
try {
|
|
344
|
+
const { stdout } = await execFileAsync('git', ['diff', 'HEAD~1', 'HEAD'], { cwd });
|
|
345
|
+
if (stdout.trim()) return stdout.slice(0, MAX_DIFF_SIZE);
|
|
346
|
+
} catch { /* ignore */ }
|
|
347
|
+
|
|
348
|
+
return '';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Assembles context, builds planning prompt, runs headless Claude, parses plan.
|
|
353
|
+
*/
|
|
354
|
+
async function plan(request, cwd, options = {}) {
|
|
355
|
+
const config = { ...getDefaultConfig(), ...options };
|
|
356
|
+
|
|
357
|
+
// Assemble context
|
|
358
|
+
const context = await codingContext.assembleContext(request, cwd, {
|
|
359
|
+
brain: options.brain,
|
|
360
|
+
mcpClient: options.mcpClient,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Resolve test command
|
|
364
|
+
if (config.test_command === 'auto') {
|
|
365
|
+
config.test_command = context.testCommand || null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Build planning prompt
|
|
369
|
+
const prompt = buildPlanningPrompt(request, context, { max_subtasks: config.max_subtasks });
|
|
370
|
+
|
|
371
|
+
// Run headless Claude for planning
|
|
372
|
+
const result = await runHeadless(prompt, {
|
|
373
|
+
cwd,
|
|
374
|
+
timeoutMs: config.subtask_timeout_ms,
|
|
375
|
+
budgetUsd: config.subtask_budget_usd,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (!result.success && !result.output) {
|
|
379
|
+
throw new Error(`Planning failed: ${result.stderr || 'no output'}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Parse plan from output
|
|
383
|
+
const planObj = parsePlan(result.output);
|
|
384
|
+
|
|
385
|
+
// Enforce max_subtasks
|
|
386
|
+
if (planObj.subtasks.length > config.max_subtasks) {
|
|
387
|
+
planObj.subtasks = planObj.subtasks.slice(0, config.max_subtasks);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { plan: planObj, context, config };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Main execution loop. For each subtask: build prompt, run headless, verify, checkpoint.
|
|
395
|
+
*/
|
|
396
|
+
async function execute(planData, { cwd, onProgress, startFrom = 0 } = {}) {
|
|
397
|
+
const { plan: planObj, context, config } = planData;
|
|
398
|
+
const subtasks = planObj.subtasks;
|
|
399
|
+
const sessionIds = [];
|
|
400
|
+
const statePath = path.join(cwd, `.coding-orchestrator-state-${Date.now()}.json`);
|
|
401
|
+
let budgetUsed = 0;
|
|
402
|
+
let cumulativeContext = '';
|
|
403
|
+
|
|
404
|
+
// Create or switch to branch
|
|
405
|
+
try {
|
|
406
|
+
await execFileAsync('git', ['checkout', '-b', planObj.branch_name], { cwd });
|
|
407
|
+
} catch {
|
|
408
|
+
try {
|
|
409
|
+
await execFileAsync('git', ['checkout', planObj.branch_name], { cwd });
|
|
410
|
+
} catch {
|
|
411
|
+
// Already on the branch or no git — continue anyway
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (let i = startFrom; i < subtasks.length; i++) {
|
|
416
|
+
const subtask = subtasks[i];
|
|
417
|
+
|
|
418
|
+
if (onProgress) {
|
|
419
|
+
onProgress({ type: 'subtask_start', index: i, title: subtask.title });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Budget guard
|
|
423
|
+
if (budgetUsed >= config.total_budget_usd) {
|
|
424
|
+
return {
|
|
425
|
+
success: false,
|
|
426
|
+
completed: i,
|
|
427
|
+
session_ids: sessionIds,
|
|
428
|
+
state_path: statePath,
|
|
429
|
+
error: 'Budget exceeded',
|
|
430
|
+
failed_subtask: i,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let succeeded = false;
|
|
435
|
+
let lastError = '';
|
|
436
|
+
|
|
437
|
+
for (let retry = 0; retry <= config.max_retries; retry++) {
|
|
438
|
+
// Build subtask prompt
|
|
439
|
+
const cappedContext = cumulativeContext.length > MAX_CUMULATIVE_CONTEXT
|
|
440
|
+
? cumulativeContext.slice(-MAX_CUMULATIVE_CONTEXT)
|
|
441
|
+
: cumulativeContext;
|
|
442
|
+
|
|
443
|
+
const prompt = buildSubtaskPrompt(subtask, cappedContext, context.claudeMd);
|
|
444
|
+
|
|
445
|
+
// Run headless
|
|
446
|
+
const result = await runHeadless(prompt, {
|
|
447
|
+
cwd,
|
|
448
|
+
timeoutMs: config.subtask_timeout_ms,
|
|
449
|
+
budgetUsd: config.subtask_budget_usd,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
sessionIds.push(result.sessionId);
|
|
453
|
+
budgetUsed += config.subtask_budget_usd;
|
|
454
|
+
|
|
455
|
+
if (!result.success && !result.output) {
|
|
456
|
+
lastError = `Headless session failed: ${result.stderr || 'no output'}`;
|
|
457
|
+
if (onProgress) onProgress({ type: 'retry', index: i, retry, error: lastError });
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Run tests if configured
|
|
462
|
+
let testsOk = true;
|
|
463
|
+
if (config.test_command) {
|
|
464
|
+
const testResult = await runTests(cwd, config.test_command);
|
|
465
|
+
if (!testResult.success) {
|
|
466
|
+
lastError = `Tests failed: ${testResult.output.slice(0, 500)}`;
|
|
467
|
+
if (onProgress) onProgress({ type: 'retry', index: i, retry, error: lastError });
|
|
468
|
+
testsOk = false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Review if configured
|
|
473
|
+
let reviewOk = true;
|
|
474
|
+
if (config.review && testsOk) {
|
|
475
|
+
const diff = await getGitDiff(cwd);
|
|
476
|
+
if (diff) {
|
|
477
|
+
const verdict = await codingReview.reviewSubtask(subtask, diff, { cwd });
|
|
478
|
+
if (!verdict.pass && verdict.severity === 'blocker') {
|
|
479
|
+
lastError = `Review failed: ${verdict.issues.join(', ')}`;
|
|
480
|
+
if (onProgress) onProgress({ type: 'retry', index: i, retry, error: lastError });
|
|
481
|
+
reviewOk = false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (testsOk && reviewOk) {
|
|
487
|
+
succeeded = true;
|
|
488
|
+
// Update cumulative context
|
|
489
|
+
cumulativeContext += `\n[Step ${i + 1}: ${subtask.title}] Completed. Output: ${(result.output || '').slice(0, 200)}`;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Checkpoint
|
|
495
|
+
const state = {
|
|
496
|
+
plan: planObj,
|
|
497
|
+
completed: succeeded ? i + 1 : i,
|
|
498
|
+
session_ids: sessionIds,
|
|
499
|
+
budget_used: budgetUsed,
|
|
500
|
+
cumulative_context: cumulativeContext,
|
|
501
|
+
};
|
|
502
|
+
writeCheckpoint(statePath, state);
|
|
503
|
+
|
|
504
|
+
if (!succeeded) {
|
|
505
|
+
if (onProgress) onProgress({ type: 'subtask_failed', index: i, error: lastError });
|
|
506
|
+
return {
|
|
507
|
+
success: false,
|
|
508
|
+
completed: i,
|
|
509
|
+
session_ids: sessionIds,
|
|
510
|
+
state_path: statePath,
|
|
511
|
+
error: lastError,
|
|
512
|
+
failed_subtask: i,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (onProgress) {
|
|
517
|
+
onProgress({ type: 'subtask_done', index: i, title: subtask.title });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
success: true,
|
|
523
|
+
completed: subtasks.length,
|
|
524
|
+
session_ids: sessionIds,
|
|
525
|
+
state_path: statePath,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Final review, git commit, store memory, cleanup.
|
|
531
|
+
*/
|
|
532
|
+
async function complete(request, planData, executeResult, { cwd, brain, onProgress } = {}) {
|
|
533
|
+
const { plan: planObj, config } = planData;
|
|
534
|
+
const report = {
|
|
535
|
+
success: executeResult.success,
|
|
536
|
+
branch: planObj.branch_name,
|
|
537
|
+
commit: null,
|
|
538
|
+
concerns: [],
|
|
539
|
+
summary: '',
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
if (!executeResult.success) {
|
|
543
|
+
report.error = executeResult.error;
|
|
544
|
+
return report;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Final review
|
|
548
|
+
if (config.review) {
|
|
549
|
+
if (onProgress) onProgress({ type: 'final_review' });
|
|
550
|
+
const diff = await getGitDiff(cwd);
|
|
551
|
+
if (diff) {
|
|
552
|
+
const verdict = await codingReview.reviewFinal(request, diff, { cwd });
|
|
553
|
+
report.concerns = verdict.issues || [];
|
|
554
|
+
report.summary = verdict.summary || '';
|
|
555
|
+
if (!verdict.pass && verdict.severity === 'blocker') {
|
|
556
|
+
report.success = false;
|
|
557
|
+
report.error = 'Final review found blocker issues';
|
|
558
|
+
return report;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Git commit
|
|
564
|
+
if (config.delivery === 'commit' || config.delivery === 'push' || config.delivery === 'pr') {
|
|
565
|
+
try {
|
|
566
|
+
await execFileAsync('git', ['add', '-A'], { cwd });
|
|
567
|
+
const sanitizedRequest = request.replace(/[\r\n]+/g, ' ').trim().slice(0, 72);
|
|
568
|
+
const commitMsg = `feat: ${sanitizedRequest}\n\nOrchestrated by Wall-E coding agent.\nSubtasks: ${planObj.subtasks.length}`;
|
|
569
|
+
const { stdout } = await execFileAsync('git', ['commit', '-m', commitMsg], { cwd });
|
|
570
|
+
// Extract commit hash
|
|
571
|
+
const hashMatch = stdout.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
|
|
572
|
+
if (hashMatch) report.commit = hashMatch[1];
|
|
573
|
+
} catch (e) {
|
|
574
|
+
report.concerns.push(`Git commit failed: ${(e.stderr || e.message || '').slice(0, 200)}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Store memory in brain
|
|
579
|
+
if (brain && typeof brain.storeMemory === 'function') {
|
|
580
|
+
try {
|
|
581
|
+
await brain.storeMemory({
|
|
582
|
+
type: 'coding_task',
|
|
583
|
+
content: `Completed coding task: ${request}. Branch: ${planObj.branch_name}. Subtasks: ${planObj.subtasks.length}.`,
|
|
584
|
+
metadata: {
|
|
585
|
+
branch: planObj.branch_name,
|
|
586
|
+
subtasks: planObj.subtasks.length,
|
|
587
|
+
success: report.success,
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
} catch {
|
|
591
|
+
// non-critical
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Cleanup state file
|
|
596
|
+
if (executeResult.state_path) {
|
|
597
|
+
try {
|
|
598
|
+
fs.unlinkSync(executeResult.state_path);
|
|
599
|
+
} catch {
|
|
600
|
+
// non-critical
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (onProgress) onProgress({ type: 'complete', report });
|
|
605
|
+
|
|
606
|
+
return report;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
module.exports = {
|
|
610
|
+
// Pure/unit-testable
|
|
611
|
+
getDefaultConfig,
|
|
612
|
+
buildPlanningPrompt,
|
|
613
|
+
parsePlan,
|
|
614
|
+
buildSubtaskPrompt,
|
|
615
|
+
writeCheckpoint,
|
|
616
|
+
readCheckpoint,
|
|
617
|
+
formatReport,
|
|
618
|
+
// Integration
|
|
619
|
+
runHeadless,
|
|
620
|
+
runTests,
|
|
621
|
+
getGitDiff,
|
|
622
|
+
plan,
|
|
623
|
+
execute,
|
|
624
|
+
complete,
|
|
625
|
+
};
|