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
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ const SERVER_EVENTS = new Set([
4
+ 'check_started',
5
+ 'check_completed',
6
+ 'check_failed',
7
+ 'update_available',
8
+ 'api_check',
9
+ 'apply_requested',
10
+ 'apply_ignored',
11
+ 'apply_started',
12
+ 'apply_spawn_error',
13
+ ]);
14
+
15
+ const CLIENT_EVENTS = new Set([
16
+ 'ui_prompt_shown',
17
+ 'ui_action',
18
+ ]);
19
+
20
+ const SURFACES = new Set(['banner', 'wizard', 'api', 'startup', 'scheduled', 'manual', 'unknown']);
21
+ const ACTIONS = new Set(['later', 'skip', 'apply', 'apply_started', 'apply_failed', 'already_up_to_date']);
22
+ const RESULTS = new Set(['available', 'up_to_date', 'error', 'started', 'ignored']);
23
+
24
+ function cleanEnum(value, allowed, fallback = 'unknown') {
25
+ const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '_');
26
+ return allowed.has(normalized) ? normalized : fallback;
27
+ }
28
+
29
+ function cleanVersion(value) {
30
+ const raw = String(value || '').trim().replace(/^v/i, '');
31
+ if (!raw || raw.length > 64) return '';
32
+ return /^[0-9A-Za-z.+_-]+$/.test(raw) ? raw : '';
33
+ }
34
+
35
+ function cleanBool(value) {
36
+ return value === true ? true : value === false ? false : undefined;
37
+ }
38
+
39
+ function errorKind(error) {
40
+ const message = String(error?.message || error || '').toLowerCase();
41
+ if (!message) return 'unknown';
42
+ if (/timeout|timed out|abort/.test(message)) return 'timeout';
43
+ if (/network|fetch|enotfound|eai_again|econn|socket/.test(message)) return 'network';
44
+ if (/npm registry returned 4\d\d/.test(message)) return 'registry_client_error';
45
+ if (/npm registry returned 5\d\d/.test(message)) return 'registry_server_error';
46
+ if (/spawn|enoent/.test(message)) return 'spawn_error';
47
+ return 'error';
48
+ }
49
+
50
+ function sanitizePayload(details = {}) {
51
+ const payload = {};
52
+ const source = cleanEnum(details.source, SURFACES, '');
53
+ const surface = cleanEnum(details.surface, SURFACES, '');
54
+ const action = cleanEnum(details.action, ACTIONS, '');
55
+ const result = cleanEnum(details.result, RESULTS, '');
56
+ const currentVersion = cleanVersion(details.currentVersion || details.current || details.current_version);
57
+ const latestVersion = cleanVersion(details.latestVersion || details.latest || details.latest_version);
58
+ const updateAvailable = cleanBool(details.updateAvailable);
59
+
60
+ if (source) payload.source = source;
61
+ if (surface) payload.surface = surface;
62
+ if (action) payload.action = action;
63
+ if (result) payload.result = result;
64
+ if (currentVersion) payload.current_version = currentVersion;
65
+ if (latestVersion) payload.latest_version = latestVersion;
66
+ if (updateAvailable !== undefined) payload.update_available = updateAvailable;
67
+ if (details.error || details.errorKind) payload.error_kind = details.errorKind || errorKind(details.error);
68
+
69
+ return payload;
70
+ }
71
+
72
+ function trackUpdateTelemetry(telemetry, event, details = {}) {
73
+ const normalized = cleanEnum(event, new Set([...SERVER_EVENTS, ...CLIENT_EVENTS]), '');
74
+ if (!normalized) return false;
75
+ try {
76
+ telemetry?.track?.(`ctm_update_${normalized}`, sanitizePayload(details));
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function clientUpdateTelemetryEvent(body = {}) {
84
+ const rawEvent = String(body.event || '').trim().toLowerCase();
85
+ if (rawEvent === 'prompt_shown') {
86
+ return {
87
+ event: 'ui_prompt_shown',
88
+ details: sanitizePayload({
89
+ surface: body.surface,
90
+ currentVersion: body.currentVersion,
91
+ latestVersion: body.latestVersion,
92
+ }),
93
+ };
94
+ }
95
+ if (rawEvent === 'action') {
96
+ return {
97
+ event: 'ui_action',
98
+ details: sanitizePayload({
99
+ surface: body.surface,
100
+ action: body.action,
101
+ currentVersion: body.currentVersion,
102
+ latestVersion: body.latestVersion,
103
+ }),
104
+ };
105
+ }
106
+ return null;
107
+ }
108
+
109
+ module.exports = {
110
+ clientUpdateTelemetryEvent,
111
+ errorKind,
112
+ sanitizePayload,
113
+ trackUpdateTelemetry,
114
+ };
@@ -12,7 +12,7 @@ function applyWalleToolEvent(toolCalls, event) {
12
12
  toolCalls.push({
13
13
  name: event.tool || event.name || 'tool',
14
14
  summary: event.summary || '',
15
- args: event.args || null,
15
+ args: event.args || event.input || null,
16
16
  status: 'working',
17
17
  output: '',
18
18
  });
@@ -29,6 +29,47 @@ function applyWalleToolEvent(toolCalls, event) {
29
29
  return toolCalls;
30
30
  }
31
31
 
32
+ function extractText(content) {
33
+ if (!content) return '';
34
+ if (typeof content === 'string') return content;
35
+ if (Array.isArray(content)) {
36
+ return content
37
+ .filter((block) => block && block.type === 'text' && block.text)
38
+ .map((block) => block.text)
39
+ .join('\n');
40
+ }
41
+ if (typeof content.text === 'string') return content.text;
42
+ return '';
43
+ }
44
+
45
+ function messageText(entry) {
46
+ if (!entry || typeof entry !== 'object') return '';
47
+ if (entry.message && entry.message.content !== undefined) return extractText(entry.message.content);
48
+ if (entry.content !== undefined) return extractText(entry.content);
49
+ return '';
50
+ }
51
+
52
+ function normalizeToolEvent(entry) {
53
+ if (!entry || typeof entry !== 'object') return null;
54
+ if (entry.type === 'tool_call' || entry.type === 'tool_result' || entry.type === 'tool_done') {
55
+ return entry;
56
+ }
57
+ if (entry.type !== 'walle_part') return null;
58
+ const data = entry.data && typeof entry.data === 'object' ? entry.data : {};
59
+ if (entry.partType !== 'tool_call' && entry.partType !== 'tool_result' && entry.partType !== 'tool_done') return null;
60
+ return {
61
+ type: entry.partType,
62
+ tool: data.tool || data.name || 'tool',
63
+ name: data.name || data.tool || 'tool',
64
+ summary: data.summary || '',
65
+ args: data.args || data.input || null,
66
+ input: data.input || data.args || null,
67
+ output: data.output || data.result || '',
68
+ result: data.result || data.output || '',
69
+ error: data.error || null,
70
+ };
71
+ }
72
+
32
73
  function readWalleCtmHistory(filePath) {
33
74
  let raw = '';
34
75
  try { raw = fs.readFileSync(filePath, 'utf8'); } catch { return []; }
@@ -41,20 +82,21 @@ function readWalleCtmHistory(filePath) {
41
82
  if (entry.type === 'user') {
42
83
  messages.push({
43
84
  role: 'user',
44
- content: entry.content || '',
85
+ content: messageText(entry),
45
86
  timestamp: entry.timestamp || 0,
46
87
  });
47
88
  continue;
48
89
  }
49
- if (entry.type === 'tool_call' || entry.type === 'tool_result' || entry.type === 'tool_done') {
50
- applyWalleToolEvent(pendingToolCalls, entry);
90
+ const toolEvent = normalizeToolEvent(entry);
91
+ if (toolEvent) {
92
+ applyWalleToolEvent(pendingToolCalls, toolEvent);
51
93
  continue;
52
94
  }
53
95
  if (entry.type === 'assistant') {
54
96
  messages.push({
55
97
  role: 'assistant',
56
- content: entry.content || '',
57
- model: entry.model || '',
98
+ content: messageText(entry),
99
+ model: entry.message?.model || entry.model || entry.modelId || '',
58
100
  latency_ms: entry.latencyMs || entry.latency_ms || 0,
59
101
  timestamp: entry.timestamp || 0,
60
102
  toolCalls: entry.toolCalls || cloneToolCalls(pendingToolCalls),
@@ -68,5 +110,6 @@ function readWalleCtmHistory(filePath) {
68
110
  module.exports = {
69
111
  applyWalleToolEvent,
70
112
  cloneToolCalls,
113
+ extractText,
71
114
  readWalleCtmHistory,
72
115
  };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const { sanitizeSetupProviderType } = require('./setup-provider-config');
4
+
5
+ function _kv(brain, key) {
6
+ try {
7
+ if (brain && typeof brain.getKv === 'function') return brain.getKv(key) || '';
8
+ if (brain && typeof brain.getDb === 'function') {
9
+ return brain.getDb().prepare('SELECT value FROM brain_metadata WHERE key = ?').get(key)?.value || '';
10
+ }
11
+ } catch (e) {
12
+ if (process.env.CTM_DEBUG_WALLE_DEFAULTS === '1') {
13
+ console.warn('[walle-default-model] metadata lookup failed:', e.message);
14
+ }
15
+ }
16
+ return '';
17
+ }
18
+
19
+ function _providerDefault(provider, providerRegistry) {
20
+ if (!provider) return '';
21
+ try {
22
+ const registry = providerRegistry || require('../../wall-e/llm/registry');
23
+ if (typeof registry.ensureBootstrapped === 'function') registry.ensureBootstrapped();
24
+ return registry.getDefaultModel(provider) || '';
25
+ } catch (e) {
26
+ if (process.env.CTM_DEBUG_WALLE_DEFAULTS === '1') {
27
+ console.warn('[walle-default-model] provider default lookup failed:', e.message);
28
+ }
29
+ return '';
30
+ }
31
+ }
32
+
33
+ function resolveWalleDefaultModelSelection(opts = {}) {
34
+ const env = opts.env || process.env;
35
+ const brain = opts.brain || null;
36
+ const provider = sanitizeSetupProviderType(
37
+ _kv(brain, 'walle_provider')
38
+ || env.WALLE_PROVIDER
39
+ || 'anthropic'
40
+ ) || 'anthropic';
41
+ const model = _kv(brain, 'walle_model_' + provider)
42
+ || _kv(brain, 'walle_model')
43
+ || env.WALLE_MODEL
44
+ || _providerDefault(provider, opts.providerRegistry)
45
+ || '';
46
+
47
+ return {
48
+ model_id: model,
49
+ model_provider: provider,
50
+ };
51
+ }
52
+
53
+ module.exports = {
54
+ resolveWalleDefaultModelSelection,
55
+ };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ let lastEnsureAt = 0;
6
+
7
+ function isMcpAutoConfigDisabled(env = process.env) {
8
+ const value = String(env.WALLE_MCP_AUTO_CONFIG || '').toLowerCase();
9
+ return value === '0' || value === 'false' || value === 'off';
10
+ }
11
+
12
+ function isMcpCapableAgentCommand(cmd) {
13
+ const base = path.basename(String(cmd || '')).toLowerCase();
14
+ return base === 'claude' || base === 'codex';
15
+ }
16
+
17
+ function loadMcpIntegration() {
18
+ return require(path.resolve(__dirname, '..', '..', 'wall-e', 'lib', 'mcp-integration'));
19
+ }
20
+
21
+ function ensureWalleMcpForAgentSession({
22
+ cmd,
23
+ wallePort,
24
+ homeDir,
25
+ env = process.env,
26
+ logger = console,
27
+ now = Date.now(),
28
+ minIntervalMs = 30000,
29
+ } = {}) {
30
+ if (!isMcpCapableAgentCommand(cmd)) return { skipped: 'not_mcp_capable_agent' };
31
+ if (isMcpAutoConfigDisabled(env)) return { skipped: 'disabled' };
32
+ if (lastEnsureAt && now - lastEnsureAt < minIntervalMs) return { skipped: 'throttled' };
33
+ lastEnsureAt = now;
34
+
35
+ try {
36
+ const { ensureMcpIntegrations } = loadMcpIntegration();
37
+ const results = ensureMcpIntegrations(wallePort, { homeDir: homeDir || env.HOME || process.env.HOME });
38
+ const changed = results.filter(r => r.kind === 'mcp_config' && (r.action === 'added' || r.action === 'updated'));
39
+ const instructionChanged = results.filter(r => r.kind === 'agent_instructions' && (r.action === 'added' || r.action === 'updated'));
40
+ const errors = results.filter(r => r.action === 'error');
41
+ if (changed.length) {
42
+ logger.log(`[ctm] Auto-configured Wall-E MCP for ${changed.length} AI tool(s) before agent launch`);
43
+ }
44
+ if (instructionChanged.length) {
45
+ logger.log(`[ctm] Updated Wall-E memory-routing instructions for ${instructionChanged.length} AI agent(s) before launch`);
46
+ }
47
+ if (errors.length) {
48
+ logger.warn(`[ctm] Wall-E MCP auto-config skipped ${errors.length} invalid config file(s): ${errors.map(r => r.tool).join(', ')}`);
49
+ }
50
+ return { results, changed: changed.length, instructionChanged: instructionChanged.length, errors: errors.length };
51
+ } catch (err) {
52
+ logger.warn(`[ctm] Wall-E MCP auto-config failed: ${err.message}`);
53
+ return { error: err.message };
54
+ }
55
+ }
56
+
57
+ function _resetWalleMcpAutoConfigState() {
58
+ lastEnsureAt = 0;
59
+ }
60
+
61
+ module.exports = {
62
+ ensureWalleMcpForAgentSession,
63
+ isMcpAutoConfigDisabled,
64
+ isMcpCapableAgentCommand,
65
+ _resetWalleMcpAutoConfigState,
66
+ };
@@ -66,6 +66,7 @@ const defaultProcessOps = {
66
66
  function createWalleSupervisor({
67
67
  ctmDir,
68
68
  configDir,
69
+ ctmPort = process.env.CTM_PORT || 3456,
69
70
  wallePort,
70
71
  instanceTag,
71
72
  walleToken = null,
@@ -124,6 +125,44 @@ function createWalleSupervisor({
124
125
  return pidSet;
125
126
  }
126
127
 
128
+ function readPidFile() {
129
+ try {
130
+ const pid = parseInt(fs.readFileSync(pidFile(), 'utf8').trim(), 10);
131
+ return pid || null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function isAlive(pid) {
138
+ if (!pid || pid === (processOps.pid || process.pid)) return false;
139
+ try {
140
+ processOps.kill(pid, 0);
141
+ return true;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ function writePidFile(pid) {
148
+ try {
149
+ fs.mkdirSync(configDir, { recursive: true });
150
+ fs.writeFileSync(pidFile(), String(pid));
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ function removePidFile() {
158
+ try {
159
+ fs.unlinkSync(pidFile());
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
127
166
  async function killAll() {
128
167
  const pidSet = await collectManagedPids();
129
168
  if (pidSet.size === 0) return waitForPortFree();
@@ -175,6 +214,8 @@ function createWalleSupervisor({
175
214
 
176
215
  const childEnv = {
177
216
  ...process.env,
217
+ CTM_PORT: String(ctmPort),
218
+ CTM_API_BASE_URL: `http://127.0.0.1:${ctmPort}`,
178
219
  WALL_E_PORT: String(wallePort),
179
220
  WALLE_INSTANCE_TAG: instanceTag,
180
221
  };
@@ -225,27 +266,53 @@ function createWalleSupervisor({
225
266
  return nextChild.pid;
226
267
  }
227
268
 
228
- function getStatus() {
229
- let running = false;
230
- let pid = null;
231
- if (childPid) {
232
- try {
233
- process.kill(childPid, 0);
234
- running = true;
235
- pid = childPid;
236
- } catch {}
269
+ async function getStatus() {
270
+ const savedPid = readPidFile();
271
+ let stalePid = null;
272
+
273
+ if (childPid && isAlive(childPid)) {
274
+ return {
275
+ running: true,
276
+ pid: childPid,
277
+ pidSource: 'child',
278
+ stalePid: savedPid && savedPid !== childPid ? savedPid : null,
279
+ repairedPidFile: savedPid !== childPid ? writePidFile(childPid) : false,
280
+ };
237
281
  }
238
- if (!running) {
239
- try {
240
- const orphanPid = parseInt(fs.readFileSync(pidFile(), 'utf8').trim(), 10);
241
- if (orphanPid && orphanPid !== process.pid) {
242
- process.kill(orphanPid, 0);
243
- running = true;
244
- pid = orphanPid;
245
- }
246
- } catch {}
282
+
283
+ if (savedPid && isAlive(savedPid)) {
284
+ return { running: true, pid: savedPid, pidSource: 'pid_file' };
247
285
  }
248
- return { running, pid };
286
+ if (savedPid) stalePid = savedPid;
287
+
288
+ const title = processTitle();
289
+ let discovered = [];
290
+ try {
291
+ const results = await Promise.all([
292
+ processOps.findPidsByExactCommand ? processOps.findPidsByExactCommand(title) : [],
293
+ processOps.findListeningPidsByExactCommand ? processOps.findListeningPidsByExactCommand(wallePort, title) : [],
294
+ ]);
295
+ discovered = [...new Set(results.flat().filter(pid => pid && isAlive(pid)))];
296
+ } catch {}
297
+
298
+ if (discovered.length > 0) {
299
+ const pid = discovered[0];
300
+ return {
301
+ running: true,
302
+ pid,
303
+ pidSource: 'discovered',
304
+ stalePid,
305
+ repairedPidFile: writePidFile(pid),
306
+ };
307
+ }
308
+
309
+ return {
310
+ running: false,
311
+ pid: null,
312
+ pidSource: stalePid ? 'stale_pid_file' : 'none',
313
+ stalePid,
314
+ removedStalePidFile: stalePid ? removePidFile() : false,
315
+ };
249
316
  }
250
317
 
251
318
  function apiStop(req, res) {
@@ -15,9 +15,7 @@ function defaultSessionsDir(env = process.env) {
15
15
  if (env.WALL_E_DATA_DIR && env.WALL_E_DATA_DIR !== path.join(os.homedir(), '.walle', 'data')) {
16
16
  return path.join(env.WALL_E_DATA_DIR, 'sessions');
17
17
  }
18
- if (env.CTM_DATA_DIR && env.CTM_DATA_DIR !== path.join(os.homedir(), '.walle', 'data')) {
19
- return path.join(env.CTM_DATA_DIR, 'sessions');
20
- }
18
+ // CTM_DATA_DIR belongs to the task manager; Wall-E transcripts stay in Wall-E-owned storage.
21
19
  return path.join(os.homedir(), '.walle', 'sessions');
22
20
  }
23
21
 
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+
7
+ function fail(error, status) {
8
+ return { ok: false, status: status || 400, error };
9
+ }
10
+
11
+ function gitText(cwd, args) {
12
+ return execFileSync('git', args, {
13
+ cwd,
14
+ encoding: 'utf8',
15
+ stdio: ['ignore', 'pipe', 'pipe'],
16
+ timeout: 5000,
17
+ }).trim();
18
+ }
19
+
20
+ function firstWorktreePath(porcelain) {
21
+ for (const line of String(porcelain || '').split('\n')) {
22
+ if (line.startsWith('worktree ')) return line.slice('worktree '.length).trim();
23
+ }
24
+ return '';
25
+ }
26
+
27
+ function realpathOrResolve(p) {
28
+ try {
29
+ return fs.realpathSync(p);
30
+ } catch (_) {
31
+ return path.resolve(p);
32
+ }
33
+ }
34
+
35
+ function resolveWorktreeRepoRoot(cwd) {
36
+ if (typeof cwd !== 'string' || !cwd.trim()) return fail('cwd is required');
37
+ const raw = cwd.trim();
38
+ if (raw.includes('~')) {
39
+ return fail('cwd must be an absolute path with no `~` (the literal-tilde is the source of ghost worktrees).');
40
+ }
41
+ if (!path.isAbsolute(raw)) return fail('cwd must be an absolute path');
42
+
43
+ let realCwd;
44
+ try {
45
+ realCwd = fs.realpathSync(raw);
46
+ } catch (_) {
47
+ return fail('cwd does not exist');
48
+ }
49
+
50
+ let stat;
51
+ try {
52
+ stat = fs.statSync(realCwd);
53
+ } catch (_) {
54
+ return fail('cwd does not exist');
55
+ }
56
+ if (!stat.isDirectory()) return fail('cwd must be a directory');
57
+
58
+ let topLevel;
59
+ try {
60
+ topLevel = gitText(realCwd, ['rev-parse', '--show-toplevel']);
61
+ } catch (_) {
62
+ return fail('cwd must be inside a Git work tree');
63
+ }
64
+ if (!topLevel) return fail('cwd must be inside a Git work tree');
65
+
66
+ let primaryRoot = '';
67
+ try {
68
+ primaryRoot = firstWorktreePath(gitText(realCwd, ['worktree', 'list', '--porcelain']));
69
+ } catch (_) {
70
+ primaryRoot = '';
71
+ }
72
+
73
+ return {
74
+ ok: true,
75
+ cwd: realpathOrResolve(primaryRoot || topLevel),
76
+ requestedCwd: realCwd,
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ resolveWorktreeRepoRoot,
82
+ };
@@ -22,6 +22,7 @@
22
22
  "dev": "node server.js --dev",
23
23
  "test": "node --test --test-force-exit \"tests/*.test.js\" \"tests/**/*.test.js\"",
24
24
  "test:render": "npx playwright test --config tests/rendering/playwright.config.js --grep-invert 'soak'",
25
+ "test:session-ux-smoke": "npx playwright test --config tests/rendering/playwright.config.js scenarios/blank-tab-retry-recovery.spec.js scenarios/blank-tab-worker-saturation.spec.js scenarios/codex-panel-restore-gap.spec.js scenarios/codex-clear-scrollback.spec.js scenarios/session-status-typing.spec.js scenarios/session-search-dedupe.spec.js scenarios/walle-session-default-model.spec.js --retries=0",
25
26
  "test:render:blank": "npx playwright test --config tests/rendering/playwright.config.js scenarios/blank-tab-real-click.spec.js --retries=0",
26
27
  "test:render:blank:native": "CTM_RENDER_NATIVE_GPU=1 npx playwright test --config tests/rendering/playwright.config.js scenarios/blank-tab-real-click.spec.js --headed --retries=0",
27
28
  "test:soak": "SOAK_DURATION_MINUTES=5 npx playwright test --config tests/rendering/playwright.config.js --grep 'soak'"
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ // Codex MCP permission form:
4
+ //
5
+ // Calling wall-e.walle_memory_status({})
6
+ // Field 1/1 ...
7
+ // Allow the wall-e MCP server to run tool "walle_memory_status"?
8
+ // 1. Allow
9
+ // 2. Allow for this session
10
+ // 3. Always allow
11
+ // 4. Cancel
12
+ // enter to submit | esc to cancel
13
+
14
+ const CALLING_RE = /\bCalling\s+([A-Za-z0-9_.-]+)\.([A-Za-z0-9_.-]+)\s*\(/i;
15
+ const QUESTION_RE = /Allow the ([A-Za-z0-9_.-]+) MCP server to run tool ["']?([A-Za-z0-9_.-]+)["']?\?/i;
16
+ const ANCHOR_RE = /enter to submit\s*\|\s*esc to cancel/i;
17
+ const ALLOW_RE = /^\s*[❯›> ]?\s*1\.\s*Allow\b/i;
18
+ const OPTION_RE = /^\s*[❯›> ]?\s*(\d+)\.\s*(Allow for this session|Always allow|Allow|Cancel)\b/i;
19
+
20
+ function extractMcpTool(lines) {
21
+ for (let i = lines.length - 1; i >= 0; i--) {
22
+ const q = lines[i].match(QUESTION_RE);
23
+ if (q) return { server: q[1], tool: q[2], questionIdx: i };
24
+ }
25
+ for (let i = lines.length - 1; i >= 0; i--) {
26
+ const c = lines[i].match(CALLING_RE);
27
+ if (c) return { server: c[1], tool: c[2], questionIdx: -1 };
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function parseOptions(lines) {
33
+ const out = {
34
+ allowShortcut: '1',
35
+ sessionShortcut: null,
36
+ alwaysShortcut: null,
37
+ };
38
+
39
+ for (const line of lines) {
40
+ const match = line.match(OPTION_RE);
41
+ if (!match) continue;
42
+ const index = match[1];
43
+ const label = match[2].toLowerCase();
44
+ if (label === 'allow for this session') out.sessionShortcut = index;
45
+ else if (label === 'always allow') out.alwaysShortcut = index;
46
+ else if (label === 'allow') out.allowShortcut = index;
47
+ }
48
+ return out;
49
+ }
50
+
51
+ module.exports = {
52
+ id: 'codex-mcp',
53
+ name: 'Codex MCP Permission',
54
+
55
+ detect(text) {
56
+ const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean);
57
+ if (lines.length < 5) return false;
58
+ if (!lines.slice(-5).some(l => ANCHOR_RE.test(l))) return false;
59
+ if (!lines.some(l => ALLOW_RE.test(l))) return false;
60
+ return lines.some(l => QUESTION_RE.test(l)) || lines.some(l => CALLING_RE.test(l));
61
+ },
62
+
63
+ parse(text) {
64
+ const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean);
65
+ const mcp = extractMcpTool(lines);
66
+ if (!mcp) return null;
67
+
68
+ let allowIdx = -1;
69
+ for (let i = 0; i < lines.length; i++) {
70
+ if (ALLOW_RE.test(lines[i])) { allowIdx = i; break; }
71
+ }
72
+ if (allowIdx < 0) return null;
73
+
74
+ const options = parseOptions(lines.slice(allowIdx, allowIdx + 8));
75
+ const ctxAnchor = mcp.questionIdx >= 0 ? mcp.questionIdx : allowIdx;
76
+ const ctxStart = Math.max(0, ctxAnchor - 5);
77
+ const ctxEnd = Math.min(lines.length, allowIdx + 8);
78
+ const command = `${mcp.server}.${mcp.tool}({})`;
79
+
80
+ return {
81
+ toolName: 'MCP',
82
+ command: command.slice(0, 2000),
83
+ warning: '',
84
+ fullContext: lines.slice(ctxStart, ctxEnd).join('\n').slice(0, 2000),
85
+ hasAllowAll: !!options.sessionShortcut,
86
+ approveShortcut: options.allowShortcut,
87
+ approveAllShortcut: options.sessionShortcut,
88
+ alwaysAllowShortcut: options.alwaysShortcut,
89
+ mcpServer: mcp.server,
90
+ mcpTool: mcp.tool,
91
+ providerId: 'codex-mcp',
92
+ };
93
+ },
94
+
95
+ approveKeystroke(ctx) {
96
+ return ctx.approveAllShortcut || ctx.approveShortcut || '1';
97
+ },
98
+
99
+ approveAllKeystroke(ctx) {
100
+ return ctx.approveAllShortcut || null;
101
+ },
102
+
103
+ requiresEnter: true,
104
+ };
@@ -4,6 +4,7 @@
4
4
  // Detection order: most distinctive signals first to minimize false positives.
5
5
 
6
6
  const claudeCode = require('./claude-code');
7
+ const codexMcp = require('./codex-mcp');
7
8
  const codex = require('./codex');
8
9
  const gemini = require('./gemini');
9
10
  const amazonQ = require('./amazon-q');
@@ -19,6 +20,7 @@ const opencode = require('./opencode');
19
20
  // Numbered-widget providers (Claude Code, Codex, Cursor, OpenCode, Kimi)
20
21
  // run before inline [Y/n] providers (Gemini, Aider, Amazon Q, Cline, Copilot).
21
22
  const providers = [
23
+ codexMcp,
22
24
  claudeCode,
23
25
  codex,
24
26
  cursor,