create-walle 0.9.11 → 0.9.13

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 (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
@@ -5,49 +5,12 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const execFileAsync = promisify(execFile);
7
7
  const execAsync = promisify(exec);
8
+ const { SHELL_ALLOWLIST, SHELL_DENYLIST } = require('./shell-policy');
8
9
 
9
10
  const HOME = process.env.HOME || '/tmp';
10
11
 
11
12
  // ── Shell execution (expanded allowlist) ──
12
13
 
13
- const SHELL_ALLOWLIST = new Set([
14
- // File ops
15
- 'ls', 'cat', 'head', 'tail', 'wc', 'find', 'file', 'stat', 'du', 'df',
16
- 'mkdir', 'cp', 'mv', 'touch', 'tee', 'readlink', 'realpath',
17
- // Path utilities
18
- 'cd', 'pwd', 'basename', 'dirname', 'which', 'type', 'command',
19
- // Search
20
- 'grep', 'rg', 'ag', 'fzf',
21
- // Text processing
22
- 'sort', 'uniq', 'cut', 'tr', 'awk', 'sed', 'jq', 'yq', 'diff', 'comm',
23
- 'xargs', 'tee', 'fmt', 'column',
24
- // Network
25
- 'curl', 'wget', 'ping', 'dig', 'host', 'nslookup',
26
- // Dev tools
27
- 'git', 'node', 'npm', 'npx', 'python3', 'pip3', 'bun', 'deno',
28
- 'make', 'cargo', 'go', 'ruby', 'perl',
29
- // Cloud / infra
30
- 'fly', 'docker', 'kubectl',
31
- // System info
32
- 'date', 'echo', 'env', 'whoami', 'hostname', 'uname', 'uptime', 'ps', 'top',
33
- 'id', 'groups', 'printenv', 'locale', 'lsof',
34
- // macOS specific
35
- 'open', 'pbcopy', 'pbpaste', 'say', 'defaults', 'mdfind', 'mdls',
36
- 'osascript', 'screencapture', 'sw_vers', 'system_profiler',
37
- // Archive
38
- 'tar', 'zip', 'unzip', 'gzip', 'gunzip', 'xz', 'bzip2',
39
- // Misc utilities
40
- 'less', 'more', 'true', 'false', 'yes', 'test', 'expr', 'seq', 'sleep',
41
- 'md5', 'shasum', 'base64', 'xxd',
42
- ]);
43
-
44
- // Commands that are NEVER allowed regardless of context
45
- const SHELL_DENYLIST = new Set([
46
- 'rm', 'rmdir', 'kill', 'killall', 'shutdown', 'reboot',
47
- 'sudo', 'su', 'chmod', 'chown', 'chgrp',
48
- 'dd', 'mkfs', 'fdisk', 'diskutil',
49
- ]);
50
-
51
14
  /**
52
15
  * Shell escape a single argument for safe interpolation.
53
16
  */
@@ -375,20 +338,41 @@ async function readFile(filePath, { max_bytes, offset, limit, sessionId, project
375
338
  content: numbered.join('\n') + '\n\n' + footer,
376
339
  truncated: more || byteCut,
377
340
  total_size: stat.size,
341
+ ...loadReadInstructionMetadata(resolved, { sessionId, projectRoot }),
378
342
  };
379
343
  }
380
344
 
345
+ function loadReadInstructionMetadata(filePath, { sessionId, projectRoot } = {}) {
346
+ if (!sessionId || !projectRoot) return {};
347
+ try {
348
+ const { InstructionService } = require('../coding/instruction-service');
349
+ const result = new InstructionService().discoverForRead(filePath, {
350
+ projectRoot,
351
+ sessionId,
352
+ });
353
+ if (!result.instructions.length) return {};
354
+ return {
355
+ instruction_paths: result.loadedPaths,
356
+ instructions: result.instructions,
357
+ };
358
+ } catch {
359
+ return {};
360
+ }
361
+ }
362
+
381
363
  async function writeFile(filePath, content, { sessionId, projectRoot } = {}) {
382
364
  const resolved = resolveToolPath(filePath, projectRoot);
383
365
  // Assert file unchanged since last read (stale edit prevention)
366
+ let FileTracker = null;
384
367
  if (sessionId) {
385
- const FileTracker = require('./file-tracker');
368
+ FileTracker = require('./file-tracker');
386
369
  FileTracker.assertUnchanged(resolved, sessionId);
387
370
  }
388
371
  // Create parent dirs if needed
389
372
  const dir = path.dirname(resolved);
390
373
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
391
374
  fs.writeFileSync(resolved, content, 'utf8');
375
+ if (FileTracker) FileTracker.recordCurrent(resolved, sessionId);
392
376
  return { written: true, path: resolved, bytes: Buffer.byteLength(content) };
393
377
  }
394
378
 
@@ -1376,8 +1360,8 @@ async function executeLocalTool(name, input) {
1376
1360
  case 'list_directory': return listDirectory(input);
1377
1361
  case 'claude_code': return runClaudeCode(input);
1378
1362
  case 'edit_file': return editFile(input.file_path, input.old_string, input.new_string, input.replace_all, { sessionId: input.sessionId, projectRoot: input.projectRoot });
1379
- case 'apply_patch': return applyPatchTool(input.patch_text, { projectRoot: input.projectRoot });
1380
- case 'multi_edit': return multiEditTool(input.file_path, input.edits, { projectRoot: input.projectRoot });
1363
+ case 'apply_patch': return applyPatchTool(input.patch_text, { sessionId: input.sessionId, projectRoot: input.projectRoot });
1364
+ case 'multi_edit': return multiEditTool(input.file_path, input.edits, { sessionId: input.sessionId, projectRoot: input.projectRoot });
1381
1365
  default: return null; // not a local tool
1382
1366
  }
1383
1367
  }
@@ -1393,34 +1377,78 @@ async function editFile(filePath, oldString, newString, replaceAll = false, { se
1393
1377
  if (oldString === newString) throw new Error('old_string and new_string must differ');
1394
1378
  const resolved = resolveToolPath(filePath, projectRoot);
1395
1379
  // Assert file unchanged since last read (stale edit prevention)
1380
+ let FileTracker = null;
1396
1381
  if (sessionId) {
1397
- const FileTracker = require('./file-tracker');
1382
+ FileTracker = require('./file-tracker');
1398
1383
  FileTracker.assertUnchanged(resolved, sessionId);
1399
1384
  }
1400
1385
  const content = fs.readFileSync(resolved, 'utf8');
1401
- const updated = fuzzyReplace(content, oldString, newString, replaceAll);
1386
+ let updated;
1387
+ try {
1388
+ updated = fuzzyReplace(content, oldString, newString, replaceAll);
1389
+ } catch (err) {
1390
+ if (isAlreadyAppliedEdit(content, oldString, newString)) {
1391
+ if (FileTracker) FileTracker.recordCurrent(resolved, sessionId);
1392
+ return { edited: false, unchanged: true, already_applied: true, path: resolved, bytes: Buffer.byteLength(content) };
1393
+ }
1394
+ throw err;
1395
+ }
1402
1396
  // Defense in depth: replace() should throw on no-match, but if any future
1403
1397
  // code path returns content unchanged, refuse to silently "succeed" with
1404
1398
  // a no-op. The agent must see the failure and try again rather than
1405
1399
  // believing the file was edited when it wasn't.
1406
1400
  if (updated === content) {
1401
+ if (isAlreadyAppliedEdit(content, oldString, newString)) {
1402
+ if (FileTracker) FileTracker.recordCurrent(resolved, sessionId);
1403
+ return { edited: false, unchanged: true, already_applied: true, path: resolved, bytes: Buffer.byteLength(content) };
1404
+ }
1407
1405
  throw new Error(
1408
1406
  'edit_file no-op: applying old_string -> new_string left the file byte-identical. The match likely missed; provide more unique old_string context.'
1409
1407
  );
1410
1408
  }
1411
1409
  fs.writeFileSync(resolved, updated, 'utf8');
1410
+ if (FileTracker) FileTracker.recordCurrent(resolved, sessionId);
1412
1411
  return { edited: true, path: resolved, bytes: Buffer.byteLength(updated) };
1413
1412
  }
1414
1413
 
1415
- async function applyPatchTool(patchText, { projectRoot } = {}) {
1414
+ function isAlreadyAppliedEdit(content, oldString, newString) {
1415
+ if (!newString || typeof newString !== 'string') return false;
1416
+ if (content.includes(oldString)) return false;
1417
+ return content.includes(newString);
1418
+ }
1419
+
1420
+ async function applyPatchTool(patchText, { sessionId, projectRoot } = {}) {
1416
1421
  if (!patchText) throw new Error('patch_text is required');
1417
- return applyPatch(patchText, { baseDir: projectRoot });
1422
+ const result = await applyPatch(patchText, { baseDir: projectRoot });
1423
+ if (sessionId) {
1424
+ const FileTracker = require('./file-tracker');
1425
+ const trackingWarnings = [];
1426
+ for (const filePath of [...(result.added || []), ...(result.modified || [])]) {
1427
+ try {
1428
+ FileTracker.recordCurrent(filePath, sessionId);
1429
+ } catch (err) {
1430
+ trackingWarnings.push(`Failed to refresh file tracker for ${filePath}: ${err.message}`);
1431
+ }
1432
+ }
1433
+ for (const filePath of result.deleted || []) {
1434
+ FileTracker.forgetFile(filePath, sessionId);
1435
+ }
1436
+ if (trackingWarnings.length > 0) result.tracking_warnings = trackingWarnings;
1437
+ }
1438
+ return result;
1418
1439
  }
1419
1440
 
1420
- async function multiEditTool(filePath, edits, { projectRoot } = {}) {
1441
+ async function multiEditTool(filePath, edits, { sessionId, projectRoot } = {}) {
1421
1442
  if (!filePath || !edits || !Array.isArray(edits)) throw new Error('file_path and edits array are required');
1422
1443
  const resolved = resolveToolPath(filePath, projectRoot);
1423
- return multiEdit(resolved, edits);
1444
+ let FileTracker = null;
1445
+ if (sessionId) {
1446
+ FileTracker = require('./file-tracker');
1447
+ FileTracker.assertUnchanged(resolved, sessionId);
1448
+ }
1449
+ const result = multiEdit(resolved, edits);
1450
+ if (FileTracker && result.edits_applied > 0) FileTracker.recordCurrent(resolved, sessionId);
1451
+ return result;
1424
1452
  }
1425
1453
 
1426
1454
  // ── Enhanced Glob (Phase 9b) ──
@@ -5,6 +5,9 @@
5
5
  'use strict';
6
6
 
7
7
  const crypto = require('node:crypto');
8
+ const fs = require('node:fs');
9
+ const os = require('node:os');
10
+ const path = require('node:path');
8
11
 
9
12
  /**
10
13
  * Session sharing — export coding sessions as JSON and import them back.
@@ -88,6 +91,8 @@ function exportSession(sessionId, brain) {
88
91
  }
89
92
  }
90
93
 
94
+ const transcript = _readWalleTranscriptMetadata(sessionId);
95
+
91
96
  // Store the share token in kv_store for later verification
92
97
  if (typeof brain.setKv === 'function') {
93
98
  brain.setKv(`share:${sessionId}`, JSON.stringify({
@@ -103,6 +108,10 @@ function exportSession(sessionId, brain) {
103
108
  plan,
104
109
  subtasks,
105
110
  events,
111
+ parts: transcript.parts,
112
+ compactions: transcript.compactions,
113
+ snapshots: transcript.snapshots,
114
+ transcriptPath: transcript.transcriptPath,
106
115
  diffs,
107
116
  createdAt,
108
117
  exportedAt: new Date().toISOString(),
@@ -228,4 +237,62 @@ function _tableExists(db, tableName) {
228
237
  }
229
238
  }
230
239
 
231
- module.exports = { exportSession, importSession, verifyShareToken, EXPORT_VERSION };
240
+ function _readWalleTranscriptMetadata(sessionId) {
241
+ const transcriptPath = _findWalleTranscriptPath(sessionId);
242
+ if (!transcriptPath) return { transcriptPath: '', parts: [], compactions: [], snapshots: [] };
243
+
244
+ const parts = [];
245
+ let raw = '';
246
+ try { raw = fs.readFileSync(transcriptPath, 'utf8'); } catch { raw = ''; }
247
+ for (const line of raw.split('\n')) {
248
+ if (!line.trim()) continue;
249
+ let row;
250
+ try { row = JSON.parse(line); } catch { continue; }
251
+ if (row?.type !== 'walle_part') continue;
252
+ const part = {
253
+ uuid: row.uuid || '',
254
+ partType: row.partType || 'event',
255
+ timestamp: row.timestamp || '',
256
+ data: row.data || {},
257
+ };
258
+ parts.push(part);
259
+ }
260
+ return {
261
+ transcriptPath,
262
+ parts,
263
+ compactions: parts.filter((part) => part.partType === 'compaction'),
264
+ snapshots: parts.filter((part) => part.partType === 'snapshot'),
265
+ };
266
+ }
267
+
268
+ function _findWalleTranscriptPath(sessionId) {
269
+ if (!sessionId) return '';
270
+ for (const root of _candidateWalleSessionRoots()) {
271
+ const candidate = path.join(root, `${sessionId}.jsonl`);
272
+ try {
273
+ if (fs.existsSync(candidate)) return candidate;
274
+ } catch { /* ignore inaccessible roots */ }
275
+ }
276
+ return '';
277
+ }
278
+
279
+ function _candidateWalleSessionRoots() {
280
+ const roots = [
281
+ process.env.WALLE_SESSIONS_DIR,
282
+ process.env.WALL_E_SESSIONS_DIR,
283
+ process.env.WALLE_DEV_DIR ? path.join(process.env.WALLE_DEV_DIR, 'sessions') : '',
284
+ process.env.WALL_E_DATA_DIR ? path.join(process.env.WALL_E_DATA_DIR, 'sessions') : '',
285
+ process.env.CTM_DATA_DIR ? path.join(process.env.CTM_DATA_DIR, 'sessions') : '',
286
+ path.join(os.homedir(), '.walle', 'sessions'),
287
+ ].filter(Boolean);
288
+ return [...new Set(roots)];
289
+ }
290
+
291
+ module.exports = {
292
+ exportSession,
293
+ importSession,
294
+ verifyShareToken,
295
+ EXPORT_VERSION,
296
+ _readWalleTranscriptMetadata,
297
+ _findWalleTranscriptPath,
298
+ };
@@ -365,7 +365,7 @@ async function analyzeShellCommand(commandStr, cwd) {
365
365
  const tree = Parser.parse(commandStr);
366
366
  if (!tree) return result;
367
367
 
368
- const { SHELL_ALLOWLIST, SHELL_DENYLIST } = require('./local-tools');
368
+ const { SHELL_ALLOWLIST, SHELL_DENYLIST } = require('./shell-policy');
369
369
  const commandNodes = collectCommandNodes(tree.rootNode);
370
370
 
371
371
  for (const node of commandNodes) {
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ // Shell execution policy shared by the local tool runner and static analyzer.
4
+ // Keep this module dependency-free so shell analysis never imports local-tools.
5
+
6
+ const SHELL_ALLOWLIST = new Set([
7
+ // File ops
8
+ 'ls', 'cat', 'head', 'tail', 'wc', 'find', 'file', 'stat', 'du', 'df',
9
+ 'mkdir', 'cp', 'mv', 'touch', 'tee', 'readlink', 'realpath',
10
+ // Path utilities
11
+ 'cd', 'pwd', 'basename', 'dirname', 'which', 'type', 'command',
12
+ // Search
13
+ 'grep', 'rg', 'ag', 'fzf',
14
+ // Text processing
15
+ 'sort', 'uniq', 'cut', 'tr', 'awk', 'sed', 'jq', 'yq', 'diff', 'comm',
16
+ 'xargs', 'tee', 'fmt', 'column',
17
+ // Network
18
+ 'curl', 'wget', 'ping', 'dig', 'host', 'nslookup',
19
+ // Dev tools
20
+ 'git', 'node', 'npm', 'npx', 'python3', 'pip3', 'bun', 'deno',
21
+ 'make', 'cargo', 'go', 'ruby', 'perl', 'tsc',
22
+ // Cloud / infra
23
+ 'fly', 'docker', 'kubectl',
24
+ // System info
25
+ 'date', 'echo', 'env', 'whoami', 'hostname', 'uname', 'uptime', 'ps', 'top',
26
+ 'id', 'groups', 'printenv', 'locale', 'lsof',
27
+ // macOS specific
28
+ 'open', 'pbcopy', 'pbpaste', 'say', 'defaults', 'mdfind', 'mdls',
29
+ 'osascript', 'screencapture', 'sw_vers', 'system_profiler',
30
+ // Archive
31
+ 'tar', 'zip', 'unzip', 'gzip', 'gunzip', 'xz', 'bzip2',
32
+ // Misc utilities
33
+ 'less', 'more', 'true', 'false', 'yes', 'test', 'expr', 'seq', 'sleep',
34
+ 'md5', 'shasum', 'base64', 'xxd',
35
+ ]);
36
+
37
+ // Commands that are NEVER allowed regardless of context.
38
+ const SHELL_DENYLIST = new Set([
39
+ 'rm', 'rmdir', 'kill', 'killall', 'shutdown', 'reboot',
40
+ 'sudo', 'su', 'chmod', 'chown', 'chgrp',
41
+ 'dd', 'mkfs', 'fdisk', 'diskutil',
42
+ ]);
43
+
44
+ module.exports = {
45
+ SHELL_ALLOWLIST,
46
+ SHELL_DENYLIST,
47
+ };
@@ -171,6 +171,48 @@ class SnapshotManager {
171
171
  return [...this.snapshots.keys()];
172
172
  }
173
173
 
174
+ /**
175
+ * Return current snapshot stack depths by file. SnapshotService uses this as
176
+ * a cheap boundary marker before a coding step starts.
177
+ *
178
+ * @returns {Record<string, number>}
179
+ */
180
+ getStackDepths() {
181
+ const depths = {};
182
+ for (const [filePath, stack] of this.snapshots) {
183
+ depths[filePath] = stack.length;
184
+ }
185
+ return depths;
186
+ }
187
+
188
+ /**
189
+ * Restore files to the state captured immediately after the provided stack
190
+ * depths. If a file was first captured after the boundary, depth 0 restores
191
+ * the pre-boundary file content (or deletes it when it was new).
192
+ *
193
+ * @param {Record<string, number>} depths
194
+ * @returns {string[]} restored file paths
195
+ */
196
+ restoreToStackDepths(depths = {}) {
197
+ const restored = [];
198
+ for (const [filePath, stack] of this.snapshots) {
199
+ const depth = Number.isFinite(depths[filePath]) ? depths[filePath] : 0;
200
+ if (stack.length <= depth) continue;
201
+ const targetIndex = Math.min(depth, stack.length - 1);
202
+ const { content } = stack[targetIndex];
203
+ if (content === null) {
204
+ try { fs.unlinkSync(filePath); } catch { /* already gone */ }
205
+ } else if (Buffer.isBuffer(content)) {
206
+ fs.writeFileSync(filePath, content);
207
+ } else {
208
+ fs.writeFileSync(filePath, content, 'utf8');
209
+ }
210
+ stack.length = targetIndex + 1;
211
+ restored.push(filePath);
212
+ }
213
+ return restored;
214
+ }
215
+
174
216
  /**
175
217
  * Return a unified diff string between the last snapshot and the current file content.
176
218
  * Uses a simple line-by-line diff algorithm (no external dependencies).
@@ -5,6 +5,17 @@ const path = require('path');
5
5
  const { createHash } = require('crypto');
6
6
  const { execSync } = require('child_process');
7
7
 
8
+ let claudeDesktopSessions = null;
9
+ function getClaudeDesktopSessions() {
10
+ if (claudeDesktopSessions) return claudeDesktopSessions;
11
+ try {
12
+ claudeDesktopSessions = require('../../claude-task-manager/lib/claude-desktop-sessions');
13
+ } catch {
14
+ return null;
15
+ }
16
+ return claudeDesktopSessions;
17
+ }
18
+
8
19
  // --- Task type classification ---
9
20
 
10
21
  function classifyTaskType(content) {
@@ -86,6 +97,46 @@ async function harvestClaudeCodeSessions(since) {
86
97
  return samples;
87
98
  }
88
99
 
100
+ // --- Claude Desktop Session Harvesting ---
101
+
102
+ async function harvestClaudeDesktopSessions(since) {
103
+ const reader = getClaudeDesktopSessions();
104
+ if (!reader) return [];
105
+
106
+ const sessions = reader.listSessions();
107
+ const samples = [];
108
+
109
+ for (const session of sessions) {
110
+ if (since && session.updatedAt && session.updatedAt <= since) continue;
111
+ const messages = Array.isArray(session.messages) ? session.messages : [];
112
+ for (let i = 0; i < messages.length - 1; i++) {
113
+ const userMsg = messages[i];
114
+ const assistantMsg = messages[i + 1];
115
+ if (userMsg.role !== 'user' || assistantMsg.role !== 'assistant') continue;
116
+ const userContent = userMsg.text || '';
117
+ const assistantContent = assistantMsg.text || '';
118
+ if (!userContent || userContent.length < 20) continue;
119
+ if (!assistantContent || assistantContent.length < 20) continue;
120
+
121
+ samples.push({
122
+ id: contentHash('claude-desktop', `${session.uuid}:${i}:${userContent}`),
123
+ source: 'claude-desktop',
124
+ session_id: session.uuid,
125
+ timestamp: userMsg.timestamp || session.updatedAt || session.createdAt || new Date().toISOString(),
126
+ task_type: classifyTaskType(userContent),
127
+ prompt: userContent,
128
+ response: assistantContent,
129
+ tool_calls: [],
130
+ outcome: 'unknown',
131
+ outcome_signal: { git_committed: false, git_diff: null, task_status: null, user_corrected: false },
132
+ model: session.model || 'unknown',
133
+ quality_label: 0.5,
134
+ });
135
+ }
136
+ }
137
+ return samples;
138
+ }
139
+
89
140
  // --- Codex Session Harvesting ---
90
141
 
91
142
  async function harvestCodexSessions(since) {
@@ -144,12 +195,13 @@ async function harvestCodexSessions(since) {
144
195
 
145
196
  // --- CTM Session Harvesting ---
146
197
 
147
- async function harvestCtmSessions(since) {
148
- const dataDir = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
198
+ async function harvestCtmSessions(since, dataDirOverride = null) {
199
+ const dataDir = dataDirOverride || process.env.WALL_E_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
149
200
  const ctmDbPath = path.join(dataDir, 'task-manager.db');
150
201
  if (!fs.existsSync(ctmDbPath)) return [];
151
202
 
152
- const Database = require('better-sqlite3');
203
+ let Database;
204
+ try { Database = require('better-sqlite3'); } catch { return []; }
153
205
  let ctmDb;
154
206
  try {
155
207
  ctmDb = new Database(ctmDbPath, { readonly: true, fileMustExist: true });
@@ -405,12 +457,13 @@ async function runHarvest({ incremental = true, brain, dataDir } = {}) {
405
457
 
406
458
  // Harvest from each source
407
459
  const claudeSamples = await harvestClaudeCodeSessions(getSince('claude-code'));
460
+ const claudeDesktopSamples = await harvestClaudeDesktopSessions(getSince('claude-desktop'));
408
461
  const codexSamples = await harvestCodexSessions(getSince('codex'));
409
462
  const chatSamples = await harvestWalleChat(brain, getSince('walle-chat'));
410
463
  const taskSamples = await harvestWalleTasks(brain, getSince('walle-task'));
411
- const ctmSamples = await harvestCtmSessions(getSince('ctm-sessions'));
464
+ const ctmSamples = await harvestCtmSessions(getSince('ctm-sessions'), dataDir);
412
465
 
413
- allSamples.push(...claudeSamples, ...codexSamples, ...chatSamples, ...taskSamples, ...ctmSamples);
466
+ allSamples.push(...claudeSamples, ...claudeDesktopSamples, ...codexSamples, ...chatSamples, ...taskSamples, ...ctmSamples);
414
467
 
415
468
  // Deduplicate by content hash
416
469
  const seen = new Set();
@@ -440,6 +493,9 @@ async function runHarvest({ incremental = true, brain, dataDir } = {}) {
440
493
  if (claudeSamples.length > 0) {
441
494
  brain.updateHarvestState('claude-code', { lastProcessedAt: now, totalHarvested: claudeSamples.length });
442
495
  }
496
+ if (claudeDesktopSamples.length > 0) {
497
+ brain.updateHarvestState('claude-desktop', { lastProcessedAt: now, totalHarvested: claudeDesktopSamples.length });
498
+ }
443
499
  if (codexSamples.length > 0) {
444
500
  brain.updateHarvestState('codex', { lastProcessedAt: now, totalHarvested: codexSamples.length });
445
501
  }
@@ -460,6 +516,7 @@ module.exports = {
460
516
  classifyTaskType,
461
517
  contentHash,
462
518
  harvestClaudeCodeSessions,
519
+ harvestClaudeDesktopSessions,
463
520
  harvestCodexSessions,
464
521
  harvestCtmSessions,
465
522
  harvestWalleChat,