create-walle 0.9.13 → 0.9.14
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.
- package/README.md +6 -1
- package/bin/create-walle.js +195 -30
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/git-utils.js +111 -3
- package/template/claude-task-manager/lib/session-history.js +144 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +5 -0
- package/template/claude-task-manager/public/index.html +1596 -283
- package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +55 -6
- package/template/claude-task-manager/public/js/walle-session.js +73 -16
- package/template/claude-task-manager/public/js/walle.js +34 -2
- package/template/claude-task-manager/server.js +780 -177
- package/template/claude-task-manager/session-integrity.js +58 -15
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +36 -7
- package/template/wall-e/api-walle.js +72 -20
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding-orchestrator.js +26 -6
- package/template/wall-e/eval/agent-runner.js +16 -4
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/lib/mcp-integration.js +220 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/mcp-server.js +272 -10
- package/template/wall-e/memory/ctm-session-context.js +910 -0
- package/template/wall-e/server.js +26 -1
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/skill-planner.js +52 -3
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +2 -2
- package/template/builder-journal.md +0 -17
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { normalizeAttention } = require('./standup-attention');
|
|
4
|
+
|
|
5
|
+
const LANE_DEFS = [
|
|
6
|
+
{
|
|
7
|
+
id: 'needs_user',
|
|
8
|
+
title: 'Needs User',
|
|
9
|
+
description: 'Approvals, questions, blockers, or failed work that need operator attention.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'ready_review',
|
|
13
|
+
title: 'Ready Review',
|
|
14
|
+
description: 'Sessions with reviewable work, finished changes, or worktree cleanup to handle.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'running',
|
|
18
|
+
title: 'Running',
|
|
19
|
+
description: 'Active sessions that are still producing work.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'continue_later',
|
|
23
|
+
title: 'Continue Later',
|
|
24
|
+
description: 'Idle sessions worth preserving for context reuse.',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const LANE_ORDER = new Map(LANE_DEFS.map((lane, index) => [lane.id, index]));
|
|
29
|
+
const STALE_AFTER_MS = 30 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
function truncateText(value, max = 180) {
|
|
32
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
33
|
+
if (!text) return '';
|
|
34
|
+
if (text.length <= max) return text;
|
|
35
|
+
return `${text.slice(0, Math.max(0, max - 1)).trim()}...`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function valueText(value) {
|
|
39
|
+
if (!value) return '';
|
|
40
|
+
if (typeof value === 'string') return value;
|
|
41
|
+
if (typeof value === 'object') {
|
|
42
|
+
return value.text || value.summary || value.phase || value.next || '';
|
|
43
|
+
}
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function progressText(progress) {
|
|
48
|
+
if (!progress) return '';
|
|
49
|
+
if (typeof progress === 'string') return progress;
|
|
50
|
+
if (typeof progress === 'object') {
|
|
51
|
+
return progress.summary || progress.next || progress.phase || '';
|
|
52
|
+
}
|
|
53
|
+
return String(progress);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toMs(value) {
|
|
57
|
+
if (!value) return 0;
|
|
58
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
59
|
+
const parsed = Date.parse(String(value));
|
|
60
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toIso(value) {
|
|
64
|
+
const ms = toMs(value);
|
|
65
|
+
return ms ? new Date(ms).toISOString() : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ageLabel(now, value) {
|
|
69
|
+
const ms = toMs(value);
|
|
70
|
+
if (!ms) return '';
|
|
71
|
+
const age = Math.max(0, now - ms);
|
|
72
|
+
if (age < 60 * 1000) return 'just now';
|
|
73
|
+
const minutes = Math.round(age / (60 * 1000));
|
|
74
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
75
|
+
const hours = Math.round(minutes / 60);
|
|
76
|
+
if (hours < 48) return `${hours}h ago`;
|
|
77
|
+
const days = Math.round(hours / 24);
|
|
78
|
+
return `${days}d ago`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeStatus(status) {
|
|
82
|
+
const text = String(status || '').toLowerCase();
|
|
83
|
+
if (!text) return 'unknown';
|
|
84
|
+
if (text === 'busy') return 'running';
|
|
85
|
+
if (text === 'waiting_input') return 'waiting_input';
|
|
86
|
+
if (text === 'waiting-for-input') return 'waiting';
|
|
87
|
+
if (['running', 'waiting', 'idle', 'exited', 'unknown'].includes(text)) return text;
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addIndexEntries(map, item) {
|
|
92
|
+
if (!item || typeof item !== 'object') return;
|
|
93
|
+
for (const key of ['id', 'sessionId', 'ctmSessionId', 'agentSessionId', 'claudeSessionId']) {
|
|
94
|
+
const value = item[key];
|
|
95
|
+
if (value) map.set(String(value), item);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildIndex(items) {
|
|
100
|
+
const map = new Map();
|
|
101
|
+
for (const item of items || []) addIndexEntries(map, item);
|
|
102
|
+
return map;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function lookupForSession(index, session) {
|
|
106
|
+
if (!index || !session) return null;
|
|
107
|
+
const ids = [
|
|
108
|
+
session.id,
|
|
109
|
+
session.sessionId,
|
|
110
|
+
session.ctmSessionId,
|
|
111
|
+
session.agentSessionId,
|
|
112
|
+
session.claudeSessionId,
|
|
113
|
+
];
|
|
114
|
+
for (const id of ids) {
|
|
115
|
+
if (id && index.has(String(id))) return index.get(String(id));
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function statusForSession(session, status, summary) {
|
|
121
|
+
if (session?.waitingForInput) return 'waiting_input';
|
|
122
|
+
const candidates = [
|
|
123
|
+
session?.standupStatus,
|
|
124
|
+
session?.liveStatus,
|
|
125
|
+
session?.computedStatus,
|
|
126
|
+
session?.state,
|
|
127
|
+
status?.captureStatus,
|
|
128
|
+
status?.status,
|
|
129
|
+
summary?.captureStatus,
|
|
130
|
+
summary?.status,
|
|
131
|
+
session?.serverState,
|
|
132
|
+
session?.status,
|
|
133
|
+
];
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
const normalized = normalizeStatus(candidate);
|
|
136
|
+
if (normalized && normalized !== 'unknown') return normalized;
|
|
137
|
+
}
|
|
138
|
+
return 'unknown';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isWalleSession(session) {
|
|
142
|
+
return session?.type === 'walle' || session?.agentType === 'walle' || session?.agent === 'walle';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function standupStatusForSession(session, status, summary) {
|
|
146
|
+
const normalized = statusForSession(session, status, summary);
|
|
147
|
+
// A quiet Wall-E chat is available for the next message, not blocked on
|
|
148
|
+
// operator input. Older live projections used "waiting" for that idle state.
|
|
149
|
+
if (normalized === 'waiting' && isWalleSession(session) && !session?.waitingForInput) return 'idle';
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function worktreeEvidence(worktree) {
|
|
154
|
+
if (!worktree) return '';
|
|
155
|
+
const dirtyFiles = Number(worktree.dirtyFiles || 0);
|
|
156
|
+
const unmergedCommits = Number(worktree.unmergedCommits || 0);
|
|
157
|
+
const parts = [];
|
|
158
|
+
if (dirtyFiles) parts.push(`${dirtyFiles} dirty`);
|
|
159
|
+
if (unmergedCommits) parts.push(`${unmergedCommits} unmerged`);
|
|
160
|
+
if (!parts.length && worktree.summary) parts.push(worktree.summary);
|
|
161
|
+
return parts.length ? `worktree ${parts.join(', ')}` : '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasReviewableWork(session, summaryText, progress) {
|
|
165
|
+
const worktree = session?.worktreeStatus || session?.worktree || null;
|
|
166
|
+
if (worktree) {
|
|
167
|
+
if (worktree.needsAttention) return true;
|
|
168
|
+
if (Number(worktree.dirtyFiles || 0) > 0) return true;
|
|
169
|
+
if (Number(worktree.unmergedCommits || 0) > 0) return true;
|
|
170
|
+
}
|
|
171
|
+
const text = `${summaryText || ''} ${progressText(progress)}`.toLowerCase();
|
|
172
|
+
return /\b(done|completed|complete|verified|passed|ready for review|all work done)\b/.test(text);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hasFailureSignal(summaryText, progress) {
|
|
176
|
+
const text = `${summaryText || ''} ${progressText(progress)}`.toLowerCase();
|
|
177
|
+
return /\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/.test(text);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function attentionForSession(session, summary) {
|
|
181
|
+
return normalizeAttention(summary?.attention || session?.attention || null);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function prependEvidence(evidence, items) {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const item of items || []) {
|
|
187
|
+
const text = truncateText(item, 90);
|
|
188
|
+
if (text && !out.some(existing => existing.toLowerCase() === text.toLowerCase())) out.push(text);
|
|
189
|
+
}
|
|
190
|
+
for (const item of evidence || []) {
|
|
191
|
+
const text = truncateText(item, 90);
|
|
192
|
+
if (text && !out.some(existing => existing.toLowerCase() === text.toLowerCase())) out.push(text);
|
|
193
|
+
}
|
|
194
|
+
return out.slice(0, 5);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function baseEvidence({ session, status, summary, now, intentText, progress }) {
|
|
198
|
+
const evidence = [];
|
|
199
|
+
if (status === 'waiting_input') evidence.push('waiting input');
|
|
200
|
+
else if (status === 'waiting') evidence.push('waiting');
|
|
201
|
+
else if (status === 'running') evidence.push('running');
|
|
202
|
+
const branch = session?.branch || session?.gitBranch || session?.worktreeStatus?.branch || '';
|
|
203
|
+
if (branch) evidence.push(`branch ${branch}`);
|
|
204
|
+
const wtEvidence = worktreeEvidence(session?.worktreeStatus || session?.worktree);
|
|
205
|
+
if (wtEvidence) evidence.push(wtEvidence);
|
|
206
|
+
const age = ageLabel(now, latestActivity(session, summary));
|
|
207
|
+
if (age) evidence.push(`last activity ${age}`);
|
|
208
|
+
if (intentText && summary?.intent?.source === 'prompt-fallback') {
|
|
209
|
+
evidence.push(`prompt: ${truncateText(intentText, 80)}`);
|
|
210
|
+
}
|
|
211
|
+
const progressPhase = progress && typeof progress === 'object' ? progress.phase : '';
|
|
212
|
+
if (progressPhase && !['unknown', status].includes(String(progressPhase))) {
|
|
213
|
+
evidence.push(`phase ${progressPhase}`);
|
|
214
|
+
}
|
|
215
|
+
return evidence.slice(0, 5);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function latestActivity(session, summary) {
|
|
219
|
+
const values = [
|
|
220
|
+
summary?.lastActivity,
|
|
221
|
+
summary?.progress?.updatedAt,
|
|
222
|
+
summary?.intent?.updatedAt,
|
|
223
|
+
session?.lastPtyActivity,
|
|
224
|
+
session?.lastActivity,
|
|
225
|
+
session?.modifiedAt,
|
|
226
|
+
session?.fileModifiedAt,
|
|
227
|
+
session?.createdAt,
|
|
228
|
+
];
|
|
229
|
+
let latest = 0;
|
|
230
|
+
for (const value of values) {
|
|
231
|
+
const ms = toMs(value);
|
|
232
|
+
if (ms > latest) latest = ms;
|
|
233
|
+
}
|
|
234
|
+
return latest;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function classifySessionStandup(session, signals = {}, now = Date.now()) {
|
|
238
|
+
const summary = signals.summary || {};
|
|
239
|
+
const streamStatus = signals.status || {};
|
|
240
|
+
const status = standupStatusForSession(session, streamStatus, summary);
|
|
241
|
+
const intent = summary.intent || summary.summary || summary.displayPrompt || summary.lastPrompt || session?.label || '';
|
|
242
|
+
const intentString = truncateText(valueText(intent), 160);
|
|
243
|
+
const progress = summary.progress || '';
|
|
244
|
+
const progressString = truncateText(progressText(progress), 180);
|
|
245
|
+
const summaryText = valueText(summary.summary) || intentString;
|
|
246
|
+
const lastActivityMs = latestActivity(session, summary);
|
|
247
|
+
const ageMs = lastActivityMs ? Math.max(0, now - lastActivityMs) : null;
|
|
248
|
+
const waitingReason = session?.waitingReason || streamStatus?.reason || summary?.statusEvidence?.[0] || '';
|
|
249
|
+
const reviewable = hasReviewableWork(session, summaryText, progress);
|
|
250
|
+
const attention = attentionForSession(session, summary);
|
|
251
|
+
const hasStructuredAttention = !!(summary?.attention || session?.attention);
|
|
252
|
+
const attentionSeverity = attention?.severity || 'none';
|
|
253
|
+
const failed = attentionSeverity === 'failure'
|
|
254
|
+
|| (!hasStructuredAttention && hasFailureSignal(summaryText, progress));
|
|
255
|
+
const warning = attentionSeverity === 'warning';
|
|
256
|
+
const isWaiting = status === 'waiting_input' || status === 'waiting';
|
|
257
|
+
const isRunning = status === 'running' || status === 'busy';
|
|
258
|
+
|
|
259
|
+
let lane = 'continue_later';
|
|
260
|
+
let actionKind = 'resume';
|
|
261
|
+
let actionLabel = 'Continue';
|
|
262
|
+
let recommendation = 'Session is idle; reuse this context when you have the next instruction.';
|
|
263
|
+
let confidence = 'medium';
|
|
264
|
+
|
|
265
|
+
if (isWaiting) {
|
|
266
|
+
lane = 'needs_user';
|
|
267
|
+
actionKind = status === 'waiting_input' || /approval|choice|input/i.test(waitingReason)
|
|
268
|
+
? 'approval_needed'
|
|
269
|
+
: 'needs_input';
|
|
270
|
+
actionLabel = 'Respond';
|
|
271
|
+
recommendation = actionKind === 'approval_needed'
|
|
272
|
+
? 'Approval or input is waiting in the terminal.'
|
|
273
|
+
: 'The session appears to need user input before it can continue.';
|
|
274
|
+
confidence = 'high';
|
|
275
|
+
} else if (isRunning) {
|
|
276
|
+
lane = 'running';
|
|
277
|
+
actionKind = 'watch';
|
|
278
|
+
actionLabel = 'Open';
|
|
279
|
+
recommendation = 'Work is still running; no operator action is needed yet.';
|
|
280
|
+
confidence = status === 'running' ? 'high' : 'medium';
|
|
281
|
+
} else if (failed) {
|
|
282
|
+
lane = 'needs_user';
|
|
283
|
+
actionKind = 'investigate';
|
|
284
|
+
actionLabel = 'Inspect';
|
|
285
|
+
recommendation = attention?.recommendation || 'A current blocker or failure appears to need attention.';
|
|
286
|
+
confidence = attention?.confidence || 'medium';
|
|
287
|
+
} else if (warning) {
|
|
288
|
+
lane = 'needs_user';
|
|
289
|
+
actionKind = 'warning';
|
|
290
|
+
actionLabel = 'Review';
|
|
291
|
+
recommendation = attention?.recommendation || 'A current warning appears to need attention.';
|
|
292
|
+
confidence = attention?.confidence || 'medium';
|
|
293
|
+
} else if (reviewable) {
|
|
294
|
+
lane = 'ready_review';
|
|
295
|
+
actionKind = 'review';
|
|
296
|
+
actionLabel = 'Review';
|
|
297
|
+
recommendation = 'Review the session output or finish the worktree changes.';
|
|
298
|
+
confidence = session?.worktreeStatus ? 'high' : 'medium';
|
|
299
|
+
} else if (ageMs != null && ageMs > STALE_AFTER_MS) {
|
|
300
|
+
lane = 'continue_later';
|
|
301
|
+
actionKind = 'resume';
|
|
302
|
+
actionLabel = 'Resume';
|
|
303
|
+
recommendation = 'Session is quiet; resume it only if this preserved context is still useful.';
|
|
304
|
+
confidence = 'medium';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let evidence = baseEvidence({ session, status, summary, now, intentText: intentString, progress });
|
|
308
|
+
if ((failed || warning) && attention?.evidence?.length) {
|
|
309
|
+
evidence = prependEvidence(evidence, attention.evidence);
|
|
310
|
+
}
|
|
311
|
+
if (failed && !isRunning && !evidence.some(item => /failed|blocked|blocker|error|failure/i.test(item))) {
|
|
312
|
+
evidence.unshift('possible blocker');
|
|
313
|
+
}
|
|
314
|
+
if (warning && !isRunning && !evidence.some(item => /warn|caution|risk|attention/i.test(item))) {
|
|
315
|
+
evidence.unshift('warning');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const title = truncateText(
|
|
319
|
+
session?.label || summary?.displayPrompt || summary?.lastPrompt || intentString || session?.id || 'Session',
|
|
320
|
+
90
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
id: session?.id || session?.sessionId || '',
|
|
325
|
+
agentSessionId: session?.agentSessionId || session?.claudeSessionId || null,
|
|
326
|
+
title,
|
|
327
|
+
agent: session?.agentType || session?.agent || session?.type || 'session',
|
|
328
|
+
provider: session?.model_provider || session?.provider || '',
|
|
329
|
+
model: session?.model_id || session?.model || '',
|
|
330
|
+
cwd: session?.cwd || '',
|
|
331
|
+
project: session?.project || session?.cwd || '',
|
|
332
|
+
branch: session?.branch || session?.gitBranch || session?.worktreeStatus?.branch || '',
|
|
333
|
+
status,
|
|
334
|
+
lane,
|
|
335
|
+
actionKind,
|
|
336
|
+
actionLabel,
|
|
337
|
+
recommendation,
|
|
338
|
+
confidence,
|
|
339
|
+
evidence,
|
|
340
|
+
attention: attention && attention.severity !== 'none' ? attention : null,
|
|
341
|
+
intent: intentString,
|
|
342
|
+
progress: progressString,
|
|
343
|
+
lastActivity: toIso(lastActivityMs),
|
|
344
|
+
ageMs,
|
|
345
|
+
worktree: session?.worktreeStatus || session?.worktree || null,
|
|
346
|
+
capabilities: session?.agentCapabilities || session?.capabilities || {},
|
|
347
|
+
priority: LANE_ORDER.get(lane) ?? 99,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function sortCards(a, b) {
|
|
352
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
353
|
+
const actionOrder = {
|
|
354
|
+
approval_needed: 0,
|
|
355
|
+
needs_input: 1,
|
|
356
|
+
investigate: 2,
|
|
357
|
+
warning: 3,
|
|
358
|
+
review: 4,
|
|
359
|
+
watch: 5,
|
|
360
|
+
resume: 6,
|
|
361
|
+
archive: 7,
|
|
362
|
+
};
|
|
363
|
+
const ao = actionOrder[a.actionKind] ?? 99;
|
|
364
|
+
const bo = actionOrder[b.actionKind] ?? 99;
|
|
365
|
+
if (ao !== bo) return ao - bo;
|
|
366
|
+
return (toMs(b.lastActivity) || 0) - (toMs(a.lastActivity) || 0);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildSessionStandupSnapshot({ sessions = [], summaries = [], statuses = [], now = Date.now() } = {}) {
|
|
370
|
+
const summaryIndex = buildIndex(summaries);
|
|
371
|
+
const statusIndex = buildIndex(statuses);
|
|
372
|
+
const cards = sessions
|
|
373
|
+
.map(session => classifySessionStandup(session, {
|
|
374
|
+
summary: lookupForSession(summaryIndex, session),
|
|
375
|
+
status: lookupForSession(statusIndex, session),
|
|
376
|
+
}, now))
|
|
377
|
+
.filter(card => card.id)
|
|
378
|
+
.sort(sortCards);
|
|
379
|
+
|
|
380
|
+
const counts = { total: cards.length };
|
|
381
|
+
for (const lane of LANE_DEFS) counts[lane.id] = 0;
|
|
382
|
+
for (const card of cards) counts[card.lane] = (counts[card.lane] || 0) + 1;
|
|
383
|
+
|
|
384
|
+
const lanes = LANE_DEFS.map(lane => ({
|
|
385
|
+
...lane,
|
|
386
|
+
count: counts[lane.id] || 0,
|
|
387
|
+
sessions: cards.filter(card => card.lane === lane.id),
|
|
388
|
+
}));
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
generatedAt: new Date(now).toISOString(),
|
|
392
|
+
counts,
|
|
393
|
+
lanes,
|
|
394
|
+
sessions: cards,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = {
|
|
399
|
+
LANE_DEFS,
|
|
400
|
+
classifySessionStandup,
|
|
401
|
+
buildSessionStandupSnapshot,
|
|
402
|
+
_private: {
|
|
403
|
+
normalizeStatus,
|
|
404
|
+
standupStatusForSession,
|
|
405
|
+
latestActivity,
|
|
406
|
+
truncateText,
|
|
407
|
+
hasFailureSignal,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
@@ -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
|
-
|
|
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;
|