tycono-server 0.1.0-beta.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/bin/cli.js +35 -0
- package/bin/server.ts +160 -0
- package/package.json +50 -0
- package/src/api/package.json +31 -0
- package/src/api/src/create-app.ts +90 -0
- package/src/api/src/create-server.ts +251 -0
- package/src/api/src/engine/agent-loop.ts +738 -0
- package/src/api/src/engine/authority-validator.ts +149 -0
- package/src/api/src/engine/context-assembler.ts +912 -0
- package/src/api/src/engine/index.ts +27 -0
- package/src/api/src/engine/knowledge-gate.ts +365 -0
- package/src/api/src/engine/llm-adapter.ts +304 -0
- package/src/api/src/engine/org-tree.ts +270 -0
- package/src/api/src/engine/role-lifecycle.ts +369 -0
- package/src/api/src/engine/runners/claude-cli.ts +796 -0
- package/src/api/src/engine/runners/direct-api.ts +66 -0
- package/src/api/src/engine/runners/index.ts +30 -0
- package/src/api/src/engine/runners/types.ts +95 -0
- package/src/api/src/engine/skill-template.ts +134 -0
- package/src/api/src/engine/tools/definitions.ts +201 -0
- package/src/api/src/engine/tools/executor.ts +611 -0
- package/src/api/src/routes/active-sessions.ts +134 -0
- package/src/api/src/routes/coins.ts +153 -0
- package/src/api/src/routes/company.ts +57 -0
- package/src/api/src/routes/cost.ts +141 -0
- package/src/api/src/routes/engine.ts +220 -0
- package/src/api/src/routes/execute.ts +1075 -0
- package/src/api/src/routes/git.ts +211 -0
- package/src/api/src/routes/knowledge.ts +378 -0
- package/src/api/src/routes/operations.ts +309 -0
- package/src/api/src/routes/preferences.ts +63 -0
- package/src/api/src/routes/presets.ts +123 -0
- package/src/api/src/routes/projects.ts +82 -0
- package/src/api/src/routes/quests.ts +41 -0
- package/src/api/src/routes/roles.ts +112 -0
- package/src/api/src/routes/save.ts +152 -0
- package/src/api/src/routes/sessions.ts +288 -0
- package/src/api/src/routes/setup.ts +437 -0
- package/src/api/src/routes/skills.ts +357 -0
- package/src/api/src/routes/speech.ts +959 -0
- package/src/api/src/routes/supervision.ts +136 -0
- package/src/api/src/routes/sync.ts +165 -0
- package/src/api/src/server.ts +59 -0
- package/src/api/src/services/activity-stream.ts +184 -0
- package/src/api/src/services/activity-tracker.ts +115 -0
- package/src/api/src/services/claude-md-manager.ts +94 -0
- package/src/api/src/services/company-config.ts +115 -0
- package/src/api/src/services/database.ts +77 -0
- package/src/api/src/services/digest-engine.ts +313 -0
- package/src/api/src/services/execution-manager.ts +1036 -0
- package/src/api/src/services/file-reader.ts +77 -0
- package/src/api/src/services/git-save.ts +614 -0
- package/src/api/src/services/job-manager.ts +16 -0
- package/src/api/src/services/knowledge-importer.ts +466 -0
- package/src/api/src/services/markdown-parser.ts +173 -0
- package/src/api/src/services/port-registry.ts +222 -0
- package/src/api/src/services/preferences.ts +150 -0
- package/src/api/src/services/preset-loader.ts +149 -0
- package/src/api/src/services/pricing.ts +34 -0
- package/src/api/src/services/scaffold.ts +546 -0
- package/src/api/src/services/session-store.ts +340 -0
- package/src/api/src/services/supervisor-heartbeat.ts +897 -0
- package/src/api/src/services/team-recommender.ts +382 -0
- package/src/api/src/services/token-ledger.ts +127 -0
- package/src/api/src/services/wave-messages.ts +194 -0
- package/src/api/src/services/wave-multiplexer.ts +356 -0
- package/src/api/src/services/wave-tracker.ts +359 -0
- package/src/api/src/utils/role-level.ts +31 -0
- package/src/core/scaffolder.ts +620 -0
- package/src/shared/types.ts +224 -0
- package/templates/CLAUDE.md.tmpl +239 -0
- package/templates/company.md.tmpl +17 -0
- package/templates/gitignore.tmpl +28 -0
- package/templates/roles.md.tmpl +8 -0
- package/templates/skills/_manifest.json +23 -0
- package/templates/skills/agent-browser/SKILL.md +159 -0
- package/templates/skills/agent-browser/meta.json +19 -0
- package/templates/skills/akb-linter/SKILL.md +125 -0
- package/templates/skills/akb-linter/meta.json +12 -0
- package/templates/skills/knowledge-gate/SKILL.md +120 -0
- package/templates/skills/knowledge-gate/meta.json +12 -0
- package/templates/teams/agency.json +58 -0
- package/templates/teams/research.json +58 -0
- package/templates/teams/startup.json +58 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CEO Supervisor Heartbeat Service
|
|
3
|
+
*
|
|
4
|
+
* The "bash while-true loop" equivalent — keeps exactly ONE CEO Supervisor
|
|
5
|
+
* session alive per wave. That session uses dispatch/watch/amend tools
|
|
6
|
+
* like any other supervisor node in the recursive tree.
|
|
7
|
+
*
|
|
8
|
+
* Heartbeat = "CEO Supervisor를 죽지 않게 살려두는 것"
|
|
9
|
+
*
|
|
10
|
+
* Dispatch Protocol Principle 6: 죽어도 세상은 돌아간다
|
|
11
|
+
* - Subordinates keep running during supervisor crash
|
|
12
|
+
* - On restart, digest catches up with all missed events
|
|
13
|
+
*/
|
|
14
|
+
import { executionManager, type Execution } from './execution-manager.js';
|
|
15
|
+
import { createSession, getSession, listSessions, addMessage, type Message } from './session-store.js';
|
|
16
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
17
|
+
import { ClaudeCliProvider } from '../engine/llm-adapter.js';
|
|
18
|
+
import { buildOrgTree, getSubordinates } from '../engine/org-tree.js';
|
|
19
|
+
import { readConfig } from './company-config.js';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { COMPANY_ROOT } from './file-reader.js';
|
|
23
|
+
import { ActivityStream } from './activity-stream.js';
|
|
24
|
+
import { saveCompletedWave } from './wave-tracker.js';
|
|
25
|
+
import { waveMultiplexer } from './wave-multiplexer.js';
|
|
26
|
+
import { appendWaveMessage, buildHistoryPrompt } from './wave-messages.js';
|
|
27
|
+
|
|
28
|
+
/* ─── Types ──────────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
interface SupervisorState {
|
|
31
|
+
waveId: string;
|
|
32
|
+
directive: string;
|
|
33
|
+
targetRoles?: string[];
|
|
34
|
+
continuous: boolean;
|
|
35
|
+
preset?: string;
|
|
36
|
+
supervisorSessionId: string | null;
|
|
37
|
+
executionId: string | null;
|
|
38
|
+
status: 'starting' | 'running' | 'restarting' | 'stopped' | 'error';
|
|
39
|
+
crashCount: number;
|
|
40
|
+
maxCrashRetries: number;
|
|
41
|
+
restartTimer: ReturnType<typeof setTimeout> | null;
|
|
42
|
+
cleanupTimer: ReturnType<typeof setTimeout> | null;
|
|
43
|
+
pendingDirectives: PendingDirective[];
|
|
44
|
+
pendingQuestions: PendingQuestion[];
|
|
45
|
+
createdAt: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PendingDirective {
|
|
49
|
+
id: string;
|
|
50
|
+
text: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
delivered: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PendingQuestion {
|
|
56
|
+
id: string;
|
|
57
|
+
question: string;
|
|
58
|
+
fromRole: string;
|
|
59
|
+
context: string;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
answer?: string;
|
|
62
|
+
answeredAt?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ─── Supervisor Heartbeat Manager ───────────── */
|
|
66
|
+
|
|
67
|
+
class SupervisorHeartbeat {
|
|
68
|
+
private supervisors = new Map<string, SupervisorState>();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Start a CEO Supervisor for a wave.
|
|
72
|
+
* This creates a supervisor session and starts an execution.
|
|
73
|
+
* If the execution dies, it auto-restarts (heartbeat).
|
|
74
|
+
*/
|
|
75
|
+
start(waveId: string, directive: string, targetRoles?: string[], continuous = false, preset?: string): SupervisorState {
|
|
76
|
+
// Check if supervisor already running for this wave
|
|
77
|
+
const existing = this.supervisors.get(waveId);
|
|
78
|
+
if (existing && (existing.status === 'running' || existing.status === 'starting')) {
|
|
79
|
+
console.log(`[Supervisor] Already running for wave ${waveId}`);
|
|
80
|
+
return existing;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const state: SupervisorState = {
|
|
84
|
+
waveId,
|
|
85
|
+
directive,
|
|
86
|
+
targetRoles,
|
|
87
|
+
continuous,
|
|
88
|
+
preset,
|
|
89
|
+
supervisorSessionId: null,
|
|
90
|
+
executionId: null,
|
|
91
|
+
status: 'starting',
|
|
92
|
+
crashCount: 0,
|
|
93
|
+
maxCrashRetries: 10,
|
|
94
|
+
restartTimer: null,
|
|
95
|
+
cleanupTimer: null,
|
|
96
|
+
pendingDirectives: [],
|
|
97
|
+
pendingQuestions: [],
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.supervisors.set(waveId, state);
|
|
102
|
+
|
|
103
|
+
// Empty directive → idle wave (don't spawn supervisor yet)
|
|
104
|
+
if (!directive) {
|
|
105
|
+
state.status = 'stopped';
|
|
106
|
+
console.log(`[Supervisor] Idle wave created: ${waveId} (no directive)`);
|
|
107
|
+
return state;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Save wave file immediately so directive persists across restarts
|
|
111
|
+
this.saveWaveFile(waveId, directive, preset);
|
|
112
|
+
|
|
113
|
+
// Record first directive in wave conversation history (Gap #1 fix)
|
|
114
|
+
appendWaveMessage(waveId, { role: 'user', content: directive });
|
|
115
|
+
|
|
116
|
+
this.spawnSupervisor(state);
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Save wave file immediately so directive persists across restarts.
|
|
122
|
+
* saveCompletedWave() adds session/role details on completion.
|
|
123
|
+
*/
|
|
124
|
+
private saveWaveFile(waveId: string, directive: string, preset?: string): void {
|
|
125
|
+
try {
|
|
126
|
+
const wavesDir = path.join(COMPANY_ROOT, '.tycono', 'waves');
|
|
127
|
+
if (!fs.existsSync(wavesDir)) fs.mkdirSync(wavesDir, { recursive: true });
|
|
128
|
+
const wavePath = path.join(wavesDir, `${waveId}.json`);
|
|
129
|
+
if (!fs.existsSync(wavePath)) {
|
|
130
|
+
const waveData: Record<string, unknown> = {
|
|
131
|
+
id: waveId,
|
|
132
|
+
waveId,
|
|
133
|
+
directive,
|
|
134
|
+
startedAt: new Date().toISOString(),
|
|
135
|
+
sessionIds: [],
|
|
136
|
+
roles: [],
|
|
137
|
+
};
|
|
138
|
+
if (preset) waveData.preset = preset;
|
|
139
|
+
fs.writeFileSync(wavePath, JSON.stringify(waveData, null, 2));
|
|
140
|
+
console.log(`[Supervisor] Wave file created: ${wavePath}`);
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.warn(`[Supervisor] Failed to save wave file for ${waveId}:`, err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Stop the supervisor for a wave (graceful).
|
|
149
|
+
*/
|
|
150
|
+
stop(waveId: string): void {
|
|
151
|
+
const state = this.supervisors.get(waveId);
|
|
152
|
+
if (!state) return;
|
|
153
|
+
|
|
154
|
+
state.status = 'stopped';
|
|
155
|
+
if (state.restartTimer) {
|
|
156
|
+
clearTimeout(state.restartTimer);
|
|
157
|
+
state.restartTimer = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Abort the running execution if any
|
|
161
|
+
if (state.executionId) {
|
|
162
|
+
const exec = executionManager.getExecution(state.executionId);
|
|
163
|
+
if (exec && exec.status === 'running') {
|
|
164
|
+
exec.abort();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`[Supervisor] Stopped for wave ${state.waveId}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Add a CEO directive to be delivered at the next supervisor tick.
|
|
173
|
+
* Dispatch Protocol Principle 2: tick이 유일한 동기화 지점.
|
|
174
|
+
*/
|
|
175
|
+
addDirective(waveId: string, text: string): PendingDirective | null {
|
|
176
|
+
let state = this.supervisors.get(waveId);
|
|
177
|
+
|
|
178
|
+
// If wave not in memory (e.g., server restarted), restore from disk
|
|
179
|
+
if (!state) {
|
|
180
|
+
// Check if this wave existed before (has sessions in session-store)
|
|
181
|
+
const waveSessions = listSessions().filter(s => s.waveId === waveId);
|
|
182
|
+
const ceoSession = waveSessions.find(s => s.roleId === 'ceo') ?? null;
|
|
183
|
+
|
|
184
|
+
// Read original directive + preset from wave artifact file
|
|
185
|
+
let originalDirective = '';
|
|
186
|
+
let originalPreset: string | undefined;
|
|
187
|
+
try {
|
|
188
|
+
const waveFile = path.join(COMPANY_ROOT, '.tycono', 'waves', `${waveId}.json`);
|
|
189
|
+
if (fs.existsSync(waveFile)) {
|
|
190
|
+
const waveData = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
|
|
191
|
+
originalDirective = waveData.directive ?? '';
|
|
192
|
+
originalPreset = waveData.preset;
|
|
193
|
+
}
|
|
194
|
+
} catch { /* ignore */ }
|
|
195
|
+
|
|
196
|
+
if (waveSessions.length > 0 || originalDirective) {
|
|
197
|
+
// Restore supervisor state — from sessions or wave file
|
|
198
|
+
state = {
|
|
199
|
+
waveId,
|
|
200
|
+
directive: originalDirective || text,
|
|
201
|
+
continuous: false,
|
|
202
|
+
preset: originalPreset,
|
|
203
|
+
supervisorSessionId: ceoSession?.id ?? null,
|
|
204
|
+
executionId: null,
|
|
205
|
+
status: 'stopped',
|
|
206
|
+
crashCount: 0,
|
|
207
|
+
maxCrashRetries: 10,
|
|
208
|
+
restartTimer: null,
|
|
209
|
+
cleanupTimer: null,
|
|
210
|
+
pendingDirectives: [],
|
|
211
|
+
pendingQuestions: [],
|
|
212
|
+
createdAt: ceoSession?.createdAt ?? new Date().toISOString(),
|
|
213
|
+
};
|
|
214
|
+
this.supervisors.set(waveId, state);
|
|
215
|
+
console.log(`[Supervisor] Restored wave ${waveId} from disk (${waveSessions.length} sessions, directive=${originalDirective ? 'yes' : 'no'})`);
|
|
216
|
+
} else {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const directive: PendingDirective = {
|
|
222
|
+
id: `dir-${Date.now()}`,
|
|
223
|
+
text,
|
|
224
|
+
createdAt: new Date().toISOString(),
|
|
225
|
+
delivered: false,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
state.pendingDirectives.push(directive);
|
|
229
|
+
console.log(`[Supervisor] Directive queued for wave ${waveId}: ${text.slice(0, 80)}`);
|
|
230
|
+
|
|
231
|
+
// Record user message in wave conversation history
|
|
232
|
+
appendWaveMessage(waveId, { role: 'user', content: text });
|
|
233
|
+
|
|
234
|
+
// If supervisor is stopped (agent finished or idle wave), wake it up
|
|
235
|
+
if (state.status === 'stopped') {
|
|
236
|
+
// Update the wave's directive if it was empty (idle wave first message)
|
|
237
|
+
if (!state.directive) {
|
|
238
|
+
state.directive = text;
|
|
239
|
+
}
|
|
240
|
+
state.crashCount = 0;
|
|
241
|
+
|
|
242
|
+
// Dual Mode: Conversation vs Dispatch
|
|
243
|
+
// AI classifies the directive (Haiku), falls back to regex
|
|
244
|
+
this.classifyDirective(text).then(isConversation => {
|
|
245
|
+
if (isConversation) {
|
|
246
|
+
this.spawnConversation(state, text);
|
|
247
|
+
} else {
|
|
248
|
+
this.scheduleRestart(state, 0);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return directive;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Answer a question from the supervisor.
|
|
258
|
+
*/
|
|
259
|
+
answerQuestion(waveId: string, questionId: string, answer: string): boolean {
|
|
260
|
+
const state = this.supervisors.get(waveId);
|
|
261
|
+
if (!state) return false;
|
|
262
|
+
|
|
263
|
+
const q = state.pendingQuestions.find(q => q.id === questionId);
|
|
264
|
+
if (!q) return false;
|
|
265
|
+
|
|
266
|
+
q.answer = answer;
|
|
267
|
+
q.answeredAt = new Date().toISOString();
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get pending (undelivered) directives for a wave.
|
|
273
|
+
* Called by DigestEngine to include in the supervisor's digest.
|
|
274
|
+
*/
|
|
275
|
+
getPendingDirectives(waveId: string): PendingDirective[] {
|
|
276
|
+
const state = this.supervisors.get(waveId);
|
|
277
|
+
if (!state) return [];
|
|
278
|
+
return state.pendingDirectives.filter(d => !d.delivered);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Mark directives as delivered.
|
|
283
|
+
*/
|
|
284
|
+
markDirectivesDelivered(waveId: string): void {
|
|
285
|
+
const state = this.supervisors.get(waveId);
|
|
286
|
+
if (!state) return;
|
|
287
|
+
for (const d of state.pendingDirectives) {
|
|
288
|
+
d.delivered = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Add a question from the supervisor to CEO.
|
|
294
|
+
*/
|
|
295
|
+
addQuestion(waveId: string, question: string, fromRole: string, context: string): PendingQuestion {
|
|
296
|
+
const state = this.supervisors.get(waveId);
|
|
297
|
+
const q: PendingQuestion = {
|
|
298
|
+
id: `q-${Date.now()}`,
|
|
299
|
+
question,
|
|
300
|
+
fromRole,
|
|
301
|
+
context,
|
|
302
|
+
createdAt: new Date().toISOString(),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (state) {
|
|
306
|
+
state.pendingQuestions.push(q);
|
|
307
|
+
}
|
|
308
|
+
return q;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get unanswered questions for a wave.
|
|
313
|
+
*/
|
|
314
|
+
getUnansweredQuestions(waveId: string): PendingQuestion[] {
|
|
315
|
+
const state = this.supervisors.get(waveId);
|
|
316
|
+
if (!state) return [];
|
|
317
|
+
return state.pendingQuestions.filter(q => !q.answer);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get the state for a wave.
|
|
322
|
+
*/
|
|
323
|
+
getState(waveId: string): SupervisorState | undefined {
|
|
324
|
+
return this.supervisors.get(waveId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* List all active supervisor states.
|
|
329
|
+
*/
|
|
330
|
+
listActive(): SupervisorState[] {
|
|
331
|
+
return Array.from(this.supervisors.values())
|
|
332
|
+
.filter(s => s.status === 'running' || s.status === 'starting' || s.status === 'restarting');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ─── Internal: Dual Mode ─────────────────── */
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Classify: is this directive a question/status check (conversation)
|
|
339
|
+
* or a work task (needs dispatch)?
|
|
340
|
+
*
|
|
341
|
+
* Uses Haiku LLM for classification when ANTHROPIC_API_KEY is available.
|
|
342
|
+
* Falls back to regex heuristic when no API key.
|
|
343
|
+
*/
|
|
344
|
+
private static readonly CLASSIFY_SYSTEM = `You classify user messages to a CEO AI assistant.
|
|
345
|
+
Reply with exactly one character:
|
|
346
|
+
- "Q" if the message is a question, status check, or casual conversation (no action needed)
|
|
347
|
+
- "T" if the message is a task, work instruction, delegation request, or any directive that requires action (creating, building, fixing, assigning work to team members, etc.)
|
|
348
|
+
|
|
349
|
+
Examples:
|
|
350
|
+
"뭐 했어?" → Q
|
|
351
|
+
"CBO한테 일 줘" → T
|
|
352
|
+
"시장 조사해" → T
|
|
353
|
+
"현재 상태 알려줘" → Q
|
|
354
|
+
"게임 만들어 CTO에게 시켜" → T
|
|
355
|
+
"how's it going?" → Q
|
|
356
|
+
"deploy the app" → T`;
|
|
357
|
+
|
|
358
|
+
private async classifyDirective(text: string): Promise<boolean> {
|
|
359
|
+
// Try AI classification (fast, accurate, language-agnostic)
|
|
360
|
+
try {
|
|
361
|
+
const config = readConfig(COMPANY_ROOT);
|
|
362
|
+
const engine = config.engine || process.env.EXECUTION_ENGINE || 'claude-cli';
|
|
363
|
+
|
|
364
|
+
let reply: string;
|
|
365
|
+
if (engine === 'claude-cli') {
|
|
366
|
+
// Claude CLI (Claude Max) — use claude -p with haiku model
|
|
367
|
+
const provider = new ClaudeCliProvider({ model: 'claude-haiku-4-5-20251001' });
|
|
368
|
+
const response = await provider.chat(
|
|
369
|
+
SupervisorHeartbeat.CLASSIFY_SYSTEM,
|
|
370
|
+
[{ role: 'user', content: text }],
|
|
371
|
+
);
|
|
372
|
+
reply = response.content.find(c => c.type === 'text')?.text?.trim() ?? '';
|
|
373
|
+
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
374
|
+
// BYOK — use Anthropic SDK directly
|
|
375
|
+
const client = new Anthropic();
|
|
376
|
+
const response = await client.messages.create({
|
|
377
|
+
model: 'claude-haiku-4-5-20251001',
|
|
378
|
+
max_tokens: 1,
|
|
379
|
+
system: SupervisorHeartbeat.CLASSIFY_SYSTEM,
|
|
380
|
+
messages: [{ role: 'user', content: text }],
|
|
381
|
+
});
|
|
382
|
+
reply = (response.content[0] as { type: 'text'; text: string }).text.trim();
|
|
383
|
+
} else {
|
|
384
|
+
// No engine available — regex fallback
|
|
385
|
+
return this.isConversationDirectiveFallback(text);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const isConversation = reply === 'Q';
|
|
389
|
+
console.log(`[Supervisor] AI classified "${text.slice(0, 40)}" as ${isConversation ? 'conversation' : 'task'} (engine=${engine})`);
|
|
390
|
+
return isConversation;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.warn(`[Supervisor] AI classification failed, falling back to regex:`, err);
|
|
393
|
+
return this.isConversationDirectiveFallback(text);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private isConversationDirectiveFallback(text: string): boolean {
|
|
398
|
+
const t = text.trim();
|
|
399
|
+
|
|
400
|
+
// Short messages with question marks → conversation
|
|
401
|
+
if (t.includes('?') && t.length < 100) return true;
|
|
402
|
+
|
|
403
|
+
// Task patterns — action verbs override
|
|
404
|
+
const taskPatterns = [
|
|
405
|
+
/만들어/, /구현해/, /개발해/, /수정해/, /변경해/, /리팩토링/,
|
|
406
|
+
/설계해/, /작성해/, /배포해/, /테스트해/, /고쳐/, /해줘/, /해봐/,
|
|
407
|
+
/진행시켜/, /진행해/, /시작해/, /실행해/, /돌려/,
|
|
408
|
+
/시켜/, /맡겨/, /일\s*줘/, /지시해/, /분석해/, /조사해/, /검토해/,
|
|
409
|
+
/에게\s*(시|맡|줘)/, /한테\s*(시|맡|줘)/,
|
|
410
|
+
/build/i, /create/i, /implement/i, /develop/i, /fix/i, /deploy/i, /refactor/i,
|
|
411
|
+
/proceed/i, /start/i, /execute/i, /run/i, /do it/i, /go ahead/i,
|
|
412
|
+
/assign/i, /dispatch/i, /delegate/i, /tell.*to/i, /ask.*to/i,
|
|
413
|
+
];
|
|
414
|
+
if (taskPatterns.some(p => p.test(t))) return false;
|
|
415
|
+
|
|
416
|
+
// Default: short → conversation, long → dispatch
|
|
417
|
+
return t.length < 60;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Spawn a lightweight conversation session (no dispatch tools).
|
|
422
|
+
* CEO reads files and answers directly.
|
|
423
|
+
*/
|
|
424
|
+
/**
|
|
425
|
+
* Load conversation history from activity-stream files for a wave.
|
|
426
|
+
* Used when supervisor restarts (e.g., TUI restarted) to restore context.
|
|
427
|
+
*/
|
|
428
|
+
private loadWaveHistory(waveId: string): string {
|
|
429
|
+
try {
|
|
430
|
+
// Find CEO sessions for this wave from session-store
|
|
431
|
+
const allSessions = listSessions();
|
|
432
|
+
console.log(`[WaveHistory] Loading for wave=${waveId}, total sessions=${allSessions.length}`);
|
|
433
|
+
const waveCeoSessions = allSessions
|
|
434
|
+
.filter(s => s.waveId === waveId && s.roleId === 'ceo')
|
|
435
|
+
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
436
|
+
|
|
437
|
+
console.log(`[WaveHistory] Found ${waveCeoSessions.length} CEO sessions for wave=${waveId}: ${waveCeoSessions.map(s => s.id).join(', ')}`);
|
|
438
|
+
if (waveCeoSessions.length === 0) return '';
|
|
439
|
+
|
|
440
|
+
const exchanges: Array<{ role: 'ceo' | 'supervisor'; text: string }> = [];
|
|
441
|
+
for (const ses of waveCeoSessions.slice(-3)) {
|
|
442
|
+
if (!ActivityStream.exists(ses.id)) continue;
|
|
443
|
+
const events = ActivityStream.readAll(ses.id);
|
|
444
|
+
|
|
445
|
+
let currentText = '';
|
|
446
|
+
for (const e of events) {
|
|
447
|
+
// New turn starts — flush accumulated text from previous turn
|
|
448
|
+
if (e.type === 'msg:start') {
|
|
449
|
+
if (currentText.trim()) {
|
|
450
|
+
exchanges.push({ role: 'supervisor', text: currentText.trim().slice(0, 500) });
|
|
451
|
+
currentText = '';
|
|
452
|
+
}
|
|
453
|
+
// Extract CEO directive
|
|
454
|
+
const task = String(e.data.task ?? '');
|
|
455
|
+
const match = task.match(/\[CEO (?:Supervisor|Question)\]\s*(.*?)(?:\n|$)/);
|
|
456
|
+
if (match) {
|
|
457
|
+
exchanges.push({ role: 'ceo', text: match[1].slice(0, 200) });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Accumulate supervisor response text
|
|
461
|
+
if (e.type === 'text' && e.roleId === 'ceo') {
|
|
462
|
+
const text = String(e.data.text ?? '').trim();
|
|
463
|
+
if (text && !text.startsWith('#') && !text.startsWith('\u26D4')) {
|
|
464
|
+
currentText += text + ' ';
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Turn boundary — flush
|
|
468
|
+
if (e.type === 'msg:done' || e.type === 'msg:awaiting_input' || e.type === 'msg:error') {
|
|
469
|
+
if (currentText.trim()) {
|
|
470
|
+
exchanges.push({ role: 'supervisor', text: currentText.trim().slice(0, 500) });
|
|
471
|
+
currentText = '';
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Flush any remaining text
|
|
476
|
+
if (currentText.trim()) {
|
|
477
|
+
exchanges.push({ role: 'supervisor', text: currentText.trim().slice(0, 500) });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (exchanges.length === 0) return '';
|
|
482
|
+
|
|
483
|
+
// Keep last 10 exchanges (5 Q&A pairs), cap total at 3000 chars
|
|
484
|
+
const recent = exchanges.slice(-10);
|
|
485
|
+
const formatted = recent.map(e =>
|
|
486
|
+
e.role === 'ceo' ? `CEO: "${e.text}"` : `→ ${e.text}`
|
|
487
|
+
).join('\n');
|
|
488
|
+
|
|
489
|
+
const result = formatted.length > 3000
|
|
490
|
+
? `\n[Previous conversation]\n${formatted.slice(-3000)}\n`
|
|
491
|
+
: `\n[Previous conversation]\n${formatted}\n`;
|
|
492
|
+
console.log(`[WaveHistory] Result (${result.length} chars): ${result.slice(0, 200)}`);
|
|
493
|
+
return result;
|
|
494
|
+
} catch {
|
|
495
|
+
return '';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private spawnConversation(state: SupervisorState, directive: string): void {
|
|
500
|
+
// Build conversation history from SQLite (all previous turns in this wave)
|
|
501
|
+
const history = buildHistoryPrompt(state.waveId);
|
|
502
|
+
|
|
503
|
+
const task = `${history}
|
|
504
|
+
|
|
505
|
+
[CEO Question] ${directive}
|
|
506
|
+
|
|
507
|
+
You are the CEO Supervisor responding to the CEO's follow-up question.
|
|
508
|
+
|
|
509
|
+
## Rules
|
|
510
|
+
1. The conversation history above contains the FULL context of this wave. Use it.
|
|
511
|
+
2. **Be concrete.** Use actual data, numbers, quotes. The CEO wants substance, not metadata.
|
|
512
|
+
3. **READ files** if you need more detail on deliverables (knowledge/, roles/*/journal/).
|
|
513
|
+
4. Do NOT dispatch anyone. Do NOT create new files. This is a conversation.
|
|
514
|
+
5. Answer in the same language the CEO used.`;
|
|
515
|
+
|
|
516
|
+
// Reuse session
|
|
517
|
+
let sessionId = state.supervisorSessionId;
|
|
518
|
+
if (!sessionId || !getSession(sessionId)) {
|
|
519
|
+
const session = createSession('ceo', {
|
|
520
|
+
mode: 'do',
|
|
521
|
+
source: 'wave',
|
|
522
|
+
waveId: state.waveId,
|
|
523
|
+
});
|
|
524
|
+
sessionId = session.id;
|
|
525
|
+
state.supervisorSessionId = sessionId;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
state.status = 'running';
|
|
529
|
+
|
|
530
|
+
// Cancel pending cleanup timer — wave is active again
|
|
531
|
+
if (state.cleanupTimer) {
|
|
532
|
+
clearTimeout(state.cleanupTimer);
|
|
533
|
+
state.cleanupTimer = null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const exec = executionManager.startExecution({
|
|
538
|
+
type: 'assign', // assign = no supervisor tools (dispatch/watch/amend)
|
|
539
|
+
roleId: 'ceo',
|
|
540
|
+
task,
|
|
541
|
+
sourceRole: 'ceo',
|
|
542
|
+
sessionId,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
state.executionId = exec.id;
|
|
546
|
+
this.watchExecution(state, exec);
|
|
547
|
+
|
|
548
|
+
console.log(`[Supervisor] Conversation mode for wave ${state.waveId} | directive: ${directive.slice(0, 60)}`);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
console.error(`[Supervisor] Conversation spawn failed:`, err);
|
|
551
|
+
// Fallback to full supervisor
|
|
552
|
+
this.scheduleRestart(state, 0);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* ─── Internal: Spawn / Restart ────────────── */
|
|
557
|
+
|
|
558
|
+
private spawnSupervisor(state: SupervisorState): void {
|
|
559
|
+
// Use latest pending directive as the active task (not the wave's initial directive)
|
|
560
|
+
const undelivered = state.pendingDirectives.filter(d => !d.delivered);
|
|
561
|
+
if (undelivered.length > 0) {
|
|
562
|
+
state.directive = undelivered[undelivered.length - 1].text;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const orgTree = buildOrgTree(COMPANY_ROOT, state.preset);
|
|
566
|
+
let cLevelRoles = getSubordinates(orgTree, 'ceo');
|
|
567
|
+
|
|
568
|
+
if (state.targetRoles && state.targetRoles.length > 0) {
|
|
569
|
+
const allowed = new Set(state.targetRoles);
|
|
570
|
+
cLevelRoles = cLevelRoles.filter(r => allowed.has(r));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (cLevelRoles.length === 0) {
|
|
574
|
+
console.error(`[Supervisor] No C-Level roles found for wave ${state.waveId}`);
|
|
575
|
+
state.status = 'error';
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Build the supervisor task prompt
|
|
580
|
+
const cLevelList = cLevelRoles.map(r => {
|
|
581
|
+
const node = orgTree.nodes.get(r);
|
|
582
|
+
const name = node?.name ?? r;
|
|
583
|
+
const subs = getSubordinates(orgTree, r);
|
|
584
|
+
return `- **${name}** (\`${r}\`): ${subs.length} subordinates [${subs.join(', ')}]`;
|
|
585
|
+
}).join('\n');
|
|
586
|
+
|
|
587
|
+
const isRecovery = state.crashCount > 0;
|
|
588
|
+
const recoveryContext = isRecovery
|
|
589
|
+
? `\n\n⚠️ [RECOVERY] This is a restart after crash #${state.crashCount}. Check all session states via supervision watch.`
|
|
590
|
+
: '';
|
|
591
|
+
|
|
592
|
+
// Build conversation context from wave-messages DB (Gap #2 fix)
|
|
593
|
+
const conversationHistory = buildHistoryPrompt(state.waveId);
|
|
594
|
+
|
|
595
|
+
const supervisorTask = `[CEO Supervisor] ${state.directive}
|
|
596
|
+
${conversationHistory ? '\n' + conversationHistory + '\n' : ''}
|
|
597
|
+
## Your Role
|
|
598
|
+
You are the CEO Supervisor — the CEO's AI proxy.
|
|
599
|
+
You can answer questions directly OR dispatch C-Level roles for complex work.
|
|
600
|
+
|
|
601
|
+
## Response Mode Decision (BEFORE dispatching)
|
|
602
|
+
|
|
603
|
+
⛔ Dispatch is expensive (spawns entire teams). Judge first:
|
|
604
|
+
|
|
605
|
+
**1. Direct Answer** — Can YOU handle this without dispatching?
|
|
606
|
+
- Status check, progress report → Read files/docs yourself, answer directly
|
|
607
|
+
- Simple question → Answer directly
|
|
608
|
+
- Opinion request → Answer directly
|
|
609
|
+
- Clarification on previous work → Answer from context
|
|
610
|
+
→ **Do NOT dispatch. Just answer.**
|
|
611
|
+
|
|
612
|
+
**2. Selective Dispatch** — Only specific C-Level(s) needed?
|
|
613
|
+
- "코드 수정해" → CTO only
|
|
614
|
+
- "디자인 개선해" → CBO only
|
|
615
|
+
- "테스트해봐" → CTO only (who dispatches QA)
|
|
616
|
+
→ **Dispatch only the relevant C-Level(s).**
|
|
617
|
+
|
|
618
|
+
**3. Full Dispatch** — Multi-team collaboration required?
|
|
619
|
+
- "새 기능 만들어" → CTO + CBO
|
|
620
|
+
- "출시 준비해" → All C-Levels
|
|
621
|
+
→ **Dispatch multiple C-Levels with clear tasks.**
|
|
622
|
+
|
|
623
|
+
**Default: Direct Answer first. Dispatch only when code changes or creative work is needed.**
|
|
624
|
+
|
|
625
|
+
## Available C-Level Roles
|
|
626
|
+
${cLevelList}
|
|
627
|
+
|
|
628
|
+
## Dispatch Protocol (6 Principles)
|
|
629
|
+
1. **Universal Loop**: dispatch → watch → react → repeat (same as all supervisors)
|
|
630
|
+
2. **Tick = sync point**: All events processed at watch tick boundary
|
|
631
|
+
3. **Priority**: CEO directive > crash > abort > peer opinion > subordinate question > status
|
|
632
|
+
4. **Hierarchy**: Only dispatch your direct reports. Relay opinions, don't create shortcuts
|
|
633
|
+
5. **Done condition**: ALL subordinates must be done before you report done
|
|
634
|
+
6. **Crash resilience**: If you restart, digest catches you up
|
|
635
|
+
|
|
636
|
+
## Supervisor Guidelines
|
|
637
|
+
- G-01: After amending a C-Level, verify on next tick that they reflected it. If not, escalate to CEO directive priority.
|
|
638
|
+
- G-02: If a C-Level crashes 3+ times consecutively, stop dispatching them and report to CEO.
|
|
639
|
+
- G-03: Multiple CEO directives in same tick → apply the latest one only.
|
|
640
|
+
- G-04: If you dispatch the same role 3+ times with no progress, intervene: "specify what's wrong concretely."
|
|
641
|
+
- G-05: abort = graceful amend ("wrap up and stop"). Not a hard kill.
|
|
642
|
+
- G-06: If two sessions show no events for 3+ minutes, suspect deadlock → re-sequence their work.
|
|
643
|
+
- G-07: **Cross-team relay is YOUR job.** When a C-Level completes, immediately amend the other active C-Levels with a summary of the completed work. Example: CBO finishes game design → amend CTO: "CBO delivered game design docs. Key decisions: [summary]. Review and align your implementation."
|
|
644
|
+
- G-08: Don't just watch passively. On every tick, ask: "Does any active C-Level need information from a completed C-Level?" If yes, amend with the relevant context.
|
|
645
|
+
|
|
646
|
+
## Cross-Team Relay Protocol (CRITICAL)
|
|
647
|
+
⛔ C-Levels do NOT talk to each other directly. YOU are the relay.
|
|
648
|
+
|
|
649
|
+
When C-Level A completes while C-Level B is still active:
|
|
650
|
+
1. Review A's deliverables (read their committed files or final report)
|
|
651
|
+
2. Summarize the key decisions, artifacts, and constraints from A's work
|
|
652
|
+
3. amend B: "C-Level A completed. Here are their deliverables relevant to your work: [summary]. Review and incorporate."
|
|
653
|
+
4. On next tick, verify B acknowledged and reflected A's input
|
|
654
|
+
|
|
655
|
+
When C-Level A produces intermediate results that B needs:
|
|
656
|
+
1. amend B with the relevant intermediate output
|
|
657
|
+
2. You don't need to wait for A to finish — relay as results become available
|
|
658
|
+
|
|
659
|
+
Examples:
|
|
660
|
+
- CBO finishes game design → amend CTO: "CBO delivered: world-building doc, 15 monster specs, quest design, UI guidelines. Ensure implementation matches these specs."
|
|
661
|
+
- CTO's engineer creates API schema → amend CBO: "CTO's team defined the data schema. Here's the structure: [summary]. Adjust business docs if needed."
|
|
662
|
+
- Designer finishes UI guide → relay to CTO team: "Designer's UI guide is ready at [path]. Frontend implementation should follow these specs."
|
|
663
|
+
|
|
664
|
+
## CEO Directive Channel
|
|
665
|
+
If new CEO directives arrive mid-execution, they will appear in your supervision watch digest
|
|
666
|
+
marked as [CEO DIRECTIVE]. These are PRIORITY 1 — process before anything else.
|
|
667
|
+
${recoveryContext}
|
|
668
|
+
|
|
669
|
+
## Quality Gate (CRITICAL — G-09)
|
|
670
|
+
⛔ **"Subordinate said done" ≠ "Work is actually done."**
|
|
671
|
+
⛔ **"Code exists" ≠ "Code works."**
|
|
672
|
+
|
|
673
|
+
⛔ **You are a SUPERVISOR. You do NOT run code, servers, npm install, or builds yourself.**
|
|
674
|
+
⛔ **Dispatch QA (Tester) to run and test. Read QA's report to judge quality.**
|
|
675
|
+
|
|
676
|
+
Before declaring yourself done, you MUST:
|
|
677
|
+
|
|
678
|
+
1. **Read the actual output files** — don't trust status reports. Check the code yourself.
|
|
679
|
+
2. **Dispatch QA to test it** — QA runs the server, opens browser, clicks buttons, reports bugs.
|
|
680
|
+
- Do NOT run \`npm install\`, \`npm run build\`, \`python3 -m http.server\` yourself.
|
|
681
|
+
- Do NOT run \`agent-browser\` yourself. That is QA's job.
|
|
682
|
+
- Your job: read QA's report, decide if it passes, re-dispatch if not.
|
|
683
|
+
3. **Count against requirements** — if the directive says "15 monsters, 7 maps", count them.
|
|
684
|
+
4. **Check the directive's specific tech requirements** — if it mentions a specific library/engine, verify it's actually used in the code (grep for it).
|
|
685
|
+
5. **If quality is insufficient → re-dispatch** with specific, actionable feedback:
|
|
686
|
+
- "Arrow keys don't move the player. Fix input handling in WorldScene."
|
|
687
|
+
- "TyconoForge was required but not used. Add character rendering with TyconoForge.render()."
|
|
688
|
+
- NOT vague feedback like "improve quality" or "make it better"
|
|
689
|
+
6. **Iterate until the directive is truly fulfilled.** There is NO time limit.
|
|
690
|
+
20,000 lines of non-working code is worse than 5,000 lines that actually play.
|
|
691
|
+
|
|
692
|
+
Re-dispatch pattern:
|
|
693
|
+
- dispatch same C-Level with specific gaps identified
|
|
694
|
+
- Each iteration should close specific gaps, not redo everything
|
|
695
|
+
- Maximum 5 iterations per C-Level before escalating
|
|
696
|
+
|
|
697
|
+
${state.continuous ? `## Continuous Improvement Mode (ON)
|
|
698
|
+
⛔ 첫 번째 완성 후 멈추지 마라.
|
|
699
|
+
1. 모든 subordinate done + QA pass 후, 즉시 CBO에게 "사용자 관점에서 부족한 점, 개선할 점"을 물어라.
|
|
700
|
+
2. CTO에게 "기술적으로 개선할 점, 코드 품질, 성능"을 물어라.
|
|
701
|
+
3. CBO/CTO 의견을 종합하여 의미있는 개선이 있으면 re-dispatch.
|
|
702
|
+
4. 이 사이클을 반복하라. 각 iteration마다 게임/산출물이 점점 나아져야 한다.
|
|
703
|
+
5. 사용자가 Stop을 누를 때까지 계속한다. 스스로 done 선언하지 마라.
|
|
704
|
+
|
|
705
|
+
` : ''}## Instructions
|
|
706
|
+
1. **First: Apply Response Mode Decision** — Can you answer directly? If yes, answer and report done.
|
|
707
|
+
2. If dispatch is needed: decide which C-Level roles (not necessarily all)
|
|
708
|
+
3. Dispatch with clear, specific tasks
|
|
709
|
+
4. Enter supervision watch loop
|
|
710
|
+
5. Monitor, **actively relay results between teams**, course-correct
|
|
711
|
+
6. When subordinates report done → **verify deliverables against requirements (G-09)**
|
|
712
|
+
7. If gaps exist → re-dispatch with specific feedback. Repeat 4-6.
|
|
713
|
+
8. Only when ALL requirements are met → compile results and report`;
|
|
714
|
+
|
|
715
|
+
// BUG-008 fix: Wave:Supervisor:Session = 1:1:1 invariant.
|
|
716
|
+
// Reuse existing session on restart instead of creating a new one.
|
|
717
|
+
let sessionId = state.supervisorSessionId;
|
|
718
|
+
if (sessionId && getSession(sessionId)) {
|
|
719
|
+
console.log(`[Supervisor] Reusing existing session ${sessionId} for wave ${state.waveId}`);
|
|
720
|
+
} else {
|
|
721
|
+
const session = createSession('ceo', {
|
|
722
|
+
mode: 'do',
|
|
723
|
+
source: 'wave',
|
|
724
|
+
waveId: state.waveId,
|
|
725
|
+
});
|
|
726
|
+
sessionId = session.id;
|
|
727
|
+
state.supervisorSessionId = sessionId;
|
|
728
|
+
|
|
729
|
+
// Add the directive as CEO message so the session isn't empty (prevents deleteEmpty cleanup)
|
|
730
|
+
const ceoMsg: Message = {
|
|
731
|
+
id: `msg-${Date.now()}-ceo-supervisor`,
|
|
732
|
+
from: 'ceo',
|
|
733
|
+
content: state.directive,
|
|
734
|
+
type: 'directive',
|
|
735
|
+
status: 'done',
|
|
736
|
+
timestamp: new Date().toISOString(),
|
|
737
|
+
};
|
|
738
|
+
addMessage(sessionId, ceoMsg);
|
|
739
|
+
}
|
|
740
|
+
state.status = 'running';
|
|
741
|
+
|
|
742
|
+
// Cancel pending cleanup timer — wave is active again
|
|
743
|
+
if (state.cleanupTimer) {
|
|
744
|
+
clearTimeout(state.cleanupTimer);
|
|
745
|
+
state.cleanupTimer = null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
const exec = executionManager.startExecution({
|
|
750
|
+
type: 'wave',
|
|
751
|
+
roleId: 'ceo',
|
|
752
|
+
task: supervisorTask,
|
|
753
|
+
sourceRole: 'ceo',
|
|
754
|
+
targetRoles: state.targetRoles,
|
|
755
|
+
sessionId,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
state.executionId = exec.id;
|
|
759
|
+
|
|
760
|
+
this.watchExecution(state, exec);
|
|
761
|
+
|
|
762
|
+
console.log(`[Supervisor] Started for wave ${state.waveId} | session=${sessionId} | exec=${exec.id}`);
|
|
763
|
+
} catch (err) {
|
|
764
|
+
console.error(`[Supervisor] Failed to start for wave ${state.waveId}:`, err);
|
|
765
|
+
state.status = 'error';
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private watchExecution(state: SupervisorState, exec: Execution): void {
|
|
770
|
+
const subscriber = (event: { type: string; data: Record<string, unknown> }) => {
|
|
771
|
+
if (event.type === 'msg:done') {
|
|
772
|
+
exec.stream.unsubscribe(subscriber);
|
|
773
|
+
this.onSupervisorDone(state);
|
|
774
|
+
} else if (event.type === 'msg:error') {
|
|
775
|
+
exec.stream.unsubscribe(subscriber);
|
|
776
|
+
this.onSupervisorCrash(state, String(event.data.message ?? 'unknown error'));
|
|
777
|
+
} else if (event.type === 'msg:awaiting_input') {
|
|
778
|
+
// BUG-016: turn:limit causes awaiting_input — treat as done-guard
|
|
779
|
+
// If all children are done → complete wave. Otherwise restart supervisor.
|
|
780
|
+
exec.stream.unsubscribe(subscriber);
|
|
781
|
+
console.log(`[Supervisor] awaiting_input (turn limit) for wave ${state.waveId}. Running done-guard.`);
|
|
782
|
+
this.onSupervisorDone(state);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
exec.stream.subscribe(subscriber);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private onSupervisorDone(state: SupervisorState): void {
|
|
790
|
+
// Check if there are still running or paused C-Level sessions for this wave
|
|
791
|
+
const waveSessions = listSessions().filter(s => s.waveId === state.waveId && s.id !== state.supervisorSessionId);
|
|
792
|
+
const runningChildren = waveSessions.filter(s => {
|
|
793
|
+
const exec = executionManager.getActiveExecution(s.id);
|
|
794
|
+
return exec && exec.status === 'running';
|
|
795
|
+
});
|
|
796
|
+
const awaitingChildren = waveSessions.filter(s => {
|
|
797
|
+
const exec = executionManager.getActiveExecution(s.id);
|
|
798
|
+
return exec && exec.status === 'awaiting_input';
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
if (awaitingChildren.length > 0) {
|
|
802
|
+
// Auto-continue children that hit turn limit (using --resume for context continuity)
|
|
803
|
+
console.log(`[Supervisor] ${awaitingChildren.length} children awaiting_input (turn limit). Auto-continuing.`);
|
|
804
|
+
for (const session of awaitingChildren) {
|
|
805
|
+
executionManager.continueSession(session.id, '턴 한도에 도달했습니다. 이전 작업을 이어서 계속 진행하세요.');
|
|
806
|
+
}
|
|
807
|
+
// Restart supervisor to watch the resumed children
|
|
808
|
+
state.crashCount = 0;
|
|
809
|
+
this.scheduleRestart(state, 5_000);
|
|
810
|
+
} else if (runningChildren.length > 0) {
|
|
811
|
+
// Principle 5: can't be done with running children → restart supervisor
|
|
812
|
+
console.log(`[Supervisor] Done but ${runningChildren.length} children still running. Restarting.`);
|
|
813
|
+
state.crashCount = 0; // Not a crash, intentional restart
|
|
814
|
+
this.scheduleRestart(state, 5_000); // 5s delay
|
|
815
|
+
} else if (state.continuous) {
|
|
816
|
+
// Continuous Improvement Mode: don't stop — restart supervisor to ask C-Levels for improvements
|
|
817
|
+
console.log(`[Supervisor] Wave ${state.waveId} iteration complete. Continuous mode ON — restarting for next improvement cycle.`);
|
|
818
|
+
state.crashCount = 0;
|
|
819
|
+
this.scheduleRestart(state, 5_000);
|
|
820
|
+
} else {
|
|
821
|
+
console.log(`[Supervisor] Wave ${state.waveId} complete. All subordinates done.`);
|
|
822
|
+
state.status = 'stopped';
|
|
823
|
+
|
|
824
|
+
// Record assistant response in wave conversation history
|
|
825
|
+
if (state.executionId) {
|
|
826
|
+
const exec = executionManager.getExecution(state.executionId);
|
|
827
|
+
if (exec?.result?.output) {
|
|
828
|
+
appendWaveMessage(state.waveId, {
|
|
829
|
+
role: 'assistant',
|
|
830
|
+
content: exec.result.output,
|
|
831
|
+
executionId: state.executionId,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Auto-save the completed wave to .tycono/waves/
|
|
837
|
+
try {
|
|
838
|
+
const result = saveCompletedWave(state.waveId, state.directive);
|
|
839
|
+
if (result.ok) {
|
|
840
|
+
console.log(`[Supervisor] Wave auto-saved: ${result.path}`);
|
|
841
|
+
} else {
|
|
842
|
+
console.warn(`[Supervisor] Wave auto-save returned no result for ${state.waveId}`);
|
|
843
|
+
}
|
|
844
|
+
} catch (err) {
|
|
845
|
+
console.error(`[Supervisor] Failed to auto-save wave ${state.waveId}:`, err);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// OOM prevention: clear accumulated state + wave multiplexer sessions
|
|
849
|
+
state.pendingDirectives = [];
|
|
850
|
+
state.pendingQuestions = [];
|
|
851
|
+
|
|
852
|
+
// Delayed cleanup: remove wave sessions from multiplexer + supervisor map
|
|
853
|
+
// (delay allows SSE clients to receive final events)
|
|
854
|
+
// Cancel previous cleanup timer if exists (new directive may restart wave)
|
|
855
|
+
if (state.cleanupTimer) clearTimeout(state.cleanupTimer);
|
|
856
|
+
state.cleanupTimer = setTimeout(() => {
|
|
857
|
+
state.cleanupTimer = null;
|
|
858
|
+
waveMultiplexer.cleanupWave(state.waveId);
|
|
859
|
+
this.supervisors.delete(state.waveId);
|
|
860
|
+
console.log(`[Supervisor] Cleaned up wave ${state.waveId} from memory`);
|
|
861
|
+
}, 60_000);
|
|
862
|
+
state.cleanupTimer.unref();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private onSupervisorCrash(state: SupervisorState, error: string): void {
|
|
867
|
+
if (state.status === 'stopped') return; // Intentional stop
|
|
868
|
+
|
|
869
|
+
state.crashCount++;
|
|
870
|
+
console.log(`[Supervisor] Crash #${state.crashCount} for wave ${state.waveId}: ${error}`);
|
|
871
|
+
|
|
872
|
+
if (state.crashCount >= state.maxCrashRetries) {
|
|
873
|
+
console.error(`[Supervisor] Max retries (${state.maxCrashRetries}) reached for wave ${state.waveId}. Giving up.`);
|
|
874
|
+
state.status = 'error';
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Principle 6: restart with exponential backoff (max 30s)
|
|
879
|
+
const delay = Math.min(10_000 * Math.pow(1.5, state.crashCount - 1), 30_000);
|
|
880
|
+
this.scheduleRestart(state, delay);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private scheduleRestart(state: SupervisorState, delayMs: number): void {
|
|
884
|
+
state.status = 'restarting';
|
|
885
|
+
console.log(`[Supervisor] Scheduling restart for wave ${state.waveId} in ${delayMs}ms`);
|
|
886
|
+
|
|
887
|
+
state.restartTimer = setTimeout(() => {
|
|
888
|
+
state.restartTimer = null;
|
|
889
|
+
if (state.status !== 'restarting') return; // Cancelled
|
|
890
|
+
this.spawnSupervisor(state);
|
|
891
|
+
}, delayMs);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* ─── Singleton ──────────────────────────────── */
|
|
896
|
+
|
|
897
|
+
export const supervisorHeartbeat = new SupervisorHeartbeat();
|