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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.94-beta.1",
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": "tsup --config tsup.forge.ts",
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
- if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url === '/api/waves/save' || url === '/api/setup/import-knowledge') && method === 'POST') {
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**: Compile results and report to your superior
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 uses continue-session with amended context
374
+ # Amend sends a message to the session with amendment instructions
375
375
  body = json.dumps({
376
- 'response': f'[SUPERVISION AMENDMENT] {instruction}',
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
- // Check supervision mode from config (same as legacy /api/exec/wave)
224
- const config = readConfig(COMPANY_ROOT);
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 when waveId is present
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 allSessions = listSessions();
386
- sessionIds = allSessions
387
- .filter(s => s.waveId === waveId)
388
- .map(s => s.id);
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: now.toISOString(),
464
- duration: 0,
457
+ startedAt: startedAt.toISOString(),
458
+ duration,
465
459
  roles: rolesData,
466
460
  ...(waveId && { waveId }),
467
- ...(sessionIds.length > 0 && { sessionIds }),
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
- // Check supervision mode from config
688
- const config = readConfig(COMPANY_ROOT);
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
- fs.writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
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 opinions, course-correct, until all done
281
- 5. Compile results and report`;
282
-
283
- // Create supervisor session
284
- const session = createSession('ceo', {
285
- mode: 'do',
286
- source: 'wave',
287
- waveId: state.waveId,
288
- });
289
-
290
- state.supervisorSessionId = session.id;
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: session.id,
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=${session.id} | exec=${exec.id}`);
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