tycono 0.3.14-beta.1 → 0.3.14-beta.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.3.14-beta.1",
3
+ "version": "0.3.14-beta.11",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,7 @@ import { questsRouter } from './routes/quests.js';
33
33
  import { coinsRouter } from './routes/coins.js';
34
34
  import { activeSessionsRouter } from './routes/active-sessions.js';
35
35
  import { supervisionRouter } from './routes/supervision.js';
36
+ import { presetsRouter } from './routes/presets.js';
36
37
  import { importKnowledge } from './services/knowledge-importer.js';
37
38
  import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
38
39
  import { readConfig } from './services/company-config.js';
@@ -212,6 +213,7 @@ export function createExpressApp(): express.Application {
212
213
  app.use('/api/coins', coinsRouter);
213
214
  app.use('/api/active-sessions', activeSessionsRouter);
214
215
  app.use('/api/supervision', supervisionRouter);
216
+ app.use('/api/presets', presetsRouter);
215
217
 
216
218
  app.get('/api/health', (_req, res) => {
217
219
  res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
@@ -1,16 +1,21 @@
1
1
  /**
2
2
  * presets.ts — Preset API routes
3
3
  *
4
- * GET /api/presets — list all preset summaries
5
- * GET /api/presets/:id — get full preset detail
4
+ * GET /api/presets — list all preset summaries
5
+ * GET /api/presets/:id — get full preset detail
6
+ * POST /api/presets/install — install preset from data
7
+ * DELETE /api/presets/:id — remove installed preset
6
8
  */
7
9
  import { Router } from 'express';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import YAML from 'yaml';
8
13
  import { COMPANY_ROOT } from '../services/file-reader.js';
9
- import { getPresetSummaries, getPresetById } from '../services/preset-loader.js';
14
+ import { getPresetSummaries, getPresetById, loadPresets } from '../services/preset-loader.js';
10
15
 
11
16
  export const presetsRouter = Router();
12
17
 
13
- /** GET /api/presets — list preset summaries for TUI */
18
+ /** GET /api/presets — list preset summaries */
14
19
  presetsRouter.get('/', (_req, res) => {
15
20
  try {
16
21
  const summaries = getPresetSummaries(COMPANY_ROOT);
@@ -33,3 +38,86 @@ presetsRouter.get('/:id', (req, res) => {
33
38
  res.status(500).json({ error: 'Failed to load preset' });
34
39
  }
35
40
  });
41
+
42
+ /** POST /api/presets/install — install a preset from provided data */
43
+ presetsRouter.post('/install', (req, res) => {
44
+ try {
45
+ const { id, preset } = req.body as { id: string; preset: Record<string, unknown> };
46
+ if (!id || !preset) {
47
+ res.status(400).json({ error: 'id and preset are required' });
48
+ return;
49
+ }
50
+
51
+ // Validate preset has required fields
52
+ if (!preset.name || !preset.roles || !Array.isArray(preset.roles)) {
53
+ res.status(400).json({ error: 'preset must have name and roles array' });
54
+ return;
55
+ }
56
+
57
+ // Check for conflict with existing preset
58
+ const existing = getPresetById(COMPANY_ROOT, id);
59
+ if (existing && !existing.isDefault) {
60
+ res.status(409).json({ error: `Preset already installed: ${id}` });
61
+ return;
62
+ }
63
+
64
+ // Create preset directory and write preset.yaml
65
+ const presetDir = path.join(COMPANY_ROOT, 'company', 'presets', id);
66
+ fs.mkdirSync(presetDir, { recursive: true });
67
+
68
+ // Write preset.yaml
69
+ const yamlContent = YAML.stringify(preset);
70
+ fs.writeFileSync(path.join(presetDir, 'preset.yaml'), yamlContent);
71
+
72
+ // Create subdirectories for roles/knowledge/skills
73
+ fs.mkdirSync(path.join(presetDir, 'roles'), { recursive: true });
74
+ fs.mkdirSync(path.join(presetDir, 'knowledge'), { recursive: true });
75
+ fs.mkdirSync(path.join(presetDir, 'skills'), { recursive: true });
76
+
77
+ // Write knowledge docs if provided
78
+ const knowledge = req.body.knowledge as Array<{ filename: string; content: string }> | undefined;
79
+ if (knowledge) {
80
+ for (const doc of knowledge) {
81
+ fs.writeFileSync(path.join(presetDir, 'knowledge', doc.filename), doc.content);
82
+ }
83
+ }
84
+
85
+ // Write role yamls if provided
86
+ const roleDefinitions = req.body.roleDefinitions as Array<{ id: string; yaml: string }> | undefined;
87
+ if (roleDefinitions) {
88
+ for (const role of roleDefinitions) {
89
+ const roleDir = path.join(presetDir, 'roles', role.id);
90
+ fs.mkdirSync(roleDir, { recursive: true });
91
+ fs.writeFileSync(path.join(roleDir, 'role.yaml'), role.yaml);
92
+ }
93
+ }
94
+
95
+ res.json({ ok: true, id, path: `company/presets/${id}` });
96
+ } catch (err) {
97
+ res.status(500).json({ error: `Install failed: ${err instanceof Error ? err.message : 'unknown'}` });
98
+ }
99
+ });
100
+
101
+ /** DELETE /api/presets/:id — remove installed preset */
102
+ presetsRouter.delete('/:id', (req, res) => {
103
+ try {
104
+ const { id } = req.params;
105
+ if (id === 'default' || id === '_default') {
106
+ res.status(400).json({ error: 'Cannot remove default preset' });
107
+ return;
108
+ }
109
+
110
+ const presetDir = path.join(COMPANY_ROOT, 'company', 'presets', id);
111
+ if (!fs.existsSync(presetDir)) {
112
+ res.status(404).json({ error: `Preset not found: ${id}` });
113
+ return;
114
+ }
115
+
116
+ // Remove preset directory recursively
117
+ fs.rmSync(presetDir, { recursive: true, force: true });
118
+
119
+ res.json({ ok: true, id });
120
+ } catch (err) {
121
+ res.status(500).json({ error: `Remove failed: ${err instanceof Error ? err.message : 'unknown'}` });
122
+ }
123
+ });
@@ -180,7 +180,7 @@ class ExecutionManager {
180
180
 
181
181
  this.executions.set(execId, execution);
182
182
 
183
- this.initializeAndRunExecution(execution, params, orgTree);
183
+ this.initializeAndRunExecution(execution, params, orgTree, presetId);
184
184
 
185
185
  return execution;
186
186
  }
@@ -189,6 +189,7 @@ class ExecutionManager {
189
189
  execution: Execution,
190
190
  params: StartExecutionParams,
191
191
  orgTree: ReturnType<typeof buildOrgTree>,
192
+ presetId?: string,
192
193
  ): Promise<void> {
193
194
  try {
194
195
  const ports = await portRegistry.allocate(execution.sessionId || execution.id, params.roleId, params.task);
@@ -724,8 +725,8 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
724
725
  console.log(`[ExecMgr] Supervision recovery: ${deadExecution.roleId} died with ${runningChildren.length} running children. Restarting.`);
725
726
 
726
727
  // Propagate waveId from the dead session
727
- const deadSession = getSession(deadExecution.sessionId);
728
- const waveId = deadSession?.waveId;
728
+ const deadSes = getSession(deadExecution.sessionId);
729
+ const waveId = deadSes?.waveId;
729
730
 
730
731
  // Create new session for recovery
731
732
  const newSession = createSession(deadExecution.roleId, {
@@ -172,13 +172,15 @@ class SupervisorHeartbeat {
172
172
  const waveSessions = listSessions().filter(s => s.waveId === waveId);
173
173
  const ceoSession = waveSessions.find(s => s.roleId === 'ceo') ?? null;
174
174
 
175
- // Read original directive from wave artifact file
175
+ // Read original directive + preset from wave artifact file
176
176
  let originalDirective = '';
177
+ let originalPreset: string | undefined;
177
178
  try {
178
179
  const waveFile = path.join(COMPANY_ROOT, 'operations', 'waves', `${waveId}.json`);
179
180
  if (fs.existsSync(waveFile)) {
180
181
  const waveData = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
181
182
  originalDirective = waveData.directive ?? '';
183
+ originalPreset = waveData.preset;
182
184
  }
183
185
  } catch { /* ignore */ }
184
186
 
@@ -188,6 +190,7 @@ class SupervisorHeartbeat {
188
190
  waveId,
189
191
  directive: originalDirective || text,
190
192
  continuous: false,
193
+ preset: originalPreset,
191
194
  supervisorSessionId: ceoSession?.id ?? null,
192
195
  executionId: null,
193
196
  status: 'stopped',
package/src/tui/app.tsx CHANGED
@@ -14,6 +14,8 @@
14
14
  import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
15
15
  import { Box, Text, useApp, useInput } from 'ink';
16
16
  import { StatusBar } from './components/StatusBar';
17
+ import { OrgTree } from './components/OrgTree';
18
+ import { StreamView } from './components/StreamView';
17
19
  import { CommandMode, type StreamLine } from './components/CommandMode';
18
20
  import { PanelMode } from './components/PanelMode';
19
21
  import { SetupWizard } from './components/SetupWizard';
@@ -668,8 +670,51 @@ export const App: React.FC = () => {
668
670
  // Command Mode: scrollable terminal (no fullscreen)
669
671
  // Panel Mode: fullscreen (intentional — like vim for inspection)
670
672
  if (mode === 'panel') {
673
+ // OOM debug levels: 0=full, 1=minimal, 2=orgTree only, 3=stream only
674
+ const debugLevel = parseInt(process.env.PANEL_MINIMAL || '0', 10);
675
+ if (debugLevel === 1) {
676
+ return (
677
+ <Box flexDirection="column">
678
+ <Text color="cyan">Panel Mode (minimal)</Text>
679
+ <Text color="gray">Events: {sse.events.length} | Press Esc</Text>
680
+ </Box>
681
+ );
682
+ }
683
+ if (debugLevel === 2) {
684
+ return (
685
+ <Box flexDirection="column">
686
+ <OrgTree tree={orgTree} focused={true} selectedIndex={0} flatRoles={flatRoleIds} ceoStatus="idle" />
687
+ <Text color="gray">OrgTree only | Press Esc</Text>
688
+ </Box>
689
+ );
690
+ }
691
+ if (debugLevel === 3) {
692
+ return (
693
+ <Box flexDirection="column">
694
+ <StreamView events={sse.events} allRoleIds={flatRoleIds} streamStatus={sse.streamStatus} waveId={focusedWaveId} roleLabel="All" />
695
+ <Text color="gray">StreamView only | Press Esc</Text>
696
+ </Box>
697
+ );
698
+ }
699
+ if (debugLevel === 4) {
700
+ // Full layout structure but empty content
701
+ return (
702
+ <Box flexDirection="column" height={termHeight}>
703
+ <Box flexGrow={1}>
704
+ <Box flexDirection="column" width={28}>
705
+ <Text color="green">Left Panel</Text>
706
+ </Box>
707
+ <Text color="gray">{'\u2502'}</Text>
708
+ <Box flexGrow={1} flexDirection="column" overflow="hidden">
709
+ <Text color="cyan">Right Panel</Text>
710
+ </Box>
711
+ </Box>
712
+ <StatusBar companyName="test" waveIndex={1} waveCount={1} waveStatus="idle" activeCount={0} portCount={0} totalCost={0} />
713
+ </Box>
714
+ );
715
+ }
671
716
  return (
672
- <Box flexDirection="column" height={termHeight}>
717
+ <Box flexDirection="column">
673
718
  <Box flexGrow={1} flexDirection="column">
674
719
  <PanelMode
675
720
  tree={orgTree}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * OrgTree — left panel showing organization hierarchy with real-time status
3
- * CEO is now selectable (index 0 in flatRoles)
2
+ * OrgTree — left panel showing organization hierarchy
3
+ * Simplified to single Text render to prevent yoga OOM on wide terminals
4
4
  */
5
5
 
6
6
  import React from 'react';
@@ -16,103 +16,36 @@ interface OrgTreeProps {
16
16
  ceoStatus?: string;
17
17
  }
18
18
 
19
- function statusColor(status: string): string {
20
- switch (status) {
21
- case 'working':
22
- case 'streaming':
23
- return 'green';
24
- case 'done':
25
- return 'gray';
26
- case 'error':
27
- return 'red';
28
- case 'awaiting_input':
29
- return 'yellow';
30
- default:
31
- return 'gray';
32
- }
33
- }
34
-
35
- interface FlatEntry {
36
- roleId: string;
37
- level: string;
38
- status: string;
39
- prefix: string;
40
- }
41
-
42
- function flattenTree(nodes: OrgNode[], prefix: string = '', isLast: boolean[] = []): FlatEntry[] {
43
- const result: FlatEntry[] = [];
44
-
19
+ function flattenTree(nodes: OrgNode[], isLast: boolean[] = []): Array<{ roleId: string; status: string; line: string }> {
20
+ const result: Array<{ roleId: string; status: string; line: string }> = [];
45
21
  for (let i = 0; i < nodes.length; i++) {
46
22
  const node = nodes[i];
47
23
  const last = i === nodes.length - 1;
48
-
49
- let linePrefix = '';
24
+ let prefix = '';
50
25
  for (let j = 0; j < isLast.length; j++) {
51
- linePrefix += isLast[j] ? ' ' : '\u2502 ';
26
+ prefix += isLast[j] ? ' ' : '\u2502 ';
52
27
  }
53
- linePrefix += isLast.length > 0 || i > 0 || nodes.length > 1
54
- ? (last ? '\u2514\u2500 ' : '\u251C\u2500 ')
55
- : '';
56
-
57
- result.push({
58
- roleId: node.role.id,
59
- level: node.role.level,
60
- status: node.status,
61
- prefix: linePrefix,
62
- });
63
-
28
+ prefix += last ? '\u2514\u2500 ' : '\u251C\u2500 ';
29
+ const icon = statusIcon(node.status);
30
+ result.push({ roleId: node.role.id, status: node.status, line: `${prefix}${icon} ${node.role.id}` });
64
31
  if (node.children.length > 0) {
65
- result.push(...flattenTree(node.children, '', [...isLast, last]));
32
+ result.push(...flattenTree(node.children, [...isLast, last]));
66
33
  }
67
34
  }
68
-
69
35
  return result;
70
36
  }
71
37
 
72
38
  export const OrgTree: React.FC<OrgTreeProps> = React.memo(({ tree, focused, selectedIndex, flatRoles, ceoStatus }) => {
73
- const entries = flattenTree(tree);
74
- const isCeoSelected = focused && flatRoles[selectedIndex] === 'ceo';
75
39
  const ceoIcon = statusIcon(ceoStatus ?? 'idle');
76
- const ceoColor = statusColor(ceoStatus ?? 'idle');
40
+ const entries = flattenTree(tree);
41
+
42
+ // Render entire tree as single Text block (1 yoga node instead of 50+)
43
+ const lines = [`${ceoIcon} CEO`, ...entries.map(e => e.line)];
77
44
 
78
45
  return (
79
46
  <Box flexDirection="column" paddingX={1}>
80
47
  <Text bold color={focused ? 'cyan' : 'gray'}>{'\u2500\u2500 Org Tree \u2500\u2500'}</Text>
81
- <Box marginTop={1}>
82
- <Text color={ceoColor} bold={ceoStatus === 'working'}>{ceoIcon} </Text>
83
- <Text
84
- color={isCeoSelected ? 'cyan' : 'yellow'}
85
- bold={isCeoSelected}
86
- inverse={isCeoSelected}
87
- >
88
- CEO
89
- </Text>
90
- </Box>
91
- {entries.map((entry, i) => {
92
- const isSelected = focused && flatRoles[selectedIndex] === entry.roleId;
93
- const icon = statusIcon(entry.status);
94
- const color = statusColor(entry.status);
95
-
96
- return (
97
- <Box key={entry.roleId + '-' + i}>
98
- <Text color="gray">{entry.prefix}</Text>
99
- <Text
100
- color={color}
101
- bold={entry.status === 'working'}
102
- >
103
- {icon}
104
- </Text>
105
- <Text> </Text>
106
- <Text
107
- color={isSelected ? 'cyan' : 'white'}
108
- bold={isSelected}
109
- inverse={isSelected}
110
- >
111
- {entry.roleId}
112
- </Text>
113
- </Box>
114
- );
115
- })}
48
+ <Text color="white">{'\n' + lines.join('\n')}</Text>
116
49
  </Box>
117
50
  );
118
51
  });