tycono 0.1.96-beta.5 → 0.1.96-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  <h1 align="center">tycono</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Build an AI company. Watch them work.</strong><br>
9
- <sub>Infrastructure-as-Code defined servers. Company-as-Code defines organizations.</sub>
8
+ <strong>Cursor gives you one AI developer. Tycono gives you an AI team.</strong><br>
9
+ <sub>Give one order. Watch your AI team plan, build, and learn together.</sub>
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -25,9 +25,11 @@
25
25
 
26
26
  ---
27
27
 
28
- **tycono** is an open-source platform that lets you define and run an AI-powered organization. Roles, authority, knowledge, and workflows all defined in files, executed by AI agents, visualized in real time.
28
+ Cursor, Lovable, Bolt they all give you **one AI agent**. It helps, but you still drive everything.
29
29
 
30
- One command. Your AI company is running.
30
+ **tycono** gives you an **AI team**. A CTO reviews architecture. Engineers write code. A PM breaks down tasks. QA catches bugs. You just give the order and watch them work.
31
+
32
+ One command. Your AI team is running.
31
33
 
32
34
  ```bash
33
35
  npx tycono
@@ -83,16 +85,16 @@ Session 50 is dramatically smarter than session 1. Your company learns.
83
85
 
84
86
  ## Why Tycono?
85
87
 
86
- Coding agents simulate **one developer**. Tycono simulates **the entire company**.
88
+ Same goal as Cursor, Lovable, Bolt **get AI to do your work**. Different method.
87
89
 
88
- | | Single AI Agent | Tycono |
90
+ | | Cursor / Lovable / Bolt | Tycono |
89
91
  |---|---|---|
90
- | **What it runs** | One agent, one context | Multiple roles with org hierarchy |
91
- | **Knowledge** | Resets every session | Compounds forever (AKB Pre-K/Post-K) |
92
- | **Authority** | Can do anything (or nothing) | Scoped each role has clear boundaries |
93
- | **Delegation** | Manual prompt chaining | CEO dispatches, org chart routes automatically |
94
- | **Scale** | 1 agent | 7 700 agents |
95
- | **Visibility** | Terminal output | Real-time org tree + activity stream |
92
+ | **Agents** | 1 AI helps you | **AI team works for you** |
93
+ | **Your role** | Keep directing | **Give one order, watch** |
94
+ | **Knowledge** | Resets every session | **Compounds forever** |
95
+ | **Quality** | You review everything | **QA agent catches bugs** |
96
+ | **Scale** | 1 task at a time | **Parallel across roles** |
97
+ | **Visibility** | Editor / chat | **Real-time org tree** |
96
98
 
97
99
  ## Company-as-Code
98
100
 
@@ -258,9 +260,9 @@ npx tycono --version # Show version
258
260
  - [x] CEO Wave dispatch with org-tree targeting
259
261
  - [x] AKB — Pre-K / Post-K knowledge loop
260
262
  - [x] Port Registry for multi-agent isolation
261
- - [ ] **TUI mode** — terminal-native multi-panel interface
263
+ - [ ] **TUI mode** — terminal-native multi-panel interface *(in progress)*
262
264
  - [ ] Git worktree isolation per agent session
263
- - [ ] Desktop app (.dmg / .exe) — background execution, system notifications
265
+ - [ ] **Desktop app** (.dmg / .exe) — background execution, notifications, no API key setup needed
264
266
  - [ ] Multi-LLM support (OpenAI, local models)
265
267
 
266
268
  ## Built with Tycono
package/bin/tycono.ts CHANGED
@@ -213,38 +213,73 @@ async function startServerForTui(): Promise<void> {
213
213
  const port = process.env.PORT ? Number(process.env.PORT) : await findFreePort();
214
214
  process.env.PORT = String(port);
215
215
 
216
- // Suppress ALL server logs BEFORE creating server — redirect to file
216
+ // Suppress ALL server output BEFORE creating server — hijack process streams
217
217
  const logFile = path.resolve(process.env.COMPANY_ROOT || process.cwd(), '.tycono', 'server.log');
218
218
  try { fs.mkdirSync(path.dirname(logFile), { recursive: true }); } catch {}
219
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
220
- const origLog = console.log;
221
- const origErr = console.error;
222
- const origWarn = console.warn;
223
- console.log = (...args: unknown[]) => logStream.write(args.join(' ') + '\n');
224
- console.error = (...args: unknown[]) => logStream.write('[ERROR] ' + args.join(' ') + '\n');
225
- console.warn = (...args: unknown[]) => logStream.write('[WARN] ' + args.join(' ') + '\n');
226
-
227
- const { createHttpServer } = await import('../src/api/src/create-server.js');
228
- const server = createHttpServer();
219
+ const logFd = fs.openSync(logFile, 'a');
220
+ const logStream = fs.createWriteStream(logFile, { fd: logFd });
221
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
222
+ const origLog = (...args: unknown[]) => origStdoutWrite(args.join(' ') + '\n');
223
+
224
+ // Start API server as a CHILD PROCESS to isolate stdout completely
225
+ // This prevents server console.log from corrupting Ink's frame buffer
226
+ const { fork } = await import('node:child_process');
227
+ const serverScript = path.resolve(__dirname, '..', 'src', 'api', 'src', 'server.ts');
228
+
229
+ const child = fork(serverScript, [], {
230
+ execArgv: ['--import', 'tsx'],
231
+ env: {
232
+ ...process.env,
233
+ PORT: String(port),
234
+ COMPANY_ROOT: process.env.COMPANY_ROOT,
235
+ },
236
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
237
+ });
229
238
 
230
- const host = process.env.HOST || '0.0.0.0';
239
+ // Redirect child stdout/stderr to log file
240
+ child.stdout?.on('data', (data: Buffer) => { logStream.write(data); });
241
+ child.stderr?.on('data', (data: Buffer) => { logStream.write(data); });
242
+
243
+ // Wait for server to be ready (poll health endpoint)
244
+ const waitForServer = async () => {
245
+ const http = await import('node:http');
246
+ for (let i = 0; i < 30; i++) {
247
+ try {
248
+ await new Promise<void>((resolve, reject) => {
249
+ const req = http.get(`http://localhost:${port}/api/health`, (res) => {
250
+ res.resume();
251
+ resolve();
252
+ });
253
+ req.on('error', reject);
254
+ req.setTimeout(1000, () => { req.destroy(); reject(new Error('timeout')); });
255
+ });
256
+ return true;
257
+ } catch {
258
+ await new Promise(r => setTimeout(r, 500));
259
+ }
260
+ }
261
+ return false;
262
+ };
231
263
 
232
- await new Promise<void>((resolve) => {
233
- server.listen(port, host, () => resolve());
234
- });
264
+ const serverReady = await waitForServer();
265
+ if (!serverReady) {
266
+ origLog(' Failed to start API server. Check logs:', logFile);
267
+ process.exit(1);
268
+ }
235
269
 
236
270
  origLog(` API server started on port ${port}`);
237
271
  origLog(` Logs: ${logFile}`);
238
272
 
239
- // Graceful shutdown
273
+ // Graceful shutdown — kill child server
240
274
  const shutdown = () => {
241
- server.close(() => process.exit(0));
242
- setTimeout(() => process.exit(1), 5000);
275
+ child.kill();
276
+ process.exit(0);
243
277
  };
244
278
  process.on('SIGINT', shutdown);
245
279
  process.on('SIGTERM', shutdown);
280
+ child.on('exit', () => { process.exit(0); });
246
281
 
247
- // Start TUI
282
+ // Start TUI — clean stdout, no server interference
248
283
  const { startTui } = await import('../src/tui/index.tsx');
249
284
  await startTui({ port });
250
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.96-beta.5",
3
+ "version": "0.1.96-beta.51",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,7 @@
6
6
  import { Router } from 'express';
7
7
  import { portRegistry } from '../services/port-registry.js';
8
8
  import { executionManager } from '../services/execution-manager.js';
9
+ import { getSession } from '../services/session-store.js';
9
10
 
10
11
  export const activeSessionsRouter = Router();
11
12
 
@@ -17,8 +18,10 @@ activeSessionsRouter.get('/', (_req, res) => {
17
18
 
18
19
  const enriched = sessions.map(s => {
19
20
  const exec = executionManager.getActiveExecution(s.sessionId);
21
+ const session = getSession(s.sessionId);
20
22
  return {
21
23
  ...s,
24
+ waveId: session?.waveId ?? null,
22
25
  messageStatus: exec?.status ?? null,
23
26
  roleName: exec?.roleId ?? s.roleId,
24
27
  alive: s.pid ? isAlive(s.pid) : null,
@@ -51,25 +51,21 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
51
51
  // ── /api/waves/active — restore active waves after refresh ──
52
52
  if (method === 'GET' && url === '/api/waves/active') {
53
53
  // Recovery: rebuild wave→session mapping from session-store if lost
54
+ // Only recover ACTIVE sessions to prevent OOM on large datasets (140+ sessions)
54
55
  const waves = waveMultiplexer.getActiveWaves();
55
56
  if (waves.length === 0) {
56
57
  const allSessions = listSessions();
57
- const waveGroups = new Map<string, string[]>();
58
+ let recovered = 0;
58
59
  for (const ses of allSessions) {
59
- if (ses.waveId) {
60
- if (!waveGroups.has(ses.waveId)) waveGroups.set(ses.waveId, []);
61
- waveGroups.get(ses.waveId)!.push(ses.id);
60
+ if (!ses.waveId || ses.status !== 'active') continue;
61
+ const exec = executionManager.getActiveExecution(ses.id);
62
+ if (exec) {
63
+ waveMultiplexer.registerSession(ses.waveId, exec);
64
+ recovered++;
62
65
  }
63
66
  }
64
- // BUG-W03 fix: register ALL wave sessions (including completed) for tree display
65
- for (const [wid, sids] of waveGroups) {
66
- for (const sid of sids) {
67
- // getActiveExecution falls back to stream recovery for completed sessions
68
- const exec = executionManager.getActiveExecution(sid);
69
- if (exec) {
70
- waveMultiplexer.registerSession(wid, exec);
71
- }
72
- }
67
+ if (recovered > 0) {
68
+ console.log(`[WaveRecovery] Recovered ${recovered} active sessions`);
73
69
  }
74
70
  }
75
71
  jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
@@ -212,10 +208,8 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
212
208
  const attachments = body.attachments as ImageAttachment[] | undefined;
213
209
 
214
210
  if (type === 'wave') {
215
- if (!directive) {
216
- jsonResponse(res, 400, { error: 'directive is required for wave jobs' });
217
- return;
218
- }
211
+ // directive가 없으면 idle 상태로 시작 (empty wave)
212
+ const actualDirective = directive || '';
219
213
 
220
214
  const targetRoles = body.targetRoles as string[] | undefined;
221
215
  const continuous = body.continuous === true;
@@ -224,7 +218,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
224
218
  {
225
219
  const state = supervisorHeartbeat.start(
226
220
  `wave-${Date.now()}`,
227
- directive,
221
+ actualDirective,
228
222
  targetRoles && targetRoles.length > 0 ? targetRoles : undefined,
229
223
  continuous,
230
224
  );
@@ -238,7 +232,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
238
232
  waveId: state.waveId,
239
233
  supervisorSessionId: state.supervisorSessionId,
240
234
  mode: 'supervisor',
241
- directive,
235
+ directive: actualDirective,
242
236
  });
243
237
  return;
244
238
  }
@@ -482,13 +476,11 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
482
476
 
483
477
  let sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
484
478
 
485
- // Recovery: if wave→session mapping was lost (e.g. server restart),
486
- // rebuild from session-store (sessions have waveId) + executionManager
479
+ // Recovery: only recover ACTIVE sessions for this wave (not all 140)
487
480
  if (sessionIds.length === 0) {
488
481
  const allSessions = listSessions();
489
- const waveSessions = allSessions.filter(s => s.waveId === waveId);
482
+ const waveSessions = allSessions.filter(s => s.waveId === waveId && s.status === 'active');
490
483
  for (const ses of waveSessions) {
491
- // getActiveExecution recovers from stream files for completed sessions too
492
484
  const exec = executionManager.getActiveExecution(ses.id);
493
485
  if (exec) {
494
486
  waveMultiplexer.registerSession(waveId, exec);
@@ -497,12 +489,8 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
497
489
  sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
498
490
  }
499
491
 
500
- if (sessionIds.length === 0) {
501
- res.writeHead(404, { 'Content-Type': 'application/json' });
502
- res.end(JSON.stringify({ error: `No sessions found for wave: ${waveId}` }));
503
- return;
504
- }
505
-
492
+ // Don't 404 on empty waves — keep SSE alive, sessions will appear later
493
+ // (e.g. idle wave waiting for first directive, or supervisor restarting)
506
494
  const client = waveMultiplexer.attach(waveId, res as any, fromWaveSeq);
507
495
 
508
496
  req.on('close', () => {
@@ -769,25 +757,26 @@ function handleStatus(res: ServerResponse): void {
769
757
  );
770
758
 
771
759
  const recovered: typeof activeExecs = [];
772
- for (const ses of activeSessions) {
773
- // Check activity-stream for actual running state
760
+ // Limit recovery scan to prevent OOM on large session stores
761
+ const MAX_RECOVERY_SCAN = 20;
762
+ const recentActive = activeSessions.slice(-MAX_RECOVERY_SCAN);
763
+
764
+ for (const ses of recentActive) {
774
765
  if (!ActivityStream.exists(ses.id)) continue;
766
+ // Only read last few events to check done/error (not entire stream)
775
767
  const events = ActivityStream.readFrom(ses.id, 0);
776
768
  if (events.length === 0) continue;
777
769
 
778
- const startEvent = events.find(e => e.type === 'msg:start');
779
- if (!startEvent) continue;
780
-
781
- const doneEvent = events.find(e => e.type === 'msg:done');
782
- const errorEvent = events.find(e => e.type === 'msg:error');
783
-
784
- // Only include sessions that haven't finished yet
785
- if (doneEvent || errorEvent) continue;
770
+ // Check last 5 events for done/error (optimization: don't scan entire file)
771
+ const tail = events.slice(-5);
772
+ const isDone = tail.some(e => e.type === 'msg:done' || e.type === 'msg:error');
773
+ if (isDone) continue;
786
774
 
787
- const task = (startEvent.data?.task as string) ?? ses.title ?? '';
775
+ const startEvent = events.find(e => e.type === 'msg:start');
776
+ const task = (startEvent?.data?.task as string) ?? ses.title ?? '';
788
777
  recovered.push({
789
778
  id: `recovered-${ses.id}`,
790
- type: (startEvent.data?.type as string ?? 'assign') as 'assign' | 'wave' | 'consult',
779
+ type: (startEvent?.data?.type as string ?? 'assign') as 'assign' | 'wave' | 'consult',
791
780
  roleId: ses.roleId,
792
781
  task,
793
782
  status: 'running',
@@ -1,11 +1,40 @@
1
1
  import { COMPANY_ROOT } from './services/file-reader.js';
2
2
  import { applyConfig } from './services/company-config.js';
3
3
  import { createHttpServer } from './create-server.js';
4
+ import { listSessions, updateSession } from './services/session-store.js';
5
+ import { ActivityStream } from './services/activity-stream.js';
4
6
 
5
7
  // Load .tycono/config.json and apply to process.env
6
8
  const config = applyConfig(COMPANY_ROOT);
7
9
  console.log(`[STARTUP] Engine: ${config.engine}, API key: ${config.apiKey ? 'set' : 'none'}`);
8
10
 
11
+ // Startup: mark orphaned 'active' sessions as 'interrupted'
12
+ // These are sessions from a previous server that crashed or was killed
13
+ {
14
+ const allSessions = listSessions();
15
+ let orphaned = 0;
16
+ for (const ses of allSessions) {
17
+ if (ses.status !== 'active') continue;
18
+ // Check activity stream — if it has msg:done/msg:error, mark done
19
+ // If not, mark interrupted (previous server died mid-execution)
20
+ if (ActivityStream.exists(ses.id)) {
21
+ const events = ActivityStream.readFrom(ses.id, 0);
22
+ const tail = events.slice(-5);
23
+ const isDone = tail.some(e => e.type === 'msg:done' || e.type === 'msg:error');
24
+ if (isDone) {
25
+ updateSession(ses.id, { status: 'done' });
26
+ orphaned++;
27
+ continue;
28
+ }
29
+ }
30
+ updateSession(ses.id, { status: 'interrupted' as any });
31
+ orphaned++;
32
+ }
33
+ if (orphaned > 0) {
34
+ console.log(`[STARTUP] Cleaned ${orphaned} orphaned sessions (active → done/interrupted)`);
35
+ }
36
+ }
37
+
9
38
  const PORT = Number(process.env.PORT) || 3001;
10
39
  const server = createHttpServer();
11
40
 
@@ -13,3 +42,18 @@ server.listen(PORT, () => {
13
42
  console.log(`[API] Server running on http://localhost:${PORT}`);
14
43
  console.log(`[API] COMPANY_ROOT: ${COMPANY_ROOT}`);
15
44
  });
45
+
46
+ // Graceful shutdown: mark running sessions as interrupted
47
+ function gracefulShutdown(signal: string) {
48
+ console.log(`[SHUTDOWN] ${signal} received, marking active sessions as interrupted...`);
49
+ const sessions = listSessions();
50
+ for (const ses of sessions) {
51
+ if (ses.status === 'active') {
52
+ updateSession(ses.id, { status: 'interrupted' as any });
53
+ }
54
+ }
55
+ process.exit(0);
56
+ }
57
+
58
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
59
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
@@ -10,7 +10,7 @@ import { estimateCost } from './pricing.js';
10
10
  import { readConfig, getConversationLimits, resolveCodeRoot } from './company-config.js';
11
11
  import { postKnowledgingCheck, type KnowledgeDebtItem } from '../engine/knowledge-gate.js';
12
12
  import { earnCoinsInternal } from '../routes/coins.js';
13
- import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, appendMessageEvent, type Message, type ImageAttachment } from './session-store.js';
13
+ import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, updateSession, appendMessageEvent, type Message, type ImageAttachment } from './session-store.js';
14
14
  import { portRegistry, type PortAllocation } from './port-registry.js';
15
15
  import { type MessageStatus, isMessageActive, canTransition, messageStatusToRoleStatus } from '../../../shared/types.js';
16
16
 
@@ -603,6 +603,12 @@ class ExecutionManager {
603
603
  knowledgeDebt: execution.knowledgeDebt.map(d => ({ type: d.type, file: d.file, message: d.message })),
604
604
  }),
605
605
  });
606
+
607
+ // Mark session as done in session-store (persisted to file)
608
+ // Skip CEO supervisor sessions — they stay active for wave lifecycle
609
+ if (session.roleId !== 'ceo' || session.source !== 'wave') {
610
+ updateSession(execution.sessionId, { status: 'done' });
611
+ }
606
612
  }
607
613
 
608
614
  private cleanupOrphanedChildren(parentSessionId: string): void {
@@ -88,6 +88,14 @@ class SupervisorHeartbeat {
88
88
  };
89
89
 
90
90
  this.supervisors.set(waveId, state);
91
+
92
+ // Empty directive → idle wave (don't spawn supervisor yet)
93
+ if (!directive) {
94
+ state.status = 'stopped';
95
+ console.log(`[Supervisor] Idle wave created: ${waveId} (no directive)`);
96
+ return state;
97
+ }
98
+
91
99
  this.spawnSupervisor(state);
92
100
  return state;
93
101
  }
@@ -133,6 +141,25 @@ class SupervisorHeartbeat {
133
141
 
134
142
  state.pendingDirectives.push(directive);
135
143
  console.log(`[Supervisor] Directive queued for wave ${waveId}: ${text.slice(0, 80)}`);
144
+
145
+ // If supervisor is stopped (agent finished or idle wave), wake it up
146
+ if (state.status === 'stopped') {
147
+ // Update the wave's directive if it was empty (idle wave first message)
148
+ if (!state.directive) {
149
+ state.directive = text;
150
+ }
151
+ state.crashCount = 0;
152
+
153
+ // Dual Mode: Conversation vs Dispatch (code-level enforcement)
154
+ // If directive looks like a question/status check → spawn conversation mode
155
+ // If directive looks like a task → spawn full supervisor with dispatch tools
156
+ if (this.isConversationDirective(text)) {
157
+ this.spawnConversation(state, text);
158
+ } else {
159
+ this.scheduleRestart(state, 0);
160
+ }
161
+ }
162
+
136
163
  return directive;
137
164
  }
138
165
 
@@ -215,6 +242,120 @@ class SupervisorHeartbeat {
215
242
  .filter(s => s.status === 'running' || s.status === 'starting' || s.status === 'restarting');
216
243
  }
217
244
 
245
+ /* ─── Internal: Dual Mode ─────────────────── */
246
+
247
+ /**
248
+ * Heuristic: is this directive a question/status check (conversation)
249
+ * or a work task (needs dispatch)?
250
+ *
251
+ * Conversation signals: question marks, status keywords, short length
252
+ * Dispatch signals: imperative verbs (만들어, 수정해, 구현해), long directives
253
+ */
254
+ private isConversationDirective(text: string): boolean {
255
+ const t = text.trim();
256
+
257
+ // Short messages with question marks → conversation
258
+ if (t.includes('?') && t.length < 100) return true;
259
+
260
+ // Korean question patterns
261
+ const questionPatterns = [
262
+ /확인해/, /알려줘/, /보여줘/, /어때/, /뭐야/, /뭐지/, /뭘까/,
263
+ /상태/, /상황/, /진행/, /현재/, /어디/, /얼마/,
264
+ /what/i, /how.*going/i, /status/i, /check/i, /show/i, /tell/i,
265
+ ];
266
+ if (questionPatterns.some(p => p.test(t))) return true;
267
+
268
+ // Long directives with action verbs → dispatch
269
+ const taskPatterns = [
270
+ /만들어/, /구현해/, /개발해/, /수정해/, /변경해/, /리팩토링/,
271
+ /설계해/, /작성해/, /배포해/, /테스트해/, /고쳐/,
272
+ /build/i, /create/i, /implement/i, /develop/i, /fix/i, /deploy/i, /refactor/i,
273
+ ];
274
+ if (taskPatterns.some(p => p.test(t))) return false;
275
+
276
+ // Default: short → conversation, long → dispatch
277
+ return t.length < 60;
278
+ }
279
+
280
+ /**
281
+ * Spawn a lightweight conversation session (no dispatch tools).
282
+ * CEO reads files and answers directly.
283
+ */
284
+ private spawnConversation(state: SupervisorState, directive: string): void {
285
+ // Build conversation context: previous directives + last execution summary
286
+ const deliveredDirectives = state.pendingDirectives.filter(d => d.delivered);
287
+ const directiveHistory = deliveredDirectives.length > 0
288
+ ? deliveredDirectives.map(d => `- CEO: "${d.text}"`).join('\n')
289
+ : '';
290
+
291
+ // Extract last execution's output from activity stream (what "just happened")
292
+ let lastExecutionSummary = '';
293
+ if (state.supervisorSessionId) {
294
+ try {
295
+ const events = ActivityStream.readAll(state.supervisorSessionId);
296
+ // Get last text outputs (the supervisor's final response)
297
+ const textEvents = events.filter(e => e.type === 'text' && e.roleId === 'ceo');
298
+ const toolEvents = events.filter(e => e.type === 'tool:start' && e.roleId === 'ceo');
299
+
300
+ // Summarize: what tools were used + final text
301
+ const toolSummary = toolEvents.slice(-10).map(e => {
302
+ const name = (e.data.name as string) ?? '';
303
+ const inp = e.data.input as Record<string, unknown> | undefined;
304
+ const detail = inp?.file_path ?? inp?.command ?? '';
305
+ return ` → ${name} ${String(detail).slice(0, 60)}`;
306
+ }).join('\n');
307
+
308
+ const lastText = textEvents.slice(-5).map(e => String(e.data.text ?? '')).join('').slice(-500);
309
+
310
+ if (toolSummary || lastText) {
311
+ lastExecutionSummary = `\n[Previous execution in this wave]\nTools used:\n${toolSummary}\n\nLast response:\n${lastText.slice(0, 500)}\n`;
312
+ }
313
+ } catch { /* ignore */ }
314
+ }
315
+
316
+ const context = [directiveHistory, lastExecutionSummary].filter(Boolean).join('\n');
317
+
318
+ const task = `${context ? context + '\n' : ''}[CEO Question] ${directive}
319
+
320
+ You are the CEO's AI assistant. The above shows what happened previously in this wave.
321
+ Answer the CEO's question based on context. Be specific — reference files, results, and actions from the previous execution.
322
+ Do NOT dispatch anyone. Do NOT create new files. Just answer concisely.`;
323
+
324
+ // Reuse session
325
+ let sessionId = state.supervisorSessionId;
326
+ if (!sessionId || !getSession(sessionId)) {
327
+ const session = createSession('ceo', {
328
+ mode: 'do',
329
+ source: 'wave',
330
+ waveId: state.waveId,
331
+ });
332
+ sessionId = session.id;
333
+ state.supervisorSessionId = sessionId;
334
+ }
335
+
336
+ state.status = 'running';
337
+
338
+ try {
339
+ const exec = executionManager.startExecution({
340
+ type: 'assign', // assign = no supervisor tools (dispatch/watch/amend)
341
+ roleId: 'ceo',
342
+ task,
343
+ sourceRole: 'ceo',
344
+ readOnly: true, // readOnly = no code changes, conversation only
345
+ sessionId,
346
+ });
347
+
348
+ state.executionId = exec.id;
349
+ this.watchExecution(state, exec);
350
+
351
+ console.log(`[Supervisor] Conversation mode for wave ${state.waveId} | directive: ${directive.slice(0, 60)}`);
352
+ } catch (err) {
353
+ console.error(`[Supervisor] Conversation spawn failed:`, err);
354
+ // Fallback to full supervisor
355
+ this.scheduleRestart(state, 0);
356
+ }
357
+ }
358
+
218
359
  /* ─── Internal: Spawn / Restart ────────────── */
219
360
 
220
361
  private spawnSupervisor(state: SupervisorState): void {
@@ -245,12 +386,45 @@ class SupervisorHeartbeat {
245
386
  ? `\n\n⚠️ [RECOVERY] This is a restart after crash #${state.crashCount}. Check all session states via supervision watch.`
246
387
  : '';
247
388
 
248
- const supervisorTask = `[CEO Supervisor] ${state.directive}
389
+ // Build conversation context from previous directives
390
+ const deliveredDirectives = state.pendingDirectives.filter(d => d.delivered);
391
+ const conversationHistory = deliveredDirectives.length > 0
392
+ ? `\n## Previous Conversation in This Wave
393
+ ${deliveredDirectives.map((d, i) => `${i + 1}. CEO said: "${d.text}"`).join('\n')}
394
+
395
+ You are continuing this conversation. The CEO's latest message builds on the above context.
396
+ Do NOT re-analyze from scratch — reference your previous findings.\n`
397
+ : '';
249
398
 
399
+ const supervisorTask = `[CEO Supervisor] ${state.directive}
400
+ ${conversationHistory}
250
401
  ## Your Role
251
- You are the CEO Supervisor — the root of the supervision tree.
252
- Your job: dispatch C-Level roles, watch their progress, relay opinions between them,
253
- and ensure the CEO's directive is fulfilled.
402
+ You are the CEO Supervisor — the CEO's AI proxy.
403
+ You can answer questions directly OR dispatch C-Level roles for complex work.
404
+
405
+ ## Response Mode Decision (BEFORE dispatching)
406
+
407
+ ⛔ Dispatch is expensive (spawns entire teams). Judge first:
408
+
409
+ **1. Direct Answer** — Can YOU handle this without dispatching?
410
+ - Status check, progress report → Read files/docs yourself, answer directly
411
+ - Simple question → Answer directly
412
+ - Opinion request → Answer directly
413
+ - Clarification on previous work → Answer from context
414
+ → **Do NOT dispatch. Just answer.**
415
+
416
+ **2. Selective Dispatch** — Only specific C-Level(s) needed?
417
+ - "코드 수정해" → CTO only
418
+ - "디자인 개선해" → CBO only
419
+ - "테스트해봐" → CTO only (who dispatches QA)
420
+ → **Dispatch only the relevant C-Level(s).**
421
+
422
+ **3. Full Dispatch** — Multi-team collaboration required?
423
+ - "새 기능 만들어" → CTO + CBO
424
+ - "출시 준비해" → All C-Levels
425
+ → **Dispatch multiple C-Levels with clear tasks.**
426
+
427
+ **Default: Direct Answer first. Dispatch only when code changes or creative work is needed.**
254
428
 
255
429
  ## Available C-Level Roles
256
430
  ${cLevelList}
@@ -333,13 +507,14 @@ ${state.continuous ? `## Continuous Improvement Mode (ON)
333
507
  5. 사용자가 Stop을 누를 때까지 계속한다. 스스로 done 선언하지 마라.
334
508
 
335
509
  ` : ''}## Instructions
336
- 1. Analyze the directive and decide which C-Level roles to dispatch (not necessarily all)
337
- 2. Dispatch them with clear tasks
338
- 3. Enter supervision watch loop
339
- 4. Monitor, **actively relay results between teams**, course-correct
340
- 5. When subordinates report done **verify deliverables against requirements (G-09)**
341
- 6. If gaps existre-dispatch with specific feedback. Repeat 3-5.
342
- 7. Only when ALL requirements are met compile results and report`;
510
+ 1. **First: Apply Response Mode Decision** Can you answer directly? If yes, answer and report done.
511
+ 2. If dispatch is needed: decide which C-Level roles (not necessarily all)
512
+ 3. Dispatch with clear, specific tasks
513
+ 4. Enter supervision watch loop
514
+ 5. Monitor, **actively relay results between teams**, course-correct
515
+ 6. When subordinates report done **verify deliverables against requirements (G-09)**
516
+ 7. If gaps exist re-dispatch with specific feedback. Repeat 4-6.
517
+ 8. Only when ALL requirements are met → compile results and report`;
343
518
 
344
519
  // BUG-008 fix: Wave:Supervisor:Session = 1:1:1 invariant.
345
520
  // Reuse existing session on restart instead of creating a new one.