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.
Files changed (75) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/db.js +5 -1
  4. package/template/claude-task-manager/public/css/walle.css +317 -0
  5. package/template/claude-task-manager/public/index.html +404 -101
  6. package/template/claude-task-manager/public/js/walle.js +1256 -86
  7. package/template/claude-task-manager/server.js +189 -14
  8. package/template/docs/site/api/README.md +146 -0
  9. package/template/docs/site/skills/README.md +99 -5
  10. package/template/package.json +1 -1
  11. package/template/wall-e/agent.js +54 -0
  12. package/template/wall-e/api-walle.js +452 -3
  13. package/template/wall-e/brain.js +45 -1
  14. package/template/wall-e/channels/telegram-channel.js +96 -0
  15. package/template/wall-e/chat.js +61 -2
  16. package/template/wall-e/coding-context.js +252 -0
  17. package/template/wall-e/coding-orchestrator.js +625 -0
  18. package/template/wall-e/coding-review.js +189 -0
  19. package/template/wall-e/core-tasks.js +12 -3
  20. package/template/wall-e/deploy.sh +4 -4
  21. package/template/wall-e/fly.toml +2 -2
  22. package/template/wall-e/package.json +4 -1
  23. package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
  24. package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
  25. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
  26. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
  27. package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
  28. package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
  29. package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
  30. package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
  31. package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
  32. package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
  33. package/template/wall-e/skills/_templates/manual-action.md +19 -0
  34. package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
  35. package/template/wall-e/skills/_templates/script-runner.md +21 -0
  36. package/template/wall-e/skills/claude-code-reader.js +16 -4
  37. package/template/wall-e/skills/skill-executor.js +23 -1
  38. package/template/wall-e/skills/skill-validator.js +73 -0
  39. package/template/wall-e/tests/brain.test.js +3 -3
  40. package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
  41. package/template/wall-e/tests/coding-context.test.js +212 -0
  42. package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
  43. package/template/wall-e/tests/coding-review.test.js +141 -0
  44. package/template/claude-task-manager/package-lock.json +0 -1607
  45. package/template/claude-task-manager/tests/test-ai-search.js +0 -61
  46. package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
  47. package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
  48. package/template/claude-task-manager/tests/test-features-v2.js +0 -127
  49. package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
  50. package/template/claude-task-manager/tests/test-insights.js +0 -124
  51. package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
  52. package/template/claude-task-manager/tests/test-permissions.js +0 -122
  53. package/template/claude-task-manager/tests/test-pin.js +0 -51
  54. package/template/claude-task-manager/tests/test-prompts.js +0 -164
  55. package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
  56. package/template/claude-task-manager/tests/test-review.js +0 -104
  57. package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
  58. package/template/claude-task-manager/tests/test-send-final.js +0 -30
  59. package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
  60. package/template/claude-task-manager/tests/test-send-integration.js +0 -107
  61. package/template/claude-task-manager/tests/test-send-visual.js +0 -34
  62. package/template/claude-task-manager/tests/test-session-create.js +0 -147
  63. package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
  64. package/template/claude-task-manager/tests/test-url-hash.js +0 -68
  65. package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
  66. package/template/claude-task-manager/tests/test-ux-review.js +0 -130
  67. package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
  68. package/template/claude-task-manager/tests/test-zoom.js +0 -92
  69. package/template/claude-task-manager/tests/test-zoom2.js +0 -67
  70. package/template/docs/openclaw-vs-walle-comparison.md +0 -103
  71. package/template/docs/ux-improvement-plan.md +0 -84
  72. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
  73. package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
  74. package/template/wall-e/package-lock.json +0 -533
  75. 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
+ };