tycono 0.3.45-beta.2 → 0.3.45-beta.3
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/README.md +191 -162
- package/bin/tycono.ts +42 -10
- package/package.json +21 -15
- package/packages/server/bin/cli.js +35 -0
- package/packages/server/bin/server.ts +183 -0
- package/{src → packages/server/src}/api/src/create-server.ts +11 -3
- package/{src → packages/server/src}/api/src/engine/agent-loop.ts +30 -7
- package/{src → packages/server/src}/api/src/engine/context-assembler.ts +122 -57
- package/{src → packages/server/src}/api/src/engine/llm-adapter.ts +10 -7
- package/{src → packages/server/src}/api/src/engine/org-tree.ts +43 -3
- package/{src → packages/server/src}/api/src/engine/runners/claude-cli.ts +37 -15
- package/{src → packages/server/src}/api/src/engine/runners/types.ts +6 -0
- package/{src → packages/server/src}/api/src/engine/tools/executor.ts +65 -9
- package/{src → packages/server/src}/api/src/routes/execute.ts +221 -17
- package/packages/server/src/api/src/services/claude-md-manager.ts +190 -0
- package/{src → packages/server/src}/api/src/services/company-config.ts +1 -0
- package/{src → packages/server/src}/api/src/services/digest-engine.ts +4 -1
- package/packages/server/src/api/src/services/dispatch-classifier.ts +179 -0
- package/{src → packages/server/src}/api/src/services/execution-manager.ts +227 -21
- package/{src → packages/server/src}/api/src/services/file-reader.ts +4 -1
- package/packages/server/src/api/src/services/preset-loader.ts +310 -0
- package/{src → packages/server/src}/api/src/services/supervisor-heartbeat.ts +89 -9
- package/{src → packages/server/src}/api/src/services/wave-multiplexer.ts +18 -8
- package/{src → packages/server/src}/api/src/services/wave-tracker.ts +25 -0
- package/packages/server/src/core/scaffolder.ts +620 -0
- package/{src → packages/server/src}/shared/types.ts +3 -1
- package/packages/server/templates/CLAUDE.md.tmpl +152 -0
- package/packages/server/templates/agentic-knowledge-base.md +355 -0
- package/src/api/src/services/claude-md-manager.ts +0 -94
- package/src/api/src/services/preset-loader.ts +0 -149
- package/templates/CLAUDE.md.tmpl +0 -239
- /package/{src/web → packages/pixel}/dist/assets/index-BJyiMGkM.js +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-BOuHc64o.css +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-DDPzbp9E.js +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-DVKWFwwK.css +0 -0
- /package/{src/web → packages/pixel}/dist/assets/preview-app-DZ6WxhDc.js +0 -0
- /package/{src/web → packages/pixel}/dist/index.html +0 -0
- /package/{src/web → packages/pixel}/dist/tyconoforge.js +0 -0
- /package/{src → packages/server/src}/api/package.json +0 -0
- /package/{src → packages/server/src}/api/src/create-app.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/authority-validator.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/index.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/knowledge-gate.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/role-lifecycle.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/runners/direct-api.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/runners/index.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/skill-template.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/tools/definitions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/active-sessions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/coins.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/company.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/cost.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/engine.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/git.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/knowledge.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/operations.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/preferences.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/presets.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/projects.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/quests.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/roles.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/save.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/sessions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/setup.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/skills.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/speech.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/supervision.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/sync.ts +0 -0
- /package/{src → packages/server/src}/api/src/server.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/activity-stream.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/activity-tracker.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/database.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/git-save.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/job-manager.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/knowledge-importer.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/markdown-parser.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/port-registry.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/preferences.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/pricing.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/scaffold.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/session-store.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/team-recommender.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/token-ledger.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/wave-messages.ts +0 -0
- /package/{src → packages/server/src}/api/src/utils/role-level.ts +0 -0
- /package/{templates → packages/server/templates}/company.md.tmpl +0 -0
- /package/{templates → packages/server/templates}/gitignore.tmpl +0 -0
- /package/{templates → packages/server/templates}/roles.md.tmpl +0 -0
- /package/{templates → packages/server/templates}/skills/_manifest.json +0 -0
- /package/{templates → packages/server/templates}/skills/agent-browser/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/agent-browser/meta.json +0 -0
- /package/{templates → packages/server/templates}/skills/akb-linter/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/akb-linter/meta.json +0 -0
- /package/{templates → packages/server/templates}/skills/knowledge-gate/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/knowledge-gate/meta.json +0 -0
- /package/{templates → packages/server/templates}/teams/agency.json +0 -0
- /package/{templates → packages/server/templates}/teams/research.json +0 -0
- /package/{templates → packages/server/templates}/teams/startup.json +0 -0
- /package/{src/tui → packages/tui/src}/api.ts +0 -0
- /package/{src/tui → packages/tui/src}/app.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/CommandMode.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/OrgTree.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/PanelMode.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/SetupWizard.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/StatusBar.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/StreamView.tsx +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useApi.ts +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useCommand.ts +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useSSE.ts +0 -0
- /package/{src/tui → packages/tui/src}/index.tsx +0 -0
- /package/{src/tui → packages/tui/src}/store.ts +0 -0
- /package/{src/tui → packages/tui/src}/theme.ts +0 -0
- /package/{src/tui → packages/tui/src}/utils/markdown.tsx +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preset-loader.ts — Load presets from multiple sources (2-Layer Knowledge)
|
|
3
|
+
*
|
|
4
|
+
* Scan order (first match wins per preset ID):
|
|
5
|
+
* 1. knowledge/presets/{name}/preset.yaml (legacy/local presets)
|
|
6
|
+
* 2. .tycono/agencies/{name}/preset.yaml (local agency install)
|
|
7
|
+
* 3. ~/.tycono/agencies/{name}/preset.yaml (global agency install)
|
|
8
|
+
* 4. Bundled presets (shipped with tycono-server)
|
|
9
|
+
*
|
|
10
|
+
* Returns PresetSummary[] for TUI display and full LoadedPreset for wave creation.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import YAML from 'yaml';
|
|
18
|
+
import type { PresetDefinition, LoadedPreset, PresetSummary } from '../../../shared/types.js';
|
|
19
|
+
|
|
20
|
+
const PRESETS_DIR = 'knowledge/presets';
|
|
21
|
+
const DEFAULT_PRESET_FILE = '_default.yaml';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a default preset definition from existing roles/ directory.
|
|
25
|
+
* This is generated on-the-fly — no need to persist _default.yaml.
|
|
26
|
+
*/
|
|
27
|
+
function buildDefaultPreset(companyRoot: string): LoadedPreset {
|
|
28
|
+
const rolesDir = path.join(companyRoot, 'knowledge', 'roles');
|
|
29
|
+
const roles: string[] = [];
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(rolesDir)) {
|
|
32
|
+
const entries = fs.readdirSync(rolesDir, { withFileTypes: true });
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (!entry.isDirectory()) continue;
|
|
35
|
+
const yamlPath = path.join(rolesDir, entry.name, 'role.yaml');
|
|
36
|
+
if (fs.existsSync(yamlPath)) {
|
|
37
|
+
roles.push(entry.name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
definition: {
|
|
44
|
+
spec: 'preset/v1',
|
|
45
|
+
id: 'default',
|
|
46
|
+
name: 'Default Team',
|
|
47
|
+
tagline: 'Your current team',
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
roles,
|
|
50
|
+
},
|
|
51
|
+
path: null,
|
|
52
|
+
isDefault: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load a single preset from a directory containing preset.yaml.
|
|
58
|
+
*/
|
|
59
|
+
function loadPresetFromDir(presetDir: string): LoadedPreset | null {
|
|
60
|
+
const yamlPath = path.join(presetDir, 'preset.yaml');
|
|
61
|
+
if (!fs.existsSync(yamlPath)) return null;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as PresetDefinition;
|
|
65
|
+
if (!raw.id || !raw.name || !Array.isArray(raw.roles)) return null;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
definition: {
|
|
69
|
+
spec: raw.spec || 'preset/v1',
|
|
70
|
+
id: raw.id,
|
|
71
|
+
name: raw.name,
|
|
72
|
+
tagline: raw.tagline,
|
|
73
|
+
version: raw.version || '1.0.0',
|
|
74
|
+
description: raw.description,
|
|
75
|
+
author: raw.author,
|
|
76
|
+
category: raw.category,
|
|
77
|
+
industry: raw.industry,
|
|
78
|
+
stage: raw.stage,
|
|
79
|
+
use_case: raw.use_case,
|
|
80
|
+
roles: raw.roles,
|
|
81
|
+
knowledge_docs: raw.knowledge_docs,
|
|
82
|
+
skills_count: raw.skills_count,
|
|
83
|
+
pricing: raw.pricing,
|
|
84
|
+
tags: raw.tags,
|
|
85
|
+
languages: raw.languages,
|
|
86
|
+
stats: raw.stats,
|
|
87
|
+
wave_scoped: raw.wave_scoped,
|
|
88
|
+
},
|
|
89
|
+
path: presetDir,
|
|
90
|
+
isDefault: false,
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load all presets from knowledge/presets/ + auto-generated default.
|
|
99
|
+
* Returns [default, ...installed] — default is always first.
|
|
100
|
+
*/
|
|
101
|
+
export function loadPresets(companyRoot: string): LoadedPreset[] {
|
|
102
|
+
const presets: LoadedPreset[] = [];
|
|
103
|
+
|
|
104
|
+
// 1. Default preset (always present)
|
|
105
|
+
const defaultPreset = buildDefaultPreset(companyRoot);
|
|
106
|
+
|
|
107
|
+
// Check if _default.yaml exists with overrides
|
|
108
|
+
const defaultYamlPath = path.join(companyRoot, PRESETS_DIR, DEFAULT_PRESET_FILE);
|
|
109
|
+
if (fs.existsSync(defaultYamlPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const raw = YAML.parse(fs.readFileSync(defaultYamlPath, 'utf-8')) as Partial<PresetDefinition>;
|
|
112
|
+
if (raw.name) defaultPreset.definition.name = raw.name;
|
|
113
|
+
if (raw.tagline) defaultPreset.definition.tagline = raw.tagline;
|
|
114
|
+
if (raw.description) defaultPreset.definition.description = raw.description;
|
|
115
|
+
} catch { /* ignore malformed _default.yaml */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
presets.push(defaultPreset);
|
|
119
|
+
|
|
120
|
+
// 2. Installed presets from knowledge/presets/{name}/preset.yaml
|
|
121
|
+
const presetsDir = path.join(companyRoot, PRESETS_DIR);
|
|
122
|
+
if (fs.existsSync(presetsDir)) {
|
|
123
|
+
const entries = fs.readdirSync(presetsDir, { withFileTypes: true });
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (!entry.isDirectory()) continue;
|
|
126
|
+
const preset = loadPresetFromDir(path.join(presetsDir, entry.name));
|
|
127
|
+
if (preset) presets.push(preset);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Installed agencies from .tycono/agencies/ (2-Layer Knowledge)
|
|
132
|
+
// Local project agencies take priority, then global (~/.tycono/agencies/)
|
|
133
|
+
const agencyDirs = [
|
|
134
|
+
path.join(companyRoot, '.tycono', 'agencies'),
|
|
135
|
+
path.join(os.homedir(), '.tycono', 'agencies'),
|
|
136
|
+
];
|
|
137
|
+
const loadedIds = new Set(presets.map(p => p.definition.id));
|
|
138
|
+
for (const agenciesDir of agencyDirs) {
|
|
139
|
+
if (!fs.existsSync(agenciesDir)) continue;
|
|
140
|
+
const entries = fs.readdirSync(agenciesDir, { withFileTypes: true });
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (!entry.isDirectory()) continue;
|
|
143
|
+
if (loadedIds.has(entry.name)) continue; // earlier sources take priority
|
|
144
|
+
const preset = loadPresetFromDir(path.join(agenciesDir, entry.name));
|
|
145
|
+
if (preset) {
|
|
146
|
+
presets.push(preset);
|
|
147
|
+
loadedIds.add(preset.definition.id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 4. Bundled presets (shipped with tycono-server, fallback if not in user's project)
|
|
153
|
+
const __dirname_esm = path.dirname(fileURLToPath(import.meta.url));
|
|
154
|
+
const bundledPresetsDir = path.resolve(__dirname_esm, '../../../../presets');
|
|
155
|
+
if (fs.existsSync(bundledPresetsDir)) {
|
|
156
|
+
const entries = fs.readdirSync(bundledPresetsDir, { withFileTypes: true });
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (!entry.isDirectory()) continue;
|
|
159
|
+
if (loadedIds.has(entry.name)) continue; // user's preset takes priority
|
|
160
|
+
const preset = loadPresetFromDir(path.join(bundledPresetsDir, entry.name));
|
|
161
|
+
if (preset) presets.push(preset);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return presets;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get preset summaries for TUI display.
|
|
170
|
+
*/
|
|
171
|
+
export function getPresetSummaries(companyRoot: string): PresetSummary[] {
|
|
172
|
+
return loadPresets(companyRoot).map(p => ({
|
|
173
|
+
id: p.definition.id,
|
|
174
|
+
name: p.definition.name,
|
|
175
|
+
description: p.definition.description ?? p.definition.tagline,
|
|
176
|
+
rolesCount: p.definition.roles.length,
|
|
177
|
+
roles: p.definition.roles,
|
|
178
|
+
isDefault: p.isDefault,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Find a specific preset by ID.
|
|
184
|
+
* Falls back to remote download from tycono.ai if not found locally.
|
|
185
|
+
*/
|
|
186
|
+
export function getPresetById(companyRoot: string, presetId: string): LoadedPreset | null {
|
|
187
|
+
const presets = loadPresets(companyRoot);
|
|
188
|
+
const local = presets.find(p => p.definition.id === presetId);
|
|
189
|
+
if (local) return local;
|
|
190
|
+
|
|
191
|
+
// Try downloading from tycono.ai preset registry
|
|
192
|
+
const downloaded = downloadPreset(companyRoot, presetId);
|
|
193
|
+
return downloaded;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Download a preset from the remote registry (tycono.ai).
|
|
198
|
+
* Saves to knowledge/presets/{id}/ for future use.
|
|
199
|
+
*/
|
|
200
|
+
function downloadPreset(companyRoot: string, presetId: string): LoadedPreset | null {
|
|
201
|
+
const REGISTRY_URL = process.env.TYCONO_PRESET_REGISTRY || 'https://tycono.ai/api/presets';
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Synchronous HTTP request (preset download is a blocking init step)
|
|
205
|
+
const response = execSync(
|
|
206
|
+
`curl -s --max-time 10 "${REGISTRY_URL}/${presetId}/download"`,
|
|
207
|
+
{ encoding: 'utf-8' },
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const data = JSON.parse(response);
|
|
211
|
+
if (!data.preset || !data.files) return null;
|
|
212
|
+
|
|
213
|
+
// Save to local presets directory
|
|
214
|
+
const targetDir = path.join(companyRoot, PRESETS_DIR, presetId);
|
|
215
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
216
|
+
|
|
217
|
+
// Write preset.yaml
|
|
218
|
+
fs.writeFileSync(
|
|
219
|
+
path.join(targetDir, 'preset.yaml'),
|
|
220
|
+
YAML.stringify(data.preset),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Write knowledge files
|
|
224
|
+
if (data.files && typeof data.files === 'object') {
|
|
225
|
+
for (const [filePath, content] of Object.entries(data.files)) {
|
|
226
|
+
const fullPath = path.join(targetDir, filePath);
|
|
227
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
228
|
+
fs.writeFileSync(fullPath, content as string);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`[Preset] Downloaded "${presetId}" from ${REGISTRY_URL}`);
|
|
233
|
+
return loadPresetFromDir(targetDir);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.warn(`[Preset] Failed to download "${presetId}": ${(err as Error).message}`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Auto-select the best preset based on directive text.
|
|
242
|
+
*
|
|
243
|
+
* Matches directive words against each preset's:
|
|
244
|
+
* - wave_scoped.task_keywords (highest weight: 3)
|
|
245
|
+
* - tags (weight: 2)
|
|
246
|
+
* - use_case (weight: 2)
|
|
247
|
+
* - category + industry (weight: 1)
|
|
248
|
+
*
|
|
249
|
+
* Returns preset ID with highest score, or undefined if no meaningful match.
|
|
250
|
+
* Minimum score threshold: 2 (at least one strong keyword match).
|
|
251
|
+
*/
|
|
252
|
+
export function autoSelectPreset(companyRoot: string, directive: string): string | undefined {
|
|
253
|
+
const presets = loadPresets(companyRoot).filter(p => !p.isDefault);
|
|
254
|
+
if (presets.length === 0) return undefined;
|
|
255
|
+
|
|
256
|
+
const words = directive.toLowerCase().split(/[\s,.:;!?'"()\-]+/).filter(w => w.length > 2);
|
|
257
|
+
if (words.length === 0) return undefined;
|
|
258
|
+
|
|
259
|
+
let bestId: string | undefined;
|
|
260
|
+
let bestScore = 0;
|
|
261
|
+
|
|
262
|
+
for (const preset of presets) {
|
|
263
|
+
const def = preset.definition;
|
|
264
|
+
let score = 0;
|
|
265
|
+
|
|
266
|
+
// task_keywords: strongest signal — exact match (weight 5), partial (weight 2)
|
|
267
|
+
const taskKeywords = def.wave_scoped?.task_keywords ?? [];
|
|
268
|
+
for (const kw of taskKeywords) {
|
|
269
|
+
const kwLower = kw.toLowerCase();
|
|
270
|
+
if (words.includes(kwLower)) {
|
|
271
|
+
score += 5; // exact word match
|
|
272
|
+
} else if (words.some(w => (w.length > 3 && kwLower.includes(w)) || (kwLower.length > 3 && w.includes(kwLower)))) {
|
|
273
|
+
score += 2; // partial match (only for longer words)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// tags: exact match (weight 3), partial (weight 1)
|
|
278
|
+
const tags = def.tags ?? [];
|
|
279
|
+
for (const tag of tags) {
|
|
280
|
+
const tagLower = tag.toLowerCase();
|
|
281
|
+
if (words.includes(tagLower)) {
|
|
282
|
+
score += 3;
|
|
283
|
+
} else if (words.some(w => w.length > 3 && (w.includes(tagLower) || tagLower.includes(w)))) {
|
|
284
|
+
score += 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// use_case: word match (weight 2)
|
|
289
|
+
const useCases = def.use_case ?? [];
|
|
290
|
+
for (const uc of useCases) {
|
|
291
|
+
const ucWords = uc.toLowerCase().split(/[\s\-_]+/).filter(u => u.length > 2);
|
|
292
|
+
for (const ucw of ucWords) {
|
|
293
|
+
if (words.includes(ucw)) score += 2;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// category + industry: exact match only (weight 1)
|
|
298
|
+
if (def.category && words.includes(def.category.toLowerCase())) score += 1;
|
|
299
|
+
if (def.industry && words.includes(def.industry.toLowerCase())) score += 1;
|
|
300
|
+
|
|
301
|
+
if (score > bestScore) {
|
|
302
|
+
bestScore = score;
|
|
303
|
+
bestId = def.id;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Minimum threshold: need at least one exact keyword match (score 3+)
|
|
308
|
+
// Score 2 = only partial matches, too weak for confident auto-selection
|
|
309
|
+
return bestScore >= 3 ? bestId : undefined;
|
|
310
|
+
}
|
|
@@ -35,7 +35,7 @@ interface SupervisorState {
|
|
|
35
35
|
preset?: string;
|
|
36
36
|
supervisorSessionId: string | null;
|
|
37
37
|
executionId: string | null;
|
|
38
|
-
status: 'starting' | 'running' | 'restarting' | 'stopped' | 'error';
|
|
38
|
+
status: 'starting' | 'running' | 'restarting' | 'stopped' | 'error' | 'awaiting_approval';
|
|
39
39
|
crashCount: number;
|
|
40
40
|
maxCrashRetries: number;
|
|
41
41
|
restartTimer: ReturnType<typeof setTimeout> | null;
|
|
@@ -231,8 +231,11 @@ class SupervisorHeartbeat {
|
|
|
231
231
|
// Record user message in wave conversation history
|
|
232
232
|
appendWaveMessage(waveId, { role: 'user', content: text });
|
|
233
233
|
|
|
234
|
-
// If supervisor is stopped
|
|
235
|
-
if (state.status === 'stopped') {
|
|
234
|
+
// If supervisor is stopped or awaiting approval, wake it up
|
|
235
|
+
if (state.status === 'stopped' || state.status === 'awaiting_approval') {
|
|
236
|
+
if (state.status === 'awaiting_approval') {
|
|
237
|
+
console.log(`[Supervisor] Directive received while awaiting approval for wave ${waveId}. Restarting supervisor.`);
|
|
238
|
+
}
|
|
236
239
|
// Update the wave's directive if it was empty (idle wave first message)
|
|
237
240
|
if (!state.directive) {
|
|
238
241
|
state.directive = text;
|
|
@@ -633,6 +636,36 @@ ${cLevelList}
|
|
|
633
636
|
5. **Done condition**: ALL subordinates must be done before you report done
|
|
634
637
|
6. **Crash resilience**: If you restart, digest catches you up
|
|
635
638
|
|
|
639
|
+
## ⛔ Amend-First Rule (COST CRITICAL — G-10)
|
|
640
|
+
**When a C-Level needs follow-up work on the SAME topic, ALWAYS amend instead of re-dispatch.**
|
|
641
|
+
|
|
642
|
+
Re-dispatch creates a new session that reloads ALL context from scratch (~3M tokens = ~$45).
|
|
643
|
+
Amend sends instructions to the existing session — near-zero additional cost.
|
|
644
|
+
|
|
645
|
+
| Situation | Action | Why |
|
|
646
|
+
|-----------|--------|-----|
|
|
647
|
+
| Critic CHALLENGE on Scout's work | **amend** Scout | Scout already has the code loaded |
|
|
648
|
+
| Validator FAIL on Scout's output | **amend** Scout | Scout knows what it changed |
|
|
649
|
+
| Need different work from same role | **dispatch** new | Genuinely new scope |
|
|
650
|
+
| Role crashed or timed out | **dispatch** new | Session is dead |
|
|
651
|
+
|
|
652
|
+
**Decision rule**: If the follow-up references files/code the role already touched → **amend**.
|
|
653
|
+
Only dispatch a NEW session when the task is genuinely unrelated to previous work.
|
|
654
|
+
|
|
655
|
+
**Wrong** (costs $45 per re-dispatch):
|
|
656
|
+
\`\`\`
|
|
657
|
+
dispatch scout "fix token mapping" → ses-001 (3M tokens)
|
|
658
|
+
# Critic challenges...
|
|
659
|
+
dispatch scout "fix token mapping again" → ses-002 (3M tokens, WASTED)
|
|
660
|
+
\`\`\`
|
|
661
|
+
|
|
662
|
+
**Correct** (costs ~$0.01):
|
|
663
|
+
\`\`\`
|
|
664
|
+
dispatch scout "fix token mapping" → ses-001 (3M tokens)
|
|
665
|
+
# Critic challenges...
|
|
666
|
+
amend ses-001 "Critic found issue: [challenge]. Fix the token mapping."
|
|
667
|
+
\`\`\`
|
|
668
|
+
|
|
636
669
|
## Supervisor Guidelines
|
|
637
670
|
- G-01: After amending a C-Level, verify on next tick that they reflected it. If not, escalate to CEO directive priority.
|
|
638
671
|
- G-02: If a C-Level crashes 3+ times consecutively, stop dispatching them and report to CEO.
|
|
@@ -652,6 +685,13 @@ When C-Level A completes while C-Level B is still active:
|
|
|
652
685
|
3. amend B: "C-Level A completed. Here are their deliverables relevant to your work: [summary]. Review and incorporate."
|
|
653
686
|
4. On next tick, verify B acknowledged and reflected A's input
|
|
654
687
|
|
|
688
|
+
## Critic CHALLENGE Relay (MANDATORY)
|
|
689
|
+
⛔ When Critic issues a CHALLENGE, you MUST relay it verbatim to the target role.
|
|
690
|
+
1. Detect CHALLENGE in Critic's output (keywords: CHALLENGE, BLOCK, SNOWBALL, "진짜 원인")
|
|
691
|
+
2. amend target role: "Critic CHALLENGE: [exact challenge content]. Address this specifically."
|
|
692
|
+
3. On next tick, verify the target's response addresses the specific challenge
|
|
693
|
+
4. If not addressed: re-amend: "Critic challenged [X]. Your response did not address it. Respond to the specific challenge."
|
|
694
|
+
|
|
655
695
|
When C-Level A produces intermediate results that B needs:
|
|
656
696
|
1. amend B with the relevant intermediate output
|
|
657
697
|
2. You don't need to wait for A to finish — relay as results become available
|
|
@@ -774,6 +814,12 @@ ${state.continuous ? `## Continuous Improvement Mode (ON)
|
|
|
774
814
|
} else if (event.type === 'msg:error') {
|
|
775
815
|
exec.stream.unsubscribe(subscriber);
|
|
776
816
|
this.onSupervisorCrash(state, String(event.data.message ?? 'unknown error'));
|
|
817
|
+
} else if (event.type === 'approval:needed') {
|
|
818
|
+
// BUG-APPROVAL-DIRECTIVE-LOSS: CEO outputs [APPROVAL_NEEDED] then exits.
|
|
819
|
+
// Don't complete the wave — transition to awaiting_approval so directive can restart supervisor.
|
|
820
|
+
console.log(`[Supervisor] CEO awaiting approval for wave ${state.waveId}. Wave stays alive for directive.`);
|
|
821
|
+
state.status = 'awaiting_approval';
|
|
822
|
+
// Don't unsubscribe — CEO process will emit msg:done next, which we need to catch
|
|
777
823
|
} else if (event.type === 'msg:awaiting_input') {
|
|
778
824
|
// BUG-016: turn:limit causes awaiting_input — treat as done-guard
|
|
779
825
|
// If all children are done → complete wave. Otherwise restart supervisor.
|
|
@@ -787,23 +833,57 @@ ${state.continuous ? `## Continuous Improvement Mode (ON)
|
|
|
787
833
|
}
|
|
788
834
|
|
|
789
835
|
private onSupervisorDone(state: SupervisorState): void {
|
|
790
|
-
//
|
|
836
|
+
// BUG-APPROVAL-DIRECTIVE-LOSS: If CEO exited after [APPROVAL_NEEDED],
|
|
837
|
+
// don't complete the wave. Wait for user directive to restart supervisor.
|
|
838
|
+
if (state.status === 'awaiting_approval') {
|
|
839
|
+
console.log(`[Supervisor] CEO done with approval pending for wave ${state.waveId}. Wave stays alive for directive.`);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Check if there are still running or paused C-Level sessions for this wave
|
|
791
844
|
const waveSessions = listSessions().filter(s => s.waveId === state.waveId && s.id !== state.supervisorSessionId);
|
|
792
845
|
const runningChildren = waveSessions.filter(s => {
|
|
793
846
|
const exec = executionManager.getActiveExecution(s.id);
|
|
794
847
|
return exec && exec.status === 'running';
|
|
795
848
|
});
|
|
849
|
+
const awaitingChildren = waveSessions.filter(s => {
|
|
850
|
+
const exec = executionManager.getActiveExecution(s.id);
|
|
851
|
+
return exec && exec.status === 'awaiting_input';
|
|
852
|
+
});
|
|
796
853
|
|
|
797
|
-
if (
|
|
854
|
+
if (awaitingChildren.length > 0) {
|
|
855
|
+
// Auto-continue children that hit turn limit (using --resume for context continuity)
|
|
856
|
+
console.log(`[Supervisor] ${awaitingChildren.length} children awaiting_input (turn limit). Auto-continuing.`);
|
|
857
|
+
for (const session of awaitingChildren) {
|
|
858
|
+
executionManager.continueSession(session.id, '턴 한도에 도달했습니다. 이전 작업을 이어서 계속 진행하세요.');
|
|
859
|
+
}
|
|
860
|
+
// Restart supervisor to watch the resumed children
|
|
861
|
+
state.crashCount = 0;
|
|
862
|
+
this.scheduleRestart(state, 5_000);
|
|
863
|
+
} else if (runningChildren.length > 0) {
|
|
798
864
|
// Principle 5: can't be done with running children → restart supervisor
|
|
799
865
|
console.log(`[Supervisor] Done but ${runningChildren.length} children still running. Restarting.`);
|
|
800
866
|
state.crashCount = 0; // Not a crash, intentional restart
|
|
801
867
|
this.scheduleRestart(state, 5_000); // 5s delay
|
|
802
868
|
} else if (state.continuous) {
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
state.
|
|
806
|
-
|
|
869
|
+
// BUG-CONTINUOUS-TURN1-STORM: Check if CEO actually did meaningful work.
|
|
870
|
+
// If turn 1 + 0 dispatches → "nothing to do" → stop loop instead of infinite restart.
|
|
871
|
+
const exec = state.executionId ? executionManager.getExecution(state.executionId) : undefined;
|
|
872
|
+
const turns = exec?.result?.turns ?? 0;
|
|
873
|
+
const dispatches = exec?.result?.dispatches?.length ?? 0;
|
|
874
|
+
|
|
875
|
+
if (turns <= 1 && dispatches === 0) {
|
|
876
|
+
console.log(`[Supervisor] Continuous mode: CEO finished in turn ${turns} with 0 dispatches. Stopping loop (nothing to do).`);
|
|
877
|
+
state.status = 'stopped';
|
|
878
|
+
// Don't restart — treat as normal wave completion (same as non-continuous done)
|
|
879
|
+
return;
|
|
880
|
+
} else {
|
|
881
|
+
// Continuous Improvement Mode: restart for next iteration
|
|
882
|
+
console.log(`[Supervisor] Wave ${state.waveId} iteration complete (${turns} turns, ${dispatches} dispatches). Continuous mode ON — restarting.`);
|
|
883
|
+
state.crashCount = 0;
|
|
884
|
+
this.scheduleRestart(state, 5_000);
|
|
885
|
+
return; // Don't fall through to completion
|
|
886
|
+
}
|
|
807
887
|
} else {
|
|
808
888
|
console.log(`[Supervisor] Wave ${state.waveId} complete. All subordinates done.`);
|
|
809
889
|
state.status = 'stopped';
|
|
@@ -300,14 +300,14 @@ class WaveMultiplexer {
|
|
|
300
300
|
getActiveWaves(): Array<{
|
|
301
301
|
id: string;
|
|
302
302
|
directive: string;
|
|
303
|
-
dispatches: Array<{ sessionId: string; roleId: string; roleName: string }>;
|
|
303
|
+
dispatches: Array<{ sessionId: string; roleId: string; roleName: string; status: string; approvalNeeded?: boolean; approvalQuestion?: string }>;
|
|
304
304
|
startedAt: number;
|
|
305
305
|
sessionIds: string[];
|
|
306
306
|
}> {
|
|
307
307
|
const result: Array<{
|
|
308
308
|
id: string;
|
|
309
309
|
directive: string;
|
|
310
|
-
dispatches: Array<{ sessionId: string; roleId: string; roleName: string }>;
|
|
310
|
+
dispatches: Array<{ sessionId: string; roleId: string; roleName: string; status: string; approvalNeeded?: boolean; approvalQuestion?: string }>;
|
|
311
311
|
startedAt: number;
|
|
312
312
|
sessionIds: string[];
|
|
313
313
|
}> = [];
|
|
@@ -316,13 +316,23 @@ class WaveMultiplexer {
|
|
|
316
316
|
const hasActive = Array.from(sessions.values()).some(e => e.status === 'running' || e.status === 'awaiting_input');
|
|
317
317
|
if (!hasActive) continue;
|
|
318
318
|
|
|
319
|
+
// Include ALL sessions (not just root) so plugin can show full team status
|
|
320
|
+
const APPROVAL_RE = /\[APPROVAL_NEEDED\]|\[CEO_DECISION\]|\[DECISION_REQUIRED\]/;
|
|
319
321
|
const rootSessions = Array.from(sessions.values())
|
|
320
|
-
.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
322
|
+
.map(e => {
|
|
323
|
+
const output = e.result?.output ?? '';
|
|
324
|
+
const hasApproval = APPROVAL_RE.test(output);
|
|
325
|
+
return {
|
|
326
|
+
sessionId: e.sessionId,
|
|
327
|
+
roleId: e.roleId,
|
|
328
|
+
roleName: e.roleId.toUpperCase(),
|
|
329
|
+
status: e.status,
|
|
330
|
+
...(hasApproval && {
|
|
331
|
+
approvalNeeded: true,
|
|
332
|
+
approvalQuestion: output.slice(output.search(APPROVAL_RE)).split('\n').slice(0, 5).join(' ').slice(0, 200),
|
|
333
|
+
}),
|
|
334
|
+
};
|
|
335
|
+
});
|
|
326
336
|
|
|
327
337
|
const firstExec = rootSessions.length > 0
|
|
328
338
|
? Array.from(sessions.values()).find(e => e.sessionId === rootSessions[0].sessionId)
|
|
@@ -313,6 +313,30 @@ export function saveCompletedWave(waveId: string, directive: string): { ok: bool
|
|
|
313
313
|
} catch { /* ignore */ }
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
// Collect dispatch statistics across all sessions
|
|
317
|
+
const dispatchStats = {
|
|
318
|
+
attempted: 0,
|
|
319
|
+
succeeded: 0,
|
|
320
|
+
failed: 0,
|
|
321
|
+
errors: [] as Array<{ sourceRole: string; targetRole: string; error: string }>,
|
|
322
|
+
};
|
|
323
|
+
for (const role of rolesData) {
|
|
324
|
+
for (const e of role.events) {
|
|
325
|
+
if (e.type === 'dispatch:start') {
|
|
326
|
+
dispatchStats.attempted++;
|
|
327
|
+
dispatchStats.succeeded++;
|
|
328
|
+
} else if (e.type === 'dispatch:error') {
|
|
329
|
+
dispatchStats.attempted++;
|
|
330
|
+
dispatchStats.failed++;
|
|
331
|
+
dispatchStats.errors.push({
|
|
332
|
+
sourceRole: (e.data.sourceRole as string) ?? 'unknown',
|
|
333
|
+
targetRole: (e.data.targetRole as string) ?? 'unknown',
|
|
334
|
+
error: (e.data.error as string) ?? 'unknown',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
316
340
|
const waveJson: Record<string, unknown> = {
|
|
317
341
|
id: baseName,
|
|
318
342
|
directive,
|
|
@@ -323,6 +347,7 @@ export function saveCompletedWave(waveId: string, directive: string): { ok: bool
|
|
|
323
347
|
sessionIds: allSessionIds,
|
|
324
348
|
};
|
|
325
349
|
if (existingPreset) waveJson.preset = existingPreset;
|
|
350
|
+
if (dispatchStats.attempted > 0) waveJson.dispatch = dispatchStats;
|
|
326
351
|
fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
|
|
327
352
|
|
|
328
353
|
const relativePath = `.tycono/waves/${baseName}.json`;
|