tycono 0.3.45-beta.2 → 0.3.45-beta.3

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.
Files changed (113) hide show
  1. package/README.md +191 -162
  2. package/bin/tycono.ts +42 -10
  3. package/package.json +21 -15
  4. package/packages/server/bin/cli.js +35 -0
  5. package/packages/server/bin/server.ts +183 -0
  6. package/{src → packages/server/src}/api/src/create-server.ts +11 -3
  7. package/{src → packages/server/src}/api/src/engine/agent-loop.ts +30 -7
  8. package/{src → packages/server/src}/api/src/engine/context-assembler.ts +122 -57
  9. package/{src → packages/server/src}/api/src/engine/llm-adapter.ts +10 -7
  10. package/{src → packages/server/src}/api/src/engine/org-tree.ts +43 -3
  11. package/{src → packages/server/src}/api/src/engine/runners/claude-cli.ts +37 -15
  12. package/{src → packages/server/src}/api/src/engine/runners/types.ts +6 -0
  13. package/{src → packages/server/src}/api/src/engine/tools/executor.ts +65 -9
  14. package/{src → packages/server/src}/api/src/routes/execute.ts +221 -17
  15. package/packages/server/src/api/src/services/claude-md-manager.ts +190 -0
  16. package/{src → packages/server/src}/api/src/services/company-config.ts +1 -0
  17. package/{src → packages/server/src}/api/src/services/digest-engine.ts +4 -1
  18. package/packages/server/src/api/src/services/dispatch-classifier.ts +179 -0
  19. package/{src → packages/server/src}/api/src/services/execution-manager.ts +227 -21
  20. package/{src → packages/server/src}/api/src/services/file-reader.ts +4 -1
  21. package/packages/server/src/api/src/services/preset-loader.ts +310 -0
  22. package/{src → packages/server/src}/api/src/services/supervisor-heartbeat.ts +89 -9
  23. package/{src → packages/server/src}/api/src/services/wave-multiplexer.ts +18 -8
  24. package/{src → packages/server/src}/api/src/services/wave-tracker.ts +25 -0
  25. package/packages/server/src/core/scaffolder.ts +620 -0
  26. package/{src → packages/server/src}/shared/types.ts +3 -1
  27. package/packages/server/templates/CLAUDE.md.tmpl +152 -0
  28. package/packages/server/templates/agentic-knowledge-base.md +355 -0
  29. package/src/api/src/services/claude-md-manager.ts +0 -94
  30. package/src/api/src/services/preset-loader.ts +0 -149
  31. package/templates/CLAUDE.md.tmpl +0 -239
  32. /package/{src/web → packages/pixel}/dist/assets/index-BJyiMGkM.js +0 -0
  33. /package/{src/web → packages/pixel}/dist/assets/index-BOuHc64o.css +0 -0
  34. /package/{src/web → packages/pixel}/dist/assets/index-DDPzbp9E.js +0 -0
  35. /package/{src/web → packages/pixel}/dist/assets/index-DVKWFwwK.css +0 -0
  36. /package/{src/web → packages/pixel}/dist/assets/preview-app-DZ6WxhDc.js +0 -0
  37. /package/{src/web → packages/pixel}/dist/index.html +0 -0
  38. /package/{src/web → packages/pixel}/dist/tyconoforge.js +0 -0
  39. /package/{src → packages/server/src}/api/package.json +0 -0
  40. /package/{src → packages/server/src}/api/src/create-app.ts +0 -0
  41. /package/{src → packages/server/src}/api/src/engine/authority-validator.ts +0 -0
  42. /package/{src → packages/server/src}/api/src/engine/index.ts +0 -0
  43. /package/{src → packages/server/src}/api/src/engine/knowledge-gate.ts +0 -0
  44. /package/{src → packages/server/src}/api/src/engine/role-lifecycle.ts +0 -0
  45. /package/{src → packages/server/src}/api/src/engine/runners/direct-api.ts +0 -0
  46. /package/{src → packages/server/src}/api/src/engine/runners/index.ts +0 -0
  47. /package/{src → packages/server/src}/api/src/engine/skill-template.ts +0 -0
  48. /package/{src → packages/server/src}/api/src/engine/tools/definitions.ts +0 -0
  49. /package/{src → packages/server/src}/api/src/routes/active-sessions.ts +0 -0
  50. /package/{src → packages/server/src}/api/src/routes/coins.ts +0 -0
  51. /package/{src → packages/server/src}/api/src/routes/company.ts +0 -0
  52. /package/{src → packages/server/src}/api/src/routes/cost.ts +0 -0
  53. /package/{src → packages/server/src}/api/src/routes/engine.ts +0 -0
  54. /package/{src → packages/server/src}/api/src/routes/git.ts +0 -0
  55. /package/{src → packages/server/src}/api/src/routes/knowledge.ts +0 -0
  56. /package/{src → packages/server/src}/api/src/routes/operations.ts +0 -0
  57. /package/{src → packages/server/src}/api/src/routes/preferences.ts +0 -0
  58. /package/{src → packages/server/src}/api/src/routes/presets.ts +0 -0
  59. /package/{src → packages/server/src}/api/src/routes/projects.ts +0 -0
  60. /package/{src → packages/server/src}/api/src/routes/quests.ts +0 -0
  61. /package/{src → packages/server/src}/api/src/routes/roles.ts +0 -0
  62. /package/{src → packages/server/src}/api/src/routes/save.ts +0 -0
  63. /package/{src → packages/server/src}/api/src/routes/sessions.ts +0 -0
  64. /package/{src → packages/server/src}/api/src/routes/setup.ts +0 -0
  65. /package/{src → packages/server/src}/api/src/routes/skills.ts +0 -0
  66. /package/{src → packages/server/src}/api/src/routes/speech.ts +0 -0
  67. /package/{src → packages/server/src}/api/src/routes/supervision.ts +0 -0
  68. /package/{src → packages/server/src}/api/src/routes/sync.ts +0 -0
  69. /package/{src → packages/server/src}/api/src/server.ts +0 -0
  70. /package/{src → packages/server/src}/api/src/services/activity-stream.ts +0 -0
  71. /package/{src → packages/server/src}/api/src/services/activity-tracker.ts +0 -0
  72. /package/{src → packages/server/src}/api/src/services/database.ts +0 -0
  73. /package/{src → packages/server/src}/api/src/services/git-save.ts +0 -0
  74. /package/{src → packages/server/src}/api/src/services/job-manager.ts +0 -0
  75. /package/{src → packages/server/src}/api/src/services/knowledge-importer.ts +0 -0
  76. /package/{src → packages/server/src}/api/src/services/markdown-parser.ts +0 -0
  77. /package/{src → packages/server/src}/api/src/services/port-registry.ts +0 -0
  78. /package/{src → packages/server/src}/api/src/services/preferences.ts +0 -0
  79. /package/{src → packages/server/src}/api/src/services/pricing.ts +0 -0
  80. /package/{src → packages/server/src}/api/src/services/scaffold.ts +0 -0
  81. /package/{src → packages/server/src}/api/src/services/session-store.ts +0 -0
  82. /package/{src → packages/server/src}/api/src/services/team-recommender.ts +0 -0
  83. /package/{src → packages/server/src}/api/src/services/token-ledger.ts +0 -0
  84. /package/{src → packages/server/src}/api/src/services/wave-messages.ts +0 -0
  85. /package/{src → packages/server/src}/api/src/utils/role-level.ts +0 -0
  86. /package/{templates → packages/server/templates}/company.md.tmpl +0 -0
  87. /package/{templates → packages/server/templates}/gitignore.tmpl +0 -0
  88. /package/{templates → packages/server/templates}/roles.md.tmpl +0 -0
  89. /package/{templates → packages/server/templates}/skills/_manifest.json +0 -0
  90. /package/{templates → packages/server/templates}/skills/agent-browser/SKILL.md +0 -0
  91. /package/{templates → packages/server/templates}/skills/agent-browser/meta.json +0 -0
  92. /package/{templates → packages/server/templates}/skills/akb-linter/SKILL.md +0 -0
  93. /package/{templates → packages/server/templates}/skills/akb-linter/meta.json +0 -0
  94. /package/{templates → packages/server/templates}/skills/knowledge-gate/SKILL.md +0 -0
  95. /package/{templates → packages/server/templates}/skills/knowledge-gate/meta.json +0 -0
  96. /package/{templates → packages/server/templates}/teams/agency.json +0 -0
  97. /package/{templates → packages/server/templates}/teams/research.json +0 -0
  98. /package/{templates → packages/server/templates}/teams/startup.json +0 -0
  99. /package/{src/tui → packages/tui/src}/api.ts +0 -0
  100. /package/{src/tui → packages/tui/src}/app.tsx +0 -0
  101. /package/{src/tui → packages/tui/src}/components/CommandMode.tsx +0 -0
  102. /package/{src/tui → packages/tui/src}/components/OrgTree.tsx +0 -0
  103. /package/{src/tui → packages/tui/src}/components/PanelMode.tsx +0 -0
  104. /package/{src/tui → packages/tui/src}/components/SetupWizard.tsx +0 -0
  105. /package/{src/tui → packages/tui/src}/components/StatusBar.tsx +0 -0
  106. /package/{src/tui → packages/tui/src}/components/StreamView.tsx +0 -0
  107. /package/{src/tui → packages/tui/src}/hooks/useApi.ts +0 -0
  108. /package/{src/tui → packages/tui/src}/hooks/useCommand.ts +0 -0
  109. /package/{src/tui → packages/tui/src}/hooks/useSSE.ts +0 -0
  110. /package/{src/tui → packages/tui/src}/index.tsx +0 -0
  111. /package/{src/tui → packages/tui/src}/store.ts +0 -0
  112. /package/{src/tui → packages/tui/src}/theme.ts +0 -0
  113. /package/{src/tui → packages/tui/src}/utils/markdown.tsx +0 -0
@@ -2,6 +2,8 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { COMPANY_ROOT } from '../services/file-reader.js';
5
+ import { autoSelectPreset } from '../services/preset-loader.js';
6
+ import { readConfig } from '../services/company-config.js';
5
7
  // activity-tracker removed — executionManager is Single Source of Truth
6
8
  import { buildOrgTree, canDispatchTo, getSubordinates } from '../engine/org-tree.js';
7
9
  import { createRunner, type RunnerResult } from '../engine/runners/index.js';
@@ -21,6 +23,7 @@ import { earnCoinsInternal } from './coins.js';
21
23
  import { appendFollowUpToWave } from '../services/wave-tracker.js';
22
24
  import { waveMultiplexer } from '../services/wave-multiplexer.js';
23
25
  import { supervisorHeartbeat } from '../services/supervisor-heartbeat.js';
26
+ import { decideDispatchOrAmend } from '../services/dispatch-classifier.js';
24
27
 
25
28
  /* ─── Auto-attach child executions to wave multiplexer ── */
26
29
  executionManager.onExecutionCreated((exec) => {
@@ -126,6 +129,32 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
126
129
  return;
127
130
  }
128
131
 
132
+ // ── GET /api/waves/:waveId — Single wave status ──
133
+ const waveDetailMatch = url.match(/^\/api\/waves\/([^/]+)$/);
134
+ if (method === 'GET' && waveDetailMatch) {
135
+ const waveId = waveDetailMatch[1];
136
+ // Try active waves first
137
+ const activeWaves = waveMultiplexer.getActiveWaves();
138
+ const active = activeWaves.find((w: { waveId: string }) => w.waveId === waveId);
139
+ if (active) {
140
+ jsonResponse(res, 200, active);
141
+ return;
142
+ }
143
+ // Fallback: read wave file from disk
144
+ const wavePath = path.join(COMPANY_ROOT, '.tycono', 'waves', `${waveId}.json`);
145
+ if (fs.existsSync(wavePath)) {
146
+ try {
147
+ const waveData = JSON.parse(fs.readFileSync(wavePath, 'utf-8'));
148
+ jsonResponse(res, 200, waveData);
149
+ } catch {
150
+ jsonResponse(res, 500, { error: 'Failed to read wave file' });
151
+ }
152
+ return;
153
+ }
154
+ jsonResponse(res, 404, { error: 'Wave not found' });
155
+ return;
156
+ }
157
+
129
158
  // ── Legacy /api/exec/* routes ──
130
159
  const sessionMatch = url.match(/\/api\/exec\/session\/([^/]+)\/message$/);
131
160
 
@@ -156,31 +185,47 @@ function handleJobsRequest(url: string, method: string, req: IncomingMessage, re
156
185
  return;
157
186
  }
158
187
 
159
- // GET /api/jobs/:id — internal only
188
+ // GET /api/jobs/:id — internal only (dispatch bridge --check)
160
189
  const jobMatch = reqPath.match(/^\/api\/jobs\/([^/]+)$/);
161
190
  if (method === 'GET' && jobMatch) {
162
191
  const id = jobMatch[1];
163
192
  const exec = executionManager.getExecution(id) ?? executionManager.getActiveExecution(id);
164
193
  if (!exec) {
165
- // Try reading from stream file directly
194
+ // Fallback: read from activity-stream file on disk
166
195
  if (ActivityStream.exists(id)) {
167
196
  const events = ActivityStream.readAll(id);
168
- res.writeHead(200, { 'Content-Type': 'application/json' });
169
- res.end(JSON.stringify({ id, events }));
197
+ const doneEvent = [...events].reverse().find(e => e.type === 'msg:done' || e.type === 'msg:error');
198
+ const output = doneEvent?.data?.output as string ?? '';
199
+ const status = doneEvent?.type === 'msg:done' ? 'done' : doneEvent?.type === 'msg:error' ? 'error' : 'unknown';
200
+ jsonResponse(res, 200, { id, status, output, fromStream: true });
170
201
  } else {
171
- res.writeHead(404);
172
- res.end(JSON.stringify({ error: 'Not found' }));
202
+ jsonResponse(res, 404, { error: 'Not found' });
173
203
  }
174
204
  } else {
175
- res.writeHead(200, { 'Content-Type': 'application/json' });
176
- res.end(JSON.stringify({
205
+ // Include output from result if available
206
+ const output = exec.result?.output?.slice(-2000) ?? '';
207
+ jsonResponse(res, 200, {
177
208
  id: exec.id,
178
209
  roleId: exec.roleId,
179
210
  task: exec.task,
180
211
  status: exec.status,
181
212
  sessionId: exec.sessionId,
182
213
  createdAt: exec.createdAt,
183
- }));
214
+ output,
215
+ });
216
+ }
217
+ return;
218
+ }
219
+
220
+ // GET /api/jobs/:id/history — activity-stream events (dispatch bridge get_result)
221
+ const historyMatch = reqPath.match(/^\/api\/jobs\/([^/]+)\/history$/);
222
+ if (method === 'GET' && historyMatch) {
223
+ const id = historyMatch[1];
224
+ if (ActivityStream.exists(id)) {
225
+ const events = ActivityStream.readAll(id);
226
+ jsonResponse(res, 200, { id, events });
227
+ } else {
228
+ jsonResponse(res, 404, { error: 'Stream not found' });
184
229
  }
185
230
  return;
186
231
  }
@@ -206,7 +251,7 @@ function handleJobsRequest(url: string, method: string, req: IncomingMessage, re
206
251
 
207
252
  /* ─── POST /api/jobs ─────────────────────── */
208
253
 
209
- function handleStartJob(body: Record<string, unknown>, res: ServerResponse): void {
254
+ async function handleStartJob(body: Record<string, unknown>, res: ServerResponse): Promise<void> {
210
255
  const type = (body.type as string) ?? 'assign';
211
256
  const roleId = body.roleId as string;
212
257
  const task = body.task as string;
@@ -224,6 +269,12 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
224
269
  const targetRoles = body.targetRoles as string[] | undefined;
225
270
  const continuous = body.continuous === true;
226
271
  const preset = body.preset as string | undefined;
272
+ const permissionMode = body.permissionMode as string | undefined;
273
+
274
+ // Set permission mode for agent runners (auto = model-based safety, bypassPermissions = full access)
275
+ if (permissionMode) {
276
+ process.env.TYCONO_PERMISSION_MODE = permissionMode;
277
+ }
227
278
 
228
279
  // Always use supervisor mode — CEO supervises C-Levels who supervise members
229
280
  {
@@ -256,12 +307,76 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
256
307
  return;
257
308
  }
258
309
 
259
- const orgTree = buildOrgTree(COMPANY_ROOT);
310
+ // Resolve preset from wave for correct org tree (includes agency roles)
311
+ let presetId: string | undefined;
312
+ if (waveId) {
313
+ try {
314
+ const wavePath = path.join(COMPANY_ROOT, '.tycono', 'waves', `${waveId}.json`);
315
+ if (fs.existsSync(wavePath)) {
316
+ presetId = JSON.parse(fs.readFileSync(wavePath, 'utf-8')).preset;
317
+ }
318
+ } catch { /* ignore */ }
319
+ }
320
+ const orgTree = buildOrgTree(COMPANY_ROOT, presetId);
260
321
  if (!canDispatchTo(orgTree, sourceRole, roleId)) {
261
- jsonResponse(res, 403, { error: `${sourceRole} cannot dispatch to ${roleId}.` });
322
+ const errorMsg = `${sourceRole} cannot dispatch to ${roleId}`;
323
+ // Emit dispatch:error on parent's activity stream so it surfaces in SSE
324
+ if (parentSessionId) {
325
+ const parentStream = ActivityStream.getOrCreate(parentSessionId, sourceRole);
326
+ parentStream.emit('dispatch:error', sourceRole, {
327
+ sourceRole,
328
+ targetRole: roleId,
329
+ error: errorMsg,
330
+ timestamp: Date.now(),
331
+ });
332
+ }
333
+ console.warn(`[Dispatch:Error] ${errorMsg} (parent=${parentSessionId ?? 'none'}, wave=${waveId ?? 'none'})`);
334
+ jsonResponse(res, 403, { error: `${errorMsg}.` });
262
335
  return;
263
336
  }
264
337
 
338
+ // Auto-amend: check if we should amend an existing session instead of creating a new one
339
+ if (!readOnly && waveId) {
340
+ try {
341
+ const decision = await decideDispatchOrAmend(waveId, roleId, sourceRole, task);
342
+ if (decision.action === 'amend' && decision.prevSessionId) {
343
+ console.log(`[AutoAmend] Converting dispatch to amend: ${roleId} → ${decision.prevSessionId} (${decision.reason})`);
344
+ const amendedExec = executionManager.continueSession(
345
+ decision.prevSessionId,
346
+ `[FOLLOW-UP from ${sourceRole}] ${task}`,
347
+ sourceRole,
348
+ );
349
+ if (amendedExec) {
350
+ jsonResponse(res, 200, {
351
+ sessionId: decision.prevSessionId,
352
+ executionId: amendedExec.id,
353
+ status: 'running',
354
+ autoAmend: true,
355
+ reason: decision.reason,
356
+ });
357
+ return;
358
+ }
359
+ // continueSession failed — if role is active, do NOT create new session (1-session invariant)
360
+ if (decision.reason === 'role-already-active') {
361
+ console.warn(`[Dispatch] ${roleId}: active session ${decision.prevSessionId} cannot be amended yet (running). Queued.`);
362
+ // Store as pending amendment on the session — will be processed when execution completes
363
+ executionManager.queueAmendment(decision.prevSessionId!, `[FOLLOW-UP from ${sourceRole}] ${task}`);
364
+ jsonResponse(res, 200, {
365
+ sessionId: decision.prevSessionId,
366
+ status: 'queued',
367
+ autoAmend: true,
368
+ reason: 'amendment-queued',
369
+ });
370
+ return;
371
+ }
372
+ // done session amend failed — fall through to new dispatch as last resort
373
+ console.warn(`[AutoAmend] continueSession failed for ${decision.prevSessionId}, falling back to new dispatch`);
374
+ }
375
+ } catch (err) {
376
+ console.warn('[AutoAmend] Decision failed, proceeding with new dispatch:', err);
377
+ }
378
+ }
379
+
265
380
  const sessionSource: 'wave' | 'dispatch' = waveId ? 'wave' : 'dispatch';
266
381
  const session = createSession(roleId, {
267
382
  mode: readOnly ? 'talk' : 'do',
@@ -459,7 +574,31 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
459
574
  }
460
575
  }
461
576
 
462
- const waveJson = {
577
+ // Collect dispatch statistics
578
+ const dispatchStats = {
579
+ attempted: 0,
580
+ succeeded: 0,
581
+ failed: 0,
582
+ errors: [] as Array<{ sourceRole: string; targetRole: string; error: string }>,
583
+ };
584
+ for (const role of rolesData) {
585
+ for (const e of role.events) {
586
+ if (e.type === 'dispatch:start') {
587
+ dispatchStats.attempted++;
588
+ dispatchStats.succeeded++;
589
+ } else if (e.type === 'dispatch:error') {
590
+ dispatchStats.attempted++;
591
+ dispatchStats.failed++;
592
+ dispatchStats.errors.push({
593
+ sourceRole: (e.data.sourceRole as string) ?? 'unknown',
594
+ targetRole: (e.data.targetRole as string) ?? 'unknown',
595
+ error: (e.data.error as string) ?? 'unknown',
596
+ });
597
+ }
598
+ }
599
+ }
600
+
601
+ const waveJson: Record<string, unknown> = {
463
602
  id: baseName,
464
603
  directive,
465
604
  startedAt: startedAt.toISOString(),
@@ -468,6 +607,7 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
468
607
  ...(waveId && { waveId }),
469
608
  sessionIds: allSessionIds,
470
609
  };
610
+ if (dispatchStats.attempted > 0) waveJson.dispatch = dispatchStats;
471
611
  fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
472
612
 
473
613
  const roleCount = rolesData.length;
@@ -636,6 +776,9 @@ function handleAssign(body: Record<string, unknown>, req: IncomingMessage, res:
636
776
  case 'dispatch:start':
637
777
  sendSSE(res, 'dispatch', { roleId: event.data.targetRoleId, task: event.data.task, childSessionId: event.data.childSessionId });
638
778
  break;
779
+ case 'dispatch:error':
780
+ sendSSE(res, 'dispatch:error', { sourceRole: event.data.sourceRole, targetRole: event.data.targetRole, error: event.data.error, timestamp: event.data.timestamp });
781
+ break;
639
782
  case 'msg:turn-complete':
640
783
  sendSSE(res, 'turn', { turn: event.data.turn });
641
784
  break;
@@ -680,21 +823,79 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
680
823
 
681
824
  const targetRoles = body.targetRoles as string[] | undefined;
682
825
  const continuous = body.continuous === true;
826
+ let preset = body.preset as string | undefined;
827
+
828
+ // Agency resolution priority: --agency flag > config.defaultAgency > auto-select
829
+ if (!preset) {
830
+ const config = readConfig(COMPANY_ROOT);
831
+ if (config.defaultAgency) {
832
+ preset = config.defaultAgency;
833
+ console.log(`[Wave] Using default agency: ${preset} (from .tycono/config.json)`);
834
+ } else {
835
+ preset = autoSelectPreset(COMPANY_ROOT, directive);
836
+ if (preset) {
837
+ console.log(`[Wave] Auto-selected agency: ${preset} (from directive keywords)`);
838
+ }
839
+ }
840
+ }
841
+
842
+ // BUG-FORKBOMB: Check for active waves — amend existing CEO instead of creating new wave
843
+ const activeWaves = waveMultiplexer.getActiveWaves();
844
+ if (activeWaves.length > 0) {
845
+ const existingWave = activeWaves[0];
846
+ console.log(`[Wave] Active wave detected: ${existingWave.id}. Amending CEO instead of new wave.`);
847
+
848
+ // Find CEO session for existing wave
849
+ const ceoSession = listSessions().find(
850
+ s => s.waveId === existingWave.id && s.roleId === 'ceo' && s.status === 'active',
851
+ );
852
+
853
+ if (ceoSession) {
854
+ const amendedExec = executionManager.continueSession(
855
+ ceoSession.id,
856
+ `[ADDITIONAL DIRECTIVE] ${directive}`,
857
+ 'user',
858
+ );
859
+ if (amendedExec) {
860
+ jsonResponse(res, 200, {
861
+ waveId: existingWave.id,
862
+ sessionId: ceoSession.id,
863
+ executionId: amendedExec.id,
864
+ amended: true,
865
+ message: `Amended existing wave ${existingWave.id}`,
866
+ });
867
+ return;
868
+ }
869
+ // CEO is running — queue the amendment
870
+ executionManager.queueAmendment(ceoSession.id, `[ADDITIONAL DIRECTIVE] ${directive}`);
871
+ jsonResponse(res, 200, {
872
+ waveId: existingWave.id,
873
+ sessionId: ceoSession.id,
874
+ amended: true,
875
+ queued: true,
876
+ message: `Amendment queued for active wave ${existingWave.id}`,
877
+ });
878
+ return;
879
+ }
880
+ // No CEO session found — unusual, proceed with new wave
881
+ console.warn(`[Wave] Active wave ${existingWave.id} has no CEO session. Creating new wave.`);
882
+ }
683
883
 
684
884
  // Always supervisor mode — CEO supervises C-Levels
685
- handleWaveSupervisor(directive, targetRoles, continuous, req, res);
885
+ handleWaveSupervisor(directive, targetRoles, continuous, req, res, preset);
686
886
  }
687
887
 
688
888
  /**
689
889
  * Supervisor mode: Start a single CEO Supervisor session that dispatches C-Levels.
690
890
  * The supervisor uses dispatch/watch/amend tools — same pattern as any supervisor node.
691
891
  */
692
- function handleWaveSupervisor(directive: string, targetRoles: string[] | undefined, continuous: boolean, req: IncomingMessage, res: ServerResponse): void {
892
+ function handleWaveSupervisor(directive: string, targetRoles: string[] | undefined, continuous: boolean, req: IncomingMessage, res: ServerResponse, preset?: string): void {
693
893
  const state = supervisorHeartbeat.start(
694
894
  `wave-${Date.now()}`,
695
895
  directive,
696
896
  targetRoles && targetRoles.length > 0 ? targetRoles : undefined,
697
897
  continuous,
898
+ preset,
698
899
  );
699
900
 
700
901
  if (state.status === 'error') {
@@ -733,11 +934,14 @@ function handleWaveDirective(waveId: string, body: Record<string, unknown>, res:
733
934
  }
734
935
 
735
936
  if (!directive) {
736
- jsonResponse(res, 404, { error: `No active supervisor for wave ${waveId}` });
937
+ jsonResponse(res, 404, { error: `No active supervisor for wave ${waveId}. The wave may have been cleaned up.` });
737
938
  return;
738
939
  }
739
940
 
740
- jsonResponse(res, 200, { directive });
941
+ // Provide status context so caller knows what's happening
942
+ const state = supervisorHeartbeat.getState(waveId);
943
+ const status = state?.status ?? 'unknown';
944
+ jsonResponse(res, 200, { directive, supervisorStatus: status });
741
945
  }
742
946
 
743
947
  /* ─── POST /api/waves/:waveId/question ──────── */
@@ -0,0 +1,190 @@
1
+ /**
2
+ * claude-md-manager.ts — CLAUDE.md lifecycle management
3
+ *
4
+ * Three modes:
5
+ * 1. No CLAUDE.md → create from AKB template
6
+ * 2. User-owned CLAUDE.md (no tycono:managed marker) → append AKB section
7
+ * 3. Tycono-managed CLAUDE.md → full replace on version change
8
+ *
9
+ * Also installs methodology/agentic-knowledge-base.md if missing.
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const TEMPLATES_DIR = path.resolve(__dirname, '../../../../templates');
18
+
19
+ const AKB_SECTION_MARKER = '<!-- tycono:akb-guide -->';
20
+
21
+ function getPackageVersion(): string {
22
+ const pkgPath = path.resolve(__dirname, '../../../../package.json');
23
+ try {
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
25
+ return pkg.version || '0.0.0';
26
+ } catch {
27
+ return '0.0.0';
28
+ }
29
+ }
30
+
31
+ function generateClaudeMd(version: string): string {
32
+ const tmplPath = path.join(TEMPLATES_DIR, 'CLAUDE.md.tmpl');
33
+ const template = fs.readFileSync(tmplPath, 'utf-8');
34
+ return template.replaceAll('{{VERSION}}', version);
35
+ }
36
+
37
+ /**
38
+ * Generate the AKB appendix section for user-owned CLAUDE.md files.
39
+ * This is a condensed version of the key AKB principles.
40
+ */
41
+ function generateAkbAppendix(version: string): string {
42
+ return `
43
+ ${AKB_SECTION_MARKER}
44
+
45
+ ---
46
+
47
+ ## AKB Knowledge Navigation (Tycono)
48
+
49
+ > Auto-appended by Tycono v${version}. Your content above is untouched.
50
+ > Full reference: \`methodology/agentic-knowledge-base.md\`
51
+
52
+ ### Structure: Root → Hub → Node
53
+
54
+ | Layer | Role | AI Usage |
55
+ |-------|------|----------|
56
+ | **Root** (CLAUDE.md) | Minimal routing | Auto-injected as system prompt |
57
+ | **Hub** ({folder}.md) | Human TOC + guides | **Check before starting work** |
58
+ | **Node** (*.md) | Actual information | Direct search via Grep/Glob |
59
+
60
+ ### Hub-First Principle
61
+
62
+ > ⛔ **Read Hub document BEFORE implementing/testing**
63
+
64
+ | Situation | Read First | What to Find |
65
+ |-----------|------------|--------------|
66
+ | Debugging/Testing | Hub → guides/ | Existing debug tools |
67
+ | API Calls | Hub → related Node | Documented methods |
68
+ | New Feature | Hub | Similar existing features |
69
+ | Strategy/Design | Hub + detail docs | Design philosophy, past decisions |
70
+
71
+ ### Anti-Patterns
72
+
73
+ \`\`\`
74
+ ❌ Skip Hub → code directly
75
+ ❌ Skip Hub → Write from scratch
76
+ ❌ Read only Hubs → "sufficient" judgment → superficial answers
77
+ \`\`\`
78
+
79
+ ### Exploration Depth
80
+
81
+ | Question Type | Minimum | Additional |
82
+ |---------------|---------|------------|
83
+ | Implementation | Hub | Related Nodes |
84
+ | **Strategy/Ideas** | Hub | **Design philosophy, core problems, phase docs** |
85
+ | **Connecting A and B** | **Both Hubs** | **Both core docs** |
86
+
87
+ ### Knowledge Gate
88
+
89
+ > Before creating a new document, search existing docs first (grep 3+ keywords).
90
+
91
+ <!-- tycono:akb-guide-end -->
92
+ `;
93
+ }
94
+
95
+ /**
96
+ * Install methodology/agentic-knowledge-base.md if not present.
97
+ */
98
+ function ensureAkbMethodology(companyRoot: string): void {
99
+ const targetDir = path.join(companyRoot, 'knowledge', 'methodology');
100
+ const targetPath = path.join(targetDir, 'agentic-knowledge-base.md');
101
+
102
+ if (fs.existsSync(targetPath)) return;
103
+
104
+ // Also check methodologies/ (plural)
105
+ const altDir = path.join(companyRoot, 'knowledge', 'methodologies');
106
+ const altPath = path.join(altDir, 'agentic-knowledge-base.md');
107
+ if (fs.existsSync(altPath)) return;
108
+
109
+ // Try to copy from templates
110
+ const srcPath = path.join(TEMPLATES_DIR, 'agentic-knowledge-base.md');
111
+ if (!fs.existsSync(srcPath)) return;
112
+
113
+ // Use whichever directory exists, or create methodology/
114
+ const dir = fs.existsSync(altDir) ? altDir : fs.existsSync(targetDir) ? targetDir : targetDir;
115
+ if (!fs.existsSync(dir)) {
116
+ fs.mkdirSync(dir, { recursive: true });
117
+ }
118
+
119
+ fs.copyFileSync(srcPath, path.join(dir, 'agentic-knowledge-base.md'));
120
+ console.log(`[AKB] Installed methodology/agentic-knowledge-base.md`);
121
+ }
122
+
123
+ /**
124
+ * Ensure CLAUDE.md has AKB navigation guide.
125
+ *
126
+ * Two modes:
127
+ * 1. No CLAUDE.md → create from full AKB template
128
+ * 2. CLAUDE.md exists (any) → append/update AKB section only, never touch user content
129
+ *
130
+ * No full replacement ever. User content is always preserved.
131
+ */
132
+ export function ensureClaudeMd(companyRoot: string): void {
133
+ const tyconoDir = path.join(companyRoot, '.tycono');
134
+ const rulesVersionPath = path.join(tyconoDir, 'rules-version');
135
+ const claudeMdPath = path.join(companyRoot, 'knowledge', 'CLAUDE.md');
136
+ const knowledgeDir = path.join(companyRoot, 'knowledge');
137
+ const customRulesPath = path.join(knowledgeDir, 'custom-rules.md');
138
+
139
+ // Skip if not initialized (no .tycono/ directory)
140
+ if (!fs.existsSync(tyconoDir)) return;
141
+
142
+ const currentVersion = getPackageVersion();
143
+
144
+ // Read stored version
145
+ let storedVersion = '0.0.0';
146
+ if (fs.existsSync(rulesVersionPath)) {
147
+ storedVersion = fs.readFileSync(rulesVersionPath, 'utf-8').trim();
148
+ }
149
+
150
+ // Skip if already up-to-date
151
+ if (storedVersion === currentVersion) return;
152
+
153
+ // No knowledge/ directory → don't create anything (plugin mode: zero footprint)
154
+ if (!fs.existsSync(knowledgeDir)) {
155
+ console.log(`[CLAUDE.md] Skipping — no knowledge/ directory (plugin mode)`);
156
+ fs.writeFileSync(rulesVersionPath, currentVersion);
157
+ return;
158
+ }
159
+
160
+ // No CLAUDE.md → don't create (user hasn't set up AKB)
161
+ if (!fs.existsSync(claudeMdPath)) {
162
+ console.log(`[CLAUDE.md] Skipping — no CLAUDE.md found`);
163
+ fs.writeFileSync(rulesVersionPath, currentVersion);
164
+ return;
165
+ }
166
+
167
+ // CLAUDE.md exists → append/update AKB section only, never create files
168
+ ensureAkbMethodology(companyRoot);
169
+
170
+ const existing = fs.readFileSync(claudeMdPath, 'utf-8');
171
+
172
+ if (existing.includes(AKB_SECTION_MARKER)) {
173
+ // Already has AKB section → replace just that section
174
+ const before = existing.split(AKB_SECTION_MARKER)[0].trimEnd();
175
+ const afterMarker = existing.indexOf('<!-- tycono:akb-guide-end -->');
176
+ const after = afterMarker >= 0
177
+ ? existing.substring(afterMarker + '<!-- tycono:akb-guide-end -->'.length).trimStart()
178
+ : '';
179
+ const updated = before + generateAkbAppendix(currentVersion) + (after ? '\n' + after : '');
180
+ fs.writeFileSync(claudeMdPath, updated);
181
+ console.log(`[CLAUDE.md] Updated AKB section (v${currentVersion})`);
182
+ } else {
183
+ // No AKB section yet → append
184
+ const updated = existing.trimEnd() + '\n' + generateAkbAppendix(currentVersion);
185
+ fs.writeFileSync(claudeMdPath, updated);
186
+ console.log(`[CLAUDE.md] Appended AKB guide (v${currentVersion})`);
187
+ }
188
+
189
+ fs.writeFileSync(rulesVersionPath, currentVersion);
190
+ }
@@ -20,6 +20,7 @@ export interface CompanyConfig {
20
20
  model?: string;
21
21
  apiKey?: string;
22
22
  codeRoot?: string; // 코드 프로젝트 경로 (AKB와 분리된 코드 repo)
23
+ defaultAgency?: string; // 기본 agency — /tycono에서 --agency 없을 때 사용
23
24
  conversationLimits?: Partial<ConversationLimits>;
24
25
  supervision?: {
25
26
  mode: 'supervisor' | 'direct';
@@ -12,7 +12,7 @@ import type { ActivityEvent, ActivityEventType } from '../../../shared/types.js'
12
12
  /* ─── Types ──────────────────────────────────── */
13
13
 
14
14
  export interface Anomaly {
15
- type: 'error' | 'stall' | 'scope_creep' | 'awaiting_input' | 'budget_warning' | 'ceo_directive';
15
+ type: 'error' | 'stall' | 'scope_creep' | 'awaiting_input' | 'budget_warning' | 'ceo_directive' | 'dispatch_error';
16
16
  sessionId: string;
17
17
  message: string;
18
18
  severity: number; // 0-10
@@ -37,6 +37,7 @@ const EVENT_TIER_MAP: Partial<Record<ActivityEventType, EventTier>> = {
37
37
  'msg:awaiting_input': 'critical',
38
38
  'dispatch:start': 'high',
39
39
  'dispatch:done': 'high',
40
+ 'dispatch:error': 'critical',
40
41
  'msg:done': 'high',
41
42
  'msg:start': 'high',
42
43
  'thinking': 'medium',
@@ -197,6 +198,8 @@ function summarizeEvent(event: ActivityEvent): string | null {
197
198
  return `Dispatched → ${event.data?.targetRoleId}: ${(event.data?.task as string ?? '').slice(0, 60)}`;
198
199
  case 'dispatch:done':
199
200
  return `Dispatch completed: ${event.data?.targetRoleId}`;
201
+ case 'dispatch:error':
202
+ return `❌ Dispatch FAILED: ${event.data?.sourceRole} → ${event.data?.targetRole}: ${(event.data?.error as string ?? 'unknown').slice(0, 80)}`;
200
203
  case 'tool:start': {
201
204
  const toolName = event.data?.name as string ?? 'unknown';
202
205
  const input = event.data?.input as Record<string, unknown> | undefined;