tycono 0.3.13 → 0.3.14-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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.3.13",
3
+ "version": "0.3.14-beta.0",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -85,7 +85,7 @@ interface RawRoleYaml {
85
85
 
86
86
  /* ─── Build ──────────────────────────────────── */
87
87
 
88
- export function buildOrgTree(companyRoot: string): OrgTree {
88
+ export function buildOrgTree(companyRoot: string, presetId?: string): OrgTree {
89
89
  const rolesDir = path.join(companyRoot, 'roles');
90
90
  const tree: OrgTree = { root: 'ceo', nodes: new Map() };
91
91
 
@@ -102,53 +102,68 @@ export function buildOrgTree(companyRoot: string): OrgTree {
102
102
  reports: { daily: '', weekly: '' },
103
103
  });
104
104
 
105
- if (!fs.existsSync(rolesDir)) return tree;
106
-
107
- // Read all role.yaml files
108
- const entries = fs.readdirSync(rolesDir, { withFileTypes: true });
109
- for (const entry of entries) {
110
- if (!entry.isDirectory()) continue;
111
- const yamlPath = path.join(rolesDir, entry.name, 'role.yaml');
112
- if (!fs.existsSync(yamlPath)) continue;
113
-
114
- try {
115
- const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as RawRoleYaml;
116
- const node: OrgNode = {
117
- id: raw.id || entry.name,
118
- name: raw.name || entry.name,
119
- level: (raw.level as OrgNode['level']) || 'member',
120
- reportsTo: (raw.reports_to || 'ceo').toLowerCase(),
121
- children: [],
122
- persona: raw.persona || '',
123
- authority: {
124
- autonomous: raw.authority?.autonomous ?? [],
125
- needsApproval: raw.authority?.needs_approval ?? [],
126
- },
127
- knowledge: {
128
- reads: raw.knowledge?.reads ?? [],
129
- writes: raw.knowledge?.writes ?? [],
130
- },
131
- reports: {
132
- daily: raw.reports?.daily ?? '',
133
- weekly: raw.reports?.weekly ?? '',
134
- },
135
- skills: raw.skills,
136
- model: raw.model,
137
- source: raw.source ? {
138
- id: raw.source.id || '',
139
- sync: (raw.source.sync as RoleSource['sync']) || 'manual',
140
- forked_at: raw.source.forked_at,
141
- upstream_version: raw.source.upstream_version,
142
- } : undefined,
143
- heartbeat: raw.heartbeat ? {
144
- enabled: raw.heartbeat.enabled ?? false,
145
- intervalSec: raw.heartbeat.intervalSec ?? 120,
146
- maxTicks: raw.heartbeat.maxTicks ?? 60,
147
- } : undefined,
148
- };
149
- tree.nodes.set(node.id, node);
150
- } catch {
151
- // Skip malformed YAML
105
+ // Collect role directories to scan: base roles/ + preset roles/
106
+ const roleDirs: string[] = [];
107
+ if (fs.existsSync(rolesDir)) roleDirs.push(rolesDir);
108
+
109
+ // If preset specified, also scan preset's roles directory
110
+ if (presetId && presetId !== 'default') {
111
+ const presetRolesDir = path.join(companyRoot, 'company', 'presets', presetId, 'roles');
112
+ if (fs.existsSync(presetRolesDir)) roleDirs.push(presetRolesDir);
113
+ }
114
+
115
+ // Read all role.yaml files from all role directories
116
+ for (const dir of roleDirs) {
117
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ if (!entry.isDirectory()) continue;
120
+ const yamlPath = path.join(dir, entry.name, 'role.yaml');
121
+ if (!fs.existsSync(yamlPath)) continue;
122
+
123
+ try {
124
+ const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as RawRoleYaml;
125
+ const nodeId = raw.id || entry.name;
126
+
127
+ // Skip if already loaded (base roles take precedence over preset roles)
128
+ if (tree.nodes.has(nodeId)) continue;
129
+
130
+ const node: OrgNode = {
131
+ id: nodeId,
132
+ name: raw.name || entry.name,
133
+ level: (raw.level as OrgNode['level']) || 'member',
134
+ reportsTo: (raw.reports_to || 'ceo').toLowerCase(),
135
+ children: [],
136
+ persona: raw.persona || '',
137
+ authority: {
138
+ autonomous: raw.authority?.autonomous ?? [],
139
+ needsApproval: raw.authority?.needs_approval ?? [],
140
+ },
141
+ knowledge: {
142
+ reads: raw.knowledge?.reads ?? [],
143
+ writes: raw.knowledge?.writes ?? [],
144
+ },
145
+ reports: {
146
+ daily: raw.reports?.daily ?? '',
147
+ weekly: raw.reports?.weekly ?? '',
148
+ },
149
+ skills: raw.skills,
150
+ model: raw.model,
151
+ source: raw.source ? {
152
+ id: raw.source.id || '',
153
+ sync: (raw.source.sync as RoleSource['sync']) || 'manual',
154
+ forked_at: raw.source.forked_at,
155
+ upstream_version: raw.source.upstream_version,
156
+ } : undefined,
157
+ heartbeat: raw.heartbeat ? {
158
+ enabled: raw.heartbeat.enabled ?? false,
159
+ intervalSec: raw.heartbeat.intervalSec ?? 120,
160
+ maxTicks: raw.heartbeat.maxTicks ?? 60,
161
+ } : undefined,
162
+ };
163
+ tree.nodes.set(node.id, node);
164
+ } catch {
165
+ // Skip malformed YAML
166
+ }
152
167
  }
153
168
  }
154
169
 
@@ -231,8 +246,8 @@ export function canConsult(tree: OrgTree, source: string, target: string): boole
231
246
  }
232
247
 
233
248
  /** Refresh tree (re-read all role.yaml files) */
234
- export function refreshOrgTree(companyRoot: string): OrgTree {
235
- return buildOrgTree(companyRoot);
249
+ export function refreshOrgTree(companyRoot: string, presetId?: string): OrgTree {
250
+ return buildOrgTree(companyRoot, presetId);
236
251
  }
237
252
 
238
253
  /** Get a human-readable org chart string for context injection */
@@ -223,6 +223,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
223
223
 
224
224
  const targetRoles = body.targetRoles as string[] | undefined;
225
225
  const continuous = body.continuous === true;
226
+ const preset = body.preset as string | undefined;
226
227
 
227
228
  // Always use supervisor mode — CEO supervises C-Levels who supervise members
228
229
  {
@@ -231,6 +232,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
231
232
  actualDirective,
232
233
  targetRoles && targetRoles.length > 0 ? targetRoles : undefined,
233
234
  continuous,
235
+ preset,
234
236
  );
235
237
 
236
238
  if (state.status === 'error') {
@@ -319,7 +319,7 @@ export function scaffold(config: ScaffoldConfig): string[] {
319
319
  'operations/decisions', 'operations/activity-streams',
320
320
  'operations/sessions', 'operations/cost',
321
321
  'knowledge', 'methodologies', '.claude/skills',
322
- '.claude/skills/_shared', '.tycono',
322
+ '.claude/skills/_shared', '.tycono', 'company/presets',
323
323
  ];
324
324
  for (const dir of dirs) {
325
325
  fs.mkdirSync(path.join(root, dir), { recursive: true });
@@ -28,6 +28,7 @@ interface SupervisorState {
28
28
  directive: string;
29
29
  targetRoles?: string[];
30
30
  continuous: boolean;
31
+ preset?: string;
31
32
  supervisorSessionId: string | null;
32
33
  executionId: string | null;
33
34
  status: 'starting' | 'running' | 'restarting' | 'stopped' | 'error';
@@ -66,7 +67,7 @@ class SupervisorHeartbeat {
66
67
  * This creates a supervisor session and starts an execution.
67
68
  * If the execution dies, it auto-restarts (heartbeat).
68
69
  */
69
- start(waveId: string, directive: string, targetRoles?: string[], continuous = false): SupervisorState {
70
+ start(waveId: string, directive: string, targetRoles?: string[], continuous = false, preset?: string): SupervisorState {
70
71
  // Check if supervisor already running for this wave
71
72
  const existing = this.supervisors.get(waveId);
72
73
  if (existing && (existing.status === 'running' || existing.status === 'starting')) {
@@ -79,6 +80,7 @@ class SupervisorHeartbeat {
79
80
  directive,
80
81
  targetRoles,
81
82
  continuous,
83
+ preset,
82
84
  supervisorSessionId: null,
83
85
  executionId: null,
84
86
  status: 'starting',
@@ -100,7 +102,7 @@ class SupervisorHeartbeat {
100
102
  }
101
103
 
102
104
  // Save wave file immediately so directive persists across restarts
103
- this.saveWaveFile(waveId, directive);
105
+ this.saveWaveFile(waveId, directive, preset);
104
106
 
105
107
  this.spawnSupervisor(state);
106
108
  return state;
@@ -110,20 +112,22 @@ class SupervisorHeartbeat {
110
112
  * Save wave file immediately so directive persists across restarts.
111
113
  * saveCompletedWave() adds session/role details on completion.
112
114
  */
113
- private saveWaveFile(waveId: string, directive: string): void {
115
+ private saveWaveFile(waveId: string, directive: string, preset?: string): void {
114
116
  try {
115
117
  const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
116
118
  if (!fs.existsSync(wavesDir)) fs.mkdirSync(wavesDir, { recursive: true });
117
119
  const wavePath = path.join(wavesDir, `${waveId}.json`);
118
120
  if (!fs.existsSync(wavePath)) {
119
- fs.writeFileSync(wavePath, JSON.stringify({
121
+ const waveData: Record<string, unknown> = {
120
122
  id: waveId,
121
123
  waveId,
122
124
  directive,
123
125
  startedAt: new Date().toISOString(),
124
126
  sessionIds: [],
125
127
  roles: [],
126
- }, null, 2));
128
+ };
129
+ if (preset) waveData.preset = preset;
130
+ fs.writeFileSync(wavePath, JSON.stringify(waveData, null, 2));
127
131
  console.log(`[Supervisor] Wave file created: ${wavePath}`);
128
132
  }
129
133
  } catch (err) {
@@ -510,7 +514,7 @@ Do NOT dispatch anyone. Do NOT create new files. Just answer concisely.`;
510
514
  /* ─── Internal: Spawn / Restart ────────────── */
511
515
 
512
516
  private spawnSupervisor(state: SupervisorState): void {
513
- const orgTree = buildOrgTree(COMPANY_ROOT);
517
+ const orgTree = buildOrgTree(COMPANY_ROOT, state.preset);
514
518
  let cLevelRoles = getSubordinates(orgTree, 'ceo');
515
519
 
516
520
  if (state.targetRoles && state.targetRoles.length > 0) {
@@ -304,7 +304,16 @@ export function saveCompletedWave(waveId: string, directive: string): { ok: bool
304
304
  }
305
305
  }
306
306
 
307
- const waveJson = {
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> = {
308
317
  id: baseName,
309
318
  directive,
310
319
  startedAt: startedAt.toISOString(),
@@ -313,6 +322,7 @@ export function saveCompletedWave(waveId: string, directive: string): { ok: bool
313
322
  waveId,
314
323
  sessionIds: allSessionIds,
315
324
  };
325
+ if (existingPreset) waveJson.preset = existingPreset;
316
326
  fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
317
327
 
318
328
  const relativePath = `operations/waves/${baseName}.json`;
package/src/tui/app.tsx CHANGED
@@ -21,7 +21,7 @@ import { useApi } from './hooks/useApi';
21
21
  import { useSSE } from './hooks/useSSE';
22
22
  import { useCommand, type WaveInfo } from './hooks/useCommand';
23
23
  import { dispatchWave } from './api';
24
- import type { ActiveSessionInfo } from './api';
24
+ import type { ActiveSessionInfo, PresetSummary } from './api';
25
25
  import { buildOrgTree, flattenOrgRoleIds } from './store';
26
26
 
27
27
  type Mode = 'command' | 'panel';
@@ -231,6 +231,10 @@ export const App: React.FC = () => {
231
231
  // System messages (command feedback displayed in stream area)
232
232
  const [systemMessages, setSystemMessages] = useState<StreamLine[]>([]);
233
233
 
234
+ // Preset selection state (for /new without args)
235
+ const [pendingPresetSelect, setPendingPresetSelect] = useState<PresetSummary[] | null>(null);
236
+ const selectedPresetRef = useRef<string | null>(null);
237
+
234
238
  // Terminal full height with resize tracking (minus 1 for wide-char overflow safety)
235
239
  const [termHeight, setTermHeight] = useState((process.stdout.rows || 30) - 1);
236
240
 
@@ -337,21 +341,24 @@ export const App: React.FC = () => {
337
341
  return waves.find(w => w.waveId === focusedWaveId)?.startedAt ?? 0;
338
342
  }, [focusedWaveId, waves]);
339
343
 
344
+ // Wave creation callback — shared by useCommand and preset selection flow
345
+ const onWaveCreated = useCallback((newWaveId: string, directive: string) => {
346
+ const newWave: WaveInfo = {
347
+ waveId: newWaveId,
348
+ directive,
349
+ startedAt: Date.now(),
350
+ };
351
+ setWaves(prev => [...prev, newWave]);
352
+ setFocusedWaveId(newWaveId);
353
+ sse.clearEvents();
354
+ api.refresh();
355
+ }, [sse, api]);
356
+
340
357
  // Command handler
341
358
  const { execute } = useCommand({
342
359
  focusedWaveId,
343
360
  waves,
344
- onWaveCreated: (newWaveId, directive) => {
345
- const newWave: WaveInfo = {
346
- waveId: newWaveId,
347
- directive,
348
- startedAt: Date.now(),
349
- };
350
- setWaves(prev => [...prev, newWave]);
351
- setFocusedWaveId(newWaveId);
352
- sse.clearEvents();
353
- api.refresh();
354
- },
361
+ onWaveCreated,
355
362
  onFocusWave: (waveId) => {
356
363
  setFocusedWaveId(waveId);
357
364
  sse.clearEvents();
@@ -366,6 +373,44 @@ export const App: React.FC = () => {
366
373
  const handleCommandSubmit = useCallback(async (input: string) => {
367
374
  // User input is already shown by CommandMode (immediate commit to Static)
368
375
 
376
+ // Preset selection mode: user types a number to pick preset
377
+ if (pendingPresetSelect) {
378
+ const trimmed = input.trim();
379
+ const idx = parseInt(trimmed, 10);
380
+ if (!isNaN(idx) && idx >= 1 && idx <= pendingPresetSelect.length) {
381
+ const selected = pendingPresetSelect[idx - 1];
382
+ setPendingPresetSelect(null);
383
+ addSystemMessage(`Selected: ${selected.name}. Type your directive:`, 'cyan');
384
+ // Store selected preset for next input
385
+ selectedPresetRef.current = selected.id;
386
+ return;
387
+ }
388
+ // If user typed text instead of number, treat as directive with selected/default preset
389
+ const presetId = selectedPresetRef.current || 'default';
390
+ setPendingPresetSelect(null);
391
+ selectedPresetRef.current = null;
392
+ try {
393
+ const waveResult = await dispatchWave(trimmed || undefined, { preset: presetId });
394
+ onWaveCreated(waveResult.waveId, trimmed);
395
+ } catch (err) {
396
+ addSystemMessage(`Wave failed: ${err instanceof Error ? err.message : 'unknown'}`, 'red');
397
+ }
398
+ return;
399
+ }
400
+
401
+ // If a preset was selected previously, this input is the directive
402
+ if (selectedPresetRef.current) {
403
+ const presetId = selectedPresetRef.current;
404
+ selectedPresetRef.current = null;
405
+ try {
406
+ const waveResult = await dispatchWave(input.trim() || undefined, { preset: presetId });
407
+ onWaveCreated(waveResult.waveId, input.trim());
408
+ } catch (err) {
409
+ addSystemMessage(`Wave failed: ${err instanceof Error ? err.message : 'unknown'}`, 'red');
410
+ }
411
+ return;
412
+ }
413
+
369
414
  const result = await execute(input);
370
415
 
371
416
  switch (result.type) {
@@ -513,6 +558,7 @@ export const App: React.FC = () => {
513
558
  addSystemMessage(' /sessions Sessions + ports (kill/cleanup)', 'white');
514
559
  addSystemMessage(' /kill <id> Kill a session', 'white');
515
560
  addSystemMessage(' /cleanup Remove dead sessions', 'white');
561
+ addSystemMessage(' /preset list Installed presets', 'white');
516
562
  addSystemMessage(' /help This help', 'white');
517
563
  addSystemMessage(' /quit Exit', 'white');
518
564
  addSystemMessage('Keys: [Tab] team panel [1-9] wave [Esc] back [Ctrl+C] quit', 'gray');
@@ -526,6 +572,46 @@ export const App: React.FC = () => {
526
572
  addSystemMessage(`Sessions: ${api.sessions.length} Active: ${activeCount} Waves: ${waves.length} Ports: ${api.portSummary.totalPorts}`, 'white');
527
573
  }
528
574
  break;
575
+ case 'preset_list': {
576
+ const presets = result.presets ?? [];
577
+ if (presets.length === 0) {
578
+ addSystemMessage('No presets installed.', 'gray');
579
+ } else {
580
+ addSystemMessage('Installed presets:', 'cyan');
581
+ for (const p of presets) {
582
+ const star = p.isDefault ? ' \u2605' : '';
583
+ const desc = p.description ? ` \u2014 ${p.description}` : '';
584
+ addSystemMessage(` ${p.id} (${p.rolesCount} roles)${desc}${star}`, p.isDefault ? 'green' : 'white');
585
+ }
586
+ }
587
+ break;
588
+ }
589
+ case 'preset_select': {
590
+ const presets = result.presets ?? [];
591
+ if (presets.length === 0) {
592
+ addSystemMessage('No presets. Creating wave with default team.', 'gray');
593
+ try {
594
+ const waveResult = await dispatchWave();
595
+ onWaveCreated(waveResult.waveId, '');
596
+ } catch { /* ignore */ }
597
+ } else if (presets.length === 1) {
598
+ // Only default → show prompt to enter directive
599
+ addSystemMessage('Only default preset available. Type your directive:', 'gray');
600
+ } else {
601
+ // Multiple presets → show selection
602
+ addSystemMessage('Select a team preset for this wave:', 'cyan');
603
+ for (let i = 0; i < presets.length; i++) {
604
+ const p = presets[i];
605
+ const star = p.isDefault ? ' \u2605' : '';
606
+ const desc = p.description ? ` \u2014 ${p.description}` : '';
607
+ addSystemMessage(` ${i + 1}. ${p.name} (${p.rolesCount} roles)${desc}${star}`, p.isDefault ? 'green' : 'white');
608
+ }
609
+ addSystemMessage('Type a number to select, then enter your directive.', 'gray');
610
+ // Store presets for number selection — handled via pendingPresetSelect
611
+ setPendingPresetSelect(presets);
612
+ }
613
+ break;
614
+ }
529
615
  case 'panel':
530
616
  break;
531
617
  case 'quit':
@@ -535,7 +621,7 @@ export const App: React.FC = () => {
535
621
  addSystemMessage(result.message, 'green');
536
622
  }
537
623
  }
538
- }, [execute, addSystemMessage, addSystemLines, focusedWaveId, focusedWaveIndex, derivedWaveStatus, api.sessions.length, activeCount, waves, api.activeSessions, api.portSummary]);
624
+ }, [execute, addSystemMessage, addSystemLines, focusedWaveId, focusedWaveIndex, derivedWaveStatus, api.sessions.length, activeCount, waves, api.activeSessions, api.portSummary, pendingPresetSelect, onWaveCreated]);
539
625
 
540
626
  // Global key handler: Tab to toggle mode, Ctrl+C always exits
541
627
  useInput((input, key) => {
@@ -299,6 +299,7 @@ const COMMANDS: Array<{ cmd: string; desc: string }> = [
299
299
  { cmd: '/sessions', desc: 'Active sessions' },
300
300
  { cmd: '/kill <id>', desc: 'Kill session' },
301
301
  { cmd: '/cleanup', desc: 'Remove dead' },
302
+ { cmd: '/preset list', desc: 'Installed presets' },
302
303
  { cmd: '/help', desc: 'Help' },
303
304
  { cmd: '/quit', desc: 'Exit' },
304
305
  ];
@@ -139,7 +139,7 @@ function readFilePreview(filePath: string, maxLines: number): string[] {
139
139
  }
140
140
  }
141
141
 
142
- export const PanelMode: React.FC<PanelModeProps> = ({
142
+ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
143
143
  tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
144
144
  streamStatus, waveId, activeSessions, allSessions, companyRoot, waves,
145
145
  focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
@@ -545,4 +545,4 @@ export const PanelMode: React.FC<PanelModeProps> = ({
545
545
  </Box>
546
546
  </Box>
547
547
  );
548
- };
548
+ }));
@@ -116,7 +116,7 @@ function renderEvent(event: SSEEvent): { content: string; contentColor: string }
116
116
  }
117
117
  }
118
118
 
119
- export const StreamView: React.FC<StreamViewProps> = ({
119
+ export const StreamView: React.FC<StreamViewProps> = React.memo(({
120
120
  events,
121
121
  allRoleIds,
122
122
  streamStatus,
@@ -162,10 +162,10 @@ export const StreamView: React.FC<StreamViewProps> = ({
162
162
  <Box key={`${event.seq}-${i}`}>
163
163
  <Text color="gray" dimColor>{formatTime(event.ts)} </Text>
164
164
  <Text color={roleColor} bold>{event.roleId.padEnd(12)}</Text>
165
- <Text color={rendered.contentColor} wrap="wrap">{rendered.content}</Text>
165
+ <Text color={rendered.contentColor} wrap="truncate">{rendered.content}</Text>
166
166
  </Box>
167
167
  );
168
168
  })}
169
169
  </Box>
170
170
  );
171
- };
171
+ }));