tycono 0.3.14-beta.0 → 0.3.14-beta.2

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.0",
3
+ "version": "0.3.14-beta.2",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,7 @@ export interface AgentConfig {
29
29
  tokenLedger?: TokenLedger; // Token usage ledger (optional)
30
30
  attachments?: ImageAttachment[]; // Image attachments for vision
31
31
  targetRoles?: string[]; // Selective dispatch scope
32
+ presetId?: string; // Wave-scoped preset for knowledge injection
32
33
  // Callbacks
33
34
  onText?: (text: string) => void;
34
35
  onToolExec?: (name: string, input: Record<string, unknown>) => void;
@@ -162,7 +163,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
162
163
  const llm = config.llm ?? new AnthropicProvider();
163
164
 
164
165
  // 1. Assemble context
165
- const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus: config.teamStatus, targetRoles: config.targetRoles });
166
+ const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus: config.teamStatus, targetRoles: config.targetRoles, presetId: config.presetId });
166
167
 
167
168
  // Trace: capture assembled prompt for debugging
168
169
  config.onPromptAssembled?.(context.systemPrompt, task);
@@ -51,7 +51,7 @@ export function assembleContext(
51
51
  task: string,
52
52
  sourceRole: string,
53
53
  orgTree: OrgTree,
54
- options?: { teamStatus?: TeamStatus; targetRoles?: string[] },
54
+ options?: { teamStatus?: TeamStatus; targetRoles?: string[]; presetId?: string },
55
55
  ): AssembledContext {
56
56
  const node = orgTree.nodes.get(roleId);
57
57
  if (!node) {
@@ -123,6 +123,14 @@ Use the code repository path for all source code work (reading, writing, buildin
123
123
  sections.push(preKSection);
124
124
  }
125
125
 
126
+ // 11. Preset Knowledge (wave-scoped preset docs)
127
+ if (options?.presetId && options.presetId !== 'default') {
128
+ const presetKnowledge = loadPresetKnowledge(companyRoot, options.presetId);
129
+ if (presetKnowledge) {
130
+ sections.push('# Preset Knowledge\n\n' + presetKnowledge);
131
+ }
132
+ }
133
+
126
134
  // Task는 별도 필드로 분리
127
135
  let subordinates = getSubordinates(orgTree, roleId);
128
136
 
@@ -262,6 +270,27 @@ ${docList}
262
270
  > **Knowledging Rule**: Check these documents first. If your work produces new knowledge, update existing docs or create new ones with cross-links.`;
263
271
  }
264
272
 
273
+ /**
274
+ * Load knowledge docs from a preset's knowledge/ directory.
275
+ * Returns concatenated content (capped at 2000 chars per doc).
276
+ */
277
+ function loadPresetKnowledge(companyRoot: string, presetId: string): string | null {
278
+ const knowledgeDir = path.join(companyRoot, 'company', 'presets', presetId, 'knowledge');
279
+ if (!fs.existsSync(knowledgeDir)) return null;
280
+
281
+ const parts: string[] = [];
282
+ try {
283
+ const entries = fs.readdirSync(knowledgeDir).filter(f => f.endsWith('.md'));
284
+ for (const file of entries.slice(0, 10)) { // Cap at 10 docs
285
+ const content = fs.readFileSync(path.join(knowledgeDir, file), 'utf-8');
286
+ const preview = content.slice(0, 2000);
287
+ parts.push(`## ${file}\n\n${preview}${content.length > 2000 ? '\n\n... (truncated)' : ''}`);
288
+ }
289
+ } catch { /* ignore */ }
290
+
291
+ return parts.length > 0 ? parts.join('\n\n---\n\n') : null;
292
+ }
293
+
265
294
  function loadCompanyRules(companyRoot: string): string | null {
266
295
  const parts: string[] = [];
267
296
 
@@ -407,7 +407,7 @@ else:
407
407
  */
408
408
  export class ClaudeCliRunner implements ExecutionRunner {
409
409
  execute(config: RunnerConfig, callbacks: RunnerCallbacks): RunnerHandle {
410
- const { companyRoot, roleId, task, sourceRole, orgTree, readOnly = false, teamStatus, attachments, targetRoles } = config;
410
+ const { companyRoot, roleId, task, sourceRole, orgTree, readOnly = false, teamStatus, attachments, targetRoles, presetId } = config;
411
411
 
412
412
  // Note: Claude CLI doesn't support inline image attachments.
413
413
  // Images will be ignored with a warning if passed.
@@ -416,7 +416,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
416
416
  }
417
417
 
418
418
  // 1. Context Assembly
419
- const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus, targetRoles });
419
+ const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus, targetRoles, presetId });
420
420
 
421
421
  // Trace: capture assembled prompt for debugging
422
422
  callbacks.onPromptAssembled?.(context.systemPrompt, task);
@@ -45,6 +45,8 @@ export interface RunnerConfig {
45
45
  codeRoot?: string;
46
46
  /** PSM-004: Environment variables to inject (e.g., port assignments) */
47
47
  env?: Record<string, string>;
48
+ /** Wave-scoped preset ID for knowledge injection */
49
+ presetId?: string;
48
50
  /** SV-7: Supervision — abort a running session */
49
51
  onAbortSession?: (sessionId: string) => boolean;
50
52
  /** SV-6: Supervision — amend a running session */
@@ -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
+ });
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { COMPANY_ROOT } from './file-reader.js';
2
4
  import { ActivityStream, type ActivityEvent } from './activity-stream.js';
3
5
  import { buildOrgTree } from '../engine/org-tree.js';
@@ -116,7 +118,20 @@ class ExecutionManager {
116
118
 
117
119
  startExecution(params: StartExecutionParams): Execution {
118
120
  const execId = `exec-${Date.now()}-${this.nextId++}`;
119
- const orgTree = buildOrgTree(COMPANY_ROOT);
121
+
122
+ // Resolve preset from wave file for org tree building
123
+ let presetId: string | undefined;
124
+ const session = getSession(params.sessionId);
125
+ if (session?.waveId) {
126
+ try {
127
+ const wavePath = path.join(COMPANY_ROOT, 'operations', 'waves', `${session.waveId}.json`);
128
+ if (fs.existsSync(wavePath)) {
129
+ const waveData = JSON.parse(fs.readFileSync(wavePath, 'utf-8'));
130
+ presetId = waveData.preset;
131
+ }
132
+ } catch { /* ignore */ }
133
+ }
134
+ const orgTree = buildOrgTree(COMPANY_ROOT, presetId);
120
135
 
121
136
  // Authority gate
122
137
  if (params.sourceRole && params.sourceRole !== 'ceo') {
@@ -243,6 +258,7 @@ class ExecutionManager {
243
258
  sessionId: params.sessionId,
244
259
  teamStatus,
245
260
  targetRoles: params.targetRoles,
261
+ presetId,
246
262
  codeRoot: resolveCodeRoot(COMPANY_ROOT),
247
263
  attachments: params.attachments,
248
264
  env: {
@@ -676,7 +692,18 @@ class ExecutionManager {
676
692
  if (runningChildren.length === 0) return;
677
693
 
678
694
  // Only restart C-Level roles (CTO, CBO etc.)
679
- const orgTree = buildOrgTree(COMPANY_ROOT);
695
+ // Resolve preset from wave file for correct org tree
696
+ let recoveryPresetId: string | undefined;
697
+ const deadSession = getSession(deadExecution.sessionId);
698
+ if (deadSession?.waveId) {
699
+ try {
700
+ const wp = path.join(COMPANY_ROOT, 'operations', 'waves', `${deadSession.waveId}.json`);
701
+ if (fs.existsSync(wp)) {
702
+ recoveryPresetId = JSON.parse(fs.readFileSync(wp, 'utf-8')).preset;
703
+ }
704
+ } catch { /* ignore */ }
705
+ }
706
+ const orgTree = buildOrgTree(COMPANY_ROOT, recoveryPresetId);
680
707
  const node = orgTree.nodes.get(deadExecution.roleId);
681
708
  if (!node || node.level !== 'c-level') return;
682
709
 
@@ -697,8 +724,8 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
697
724
  console.log(`[ExecMgr] Supervision recovery: ${deadExecution.roleId} died with ${runningChildren.length} running children. Restarting.`);
698
725
 
699
726
  // Propagate waveId from the dead session
700
- const deadSession = getSession(deadExecution.sessionId);
701
- const waveId = deadSession?.waveId;
727
+ const deadSes = getSession(deadExecution.sessionId);
728
+ const waveId = deadSes?.waveId;
702
729
 
703
730
  // Create new session for recovery
704
731
  const newSession = createSession(deadExecution.roleId, {
@@ -139,7 +139,7 @@ function readFilePreview(filePath: string, maxLines: number): string[] {
139
139
  }
140
140
  }
141
141
 
142
- export const PanelMode: React.FC<PanelModeProps> = React.memo(({
142
+ const PanelModeInner: React.FC<PanelModeProps> = ({
143
143
  tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
144
144
  streamStatus, waveId, activeSessions, allSessions, companyRoot, waves,
145
145
  focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
@@ -338,6 +338,19 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
338
338
  ? allSessions.filter(s => s.waveId === focusedWaveId).length
339
339
  : 0;
340
340
 
341
+ // Read preset from wave file on disk
342
+ const wavePreset = useMemo(() => {
343
+ if (!focusedWaveId || !companyRoot) return null;
344
+ try {
345
+ const wavePath = path.join(companyRoot, 'operations', 'waves', `${focusedWaveId}.json`);
346
+ if (fs.existsSync(wavePath)) {
347
+ const data = JSON.parse(fs.readFileSync(wavePath, 'utf-8'));
348
+ return data.preset as string | undefined;
349
+ }
350
+ } catch { /* ignore */ }
351
+ return null;
352
+ }, [focusedWaveId, companyRoot]);
353
+
341
354
  const leftWidth = 28;
342
355
 
343
356
  return (
@@ -347,6 +360,9 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
347
360
  <Box flexDirection="column" width={leftWidth}>
348
361
  <Box paddingX={1}>
349
362
  <Text color="green" bold>W{focusedWaveIndex}</Text>
363
+ {wavePreset && wavePreset !== 'default' && (
364
+ <Text color="magenta"> ({wavePreset})</Text>
365
+ )}
350
366
  <Text color="gray"> </Text>
351
367
  <Text color="white" wrap="truncate">
352
368
  {focusedWave?.directive ? focusedWave.directive.slice(0, leftWidth - 6) : '(idle)'}
@@ -505,6 +521,7 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
505
521
  <Text bold color="cyan">Wave Info</Text>
506
522
  <Text color="gray">{'\u2500'.repeat(40)}</Text>
507
523
  <Text color="white">Wave: {focusedWave?.waveId ?? 'none'}</Text>
524
+ {wavePreset && <Text color="magenta">Preset: {wavePreset}</Text>}
508
525
  <Text color="white">Directive: {focusedWave?.directive || '(idle)'}</Text>
509
526
  <Text color="white">Sessions: {waveSessionCount}</Text>
510
527
  <Text color="white">Files modified: {waveFileSet.size}</Text>
@@ -545,4 +562,6 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
545
562
  </Box>
546
563
  </Box>
547
564
  );
548
- }));
565
+ };
566
+
567
+ export const PanelMode = React.memo(PanelModeInner);
@@ -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> = React.memo(({
119
+ const StreamViewInner: React.FC<StreamViewProps> = ({
120
120
  events,
121
121
  allRoleIds,
122
122
  streamStatus,
@@ -168,4 +168,6 @@ export const StreamView: React.FC<StreamViewProps> = React.memo(({
168
168
  })}
169
169
  </Box>
170
170
  );
171
- }));
171
+ };
172
+
173
+ export const StreamView = React.memo(StreamViewInner);