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
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawn } = require('node:child_process');
6
+ const { findSkill } = require('./skill-loader');
7
+
8
+ const WALL_E_DIR = path.join(__dirname, '..');
9
+
10
+ async function runScriptSkillByName(skillName, task = {}, opts = {}) {
11
+ const skill = opts.skill || findSkill(skillName);
12
+ if (!skill) throw new Error(`Skill not found: ${skillName}`);
13
+ return runScriptSkill(skill, task, opts);
14
+ }
15
+
16
+ function runScriptSkill(skill, task = {}, opts = {}) {
17
+ if (!skill || skill.execution !== 'script') {
18
+ throw new Error(`Skill "${skill?.name || 'unknown'}" is not script-based`);
19
+ }
20
+ const entryPath = resolveSkillEntry(skill);
21
+ const skillConfig = buildSkillConfig(skill, task);
22
+ const args = buildSkillArgs(skill, skillConfig);
23
+ const log = typeof opts.log === 'function' ? opts.log : () => {};
24
+ log(`Running: node ${entryPath}${args.length ? ' ' + args.join(' ') : ''}`);
25
+
26
+ return new Promise((resolve, reject) => {
27
+ const env = {
28
+ ...process.env,
29
+ HOME: process.env.HOME,
30
+ WALL_E_SKILL_CONFIG: JSON.stringify(skillConfig),
31
+ WALL_E_SKILL_DIR: skill.dir,
32
+ WALL_E_SRC_DIR: WALL_E_DIR,
33
+ ...(opts.env || {}),
34
+ };
35
+ if (task.checkpoint) env.WALL_E_CHECKPOINT = task.checkpoint;
36
+
37
+ const child = spawn(process.execPath, [entryPath, ...args], {
38
+ cwd: opts.cwd || WALL_E_DIR,
39
+ env,
40
+ stdio: ['ignore', 'pipe', 'pipe'],
41
+ });
42
+ if (typeof opts.onProcess === 'function') opts.onProcess(child);
43
+
44
+ let stdout = '';
45
+ let stderr = '';
46
+ child.stdout.setEncoding('utf8');
47
+ child.stderr.setEncoding('utf8');
48
+ child.stdout.on('data', (chunk) => {
49
+ stdout += chunk;
50
+ for (const line of chunk.split('\n').filter(Boolean)) {
51
+ log(line);
52
+ if (line.startsWith('CHECKPOINT:') && typeof opts.onCheckpoint === 'function') {
53
+ opts.onCheckpoint(line.slice(11).trim());
54
+ }
55
+ }
56
+ });
57
+ child.stderr.on('data', (chunk) => {
58
+ stderr += chunk;
59
+ for (const line of chunk.split('\n').filter(Boolean)) log(`[stderr] ${line}`);
60
+ });
61
+ child.on('close', (code) => {
62
+ if (typeof opts.onClose === 'function') opts.onClose(child, code);
63
+ if (code !== 0 && code !== null) {
64
+ reject(new Error(`Exit code ${code}: ${stderr.slice(0, 500)}`));
65
+ return;
66
+ }
67
+ let output = stdout.trim();
68
+ if (stderr.trim()) output += '\n[stderr] ' + stderr.trim();
69
+ resolve(output);
70
+ });
71
+ child.on('error', (err) => {
72
+ if (typeof opts.onClose === 'function') opts.onClose(child, null);
73
+ reject(err);
74
+ });
75
+ });
76
+ }
77
+
78
+ function resolveSkillEntry(skill) {
79
+ const entry = skill.entry || 'run.js';
80
+ const entryPath = path.resolve(skill.dir, entry);
81
+ const lexicalSkillRel = path.relative(path.resolve(skill.dir), entryPath);
82
+ const lexicalWallERel = path.relative(WALL_E_DIR, entryPath);
83
+ const lexicallyInsideSkill = !lexicalSkillRel.startsWith('..') && !path.isAbsolute(lexicalSkillRel);
84
+ const lexicallyInsideWallE = !lexicalWallERel.startsWith('..') && !path.isAbsolute(lexicalWallERel);
85
+ if (!lexicallyInsideSkill && !lexicallyInsideWallE) {
86
+ throw new Error(`Script skill ${skill.name} entry escapes skill and Wall-E directories`);
87
+ }
88
+ if (!fs.existsSync(entryPath)) {
89
+ throw new Error(`Skill entry point not found: ${entryPath}`);
90
+ }
91
+ const realEntryPath = fs.realpathSync(entryPath);
92
+ const realSkillDir = fs.realpathSync(skill.dir);
93
+ const realWallEDir = fs.realpathSync(WALL_E_DIR);
94
+ const skillRel = path.relative(realSkillDir, realEntryPath);
95
+ const wallERel = path.relative(realWallEDir, realEntryPath);
96
+ const insideSkillDir = !skillRel.startsWith('..') && !path.isAbsolute(skillRel);
97
+ const insideWallE = !wallERel.startsWith('..') && !path.isAbsolute(wallERel);
98
+ if (!insideSkillDir && !insideWallE) {
99
+ throw new Error(`Script skill ${skill.name} entry escapes skill and Wall-E directories`);
100
+ }
101
+ return realEntryPath;
102
+ }
103
+
104
+ function buildSkillConfig(skill, task = {}) {
105
+ const skillConfig = {};
106
+ if (skill.config) {
107
+ for (const [key, value] of Object.entries(skill.config)) {
108
+ if (value && typeof value === 'object' && value.default !== undefined) {
109
+ skillConfig[key] = value.default;
110
+ }
111
+ }
112
+ }
113
+ let taskConfig = {};
114
+ if (task.skill_config) {
115
+ if (typeof task.skill_config === 'string') {
116
+ try {
117
+ taskConfig = JSON.parse(task.skill_config);
118
+ } catch {
119
+ taskConfig = {};
120
+ }
121
+ } else {
122
+ taskConfig = task.skill_config;
123
+ }
124
+ }
125
+ return { ...skillConfig, ...taskConfig };
126
+ }
127
+
128
+ function buildSkillArgs(skill, skillConfig = {}) {
129
+ const args = [...(skill.args || [])].map(arg => String(arg));
130
+ if (skillConfig.mode && !args.includes(skillConfig.mode) && skill.name === 'slack-backfill') {
131
+ args.push(String(skillConfig.mode));
132
+ if (skillConfig.month) args.push(String(skillConfig.month));
133
+ }
134
+ return args;
135
+ }
136
+
137
+ module.exports = {
138
+ buildSkillArgs,
139
+ buildSkillConfig,
140
+ resolveSkillEntry,
141
+ runScriptSkill,
142
+ runScriptSkillByName,
143
+ };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  const brain = require('../brain');
3
- const { getDefaultClient } = require('../llm/client');
3
+ const { getDefaultClient, getDefaultModel, resolveCompatibleModel } = require('../llm/client');
4
4
  const { executeTool, getToolDefinitions } = require('./tool-executor');
5
5
  const { augmentSystemPrompt } = require('../loops/loop-directives');
6
6
  let telemetry;
@@ -16,7 +16,7 @@ try { telemetry = require('../telemetry'); } catch { telemetry = { track() {} };
16
16
  */
17
17
  async function runSkill(skill, opts = {}) {
18
18
  const { selectSkillHarness, ensureDefaultHarness } = require('./skill-harness-registry');
19
- ensureDefaultHarness();
19
+ ensureDefaultHarness(_runSkillInternal);
20
20
  const selection = selectSkillHarness(skill, opts);
21
21
  // Log the selection — mirrors OpenClaw's logAgentHarnessSelection
22
22
  // (selection.ts:246). Even at debug level, the trail exists in telemetry.
@@ -39,9 +39,6 @@ async function _runSkillInternal(skill, opts = {}) {
39
39
  const ownerName = brain.getOwnerName();
40
40
  const startTime = Date.now();
41
41
 
42
- // Resolve model once so it can be tagged on skill_executions (Item G).
43
- // Single source of truth — same precedence as the API call below.
44
- const resolvedModel = opts.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001';
45
42
  const decisionReason = opts.decision_reason || 'due';
46
43
 
47
44
  // Build tool definitions — use skill's custom ones or defaults
@@ -52,8 +49,10 @@ async function _runSkillInternal(skill, opts = {}) {
52
49
  // Build the prompt
53
50
  const prompt = skill.prompt_template || buildDefaultPrompt(skill, ownerName);
54
51
 
55
- // Call Claude with tools
52
+ // Call the configured provider with a provider-compatible model. This avoids
53
+ // sending stale Claude model ids to Gemini/OpenAI after provider switches.
56
54
  const provider = getDefaultClient();
55
+ const resolvedModel = resolveCompatibleModel(opts.model || process.env.WALLE_MODEL || getDefaultModel(), provider.type);
57
56
  const messages = [{ role: 'user', content: prompt }];
58
57
  const allToolCalls = [];
59
58
  const allToolResults = [];
@@ -30,6 +30,7 @@
30
30
 
31
31
  let telemetry;
32
32
  try { telemetry = require('../telemetry'); } catch { telemetry = { track() {} }; }
33
+ const { getDefaultModel, getDefaultProviderType, resolveCompatibleModel } = require('../llm/client');
33
34
 
34
35
  function _resolveCandidates(skill, opts) {
35
36
  if (Array.isArray(opts?.model_candidates) && opts.model_candidates.length) {
@@ -68,7 +69,8 @@ function _resolveCandidates(skill, opts) {
68
69
  } catch {}
69
70
 
70
71
  // Single-model fallback (no chain)
71
- const single = opts?.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001';
72
+ const providerType = opts?.provider || opts?.providerType || getDefaultProviderType();
73
+ const single = resolveCompatibleModel(opts?.model || process.env.WALLE_MODEL || getDefaultModel(), providerType);
72
74
  return [single];
73
75
  }
74
76
 
@@ -140,26 +140,25 @@ function listRegisteredSkillHarnesses() {
140
140
 
141
141
  function clearSkillHarnessRegistry() {
142
142
  _harnesses.clear();
143
+ _defaultHarnessRegistered = false;
143
144
  }
144
145
 
145
146
  // ── Bundled default harness: claude-tool-use ──
146
147
  // The existing runSkill body becomes the universal-fallback harness.
147
148
  // priority=0 ensures any registered specialized harness can preempt it.
148
- //
149
- // Lazy-required to avoid circular dep (skill-executor → registry → executor).
149
+ // The executor passes its implementation in so this registry stays dependency-light.
150
150
  let _defaultHarnessRegistered = false;
151
- function ensureDefaultHarness() {
151
+ function ensureDefaultHarness(runAttempt) {
152
152
  if (_defaultHarnessRegistered) return;
153
+ if (typeof runAttempt !== 'function') {
154
+ throw new Error('ensureDefaultHarness requires a runAttempt function');
155
+ }
153
156
  _defaultHarnessRegistered = true;
154
157
  registerSkillHarness({
155
158
  id: 'claude-tool-use',
156
159
  label: 'Claude tool-use loop (default)',
157
160
  supports: () => ({ supported: true, priority: 0 }),
158
- runAttempt: async (skill, opts) => {
159
- // Lazy require avoids circular dependency at module load
160
- const { _runSkillInternal } = require('./skill-executor');
161
- return await _runSkillInternal(skill, opts);
162
- },
161
+ runAttempt,
163
162
  });
164
163
  }
165
164
 
@@ -9,6 +9,41 @@ const { resolveInternalSkill } = require('./internal-skill-registry');
9
9
  let telemetry;
10
10
  try { telemetry = require('../telemetry'); } catch { telemetry = { trackError() {}, track() {} }; }
11
11
 
12
+ const DECISION_TELEMETRY_MIN_INTERVAL_MS = 15 * 60 * 1000;
13
+ const _lastDecisionTelemetry = new Map();
14
+ let _slackAlertMigrationDone = false;
15
+
16
+ function isBrainReady() {
17
+ try {
18
+ brain.getDb();
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ function ensurePlannerBrainReady() {
26
+ if (isBrainReady()) return true;
27
+ try {
28
+ brain.initDb();
29
+ return true;
30
+ } catch (err) {
31
+ console.warn(`[skill-planner] Brain DB unavailable; skipping scheduled skills: ${err.message}`);
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function shouldTrackDecisionTelemetry(skillName, decision) {
37
+ if (!decision) return false;
38
+ if (decision.decision === 'run' || decision.reason === 'just_auto_disabled') return true;
39
+ const key = `${skillName}:${decision.decision}:${decision.reason || ''}`;
40
+ const now = Date.now();
41
+ const last = _lastDecisionTelemetry.get(key) || 0;
42
+ if (now - last < DECISION_TELEMETRY_MIN_INTERVAL_MS) return false;
43
+ _lastDecisionTelemetry.set(key, now);
44
+ return true;
45
+ }
46
+
12
47
  // ── Service alerts ─────────────────────────────────────────────────────────
13
48
  // Stored in kv_store as JSON array. Surfaces auth failures, version updates, etc.
14
49
 
@@ -41,6 +76,7 @@ function addServiceAlert(alert) {
41
76
  const existing = alerts.findIndex(a => a.service === alert.service && a.type === alert.type);
42
77
  if (existing >= 0) alerts.splice(existing, 1);
43
78
  alerts.push({
79
+ ...alert,
44
80
  id: `${alert.service}:${alert.type}:${Date.now()}`,
45
81
  service: alert.service,
46
82
  type: alert.type,
@@ -99,9 +135,16 @@ function migrateSlackFamilyAlerts() {
99
135
  }
100
136
  }
101
137
 
102
- // Run migration once at module load (wall-e reloads skill-planner on boot,
103
- // and this is cheap — a single KV read + possible write).
104
- migrateSlackFamilyAlerts();
138
+ function runSlackAlertMigrationIfReady() {
139
+ if (_slackAlertMigrationDone || !isBrainReady()) return;
140
+ migrateSlackFamilyAlerts();
141
+ _slackAlertMigrationDone = true;
142
+ }
143
+
144
+ // Run opportunistically at module load only after the brain singleton is ready.
145
+ // Some daemon paths import the planner before initDb(); touching KV there
146
+ // generated noisy "Database not initialized" telemetry.
147
+ runSlackAlertMigrationIfReady();
105
148
 
106
149
  // Note: `isSkillBackedOff`, `isSkillDue`, and `getBackoffMs` were removed
107
150
  // in favour of the pure-function `decideSkillDispatch` in
@@ -176,6 +219,9 @@ async function attemptAuthRecovery(skillName, err) {
176
219
  * Run all enabled skills that are due.
177
220
  */
178
221
  async function runDueSkills(opts = {}) {
222
+ if (!ensurePlannerBrainReady()) return { executed: 0, memoriesCreated: 0, skipped: 'db_unavailable' };
223
+ runSlackAlertMigrationIfReady();
224
+
179
225
  // Iterate ALL skills — including disabled ones — so decideSkillDispatch
180
226
  // emits per-skill decisions. The decision function itself filters
181
227
  // disabled/auto_disabled/backed_off; we only proceed to dispatch when
@@ -211,7 +257,9 @@ async function runDueSkills(opts = {}) {
211
257
  // Telemetry: structured event per decision per tick. Operator can
212
258
  // grep "skill_dispatch_decision skill=X reason=backed_off" without
213
259
  // reading source.
214
- try { telemetry.track('skill_dispatch_decision', { skill: skill.name, decision: decision.decision, reason: decision.reason }); } catch {}
260
+ if (shouldTrackDecisionTelemetry(skill.name, decision)) {
261
+ try { telemetry.track('skill_dispatch_decision', { skill: skill.name, decision: decision.decision, reason: decision.reason }); } catch {}
262
+ }
215
263
  // Recovery logger: long-form structured trace for post-incident
216
264
  // debugging (Items E + A of error-recovery deep-dive).
217
265
  if (decision.reason === 'backed_off') {
@@ -4,11 +4,13 @@ const path = require('path');
4
4
  const brain = require('../brain');
5
5
 
6
6
  const CLAUDE_DIR = path.join(process.env.HOME, '.claude');
7
- const OWNER_SLACK_ID = process.env.SLACK_OWNER_USER_ID;
8
- if (!OWNER_SLACK_ID) throw new Error('SLACK_OWNER_USER_ID env var required');
9
7
  const BATCH_SIZE = 100; // messages per API call
10
8
  const RATE_LIMIT_MS = 1200; // Slack rate limit: ~1 req/sec
11
9
 
10
+ function getOwnerSlackId() {
11
+ return process.env.SLACK_OWNER_USER_ID || null;
12
+ }
13
+
12
14
  /**
13
15
  * Get fresh Slack token from Claude Code's credentials.
14
16
  */
@@ -181,7 +183,7 @@ async function fetchConversationHistory(conv, token, oldestTs) {
181
183
  for (const msg of (data.messages || [])) {
182
184
  if (!msg.text && !msg.attachments) continue;
183
185
 
184
- const isOwner = msg.user === OWNER_SLACK_ID;
186
+ const isOwner = msg.user === getOwnerSlackId();
185
187
  const text = msg.text || '';
186
188
  if (!text.trim()) continue;
187
189
 
@@ -230,6 +232,12 @@ async function fetchConversationHistory(conv, token, oldestTs) {
230
232
  * Returns { done, messagesIngested, conversationsProcessed }
231
233
  */
232
234
  async function runIngestBatch(opts = {}) {
235
+ if (!getOwnerSlackId()) {
236
+ const message = 'SLACK_OWNER_USER_ID env var required';
237
+ console.log(`[slack-ingest] ${message}`);
238
+ return { done: false, messagesIngested: 0, conversationsProcessed: 0, error: 'missing_env', message };
239
+ }
240
+
233
241
  // Pre-check: try to refresh token if it's about to expire
234
242
  try {
235
243
  const slackMcp = require('../tools/slack-mcp');
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ PRIVACY_CLASSES,
5
+ assertAdapterSchema,
6
+ assertIngestRecord,
7
+ assertSourceRef,
8
+ } = require('./record-types');
9
+
10
+ class SourceAdapterBase {
11
+ constructor(opts = {}) {
12
+ this.opts = opts;
13
+ this.schema = assertAdapterSchema(this.describeSchema());
14
+ }
15
+
16
+ describeSchema() {
17
+ return this.constructor.schema || {
18
+ adapterId: this.constructor.adapterId || this.constructor.name || 'source-adapter',
19
+ version: '0.0.0',
20
+ supportsIncremental: false,
21
+ defaultPrivacyClass: PRIVACY_CLASSES.PII_POTENTIAL,
22
+ declaredTransformations: [],
23
+ fields: {},
24
+ };
25
+ }
26
+
27
+ async *ingest() {
28
+ throw new Error(`${this.schema.adapterId}.ingest() not implemented`);
29
+ }
30
+
31
+ async isCurrent() {
32
+ return false;
33
+ }
34
+
35
+ async sourceSummary() {
36
+ return null;
37
+ }
38
+
39
+ async close() {}
40
+
41
+ validateSource(source) {
42
+ return assertSourceRef({
43
+ privacyClass: this.schema.defaultPrivacyClass,
44
+ ...source,
45
+ adapterId: source?.adapterId || this.schema.adapterId,
46
+ });
47
+ }
48
+
49
+ validateRecord(record) {
50
+ return assertIngestRecord(record, this.schema);
51
+ }
52
+ }
53
+
54
+ class SourceIngestContext {
55
+ constructor({ isCurrent = null, diagnostics = null } = {}) {
56
+ this._isCurrent = isCurrent;
57
+ this.diagnostics = diagnostics || [];
58
+ }
59
+
60
+ async isCurrent(item) {
61
+ if (typeof this._isCurrent !== 'function') return false;
62
+ return Boolean(await this._isCurrent(item));
63
+ }
64
+
65
+ warn(message, fields = {}) {
66
+ this.diagnostics.push({ level: 'warn', message, ...fields });
67
+ }
68
+
69
+ error(message, fields = {}) {
70
+ this.diagnostics.push({ level: 'error', message, ...fields });
71
+ }
72
+ }
73
+
74
+ async function collectIngestRecords(adapter, source, { context = null, validate = true } = {}) {
75
+ const ctx = context || new SourceIngestContext();
76
+ const src = typeof adapter.validateSource === 'function' ? adapter.validateSource(source) : assertSourceRef(source);
77
+ const records = [];
78
+ for await (const record of adapter.ingest({ source: src, context: ctx })) {
79
+ records.push(validate && typeof adapter.validateRecord === 'function'
80
+ ? adapter.validateRecord(record)
81
+ : record);
82
+ }
83
+ return { records, diagnostics: ctx.diagnostics, source: src };
84
+ }
85
+
86
+ module.exports = {
87
+ SourceAdapterBase,
88
+ SourceIngestContext,
89
+ collectIngestRecords,
90
+ };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const registry = require('./registry');
4
+ const ClaudeCodeJsonlAdapter = require('./claude-code-jsonl');
5
+ const CodexJsonlAdapter = require('./codex-jsonl');
6
+ const GeminiJsonlAdapter = require('./gemini-jsonl');
7
+ const WalleJsonlAdapter = require('./walle-jsonl');
8
+
9
+ const BUILTIN_SOURCE_ADAPTERS = Object.freeze([
10
+ ClaudeCodeJsonlAdapter,
11
+ CodexJsonlAdapter,
12
+ GeminiJsonlAdapter,
13
+ WalleJsonlAdapter,
14
+ ]);
15
+
16
+ function ensureBuiltinSourceAdapters() {
17
+ for (const Adapter of BUILTIN_SOURCE_ADAPTERS) {
18
+ const instance = new Adapter();
19
+ if (!registry.has(instance.schema.adapterId)) {
20
+ registry.register(Adapter);
21
+ }
22
+ }
23
+ return registry;
24
+ }
25
+
26
+ module.exports = {
27
+ BUILTIN_SOURCE_ADAPTERS,
28
+ ClaudeCodeJsonlAdapter,
29
+ CodexJsonlAdapter,
30
+ GeminiJsonlAdapter,
31
+ WalleJsonlAdapter,
32
+ ensureBuiltinSourceAdapters,
33
+ };
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const { SourceAdapterBase } = require('./base');
4
+ const {
5
+ cleanUserText,
6
+ extractMessageText,
7
+ extractToolCalls,
8
+ fileVersion,
9
+ looksLikeToolResult,
10
+ readJsonlFile,
11
+ sourceIdFromFile,
12
+ toIso,
13
+ } = require('./jsonl-utils');
14
+ const { buildExchangeRecords, messageToRecords } = require('./coding-session-utils');
15
+
16
+ class ClaudeCodeJsonlAdapter extends SourceAdapterBase {
17
+ static schema = {
18
+ adapterId: 'claude-code-jsonl',
19
+ version: '1.0.0',
20
+ supportsIncremental: true,
21
+ defaultPrivacyClass: 'pii_potential',
22
+ declaredTransformations: [
23
+ 'newline_normalize',
24
+ 'strip_tool_chrome',
25
+ 'base64_payload_omit',
26
+ 'tool_result_summarize',
27
+ 'whitespace_trim',
28
+ 'exchange_pair_chunk',
29
+ ],
30
+ fields: {
31
+ sourceId: { type: 'string', required: true },
32
+ sourceFile: { type: 'string', required: true },
33
+ cwd: { type: 'string', required: false },
34
+ gitBranch: { type: 'string', required: false },
35
+ },
36
+ };
37
+
38
+ validateSource(source) {
39
+ const normalized = super.validateSource(source);
40
+ if (!source.sourceId) normalized.sourceId = sourceIdFromFile('claude', normalized.sourceFile || normalized.uri);
41
+ return normalized;
42
+ }
43
+
44
+ async *ingest({ source, context }) {
45
+ const filePath = source.sourceFile || source.uri;
46
+ const version = fileVersion(filePath);
47
+ const { events, diagnostics } = readJsonlFile(filePath);
48
+ for (const diagnostic of diagnostics) context?.warn?.(diagnostic.message, diagnostic);
49
+
50
+ const messages = [];
51
+ for (const { line, event } of events) {
52
+ if (!event?.message || !['user', 'assistant'].includes(event.type)) continue;
53
+ const role = event.message.role || event.type;
54
+ if (!['user', 'assistant'].includes(role)) continue;
55
+ let content = extractMessageText(event.message);
56
+ if (role === 'user') content = cleanUserText(content);
57
+ if (!content || looksLikeToolResult(content)) continue;
58
+
59
+ const message = {
60
+ itemId: event.uuid || `line-${line}`,
61
+ role,
62
+ content,
63
+ timestamp: toIso(event.timestamp || event.message.timestamp),
64
+ cwd: event.cwd || source.cwd || '',
65
+ gitBranch: event.gitBranch || source.metadata?.gitBranch || '',
66
+ rawType: event.type,
67
+ harness: 'claude-code',
68
+ toolCalls: extractToolCalls(event.message),
69
+ };
70
+ messages.push(message);
71
+ for (const record of messageToRecords({ source, message, version })) yield record;
72
+ }
73
+
74
+ for (const record of buildExchangeRecords(source, messages)) yield record;
75
+ }
76
+ }
77
+
78
+ module.exports = ClaudeCodeJsonlAdapter;
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ const { SourceAdapterBase } = require('./base');
4
+ const {
5
+ cleanUserText,
6
+ extractMessageText,
7
+ fileVersion,
8
+ looksLikeToolResult,
9
+ parseToolInput,
10
+ readJsonlFile,
11
+ sourceIdFromFile,
12
+ summarizeToolInput,
13
+ toIso,
14
+ } = require('./jsonl-utils');
15
+ const { buildExchangeRecords, memoryRecord, messageToRecords, sourceItem } = require('./coding-session-utils');
16
+
17
+ class CodexJsonlAdapter extends SourceAdapterBase {
18
+ static schema = {
19
+ adapterId: 'codex-jsonl',
20
+ version: '1.0.0',
21
+ supportsIncremental: true,
22
+ defaultPrivacyClass: 'pii_potential',
23
+ declaredTransformations: [
24
+ 'newline_normalize',
25
+ 'strip_tool_chrome',
26
+ 'base64_payload_omit',
27
+ 'tool_result_summarize',
28
+ 'whitespace_trim',
29
+ 'exchange_pair_chunk',
30
+ ],
31
+ fields: {
32
+ sourceId: { type: 'string', required: true },
33
+ sourceFile: { type: 'string', required: true },
34
+ cwd: { type: 'string', required: false },
35
+ gitBranch: { type: 'string', required: false },
36
+ },
37
+ };
38
+
39
+ validateSource(source) {
40
+ const normalized = super.validateSource(source);
41
+ if (!source.sourceId) normalized.sourceId = sourceIdFromFile('codex', normalized.sourceFile || normalized.uri);
42
+ return normalized;
43
+ }
44
+
45
+ async *ingest({ source, context }) {
46
+ const filePath = source.sourceFile || source.uri;
47
+ const version = fileVersion(filePath);
48
+ const { events, diagnostics } = readJsonlFile(filePath);
49
+ for (const diagnostic of diagnostics) context?.warn?.(diagnostic.message, diagnostic);
50
+
51
+ let cwd = source.cwd || '';
52
+ let gitBranch = source.metadata?.gitBranch || '';
53
+ const messages = [];
54
+ const seen = new Set();
55
+
56
+ for (const { line, event } of events) {
57
+ const payload = event.payload || {};
58
+ const item = payload.item || payload;
59
+ if (event.type === 'session_meta') {
60
+ cwd = payload.cwd || cwd;
61
+ gitBranch = payload.gitBranch || gitBranch;
62
+ continue;
63
+ }
64
+ if (event.type === 'turn_context') {
65
+ cwd = payload.cwd || cwd;
66
+ gitBranch = payload.gitBranch || gitBranch;
67
+ continue;
68
+ }
69
+
70
+ if (event.type !== 'response_item' && event.type !== 'event_msg') continue;
71
+
72
+ const role = item.role || payload.role || event.role || null;
73
+ if (role === 'user' || role === 'assistant') {
74
+ const content = role === 'user'
75
+ ? cleanUserText(extractMessageText(item))
76
+ : extractMessageText(item);
77
+ if (!content || looksLikeToolResult(content)) continue;
78
+ const itemId = item.id || payload.id || `${event.type}-${line}`;
79
+ const key = `${role}:${itemId}:${content}`;
80
+ if (seen.has(key)) continue;
81
+ seen.add(key);
82
+ const message = {
83
+ itemId,
84
+ role,
85
+ content,
86
+ timestamp: toIso(event.timestamp || payload.timestamp || item.timestamp),
87
+ cwd,
88
+ gitBranch,
89
+ rawType: event.type,
90
+ harness: 'codex',
91
+ toolCalls: [],
92
+ };
93
+ messages.push(message);
94
+ for (const record of messageToRecords({ source, message, version })) yield record;
95
+ continue;
96
+ }
97
+
98
+ if (payload.type === 'function_call' || item.type === 'function_call') {
99
+ const name = item.name || payload.name || 'tool';
100
+ const input = parseToolInput(item.arguments ?? payload.arguments ?? item.input ?? payload.input);
101
+ const itemId = item.id || payload.id || `tool-${line}`;
102
+ yield sourceItem({
103
+ source,
104
+ itemId,
105
+ version,
106
+ timestamp: event.timestamp || payload.timestamp,
107
+ metadata: { role: 'tool', cwd, gitBranch, name, input },
108
+ });
109
+ yield memoryRecord({
110
+ source,
111
+ itemId: `${itemId}:tool`,
112
+ memoryType: 'coding_session_tool_call',
113
+ role: 'tool',
114
+ contentRaw: `[Tool: ${name}] ${summarizeToolInput(name, input)}`,
115
+ timestamp: event.timestamp || payload.timestamp,
116
+ metadata: { cwd, gitBranch, name, input },
117
+ });
118
+ }
119
+ }
120
+
121
+ for (const record of buildExchangeRecords(source, messages)) yield record;
122
+ }
123
+ }
124
+
125
+ module.exports = CodexJsonlAdapter;