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.
- package/README.md +191 -162
- package/bin/tycono.ts +42 -10
- package/package.json +21 -15
- package/packages/server/bin/cli.js +35 -0
- package/packages/server/bin/server.ts +183 -0
- package/{src → packages/server/src}/api/src/create-server.ts +11 -3
- package/{src → packages/server/src}/api/src/engine/agent-loop.ts +30 -7
- package/{src → packages/server/src}/api/src/engine/context-assembler.ts +122 -57
- package/{src → packages/server/src}/api/src/engine/llm-adapter.ts +10 -7
- package/{src → packages/server/src}/api/src/engine/org-tree.ts +43 -3
- package/{src → packages/server/src}/api/src/engine/runners/claude-cli.ts +37 -15
- package/{src → packages/server/src}/api/src/engine/runners/types.ts +6 -0
- package/{src → packages/server/src}/api/src/engine/tools/executor.ts +65 -9
- package/{src → packages/server/src}/api/src/routes/execute.ts +221 -17
- package/packages/server/src/api/src/services/claude-md-manager.ts +190 -0
- package/{src → packages/server/src}/api/src/services/company-config.ts +1 -0
- package/{src → packages/server/src}/api/src/services/digest-engine.ts +4 -1
- package/packages/server/src/api/src/services/dispatch-classifier.ts +179 -0
- package/{src → packages/server/src}/api/src/services/execution-manager.ts +227 -21
- package/{src → packages/server/src}/api/src/services/file-reader.ts +4 -1
- package/packages/server/src/api/src/services/preset-loader.ts +310 -0
- package/{src → packages/server/src}/api/src/services/supervisor-heartbeat.ts +89 -9
- package/{src → packages/server/src}/api/src/services/wave-multiplexer.ts +18 -8
- package/{src → packages/server/src}/api/src/services/wave-tracker.ts +25 -0
- package/packages/server/src/core/scaffolder.ts +620 -0
- package/{src → packages/server/src}/shared/types.ts +3 -1
- package/packages/server/templates/CLAUDE.md.tmpl +152 -0
- package/packages/server/templates/agentic-knowledge-base.md +355 -0
- package/src/api/src/services/claude-md-manager.ts +0 -94
- package/src/api/src/services/preset-loader.ts +0 -149
- package/templates/CLAUDE.md.tmpl +0 -239
- /package/{src/web → packages/pixel}/dist/assets/index-BJyiMGkM.js +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-BOuHc64o.css +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-DDPzbp9E.js +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-DVKWFwwK.css +0 -0
- /package/{src/web → packages/pixel}/dist/assets/preview-app-DZ6WxhDc.js +0 -0
- /package/{src/web → packages/pixel}/dist/index.html +0 -0
- /package/{src/web → packages/pixel}/dist/tyconoforge.js +0 -0
- /package/{src → packages/server/src}/api/package.json +0 -0
- /package/{src → packages/server/src}/api/src/create-app.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/authority-validator.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/index.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/knowledge-gate.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/role-lifecycle.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/runners/direct-api.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/runners/index.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/skill-template.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/tools/definitions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/active-sessions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/coins.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/company.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/cost.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/engine.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/git.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/knowledge.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/operations.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/preferences.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/presets.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/projects.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/quests.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/roles.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/save.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/sessions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/setup.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/skills.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/speech.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/supervision.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/sync.ts +0 -0
- /package/{src → packages/server/src}/api/src/server.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/activity-stream.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/activity-tracker.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/database.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/git-save.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/job-manager.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/knowledge-importer.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/markdown-parser.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/port-registry.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/preferences.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/pricing.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/scaffold.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/session-store.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/team-recommender.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/token-ledger.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/wave-messages.ts +0 -0
- /package/{src → packages/server/src}/api/src/utils/role-level.ts +0 -0
- /package/{templates → packages/server/templates}/company.md.tmpl +0 -0
- /package/{templates → packages/server/templates}/gitignore.tmpl +0 -0
- /package/{templates → packages/server/templates}/roles.md.tmpl +0 -0
- /package/{templates → packages/server/templates}/skills/_manifest.json +0 -0
- /package/{templates → packages/server/templates}/skills/agent-browser/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/agent-browser/meta.json +0 -0
- /package/{templates → packages/server/templates}/skills/akb-linter/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/akb-linter/meta.json +0 -0
- /package/{templates → packages/server/templates}/skills/knowledge-gate/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/knowledge-gate/meta.json +0 -0
- /package/{templates → packages/server/templates}/teams/agency.json +0 -0
- /package/{templates → packages/server/templates}/teams/research.json +0 -0
- /package/{templates → packages/server/templates}/teams/startup.json +0 -0
- /package/{src/tui → packages/tui/src}/api.ts +0 -0
- /package/{src/tui → packages/tui/src}/app.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/CommandMode.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/OrgTree.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/PanelMode.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/SetupWizard.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/StatusBar.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/StreamView.tsx +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useApi.ts +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useCommand.ts +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useSSE.ts +0 -0
- /package/{src/tui → packages/tui/src}/index.tsx +0 -0
- /package/{src/tui → packages/tui/src}/store.ts +0 -0
- /package/{src/tui → packages/tui/src}/theme.ts +0 -0
- /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
|
-
//
|
|
194
|
+
// Fallback: read from activity-stream file on disk
|
|
166
195
|
if (ActivityStream.exists(id)) {
|
|
167
196
|
const events = ActivityStream.readAll(id);
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
172
|
-
res.end(JSON.stringify({ error: 'Not found' }));
|
|
202
|
+
jsonResponse(res, 404, { error: 'Not found' });
|
|
173
203
|
}
|
|
174
204
|
} else {
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|