create-walle 0.9.13 → 0.9.15

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 (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. package/template/builder-journal.md +0 -17
@@ -15,11 +15,60 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
18
+ const { codexRolloutIdFromPath, findCodexSessionFiles } = require('./lib/session-history');
18
19
 
19
20
  const CLAUDE_PROJECTS_DIR = path.join(process.env.HOME, '.claude', 'projects');
20
21
 
21
22
  // --- Detection ---
22
23
 
24
+ function sessionFileIdFromPath(filePath) {
25
+ const virtual = claudeDesktopSessions.parseVirtualSessionPath(filePath);
26
+ if (virtual) return virtual.sessionId;
27
+ const codexId = codexRolloutIdFromPath(filePath);
28
+ if (codexId) return codexId;
29
+ const base = path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
30
+ const uuid = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
31
+ return uuid ? uuid[1].toLowerCase() : base;
32
+ }
33
+
34
+ function fileEntryFromPath(filePath, expectedFileId, projectEntry = '') {
35
+ if (!filePath) return null;
36
+ const actualFileId = sessionFileIdFromPath(filePath);
37
+ if (expectedFileId && actualFileId !== expectedFileId) return null;
38
+ try {
39
+ const sourcePath = claudeDesktopSessions.sourcePathForStat(filePath);
40
+ const stat = fs.statSync(sourcePath);
41
+ if (!stat.isFile()) return null;
42
+ return { filePath, stat, projectEntry };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function addFileIndexEntry(fileIndex, filePath, projectEntry) {
49
+ const file = fileEntryFromPath(filePath, null, projectEntry);
50
+ if (file) fileIndex[sessionFileIdFromPath(filePath)] = file;
51
+ }
52
+
53
+ function resolveDbSessionFile(row, expectedFileId, fileIndex) {
54
+ if (!expectedFileId) return null;
55
+ if (fileIndex[expectedFileId]) return fileIndex[expectedFileId];
56
+
57
+ const stored = fileEntryFromPath(row?.jsonl_path, expectedFileId);
58
+ if (stored) return stored;
59
+
60
+ if ((row?.provider === 'codex') || String(row?.jsonl_path || '').includes(`${path.sep}.codex${path.sep}sessions${path.sep}`)) {
61
+ try {
62
+ for (const filePath of findCodexSessionFiles(expectedFileId)) {
63
+ const file = fileEntryFromPath(filePath, expectedFileId);
64
+ if (file) return file;
65
+ }
66
+ } catch {}
67
+ }
68
+
69
+ return null;
70
+ }
71
+
23
72
  function dbTimestampFromIso(value) {
24
73
  if (!value) return '';
25
74
  const ms = new Date(value).getTime();
@@ -138,7 +187,7 @@ function detectMismatches(db, getAllSessionFiles) {
138
187
  } catch {}
139
188
  const slugCol = hasSlugColumn ? 'a.slug' : "'' AS slug";
140
189
  allSessions = db.prepare(`
141
- SELECT c.id, c.title, c.user_renamed, c.starred, c.project_path, c.cwd,
190
+ SELECT c.id, c.provider, c.title, c.user_renamed, c.starred, c.project_path, c.cwd,
142
191
  c.created_at, c.updated_at,
143
192
  a.agent_session_id, a.jsonl_path, a.file_size, a.first_message,
144
193
  a.modified_at, a.hostname, a.model, a.git_branch, a.user_msg_count,
@@ -156,12 +205,7 @@ function detectMismatches(db, getAllSessionFiles) {
156
205
  const fileIndex = {}; // uuid -> { filePath, stat, projectEntry }
157
206
  try {
158
207
  for (const { filePath, projectEntry } of getAllSessionFiles()) {
159
- const virtual = claudeDesktopSessions.parseVirtualSessionPath(filePath);
160
- const uuid = virtual ? virtual.sessionId : path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
161
- try {
162
- const stat = fs.statSync(claudeDesktopSessions.sourcePathForStat(filePath));
163
- fileIndex[uuid] = { filePath, stat, projectEntry };
164
- } catch {}
208
+ addFileIndexEntry(fileIndex, filePath, projectEntry);
165
209
  }
166
210
  } catch (e) {
167
211
  issues.push({ type: 'scan_error', severity: 'warning', sessionId: null,
@@ -183,7 +227,7 @@ function detectMismatches(db, getAllSessionFiles) {
183
227
 
184
228
  // Skip DB-only rows with no file expectation (legacy tabs with no agent_session_id)
185
229
  const expectedFileId = (agentId && agentId !== sid) ? agentId : sid;
186
- const file = fileIndex[expectedFileId];
230
+ const file = resolveDbSessionFile(row, expectedFileId, fileIndex);
187
231
 
188
232
  // Check 1: Missing file
189
233
  if (!file && row.file_size > 0) {
@@ -193,6 +237,7 @@ function detectMismatches(db, getAllSessionFiles) {
193
237
  expected_file_id: expectedFileId,
194
238
  db_file_size: row.file_size,
195
239
  db_jsonl_path: row.jsonl_path || '',
240
+ db_provider: row.provider || '',
196
241
  db_title: row.title || '',
197
242
  },
198
243
  suggestion: 'File may have been deleted or moved. Check .jsonl.bak variant.',
@@ -215,6 +260,7 @@ function detectMismatches(db, getAllSessionFiles) {
215
260
  db_file_size: row.file_size,
216
261
  actual_file_size: file.stat.size,
217
262
  size_diff: sizeDiff,
263
+ db_jsonl_path: row.jsonl_path || '',
218
264
  },
219
265
  suggestion: 'DB metadata is stale — will be refreshed on next session list load.',
220
266
  });
@@ -439,12 +485,7 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
439
485
  const fileIndex = {};
440
486
  try {
441
487
  for (const { filePath, projectEntry } of getAllSessionFiles()) {
442
- const virtual = claudeDesktopSessions.parseVirtualSessionPath(filePath);
443
- const uuid = virtual ? virtual.sessionId : path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
444
- try {
445
- const stat = fs.statSync(claudeDesktopSessions.sourcePathForStat(filePath));
446
- fileIndex[uuid] = { filePath, stat, projectEntry };
447
- } catch {}
488
+ addFileIndexEntry(fileIndex, filePath, projectEntry);
448
489
  }
449
490
  } catch {}
450
491
 
@@ -505,7 +546,9 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
505
546
  case 'stale_metadata': {
506
547
  // Refresh metadata from actual file
507
548
  const fileId = issue.details.file_id;
508
- const file = fileIndex[fileId];
549
+ const file = fileIndex[fileId]
550
+ || fileEntryFromPath(issue.details?.db_jsonl_path, fileId)
551
+ || resolveDbSessionFile({ provider: 'codex', jsonl_path: issue.details?.db_jsonl_path || '' }, fileId, fileIndex);
509
552
  if (!file) { result.skipped++; break; }
510
553
  try {
511
554
  db.prepare('UPDATE agent_sessions SET file_size = ?, modified_at = ?, updated_at = datetime(\'now\') WHERE ctm_session_id = ?')
@@ -555,11 +598,14 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
555
598
  return (b.updated_at || '').localeCompare(a.updated_at || '');
556
599
  });
557
600
  const keeper = rows[0];
558
- for (const row of rows.slice(1)) {
559
- // Unlink the agent_session from the losing ctm_session (delete the agent_sessions row for this CTM)
560
- db.prepare("DELETE FROM agent_sessions WHERE ctm_session_id = ? AND agent_session_id = ?")
561
- .run(row.id, issue.details.agent_session_id);
562
- }
601
+ const unlinkLosers = db.transaction((losers) => {
602
+ for (const row of losers) {
603
+ // Unlink the agent_session from the losing ctm_session (delete the agent_sessions row for this CTM)
604
+ db.prepare("DELETE FROM agent_sessions WHERE ctm_session_id = ? AND agent_session_id = ?")
605
+ .run(row.id, issue.details.agent_session_id);
606
+ }
607
+ });
608
+ unlinkLosers(rows.slice(1));
563
609
  result.fixed++;
564
610
  result.actions.push(`Resolved duplicate claim on ${issue.details.agent_session_id.slice(0, 8)}: kept ${keeper.id.slice(0, 8)}, unlinked ${rows.length - 1} others`);
565
611
  } catch {
@@ -574,14 +620,17 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
574
620
  const agentId = issue.details.agent_session_id;
575
621
  if (!agentId) { result.skipped++; break; }
576
622
  try {
577
- // Create a new ctm_session using the agent_session_id as the CTM id
578
- db.prepare(`
579
- INSERT OR IGNORE INTO ctm_sessions (id, provider, project_path, cwd, title)
580
- VALUES (?, 'claude', ?, ?, '')
581
- `).run(agentId, issue.details.project_path || '', issue.details.project_path || '');
582
- // Link the agent_session to its new parent
583
- db.prepare('UPDATE agent_sessions SET ctm_session_id = ? WHERE agent_session_id = ?')
584
- .run(agentId, agentId);
623
+ const txn = db.transaction(() => {
624
+ // Create a new ctm_session using the agent_session_id as the CTM id
625
+ db.prepare(`
626
+ INSERT OR IGNORE INTO ctm_sessions (id, provider, project_path, cwd, title)
627
+ VALUES (?, 'claude', ?, ?, '')
628
+ `).run(agentId, issue.details.project_path || '', issue.details.project_path || '');
629
+ // Link the agent_session to its new parent
630
+ db.prepare('UPDATE agent_sessions SET ctm_session_id = ? WHERE agent_session_id = ?')
631
+ .run(agentId, agentId);
632
+ });
633
+ txn();
585
634
  result.fixed++;
586
635
  result.actions.push(`Created ctm_session parent for orphan agent ${agentId.slice(0, 8)}`);
587
636
  } catch (e) {
@@ -28,12 +28,12 @@ const ABOVE_ANCHOR_DEPTH = 40;
28
28
  // Claude Code: "Esc to cancel". Codex: "Press enter to confirm or esc to cancel".
29
29
  const ANCHOR_RE = /Esc to cancel|esc to cancel|Press enter to confirm/;
30
30
 
31
- // Yes-option pattern. Accepts an optional selection-arrow prefix in any of
31
+ // Approval-option pattern. Accepts an optional selection-arrow prefix in any of
32
32
  // the forms different CLIs use: ❯ (Claude Code), ›/▶/▸ (Cursor/others), or
33
33
  // plain ASCII > (Codex). Without this, Codex's "> 1. Yes, proceed (y)" would
34
34
  // be skipped over and the validator would lock onto option 2 ("2. Yes, ...")
35
35
  // — which is unstyled in Codex's renderer and trips no-widget-formatting.
36
- const YES_RE = /^\s*(?:[❯›▶▸>]\s*)?\d+\.\s*Yes\b/i;
36
+ const YES_RE = /^\s*(?:[❯›▶▸>]\s*)?\d+\.\s*(?:Yes|Allow)\b/i;
37
37
 
38
38
  /**
39
39
  * Check if the terminal is currently displaying an active approval widget.
@@ -142,9 +142,10 @@ function _hasWidgetFormatting(buf, yesRow, totalRows) {
142
142
  const yesText = yesLine.translateToString(true);
143
143
  if (/[❯›▶▸]/.test(yesText)) return true;
144
144
 
145
- // Check for "❯" marker anywhere in bottom 3 rows
146
- const arrowScanStart = Math.max(yesRow - 1, totalRows - 5);
147
- for (let row = arrowScanStart; row < totalRows; row++) {
145
+ // Check for a selection marker near the approval options. Codex MCP forms can
146
+ // select option 2 ("Allow for this session"), while option 1 is the first
147
+ // approval-shaped line used for anchoring.
148
+ for (let row = Math.max(0, yesRow - 1); row < Math.min(totalRows, yesRow + 8); row++) {
148
149
  const line = buf.getLine(buf.viewportY + row);
149
150
  if (!line) continue;
150
151
  const text = line.translateToString(true);
@@ -152,6 +153,15 @@ function _hasWidgetFormatting(buf, yesRow, totalRows) {
152
153
  if (/[❯›▶▸]/.test(text)) return true;
153
154
  }
154
155
 
156
+ // Check for "❯" marker anywhere in bottom 5 rows for prompts whose option
157
+ // block is pushed down by long wrapped content.
158
+ for (let row = Math.max(0, totalRows - 5); row < totalRows; row++) {
159
+ const line = buf.getLine(buf.viewportY + row);
160
+ if (!line) continue;
161
+ const text = line.translateToString(true);
162
+ if (/[❯›▶▸]/.test(text)) return true;
163
+ }
164
+
155
165
  // Check for ANSI foreground color on the Yes-option line.
156
166
  // xterm's BufferLine.getCell(x) returns an IBufferCell with .getFgColor()
157
167
  // (0 = default). Any non-default fg color = styled = widget.
@@ -36,14 +36,13 @@ parentPort.on('message', (msg) => {
36
36
  const insert = d.prepare('INSERT OR REPLACE INTO scrollback_log (ctm_session_id, chunk_seq, data) VALUES (?, ?, ?)');
37
37
  const txn = d.transaction((items) => {
38
38
  for (const c of items) insert.run(msg.sessionId, c.seq, c.data);
39
+ const maxSeq = items[items.length - 1]?.seq || 0;
40
+ if (maxSeq > SCROLLBACK_MAX_CHUNKS) {
41
+ const cutoff = maxSeq - SCROLLBACK_MAX_CHUNKS;
42
+ d.prepare('DELETE FROM scrollback_log WHERE ctm_session_id = ? AND chunk_seq < ?').run(msg.sessionId, cutoff);
43
+ }
39
44
  });
40
45
  txn(msg.chunks);
41
- // Prune oldest chunks
42
- const maxSeq = msg.chunks[msg.chunks.length - 1].seq;
43
- if (maxSeq > SCROLLBACK_MAX_CHUNKS) {
44
- const cutoff = maxSeq - SCROLLBACK_MAX_CHUNKS;
45
- d.prepare('DELETE FROM scrollback_log WHERE ctm_session_id = ? AND chunk_seq < ?').run(msg.sessionId, cutoff);
46
- }
47
46
  if (msg.requestId) parentPort.postMessage({ type: 'appended', requestId: msg.requestId });
48
47
  } catch (e) {
49
48
  // Non-fatal — scrollback is best-effort
@@ -47,6 +47,12 @@ function isCodexStatusRedraw(data) {
47
47
  module.exports = {
48
48
  ...baseDetector,
49
49
  id: 'codex',
50
+ // Codex's ratatui status frames arrive in bursts. A short Claude-style
51
+ // debounce lets the sidebar bounce between Running and Waiting/Idle while the
52
+ // terminal still says "Working". Keep the busy state stable long enough for
53
+ // multiple server heartbeats to confirm or renew it, while explicit
54
+ // approval/choice prompts still bypass this elsewhere.
55
+ idleDebounceMs: 15000,
50
56
 
51
57
  isActiveChunk(data) {
52
58
  if (!baseDetector.isActiveChunk(data)) return false;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {
@@ -14,6 +14,8 @@ const CLAUDE_ENV_KEYS = [
14
14
  'CLAUDE_CODE',
15
15
  'CLAUDE_CODE_ENTRYPOINT',
16
16
  'CLAUDE_CODE_ENABLE_TELEMETRY',
17
+ 'ANTHROPIC_API_KEY',
18
+ 'ANTHROPIC_AUTH_TOKEN',
17
19
  ];
18
20
 
19
21
  function buildClaudeArgs(prompt, opts = {}) {
@@ -109,7 +109,8 @@ function bootstrapSkills() {
109
109
  description: 'Scan Claude Code session files for new conversations',
110
110
  trigger_type: 'interval',
111
111
  trigger_config: JSON.stringify({ interval_ms: 60000 }),
112
- prompt_template: 'Scan the Claude Code session directory at ~/.claude/projects/ for any new or updated .jsonl session files. Read the most recently modified files and extract user messages and assistant responses as observations.',
112
+ prompt_template: 'INTERNAL_SKILL:scan-ctm-sessions',
113
+ execution: 'script',
113
114
  });
114
115
 
115
116
  brain.insertSkill({
@@ -140,16 +141,43 @@ function bootstrapSkills() {
140
141
  function syncBundledSkills() {
141
142
  const filesystemSkills = loadAllSkills();
142
143
  const dbSkills = brain.listSkills({});
143
- const dbNames = new Set(dbSkills.map(s => s.name));
144
+ const dbByName = new Map(dbSkills.map(s => [s.name, s]));
145
+ const dbNames = new Set(dbByName.keys());
144
146
 
145
147
  let added = 0;
148
+ let updated = 0;
146
149
  for (const skill of filesystemSkills) {
147
- if (dbNames.has(skill.name)) continue;
148
-
149
150
  const triggerType = (skill.trigger && skill.trigger.type) || skill.execution || 'manual';
150
151
  const triggerConfig = skill.trigger && skill.trigger.interval_ms
151
152
  ? JSON.stringify({ interval_ms: skill.trigger.interval_ms })
152
153
  : null;
154
+ const promptTemplate = skill.execution === 'script'
155
+ ? `INTERNAL_SKILL:${skill.name}`
156
+ : skill.instructions || '';
157
+
158
+ if (dbNames.has(skill.name)) {
159
+ const existing = dbByName.get(skill.name);
160
+ // Upgrade legacy prompt-based CTM scanning to the deterministic script
161
+ // path. Session continuity must not depend on live LLM/network access.
162
+ if (skill.name === 'scan-ctm-sessions' && skill.execution === 'script' && existing) {
163
+ const updates = {};
164
+ if (existing.execution !== 'script') updates.execution = 'script';
165
+ if (existing.prompt_template !== promptTemplate) updates.prompt_template = promptTemplate;
166
+ if (existing.trigger_type !== triggerType) updates.trigger_type = triggerType;
167
+ if (triggerConfig && existing.trigger_config !== triggerConfig) updates.trigger_config = triggerConfig;
168
+ if (existing.auto_disabled_at) {
169
+ updates.enabled = 1;
170
+ updates.auto_disabled_at = null;
171
+ }
172
+ if (existing.auto_disabled_reason) updates.auto_disabled_reason = null;
173
+ if (Object.keys(updates).length > 0) {
174
+ brain.updateSkill(existing.id, updates);
175
+ updated++;
176
+ console.log(`[wall-e] Updated bundled skill: ${skill.name}`);
177
+ }
178
+ }
179
+ continue;
180
+ }
153
181
 
154
182
  brain.insertSkill({
155
183
  name: skill.name,
@@ -159,9 +187,7 @@ function syncBundledSkills() {
159
187
  // Persist the legacy `INTERNAL_SKILL:` marker so downgrades can still
160
188
  // dispatch script skills via the prompt_template fallback. The schema
161
189
  // column `execution` is the authoritative source going forward.
162
- prompt_template: skill.execution === 'script'
163
- ? `INTERNAL_SKILL:${skill.name}`
164
- : skill.instructions || '',
190
+ prompt_template: promptTemplate,
165
191
  execution: skill.execution === 'script' ? 'script' : 'agent',
166
192
  });
167
193
  added++;
@@ -171,6 +197,9 @@ function syncBundledSkills() {
171
197
  if (added > 0) {
172
198
  console.log(`[wall-e] Synced ${added} new bundled skill(s) to DB`);
173
199
  }
200
+ if (updated > 0) {
201
+ console.log(`[wall-e] Updated ${updated} bundled skill(s) in DB`);
202
+ }
174
203
  }
175
204
 
176
205
  function bootstrapTasks() {
@@ -185,6 +214,7 @@ async function checkForUpdates() {
185
214
  try {
186
215
  const { execFile } = require('node:child_process');
187
216
  const { promisify } = require('node:util');
217
+ const telemetry = require('./telemetry');
188
218
  const execFileAsync = promisify(execFile);
189
219
  const pkgPath = path.join(__dirname, '..', 'package.json');
190
220
  const current = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
@@ -200,11 +230,21 @@ async function checkForUpdates() {
200
230
  brain.setKv('update_available', JSON.stringify({
201
231
  current, latest, checked_at: new Date().toISOString(),
202
232
  }));
233
+ telemetry.track('upgrade_check', { status: 'available', current, latest });
234
+ telemetry.trackFunnelStep('upgrade_available');
203
235
  console.log(`[wall-e] Update available: ${current} -> ${latest}`);
204
236
  } else {
205
237
  brain.setKv('update_available', '');
238
+ telemetry.track('upgrade_check', { status: 'current', current, latest: latest || 'unknown' });
206
239
  }
207
- } catch {
240
+ } catch (err) {
241
+ try {
242
+ const telemetry = require('./telemetry');
243
+ telemetry.track('upgrade_check', {
244
+ status: 'failed',
245
+ reason: err?.code || err?.name || 'error',
246
+ });
247
+ } catch {}
208
248
  // npm not available or network error — skip silently
209
249
  }
210
250
  }
@@ -386,6 +426,17 @@ async function main() {
386
426
  process.exit(e && e.code === 'EADDRINUSE' ? 98 : 1);
387
427
  }
388
428
 
429
+ // Slack Socket Mode is optional and event-driven. It only starts when an
430
+ // app-level xapp token is configured, so the existing polling path remains
431
+ // unchanged for workspaces that have not registered a Wall-E Slack app.
432
+ let slackSocketMode = null;
433
+ try {
434
+ const { startSlackSocketModeListener } = require('./slack/socket-mode-listener');
435
+ slackSocketMode = startSlackSocketModeListener();
436
+ } catch (e) {
437
+ console.warn('[wall-e] Slack Socket Mode startup skipped:', e.message);
438
+ }
439
+
389
440
  // Start telemetry (anonymous, opt-out via WALLE_TELEMETRY=0)
390
441
  telemetry.printNoticeIfFirstRun();
391
442
  telemetry.start();
@@ -613,6 +664,7 @@ async function main() {
613
664
  startDelayMs: 25000,
614
665
  debounceMs: 30000,
615
666
  onResult: (r) => {
667
+ if (r?.skipped) return;
616
668
  if (r.decision !== 'noop' && r.decision !== 'observed') {
617
669
  console.log(`[wall-e] Initiative: ${r.decision} — ${r.reasoning?.slice(0, 100)}`);
618
670
  }
@@ -923,6 +975,9 @@ async function main() {
923
975
  for (const ch of channels) {
924
976
  try { await ch.stop(); } catch {}
925
977
  }
978
+ if (slackSocketMode) {
979
+ try { slackSocketMode.stop(); } catch {}
980
+ }
926
981
 
927
982
  telemetry.track('shutdown', { uptime: process.uptime() });
928
983
  telemetry.stop();