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,340 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { COMPANY_ROOT } from './file-reader.js';
4
+ import { type ActivityEvent, type SessionSource, type SessionStatus, type MessageStatus, isMessageTerminal } from '../../../shared/types.js';
5
+
6
+ /* ─── Types ─────────────────────────────── */
7
+
8
+ export interface ImageAttachment {
9
+ type: 'image';
10
+ data: string; // base64 encoded
11
+ name: string;
12
+ mediaType: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
13
+ }
14
+
15
+ /** Dispatch link — reference to a child session created via dispatch */
16
+ export interface DispatchLink {
17
+ sessionId: string;
18
+ roleId: string;
19
+ }
20
+
21
+ export interface Message {
22
+ id: string;
23
+ from: 'ceo' | 'role';
24
+ content: string;
25
+ type: 'conversation' | 'directive' | 'system';
26
+ status?: MessageStatus;
27
+ timestamp: string;
28
+ attachments?: ImageAttachment[];
29
+
30
+ /* ─── D-014: Session-Centric extensions ─── */
31
+ /** Execution events embedded in this message (replaces separate JSONL) */
32
+ events?: ActivityEvent[];
33
+ /** Child sessions spawned by dispatch during this message's execution */
34
+ dispatches?: DispatchLink[];
35
+ /** @deprecated D-014: Internal job ID for runtime tracking. Use sessionId for external references. */
36
+ jobId?: string;
37
+ /** True for consult/ask messages (read-only execution) */
38
+ readOnly?: boolean;
39
+ /** Execution stats */
40
+ turns?: number;
41
+ tokens?: { input: number; output: number };
42
+ /** KP-006: Knowledge debt warnings from Post-K check */
43
+ knowledgeDebt?: Array<{ type: string; file?: string; message: string }>;
44
+ }
45
+
46
+ export interface Session {
47
+ id: string;
48
+ roleId: string;
49
+ title: string;
50
+ mode: 'talk' | 'do';
51
+ messages: Message[];
52
+ status: SessionStatus;
53
+ createdAt: string;
54
+ updatedAt: string;
55
+
56
+ /* ─── D-014: Session-Centric extensions ─── */
57
+ /** How this session was created */
58
+ source?: SessionSource;
59
+ /** Parent session ID (when created via dispatch) */
60
+ parentSessionId?: string;
61
+ /** Wave ID (when created via wave) */
62
+ waveId?: string;
63
+ }
64
+
65
+ /* ─── Session directory ─────────────────── */
66
+
67
+ function sessionsDir(): string {
68
+ return path.join(COMPANY_ROOT, '.tycono', 'sessions');
69
+ }
70
+
71
+ function ensureDir(): void {
72
+ const dir = sessionsDir();
73
+ if (!fs.existsSync(dir)) {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ }
76
+ }
77
+
78
+ function sessionPath(id: string): string {
79
+ return path.join(sessionsDir(), `${id}.json`);
80
+ }
81
+
82
+ /* ─── Debounced write ───────────────────── */
83
+
84
+ const writeTimers = new Map<string, ReturnType<typeof setTimeout>>();
85
+ const DEBOUNCE_MS = 2000;
86
+
87
+ function debouncedWrite(session: Session): void {
88
+ const existing = writeTimers.get(session.id);
89
+ if (existing) clearTimeout(existing);
90
+
91
+ writeTimers.set(session.id, setTimeout(() => {
92
+ writeTimers.delete(session.id);
93
+ writeImmediate(session);
94
+ }, DEBOUNCE_MS));
95
+ }
96
+
97
+ function writeImmediate(session: Session): void {
98
+ ensureDir();
99
+ const timer = writeTimers.get(session.id);
100
+ if (timer) {
101
+ clearTimeout(timer);
102
+ writeTimers.delete(session.id);
103
+ }
104
+ try {
105
+ fs.writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
106
+ } catch (err) {
107
+ console.error(`[SessionStore] WRITE FAILED for ${session.id}:`, err);
108
+ }
109
+ }
110
+
111
+ /* ─── In-memory cache ───────────────────── */
112
+ /*
113
+ * OOM prevention: inactive sessions cache metadata only (no messages).
114
+ * Messages are loaded on-demand from disk when getSession() is called.
115
+ * Active sessions keep messages in memory for streaming writes.
116
+ *
117
+ * Before: 61 sessions × 50KB avg messages = 3MB+ baseline, grows to 100s of MB
118
+ * After: 61 sessions × 1KB metadata + 5 active × 50KB = ~310KB baseline
119
+ */
120
+
121
+ const cache = new Map<string, Session>();
122
+
123
+ /** Strip messages from session to save memory (keeps metadata) */
124
+ function stripMessages(session: Session): Session {
125
+ if (session.messages.length === 0) return session;
126
+ return { ...session, messages: [] };
127
+ }
128
+
129
+ /** Load full session from disk (with messages) */
130
+ function loadFromDisk(id: string): Session | undefined {
131
+ const p = sessionPath(id);
132
+ if (!fs.existsSync(p)) return undefined;
133
+ try {
134
+ return JSON.parse(fs.readFileSync(p, 'utf-8')) as Session;
135
+ } catch { return undefined; }
136
+ }
137
+
138
+ function loadAll(): void {
139
+ ensureDir();
140
+ const files = fs.readdirSync(sessionsDir()).filter((f) => f.endsWith('.json'));
141
+ for (const file of files) {
142
+ try {
143
+ const data = JSON.parse(fs.readFileSync(path.join(sessionsDir(), file), 'utf-8')) as Session;
144
+ // Only keep messages for active sessions; strip for done/interrupted
145
+ if (data.status === 'active' || data.status === 'awaiting_input') {
146
+ cache.set(data.id, data);
147
+ } else {
148
+ cache.set(data.id, stripMessages(data));
149
+ }
150
+ } catch { /* skip corrupted */ }
151
+ }
152
+ }
153
+
154
+ // Lazy load: defer until first access (avoids creating dirs in CWD before scaffold)
155
+ let loaded = false;
156
+ function ensureLoaded(): void {
157
+ if (loaded) return;
158
+ loaded = true;
159
+ loadAll();
160
+ }
161
+
162
+ /* ─── Public API ────────────────────────── */
163
+
164
+ /** Options for creating a session with D-014 extensions */
165
+ export interface CreateSessionOptions {
166
+ mode?: 'talk' | 'do';
167
+ source?: SessionSource;
168
+ parentSessionId?: string;
169
+ waveId?: string;
170
+ }
171
+
172
+ export function createSession(roleId: string, opts: CreateSessionOptions = {}): Session {
173
+ ensureLoaded();
174
+ const id = `ses-${roleId}-${Date.now()}`;
175
+ const now = new Date().toISOString();
176
+ const session: Session = {
177
+ id,
178
+ roleId,
179
+ title: `New ${roleId.toUpperCase()} session`,
180
+ mode: opts.mode ?? 'talk',
181
+ messages: [],
182
+ status: 'active',
183
+ createdAt: now,
184
+ updatedAt: now,
185
+ ...(opts.source && { source: opts.source }),
186
+ ...(opts.parentSessionId && { parentSessionId: opts.parentSessionId }),
187
+ ...(opts.waveId && { waveId: opts.waveId }),
188
+ };
189
+ cache.set(id, session);
190
+ writeImmediate(session);
191
+ return session;
192
+ }
193
+
194
+ export function getSession(id: string): Session | undefined {
195
+ ensureLoaded();
196
+ const cached = cache.get(id);
197
+ if (!cached) return undefined;
198
+
199
+ // If messages were stripped (inactive session), reload from disk on demand
200
+ if (cached.messages.length === 0 && cached.status !== 'active') {
201
+ const full = loadFromDisk(id);
202
+ if (full) return full; // Return disk version (don't cache — it's a one-time read)
203
+ }
204
+ return cached;
205
+ }
206
+
207
+ export function listSessions(): Omit<Session, 'messages'>[] {
208
+ ensureLoaded();
209
+ return Array.from(cache.values())
210
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
211
+ .map(({ messages: _, ...meta }) => meta);
212
+ }
213
+
214
+ export function addMessage(sessionId: string, msg: Message, streaming = false): Session | undefined {
215
+ const session = cache.get(sessionId);
216
+ if (!session) return undefined;
217
+
218
+ session.messages.push(msg);
219
+ session.updatedAt = new Date().toISOString();
220
+
221
+ // Auto-generate title from first CEO message
222
+ if (session.messages.length === 1 && msg.from === 'ceo') {
223
+ session.title = msg.content.slice(0, 40).replace(/\n/g, ' ');
224
+ }
225
+
226
+ if (streaming) {
227
+ debouncedWrite(session);
228
+ } else {
229
+ writeImmediate(session);
230
+ }
231
+ return session;
232
+ }
233
+
234
+ /** Fields that can be updated on a message */
235
+ export type MessageUpdate = Partial<Pick<Message, 'content' | 'status' | 'turns' | 'tokens' | 'dispatches' | 'readOnly' | 'knowledgeDebt'>>;
236
+
237
+ export function updateMessage(sessionId: string, messageId: string, updates: MessageUpdate): Session | undefined {
238
+ const session = cache.get(sessionId);
239
+ if (!session) return undefined;
240
+
241
+ const msg = session.messages.find((m) => m.id === messageId);
242
+ if (!msg) return undefined;
243
+
244
+ if (updates.content !== undefined) msg.content = updates.content;
245
+ if (updates.status !== undefined) msg.status = updates.status;
246
+ if (updates.turns !== undefined) msg.turns = updates.turns;
247
+ if (updates.tokens !== undefined) msg.tokens = updates.tokens;
248
+ if (updates.dispatches !== undefined) msg.dispatches = updates.dispatches;
249
+ if (updates.readOnly !== undefined) msg.readOnly = updates.readOnly;
250
+ if (updates.knowledgeDebt !== undefined) msg.knowledgeDebt = updates.knowledgeDebt;
251
+ session.updatedAt = new Date().toISOString();
252
+
253
+ if (updates.status && isMessageTerminal(updates.status)) {
254
+ writeImmediate(session);
255
+ } else {
256
+ debouncedWrite(session);
257
+ }
258
+ return session;
259
+ }
260
+
261
+ /** Append an execution event to a message (D-014: events embedded in message) */
262
+ export function appendMessageEvent(sessionId: string, messageId: string, event: ActivityEvent): boolean {
263
+ const session = cache.get(sessionId);
264
+ if (!session) return false;
265
+
266
+ const msg = session.messages.find((m) => m.id === messageId);
267
+ if (!msg) return false;
268
+
269
+ if (!msg.events) msg.events = [];
270
+ msg.events.push(event);
271
+ session.updatedAt = new Date().toISOString();
272
+
273
+ // Debounce during streaming — events come in fast
274
+ debouncedWrite(session);
275
+ return true;
276
+ }
277
+
278
+ export function updateSession(id: string, updates: Partial<Pick<Session, 'title' | 'mode' | 'status' | 'source' | 'parentSessionId' | 'waveId'>>): Session | undefined {
279
+ const session = cache.get(id);
280
+ if (!session) return undefined;
281
+
282
+ if (updates.title !== undefined) session.title = updates.title;
283
+ if (updates.mode !== undefined) session.mode = updates.mode;
284
+ if (updates.status !== undefined) session.status = updates.status;
285
+ if (updates.source !== undefined) session.source = updates.source;
286
+ if (updates.parentSessionId !== undefined) session.parentSessionId = updates.parentSessionId;
287
+ if (updates.waveId !== undefined) session.waveId = updates.waveId;
288
+ session.updatedAt = new Date().toISOString();
289
+ writeImmediate(session);
290
+
291
+ // OOM prevention: when session becomes inactive, strip messages from cache
292
+ if (updates.status && updates.status !== 'active' && updates.status !== 'awaiting_input') {
293
+ cache.set(id, stripMessages(session));
294
+ }
295
+
296
+ return session;
297
+ }
298
+
299
+ export function deleteSession(id: string, force = false): boolean {
300
+ const session = cache.get(id);
301
+ if (!session) return false;
302
+
303
+ // BUG-008 fix: protect wave sessions from accidental deletion
304
+ if (session.waveId && !force) {
305
+ console.warn(`[SessionStore] BLOCKED deletion of wave session ${id} (waveId=${session.waveId}, roleId=${session.roleId}). Use force=true to override.`);
306
+ return false;
307
+ }
308
+ // BUG-008 hard guard: CEO supervisor session is NEVER deletable during wave
309
+ if (session.roleId === 'ceo' && session.waveId && session.source === 'wave') {
310
+ console.error(`[SessionStore] HARD BLOCK: CEO supervisor session ${id} cannot be deleted (waveId=${session.waveId}). This is a 1:1:1 invariant.`);
311
+ return false;
312
+ }
313
+
314
+ console.log(`[SessionStore] Deleting session ${id} (roleId=${session.roleId}, waveId=${session.waveId ?? 'none'}, messages=${session.messages.length})`);
315
+ cache.delete(id);
316
+ const p = sessionPath(id);
317
+ if (fs.existsSync(p)) fs.unlinkSync(p);
318
+ return true;
319
+ }
320
+
321
+ export function deleteMany(ids: string[]): number {
322
+ let count = 0;
323
+ for (const id of ids) {
324
+ if (deleteSession(id)) count++;
325
+ }
326
+ return count;
327
+ }
328
+
329
+ export function deleteEmpty(): { deleted: number; ids: string[] } {
330
+ const ids: string[] = [];
331
+ for (const [id, session] of cache) {
332
+ if (session.messages.length === 0) {
333
+ // BUG-008 fix: never delete wave sessions — they are managed by supervisor lifecycle
334
+ if (session.waveId) continue;
335
+ ids.push(id);
336
+ }
337
+ }
338
+ const deleted = deleteMany(ids);
339
+ return { deleted, ids };
340
+ }