tycono-server 0.1.0-beta.0

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 (84) hide show
  1. package/bin/cli.js +35 -0
  2. package/bin/server.ts +160 -0
  3. package/package.json +50 -0
  4. package/src/api/package.json +31 -0
  5. package/src/api/src/create-app.ts +90 -0
  6. package/src/api/src/create-server.ts +251 -0
  7. package/src/api/src/engine/agent-loop.ts +738 -0
  8. package/src/api/src/engine/authority-validator.ts +149 -0
  9. package/src/api/src/engine/context-assembler.ts +912 -0
  10. package/src/api/src/engine/index.ts +27 -0
  11. package/src/api/src/engine/knowledge-gate.ts +365 -0
  12. package/src/api/src/engine/llm-adapter.ts +304 -0
  13. package/src/api/src/engine/org-tree.ts +270 -0
  14. package/src/api/src/engine/role-lifecycle.ts +369 -0
  15. package/src/api/src/engine/runners/claude-cli.ts +796 -0
  16. package/src/api/src/engine/runners/direct-api.ts +66 -0
  17. package/src/api/src/engine/runners/index.ts +30 -0
  18. package/src/api/src/engine/runners/types.ts +95 -0
  19. package/src/api/src/engine/skill-template.ts +134 -0
  20. package/src/api/src/engine/tools/definitions.ts +201 -0
  21. package/src/api/src/engine/tools/executor.ts +611 -0
  22. package/src/api/src/routes/active-sessions.ts +134 -0
  23. package/src/api/src/routes/coins.ts +153 -0
  24. package/src/api/src/routes/company.ts +57 -0
  25. package/src/api/src/routes/cost.ts +141 -0
  26. package/src/api/src/routes/engine.ts +220 -0
  27. package/src/api/src/routes/execute.ts +1075 -0
  28. package/src/api/src/routes/git.ts +211 -0
  29. package/src/api/src/routes/knowledge.ts +378 -0
  30. package/src/api/src/routes/operations.ts +309 -0
  31. package/src/api/src/routes/preferences.ts +63 -0
  32. package/src/api/src/routes/presets.ts +123 -0
  33. package/src/api/src/routes/projects.ts +82 -0
  34. package/src/api/src/routes/quests.ts +41 -0
  35. package/src/api/src/routes/roles.ts +112 -0
  36. package/src/api/src/routes/save.ts +152 -0
  37. package/src/api/src/routes/sessions.ts +288 -0
  38. package/src/api/src/routes/setup.ts +437 -0
  39. package/src/api/src/routes/skills.ts +357 -0
  40. package/src/api/src/routes/speech.ts +959 -0
  41. package/src/api/src/routes/supervision.ts +136 -0
  42. package/src/api/src/routes/sync.ts +165 -0
  43. package/src/api/src/server.ts +59 -0
  44. package/src/api/src/services/activity-stream.ts +184 -0
  45. package/src/api/src/services/activity-tracker.ts +115 -0
  46. package/src/api/src/services/claude-md-manager.ts +94 -0
  47. package/src/api/src/services/company-config.ts +115 -0
  48. package/src/api/src/services/database.ts +77 -0
  49. package/src/api/src/services/digest-engine.ts +313 -0
  50. package/src/api/src/services/execution-manager.ts +1036 -0
  51. package/src/api/src/services/file-reader.ts +77 -0
  52. package/src/api/src/services/git-save.ts +614 -0
  53. package/src/api/src/services/job-manager.ts +16 -0
  54. package/src/api/src/services/knowledge-importer.ts +466 -0
  55. package/src/api/src/services/markdown-parser.ts +173 -0
  56. package/src/api/src/services/port-registry.ts +222 -0
  57. package/src/api/src/services/preferences.ts +150 -0
  58. package/src/api/src/services/preset-loader.ts +149 -0
  59. package/src/api/src/services/pricing.ts +34 -0
  60. package/src/api/src/services/scaffold.ts +546 -0
  61. package/src/api/src/services/session-store.ts +340 -0
  62. package/src/api/src/services/supervisor-heartbeat.ts +897 -0
  63. package/src/api/src/services/team-recommender.ts +382 -0
  64. package/src/api/src/services/token-ledger.ts +127 -0
  65. package/src/api/src/services/wave-messages.ts +194 -0
  66. package/src/api/src/services/wave-multiplexer.ts +356 -0
  67. package/src/api/src/services/wave-tracker.ts +359 -0
  68. package/src/api/src/utils/role-level.ts +31 -0
  69. package/src/core/scaffolder.ts +620 -0
  70. package/src/shared/types.ts +224 -0
  71. package/templates/CLAUDE.md.tmpl +239 -0
  72. package/templates/company.md.tmpl +17 -0
  73. package/templates/gitignore.tmpl +28 -0
  74. package/templates/roles.md.tmpl +8 -0
  75. package/templates/skills/_manifest.json +23 -0
  76. package/templates/skills/agent-browser/SKILL.md +159 -0
  77. package/templates/skills/agent-browser/meta.json +19 -0
  78. package/templates/skills/akb-linter/SKILL.md +125 -0
  79. package/templates/skills/akb-linter/meta.json +12 -0
  80. package/templates/skills/knowledge-gate/SKILL.md +120 -0
  81. package/templates/skills/knowledge-gate/meta.json +12 -0
  82. package/templates/teams/agency.json +58 -0
  83. package/templates/teams/research.json +58 -0
  84. package/templates/teams/startup.json +58 -0
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Wave Tracker — tracks session state in wave JSON files.
3
+ * Persists state so navigating between waves doesn't lose progress.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { COMPANY_ROOT } from './file-reader.js';
8
+ import { ActivityStream, type ActivityEvent } from './activity-stream.js';
9
+ import { executionManager } from './execution-manager.js';
10
+ import { listSessions } from './session-store.js';
11
+ import { type WaveRoleStatus, eventTypeToMessageStatus } from '../../../shared/types.js';
12
+
13
+ /* ─── Find wave file ──────────────────────── */
14
+
15
+ export function findWaveFile(waveId: string): string | null {
16
+ const wavesDir = path.join(COMPANY_ROOT, '.tycono', 'waves');
17
+ if (!fs.existsSync(wavesDir)) return null;
18
+
19
+ const direct = path.join(wavesDir, `${waveId}.json`);
20
+ if (fs.existsSync(direct)) return direct;
21
+
22
+ try {
23
+ for (const f of fs.readdirSync(wavesDir)) {
24
+ if (!f.endsWith('.json')) continue;
25
+ try {
26
+ const data = JSON.parse(fs.readFileSync(path.join(wavesDir, f), 'utf-8'));
27
+ if (data.waveId === waveId || data.id === waveId) {
28
+ return path.join(wavesDir, f);
29
+ }
30
+ } catch { /* skip */ }
31
+ }
32
+ } catch { /* skip */ }
33
+ return null;
34
+ }
35
+
36
+ /* ─── Append follow-up to wave ────────────── */
37
+
38
+ export function appendFollowUpToWave(
39
+ waveId: string, sessionId: string, roleId: string, task: string,
40
+ ): void {
41
+ const waveFile = findWaveFile(waveId);
42
+ if (!waveFile) {
43
+ console.warn(`[WaveTracker] Wave file not found for ${waveId}`);
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const data = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
49
+ if (!data.roles) data.roles = [];
50
+
51
+ data.roles.push({
52
+ roleId,
53
+ roleName: roleId,
54
+ sessionId,
55
+ status: 'running',
56
+ events: [],
57
+ childSessions: [],
58
+ isFollowUp: true,
59
+ followUpTask: task,
60
+ });
61
+
62
+ fs.writeFileSync(waveFile, JSON.stringify(data, null, 2), 'utf-8');
63
+ console.log(`[WaveTracker] Appended session ${sessionId} to wave ${waveId}`);
64
+
65
+ watchExecutionCompletion(waveId, sessionId, roleId);
66
+ } catch (err) {
67
+ console.error(`[WaveTracker] Failed to append to wave:`, err);
68
+ }
69
+ }
70
+
71
+ /* ─── Update follow-up entry on reply (continuation) ── */
72
+
73
+ export function updateFollowUpForReply(
74
+ waveId: string, roleId: string, sessionId: string,
75
+ ): void {
76
+ const waveFile = findWaveFile(waveId);
77
+ if (!waveFile) return;
78
+
79
+ try {
80
+ const data = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
81
+ if (!data.roles) return;
82
+
83
+ // Find entry by sessionId or roleId
84
+ let idx = data.roles.findIndex((r: { sessionId?: string }) => r.sessionId === sessionId);
85
+ if (idx < 0) {
86
+ for (let i = data.roles.length - 1; i >= 0; i--) {
87
+ if (data.roles[i].roleId === roleId) { idx = i; break; }
88
+ }
89
+ }
90
+
91
+ if (idx >= 0) {
92
+ data.roles[idx] = {
93
+ ...data.roles[idx],
94
+ sessionId,
95
+ status: 'running',
96
+ };
97
+ } else {
98
+ data.roles.push({
99
+ roleId,
100
+ roleName: roleId,
101
+ sessionId,
102
+ status: 'running',
103
+ events: [],
104
+ childSessions: [],
105
+ isFollowUp: true,
106
+ });
107
+ }
108
+
109
+ fs.writeFileSync(waveFile, JSON.stringify(data, null, 2), 'utf-8');
110
+ console.log(`[WaveTracker] Updated follow-up for reply: ${roleId} session=${sessionId}`);
111
+
112
+ watchExecutionCompletion(waveId, sessionId, roleId);
113
+ } catch (err) {
114
+ console.error(`[WaveTracker] Failed to update follow-up for reply:`, err);
115
+ }
116
+ }
117
+
118
+ /* ─── Update follow-up entry on execution completion ── */
119
+
120
+ export function updateFollowUpInWave(waveId: string, sessionId: string, roleId: string): void {
121
+ const waveFile = findWaveFile(waveId);
122
+ if (!waveFile) return;
123
+
124
+ try {
125
+ const data = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
126
+ if (!data.roles) return;
127
+
128
+ const newEvents = ActivityStream.readAll(sessionId);
129
+ const doneEvent = newEvents.find(e =>
130
+ e.type === 'msg:done' || e.type === 'msg:error' || e.type === 'msg:awaiting_input'
131
+ );
132
+ const status: WaveRoleStatus = doneEvent ? eventTypeToMessageStatus(doneEvent.type) as WaveRoleStatus : 'streaming';
133
+
134
+ // Collect child sessions
135
+ const childSessions: Array<{ roleId: string; roleName: string; sessionId: string; status: string; events: ReturnType<typeof ActivityStream.readAll> }> = [];
136
+ for (const e of newEvents) {
137
+ const childSessionId = e.data.childSessionId as string | undefined;
138
+ if (e.type === 'dispatch:start' && childSessionId) {
139
+ const targetRoleId = (e.data.targetRoleId as string) ?? 'unknown';
140
+ const childEvents = ActivityStream.readAll(childSessionId);
141
+ const childDone = childEvents.find(ce =>
142
+ ce.type === 'msg:done' || ce.type === 'msg:error' || ce.type === 'msg:awaiting_input'
143
+ );
144
+ const childStatus: WaveRoleStatus = childDone ? eventTypeToMessageStatus(childDone.type) as WaveRoleStatus : 'unknown';
145
+ childSessions.push({ roleId: targetRoleId, roleName: targetRoleId, sessionId: childSessionId, status: childStatus, events: childEvents });
146
+ }
147
+ }
148
+
149
+ // Find entry by sessionId
150
+ const idx = data.roles.findIndex((r: { sessionId?: string }) => r.sessionId === sessionId);
151
+ if (idx >= 0) {
152
+ data.roles[idx] = { ...data.roles[idx], status, events: newEvents, childSessions };
153
+ }
154
+
155
+ fs.writeFileSync(waveFile, JSON.stringify(data, null, 2), 'utf-8');
156
+ console.log(`[WaveTracker] Updated session ${sessionId} in wave ${waveId} → ${status}`);
157
+ } catch (err) {
158
+ console.error(`[WaveTracker] Failed to update wave:`, err);
159
+ }
160
+ }
161
+
162
+ /* ─── Save completed wave to .tycono/waves/ ── */
163
+
164
+ /**
165
+ * Auto-save a completed wave to disk.
166
+ * Called by supervisor-heartbeat when all children are done.
167
+ * Mirrors the logic of handleSaveWave in execute.ts but callable from services.
168
+ */
169
+ export function saveCompletedWave(waveId: string, directive: string): { ok: boolean; path?: string } {
170
+ try {
171
+ // BUG-009 fix: collect sessions from BOTH session-store AND activity-streams.
172
+ // Session-store cache may miss the CEO supervisor session (BUG-008).
173
+ // Activity-streams on disk are the source of truth for what actually ran.
174
+ const sessionIdSet = new Set(
175
+ listSessions().filter(s => s.waveId === waveId).map(s => s.id)
176
+ );
177
+
178
+ // Scan activity-streams for ALL sessions belonging to this wave.
179
+ // Wave sessions share a traceId chain: CEO → C-Level → subordinates.
180
+ // We find the CEO session (waveId timestamp embedded in its ID), then follow dispatch:start events.
181
+ const streamsDir = path.join(COMPANY_ROOT, '.tycono', 'activity-streams');
182
+ if (fs.existsSync(streamsDir)) {
183
+ // Find all activity stream files and check if they belong to this wave
184
+ const waveTimestamp = waveId.replace('wave-', '');
185
+ for (const file of fs.readdirSync(streamsDir)) {
186
+ if (!file.endsWith('.jsonl')) continue;
187
+ const sid = file.replace('.jsonl', '');
188
+ if (sessionIdSet.has(sid)) continue;
189
+ // Check if session ID contains the wave timestamp (CEO session)
190
+ // or if the session was dispatched from a known wave session
191
+ if (sid.includes(waveTimestamp)) {
192
+ sessionIdSet.add(sid);
193
+ }
194
+ }
195
+
196
+ // Now recursively find all child sessions via dispatch:start events
197
+ let foundNew = true;
198
+ while (foundNew) {
199
+ foundNew = false;
200
+ for (const sid of Array.from(sessionIdSet)) {
201
+ try {
202
+ const events = ActivityStream.readAll(sid);
203
+ for (const e of events) {
204
+ const childSessionId = e.data.childSessionId as string | undefined;
205
+ if (e.type === 'dispatch:start' && childSessionId && !sessionIdSet.has(childSessionId)) {
206
+ sessionIdSet.add(childSessionId);
207
+ foundNew = true;
208
+ }
209
+ }
210
+ } catch { /* skip */ }
211
+ }
212
+ }
213
+ }
214
+
215
+ const sessionIds = Array.from(sessionIdSet);
216
+
217
+ if (sessionIds.length === 0) {
218
+ console.warn(`[WaveTracker] No sessions found for wave ${waveId}, skipping save`);
219
+ return { ok: false };
220
+ }
221
+
222
+ console.log(`[WaveTracker] Auto-saving wave ${waveId} with ${sessionIds.length} sessions`);
223
+
224
+ interface WaveRoleData {
225
+ roleId: string;
226
+ roleName: string;
227
+ sessionId: string;
228
+ status: WaveRoleStatus | 'unknown';
229
+ events: ReturnType<typeof ActivityStream.readAll>;
230
+ childSessions: Array<{ roleId: string; roleName: string; sessionId: string; status: WaveRoleStatus | 'unknown'; events: ReturnType<typeof ActivityStream.readAll> }>;
231
+ }
232
+ const rolesData: WaveRoleData[] = [];
233
+
234
+ for (const sid of sessionIds) {
235
+ const events = ActivityStream.readAll(sid);
236
+ const startEvent = events.find(e => e.type === 'msg:start');
237
+ const roleId = startEvent?.roleId ?? 'unknown';
238
+ const roleName = (startEvent?.data?.roleName as string) ?? roleId;
239
+ const doneEvent = events.find(e => e.type === 'msg:done' || e.type === 'msg:awaiting_input' || e.type === 'msg:error');
240
+ const status: WaveRoleStatus | 'unknown' = doneEvent ? eventTypeToMessageStatus(doneEvent.type) as WaveRoleStatus : 'unknown';
241
+
242
+ const childSessions: WaveRoleData['childSessions'] = [];
243
+ for (const e of events) {
244
+ const childSessionId = e.data.childSessionId as string | undefined;
245
+ if (e.type === 'dispatch:start' && childSessionId) {
246
+ const targetRoleId = (e.data.targetRoleId as string) ?? 'unknown';
247
+ const childEvents = ActivityStream.readAll(childSessionId);
248
+ const childDone = childEvents.find(ce => ce.type === 'msg:done' || ce.type === 'msg:error' || ce.type === 'msg:awaiting_input');
249
+ const childStatus: WaveRoleStatus | 'unknown' = childDone ? eventTypeToMessageStatus(childDone.type) as WaveRoleStatus : 'unknown';
250
+ childSessions.push({
251
+ roleId: targetRoleId,
252
+ roleName: (childEvents.find(ce => ce.type === 'msg:start')?.data?.roleName as string) ?? targetRoleId,
253
+ sessionId: childSessionId,
254
+ status: childStatus,
255
+ events: childEvents,
256
+ });
257
+ }
258
+ }
259
+
260
+ rolesData.push({ roleId, roleName, sessionId: sid, status, events, childSessions });
261
+ }
262
+
263
+ const wavesDir = path.join(COMPANY_ROOT, '.tycono', 'waves');
264
+ if (!fs.existsSync(wavesDir)) {
265
+ fs.mkdirSync(wavesDir, { recursive: true });
266
+ }
267
+
268
+ // Check if wave file already exists (e.g. from appendFollowUp)
269
+ const existing = findWaveFile(waveId);
270
+ const baseName = existing
271
+ ? path.basename(existing, '.json')
272
+ : waveId;
273
+ const jsonPath = existing ?? path.join(wavesDir, `${baseName}.json`);
274
+
275
+ // BUG-009 fix: calculate actual duration from activity stream timestamps
276
+ const now = new Date();
277
+ let startedAt = now;
278
+ let endedAt = now;
279
+ for (const role of rolesData) {
280
+ if (role.events.length > 0) {
281
+ const firstTs = new Date(role.events[0].ts);
282
+ const lastTs = new Date(role.events[role.events.length - 1].ts);
283
+ if (firstTs < startedAt) startedAt = firstTs;
284
+ if (lastTs > endedAt) endedAt = lastTs;
285
+ }
286
+ for (const child of role.childSessions) {
287
+ if (child.events.length > 0) {
288
+ const firstTs = new Date(child.events[0].ts);
289
+ const lastTs = new Date(child.events[child.events.length - 1].ts);
290
+ if (firstTs < startedAt) startedAt = firstTs;
291
+ if (lastTs > endedAt) endedAt = lastTs;
292
+ }
293
+ }
294
+ }
295
+ const duration = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
296
+
297
+ // Collect ALL session IDs including child sessions
298
+ const allSessionIds = [...sessionIds];
299
+ for (const role of rolesData) {
300
+ for (const child of role.childSessions) {
301
+ if (!allSessionIds.includes(child.sessionId)) {
302
+ allSessionIds.push(child.sessionId);
303
+ }
304
+ }
305
+ }
306
+
307
+ // Preserve preset field from existing wave file
308
+ let existingPreset: string | undefined;
309
+ if (existing) {
310
+ try {
311
+ const existingData = JSON.parse(fs.readFileSync(existing, 'utf-8'));
312
+ existingPreset = existingData.preset;
313
+ } catch { /* ignore */ }
314
+ }
315
+
316
+ const waveJson: Record<string, unknown> = {
317
+ id: baseName,
318
+ directive,
319
+ startedAt: startedAt.toISOString(),
320
+ duration,
321
+ roles: rolesData,
322
+ waveId,
323
+ sessionIds: allSessionIds,
324
+ };
325
+ if (existingPreset) waveJson.preset = existingPreset;
326
+ fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
327
+
328
+ const relativePath = `.tycono/waves/${baseName}.json`;
329
+ console.log(`[WaveTracker] Wave saved: ${relativePath} (${rolesData.length} roles)`);
330
+
331
+ // Earn coins for wave completion (non-critical)
332
+ try {
333
+ const { earnCoinsInternal } = require('../routes/coins.js');
334
+ if (rolesData.length > 0) {
335
+ earnCoinsInternal(rolesData.length * 500, `Wave done: ${rolesData.length} roles`, `wave:${baseName}`);
336
+ }
337
+ } catch { /* non-critical */ }
338
+
339
+ return { ok: true, path: relativePath };
340
+ } catch (err) {
341
+ console.error(`[WaveTracker] Failed to auto-save wave ${waveId}:`, err);
342
+ return { ok: false };
343
+ }
344
+ }
345
+
346
+ /* ─── Helpers ─────────────────────────────── */
347
+
348
+ function watchExecutionCompletion(waveId: string, sessionId: string, roleId: string): void {
349
+ const exec = executionManager.getActiveExecution(sessionId);
350
+ if (!exec) return;
351
+
352
+ const subscriber = (event: ActivityEvent) => {
353
+ if (event.type === 'msg:done' || event.type === 'msg:error' || event.type === 'msg:awaiting_input') {
354
+ updateFollowUpInWave(waveId, sessionId, roleId);
355
+ exec.stream.unsubscribe(subscriber);
356
+ }
357
+ };
358
+ exec.stream.subscribe(subscriber);
359
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * role-level.ts — Server-side role level calculation
3
+ *
4
+ * Mirrors the frontend level system.
5
+ * Formula: level = floor(√(tokens ÷ 50,000))
6
+ * Infinite levels, quadratic scaling.
7
+ */
8
+
9
+ const BASE_XP = 50_000;
10
+
11
+ export function calcLevel(totalTokens: number): number {
12
+ if (totalTokens < BASE_XP) return 1;
13
+ return Math.max(1, Math.floor(Math.sqrt(totalTokens / BASE_XP)));
14
+ }
15
+
16
+ export function tokensForLevel(level: number): number {
17
+ return BASE_XP * level * level;
18
+ }
19
+
20
+ export function calcProgress(totalTokens: number): number {
21
+ const level = calcLevel(totalTokens);
22
+ const current = tokensForLevel(level);
23
+ const next = tokensForLevel(level + 1);
24
+ return Math.min(1, (totalTokens - current) / (next - current));
25
+ }
26
+
27
+ export function formatTokens(tokens: number): string {
28
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
29
+ if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(0)}K`;
30
+ return String(tokens);
31
+ }