create-walle 0.9.25 → 0.9.26

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 (179) hide show
  1. package/README.md +8 -0
  2. package/bin/create-walle.js +815 -45
  3. package/package.json +2 -2
  4. package/template/bin/ctm-dev-cleanup.js +90 -4
  5. package/template/bin/ctm-launch.sh +49 -1
  6. package/template/bin/dev.sh +45 -1
  7. package/template/bin/ensure-stable-node.js +132 -0
  8. package/template/bin/install-service.sh +9 -0
  9. package/template/claude-task-manager/api-prompts.js +899 -119
  10. package/template/claude-task-manager/approval-agent.js +360 -40
  11. package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
  12. package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
  13. package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
  14. package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
  15. package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
  16. package/template/claude-task-manager/db.js +399 -48
  17. package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
  18. package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
  19. package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
  20. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
  21. package/template/claude-task-manager/lib/approval-hook.js +200 -0
  22. package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
  23. package/template/claude-task-manager/lib/auth-rules.js +11 -0
  24. package/template/claude-task-manager/lib/background-llm.js +32 -4
  25. package/template/claude-task-manager/lib/codesign-identity.js +140 -0
  26. package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
  27. package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
  28. package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
  29. package/template/claude-task-manager/lib/codex-paths.js +73 -0
  30. package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
  31. package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
  32. package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
  33. package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
  34. package/template/claude-task-manager/lib/command-targets.js +163 -0
  35. package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
  36. package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
  37. package/template/claude-task-manager/lib/escalation-review.js +80 -3
  38. package/template/claude-task-manager/lib/flow-control.js +52 -0
  39. package/template/claude-task-manager/lib/fs-watcher.js +24 -15
  40. package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
  41. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
  42. package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
  43. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
  44. package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
  45. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
  46. package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
  47. package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
  48. package/template/claude-task-manager/lib/perf-tracker.js +29 -2
  49. package/template/claude-task-manager/lib/permission-match.js +146 -16
  50. package/template/claude-task-manager/lib/project-slug.js +33 -0
  51. package/template/claude-task-manager/lib/prompt-intent.js +51 -4
  52. package/template/claude-task-manager/lib/read-pool-client.js +48 -3
  53. package/template/claude-task-manager/lib/real-node.js +73 -0
  54. package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
  55. package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
  56. package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
  57. package/template/claude-task-manager/lib/session-history.js +5 -7
  58. package/template/claude-task-manager/lib/session-host-manager.js +19 -0
  59. package/template/claude-task-manager/lib/session-jobs.js +6 -0
  60. package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
  61. package/template/claude-task-manager/lib/session-messages-page.js +211 -0
  62. package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
  63. package/template/claude-task-manager/lib/session-standup.js +8 -0
  64. package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
  65. package/template/claude-task-manager/lib/session-token-usage.js +30 -8
  66. package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
  67. package/template/claude-task-manager/lib/storage-migration.js +2 -1
  68. package/template/claude-task-manager/lib/transcript-store.js +179 -12
  69. package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
  70. package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
  71. package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
  72. package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
  73. package/template/claude-task-manager/package.json +5 -2
  74. package/template/claude-task-manager/prompt-harvest.js +31 -11
  75. package/template/claude-task-manager/providers/claude-code.js +29 -1
  76. package/template/claude-task-manager/providers/codex.js +13 -1
  77. package/template/claude-task-manager/public/css/setup.css +11 -0
  78. package/template/claude-task-manager/public/css/walle-session.css +132 -4
  79. package/template/claude-task-manager/public/css/walle.css +89 -0
  80. package/template/claude-task-manager/public/icon-16.png +0 -0
  81. package/template/claude-task-manager/public/icon-32.png +0 -0
  82. package/template/claude-task-manager/public/icon-512.png +0 -0
  83. package/template/claude-task-manager/public/index.html +2483 -165
  84. package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
  85. package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
  86. package/template/claude-task-manager/public/js/message-renderer.js +60 -1
  87. package/template/claude-task-manager/public/js/prompts.js +13 -1
  88. package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
  89. package/template/claude-task-manager/public/js/setup.js +54 -10
  90. package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
  91. package/template/claude-task-manager/public/js/stream-view.js +78 -0
  92. package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
  93. package/template/claude-task-manager/public/js/tool-state.js +155 -0
  94. package/template/claude-task-manager/public/js/walle-session.js +887 -326
  95. package/template/claude-task-manager/public/js/walle.js +306 -195
  96. package/template/claude-task-manager/public/m/app.css +1 -0
  97. package/template/claude-task-manager/public/m/app.js +33 -3
  98. package/template/claude-task-manager/queue-engine.js +45 -1
  99. package/template/claude-task-manager/server.js +3367 -540
  100. package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
  101. package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
  102. package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
  103. package/template/claude-task-manager/workers/session-host-process.js +10 -0
  104. package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
  105. package/template/package.json +2 -3
  106. package/template/shared/icons/AppIcon-ctm.icns +0 -0
  107. package/template/shared/icons/AppIcon-walle.icns +0 -0
  108. package/template/wall-e/agent.js +139 -18
  109. package/template/wall-e/api-walle.js +201 -22
  110. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
  111. package/template/wall-e/brain.js +1049 -39
  112. package/template/wall-e/chat.js +427 -86
  113. package/template/wall-e/coding/acceptance-contract.js +26 -1
  114. package/template/wall-e/coding/action-memory-policy.js +353 -0
  115. package/template/wall-e/coding/action-memory-store.js +814 -0
  116. package/template/wall-e/coding/initial-messages.js +197 -0
  117. package/template/wall-e/coding/no-progress-guard.js +327 -0
  118. package/template/wall-e/coding/permission-service.js +88 -22
  119. package/template/wall-e/coding/session-workspaces.js +81 -0
  120. package/template/wall-e/coding/shell-sandbox.js +124 -0
  121. package/template/wall-e/coding/stream-processor.js +63 -2
  122. package/template/wall-e/coding/tool-execution-controller.js +14 -1
  123. package/template/wall-e/coding/tool-registry.js +1 -1
  124. package/template/wall-e/coding/transcript-writer.js +3 -0
  125. package/template/wall-e/coding-orchestrator.js +636 -35
  126. package/template/wall-e/coding-prompts.js +51 -2
  127. package/template/wall-e/docs/model-routing-policy.md +59 -0
  128. package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
  129. package/template/wall-e/extraction/knowledge-extractor.js +76 -23
  130. package/template/wall-e/http/chat-api.js +30 -12
  131. package/template/wall-e/http/model-admin.js +93 -1
  132. package/template/wall-e/lib/background-lanes.js +133 -0
  133. package/template/wall-e/lib/boot-profile.js +11 -0
  134. package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
  135. package/template/wall-e/lib/brain-read-pool-client.js +311 -0
  136. package/template/wall-e/lib/diagnostics-flags.js +87 -0
  137. package/template/wall-e/lib/event-loop-monitor.js +74 -3
  138. package/template/wall-e/lib/mcp-integration.js +7 -1
  139. package/template/wall-e/lib/real-node.js +98 -0
  140. package/template/wall-e/lib/runtime-health.js +206 -0
  141. package/template/wall-e/lib/runtime-worker-pool.js +101 -0
  142. package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
  143. package/template/wall-e/lib/scheduler.js +446 -17
  144. package/template/wall-e/lib/service-health.js +61 -2
  145. package/template/wall-e/lib/service-readiness.js +258 -0
  146. package/template/wall-e/lib/usage.js +152 -0
  147. package/template/wall-e/lib/worker-thread-pool.js +389 -0
  148. package/template/wall-e/llm/client.js +81 -4
  149. package/template/wall-e/llm/default-fallback.js +54 -8
  150. package/template/wall-e/llm/mlx.js +536 -73
  151. package/template/wall-e/llm/mlx.plugin.json +1 -1
  152. package/template/wall-e/llm/ollama.js +342 -43
  153. package/template/wall-e/llm/provider-error.js +18 -1
  154. package/template/wall-e/llm/provider-health-state.js +176 -0
  155. package/template/wall-e/llm/routing-policy.js +796 -0
  156. package/template/wall-e/llm/supported-models.js +5 -0
  157. package/template/wall-e/loops/tasks.js +60 -14
  158. package/template/wall-e/loops/think.js +89 -24
  159. package/template/wall-e/mcp-server.js +192 -28
  160. package/template/wall-e/server.js +32 -7
  161. package/template/wall-e/skills/script-skill-runner.js +8 -1
  162. package/template/wall-e/skills/skill-planner.js +64 -1
  163. package/template/wall-e/tools/builtin-middleware.js +67 -2
  164. package/template/wall-e/tools/local-tools.js +116 -26
  165. package/template/wall-e/tools/permission-checker.js +52 -4
  166. package/template/wall-e/tools/permission-rules.js +36 -0
  167. package/template/wall-e/tools/shell-analyzer.js +46 -1
  168. package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
  169. package/template/wall-e/training/real-trajectory-miner.js +2617 -0
  170. package/template/wall-e/training/replay-eval-analysis.js +151 -0
  171. package/template/wall-e/training/run-shell-command-selector.js +277 -0
  172. package/template/wall-e/training/tool-sft-dataset.js +312 -0
  173. package/template/wall-e/training/tool-sft-renderers.js +144 -0
  174. package/template/wall-e/training/tool-trace-harvester.js +1440 -0
  175. package/template/wall-e/training/trajectory-action-selector.js +364 -0
  176. package/template/wall-e/weather-runtime.js +232 -0
  177. package/template/wall-e/workers/brain-owner-worker.js +162 -0
  178. package/template/wall-e/workers/brain-read-worker.js +148 -0
  179. package/template/wall-e/workers/runtime-worker.js +145 -0
@@ -73,12 +73,19 @@ function startServer(options = {}) {
73
73
  return;
74
74
  }
75
75
 
76
- // Task stop endpoint — must be in this process (has the child process reference)
76
+ // Task stop endpoint — task execution may live in a runtime worker, so ask
77
+ // every worker and keep the old in-process path as a recovery fallback.
77
78
  const taskStopMatch = url.pathname.match(/^\/api\/wall-e\/tasks\/([^/]+)\/stop$/);
78
79
  if (taskStopMatch && req.method === 'POST') {
79
80
  try {
81
+ let stopped = false;
82
+ try {
83
+ const { broadcastRuntimeWorker } = require('./lib/runtime-worker-pool');
84
+ const results = await broadcastRuntimeWorker('tasks.stop', { taskId: taskStopMatch[1] }, { timeoutMs: 1500 });
85
+ stopped = results.some((entry) => entry.ok && entry.result?.stopped);
86
+ } catch {}
80
87
  const { stopTask } = require('./loops/tasks');
81
- const stopped = stopTask(taskStopMatch[1]);
88
+ stopped = stopTask(taskStopMatch[1]) || stopped;
82
89
  const body = JSON.stringify({ data: { stopped } });
83
90
  res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
84
91
  res.end(body);
@@ -94,9 +101,20 @@ function startServer(options = {}) {
94
101
  const taskRunNowMatch = url.pathname.match(/^\/api\/wall-e\/tasks\/([^/]+)\/run-now$/);
95
102
  if (taskRunNowMatch && req.method === 'POST') {
96
103
  try {
97
- const { runTaskById } = require('./loops/tasks');
98
- // Fire and forget — respond immediately, task runs in background
99
- runTaskById(taskRunNowMatch[1]).catch(e => {
104
+ const taskId = taskRunNowMatch[1];
105
+ // Fire and forget — respond immediately, task runs in the runtime
106
+ // worker when available, with local execution as the opt-out/failure
107
+ // fallback.
108
+ (async () => {
109
+ try {
110
+ const { requestRuntimeWorker } = require('./lib/runtime-worker-pool');
111
+ await requestRuntimeWorker('tasks.runById', { taskId }, { heavy: true, timeoutMs: 0 });
112
+ } catch (workerErr) {
113
+ console.warn('[wall-e] run-now worker unavailable; falling back locally:', workerErr.message);
114
+ const { runTaskById } = require('./loops/tasks');
115
+ await runTaskById(taskId);
116
+ }
117
+ })().catch(e => {
100
118
  console.error('[wall-e] run-now background error:', e.message);
101
119
  });
102
120
  const body = JSON.stringify({ data: { started: true } });
@@ -110,12 +128,19 @@ function startServer(options = {}) {
110
128
  return;
111
129
  }
112
130
 
113
- // Task logs endpoint — must be in this process (in-memory buffer)
131
+ // Task logs endpoint — task execution may live in a runtime worker.
114
132
  const taskLogsMatch = url.pathname.match(/^\/api\/wall-e\/tasks\/([^/]+)\/logs$/);
115
133
  if (taskLogsMatch && req.method === 'GET') {
116
134
  try {
135
+ let logs = null;
136
+ try {
137
+ const { broadcastRuntimeWorker } = require('./lib/runtime-worker-pool');
138
+ const results = await broadcastRuntimeWorker('tasks.getLogs', { taskId: taskLogsMatch[1] }, { timeoutMs: 1500 });
139
+ const hit = results.find((entry) => entry.ok && entry.result);
140
+ if (hit) logs = hit.result;
141
+ } catch {}
117
142
  const { getTaskLogs } = require('./loops/tasks');
118
- const logs = getTaskLogs(taskLogsMatch[1]);
143
+ logs = logs || getTaskLogs(taskLogsMatch[1]);
119
144
  const body = JSON.stringify({ data: logs || { lines: [], status: 'unknown' } });
120
145
  res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
121
146
  res.end(body);
@@ -60,7 +60,14 @@ function runScriptSkill(skill, task = {}, opts = {}) {
60
60
  for (const line of chunk.split('\n').filter(Boolean)) {
61
61
  log(line);
62
62
  if (line.startsWith('CHECKPOINT:') && typeof opts.onCheckpoint === 'function') {
63
- opts.onCheckpoint(line.slice(11).trim());
63
+ try {
64
+ const maybePromise = opts.onCheckpoint(line.slice(11).trim());
65
+ if (maybePromise && typeof maybePromise.catch === 'function') {
66
+ maybePromise.catch(err => log(`[checkpoint skipped] ${err?.message || err}`));
67
+ }
68
+ } catch (err) {
69
+ log(`[checkpoint skipped] ${err?.message || err}`);
70
+ }
64
71
  }
65
72
  }
66
73
  });
@@ -16,6 +16,7 @@ try { telemetry = require('../telemetry'); } catch { telemetry = { trackError()
16
16
 
17
17
  const DECISION_TELEMETRY_MIN_INTERVAL_MS = 60 * 60 * 1000;
18
18
  const _lastDecisionTelemetry = new Map();
19
+ const _lastBackoffRecoveryLog = new Map();
19
20
  let _slackAlertMigrationDone = false;
20
21
 
21
22
  function isBrainReady() {
@@ -54,6 +55,25 @@ function shouldTrackDecisionTelemetry(skillName, decision) {
54
55
  return true;
55
56
  }
56
57
 
58
+ function shouldLogBackoffRecovery(skillName, decision) {
59
+ const details = decision?.details || {};
60
+ const signature = [
61
+ decision?.reason || '',
62
+ details.consecutive_failures || 0,
63
+ details.next_allowed_at || '',
64
+ details.backoff_ms || 0,
65
+ details.schedule_source || '',
66
+ ].join('|');
67
+ const key = String(skillName || '');
68
+ if (_lastBackoffRecoveryLog.get(key) === signature) return false;
69
+ _lastBackoffRecoveryLog.set(key, signature);
70
+ if (_lastBackoffRecoveryLog.size > 500) {
71
+ const first = _lastBackoffRecoveryLog.keys().next().value;
72
+ if (first) _lastBackoffRecoveryLog.delete(first);
73
+ }
74
+ return true;
75
+ }
76
+
57
77
  // ── Service alerts ─────────────────────────────────────────────────────────
58
78
  // Stored in kv_store as JSON array. Surfaces auth failures, version updates, etc.
59
79
 
@@ -132,6 +152,48 @@ function clearServiceAlerts(service) {
132
152
  brain.setKv('service_alerts', JSON.stringify(alerts));
133
153
  }
134
154
 
155
+ function _providerAlertMatches(alert, provider, model) {
156
+ const providerNeedle = String(provider || '').trim().toLowerCase();
157
+ const modelNeedle = String(model || '').trim().toLowerCase();
158
+ const providerError = alert?.provider_error || alert?.providerError || {};
159
+ const alertProvider = String(providerError.provider || alert?.provider || '').trim().toLowerCase();
160
+ const alertModel = String(providerError.model || alert?.model || '').trim().toLowerCase();
161
+ if (providerNeedle && alertProvider && alertProvider !== providerNeedle) return false;
162
+ if (modelNeedle && alertModel && alertModel !== modelNeedle) return false;
163
+ if (providerNeedle && !alertProvider && !String(alert?.type || '').toLowerCase().includes(providerNeedle)) return false;
164
+ return true;
165
+ }
166
+
167
+ function _isTransientProviderAlert(alert) {
168
+ const providerError = alert?.provider_error || alert?.providerError || {};
169
+ const text = [
170
+ alert?.service,
171
+ alert?.type,
172
+ alert?.title,
173
+ alert?.message,
174
+ providerError.type,
175
+ providerError.title,
176
+ providerError.message,
177
+ providerError.userMessage,
178
+ ].filter(Boolean).join(' ').toLowerCase();
179
+ if (!text.includes('ai_provider') && String(alert?.service || '') !== 'ai_provider') return false;
180
+ if (/auth|quota|billing|credit|insufficient|invalid|forbidden|unauthorized|permission/.test(text)) return false;
181
+ return /timeout|timed out|abort|aborted|network|unavailable|overloaded|rate_limit|connection|reset|econn/.test(text);
182
+ }
183
+
184
+ function clearRecoveredProviderAlerts({ provider, model } = {}) {
185
+ const before = getServiceAlerts();
186
+ const after = before.filter((alert) => {
187
+ if (!_isTransientProviderAlert(alert)) return true;
188
+ if (!_providerAlertMatches(alert, provider, model)) return true;
189
+ return false;
190
+ });
191
+ if (after.length !== before.length) {
192
+ brain.setKv('service_alerts', JSON.stringify(after));
193
+ }
194
+ return { cleared: before.length - after.length };
195
+ }
196
+
135
197
  function clearResolvedSlackHealthAlerts({ authenticated = false, ownerConfigured = false } = {}) {
136
198
  if (!authenticated) return { cleared: 0 };
137
199
  const before = getServiceAlerts();
@@ -631,7 +693,7 @@ async function runDueSkills(opts = {}) {
631
693
  }
632
694
  // Recovery logger: long-form structured trace for post-incident
633
695
  // debugging (Items E + A of error-recovery deep-dive).
634
- if (decision.reason === 'backed_off') {
696
+ if (decision.reason === 'backed_off' && shouldLogBackoffRecovery(skill.name, decision)) {
635
697
  try {
636
698
  require('../lib/recovery-logger').warn('skill-dispatch', 'backoff_applied', {
637
699
  skill: skill.name,
@@ -909,6 +971,7 @@ module.exports = {
909
971
  addServiceAlert,
910
972
  dismissServiceAlert,
911
973
  clearServiceAlerts,
974
+ clearRecoveredProviderAlerts,
912
975
  clearResolvedSlackHealthAlerts,
913
976
  clearResolvedGoogleWorkspaceHealthAlerts,
914
977
  _test: {
@@ -15,6 +15,69 @@ function providerId(provider) {
15
15
  return provider.type || provider.provider || provider.id || '';
16
16
  }
17
17
 
18
+ const OLLAMA_GEMMA_CORE_TOOL_NAMES = new Set([
19
+ 'read_file',
20
+ 'write_file',
21
+ 'run_shell',
22
+ 'bg_output',
23
+ 'bg_kill',
24
+ 'glob',
25
+ 'grep_files',
26
+ 'list_directory',
27
+ 'browser_screenshot',
28
+ 'browser_smoke_test',
29
+ 'check_url',
30
+ 'web_search',
31
+ 'web_fetch',
32
+ 'start_static_server',
33
+ 'stop_static_server',
34
+ 'pdf_info',
35
+ 'pdf_render_pages',
36
+ 'pdf_read_pages',
37
+ 'make_pdf',
38
+ 'edit_file',
39
+ 'apply_patch',
40
+ 'multi_edit',
41
+ 'update_todos',
42
+ 'lsp_symbols',
43
+ 'lsp_definition',
44
+ 'lsp_references',
45
+ 'lsp_diagnostics',
46
+ 'lsp_hover',
47
+ 'lsp_implementation',
48
+ 'ask_user',
49
+ ]);
50
+
51
+ function isGemma4Ollama(provider, model) {
52
+ return providerId(provider) === 'ollama' && /^gemma4:/i.test(String(model || ''));
53
+ }
54
+
55
+ function compactToolsForGemma4Ollama(ctx = {}, tools = []) {
56
+ if (process.env.WALLE_OLLAMA_FULL_TOOL_CATALOG === '1' || process.env.WALLE_OLLAMA_COMPACT_TOOLS === '0') {
57
+ return tools;
58
+ }
59
+ const provider = ctx.provider || '';
60
+ const model = String(ctx.model || '');
61
+ if (!isGemma4Ollama(provider, model)) return tools;
62
+ const core = tools.filter((tool) => OLLAMA_GEMMA_CORE_TOOL_NAMES.has(tool?.name));
63
+ if (core.length === 0) return tools;
64
+
65
+ const promptCapabilities = ctx.promptCapabilities || {};
66
+ const explicitSkills = Array.isArray(promptCapabilities.skills) && promptCapabilities.skills.length > 0;
67
+ const explicitMcp = Boolean(
68
+ promptCapabilities.mcp
69
+ && (
70
+ (Array.isArray(promptCapabilities.mcp.servers) && promptCapabilities.mcp.servers.length > 0)
71
+ || (Array.isArray(promptCapabilities.mcp.tools) && promptCapabilities.mcp.tools.length > 0)
72
+ )
73
+ );
74
+ const extras = tools.filter((tool) => (
75
+ (explicitSkills && (tool?.name === 'load_skill' || tool?.name === 'skill'))
76
+ || (explicitMcp && tool?.name === 'mcp_call')
77
+ ));
78
+ return [...core, ...extras];
79
+ }
80
+
18
81
  function isOpenWeightReasoningModel(provider, model) {
19
82
  if (providerId(provider) !== 'ollama') return false;
20
83
  return isReasoningCapableOllamaModel(model);
@@ -178,12 +241,14 @@ Do not make any edits. Review the diff and assess quality.`);
178
241
  // -- tool.definitions: adapt per model --
179
242
  mw.use('tool.definitions', async (ctx, tools) => {
180
243
  const provider = ctx.provider || opts.provider;
181
- if (provider === 'ollama') {
244
+ const model = String(ctx.model || opts.model || '');
245
+ if (providerId(provider) === 'ollama') {
182
246
  // Brief descriptions for small models
183
- return tools.map(t => ({
247
+ const shortened = tools.map(t => ({
184
248
  ...t,
185
249
  description: t.description.split('.')[0] + '.',
186
250
  }));
251
+ return compactToolsForGemma4Ollama({ ...ctx, provider, model }, shortened);
187
252
  }
188
253
  return tools;
189
254
  });
@@ -10,8 +10,29 @@ const https = require('https');
10
10
  const execFileAsync = promisify(execFile);
11
11
  const execAsync = promisify(exec);
12
12
  const { SHELL_ALLOWLIST, SHELL_DENYLIST } = require('./shell-policy');
13
+ const shellSandbox = require('../coding/shell-sandbox');
13
14
 
14
15
  const HOME = process.env.HOME || '/tmp';
16
+
17
+ // When WALLE_SHELL_SANDBOX is on and an OS sandbox backend exists, return the
18
+ // sandbox-wrapped argv for a run_shell command (Seatbelt/bubblewrap; writes contained to
19
+ // cwd+tmp, network allowed). Returns null to run un-sandboxed (flag off, no backend, or a
20
+ // hard-denylisted head — those keep going through the normal allowlist gate and are denied
21
+ // there). Under the sandbox the OS is the safety boundary, so the allowlist requirement is
22
+ // skipped for the command; the destructive denylist is still honored here as a floor.
23
+ function maybeSandboxShellArgv(commandStr, cwd) {
24
+ if (!shellSandbox.shellSandboxEnabled() || !shellSandbox.sandboxAvailable()) return null;
25
+ // Defense-in-depth floor: never sandbox a command whose ANY clause head is on the hard
26
+ // denylist (sudo/dd/mkfs/…). Split on shell operators so `echo x && sudo y` is caught too.
27
+ // Returning null routes it through the normal allowlist gate, which denies it. The OS
28
+ // sandbox is still the real boundary for everything that does run.
29
+ const clauses = String(commandStr || '').split(/&&|\|\||[|;\n]/);
30
+ for (const clause of clauses) {
31
+ const head = path.basename(clause.trim().split(/\s+/)[0] || '');
32
+ if (head && SHELL_DENYLIST.has(head)) return null;
33
+ }
34
+ return shellSandbox.wrapShellArgv({ command: commandStr, cwd });
35
+ }
15
36
  const MAIL_AUTOMATION_LOCK_PATH = process.env.WALLE_MAIL_LOCK_PATH ||
16
37
  path.join(os.tmpdir(), 'wall-e-mail-appleevent.lock');
17
38
  const MAIL_AUTOMATION_COOLDOWN_PATH = process.env.WALLE_MAIL_COOLDOWN_PATH ||
@@ -1527,9 +1548,14 @@ async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
1527
1548
  let background = false;
1528
1549
  let sessionId = '';
1529
1550
  let persist = false;
1551
+ // signal: aborts the child when the turn is cancelled (user Stop). onProgress:
1552
+ // a heartbeat callback so a long-running command shows live motion instead of a
1553
+ // frozen "Running run_shell" card. Both are best-effort and optional.
1554
+ let signal = null;
1555
+ let onProgress = null;
1530
1556
  if (typeof commandOrOpts === 'object' && commandOrOpts !== null) {
1531
- // New format: runShell({ command, args, timeout_ms, cwd, background })
1532
- ({ command, args, timeout_ms = 30000, cwd, background = false, sessionId = '', persist = false } = commandOrOpts);
1557
+ // New format: runShell({ command, args, timeout_ms, cwd, background, signal, onProgress })
1558
+ ({ command, args, timeout_ms = 30000, cwd, background = false, sessionId = '', persist = false, signal = null, onProgress = null } = commandOrOpts);
1533
1559
  } else {
1534
1560
  // Legacy format: runShell(command, args, { timeout_ms })
1535
1561
  command = commandOrOpts;
@@ -1562,28 +1588,84 @@ async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
1562
1588
  return runShellBackground({ command: commandStr, cwd, sessionId, persist });
1563
1589
  }
1564
1590
 
1565
- // Tree-sitter analysis gates execution
1566
- const analysis = await analyzeShellCommand(commandStr, cwd || process.cwd());
1567
- if (!analysis.allowed) {
1568
- throw new Error(analysis.reason);
1591
+ // Run contained in the OS sandbox when enabled; otherwise gate on the static
1592
+ // allowlist via tree-sitter analysis.
1593
+ const sandboxArgv = maybeSandboxShellArgv(commandStr, cwd);
1594
+ if (!sandboxArgv) {
1595
+ const analysis = await analyzeShellCommand(commandStr, cwd || process.cwd());
1596
+ if (!analysis.allowed) {
1597
+ throw new Error(analysis.reason);
1598
+ }
1569
1599
  }
1570
1600
 
1601
+ // Per-command timeout: honor the caller's value (the controller defaults it from
1602
+ // WALLE_SHELL_TIMEOUT_MS) but clamp to a hard ceiling so a single command can
1603
+ // never wedge a turn indefinitely.
1604
+ const SHELL_HARD_TIMEOUT_CAP_MS = 600000; // 10 min
1605
+ const SHELL_PROGRESS_AFTER_MS = Number(process.env.WALLE_SHELL_PROGRESS_AFTER_MS) || 8000;
1606
+ let effectiveTimeout = Number(timeout_ms) > 0 ? Number(timeout_ms) : (Number(process.env.WALLE_SHELL_TIMEOUT_MS) || 30000);
1607
+ effectiveTimeout = Math.min(effectiveTimeout, SHELL_HARD_TIMEOUT_CAP_MS);
1608
+
1609
+ // Heartbeat: surface a "still running (Ns)" progress beat for long commands so a
1610
+ // multi-second shell step doesn't read as a frozen card. Gated by WALLE_SHELL_PROGRESS.
1611
+ const startedAt = Date.now();
1612
+ const progressEnabled = typeof onProgress === 'function' && process.env.WALLE_SHELL_PROGRESS !== '0';
1613
+ let hbTimer = null;
1614
+ if (progressEnabled) {
1615
+ hbTimer = setInterval(() => {
1616
+ const elapsedMs = Date.now() - startedAt;
1617
+ try {
1618
+ onProgress({
1619
+ type: 'shell_progress',
1620
+ message: `run_shell still running (${Math.round(elapsedMs / 1000)}s): ${commandStr.slice(0, 80)}`,
1621
+ elapsedMs,
1622
+ command: commandStr.slice(0, 200),
1623
+ });
1624
+ } catch { /* progress is best-effort */ }
1625
+ }, SHELL_PROGRESS_AFTER_MS);
1626
+ if (hbTimer.unref) hbTimer.unref();
1627
+ }
1628
+
1629
+ // Serial loops (for…do…done / while…do…done) of separate sub-commands are the
1630
+ // classic budget sink — nudge the model toward one batched command or background.
1631
+ const looksLikeSerialLoop = /\bfor\b[\s\S]*\bdo\b[\s\S]*\bdone\b/.test(commandStr) || /\bwhile\b[\s\S]*\bdo\b[\s\S]*\bdone\b/.test(commandStr);
1632
+
1571
1633
  try {
1572
- const { stdout, stderr } = await execAsync(commandStr, {
1573
- timeout: timeout_ms,
1574
- maxBuffer: 1024 * 1024, // 1MB
1575
- cwd: cwd || undefined,
1576
- env: { ...process.env, HOME },
1577
- shell: '/bin/bash',
1578
- });
1579
- return {
1634
+ const { stdout, stderr } = sandboxArgv
1635
+ ? await execFileAsync(sandboxArgv[0], sandboxArgv.slice(1), {
1636
+ timeout: effectiveTimeout,
1637
+ maxBuffer: 1024 * 1024, // 1MB
1638
+ cwd: cwd || undefined,
1639
+ env: { ...process.env, HOME },
1640
+ signal: signal || undefined,
1641
+ })
1642
+ : await execAsync(commandStr, {
1643
+ timeout: effectiveTimeout,
1644
+ maxBuffer: 1024 * 1024, // 1MB
1645
+ cwd: cwd || undefined,
1646
+ env: { ...process.env, HOME },
1647
+ shell: '/bin/bash',
1648
+ signal: signal || undefined,
1649
+ });
1650
+ const elapsedMs = Date.now() - startedAt;
1651
+ const result = {
1580
1652
  stdout: stdout.slice(0, 50000),
1581
1653
  stderr: stderr.slice(0, 5000),
1582
1654
  };
1655
+ if (looksLikeSerialLoop && elapsedMs >= SHELL_PROGRESS_AFTER_MS) {
1656
+ result.hint = `This serial loop took ${Math.round(elapsedMs / 1000)}s. For many similar operations, prefer ONE batched command (most CLIs accept multiple paths) or run it with background:true and poll bg_output, instead of a loop of separate run_shell calls.`;
1657
+ }
1658
+ return result;
1583
1659
  } catch (err) {
1584
- // Timeout / killed: re-throw so callers see the failure
1660
+ // User Stop / turn abort: the child was killed via the AbortSignal. Return a
1661
+ // clean cancelled result rather than a confusing "Command failed: SIGTERM".
1662
+ if (signal?.aborted || err.code === 'ABORT_ERR' || err.name === 'AbortError') {
1663
+ return { stdout: (err.stdout || '').slice(0, 50000), stderr: (err.stderr || '').slice(0, 5000), cancelled: true, error: 'Command cancelled' };
1664
+ }
1665
+ // Timeout / killed: re-throw so callers see the failure, with a route forward.
1585
1666
  if (err.killed || err.signal) {
1586
- throw new Error(`Command failed: ${err.signal || 'timed out'}`);
1667
+ const why = err.signal === 'SIGTERM' ? `timed out after ${effectiveTimeout}ms` : (err.signal || 'killed');
1668
+ throw new Error(`Command failed: ${why}. For long-running work, pass background:true and poll bg_output, or raise timeout_ms (cap ${SHELL_HARD_TIMEOUT_CAP_MS}ms).`);
1587
1669
  }
1588
1670
  // Non-zero exit code: return output as result, not as error
1589
1671
  // This is important for test runners that exit 1 on test failure
@@ -1595,6 +1677,8 @@ async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
1595
1677
  };
1596
1678
  }
1597
1679
  throw err; // Re-throw actual errors (spawn failure, etc.)
1680
+ } finally {
1681
+ if (hbTimer) clearInterval(hbTimer);
1598
1682
  }
1599
1683
  }
1600
1684
 
@@ -3075,19 +3159,25 @@ async function runShellBackground({ command, cwd, sessionId = '', persist = fals
3075
3159
  if (!command || typeof command !== 'string') return { ok: false, error: 'command is required' };
3076
3160
  const { analyzeShellCommand, initParser } = require('./shell-analyzer');
3077
3161
  await initParser();
3078
- const analysis = await analyzeShellCommand(command, cwd || process.cwd());
3079
- if (!analysis.allowed) return { ok: false, error: analysis.reason };
3162
+ const sandboxArgv = maybeSandboxShellArgv(command, cwd);
3163
+ if (!sandboxArgv) {
3164
+ const analysis = await analyzeShellCommand(command, cwd || process.cwd());
3165
+ if (!analysis.allowed) return { ok: false, error: analysis.reason };
3166
+ }
3080
3167
 
3081
3168
  const logFile = path.join(os.tmpdir(), `walle-bg-${Date.now()}-${Math.random().toString(36).slice(2)}.log`);
3082
3169
  const logFd = fs.openSync(logFile, 'a');
3083
3170
  let child;
3084
3171
  try {
3085
- child = require('child_process').spawn('/bin/bash', ['-c', command], {
3086
- cwd: cwd || undefined,
3087
- env: { ...process.env, HOME },
3088
- stdio: ['ignore', logFd, logFd],
3089
- detached: true, // own process group so bg_kill can stop child trees
3090
- });
3172
+ child = require('child_process').spawn(
3173
+ sandboxArgv ? sandboxArgv[0] : '/bin/bash',
3174
+ sandboxArgv ? sandboxArgv.slice(1) : ['-c', command],
3175
+ {
3176
+ cwd: cwd || undefined,
3177
+ env: { ...process.env, HOME },
3178
+ stdio: ['ignore', logFd, logFd],
3179
+ detached: true, // own process group so bg_kill can stop child trees
3180
+ });
3091
3181
  } catch (err) {
3092
3182
  fs.closeSync(logFd);
3093
3183
  return { ok: false, error: err.message };
@@ -4801,14 +4891,14 @@ function getLspManager() { return _lspManager; }
4801
4891
 
4802
4892
  // ── Tool executor dispatch ──
4803
4893
 
4804
- async function executeLocalTool(name, input) {
4894
+ async function executeLocalTool(name, input, ctx = {}) {
4805
4895
  // LSP tools (SP5) — language-aware code intelligence
4806
4896
  if (name.startsWith('lsp_')) {
4807
4897
  return _executeLspTool(name, input);
4808
4898
  }
4809
4899
 
4810
4900
  switch (name) {
4811
- case 'run_shell': return runShell({ command: input.command, args: input.args, timeout_ms: input.timeout_ms, cwd: input.cwd, background: input.background, sessionId: input.sessionId || input.session_id, persist: input.persist });
4901
+ case 'run_shell': return runShell({ command: input.command, args: input.args, timeout_ms: input.timeout_ms, cwd: input.cwd, background: input.background, sessionId: input.sessionId || input.session_id, persist: input.persist, signal: ctx?.signal || undefined, onProgress: ctx?.onProgress || undefined });
4812
4902
  case 'bg_output': return bgOutput(input);
4813
4903
  case 'bg_kill': return bgKill(input);
4814
4904
  case 'read_file': return readFile(input.file_path, { max_bytes: input.max_bytes, offset: input.offset, limit: input.limit, sessionId: input.sessionId, projectRoot: input.projectRoot });
@@ -69,6 +69,15 @@ let _ctmBaseUrl = null;
69
69
  let _ctmToken = null;
70
70
  let _userRulesCache = null;
71
71
 
72
+ // How long to wait for CTM's auto-approver (/api/permissions/walle-check) before
73
+ // giving up and falling through to the local static rules. Must comfortably cover
74
+ // the goal-aligned LLM verifier, whose own budget is BUILTIN_TIMEOUT_MS = 20s in
75
+ // claude-task-manager/lib/auto-approval-verifier.js. The previous 2s cut the
76
+ // verifier off for EVERY medium+ command (anything with a loop, an unknown binary,
77
+ // etc.) → the bridge aborted → the agent parked on a static "ask" for routine local
78
+ // commands. 25s = 20s verifier cap + network/parse overhead. Env-overridable.
79
+ const CTM_APPROVAL_TIMEOUT_MS = Number(process.env.WALLE_CTM_APPROVAL_TIMEOUT_MS) || 25000;
80
+
72
81
  function configure({ ctmUrl, ctmToken } = {}) {
73
82
  _ctmBaseUrl = ctmUrl || null;
74
83
  _ctmToken = ctmToken || null;
@@ -154,7 +163,7 @@ function listTrustedDirectories() {
154
163
  *
155
164
  * Returns: { decision: 'allow'|'deny'|'ask', source, reason, ruleId? }
156
165
  */
157
- async function checkPermission({ tool, command, commandTokens, commandClauses, args, projectPath, sessionId, mode }) {
166
+ async function checkPermission({ tool, command, commandTokens, commandClauses, args, projectPath, sessionId, mode, goal }) {
158
167
  // Layer 1: Hardcoded denylist (safety floor — never overridable)
159
168
  if (tool === 'run_shell') {
160
169
  // Extract first command token for denylist check
@@ -180,13 +189,16 @@ async function checkPermission({ tool, command, commandTokens, commandClauses, a
180
189
  // so coding never hard-breaks offline.
181
190
  if (_ctmBaseUrl && tool === 'run_shell') {
182
191
  try {
183
- const body = { tool, command: command || '', args: args || [], project_path: projectPath || '', session_id: sessionId || '', agent: 'walle' };
192
+ // `goal` (the coding task) lets CTM's goal-aligned verifier judge by
193
+ // goal-alignment instead of its weaker no-goal path; best-effort, a missing
194
+ // goal degrades to the prior behaviour.
195
+ const body = { tool, command: command || '', args: args || [], project_path: projectPath || '', session_id: sessionId || '', goal: goal || '', agent: 'walle' };
184
196
  const url = `${_ctmBaseUrl}/api/permissions/walle-check${_ctmToken ? '?token=' + encodeURIComponent(_ctmToken) : ''}`;
185
197
  const resp = await fetch(url, {
186
198
  method: 'POST',
187
199
  headers: { 'Content-Type': 'application/json' },
188
200
  body: JSON.stringify(body),
189
- signal: AbortSignal.timeout(2000),
201
+ signal: AbortSignal.timeout(CTM_APPROVAL_TIMEOUT_MS),
190
202
  });
191
203
  if (resp.ok) {
192
204
  const result = await resp.json();
@@ -269,6 +281,42 @@ async function checkPermission({ tool, command, commandTokens, commandClauses, a
269
281
  return { decision: 'allow', source: 'default', reason: 'No restrictions' };
270
282
  }
271
283
 
284
+ /**
285
+ * Register a parked approval with CTM so it surfaces in the shared "Pending" tab
286
+ * and survives a page reload. Best-effort: when CTM is unreachable the local ask
287
+ * still stands on its own (durable transcript card + WORK bar), so a failure here
288
+ * must never block the turn. Returns the CTM row id, or null.
289
+ */
290
+ async function registerEscalation({ requestId, tool, command, pattern, projectPath, sessionId, reason, riskLevel }) {
291
+ if (!_ctmBaseUrl || !requestId) return null;
292
+ try {
293
+ const url = `${_ctmBaseUrl}/api/permissions/walle-register${_ctmToken ? '?token=' + encodeURIComponent(_ctmToken) : ''}`;
294
+ const resp = await fetch(url, {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({
298
+ request_id: requestId,
299
+ tool: tool || '',
300
+ command: command || '',
301
+ pattern: pattern || '',
302
+ project_path: projectPath || '',
303
+ session_id: sessionId || '',
304
+ reason: reason || '',
305
+ risk_level: riskLevel || 'medium',
306
+ agent: 'walle',
307
+ }),
308
+ signal: AbortSignal.timeout(4000),
309
+ });
310
+ if (resp.ok) {
311
+ const result = await resp.json();
312
+ return result && result.rowId != null ? result.rowId : null;
313
+ }
314
+ } catch {
315
+ // CTM unavailable — the local ask still surfaces; nothing to do.
316
+ }
317
+ return null;
318
+ }
319
+
272
320
  /**
273
321
  * Record a permission decision (for learning)
274
322
  */
@@ -286,7 +334,7 @@ async function recordDecision({ tool, command, args, decision, sessionId, projec
286
334
  }
287
335
 
288
336
  module.exports = {
289
- checkPermission, recordDecision, configure,
337
+ checkPermission, recordDecision, registerEscalation, configure,
290
338
  evaluateBashClauses,
291
339
  DENYLIST, ALWAYS_ASK_TOOLS, NAVIGATION_BUILTINS,
292
340
  isDirectoryTrusted, trustDirectory, untrustDirectory, listTrustedDirectories,
@@ -145,6 +145,42 @@ const DEFAULT_RULES = [
145
145
  { permission: 'bash', pattern: 'sed *', action: 'allow' },
146
146
  { permission: 'bash', pattern: 'jq *', action: 'allow' },
147
147
  { permission: 'bash', pattern: 'diff *', action: 'allow' },
148
+ // More read-only inspection commands: allow. These only READ files/metadata or
149
+ // print derived info — they never write, delete, or execute another binary — so
150
+ // they should never park a coding turn awaiting approval. (`du` was the trailing
151
+ // clause that parked the live `cd … && find … | du -sh` turn for ~24 min: the
152
+ // per-clause most-restrictive evaluator returned `ask` because `du` had no rule.)
153
+ { permission: 'bash', pattern: 'du *', action: 'allow' },
154
+ { permission: 'bash', pattern: 'df *', action: 'allow' },
155
+ { permission: 'bash', pattern: 'stat *', action: 'allow' },
156
+ { permission: 'bash', pattern: 'file *', action: 'allow' },
157
+ { permission: 'bash', pattern: 'tree *', action: 'allow' },
158
+ { permission: 'bash', pattern: 'realpath *', action: 'allow' },
159
+ { permission: 'bash', pattern: 'readlink *', action: 'allow' },
160
+ { permission: 'bash', pattern: 'basename *', action: 'allow' },
161
+ { permission: 'bash', pattern: 'dirname *', action: 'allow' },
162
+ { permission: 'bash', pattern: 'which *', action: 'allow' },
163
+ { permission: 'bash', pattern: 'type *', action: 'allow' },
164
+ { permission: 'bash', pattern: 'whoami *', action: 'allow' },
165
+ { permission: 'bash', pattern: 'id *', action: 'allow' },
166
+ { permission: 'bash', pattern: 'hostname *', action: 'allow' },
167
+ { permission: 'bash', pattern: 'uname *', action: 'allow' },
168
+ { permission: 'bash', pattern: 'printf *', action: 'allow' },
169
+ { permission: 'bash', pattern: 'seq *', action: 'allow' },
170
+ { permission: 'bash', pattern: 'nl *', action: 'allow' },
171
+ { permission: 'bash', pattern: 'column *', action: 'allow' },
172
+ { permission: 'bash', pattern: 'tac *', action: 'allow' },
173
+ { permission: 'bash', pattern: 'rev *', action: 'allow' },
174
+ { permission: 'bash', pattern: 'fold *', action: 'allow' },
175
+ { permission: 'bash', pattern: 'comm *', action: 'allow' },
176
+ { permission: 'bash', pattern: 'cksum *', action: 'allow' },
177
+ { permission: 'bash', pattern: 'shasum *', action: 'allow' },
178
+ { permission: 'bash', pattern: 'md5sum *', action: 'allow' },
179
+ { permission: 'bash', pattern: 'sha256sum *', action: 'allow' },
180
+ { permission: 'bash', pattern: 'xxd *', action: 'allow' },
181
+ { permission: 'bash', pattern: 'od *', action: 'allow' },
182
+ { permission: 'bash', pattern: 'hexdump *', action: 'allow' },
183
+ { permission: 'bash', pattern: 'strings *', action: 'allow' },
148
184
  { permission: 'bash', pattern: 'git status *', action: 'allow' },
149
185
  { permission: 'bash', pattern: 'git log *', action: 'allow' },
150
186
  { permission: 'bash', pattern: 'git diff *', action: 'allow' },