tycono 0.1.94-beta.1 → 0.1.94-beta.10
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/package.json +3 -2
- package/src/api/src/create-server.ts +2 -1
- package/src/api/src/engine/context-assembler.ts +7 -1
- package/src/api/src/engine/runners/claude-cli.ts +2 -3
- package/src/api/src/routes/execute.ts +76 -188
- package/src/api/src/routes/sessions.ts +2 -0
- package/src/api/src/services/session-store.ts +20 -2
- package/src/api/src/services/supervisor-heartbeat.ts +97 -14
- package/src/api/src/services/wave-tracker.ts +175 -0
- package/src/web/dist/assets/index-B8yxzPmd.css +1 -0
- package/src/web/dist/assets/index-C9U34tT1.js +138 -0
- package/src/web/dist/assets/index-DuB5baFp.js +1 -0
- package/src/web/dist/assets/preview-app-QWV7zpb0.js +1 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/tyconoforge.js +1 -0
- package/src/web/dist/assets/index-BnhRwHgb.css +0 -1
- package/src/web/dist/assets/index-C69_ijxi.js +0 -116
- package/src/web/dist/assets/index-Dui2gg0N.js +0 -1
- package/src/web/dist/assets/preview-app-ClsM1ZiD.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tycono",
|
|
3
|
-
"version": "0.1.94-beta.
|
|
3
|
+
"version": "0.1.94-beta.10",
|
|
4
4
|
"description": "Build an AI company. Watch them work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"dev:web": "npm run dev --prefix src/web",
|
|
24
24
|
"build": "npm run build:web && npm run build:forge",
|
|
25
25
|
"build:web": "npm run build --prefix src/web",
|
|
26
|
-
"build:forge": "
|
|
26
|
+
"build:forge": "cp node_modules/tyconoforge/dist/tyconoforge.js src/web/dist/tyconoforge.js",
|
|
27
27
|
"typecheck": "npm run typecheck:api && npm run typecheck:web",
|
|
28
28
|
"typecheck:api": "cd src/api && npx tsc --noEmit",
|
|
29
29
|
"typecheck:web": "cd src/web && npx tsc --noEmit",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"gray-matter": "^4.0.3",
|
|
39
39
|
"marked": "^15.0.6",
|
|
40
40
|
"tsx": "^4.19.3",
|
|
41
|
+
"tyconoforge": "^0.1.0-beta.0",
|
|
41
42
|
"yaml": "^2.7.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
@@ -128,7 +128,8 @@ export function createHttpServer(): http.Server {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// SSE 엔드포인트: Express 우회하여 raw HTTP로 처리
|
|
131
|
-
|
|
131
|
+
// BUG-008: /api/waves/:waveId/directive and /api/waves/:waveId/question POST도 포함
|
|
132
|
+
if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url.startsWith('/api/waves/') || url === '/api/waves/save' || url === '/api/setup/import-knowledge') && method === 'POST') {
|
|
132
133
|
setExecCors(req, res);
|
|
133
134
|
if (url === '/api/setup/import-knowledge') {
|
|
134
135
|
handleImportKnowledge(req, res);
|
|
@@ -743,8 +743,14 @@ function buildSupervisionSection(node: OrgNode): string {
|
|
|
743
743
|
- ✅ **Peer consult**: Unsure about business/market direction? → \`python3 "$CONSULT_CMD" cbo "question"\`
|
|
744
744
|
- ⚠️ **Course correct**: Wrong direction → \`python3 "$SUPERVISION_CMD" amend <ses-id> "new instruction"\`
|
|
745
745
|
- 🛑 **Abort**: Seriously wrong → \`python3 "$SUPERVISION_CMD" abort <ses-id> --reason "why"\`
|
|
746
|
-
- ✅ **All done
|
|
746
|
+
- ✅ **All done?** → Before reporting done, **verify deliverables** (see Quality Gate below)
|
|
747
747
|
4. **Repeat** watch until all subordinates complete. Do NOT stop after one tick.
|
|
748
|
+
5. **Quality Gate**: When subordinates report done, **run and test** the output:
|
|
749
|
+
- For web apps/games: start a local server and open in browser to verify it actually works
|
|
750
|
+
- Try the core user interactions — if basic things don't work, it's NOT done
|
|
751
|
+
- Check that required libraries/tools mentioned in the task are actually used
|
|
752
|
+
- If gaps found → re-dispatch with **specific, actionable** feedback (not "improve quality")
|
|
753
|
+
- There is NO time limit. Non-working code is worse than less code that works.
|
|
748
754
|
|
|
749
755
|
## Supervision Commands
|
|
750
756
|
|
|
@@ -371,10 +371,9 @@ elif cmd == 'amend':
|
|
|
371
371
|
log('Usage: supervision amend <sessionId> "<instruction>"')
|
|
372
372
|
sys.exit(1)
|
|
373
373
|
|
|
374
|
-
# Amend
|
|
374
|
+
# Amend sends a message to the session with amendment instructions
|
|
375
375
|
body = json.dumps({
|
|
376
|
-
'
|
|
377
|
-
'responderRole': os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo'),
|
|
376
|
+
'content': f'[SUPERVISION AMENDMENT] {instruction}',
|
|
378
377
|
}).encode()
|
|
379
378
|
|
|
380
379
|
try:
|
|
@@ -21,7 +21,6 @@ import { earnCoinsInternal } from './coins.js';
|
|
|
21
21
|
import { appendFollowUpToWave } from '../services/wave-tracker.js';
|
|
22
22
|
import { waveMultiplexer } from '../services/wave-multiplexer.js';
|
|
23
23
|
import { supervisorHeartbeat } from '../services/supervisor-heartbeat.js';
|
|
24
|
-
import { readConfig } from '../services/company-config.js';
|
|
25
24
|
|
|
26
25
|
/* ─── Auto-attach child executions to wave multiplexer ── */
|
|
27
26
|
executionManager.onExecutionCreated((exec) => {
|
|
@@ -220,12 +219,8 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
220
219
|
|
|
221
220
|
const targetRoles = body.targetRoles as string[] | undefined;
|
|
222
221
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
const supervisionMode = config.supervision?.mode ?? 'direct';
|
|
226
|
-
|
|
227
|
-
if (supervisionMode === 'supervisor') {
|
|
228
|
-
// Supervisor mode: start a single CEO Supervisor session that dispatches C-Levels
|
|
222
|
+
// Always use supervisor mode — CEO supervises C-Levels who supervise members
|
|
223
|
+
{
|
|
229
224
|
const state = supervisorHeartbeat.start(
|
|
230
225
|
`wave-${Date.now()}`,
|
|
231
226
|
directive,
|
|
@@ -245,71 +240,6 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
245
240
|
});
|
|
246
241
|
return;
|
|
247
242
|
}
|
|
248
|
-
|
|
249
|
-
// Direct mode (default): dispatch all C-Levels simultaneously
|
|
250
|
-
const orgTree = buildOrgTree(COMPANY_ROOT);
|
|
251
|
-
let cLevelRoles = getSubordinates(orgTree, 'ceo');
|
|
252
|
-
|
|
253
|
-
if (targetRoles && Array.isArray(targetRoles) && targetRoles.length > 0) {
|
|
254
|
-
const allowed = new Set(targetRoles);
|
|
255
|
-
cLevelRoles = cLevelRoles.filter(r => allowed.has(r));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (cLevelRoles.length === 0) {
|
|
259
|
-
jsonResponse(res, 400, { error: 'No C-level roles found to dispatch wave.' });
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const fullTargetScope = targetRoles && targetRoles.length > 0 ? targetRoles : undefined;
|
|
264
|
-
|
|
265
|
-
const newWaveId = `wave-${Date.now()}`;
|
|
266
|
-
const sessionIds: string[] = [];
|
|
267
|
-
|
|
268
|
-
for (const cRole of cLevelRoles) {
|
|
269
|
-
const session = createSession(cRole, {
|
|
270
|
-
mode: 'do',
|
|
271
|
-
source: 'wave',
|
|
272
|
-
waveId: newWaveId,
|
|
273
|
-
});
|
|
274
|
-
sessionIds.push(session.id);
|
|
275
|
-
|
|
276
|
-
const ceoMsg: Message = {
|
|
277
|
-
id: `msg-${Date.now()}-ceo-${cRole}`,
|
|
278
|
-
from: 'ceo',
|
|
279
|
-
content: directive,
|
|
280
|
-
type: 'directive',
|
|
281
|
-
status: 'done',
|
|
282
|
-
timestamp: new Date().toISOString(),
|
|
283
|
-
attachments,
|
|
284
|
-
};
|
|
285
|
-
addMessage(session.id, ceoMsg);
|
|
286
|
-
|
|
287
|
-
const exec = executionManager.startExecution({
|
|
288
|
-
type: 'wave',
|
|
289
|
-
roleId: cRole,
|
|
290
|
-
task: `[CEO Wave] ${directive}`,
|
|
291
|
-
sourceRole: 'ceo',
|
|
292
|
-
parentSessionId,
|
|
293
|
-
targetRoles: fullTargetScope,
|
|
294
|
-
sessionId: session.id,
|
|
295
|
-
attachments,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
waveMultiplexer.registerSession(newWaveId, exec);
|
|
299
|
-
|
|
300
|
-
const roleMsg: Message = {
|
|
301
|
-
id: `msg-${Date.now() + 1}-role-${cRole}`,
|
|
302
|
-
from: 'role',
|
|
303
|
-
content: '',
|
|
304
|
-
type: 'conversation',
|
|
305
|
-
status: 'streaming',
|
|
306
|
-
timestamp: new Date().toISOString(),
|
|
307
|
-
};
|
|
308
|
-
addMessage(session.id, roleMsg, true);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
jsonResponse(res, 200, { sessionIds, waveId: newWaveId });
|
|
312
|
-
return;
|
|
313
243
|
}
|
|
314
244
|
|
|
315
245
|
// Assign
|
|
@@ -380,12 +310,45 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
380
310
|
let sessionIds = (body.sessionIds ?? body.jobIds) as string[] | undefined;
|
|
381
311
|
const waveId = body.waveId as string | undefined;
|
|
382
312
|
|
|
383
|
-
// BUG-W01 fix: auto-collect sessionIds from session-store
|
|
313
|
+
// BUG-W01 + BUG-009 fix: auto-collect sessionIds from session-store AND activity-streams
|
|
384
314
|
if (waveId && (!sessionIds || sessionIds.length === 0)) {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
315
|
+
const sessionIdSet = new Set(
|
|
316
|
+
listSessions().filter(s => s.waveId === waveId).map(s => s.id)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Scan activity-streams for sessions belonging to this wave
|
|
320
|
+
const streamsDir = path.join(COMPANY_ROOT, 'operations', 'activity-streams');
|
|
321
|
+
if (fs.existsSync(streamsDir)) {
|
|
322
|
+
const waveTimestamp = waveId.replace('wave-', '');
|
|
323
|
+
for (const file of fs.readdirSync(streamsDir)) {
|
|
324
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
325
|
+
const sid = file.replace('.jsonl', '');
|
|
326
|
+
if (sessionIdSet.has(sid)) continue;
|
|
327
|
+
if (sid.includes(waveTimestamp)) {
|
|
328
|
+
sessionIdSet.add(sid);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Recursively find all child sessions via dispatch:start events
|
|
333
|
+
let foundNew = true;
|
|
334
|
+
while (foundNew) {
|
|
335
|
+
foundNew = false;
|
|
336
|
+
for (const sid of Array.from(sessionIdSet)) {
|
|
337
|
+
try {
|
|
338
|
+
const events = ActivityStream.readAll(sid);
|
|
339
|
+
for (const e of events) {
|
|
340
|
+
const childSessionId = e.data.childSessionId as string | undefined;
|
|
341
|
+
if (e.type === 'dispatch:start' && childSessionId && !sessionIdSet.has(childSessionId)) {
|
|
342
|
+
sessionIdSet.add(childSessionId);
|
|
343
|
+
foundNew = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch { /* skip */ }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
sessionIds = Array.from(sessionIdSet);
|
|
389
352
|
console.log(`[WaveSave] Auto-collected ${sessionIds.length} sessionIds for wave ${waveId}`);
|
|
390
353
|
}
|
|
391
354
|
|
|
@@ -457,14 +420,45 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
457
420
|
}
|
|
458
421
|
const jsonPath = path.join(wavesDir, `${baseName}.json`);
|
|
459
422
|
|
|
423
|
+
// Calculate actual duration from activity stream timestamps
|
|
424
|
+
let startedAt = now;
|
|
425
|
+
let endedAt = now;
|
|
426
|
+
for (const role of rolesData) {
|
|
427
|
+
if (role.events.length > 0) {
|
|
428
|
+
const firstTs = new Date(role.events[0].ts);
|
|
429
|
+
const lastTs = new Date(role.events[role.events.length - 1].ts);
|
|
430
|
+
if (firstTs < startedAt) startedAt = firstTs;
|
|
431
|
+
if (lastTs > endedAt) endedAt = lastTs;
|
|
432
|
+
}
|
|
433
|
+
for (const child of role.childSessions) {
|
|
434
|
+
if (child.events.length > 0) {
|
|
435
|
+
const firstTs = new Date(child.events[0].ts);
|
|
436
|
+
const lastTs = new Date(child.events[child.events.length - 1].ts);
|
|
437
|
+
if (firstTs < startedAt) startedAt = firstTs;
|
|
438
|
+
if (lastTs > endedAt) endedAt = lastTs;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const duration = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
|
|
443
|
+
|
|
444
|
+
// Collect ALL session IDs including child sessions
|
|
445
|
+
const allSessionIds = [...sessionIds];
|
|
446
|
+
for (const role of rolesData) {
|
|
447
|
+
for (const child of role.childSessions) {
|
|
448
|
+
if (!allSessionIds.includes(child.sessionId)) {
|
|
449
|
+
allSessionIds.push(child.sessionId);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
460
454
|
const waveJson = {
|
|
461
455
|
id: baseName,
|
|
462
456
|
directive,
|
|
463
|
-
startedAt:
|
|
464
|
-
duration
|
|
457
|
+
startedAt: startedAt.toISOString(),
|
|
458
|
+
duration,
|
|
465
459
|
roles: rolesData,
|
|
466
460
|
...(waveId && { waveId }),
|
|
467
|
-
|
|
461
|
+
sessionIds: allSessionIds,
|
|
468
462
|
};
|
|
469
463
|
fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
|
|
470
464
|
|
|
@@ -684,15 +678,8 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
684
678
|
|
|
685
679
|
const targetRoles = body.targetRoles as string[] | undefined;
|
|
686
680
|
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
const supervisionMode = config.supervision?.mode ?? 'direct';
|
|
690
|
-
|
|
691
|
-
if (supervisionMode === 'supervisor') {
|
|
692
|
-
handleWaveSupervisor(directive, targetRoles, req, res);
|
|
693
|
-
} else {
|
|
694
|
-
handleWaveDirect(directive, targetRoles, req, res);
|
|
695
|
-
}
|
|
681
|
+
// Always supervisor mode — CEO supervises C-Levels
|
|
682
|
+
handleWaveSupervisor(directive, targetRoles, req, res);
|
|
696
683
|
}
|
|
697
684
|
|
|
698
685
|
/**
|
|
@@ -721,105 +708,6 @@ function handleWaveSupervisor(directive: string, targetRoles: string[] | undefin
|
|
|
721
708
|
});
|
|
722
709
|
}
|
|
723
710
|
|
|
724
|
-
/**
|
|
725
|
-
* Direct mode (legacy): Dispatch all C-Levels simultaneously.
|
|
726
|
-
* No CEO Supervisor — each C-Level runs independently.
|
|
727
|
-
*/
|
|
728
|
-
function handleWaveDirect(directive: string, targetRoles: string[] | undefined, req: IncomingMessage, res: ServerResponse): void {
|
|
729
|
-
const orgTree = buildOrgTree(COMPANY_ROOT);
|
|
730
|
-
let cLevelRoles = getSubordinates(orgTree, 'ceo');
|
|
731
|
-
|
|
732
|
-
if (targetRoles && Array.isArray(targetRoles) && targetRoles.length > 0) {
|
|
733
|
-
const allowed = new Set(targetRoles);
|
|
734
|
-
cLevelRoles = cLevelRoles.filter(r => allowed.has(r));
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (cLevelRoles.length === 0) {
|
|
738
|
-
jsonResponse(res, 400, { error: 'No C-level roles found to dispatch wave.' });
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const fullTargetScope = targetRoles && targetRoles.length > 0 ? targetRoles : undefined;
|
|
743
|
-
|
|
744
|
-
const executions: Execution[] = [];
|
|
745
|
-
for (const cRole of cLevelRoles) {
|
|
746
|
-
const session = createSession(cRole, { mode: 'do' });
|
|
747
|
-
const exec = executionManager.startExecution({
|
|
748
|
-
type: 'wave',
|
|
749
|
-
roleId: cRole,
|
|
750
|
-
task: `[CEO Wave] ${directive}`,
|
|
751
|
-
sourceRole: 'ceo',
|
|
752
|
-
targetRoles: fullTargetScope,
|
|
753
|
-
sessionId: session.id,
|
|
754
|
-
});
|
|
755
|
-
executions.push(exec);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
startSSE(res);
|
|
759
|
-
sendSSE(res, 'start', {
|
|
760
|
-
ids: executions.map((e) => e.id),
|
|
761
|
-
directive,
|
|
762
|
-
targetRoles: cLevelRoles,
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
let doneCount = 0;
|
|
766
|
-
const subscribers: Array<{ exec: Execution; sub: ActivitySubscriber }> = [];
|
|
767
|
-
|
|
768
|
-
for (const exec of executions) {
|
|
769
|
-
const subscriber: ActivitySubscriber = (event: ActivityEvent) => {
|
|
770
|
-
const rolePrefix = exec.roleId;
|
|
771
|
-
switch (event.type) {
|
|
772
|
-
case 'text':
|
|
773
|
-
sendSSE(res, 'output', { roleId: rolePrefix, text: event.data.text });
|
|
774
|
-
break;
|
|
775
|
-
case 'thinking':
|
|
776
|
-
sendSSE(res, 'thinking', { roleId: rolePrefix, text: event.data.text });
|
|
777
|
-
break;
|
|
778
|
-
case 'tool:start':
|
|
779
|
-
sendSSE(res, 'tool', { roleId: rolePrefix, name: event.data.name, input: event.data.input });
|
|
780
|
-
break;
|
|
781
|
-
case 'dispatch:start':
|
|
782
|
-
sendSSE(res, 'dispatch', { roleId: rolePrefix, targetRoleId: event.data.targetRoleId, task: event.data.task, childSessionId: event.data.childSessionId });
|
|
783
|
-
break;
|
|
784
|
-
case 'msg:turn-complete':
|
|
785
|
-
sendSSE(res, 'turn', { roleId: rolePrefix, turn: event.data.turn });
|
|
786
|
-
break;
|
|
787
|
-
case 'stderr':
|
|
788
|
-
sendSSE(res, 'stderr', { roleId: rolePrefix, message: event.data.message });
|
|
789
|
-
break;
|
|
790
|
-
case 'msg:awaiting_input':
|
|
791
|
-
sendSSE(res, 'role:awaiting_input', { roleId: rolePrefix, question: event.data.question, targetRole: event.data.targetRole, reason: event.data.reason });
|
|
792
|
-
break;
|
|
793
|
-
case 'msg:done':
|
|
794
|
-
sendSSE(res, 'role:done', { roleId: rolePrefix, ...event.data });
|
|
795
|
-
doneCount++;
|
|
796
|
-
if (doneCount >= executions.length) {
|
|
797
|
-
sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
|
|
798
|
-
res.end();
|
|
799
|
-
}
|
|
800
|
-
break;
|
|
801
|
-
case 'msg:error':
|
|
802
|
-
sendSSE(res, 'role:error', { roleId: rolePrefix, message: event.data.message });
|
|
803
|
-
doneCount++;
|
|
804
|
-
if (doneCount >= executions.length) {
|
|
805
|
-
sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
|
|
806
|
-
res.end();
|
|
807
|
-
}
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
exec.stream.subscribe(subscriber);
|
|
813
|
-
subscribers.push({ exec, sub: subscriber });
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
req.on('close', () => {
|
|
817
|
-
for (const { exec, sub } of subscribers) {
|
|
818
|
-
exec.stream.unsubscribe(sub);
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
|
|
823
711
|
/* ─── POST /api/waves/:waveId/directive ──────── */
|
|
824
712
|
|
|
825
713
|
function handleWaveDirective(waveId: string, body: Record<string, unknown>, res: ServerResponse): void {
|
|
@@ -56,6 +56,7 @@ sessionsRouter.patch('/:id', (req, res) => {
|
|
|
56
56
|
|
|
57
57
|
/* DELETE /api/sessions — bulk delete (body: { ids }) or ?empty=true */
|
|
58
58
|
sessionsRouter.delete('/', (req, res) => {
|
|
59
|
+
console.log(`[Sessions] DELETE / called (empty=${req.query.empty}, origin=${req.headers.origin ?? req.headers.referer ?? 'unknown'})`);
|
|
59
60
|
if (req.query.empty === 'true') {
|
|
60
61
|
const result = deleteEmpty();
|
|
61
62
|
res.json(result);
|
|
@@ -72,6 +73,7 @@ sessionsRouter.delete('/', (req, res) => {
|
|
|
72
73
|
|
|
73
74
|
/* DELETE /api/sessions/:id — delete session */
|
|
74
75
|
sessionsRouter.delete('/:id', (req, res) => {
|
|
76
|
+
console.log(`[Sessions] DELETE /${req.params.id} called (origin=${req.headers.origin ?? req.headers.referer ?? 'unknown'})`);
|
|
75
77
|
const ok = deleteSession(req.params.id);
|
|
76
78
|
if (!ok) {
|
|
77
79
|
res.status(404).json({ error: 'Session not found' });
|
|
@@ -101,7 +101,11 @@ function writeImmediate(session: Session): void {
|
|
|
101
101
|
clearTimeout(timer);
|
|
102
102
|
writeTimers.delete(session.id);
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
try {
|
|
105
|
+
fs.writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(`[SessionStore] WRITE FAILED for ${session.id}:`, err);
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/* ─── In-memory cache ───────────────────── */
|
|
@@ -250,10 +254,22 @@ export function updateSession(id: string, updates: Partial<Pick<Session, 'title'
|
|
|
250
254
|
return session;
|
|
251
255
|
}
|
|
252
256
|
|
|
253
|
-
export function deleteSession(id: string): boolean {
|
|
257
|
+
export function deleteSession(id: string, force = false): boolean {
|
|
254
258
|
const session = cache.get(id);
|
|
255
259
|
if (!session) return false;
|
|
256
260
|
|
|
261
|
+
// BUG-008 fix: protect wave sessions from accidental deletion
|
|
262
|
+
if (session.waveId && !force) {
|
|
263
|
+
console.warn(`[SessionStore] BLOCKED deletion of wave session ${id} (waveId=${session.waveId}, roleId=${session.roleId}). Use force=true to override.`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
// BUG-008 hard guard: CEO supervisor session is NEVER deletable during wave
|
|
267
|
+
if (session.roleId === 'ceo' && session.waveId && session.source === 'wave') {
|
|
268
|
+
console.error(`[SessionStore] HARD BLOCK: CEO supervisor session ${id} cannot be deleted (waveId=${session.waveId}). This is a 1:1:1 invariant.`);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`[SessionStore] Deleting session ${id} (roleId=${session.roleId}, waveId=${session.waveId ?? 'none'}, messages=${session.messages.length})`);
|
|
257
273
|
cache.delete(id);
|
|
258
274
|
const p = sessionPath(id);
|
|
259
275
|
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
@@ -272,6 +288,8 @@ export function deleteEmpty(): { deleted: number; ids: string[] } {
|
|
|
272
288
|
const ids: string[] = [];
|
|
273
289
|
for (const [id, session] of cache) {
|
|
274
290
|
if (session.messages.length === 0) {
|
|
291
|
+
// BUG-008 fix: never delete wave sessions — they are managed by supervisor lifecycle
|
|
292
|
+
if (session.waveId) continue;
|
|
275
293
|
ids.push(id);
|
|
276
294
|
}
|
|
277
295
|
}
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* - On restart, digest catches up with all missed events
|
|
13
13
|
*/
|
|
14
14
|
import { executionManager, type Execution } from './execution-manager.js';
|
|
15
|
-
import { createSession, getSession, listSessions } from './session-store.js';
|
|
15
|
+
import { createSession, getSession, listSessions, addMessage, type Message } from './session-store.js';
|
|
16
16
|
import { buildOrgTree, getSubordinates } from '../engine/org-tree.js';
|
|
17
17
|
import { COMPANY_ROOT } from './file-reader.js';
|
|
18
18
|
import { ActivityStream } from './activity-stream.js';
|
|
19
|
+
import { saveCompletedWave } from './wave-tracker.js';
|
|
19
20
|
|
|
20
21
|
/* ─── Types ──────────────────────────────────── */
|
|
21
22
|
|
|
@@ -267,27 +268,91 @@ ${cLevelList}
|
|
|
267
268
|
- G-04: If you dispatch the same role 3+ times with no progress, intervene: "specify what's wrong concretely."
|
|
268
269
|
- G-05: abort = graceful amend ("wrap up and stop"). Not a hard kill.
|
|
269
270
|
- G-06: If two sessions show no events for 3+ minutes, suspect deadlock → re-sequence their work.
|
|
271
|
+
- 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."
|
|
272
|
+
- 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.
|
|
273
|
+
|
|
274
|
+
## Cross-Team Relay Protocol (CRITICAL)
|
|
275
|
+
⛔ C-Levels do NOT talk to each other directly. YOU are the relay.
|
|
276
|
+
|
|
277
|
+
When C-Level A completes while C-Level B is still active:
|
|
278
|
+
1. Review A's deliverables (read their committed files or final report)
|
|
279
|
+
2. Summarize the key decisions, artifacts, and constraints from A's work
|
|
280
|
+
3. amend B: "C-Level A completed. Here are their deliverables relevant to your work: [summary]. Review and incorporate."
|
|
281
|
+
4. On next tick, verify B acknowledged and reflected A's input
|
|
282
|
+
|
|
283
|
+
When C-Level A produces intermediate results that B needs:
|
|
284
|
+
1. amend B with the relevant intermediate output
|
|
285
|
+
2. You don't need to wait for A to finish — relay as results become available
|
|
286
|
+
|
|
287
|
+
Examples:
|
|
288
|
+
- CBO finishes game design → amend CTO: "CBO delivered: world-building doc, 15 monster specs, quest design, UI guidelines. Ensure implementation matches these specs."
|
|
289
|
+
- 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."
|
|
290
|
+
- Designer finishes UI guide → relay to CTO team: "Designer's UI guide is ready at [path]. Frontend implementation should follow these specs."
|
|
270
291
|
|
|
271
292
|
## CEO Directive Channel
|
|
272
293
|
If new CEO directives arrive mid-execution, they will appear in your supervision watch digest
|
|
273
294
|
marked as [CEO DIRECTIVE]. These are PRIORITY 1 — process before anything else.
|
|
274
295
|
${recoveryContext}
|
|
275
296
|
|
|
297
|
+
## Quality Gate (CRITICAL — G-09)
|
|
298
|
+
⛔ **"Subordinate said done" ≠ "Work is actually done."**
|
|
299
|
+
⛔ **"Code exists" ≠ "Code works."** You MUST run and test the output, not just read files.
|
|
300
|
+
|
|
301
|
+
Before declaring yourself done, you MUST:
|
|
302
|
+
|
|
303
|
+
1. **Read the actual output files** — don't trust status reports. Check the code yourself.
|
|
304
|
+
2. **RUN it and test it** — this is the most important step:
|
|
305
|
+
- For web apps/games: \`cd <code-dir> && python3 -m http.server 9999\` then open in browser
|
|
306
|
+
- Actually try the core interactions (click buttons, press keys, navigate)
|
|
307
|
+
- If basic interactions fail (can't move, can't click, blank screen) → it's NOT done
|
|
308
|
+
3. **Count against requirements** — if the directive says "15 monsters, 7 maps", count them.
|
|
309
|
+
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).
|
|
310
|
+
5. **If quality is insufficient → re-dispatch** with specific, actionable feedback:
|
|
311
|
+
- "Arrow keys don't move the player. Fix input handling in WorldScene."
|
|
312
|
+
- "TyconoForge was required but not used. Add character rendering with TyconoForge.render()."
|
|
313
|
+
- NOT vague feedback like "improve quality" or "make it better"
|
|
314
|
+
6. **Iterate until the directive is truly fulfilled.** There is NO time limit.
|
|
315
|
+
20,000 lines of non-working code is worse than 5,000 lines that actually play.
|
|
316
|
+
|
|
317
|
+
Re-dispatch pattern:
|
|
318
|
+
- dispatch same C-Level with specific gaps identified
|
|
319
|
+
- Each iteration should close specific gaps, not redo everything
|
|
320
|
+
- Maximum 5 iterations per C-Level before escalating
|
|
321
|
+
|
|
276
322
|
## Instructions
|
|
277
323
|
1. Analyze the directive and decide which C-Level roles to dispatch (not necessarily all)
|
|
278
324
|
2. Dispatch them with clear tasks
|
|
279
325
|
3. Enter supervision watch loop
|
|
280
|
-
4. Monitor, relay
|
|
281
|
-
5.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
326
|
+
4. Monitor, **actively relay results between teams**, course-correct
|
|
327
|
+
5. When subordinates report done → **verify deliverables against requirements (G-09)**
|
|
328
|
+
6. If gaps exist → re-dispatch with specific feedback. Repeat 3-5.
|
|
329
|
+
7. Only when ALL requirements are met → compile results and report`;
|
|
330
|
+
|
|
331
|
+
// BUG-008 fix: Wave:Supervisor:Session = 1:1:1 invariant.
|
|
332
|
+
// Reuse existing session on restart instead of creating a new one.
|
|
333
|
+
let sessionId = state.supervisorSessionId;
|
|
334
|
+
if (sessionId && getSession(sessionId)) {
|
|
335
|
+
console.log(`[Supervisor] Reusing existing session ${sessionId} for wave ${state.waveId}`);
|
|
336
|
+
} else {
|
|
337
|
+
const session = createSession('ceo', {
|
|
338
|
+
mode: 'do',
|
|
339
|
+
source: 'wave',
|
|
340
|
+
waveId: state.waveId,
|
|
341
|
+
});
|
|
342
|
+
sessionId = session.id;
|
|
343
|
+
state.supervisorSessionId = sessionId;
|
|
344
|
+
|
|
345
|
+
// Add the directive as CEO message so the session isn't empty (prevents deleteEmpty cleanup)
|
|
346
|
+
const ceoMsg: Message = {
|
|
347
|
+
id: `msg-${Date.now()}-ceo-supervisor`,
|
|
348
|
+
from: 'ceo',
|
|
349
|
+
content: state.directive,
|
|
350
|
+
type: 'directive',
|
|
351
|
+
status: 'done',
|
|
352
|
+
timestamp: new Date().toISOString(),
|
|
353
|
+
};
|
|
354
|
+
addMessage(sessionId, ceoMsg);
|
|
355
|
+
}
|
|
291
356
|
state.status = 'running';
|
|
292
357
|
|
|
293
358
|
try {
|
|
@@ -297,14 +362,14 @@ ${recoveryContext}
|
|
|
297
362
|
task: supervisorTask,
|
|
298
363
|
sourceRole: 'ceo',
|
|
299
364
|
targetRoles: state.targetRoles,
|
|
300
|
-
sessionId
|
|
365
|
+
sessionId,
|
|
301
366
|
});
|
|
302
367
|
|
|
303
368
|
state.executionId = exec.id;
|
|
304
369
|
|
|
305
370
|
this.watchExecution(state, exec);
|
|
306
371
|
|
|
307
|
-
console.log(`[Supervisor] Started for wave ${state.waveId} | session=${
|
|
372
|
+
console.log(`[Supervisor] Started for wave ${state.waveId} | session=${sessionId} | exec=${exec.id}`);
|
|
308
373
|
} catch (err) {
|
|
309
374
|
console.error(`[Supervisor] Failed to start for wave ${state.waveId}:`, err);
|
|
310
375
|
state.status = 'error';
|
|
@@ -319,6 +384,12 @@ ${recoveryContext}
|
|
|
319
384
|
} else if (event.type === 'msg:error') {
|
|
320
385
|
exec.stream.unsubscribe(subscriber);
|
|
321
386
|
this.onSupervisorCrash(state, String(event.data.message ?? 'unknown error'));
|
|
387
|
+
} else if (event.type === 'msg:awaiting_input') {
|
|
388
|
+
// BUG-016: turn:limit causes awaiting_input — treat as done-guard
|
|
389
|
+
// If all children are done → complete wave. Otherwise restart supervisor.
|
|
390
|
+
exec.stream.unsubscribe(subscriber);
|
|
391
|
+
console.log(`[Supervisor] awaiting_input (turn limit) for wave ${state.waveId}. Running done-guard.`);
|
|
392
|
+
this.onSupervisorDone(state);
|
|
322
393
|
}
|
|
323
394
|
};
|
|
324
395
|
|
|
@@ -341,6 +412,18 @@ ${recoveryContext}
|
|
|
341
412
|
} else {
|
|
342
413
|
console.log(`[Supervisor] Wave ${state.waveId} complete. All subordinates done.`);
|
|
343
414
|
state.status = 'stopped';
|
|
415
|
+
|
|
416
|
+
// Auto-save the completed wave to operations/waves/
|
|
417
|
+
try {
|
|
418
|
+
const result = saveCompletedWave(state.waveId, state.directive);
|
|
419
|
+
if (result.ok) {
|
|
420
|
+
console.log(`[Supervisor] Wave auto-saved: ${result.path}`);
|
|
421
|
+
} else {
|
|
422
|
+
console.warn(`[Supervisor] Wave auto-save returned no result for ${state.waveId}`);
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error(`[Supervisor] Failed to auto-save wave ${state.waveId}:`, err);
|
|
426
|
+
}
|
|
344
427
|
}
|
|
345
428
|
}
|
|
346
429
|
|