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
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const crypto = require('crypto');
2
3
  const fsp = require('fs/promises');
3
4
  const EventEmitter = require('events');
4
5
  const { looksLikeCompactContinuation } = require('./compact-stitch');
@@ -312,11 +313,16 @@ class SessionStream extends EventEmitter {
312
313
  }
313
314
 
314
315
  if (!displayPromptEntry) displayPromptEntry = lastPromptEntry;
316
+ const promptSnapshot = this._promptCacheSnapshot(st);
317
+ const latestPrompt = this._buildPromptPayload(lastPromptEntry);
318
+ const displayPrompt = this._buildPromptPayload(displayPromptEntry);
315
319
  const summaryRecord = this._usableSummary(st.cachedSummary);
316
- const summary = summaryRecord?.text || (st.cachedSummary?.model === 'fallback' ? st.cachedSummary.text : null);
317
- const intent = this._buildIntent(summaryRecord, displayPromptEntry, lastPromptEntry);
320
+ const aiSummary = this._buildAiSummary(st.cachedSummary, summaryRecord, promptSnapshot, sessionId);
321
+ const currentTask = this._buildCurrentTask(aiSummary, latestPrompt, displayPrompt);
322
+ const summary = aiSummary.text || (st.cachedSummary?.model === 'fallback' ? st.cachedSummary.text : null);
323
+ const intent = this._buildIntent(currentTask, displayPromptEntry, lastPromptEntry);
318
324
  const progress = this._buildProgress(st, intent);
319
- if (!summaryRecord && st.cachedSummary?.model === 'fallback') {
325
+ if (aiSummary.status === 'fallback' || aiSummary.status === 'stale' || aiSummary.status === 'rejected') {
320
326
  this._maybeRetryFallbackSummary(sessionId, st);
321
327
  }
322
328
 
@@ -328,6 +334,9 @@ class SessionStream extends EventEmitter {
328
334
  summarySource: summaryRecord?.model || null,
329
335
  lastPrompt: this._promptText(lastPromptEntry),
330
336
  displayPrompt: this._promptText(displayPromptEntry),
337
+ latestPrompt,
338
+ aiSummary,
339
+ currentTask,
331
340
  intent,
332
341
  progress,
333
342
  turnCount: mergedTurns.length,
@@ -658,6 +667,29 @@ class SessionStream extends EventEmitter {
658
667
  }
659
668
 
660
669
  _processCodexEntry(agentSessionId, st, entry) {
670
+ if (entry?.type === 'event_msg' && entry.payload?.type === 'task_complete' && entry.payload.last_agent_message) {
671
+ const timestamp = eventTimestampMs(entry);
672
+ const rawText = String(entry.payload.last_agent_message || '').trim();
673
+ const text = rawText.length > MAX_TEXT_LEN ? rawText.slice(0, MAX_TEXT_LEN) + `...truncated (${rawText.length} chars)` : rawText;
674
+ const evt = decorateStreamEvent({
675
+ type: 'turn_complete',
676
+ sessionId: agentSessionId,
677
+ ctmSessionId: st.ctmSessionId,
678
+ timestamp,
679
+ seq: st.seq++,
680
+ data: {
681
+ text,
682
+ turnId: entry.payload.turn_id || '',
683
+ durationMs: entry.payload.duration_ms || 0,
684
+ },
685
+ }, 'codex-jsonl');
686
+ // Do not append this lifecycle event to the ring buffer. The latest
687
+ // assistant message should remain the status/summary anchor; this event
688
+ // exists for Codex terminal recovery subscribers.
689
+ this._emitEvent(agentSessionId, evt);
690
+ return true;
691
+ }
692
+
661
693
  const msg = codexMessageFromEntry(entry);
662
694
  if (!msg) return false;
663
695
 
@@ -755,6 +787,42 @@ class SessionStream extends EventEmitter {
755
787
  return Number.isFinite(entry.timestamp) ? entry.timestamp : 0;
756
788
  }
757
789
 
790
+ _buildPromptPayload(entry) {
791
+ const text = this._promptText(entry);
792
+ const timestamp = this._promptTimestamp(entry);
793
+ return {
794
+ text: text || null,
795
+ timestamp,
796
+ isContentRich: this._isContentRich(text),
797
+ };
798
+ }
799
+
800
+ _promptCacheSnapshot(st) {
801
+ const entries = (st?.userPromptCache || [])
802
+ .filter(entry => this._promptText(entry))
803
+ .map(entry => ({
804
+ text: this._promptText(entry),
805
+ timestamp: this._promptTimestamp(entry),
806
+ }));
807
+ const prompts = entries.map(entry => entry.text);
808
+ const latestEntry = entries[entries.length - 1] || null;
809
+ return {
810
+ entries,
811
+ prompts,
812
+ latestEntry,
813
+ latestText: latestEntry?.text || '',
814
+ latestTimestamp: latestEntry?.timestamp || 0,
815
+ promptCount: prompts.length,
816
+ promptHash: this._promptCacheHash(prompts),
817
+ };
818
+ }
819
+
820
+ _promptCacheHash(prompts) {
821
+ const list = Array.isArray(prompts) ? prompts.filter(Boolean) : [];
822
+ if (list.length === 0) return '';
823
+ return crypto.createHash('sha1').update(list.join('\u0000')).digest('hex');
824
+ }
825
+
758
826
  _computeStatus(st) {
759
827
  const now = Date.now();
760
828
  // PTY activity within 5s = running
@@ -904,18 +972,150 @@ class SessionStream extends EventEmitter {
904
972
  return false;
905
973
  }
906
974
 
907
- _buildIntent(summaryRecord, displayPromptEntry, lastPromptEntry) {
908
- const promptEntry = displayPromptEntry || lastPromptEntry;
909
- const promptText = this._promptText(promptEntry);
975
+ _buildAiSummary(cachedSummary, summaryRecord, promptSnapshot, agentSessionId) {
976
+ const timerPending = this._summaryTimers.has(agentSessionId);
977
+ const latestTimestamp = promptSnapshot?.latestTimestamp || 0;
978
+ const currentHash = promptSnapshot?.promptHash || '';
979
+ const currentCount = promptSnapshot?.promptCount || 0;
980
+
910
981
  if (summaryRecord?.text) {
982
+ const coveredTimestamp = Number.isFinite(summaryRecord.promptTimestamp)
983
+ ? summaryRecord.promptTimestamp
984
+ : (summaryRecord.timestamp || 0);
985
+ const hasHash = !!summaryRecord.promptHash;
986
+ const fresh = hasHash
987
+ ? summaryRecord.promptHash === currentHash
988
+ : (!latestTimestamp || (summaryRecord.timestamp || 0) >= latestTimestamp);
989
+ const status = fresh ? 'fresh' : (timerPending ? 'updating' : 'stale');
911
990
  return {
912
991
  text: summaryRecord.text,
913
992
  source: 'ai-summary',
914
- confidence: 'high',
915
- prompt: promptText || null,
916
- fullPrompt: promptText || null,
917
- timestamp: this._promptTimestamp(promptEntry),
993
+ status,
994
+ freshness: status,
995
+ model: summaryRecord.model || null,
918
996
  updatedAt: summaryRecord.timestamp || 0,
997
+ promptTimestamp: coveredTimestamp || 0,
998
+ promptCount: summaryRecord.promptCount || summaryRecord.turnCount || 0,
999
+ currentPromptCount: currentCount,
1000
+ promptHash: summaryRecord.promptHash || null,
1001
+ };
1002
+ }
1003
+
1004
+ if (cachedSummary?.model === 'fallback') {
1005
+ return {
1006
+ text: this._sanitizeSummary(cachedSummary.text || '') || null,
1007
+ source: 'fallback',
1008
+ status: timerPending ? 'updating' : 'fallback',
1009
+ freshness: timerPending ? 'updating' : 'fallback',
1010
+ model: 'fallback',
1011
+ updatedAt: cachedSummary.timestamp || 0,
1012
+ promptTimestamp: cachedSummary.promptTimestamp || 0,
1013
+ promptCount: cachedSummary.promptCount || cachedSummary.turnCount || 0,
1014
+ currentPromptCount: currentCount,
1015
+ promptHash: cachedSummary.promptHash || null,
1016
+ };
1017
+ }
1018
+
1019
+ if (cachedSummary && cachedSummary.model) {
1020
+ return {
1021
+ text: null,
1022
+ source: 'ai-summary',
1023
+ status: 'rejected',
1024
+ freshness: 'rejected',
1025
+ model: cachedSummary.model || null,
1026
+ updatedAt: cachedSummary.timestamp || 0,
1027
+ promptTimestamp: cachedSummary.promptTimestamp || 0,
1028
+ promptCount: cachedSummary.promptCount || cachedSummary.turnCount || 0,
1029
+ currentPromptCount: currentCount,
1030
+ promptHash: cachedSummary.promptHash || null,
1031
+ };
1032
+ }
1033
+
1034
+ return {
1035
+ text: null,
1036
+ source: 'none',
1037
+ status: timerPending ? 'updating' : 'missing',
1038
+ freshness: timerPending ? 'updating' : 'missing',
1039
+ model: null,
1040
+ updatedAt: 0,
1041
+ promptTimestamp: 0,
1042
+ promptCount: 0,
1043
+ currentPromptCount: currentCount,
1044
+ promptHash: null,
1045
+ };
1046
+ }
1047
+
1048
+ _buildCurrentTask(aiSummary, latestPrompt, displayPrompt) {
1049
+ const freshAi = aiSummary?.text && aiSummary.status === 'fresh';
1050
+ const prompt = (latestPrompt?.isContentRich || !displayPrompt?.text)
1051
+ ? latestPrompt
1052
+ : displayPrompt;
1053
+ const promptText = prompt?.text || null;
1054
+ const promptTimestamp = prompt?.timestamp || 0;
1055
+
1056
+ if (freshAi) {
1057
+ return {
1058
+ text: aiSummary.text,
1059
+ source: 'ai-summary',
1060
+ confidence: 'high',
1061
+ freshness: 'fresh',
1062
+ prompt: promptText,
1063
+ fullPrompt: promptText,
1064
+ timestamp: promptTimestamp,
1065
+ promptTimestamp,
1066
+ updatedAt: aiSummary.updatedAt || 0,
1067
+ aiUpdatedAt: aiSummary.updatedAt || 0,
1068
+ };
1069
+ }
1070
+
1071
+ if (promptText) {
1072
+ const staleAiPresent = aiSummary?.source === 'ai-summary' && aiSummary?.text && aiSummary.status !== 'fresh';
1073
+ const source = staleAiPresent && prompt === latestPrompt ? 'latest-prompt' : 'prompt-fallback';
1074
+ const freshness = staleAiPresent
1075
+ ? (aiSummary.status === 'updating' ? 'updating' : 'stale')
1076
+ : (aiSummary?.status === 'updating' ? 'updating' : 'fallback');
1077
+ return {
1078
+ text: promptText,
1079
+ source,
1080
+ confidence: this._isContentRich(promptText) ? 'medium' : 'low',
1081
+ freshness,
1082
+ prompt: promptText,
1083
+ fullPrompt: promptText,
1084
+ timestamp: promptTimestamp,
1085
+ promptTimestamp,
1086
+ updatedAt: promptTimestamp,
1087
+ aiUpdatedAt: aiSummary?.updatedAt || 0,
1088
+ };
1089
+ }
1090
+
1091
+ return {
1092
+ text: null,
1093
+ source: 'missing',
1094
+ confidence: 'low',
1095
+ freshness: aiSummary?.status || 'missing',
1096
+ prompt: null,
1097
+ fullPrompt: null,
1098
+ timestamp: 0,
1099
+ promptTimestamp: 0,
1100
+ updatedAt: 0,
1101
+ aiUpdatedAt: aiSummary?.updatedAt || 0,
1102
+ };
1103
+ }
1104
+
1105
+ _buildIntent(currentTask, displayPromptEntry, lastPromptEntry) {
1106
+ const promptEntry = displayPromptEntry || lastPromptEntry;
1107
+ const promptText = this._promptText(promptEntry);
1108
+ if (currentTask?.text) {
1109
+ return {
1110
+ text: currentTask.text,
1111
+ source: currentTask.source || 'missing',
1112
+ confidence: currentTask.confidence || 'low',
1113
+ freshness: currentTask.freshness || 'missing',
1114
+ prompt: currentTask.prompt || promptText || null,
1115
+ fullPrompt: currentTask.fullPrompt || promptText || null,
1116
+ timestamp: currentTask.timestamp || this._promptTimestamp(promptEntry),
1117
+ updatedAt: currentTask.updatedAt || currentTask.timestamp || 0,
1118
+ aiUpdatedAt: currentTask.aiUpdatedAt || 0,
919
1119
  };
920
1120
  }
921
1121
  if (promptText) {
@@ -923,20 +1123,24 @@ class SessionStream extends EventEmitter {
923
1123
  text: promptText,
924
1124
  source: 'prompt-fallback',
925
1125
  confidence: this._isContentRich(promptText) ? 'medium' : 'low',
1126
+ freshness: 'fallback',
926
1127
  prompt: promptText,
927
1128
  fullPrompt: promptText,
928
1129
  timestamp: this._promptTimestamp(promptEntry),
929
1130
  updatedAt: this._promptTimestamp(promptEntry),
1131
+ aiUpdatedAt: 0,
930
1132
  };
931
1133
  }
932
1134
  return {
933
1135
  text: null,
934
1136
  source: 'missing',
935
1137
  confidence: 'low',
1138
+ freshness: 'missing',
936
1139
  prompt: null,
937
1140
  fullPrompt: null,
938
1141
  timestamp: 0,
939
1142
  updatedAt: 0,
1143
+ aiUpdatedAt: 0,
940
1144
  };
941
1145
  }
942
1146
 
@@ -1056,12 +1260,9 @@ class SessionStream extends EventEmitter {
1056
1260
 
1057
1261
  async _generateSummary(agentSessionId, st) {
1058
1262
  st.lastSummaryAttempt = Date.now();
1059
- // Use userPromptCache (survives ring buffer churn from tool_result-only events)
1060
- const userPrompts = st.userPromptCache.map((entry) => this._promptText(entry)).filter(Boolean);
1061
- if (userPrompts.length === 0) return;
1062
-
1063
- const turnsText = userPrompts.map((t, i) => `Prompt ${i + 1}: ${t}`).join('\n');
1064
- const sysPrompt = 'Summarize what the user is working on based on their prompts. 10-15 words. Return ONLY the summary, no quotes or prefix.';
1263
+ const summaryInput = this._buildSummaryInput(st);
1264
+ if (!summaryInput) return;
1265
+ const { turnsText, sysPrompt, promptSnapshot } = summaryInput;
1065
1266
 
1066
1267
  // Tier 0: configured Wall-E provider (DeepSeek/OpenAI/Claude/Gemini/etc.).
1067
1268
  let summaryText = '';
@@ -1085,7 +1286,10 @@ class SessionStream extends EventEmitter {
1085
1286
  if (summaryText) {
1086
1287
  st.cachedSummary = {
1087
1288
  text: truncateText(summaryText, SUMMARY_TEXT_LIMIT),
1088
- turnCount: userPrompts.length,
1289
+ turnCount: promptSnapshot.promptCount,
1290
+ promptCount: promptSnapshot.promptCount,
1291
+ promptHash: promptSnapshot.promptHash,
1292
+ promptTimestamp: promptSnapshot.latestTimestamp || 0,
1089
1293
  model: usedModel,
1090
1294
  timestamp: Date.now(),
1091
1295
  };
@@ -1102,10 +1306,31 @@ class SessionStream extends EventEmitter {
1102
1306
  this._emitEvent(agentSessionId, summaryEvt);
1103
1307
  } else {
1104
1308
  // Tier 3: heuristic fallback — picks the most recent content-rich prompt
1105
- st.cachedSummary = this._fallbackSummary(userPrompts);
1309
+ st.cachedSummary = this._fallbackSummary(promptSnapshot.prompts, promptSnapshot);
1106
1310
  }
1107
1311
  }
1108
1312
 
1313
+ _buildSummaryInput(st) {
1314
+ const promptSnapshot = this._promptCacheSnapshot(st);
1315
+ if (promptSnapshot.prompts.length === 0) return null;
1316
+
1317
+ const latest = promptSnapshot.latestText;
1318
+ const recentContext = promptSnapshot.prompts.slice(0, -1).slice(-3);
1319
+ const parts = ['Latest prompt:', latest];
1320
+ if (recentContext.length) {
1321
+ parts.push('', 'Recent context:');
1322
+ recentContext.forEach((text, idx) => {
1323
+ parts.push(`${idx + 1}. ${text}`);
1324
+ });
1325
+ }
1326
+
1327
+ return {
1328
+ turnsText: parts.join('\n'),
1329
+ sysPrompt: "Summarize the user's current task. Prioritize the latest prompt. Use older prompts only as context. Return 8-14 words. Return only the summary, no quotes or prefix.",
1330
+ promptSnapshot,
1331
+ };
1332
+ }
1333
+
1109
1334
  /** Tier 0: CTM-injected Wall-E provider. Returns { text, model } or null. */
1110
1335
  async _tryConfiguredSummary(turnsText, sysPrompt) {
1111
1336
  if (!this._summaryProvider) return null;
@@ -1205,12 +1430,20 @@ class SessionStream extends EventEmitter {
1205
1430
  } catch { return null; }
1206
1431
  }
1207
1432
 
1208
- _fallbackSummary(userPrompts) {
1433
+ _fallbackSummary(userPrompts, promptSnapshot = null) {
1209
1434
  // Prefer a content-rich prompt over the latest one — "/cmd yes go ahead"
1210
1435
  // tells the user nothing about what the session is doing.
1211
1436
  const picked = this._pickContentRichPrompt(userPrompts) || userPrompts[userPrompts.length - 1] || '';
1212
1437
  const text = this._sanitizeSummary(picked.split('\n')[0].slice(0, 80));
1213
- return { text, turnCount: userPrompts.length, model: 'fallback', timestamp: Date.now() };
1438
+ return {
1439
+ text,
1440
+ turnCount: userPrompts.length,
1441
+ promptCount: promptSnapshot?.promptCount || userPrompts.length,
1442
+ promptHash: promptSnapshot?.promptHash || this._promptCacheHash(userPrompts),
1443
+ promptTimestamp: promptSnapshot?.latestTimestamp || 0,
1444
+ model: 'fallback',
1445
+ timestamp: Date.now(),
1446
+ };
1214
1447
  }
1215
1448
 
1216
1449
  /**
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const STICKY_ATTENTION_MS = 12 * 60 * 60 * 1000;
6
+
7
+ function compactText(value) {
8
+ if (!value) return '';
9
+ if (typeof value === 'string') return value.replace(/\s+/g, ' ').trim();
10
+ if (typeof value === 'object') {
11
+ return compactText(value.text || value.summary || value.phase || value.next || value.reason || '');
12
+ }
13
+ return String(value).replace(/\s+/g, ' ').trim();
14
+ }
15
+
16
+ function truncateText(value, max = 700) {
17
+ const text = compactText(value);
18
+ if (text.length <= max) return text;
19
+ return `${text.slice(0, Math.max(0, max - 1)).trim()}...`;
20
+ }
21
+
22
+ function normalizeSeverity(value) {
23
+ const text = String(value || '').toLowerCase().trim();
24
+ if (text === 'failure' || text === 'fail' || text === 'blocked' || text === 'blocker') return 'failure';
25
+ if (text === 'warning' || text === 'warn' || text === 'caution' || text === 'risk') return 'warning';
26
+ return 'none';
27
+ }
28
+
29
+ function normalizeConfidence(value) {
30
+ const text = String(value || '').toLowerCase().trim();
31
+ if (text === 'high' || text === 'medium' || text === 'low') return text;
32
+ return 'medium';
33
+ }
34
+
35
+ function normalizeAttention(value, defaults = {}) {
36
+ if (!value || typeof value !== 'object') return null;
37
+ const severity = normalizeSeverity(value.severity || value.level || value.kind);
38
+ const recommendation = truncateText(
39
+ value.recommendation || value.reason || value.summary || defaults.recommendation || '',
40
+ 220
41
+ );
42
+ const evidence = Array.isArray(value.evidence)
43
+ ? value.evidence.map(item => truncateText(item, 100)).filter(Boolean).slice(0, 3)
44
+ : [];
45
+ return {
46
+ severity,
47
+ actionLabel: severity === 'failure' ? 'Inspect' : severity === 'warning' ? 'Review' : '',
48
+ recommendation: recommendation || defaultRecommendation(severity),
49
+ confidence: normalizeConfidence(value.confidence || defaults.confidence),
50
+ evidence,
51
+ source: value.source || defaults.source || 'unknown',
52
+ model: value.model || defaults.model || '',
53
+ updatedAt: value.updatedAt || defaults.updatedAt || null,
54
+ sticky: !!value.sticky,
55
+ };
56
+ }
57
+
58
+ function defaultRecommendation(severity) {
59
+ if (severity === 'failure') return 'A current blocker or failure appears to need attention.';
60
+ if (severity === 'warning') return 'A current warning appears to need attention.';
61
+ return '';
62
+ }
63
+
64
+ function attentionContextHash(text) {
65
+ return crypto.createHash('sha1').update(String(text || '')).digest('hex');
66
+ }
67
+
68
+ function buildStandupAttentionContext({ session = {}, summary = {}, status = {} } = {}) {
69
+ const evidence = Array.isArray(summary.statusEvidence) ? summary.statusEvidence : [];
70
+ const progress = summary.progress || {};
71
+ const lines = [
72
+ `Title: ${compactText(session.label || summary.displayPrompt || summary.lastPrompt || session.id || '')}`,
73
+ `Agent: ${compactText(session.agentType || session.agent || session.type || '')}`,
74
+ `Status: ${compactText(session.standupStatus || session.liveStatus || session.serverState || status.status || summary.status || '')}`,
75
+ `Waiting: ${compactText(session.waitingReason || status.reason || '')}`,
76
+ `Intent: ${compactText(summary.intent || summary.displayPrompt || summary.lastPrompt || '')}`,
77
+ `Summary: ${compactText(summary.summary || '')}`,
78
+ `Progress: ${compactText(progress)}`,
79
+ `Evidence: ${evidence.map(compactText).filter(Boolean).join('; ')}`,
80
+ ].filter(line => !/:\s*$/.test(line));
81
+ const text = truncateText(lines.join('\n'), 2500);
82
+ return {
83
+ text,
84
+ hash: attentionContextHash(text),
85
+ hasAttentionLanguage: hasAttentionLanguage(text),
86
+ };
87
+ }
88
+
89
+ function hasAttentionLanguage(text) {
90
+ return /\b(warn(?:ing|ed)?|caution|risk|blocked|blocker|failed|failing|failure|error|exception|permission denied|cannot proceed|stuck|needs attention)\b/i.test(String(text || ''));
91
+ }
92
+
93
+ function hasResolutionSignal(text) {
94
+ return /\b(resolved|fixed|cleared|no longer|not blocked|not a blocker|no blockers?|not a failure|no failures?|not an error|no errors?|no warnings?|unblocked|passed|verified|working now|succeeded|successful|recovered)\b/i.test(String(text || ''));
95
+ }
96
+
97
+ function heuristicStandupAttention(context) {
98
+ const text = String(context?.text || context || '');
99
+ const lower = text.toLowerCase();
100
+ const resolved = hasResolutionSignal(lower);
101
+ const hardFailure = /\b(cannot proceed|blocked|blocker|stuck|permission denied|fatal|panic|segmentation fault|uncaught|unhandled exception|traceback|command failed|build failed|tests? failed|npm err!)\b/i.test(lower);
102
+ const warning = /\b(warn(?:ing|ed)?|caution|risk|heads up|possible issue|needs attention)\b/i.test(lower);
103
+ const weakFailure = /\b(failed|failing|failure|error|exception)\b/i.test(lower);
104
+
105
+ if (resolved && !warning && !hardFailure) {
106
+ return normalizeAttention({ severity: 'none', source: 'heuristic', confidence: 'medium' });
107
+ }
108
+ if (hardFailure && !resolved) {
109
+ return normalizeAttention({
110
+ severity: 'failure',
111
+ recommendation: 'A current blocker or failure appears to need attention.',
112
+ evidence: [firstMatchingPhrase(text, /(cannot proceed|blocked|blocker|stuck|permission denied|fatal|unhandled exception|command failed|build failed|tests? failed)/i)],
113
+ source: 'heuristic',
114
+ confidence: 'medium',
115
+ });
116
+ }
117
+ if (warning) {
118
+ return normalizeAttention({
119
+ severity: 'warning',
120
+ recommendation: 'A current warning appears to need attention.',
121
+ evidence: [firstMatchingPhrase(text, /(warn(?:ing|ed)?|caution|risk|possible issue|needs attention)/i)],
122
+ source: 'heuristic',
123
+ confidence: 'medium',
124
+ });
125
+ }
126
+ if (weakFailure && !resolved) {
127
+ return normalizeAttention({
128
+ severity: 'warning',
129
+ recommendation: 'Failure or error language appears in the latest context; review whether it is still relevant.',
130
+ evidence: [firstMatchingPhrase(text, /(failed|failing|failure|error|exception)/i)],
131
+ source: 'heuristic',
132
+ confidence: 'low',
133
+ });
134
+ }
135
+ return normalizeAttention({ severity: 'none', source: 'heuristic', confidence: 'medium' });
136
+ }
137
+
138
+ function firstMatchingPhrase(text, regex) {
139
+ const match = String(text || '').match(regex);
140
+ return match ? match[0] : '';
141
+ }
142
+
143
+ function parseStandupAttentionResult(text, defaults = {}) {
144
+ const raw = String(text || '').trim();
145
+ if (!raw) return null;
146
+ const cleaned = raw
147
+ .replace(/^```(?:json)?\s*/i, '')
148
+ .replace(/\s*```$/i, '')
149
+ .trim();
150
+ const start = cleaned.indexOf('{');
151
+ const end = cleaned.lastIndexOf('}');
152
+ if (start === -1 || end === -1 || end <= start) return null;
153
+ try {
154
+ return normalizeAttention(JSON.parse(cleaned.slice(start, end + 1)), defaults);
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ function mergeStickyStandupAttention(previous, next, contextText, now = Date.now(), opts = {}) {
161
+ const stickyMs = Number.isFinite(opts.stickyMs) ? opts.stickyMs : STICKY_ATTENTION_MS;
162
+ const normalizedNext = normalizeAttention(next, { updatedAt: new Date(now).toISOString() })
163
+ || normalizeAttention({ severity: 'none', source: 'none' }, { updatedAt: new Date(now).toISOString() });
164
+ const normalizedPrevious = normalizeAttention(previous);
165
+
166
+ if (normalizedNext.severity === 'failure' || normalizedNext.severity === 'warning') {
167
+ return {
168
+ ...normalizedNext,
169
+ updatedAt: normalizedNext.updatedAt || new Date(now).toISOString(),
170
+ sticky: false,
171
+ };
172
+ }
173
+
174
+ if (!normalizedPrevious || !['failure', 'warning'].includes(normalizedPrevious.severity)) {
175
+ return normalizedNext;
176
+ }
177
+
178
+ if (hasResolutionSignal(contextText)) return normalizedNext;
179
+
180
+ const previousMs = Date.parse(normalizedPrevious.updatedAt || '') || now;
181
+ if (now - previousMs > stickyMs) return normalizedNext;
182
+
183
+ return {
184
+ ...normalizedPrevious,
185
+ updatedAt: normalizedPrevious.updatedAt || new Date(now).toISOString(),
186
+ sticky: true,
187
+ };
188
+ }
189
+
190
+ module.exports = {
191
+ STICKY_ATTENTION_MS,
192
+ buildStandupAttentionContext,
193
+ hasAttentionLanguage,
194
+ hasResolutionSignal,
195
+ heuristicStandupAttention,
196
+ mergeStickyStandupAttention,
197
+ normalizeAttention,
198
+ normalizeSeverity,
199
+ parseStandupAttentionResult,
200
+ };
@@ -12,12 +12,18 @@
12
12
  const { SessionStateBus } = require('./session-state-bus');
13
13
  const { runHook, buildHookEnv } = require('./hook-executor');
14
14
  const { resolveHooks, commandForState } = require('./status-hooks-config');
15
+ const { listStateDetectors } = require('../workers/state-detectors');
15
16
 
16
17
  const _sessionMeta = new Map(); // sessionId -> { providerId, cwd, branch, tabTitle }
17
18
 
18
19
  const bus = new SessionStateBus();
19
- // Default per the user spec: 1500ms for Claude Code (gap #4).
20
- bus.setProviderDebounce('claude-code', 1500);
20
+ // Default per the user spec: 1500ms for Claude Code (gap #4). Provider
21
+ // detectors can widen this for bursty TUIs such as Codex.
22
+ for (const detector of listStateDetectors()) {
23
+ if (detector && Number.isFinite(detector.idleDebounceMs)) {
24
+ bus.setProviderDebounce(detector.providerId, detector.idleDebounceMs);
25
+ }
26
+ }
21
27
  bus.setDefaultDebounce(1500);
22
28
 
23
29
  let _dbModule = null;