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
@@ -6,6 +6,11 @@ const { validateSkillRequirements } = require('./skill-validator');
6
6
  const { decideSkillDispatch } = require('./skill-dispatch-decision');
7
7
  const { getCurrentSkillSnapshot } = require('./skill-snapshot');
8
8
  const { resolveInternalSkill } = require('./internal-skill-registry');
9
+ const {
10
+ OWNER_ENV_KEY: SLACK_OWNER_ENV_KEY,
11
+ getSlackOwnerRepairState,
12
+ repairSlackOwnerIdentity,
13
+ } = require('../tools/slack-owner');
9
14
  let telemetry;
10
15
  try { telemetry = require('../telemetry'); } catch { telemetry = { trackError() {}, track() {} }; }
11
16
 
@@ -59,7 +64,11 @@ function getServiceAlerts({ includeVersionUpdate = false } = {}) {
59
64
  if (update.latest && update.current && update.latest !== update.current) {
60
65
  alerts.push({
61
66
  id: 'version_update', service: 'system', type: 'update_available',
62
- message: `Wall-E ${update.latest} is available (you have ${update.current}). Run: npx create-walle@latest`,
67
+ message: `CTM / Wall-E ${update.latest} is available (you have ${update.current}).`,
68
+ action: 'show_update_wizard',
69
+ action_label: 'Upgrade',
70
+ current: update.current,
71
+ latest: update.latest,
63
72
  created_at: update.checked_at,
64
73
  });
65
74
  }
@@ -95,10 +104,63 @@ function dismissServiceAlert(alertId) {
95
104
  }
96
105
 
97
106
  function clearServiceAlerts(service) {
98
- const alerts = getServiceAlerts().filter(a => a.service !== service);
107
+ const alerts = getServiceAlerts().filter((a) => {
108
+ if (service === 'slack') return !(a.service === 'slack' || (typeof a.service === 'string' && a.service.startsWith('slack-')));
109
+ return a.service !== service;
110
+ });
99
111
  brain.setKv('service_alerts', JSON.stringify(alerts));
100
112
  }
101
113
 
114
+ function clearResolvedSlackHealthAlerts({ authenticated = false, ownerConfigured = false } = {}) {
115
+ if (!authenticated) return { cleared: 0 };
116
+ const before = getServiceAlerts();
117
+ const after = before.filter((a) => {
118
+ const service = String(a.service || '');
119
+ const type = String(a.type || '');
120
+ const message = String(a.message || '');
121
+ const slackFamily = service === 'slack' || service.startsWith('slack-');
122
+ if (!slackFamily) return true;
123
+ if (type === 'auth_expired') return false;
124
+ if (ownerConfigured && type === 'owner_identity_missing') return false;
125
+ // `slack-sync` was an older Slack path. A disabled legacy alert should not
126
+ // keep current Slack MCP/OAuth status red once slack-mentions is healthy.
127
+ if (type === 'skill_disabled' && /\bslack-sync\b/.test(message + ' ' + service)) return false;
128
+ return true;
129
+ });
130
+ if (after.length !== before.length) {
131
+ brain.setKv('service_alerts', JSON.stringify(after));
132
+ }
133
+ return { cleared: before.length - after.length };
134
+ }
135
+
136
+ function _hasMissingSlackOwner(validation) {
137
+ return !!(validation && Array.isArray(validation.missing)
138
+ && validation.missing.some(m => m.type === 'env' && m.name === SLACK_OWNER_ENV_KEY));
139
+ }
140
+
141
+ function _addSlackOwnerIdentityAlert() {
142
+ const state = getSlackOwnerRepairState();
143
+ if (state.canRepair) {
144
+ addServiceAlert({
145
+ service: 'slack',
146
+ type: 'owner_identity_missing',
147
+ message: 'Slack sync needs to know which Slack user is you. Wall-E can fix this from your existing Slack connection.',
148
+ action_label: 'Fix automatically',
149
+ action: 'repair_slack_owner',
150
+ action_endpoint: '/api/wall-e/slack/repair-owner',
151
+ });
152
+ return;
153
+ }
154
+
155
+ addServiceAlert({
156
+ service: 'slack',
157
+ type: 'owner_identity_missing',
158
+ message: 'Slack sync needs to know which Slack user is you. Connect Slack so Wall-E can configure this automatically.',
159
+ action_label: 'Connect Slack',
160
+ action_url: '/setup.html#slack',
161
+ });
162
+ }
163
+
102
164
  /**
103
165
  * One-time migration: alerts fired before the attemptAuthRecovery fix used
104
166
  * the skill name as the service (e.g. service='slack-mentions') which meant
@@ -307,8 +369,21 @@ async function runDueSkills(opts = {}) {
307
369
  const hasRequires = loaderSkill && loaderSkill.requires &&
308
370
  (loaderSkill.requires.bins || loaderSkill.requires.env || loaderSkill.requires.mcp);
309
371
  if (hasRequires) {
310
- const validation = validateSkillRequirements(loaderSkill);
372
+ let validation = validateSkillRequirements(loaderSkill);
373
+ if (!validation.valid && _hasMissingSlackOwner(validation)) {
374
+ const repaired = repairSlackOwnerIdentity({ persist: true });
375
+ if (repaired.ok) {
376
+ console.log(`[skill-planner] Repaired ${SLACK_OWNER_ENV_KEY} from Slack OAuth token for "${skill.name}"`);
377
+ clearServiceAlerts('slack');
378
+ validation = validateSkillRequirements(loaderSkill);
379
+ }
380
+ }
311
381
  if (!validation.valid) {
382
+ if (_hasMissingSlackOwner(validation)) {
383
+ console.log(`[skill-planner] Preflight failed for "${skill.name}": Slack owner identity missing`);
384
+ _addSlackOwnerIdentityAlert();
385
+ continue;
386
+ }
312
387
  const items = validation.missing
313
388
  .map(m => `${m.type}=${m.name}`)
314
389
  .join(', ');
@@ -506,4 +581,11 @@ async function runDueSkills(opts = {}) {
506
581
 
507
582
  // `isSkillDue` was inlined into `decideSkillDispatch` (Item C).
508
583
 
509
- module.exports = { runDueSkills, getServiceAlerts, addServiceAlert, dismissServiceAlert, clearServiceAlerts };
584
+ module.exports = {
585
+ runDueSkills,
586
+ getServiceAlerts,
587
+ addServiceAlert,
588
+ dismissServiceAlert,
589
+ clearServiceAlerts,
590
+ clearResolvedSlackHealthAlerts,
591
+ };
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_RECONNECT_MS = 5000;
4
+ const MAX_RECONNECT_MS = 60000;
5
+
6
+ function getAppToken(env = process.env) {
7
+ return env.WALLE_SLACK_APP_TOKEN || env.SLACK_APP_TOKEN || env.SLACK_SOCKET_MODE_APP_TOKEN || '';
8
+ }
9
+
10
+ function socketModeDisabled(env = process.env) {
11
+ const value = String(env.WALLE_SLACK_SOCKET_MODE || env.SLACK_SOCKET_MODE || '').toLowerCase();
12
+ return value === '0' || value === 'false' || value === 'off';
13
+ }
14
+
15
+ function isConfigured(env = process.env) {
16
+ const token = getAppToken(env);
17
+ return !socketModeDisabled(env) && /^xapp-/.test(token);
18
+ }
19
+
20
+ function resolveWebSocketCtor() {
21
+ if (typeof WebSocket === 'function') return WebSocket;
22
+ try { return require('ws'); } catch {}
23
+ return null;
24
+ }
25
+
26
+ function extractSlackEvent(envelope) {
27
+ if (!envelope || envelope.type !== 'events_api') return null;
28
+ const payload = envelope.payload || {};
29
+ if (payload.type !== 'event_callback') return null;
30
+ return payload.event || null;
31
+ }
32
+
33
+ function isRealtimeMessageEvent(event) {
34
+ if (!event || !event.type) return false;
35
+ if (event.bot_id || event.subtype) return false;
36
+ if (event.type === 'app_mention') return true;
37
+ if (event.type !== 'message') return false;
38
+ return event.channel_type === 'im' || event.channel_type === 'mpim';
39
+ }
40
+
41
+ class SlackSocketModeListener {
42
+ constructor(opts = {}) {
43
+ this.env = opts.env || process.env;
44
+ this.fetchImpl = opts.fetch || global.fetch;
45
+ this.WebSocketCtor = opts.WebSocketCtor || resolveWebSocketCtor();
46
+ this.handleEvent = opts.handleEvent || defaultHandleEvent;
47
+ this.logger = opts.logger || console;
48
+ this.reconnectBaseMs = opts.reconnectBaseMs || DEFAULT_RECONNECT_MS;
49
+ this.maxReconnectMs = opts.maxReconnectMs || MAX_RECONNECT_MS;
50
+ this.started = false;
51
+ this.running = false;
52
+ this.ws = null;
53
+ this.reconnectTimer = null;
54
+ this.reconnectAttempts = 0;
55
+ this.lastConnectedAt = null;
56
+ this.lastEventAt = null;
57
+ this.lastError = null;
58
+ this._seenEventIds = new Map();
59
+ }
60
+
61
+ get configured() {
62
+ return isConfigured(this.env);
63
+ }
64
+
65
+ getStatus() {
66
+ return {
67
+ configured: this.configured,
68
+ enabled: !socketModeDisabled(this.env),
69
+ running: this.running,
70
+ started: this.started,
71
+ last_connected_at: this.lastConnectedAt,
72
+ last_event_at: this.lastEventAt,
73
+ last_error: this.lastError,
74
+ reconnect_attempts: this.reconnectAttempts,
75
+ };
76
+ }
77
+
78
+ start() {
79
+ if (this.started) return this.getStatus();
80
+ this.started = true;
81
+ if (!this.configured) {
82
+ this.logger.log?.('[slack-socket] Not configured; set WALLE_SLACK_APP_TOKEN or SLACK_APP_TOKEN to enable Socket Mode');
83
+ return this.getStatus();
84
+ }
85
+ if (!this.WebSocketCtor) {
86
+ this.lastError = 'No WebSocket implementation available';
87
+ this.logger.warn?.('[slack-socket] No WebSocket implementation available');
88
+ return this.getStatus();
89
+ }
90
+ this._connect().catch((err) => {
91
+ this._recordError(err);
92
+ this._scheduleReconnect();
93
+ });
94
+ return this.getStatus();
95
+ }
96
+
97
+ stop() {
98
+ this.started = false;
99
+ this.running = false;
100
+ if (this.reconnectTimer) {
101
+ clearTimeout(this.reconnectTimer);
102
+ this.reconnectTimer = null;
103
+ }
104
+ if (this.ws) {
105
+ try { this.ws.close(); } catch {}
106
+ this.ws = null;
107
+ }
108
+ }
109
+
110
+ async _connect() {
111
+ if (!this.started || !this.configured) return;
112
+ const appToken = getAppToken(this.env);
113
+ if (!this.fetchImpl) throw new Error('fetch is not available');
114
+
115
+ const resp = await this.fetchImpl('https://slack.com/api/apps.connections.open', {
116
+ method: 'POST',
117
+ headers: {
118
+ Authorization: `Bearer ${appToken}`,
119
+ 'Content-Type': 'application/x-www-form-urlencoded',
120
+ },
121
+ });
122
+ const data = await resp.json();
123
+ if (!data.ok || !data.url) {
124
+ throw new Error(`apps.connections.open failed: ${data.error || resp.status}`);
125
+ }
126
+
127
+ const ws = new this.WebSocketCtor(data.url);
128
+ this.ws = ws;
129
+ this._bindSocket(ws);
130
+ }
131
+
132
+ _bindSocket(ws) {
133
+ const onOpen = () => {
134
+ this.running = true;
135
+ this.reconnectAttempts = 0;
136
+ this.lastConnectedAt = new Date().toISOString();
137
+ this.lastError = null;
138
+ this.logger.log?.('[slack-socket] Connected');
139
+ };
140
+ const onMessage = (eventOrData) => {
141
+ const data = eventOrData && Object.prototype.hasOwnProperty.call(eventOrData, 'data')
142
+ ? eventOrData.data
143
+ : eventOrData;
144
+ this._handleMessage(data).catch((err) => this._recordError(err));
145
+ };
146
+ const onError = (err) => this._recordError(err);
147
+ const onClose = () => {
148
+ this.running = false;
149
+ if (this.ws === ws) this.ws = null;
150
+ if (this.started) this._scheduleReconnect();
151
+ };
152
+
153
+ if (typeof ws.on === 'function') {
154
+ ws.on('open', onOpen);
155
+ ws.on('message', onMessage);
156
+ ws.on('error', onError);
157
+ ws.on('close', onClose);
158
+ } else {
159
+ ws.onopen = onOpen;
160
+ ws.onmessage = onMessage;
161
+ ws.onerror = onError;
162
+ ws.onclose = onClose;
163
+ }
164
+ }
165
+
166
+ async _handleMessage(data) {
167
+ const text = Buffer.isBuffer(data)
168
+ ? data.toString('utf8')
169
+ : (data instanceof ArrayBuffer)
170
+ ? Buffer.from(data).toString('utf8')
171
+ : ArrayBuffer.isView(data)
172
+ ? Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8')
173
+ : String(data || '');
174
+ if (!text) return;
175
+ let envelope;
176
+ try {
177
+ envelope = JSON.parse(text);
178
+ } catch {
179
+ return;
180
+ }
181
+
182
+ if (envelope.envelope_id) this._ack(envelope.envelope_id);
183
+ if (envelope.type === 'hello') return;
184
+ if (envelope.type === 'disconnect') {
185
+ this.lastError = envelope.reason || 'Slack requested disconnect';
186
+ this._scheduleReconnect();
187
+ return;
188
+ }
189
+
190
+ const event = extractSlackEvent(envelope);
191
+ if (!isRealtimeMessageEvent(event)) return;
192
+ if (this._seenRecently(envelope.payload?.event_id || event.event_ts || event.ts)) return;
193
+
194
+ this.lastEventAt = new Date().toISOString();
195
+ await this.handleEvent(event, { envelope });
196
+ }
197
+
198
+ _ack(envelopeId) {
199
+ if (!this.ws || typeof this.ws.send !== 'function') return;
200
+ try {
201
+ this.ws.send(JSON.stringify({ envelope_id: envelopeId }));
202
+ } catch (err) {
203
+ this._recordError(err);
204
+ }
205
+ }
206
+
207
+ _seenRecently(id) {
208
+ if (!id) return false;
209
+ const now = Date.now();
210
+ for (const [key, ts] of this._seenEventIds) {
211
+ if (now - ts > 10 * 60_000) this._seenEventIds.delete(key);
212
+ }
213
+ if (this._seenEventIds.has(id)) return true;
214
+ this._seenEventIds.set(id, now);
215
+ return false;
216
+ }
217
+
218
+ _recordError(err) {
219
+ const msg = err && err.message ? err.message : String(err || 'unknown error');
220
+ this.lastError = msg.slice(0, 300);
221
+ this.logger.warn?.(`[slack-socket] ${this.lastError}`);
222
+ }
223
+
224
+ _scheduleReconnect() {
225
+ if (!this.started || this.reconnectTimer) return;
226
+ this.running = false;
227
+ this.reconnectAttempts += 1;
228
+ const delay = Math.min(this.reconnectBaseMs * Math.pow(2, Math.max(0, this.reconnectAttempts - 1)), this.maxReconnectMs);
229
+ this.reconnectTimer = setTimeout(() => {
230
+ this.reconnectTimer = null;
231
+ this._connect().catch((err) => {
232
+ this._recordError(err);
233
+ this._scheduleReconnect();
234
+ });
235
+ }, delay);
236
+ if (typeof this.reconnectTimer.unref === 'function') this.reconnectTimer.unref();
237
+ }
238
+ }
239
+
240
+ async function defaultHandleEvent(event) {
241
+ const slackMentions = require('../skills/_bundled/slack-mentions/run');
242
+ return slackMentions.processIncomingSlackEvent(event);
243
+ }
244
+
245
+ let singleton = null;
246
+
247
+ function startSlackSocketModeListener(opts = {}) {
248
+ if (singleton) return singleton;
249
+ singleton = new SlackSocketModeListener(opts);
250
+ singleton.start();
251
+ return singleton;
252
+ }
253
+
254
+ function getSlackSocketModeStatus() {
255
+ if (singleton) return singleton.getStatus();
256
+ return {
257
+ configured: isConfigured(process.env),
258
+ enabled: !socketModeDisabled(process.env),
259
+ running: false,
260
+ started: false,
261
+ last_connected_at: null,
262
+ last_event_at: null,
263
+ last_error: null,
264
+ reconnect_attempts: 0,
265
+ };
266
+ }
267
+
268
+ module.exports = {
269
+ SlackSocketModeListener,
270
+ startSlackSocketModeListener,
271
+ getSlackSocketModeStatus,
272
+ getAppToken,
273
+ isConfigured,
274
+ extractSlackEvent,
275
+ isRealtimeMessageEvent,
276
+ };
@@ -9,6 +9,8 @@ const FLUSH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
9
9
  const MAX_BATCH_SIZE = 100;
10
10
  const DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME || '/tmp', '.walle', 'data');
11
11
  const INSTALL_ID_PATH = path.join(DATA_DIR, '.install-id');
12
+ const CLI_LIFECYCLE_PATH = path.join(DATA_DIR, '.cli-lifecycle.jsonl');
13
+ const MACHINE_ID_PATH = process.env.WALL_E_MACHINE_ID_PATH || path.join(path.dirname(DATA_DIR), '.machine-id');
12
14
 
13
15
  // --- Opt-out ---
14
16
  function isDisabled() {
@@ -33,6 +35,27 @@ function getInstallId() {
33
35
  return _installId;
34
36
  }
35
37
 
38
+ // --- Machine bucket (anonymous, stable across reinstall churn) ---
39
+ let _machineBucket = null;
40
+ function getMachineBucket() {
41
+ if (_machineBucket) return _machineBucket;
42
+ let machineId = '';
43
+ try {
44
+ if (fs.existsSync(MACHINE_ID_PATH)) {
45
+ machineId = fs.readFileSync(MACHINE_ID_PATH, 'utf8').trim();
46
+ }
47
+ if (!machineId) {
48
+ machineId = crypto.randomUUID();
49
+ fs.mkdirSync(path.dirname(MACHINE_ID_PATH), { recursive: true });
50
+ fs.writeFileSync(MACHINE_ID_PATH, machineId, { encoding: 'utf8', mode: 0o600 });
51
+ }
52
+ } catch {
53
+ machineId = getInstallId();
54
+ }
55
+ _machineBucket = crypto.createHash('sha256').update(machineId).digest('hex').slice(0, 16);
56
+ return _machineBucket;
57
+ }
58
+
36
59
  // --- Local JSONL log (survives crashes, unlike the in-memory buffer) ---
37
60
  const LOCAL_LOG_DIR = path.join(process.env.HOME || '/tmp', '.walle', 'logs');
38
61
  const LOCAL_LOG_PATH = path.join(LOCAL_LOG_DIR, 'telemetry.jsonl');
@@ -74,6 +97,7 @@ async function flush() {
74
97
  const version = getVersion();
75
98
  const payload = {
76
99
  id: getInstallId(),
100
+ machine: getMachineBucket(),
77
101
  v: version,
78
102
  os: process.platform,
79
103
  node: process.version,
@@ -102,7 +126,18 @@ async function flush() {
102
126
 
103
127
  // --- First-run funnel tracking ---
104
128
  const FUNNEL_PATH = path.join(DATA_DIR, '.telemetry-funnel.json');
105
- const FUNNEL_STEPS = ['install', 'boot', 'first_ingest', 'first_chat', 'first_skill'];
129
+ const FUNNEL_STEPS = [
130
+ 'install',
131
+ 'boot',
132
+ 'first_ingest',
133
+ 'first_chat',
134
+ 'first_skill',
135
+ 'upgrade_available',
136
+ 'upgrade_prompt_shown',
137
+ 'upgrade_prompt_dismissed',
138
+ 'upgrade_prompt_command_copied',
139
+ 'upgrade_completed',
140
+ ];
106
141
 
107
142
  function trackFunnelStep(step) {
108
143
  if (isDisabled()) return;
@@ -133,10 +168,43 @@ function getVersion() {
133
168
  return _version;
134
169
  }
135
170
 
171
+ function drainCliLifecycleEvents() {
172
+ if (isDisabled()) return;
173
+ let lines = [];
174
+ try {
175
+ if (!fs.existsSync(CLI_LIFECYCLE_PATH)) return;
176
+ lines = fs.readFileSync(CLI_LIFECYCLE_PATH, 'utf8')
177
+ .split(/\r?\n/)
178
+ .filter(Boolean)
179
+ .slice(-100);
180
+ fs.unlinkSync(CLI_LIFECYCLE_PATH);
181
+ } catch {
182
+ return;
183
+ }
184
+ const allowed = new Set([
185
+ 'cli_install_started',
186
+ 'cli_install_completed',
187
+ 'cli_update_started',
188
+ 'cli_update_completed',
189
+ ]);
190
+ for (const line of lines) {
191
+ try {
192
+ const entry = JSON.parse(line);
193
+ if (!entry || !allowed.has(entry.event)) continue;
194
+ const meta = entry.meta && typeof entry.meta === 'object' ? entry.meta : {};
195
+ track(entry.event, {
196
+ ...meta,
197
+ age_seconds: entry.t ? Math.max(0, Math.round((Date.now() - entry.t) / 1000)) : undefined,
198
+ });
199
+ } catch {}
200
+ }
201
+ }
202
+
136
203
  // --- Lifecycle ---
137
204
  function start() {
138
205
  if (isDisabled()) return;
139
206
  if (flushTimer) return;
207
+ drainCliLifecycleEvents();
140
208
  track('startup', {
141
209
  uptime: process.uptime(),
142
210
  });
@@ -207,4 +275,4 @@ function trackCompatUsage(features) {
207
275
  track('compat_usage', features);
208
276
  }
209
277
 
210
- module.exports = { track, trackError, flush, start, stop, isDisabled, getInstallId, getVersion, printNoticeIfFirstRun, trackFunnelStep, trackCompatUsage };
278
+ module.exports = { track, trackError, flush, start, stop, isDisabled, getInstallId, getMachineBucket, getVersion, printNoticeIfFirstRun, trackFunnelStep, trackCompatUsage, drainCliLifecycleEvents };
@@ -32,8 +32,32 @@ function registerBuiltinMiddleware(mw, opts = {}) {
32
32
  if (!ctx.params) ctx.params = {};
33
33
  // Deterministic output for coding
34
34
  ctx.params.temperature = 0;
35
- // Cap tokens for small models
36
- if (ctx.provider === 'ollama' || opts.provider === 'ollama') {
35
+ const provider = ctx.provider || opts.provider;
36
+ const model = String(ctx.model || opts.model || '');
37
+ const isOllama = provider === 'ollama';
38
+ const isGemma4 = isOllama && /^gemma4:/i.test(model);
39
+ if (isGemma4) {
40
+ const benchmarkMode = Boolean(opts.benchmark || ctx.benchmark);
41
+ const noToolsTurn = ctx.toolsAvailable === false;
42
+ // Gemma4's Ollama thinking trace shares the output budget with final
43
+ // content. Small coding budgets can be consumed entirely by reasoning.
44
+ const floor = /26b/i.test(model) ? 6144 : 8192;
45
+ if (benchmarkMode && noToolsTurn) {
46
+ ctx.params.maxTokens = Math.min(ctx.params.maxTokens || 1024, 1024);
47
+ } else {
48
+ ctx.params.maxTokens = Math.max(ctx.params.maxTokens || 4096, floor);
49
+ }
50
+ // Keep structured tool-call turns direct. Thinking traces improve
51
+ // no-tool planning/wrap-up turns but can crowd out or destabilize JSON
52
+ // function-call arguments during active edit/test loops.
53
+ if (ctx.params.thinking == null) ctx.params.thinking = noToolsTurn && !benchmarkMode;
54
+ ctx.params.options = {
55
+ ...(ctx.params.options || {}),
56
+ num_ctx: Math.max(ctx.params.options?.num_ctx || 0, 16384),
57
+ };
58
+ } else if (isOllama) {
59
+ // Cap generic local models; many smaller tags degrade if asked for huge
60
+ // coding turns. Gemma4 is handled separately above.
37
61
  ctx.params.maxTokens = Math.min(ctx.params.maxTokens || 4096, 2048);
38
62
  }
39
63
  });
@@ -72,6 +96,18 @@ Do not make any edits. Review the diff and assess quality.`);
72
96
  - minimize output tokens. Use tools, not words.
73
97
  - Never introduce code that exposes, logs, or commits secrets.`);
74
98
 
99
+ const provider = ctx.provider || opts.provider;
100
+ const model = String(ctx.model || opts.model || '');
101
+ if (provider === 'ollama' && /^gemma4:/i.test(model)) {
102
+ sections.push(`## Gemma4 Coding Guardrails
103
+ - Before each edit, silently identify the smallest complete syntactic unit you are changing.
104
+ - Insert new sibling code between complete blocks. Do not replace a function, route, class, or object entry header with unrelated code.
105
+ - If the task says add, preserve existing behavior. For route/handler additions, keep every existing route/handler block and insert the new one as a sibling.
106
+ - For edit_file, use an old_string that includes enough surrounding context to be unique and preserves neighboring declarations.
107
+ - Do not run long-lived servers in the foreground with run_shell. Prefer the project test command; if a server must be started, use a bounded/background command.
108
+ - After one failed or timed-out verification command, read the error/output and inspect the edited code before running the same command again.`);
109
+ }
110
+
75
111
  // Tool workflow patterns (loaded once at startup)
76
112
  if (workflowDocs) sections.push(workflowDocs);
77
113
 
@@ -137,7 +173,18 @@ Do not make any edits. Review the diff and assess quality.`);
137
173
  let portsBefore = null;
138
174
  mw.use('tool.before', async (ctx, toolName, input) => {
139
175
  if (toolName !== 'run_shell') return input;
176
+ const provider = ctx.provider || opts.provider;
177
+ const model = String(ctx.model || opts.model || '');
140
178
  const { isLikelyServerCommand, getListeningPorts } = require('./port-detector');
179
+ if (opts.benchmark && provider === 'ollama' && /^gemma4:/i.test(model) &&
180
+ isLikelyServerCommand(input.command) && !isBoundedServerCommand(input.command)) {
181
+ portsBefore = null;
182
+ return {
183
+ ...input,
184
+ command: 'node -e "console.error(\'Blocked foreground server command in benchmark; run npm test or use a bounded/background server command instead.\'); process.exit(1)"',
185
+ timeout_ms: Math.min(input.timeout_ms || 5000, 5000),
186
+ };
187
+ }
141
188
  if (isLikelyServerCommand(input.command)) {
142
189
  portsBefore = getListeningPorts();
143
190
  } else {
@@ -164,4 +211,10 @@ Do not make any edits. Review the diff and assess quality.`);
164
211
  });
165
212
  }
166
213
 
214
+ function isBoundedServerCommand(command = '') {
215
+ const text = String(command || '');
216
+ return /(?:^|[;&|]\s*)(?:timeout|gtimeout)\s+\d+/i.test(text) ||
217
+ /(?:&\s*$|\bnohup\b|\bsetsid\b)/i.test(text);
218
+ }
219
+
167
220
  module.exports = { registerBuiltinMiddleware };
@@ -17,7 +17,7 @@ const SHELL_ALLOWLIST = new Set([
17
17
  // Network
18
18
  'curl', 'wget', 'ping', 'dig', 'host', 'nslookup',
19
19
  // Dev tools
20
- 'git', 'node', 'npm', 'npx', 'python3', 'pip3', 'bun', 'deno',
20
+ 'git', 'node', 'npm', 'npx', 'python', 'python3', 'pip', 'pip3', 'pytest', 'bun', 'deno',
21
21
  'make', 'cargo', 'go', 'ruby', 'perl', 'tsc',
22
22
  // Cloud / infra
23
23
  'fly', 'docker', 'kubectl',