tide-commander 0.52.0
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/LICENSE +21 -0
- package/README.md +364 -0
- package/dist/assets/characters/Textures/colormap.png +0 -0
- package/dist/assets/characters/character-female-a.glb +0 -0
- package/dist/assets/characters/character-female-b.glb +0 -0
- package/dist/assets/characters/character-female-c.glb +0 -0
- package/dist/assets/characters/character-female-d.glb +0 -0
- package/dist/assets/characters/character-female-e.glb +0 -0
- package/dist/assets/characters/character-female-f.glb +0 -0
- package/dist/assets/characters/character-male-a-processed.gltf +11862 -0
- package/dist/assets/characters/character-male-a.glb +0 -0
- package/dist/assets/characters/character-male-b.glb +0 -0
- package/dist/assets/characters/character-male-c.glb +0 -0
- package/dist/assets/characters/character-male-d.glb +0 -0
- package/dist/assets/characters/character-male-e.glb +0 -0
- package/dist/assets/characters/character-male-f.glb +0 -0
- package/dist/assets/icons/icon-192.png +0 -0
- package/dist/assets/icons/icon-512.png +0 -0
- package/dist/assets/landing-Cc0MDBAK.css +1 -0
- package/dist/assets/main-BIpLsrUu.css +1 -0
- package/dist/assets/main-DMTRw3br.js +276 -0
- package/dist/assets/textures/concrete_floor_worn_001_diff_1k.jpg +0 -0
- package/dist/assets/textures/logo-blanco.png +0 -0
- package/dist/assets/vendor-react-uS-d4TUT.js +17 -0
- package/dist/assets/vendor-three-4iQNXcoo.js +3828 -0
- package/dist/assets/web-BZdi2lG9.js +1 -0
- package/dist/assets/web-yHsOO1Qb.js +1 -0
- package/dist/index.html +38 -0
- package/dist/manifest.json +39 -0
- package/dist/src/packages/landing/index.html +463 -0
- package/dist/src/packages/server/app.js +87 -0
- package/dist/src/packages/server/auth/index.js +121 -0
- package/dist/src/packages/server/claude/backend.js +578 -0
- package/dist/src/packages/server/claude/index.js +8 -0
- package/dist/src/packages/server/claude/runner/internal-events.js +22 -0
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +208 -0
- package/dist/src/packages/server/claude/runner/recovery-store.js +72 -0
- package/dist/src/packages/server/claude/runner/resource-monitor.js +51 -0
- package/dist/src/packages/server/claude/runner/restart-policy.js +69 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +153 -0
- package/dist/src/packages/server/claude/runner/watchdog.js +114 -0
- package/dist/src/packages/server/claude/runner.js +310 -0
- package/dist/src/packages/server/claude/session-loader.js +898 -0
- package/dist/src/packages/server/claude/types.js +5 -0
- package/dist/src/packages/server/cli.js +113 -0
- package/dist/src/packages/server/codex/backend.js +119 -0
- package/dist/src/packages/server/codex/index.js +2 -0
- package/dist/src/packages/server/codex/json-event-parser.js +612 -0
- package/dist/src/packages/server/data/builtin-skills/bitbucket-pr.js +298 -0
- package/dist/src/packages/server/data/builtin-skills/full-notifications.js +49 -0
- package/dist/src/packages/server/data/builtin-skills/git-captain.js +304 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +61 -0
- package/dist/src/packages/server/data/builtin-skills/pm2-logs.js +354 -0
- package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +51 -0
- package/dist/src/packages/server/data/builtin-skills/server-logs.js +124 -0
- package/dist/src/packages/server/data/builtin-skills/streaming-exec.js +94 -0
- package/dist/src/packages/server/data/builtin-skills/types.js +4 -0
- package/dist/src/packages/server/data/builtin-skills.js +6 -0
- package/dist/src/packages/server/data/index.js +890 -0
- package/dist/src/packages/server/data/snapshots.js +371 -0
- package/dist/src/packages/server/index.js +96 -0
- package/dist/src/packages/server/prompts/tide-commander.js +13 -0
- package/dist/src/packages/server/routes/agents.js +406 -0
- package/dist/src/packages/server/routes/config.js +347 -0
- package/dist/src/packages/server/routes/custom-models.js +170 -0
- package/dist/src/packages/server/routes/exec.js +269 -0
- package/dist/src/packages/server/routes/files.js +995 -0
- package/dist/src/packages/server/routes/index.js +38 -0
- package/dist/src/packages/server/routes/notifications.js +81 -0
- package/dist/src/packages/server/routes/permissions.js +115 -0
- package/dist/src/packages/server/routes/snapshots.js +224 -0
- package/dist/src/packages/server/routes/stt.js +99 -0
- package/dist/src/packages/server/routes/tts.js +166 -0
- package/dist/src/packages/server/routes/voice-assistant.js +310 -0
- package/dist/src/packages/server/runtime/claude-runtime-provider.js +10 -0
- package/dist/src/packages/server/runtime/codex-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +2 -0
- package/dist/src/packages/server/runtime/types.js +6 -0
- package/dist/src/packages/server/services/agent-lifecycle-service.js +82 -0
- package/dist/src/packages/server/services/agent-service.js +410 -0
- package/dist/src/packages/server/services/boss-message-service.js +430 -0
- package/dist/src/packages/server/services/boss-service.js +553 -0
- package/dist/src/packages/server/services/building-service.js +867 -0
- package/dist/src/packages/server/services/claude-service.js +5 -0
- package/dist/src/packages/server/services/custom-class-service.js +323 -0
- package/dist/src/packages/server/services/database-service.js +914 -0
- package/dist/src/packages/server/services/docker-service.js +865 -0
- package/dist/src/packages/server/services/fileTracker.js +242 -0
- package/dist/src/packages/server/services/index.js +21 -0
- package/dist/src/packages/server/services/permission-service.js +258 -0
- package/dist/src/packages/server/services/pm2-service.js +435 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +168 -0
- package/dist/src/packages/server/services/runtime-events.js +357 -0
- package/dist/src/packages/server/services/runtime-service.js +308 -0
- package/dist/src/packages/server/services/runtime-status-sync.js +104 -0
- package/dist/src/packages/server/services/runtime-subagents.js +50 -0
- package/dist/src/packages/server/services/runtime-watchdog.js +74 -0
- package/dist/src/packages/server/services/secrets-service.js +206 -0
- package/dist/src/packages/server/services/skill-service.js +508 -0
- package/dist/src/packages/server/services/subordinate-context-service.js +223 -0
- package/dist/src/packages/server/services/supervisor-claude.js +132 -0
- package/dist/src/packages/server/services/supervisor-prompts.js +80 -0
- package/dist/src/packages/server/services/supervisor-service.js +659 -0
- package/dist/src/packages/server/services/work-plan-service.js +476 -0
- package/dist/src/packages/server/setup.js +86 -0
- package/dist/src/packages/server/utils/index.js +4 -0
- package/dist/src/packages/server/utils/logger.js +302 -0
- package/dist/src/packages/server/utils/string.js +39 -0
- package/dist/src/packages/server/utils/tool-formatting.js +139 -0
- package/dist/src/packages/server/utils/unicode.js +46 -0
- package/dist/src/packages/server/websocket/handler.js +290 -0
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +515 -0
- package/dist/src/packages/server/websocket/handlers/boss-handler.js +116 -0
- package/dist/src/packages/server/websocket/handlers/boss-response-handler.js +250 -0
- package/dist/src/packages/server/websocket/handlers/building-handler.js +298 -0
- package/dist/src/packages/server/websocket/handlers/command-handler.js +217 -0
- package/dist/src/packages/server/websocket/handlers/custom-class-handler.js +68 -0
- package/dist/src/packages/server/websocket/handlers/database-handler.js +223 -0
- package/dist/src/packages/server/websocket/handlers/notification-handler.js +25 -0
- package/dist/src/packages/server/websocket/handlers/permission-handler.js +21 -0
- package/dist/src/packages/server/websocket/handlers/secrets-handler.js +61 -0
- package/dist/src/packages/server/websocket/handlers/skill-handler.js +148 -0
- package/dist/src/packages/server/websocket/handlers/supervisor-handler.js +44 -0
- package/dist/src/packages/server/websocket/handlers/sync-handler.js +19 -0
- package/dist/src/packages/server/websocket/handlers/types.js +4 -0
- package/dist/src/packages/server/websocket/listeners/boss-listeners.js +21 -0
- package/dist/src/packages/server/websocket/listeners/index.js +32 -0
- package/dist/src/packages/server/websocket/listeners/permission-listeners.js +19 -0
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +196 -0
- package/dist/src/packages/server/websocket/listeners/skill-listeners.js +51 -0
- package/dist/src/packages/server/websocket/listeners/supervisor-listeners.js +37 -0
- package/dist/src/packages/shared/agent-types.js +54 -0
- package/dist/src/packages/shared/building-types.js +43 -0
- package/dist/src/packages/shared/common-types.js +1 -0
- package/dist/src/packages/shared/database-types.js +8 -0
- package/dist/src/packages/shared/types/snapshot.js +7 -0
- package/dist/src/packages/shared/types.js +12 -0
- package/dist/src/packages/shared/websocket-messages.js +1 -0
- package/dist/sw.js +37 -0
- package/package.json +90 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervisor Service
|
|
3
|
+
* Manages periodic analysis of agent activities via Claude Code
|
|
4
|
+
* Generates human-readable activity narratives
|
|
5
|
+
*/
|
|
6
|
+
import * as agentService from './agent-service.js';
|
|
7
|
+
import { loadSession } from '../claude/index.js';
|
|
8
|
+
import { callClaudeForAnalysis, stripCodeFences } from './supervisor-claude.js';
|
|
9
|
+
import { loadSupervisorHistory, saveSupervisorHistory, addSupervisorHistoryEntry, getAgentSupervisorHistory as getAgentHistoryFromStorage, deleteSupervisorHistory, } from '../data/index.js';
|
|
10
|
+
import { logger, sanitizeUnicode, generateId, truncateOrEmpty, formatToolNarrative } from '../utils/index.js';
|
|
11
|
+
import { SINGLE_AGENT_PROMPT, DEFAULT_SUPERVISOR_PROMPT } from './supervisor-prompts.js';
|
|
12
|
+
const log = logger.supervisor;
|
|
13
|
+
// In-memory narrative storage per agent
|
|
14
|
+
const narratives = new Map();
|
|
15
|
+
// Supervisor history storage per agent (persisted to disk)
|
|
16
|
+
let supervisorHistory = new Map();
|
|
17
|
+
// Configuration
|
|
18
|
+
let config = {
|
|
19
|
+
enabled: true,
|
|
20
|
+
intervalMs: 60000, // Not used for timer anymore, kept for compatibility
|
|
21
|
+
maxNarrativesPerAgent: 20,
|
|
22
|
+
autoReportOnComplete: false, // Generate report when agent completes task (disabled by default)
|
|
23
|
+
};
|
|
24
|
+
// Debounce for report generation (avoid generating too many reports in quick succession)
|
|
25
|
+
let reportDebounceTimer = null;
|
|
26
|
+
const REPORT_DEBOUNCE_MS = 3000; // Wait 3 seconds after last event before generating
|
|
27
|
+
// Per-agent debounce timers for single-agent report generation
|
|
28
|
+
const agentReportTimers = new Map();
|
|
29
|
+
const agentReportInProgress = new Set();
|
|
30
|
+
// Track if a full report is currently being generated
|
|
31
|
+
let isGeneratingReport = false;
|
|
32
|
+
// Latest report
|
|
33
|
+
let latestReport = null;
|
|
34
|
+
const listeners = new Set();
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Initialization
|
|
37
|
+
// ============================================================================
|
|
38
|
+
export function init() {
|
|
39
|
+
// Load persisted supervisor history
|
|
40
|
+
supervisorHistory = loadSupervisorHistory();
|
|
41
|
+
log.log(' Initialized (event-driven mode, using Claude Code)');
|
|
42
|
+
log.log(` Loaded history for ${supervisorHistory.size} agents`);
|
|
43
|
+
}
|
|
44
|
+
export function shutdown() {
|
|
45
|
+
if (reportDebounceTimer) {
|
|
46
|
+
clearTimeout(reportDebounceTimer);
|
|
47
|
+
reportDebounceTimer = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Event System
|
|
52
|
+
// ============================================================================
|
|
53
|
+
export function subscribe(listener) {
|
|
54
|
+
listeners.add(listener);
|
|
55
|
+
return () => listeners.delete(listener);
|
|
56
|
+
}
|
|
57
|
+
function emit(event, data) {
|
|
58
|
+
listeners.forEach((listener) => listener(event, data));
|
|
59
|
+
}
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Narrative Generation
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Generate a human-readable narrative from a Claude event
|
|
65
|
+
*/
|
|
66
|
+
export function generateNarrative(agentId, event) {
|
|
67
|
+
const agent = agentService.getAgent(agentId);
|
|
68
|
+
if (!agent)
|
|
69
|
+
return null;
|
|
70
|
+
let narrative = null;
|
|
71
|
+
let type = 'output';
|
|
72
|
+
let toolName;
|
|
73
|
+
switch (event.type) {
|
|
74
|
+
case 'tool_start':
|
|
75
|
+
type = 'tool_use';
|
|
76
|
+
toolName = event.toolName;
|
|
77
|
+
narrative = formatToolNarrative(event.toolName, event.toolInput);
|
|
78
|
+
break;
|
|
79
|
+
case 'text':
|
|
80
|
+
if (event.text && event.text.length > 10) {
|
|
81
|
+
type = 'output';
|
|
82
|
+
narrative = `Responding: "${truncateOrEmpty(event.text, 100)}"`;
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case 'thinking':
|
|
86
|
+
if (event.text) {
|
|
87
|
+
type = 'thinking';
|
|
88
|
+
narrative = `Thinking: "${truncateOrEmpty(event.text, 80)}"`;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 'error':
|
|
92
|
+
type = 'error';
|
|
93
|
+
narrative = `Error occurred: ${event.errorMessage || 'Unknown error'}`;
|
|
94
|
+
break;
|
|
95
|
+
case 'step_complete':
|
|
96
|
+
type = 'task_complete';
|
|
97
|
+
narrative = `Completed processing step (${event.tokens?.input || 0} input, ${event.tokens?.output || 0} output tokens)`;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (!narrative)
|
|
101
|
+
return null;
|
|
102
|
+
const activityNarrative = {
|
|
103
|
+
id: generateId(),
|
|
104
|
+
agentId,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
type,
|
|
107
|
+
narrative,
|
|
108
|
+
toolName,
|
|
109
|
+
};
|
|
110
|
+
// Store narrative
|
|
111
|
+
addNarrative(agentId, activityNarrative);
|
|
112
|
+
// Emit for real-time updates
|
|
113
|
+
emit('narrative', { agentId, narrative: activityNarrative });
|
|
114
|
+
// Trigger single-agent report generation on significant events (task start or complete)
|
|
115
|
+
if (event.type === 'init' || event.type === 'step_complete') {
|
|
116
|
+
log.log(` Event trigger: ${event.type} from agent ${agentId}`);
|
|
117
|
+
scheduleAgentReportGeneration(agentId);
|
|
118
|
+
}
|
|
119
|
+
return activityNarrative;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Schedule a single-agent report generation with debouncing
|
|
123
|
+
* Only updates the specific agent that had activity, not all agents
|
|
124
|
+
*/
|
|
125
|
+
function scheduleAgentReportGeneration(agentId) {
|
|
126
|
+
if (!config.enabled) {
|
|
127
|
+
log.log(' Disabled, skipping scheduled agent report');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!config.autoReportOnComplete) {
|
|
131
|
+
log.log(' Auto-report on complete disabled, skipping scheduled agent report');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Clear any existing timer for this agent
|
|
135
|
+
const existingTimer = agentReportTimers.get(agentId);
|
|
136
|
+
if (existingTimer) {
|
|
137
|
+
clearTimeout(existingTimer);
|
|
138
|
+
}
|
|
139
|
+
log.log(` Scheduled single-agent report for ${agentId} (${REPORT_DEBOUNCE_MS}ms debounce)`);
|
|
140
|
+
// Schedule report generation after debounce period
|
|
141
|
+
const timer = setTimeout(async () => {
|
|
142
|
+
agentReportTimers.delete(agentId);
|
|
143
|
+
if (agentReportInProgress.has(agentId)) {
|
|
144
|
+
log.log(` Report already in progress for ${agentId}, skipping`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const agent = agentService.getAgent(agentId);
|
|
148
|
+
if (!agent) {
|
|
149
|
+
log.log(` Agent ${agentId} not found, skipping report`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
log.log(` Generating single-agent report for ${agent.name}...`);
|
|
154
|
+
await generateSingleAgentReport(agentId);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
log.error(` Single-agent report failed for ${agentId}:`, err);
|
|
158
|
+
}
|
|
159
|
+
}, REPORT_DEBOUNCE_MS);
|
|
160
|
+
agentReportTimers.set(agentId, timer);
|
|
161
|
+
}
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Narrative Storage
|
|
164
|
+
// ============================================================================
|
|
165
|
+
function addNarrative(agentId, narrative) {
|
|
166
|
+
if (!narratives.has(agentId)) {
|
|
167
|
+
narratives.set(agentId, []);
|
|
168
|
+
}
|
|
169
|
+
const agentNarratives = narratives.get(agentId);
|
|
170
|
+
agentNarratives.unshift(narrative);
|
|
171
|
+
// Trim to max
|
|
172
|
+
if (agentNarratives.length > config.maxNarrativesPerAgent) {
|
|
173
|
+
agentNarratives.pop();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function getNarratives(agentId) {
|
|
177
|
+
return narratives.get(agentId) || [];
|
|
178
|
+
}
|
|
179
|
+
export function getAllNarratives() {
|
|
180
|
+
return new Map(narratives);
|
|
181
|
+
}
|
|
182
|
+
export function clearNarratives(agentId) {
|
|
183
|
+
narratives.delete(agentId);
|
|
184
|
+
}
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Report Generation
|
|
187
|
+
// ============================================================================
|
|
188
|
+
export async function generateReport() {
|
|
189
|
+
log.log(' generateReport() called');
|
|
190
|
+
// If already generating, return the latest report (or wait for current one)
|
|
191
|
+
if (isGeneratingReport) {
|
|
192
|
+
log.log(' Report already in progress, returning latest');
|
|
193
|
+
// Return latest report if available, otherwise return a pending status
|
|
194
|
+
if (latestReport) {
|
|
195
|
+
return latestReport;
|
|
196
|
+
}
|
|
197
|
+
// No report yet, return empty one
|
|
198
|
+
return {
|
|
199
|
+
id: generateId(),
|
|
200
|
+
timestamp: Date.now(),
|
|
201
|
+
agentSummaries: [],
|
|
202
|
+
overallStatus: 'healthy',
|
|
203
|
+
insights: ['Report generation in progress...'],
|
|
204
|
+
recommendations: [],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
isGeneratingReport = true;
|
|
208
|
+
log.log(' Starting report generation...');
|
|
209
|
+
try {
|
|
210
|
+
const agents = agentService.getAllAgents();
|
|
211
|
+
if (agents.length === 0) {
|
|
212
|
+
// Return empty report if no agents
|
|
213
|
+
const emptyReport = {
|
|
214
|
+
id: generateId(),
|
|
215
|
+
timestamp: Date.now(),
|
|
216
|
+
agentSummaries: [],
|
|
217
|
+
overallStatus: 'healthy',
|
|
218
|
+
insights: ['No agents currently active'],
|
|
219
|
+
recommendations: [],
|
|
220
|
+
};
|
|
221
|
+
latestReport = emptyReport;
|
|
222
|
+
emit('report', emptyReport);
|
|
223
|
+
return emptyReport;
|
|
224
|
+
}
|
|
225
|
+
// Build agent summaries with session history
|
|
226
|
+
const agentSummaries = await Promise.all(agents.map(async (agent) => {
|
|
227
|
+
// Try to load recent session history if agent has a session
|
|
228
|
+
let sessionNarratives = [];
|
|
229
|
+
if (agent.sessionId) {
|
|
230
|
+
try {
|
|
231
|
+
const history = await loadSession(agent.cwd, agent.sessionId, 20);
|
|
232
|
+
if (history && history.messages.length > 0) {
|
|
233
|
+
// Convert session messages to narratives
|
|
234
|
+
sessionNarratives = history.messages.map((msg, index) => ({
|
|
235
|
+
id: `session-${agent.sessionId}-${index}`,
|
|
236
|
+
agentId: agent.id,
|
|
237
|
+
timestamp: new Date(msg.timestamp).getTime(),
|
|
238
|
+
type: msg.type === 'user' ? 'task_start' :
|
|
239
|
+
msg.type === 'tool_use' ? 'tool_use' :
|
|
240
|
+
msg.type === 'tool_result' ? 'output' : 'output',
|
|
241
|
+
narrative: msg.type === 'user' ? `User asked: "${truncateOrEmpty(msg.content, 150)}"` :
|
|
242
|
+
msg.type === 'assistant' ? `Responded: "${truncateOrEmpty(msg.content, 150)}"` :
|
|
243
|
+
msg.type === 'tool_use' ? `Used tool: ${msg.toolName}` :
|
|
244
|
+
`Tool result received`,
|
|
245
|
+
toolName: msg.toolName,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
console.error(`[SupervisorService] Failed to load session for ${agent.name}:`, err);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Combine in-memory narratives with session history, preferring recent in-memory ones
|
|
254
|
+
const inMemoryNarratives = getNarratives(agent.id).slice(0, 10);
|
|
255
|
+
const allNarratives = inMemoryNarratives.length > 0
|
|
256
|
+
? inMemoryNarratives
|
|
257
|
+
: sessionNarratives.slice(-10);
|
|
258
|
+
return {
|
|
259
|
+
id: agent.id,
|
|
260
|
+
name: agent.name,
|
|
261
|
+
class: agent.class,
|
|
262
|
+
status: agent.status,
|
|
263
|
+
currentTask: agent.currentTask,
|
|
264
|
+
lastAssignedTask: agent.lastAssignedTask,
|
|
265
|
+
lastAssignedTaskTime: agent.lastAssignedTaskTime,
|
|
266
|
+
recentNarratives: allNarratives,
|
|
267
|
+
tokensUsed: agent.tokensUsed,
|
|
268
|
+
contextUsed: agent.contextUsed,
|
|
269
|
+
lastActivityTime: agent.lastActivity,
|
|
270
|
+
};
|
|
271
|
+
}));
|
|
272
|
+
// Call Claude for analysis
|
|
273
|
+
const prompt = await buildSupervisorPrompt(agentSummaries);
|
|
274
|
+
let response;
|
|
275
|
+
try {
|
|
276
|
+
response = await callClaudeForAnalysis(prompt);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
log.error(' Claude API call failed:', err);
|
|
280
|
+
// Return fallback report (but still emit it to clients)
|
|
281
|
+
const fallbackReport = createFallbackReport(agentSummaries);
|
|
282
|
+
latestReport = fallbackReport;
|
|
283
|
+
emit('report', fallbackReport);
|
|
284
|
+
return fallbackReport;
|
|
285
|
+
}
|
|
286
|
+
// Parse response
|
|
287
|
+
const report = parseClaudeResponse(response, agentSummaries);
|
|
288
|
+
// Save history entries for each agent in the report
|
|
289
|
+
saveReportToHistory(report);
|
|
290
|
+
latestReport = report;
|
|
291
|
+
log.log(` ✓ Report generated successfully (${report.agentSummaries.length} agents analyzed)`);
|
|
292
|
+
emit('report', report);
|
|
293
|
+
return report;
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
isGeneratingReport = false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Generate a supervisor report for a single agent
|
|
301
|
+
* Used when an agent finishes a task - only analyzes that specific agent
|
|
302
|
+
*/
|
|
303
|
+
async function generateSingleAgentReport(agentId) {
|
|
304
|
+
const agent = agentService.getAgent(agentId);
|
|
305
|
+
if (!agent)
|
|
306
|
+
return;
|
|
307
|
+
agentReportInProgress.add(agentId);
|
|
308
|
+
try {
|
|
309
|
+
// Build agent summary
|
|
310
|
+
const agentSummary = await buildAgentSummary(agent);
|
|
311
|
+
// Build a simpler prompt for single agent
|
|
312
|
+
const prompt = buildSingleAgentPrompt(agentSummary);
|
|
313
|
+
let response;
|
|
314
|
+
try {
|
|
315
|
+
response = await callClaudeForAnalysis(prompt);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
log.error(` Claude API call failed for single agent ${agent.name}:`, err);
|
|
319
|
+
// Create fallback analysis
|
|
320
|
+
const fallbackAnalysis = {
|
|
321
|
+
agentId: agent.id,
|
|
322
|
+
agentName: agent.name,
|
|
323
|
+
statusDescription: `${agent.status} - ${agent.currentTask || 'Task completed'}`,
|
|
324
|
+
progress: agent.status === 'working' ? 'on_track' : 'idle',
|
|
325
|
+
recentWorkSummary: agentSummary.recentNarratives[0]?.narrative || 'No recent activity',
|
|
326
|
+
};
|
|
327
|
+
saveSingleAgentToHistory(fallbackAnalysis);
|
|
328
|
+
emit('agent_analysis', { agentId, analysis: fallbackAnalysis });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// Parse response
|
|
332
|
+
const analysis = parseSingleAgentResponse(response, agentSummary);
|
|
333
|
+
// Save to history
|
|
334
|
+
saveSingleAgentToHistory(analysis);
|
|
335
|
+
// Emit the single agent update (not a full report)
|
|
336
|
+
emit('agent_analysis', { agentId, analysis });
|
|
337
|
+
log.log(` ✓ Single-agent report generated for ${agent.name}`);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
agentReportInProgress.delete(agentId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Build summary data for a single agent
|
|
345
|
+
*/
|
|
346
|
+
async function buildAgentSummary(agent) {
|
|
347
|
+
let sessionNarratives = [];
|
|
348
|
+
if (agent.sessionId) {
|
|
349
|
+
try {
|
|
350
|
+
const history = await loadSession(agent.cwd, agent.sessionId, 20);
|
|
351
|
+
if (history && history.messages.length > 0) {
|
|
352
|
+
sessionNarratives = history.messages.map((msg, index) => ({
|
|
353
|
+
id: `session-${agent.sessionId}-${index}`,
|
|
354
|
+
agentId: agent.id,
|
|
355
|
+
timestamp: new Date(msg.timestamp).getTime(),
|
|
356
|
+
type: msg.type === 'user' ? 'task_start' :
|
|
357
|
+
msg.type === 'tool_use' ? 'tool_use' :
|
|
358
|
+
msg.type === 'tool_result' ? 'output' : 'output',
|
|
359
|
+
narrative: msg.type === 'user' ? `User asked: "${truncateOrEmpty(msg.content, 150)}"` :
|
|
360
|
+
msg.type === 'assistant' ? `Responded: "${truncateOrEmpty(msg.content, 150)}"` :
|
|
361
|
+
msg.type === 'tool_use' ? `Used tool: ${msg.toolName}` :
|
|
362
|
+
`Tool result received`,
|
|
363
|
+
toolName: msg.toolName,
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
log.error(` Failed to load session for ${agent.name}:`, err);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const inMemoryNarratives = getNarratives(agent.id).slice(0, 10);
|
|
372
|
+
const allNarratives = inMemoryNarratives.length > 0
|
|
373
|
+
? inMemoryNarratives
|
|
374
|
+
: sessionNarratives.slice(-10);
|
|
375
|
+
return {
|
|
376
|
+
id: agent.id,
|
|
377
|
+
name: agent.name,
|
|
378
|
+
class: agent.class,
|
|
379
|
+
status: agent.status,
|
|
380
|
+
currentTask: agent.currentTask,
|
|
381
|
+
lastAssignedTask: agent.lastAssignedTask,
|
|
382
|
+
lastAssignedTaskTime: agent.lastAssignedTaskTime,
|
|
383
|
+
recentNarratives: allNarratives,
|
|
384
|
+
tokensUsed: agent.tokensUsed,
|
|
385
|
+
contextUsed: agent.contextUsed,
|
|
386
|
+
lastActivityTime: agent.lastActivity,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Build a prompt for analyzing a single agent
|
|
391
|
+
*/
|
|
392
|
+
function buildSingleAgentPrompt(summary) {
|
|
393
|
+
const taskAssignedSecondsAgo = summary.lastAssignedTaskTime
|
|
394
|
+
? Math.round((Date.now() - summary.lastAssignedTaskTime) / 1000)
|
|
395
|
+
: null;
|
|
396
|
+
// Sanitize all string fields to prevent invalid Unicode surrogates
|
|
397
|
+
const agentData = {
|
|
398
|
+
id: summary.id,
|
|
399
|
+
name: sanitizeUnicode(summary.name),
|
|
400
|
+
class: summary.class,
|
|
401
|
+
status: summary.status,
|
|
402
|
+
currentTask: sanitizeUnicode(summary.currentTask || 'None'),
|
|
403
|
+
assignedTask: summary.lastAssignedTask
|
|
404
|
+
? sanitizeUnicode(truncateOrEmpty(summary.lastAssignedTask, 500))
|
|
405
|
+
: 'No task assigned yet',
|
|
406
|
+
taskAssignedSecondsAgo,
|
|
407
|
+
tokensUsed: summary.tokensUsed,
|
|
408
|
+
contextPercent: Math.round((summary.contextUsed / 200000) * 100),
|
|
409
|
+
timeSinceActivity: Math.round((Date.now() - summary.lastActivityTime) / 1000),
|
|
410
|
+
recentActivities: summary.recentNarratives.map((n) => sanitizeUnicode(n.narrative)).slice(0, 5),
|
|
411
|
+
};
|
|
412
|
+
return SINGLE_AGENT_PROMPT.replace('{{AGENT_DATA}}', JSON.stringify(agentData, null, 2));
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Parse Claude's response for a single agent
|
|
416
|
+
*/
|
|
417
|
+
function parseSingleAgentResponse(response, summary) {
|
|
418
|
+
try {
|
|
419
|
+
const jsonStr = stripCodeFences(response);
|
|
420
|
+
const parsed = JSON.parse(jsonStr);
|
|
421
|
+
return {
|
|
422
|
+
agentId: parsed.agentId || summary.id,
|
|
423
|
+
agentName: parsed.agentName || summary.name,
|
|
424
|
+
statusDescription: parsed.statusDescription || `${summary.status} - ${summary.currentTask || 'Task completed'}`,
|
|
425
|
+
progress: parsed.progress || (summary.status === 'working' ? 'on_track' : 'idle'),
|
|
426
|
+
recentWorkSummary: parsed.recentWorkSummary || 'No recent activity',
|
|
427
|
+
currentFocus: parsed.currentFocus,
|
|
428
|
+
blockers: parsed.blockers || [],
|
|
429
|
+
suggestions: parsed.suggestions || [],
|
|
430
|
+
filesModified: parsed.filesModified || [],
|
|
431
|
+
concerns: parsed.concerns || [],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
log.error(' Failed to parse single agent response:', err);
|
|
436
|
+
return {
|
|
437
|
+
agentId: summary.id,
|
|
438
|
+
agentName: summary.name,
|
|
439
|
+
statusDescription: `${summary.status} - ${summary.currentTask || 'Task completed'}`,
|
|
440
|
+
progress: summary.status === 'working' ? 'on_track' : 'idle',
|
|
441
|
+
recentWorkSummary: summary.recentNarratives[0]?.narrative || 'No recent activity',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Save a single agent's analysis to history
|
|
447
|
+
*/
|
|
448
|
+
function saveSingleAgentToHistory(analysis) {
|
|
449
|
+
const entry = {
|
|
450
|
+
id: generateId(),
|
|
451
|
+
timestamp: Date.now(),
|
|
452
|
+
reportId: `single-${generateId()}`,
|
|
453
|
+
analysis,
|
|
454
|
+
};
|
|
455
|
+
addSupervisorHistoryEntry(supervisorHistory, analysis.agentId, entry);
|
|
456
|
+
saveSupervisorHistory(supervisorHistory);
|
|
457
|
+
log.log(` Saved single-agent history entry for ${analysis.agentName}`);
|
|
458
|
+
}
|
|
459
|
+
function buildSupervisorPrompt(summaries) {
|
|
460
|
+
const customPrompt = config.customPrompt || DEFAULT_SUPERVISOR_PROMPT;
|
|
461
|
+
// Sanitize all string fields to prevent invalid Unicode surrogates
|
|
462
|
+
const agentData = summaries.map((s) => {
|
|
463
|
+
// Calculate time since task was assigned
|
|
464
|
+
const taskAssignedSecondsAgo = s.lastAssignedTaskTime
|
|
465
|
+
? Math.round((Date.now() - s.lastAssignedTaskTime) / 1000)
|
|
466
|
+
: null;
|
|
467
|
+
return {
|
|
468
|
+
id: s.id, // Include ID so we can match response back
|
|
469
|
+
name: sanitizeUnicode(s.name),
|
|
470
|
+
class: s.class,
|
|
471
|
+
status: s.status,
|
|
472
|
+
currentTask: sanitizeUnicode(s.currentTask || 'None'),
|
|
473
|
+
// Include the full assigned task so supervisor knows what the agent was asked to do
|
|
474
|
+
assignedTask: s.lastAssignedTask
|
|
475
|
+
? sanitizeUnicode(truncateOrEmpty(s.lastAssignedTask, 500))
|
|
476
|
+
: 'No task assigned yet',
|
|
477
|
+
taskAssignedSecondsAgo,
|
|
478
|
+
tokensUsed: s.tokensUsed,
|
|
479
|
+
contextPercent: Math.round((s.contextUsed / 200000) * 100),
|
|
480
|
+
timeSinceActivity: Math.round((Date.now() - s.lastActivityTime) / 1000),
|
|
481
|
+
recentActivities: s.recentNarratives.map((n) => sanitizeUnicode(n.narrative)).slice(0, 5),
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
return customPrompt.replace('{{AGENT_DATA}}', JSON.stringify(agentData, null, 2));
|
|
485
|
+
}
|
|
486
|
+
function parseClaudeResponse(response, summaries) {
|
|
487
|
+
try {
|
|
488
|
+
const jsonStr = stripCodeFences(response);
|
|
489
|
+
const parsed = JSON.parse(jsonStr);
|
|
490
|
+
// Map agentAnalyses to agentSummaries (the field name in our type)
|
|
491
|
+
const agentAnalyses = (parsed.agentAnalyses || []).map((a) => ({
|
|
492
|
+
agentId: a.agentId || '',
|
|
493
|
+
agentName: a.agentName || '',
|
|
494
|
+
statusDescription: a.statusDescription || 'Unknown status',
|
|
495
|
+
progress: a.progress || 'idle',
|
|
496
|
+
recentWorkSummary: a.recentWorkSummary || 'No recent activity',
|
|
497
|
+
concerns: a.concerns || [],
|
|
498
|
+
}));
|
|
499
|
+
// Match agent IDs from summaries by name (more reliable than index)
|
|
500
|
+
agentAnalyses.forEach((analysis) => {
|
|
501
|
+
if (!analysis.agentId) {
|
|
502
|
+
// Find matching summary by name
|
|
503
|
+
const matchingSummary = summaries.find(s => s.name === analysis.agentName);
|
|
504
|
+
if (matchingSummary) {
|
|
505
|
+
analysis.agentId = matchingSummary.id;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
id: generateId(),
|
|
511
|
+
timestamp: Date.now(),
|
|
512
|
+
agentSummaries: agentAnalyses,
|
|
513
|
+
overallStatus: parsed.overallStatus || 'healthy',
|
|
514
|
+
insights: parsed.insights || [],
|
|
515
|
+
recommendations: parsed.recommendations || [],
|
|
516
|
+
rawResponse: response,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
log.error(' Failed to parse Claude response:', err);
|
|
521
|
+
log.error(' Raw response:', response);
|
|
522
|
+
// Return fallback report
|
|
523
|
+
return createFallbackReport(summaries);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function createFallbackReport(summaries) {
|
|
527
|
+
return {
|
|
528
|
+
id: generateId(),
|
|
529
|
+
timestamp: Date.now(),
|
|
530
|
+
agentSummaries: summaries.map((s) => ({
|
|
531
|
+
agentId: s.id,
|
|
532
|
+
agentName: s.name,
|
|
533
|
+
statusDescription: `${s.status} - ${s.currentTask || 'No current task'}`,
|
|
534
|
+
progress: s.status === 'working' ? 'on_track' : 'idle',
|
|
535
|
+
recentWorkSummary: s.recentNarratives[0]?.narrative || 'No recent activity',
|
|
536
|
+
})),
|
|
537
|
+
overallStatus: 'healthy',
|
|
538
|
+
insights: ['Unable to generate detailed analysis - using basic status'],
|
|
539
|
+
recommendations: [],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// ============================================================================
|
|
543
|
+
// History Management
|
|
544
|
+
// ============================================================================
|
|
545
|
+
/**
|
|
546
|
+
* Save a report's agent analyses to history
|
|
547
|
+
*/
|
|
548
|
+
function saveReportToHistory(report) {
|
|
549
|
+
for (const analysis of report.agentSummaries) {
|
|
550
|
+
const entry = {
|
|
551
|
+
id: generateId(),
|
|
552
|
+
timestamp: report.timestamp,
|
|
553
|
+
reportId: report.id,
|
|
554
|
+
analysis,
|
|
555
|
+
};
|
|
556
|
+
addSupervisorHistoryEntry(supervisorHistory, analysis.agentId, entry);
|
|
557
|
+
}
|
|
558
|
+
// Persist to disk
|
|
559
|
+
saveSupervisorHistory(supervisorHistory);
|
|
560
|
+
log.log(` Saved history entries for ${report.agentSummaries.length} agents`);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Get supervisor history for a specific agent
|
|
564
|
+
*/
|
|
565
|
+
export function getAgentSupervisorHistory(agentId) {
|
|
566
|
+
return getAgentHistoryFromStorage(supervisorHistory, agentId);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Delete supervisor history for an agent (call when agent is deleted)
|
|
570
|
+
*/
|
|
571
|
+
export function deleteAgentHistory(agentId) {
|
|
572
|
+
deleteSupervisorHistory(supervisorHistory, agentId);
|
|
573
|
+
saveSupervisorHistory(supervisorHistory);
|
|
574
|
+
log.log(` Deleted history for agent ${agentId}`);
|
|
575
|
+
}
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// Configuration
|
|
578
|
+
// ============================================================================
|
|
579
|
+
export function getConfig() {
|
|
580
|
+
return { ...config };
|
|
581
|
+
}
|
|
582
|
+
export function setConfig(updates) {
|
|
583
|
+
config = { ...config, ...updates };
|
|
584
|
+
// If disabling, cancel any pending report
|
|
585
|
+
if (!config.enabled && reportDebounceTimer) {
|
|
586
|
+
clearTimeout(reportDebounceTimer);
|
|
587
|
+
reportDebounceTimer = null;
|
|
588
|
+
}
|
|
589
|
+
emit('config_changed', config);
|
|
590
|
+
}
|
|
591
|
+
export function getLatestReport() {
|
|
592
|
+
return latestReport;
|
|
593
|
+
}
|
|
594
|
+
export function getStatus() {
|
|
595
|
+
return {
|
|
596
|
+
enabled: config.enabled,
|
|
597
|
+
autoReportOnComplete: config.autoReportOnComplete === true,
|
|
598
|
+
lastReportTime: latestReport?.timestamp || null,
|
|
599
|
+
// Reports are now event-driven (on task start/complete), not scheduled
|
|
600
|
+
nextReportTime: null,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
// Global usage stats (shared across all sessions since they use same API key)
|
|
604
|
+
let globalUsage = null;
|
|
605
|
+
/**
|
|
606
|
+
* Update global usage stats from a /usage command response
|
|
607
|
+
*/
|
|
608
|
+
export function updateGlobalUsage(agentId, agentName, usageData) {
|
|
609
|
+
globalUsage = {
|
|
610
|
+
session: usageData.session,
|
|
611
|
+
weeklyAllModels: usageData.weeklyAllModels,
|
|
612
|
+
weeklySonnet: usageData.weeklySonnet,
|
|
613
|
+
sourceAgentId: agentId,
|
|
614
|
+
sourceAgentName: agentName,
|
|
615
|
+
lastUpdated: Date.now(),
|
|
616
|
+
};
|
|
617
|
+
log.log(`✓ Updated global usage stats from ${agentName}:`);
|
|
618
|
+
log.log(` Session: ${usageData.session.percentUsed}% (resets ${usageData.session.resetTime})`);
|
|
619
|
+
log.log(` Weekly All: ${usageData.weeklyAllModels.percentUsed}% (resets ${usageData.weeklyAllModels.resetTime})`);
|
|
620
|
+
log.log(` Weekly Sonnet: ${usageData.weeklySonnet.percentUsed}% (resets ${usageData.weeklySonnet.resetTime})`);
|
|
621
|
+
// Emit event for real-time updates
|
|
622
|
+
emit('global_usage', globalUsage);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get current global usage stats
|
|
626
|
+
*/
|
|
627
|
+
export function getGlobalUsage() {
|
|
628
|
+
return globalUsage;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Request a usage refresh from any idle agent
|
|
632
|
+
* Returns the agent ID that will provide the data, or null if no agent is available
|
|
633
|
+
*/
|
|
634
|
+
export async function requestUsageRefresh() {
|
|
635
|
+
console.log('[Supervisor] requestUsageRefresh called');
|
|
636
|
+
const agents = agentService.getAllAgents();
|
|
637
|
+
console.log('[Supervisor] All agents:', agents.map(a => ({ name: a.name, status: a.status, sessionId: a.sessionId })));
|
|
638
|
+
// Find an idle agent with a session
|
|
639
|
+
const idleAgent = agents.find(a => a.status === 'idle' && a.sessionId);
|
|
640
|
+
console.log('[Supervisor] Idle agent found:', idleAgent ? idleAgent.name : 'none');
|
|
641
|
+
if (!idleAgent) {
|
|
642
|
+
log.log('No idle agent available for usage refresh');
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
log.log(`Requesting usage refresh from ${idleAgent.name}`);
|
|
646
|
+
// Import dynamically to avoid circular dependency
|
|
647
|
+
const { sendSilentCommand } = await import('./claude-service.js');
|
|
648
|
+
try {
|
|
649
|
+
console.log('[Supervisor] Sending /usage command to', idleAgent.name);
|
|
650
|
+
await sendSilentCommand(idleAgent.id, '/usage');
|
|
651
|
+
console.log('[Supervisor] /usage command sent successfully');
|
|
652
|
+
return idleAgent.id;
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
console.error('[Supervisor] Failed to send /usage command:', err);
|
|
656
|
+
log.error(`Failed to request usage from ${idleAgent.name}:`, err);
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|