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.
Files changed (113) hide show
  1. package/README.md +191 -162
  2. package/bin/tycono.ts +42 -10
  3. package/package.json +21 -15
  4. package/packages/server/bin/cli.js +35 -0
  5. package/packages/server/bin/server.ts +183 -0
  6. package/{src → packages/server/src}/api/src/create-server.ts +11 -3
  7. package/{src → packages/server/src}/api/src/engine/agent-loop.ts +30 -7
  8. package/{src → packages/server/src}/api/src/engine/context-assembler.ts +122 -57
  9. package/{src → packages/server/src}/api/src/engine/llm-adapter.ts +10 -7
  10. package/{src → packages/server/src}/api/src/engine/org-tree.ts +43 -3
  11. package/{src → packages/server/src}/api/src/engine/runners/claude-cli.ts +37 -15
  12. package/{src → packages/server/src}/api/src/engine/runners/types.ts +6 -0
  13. package/{src → packages/server/src}/api/src/engine/tools/executor.ts +65 -9
  14. package/{src → packages/server/src}/api/src/routes/execute.ts +221 -17
  15. package/packages/server/src/api/src/services/claude-md-manager.ts +190 -0
  16. package/{src → packages/server/src}/api/src/services/company-config.ts +1 -0
  17. package/{src → packages/server/src}/api/src/services/digest-engine.ts +4 -1
  18. package/packages/server/src/api/src/services/dispatch-classifier.ts +179 -0
  19. package/{src → packages/server/src}/api/src/services/execution-manager.ts +227 -21
  20. package/{src → packages/server/src}/api/src/services/file-reader.ts +4 -1
  21. package/packages/server/src/api/src/services/preset-loader.ts +310 -0
  22. package/{src → packages/server/src}/api/src/services/supervisor-heartbeat.ts +89 -9
  23. package/{src → packages/server/src}/api/src/services/wave-multiplexer.ts +18 -8
  24. package/{src → packages/server/src}/api/src/services/wave-tracker.ts +25 -0
  25. package/packages/server/src/core/scaffolder.ts +620 -0
  26. package/{src → packages/server/src}/shared/types.ts +3 -1
  27. package/packages/server/templates/CLAUDE.md.tmpl +152 -0
  28. package/packages/server/templates/agentic-knowledge-base.md +355 -0
  29. package/src/api/src/services/claude-md-manager.ts +0 -94
  30. package/src/api/src/services/preset-loader.ts +0 -149
  31. package/templates/CLAUDE.md.tmpl +0 -239
  32. /package/{src/web → packages/pixel}/dist/assets/index-BJyiMGkM.js +0 -0
  33. /package/{src/web → packages/pixel}/dist/assets/index-BOuHc64o.css +0 -0
  34. /package/{src/web → packages/pixel}/dist/assets/index-DDPzbp9E.js +0 -0
  35. /package/{src/web → packages/pixel}/dist/assets/index-DVKWFwwK.css +0 -0
  36. /package/{src/web → packages/pixel}/dist/assets/preview-app-DZ6WxhDc.js +0 -0
  37. /package/{src/web → packages/pixel}/dist/index.html +0 -0
  38. /package/{src/web → packages/pixel}/dist/tyconoforge.js +0 -0
  39. /package/{src → packages/server/src}/api/package.json +0 -0
  40. /package/{src → packages/server/src}/api/src/create-app.ts +0 -0
  41. /package/{src → packages/server/src}/api/src/engine/authority-validator.ts +0 -0
  42. /package/{src → packages/server/src}/api/src/engine/index.ts +0 -0
  43. /package/{src → packages/server/src}/api/src/engine/knowledge-gate.ts +0 -0
  44. /package/{src → packages/server/src}/api/src/engine/role-lifecycle.ts +0 -0
  45. /package/{src → packages/server/src}/api/src/engine/runners/direct-api.ts +0 -0
  46. /package/{src → packages/server/src}/api/src/engine/runners/index.ts +0 -0
  47. /package/{src → packages/server/src}/api/src/engine/skill-template.ts +0 -0
  48. /package/{src → packages/server/src}/api/src/engine/tools/definitions.ts +0 -0
  49. /package/{src → packages/server/src}/api/src/routes/active-sessions.ts +0 -0
  50. /package/{src → packages/server/src}/api/src/routes/coins.ts +0 -0
  51. /package/{src → packages/server/src}/api/src/routes/company.ts +0 -0
  52. /package/{src → packages/server/src}/api/src/routes/cost.ts +0 -0
  53. /package/{src → packages/server/src}/api/src/routes/engine.ts +0 -0
  54. /package/{src → packages/server/src}/api/src/routes/git.ts +0 -0
  55. /package/{src → packages/server/src}/api/src/routes/knowledge.ts +0 -0
  56. /package/{src → packages/server/src}/api/src/routes/operations.ts +0 -0
  57. /package/{src → packages/server/src}/api/src/routes/preferences.ts +0 -0
  58. /package/{src → packages/server/src}/api/src/routes/presets.ts +0 -0
  59. /package/{src → packages/server/src}/api/src/routes/projects.ts +0 -0
  60. /package/{src → packages/server/src}/api/src/routes/quests.ts +0 -0
  61. /package/{src → packages/server/src}/api/src/routes/roles.ts +0 -0
  62. /package/{src → packages/server/src}/api/src/routes/save.ts +0 -0
  63. /package/{src → packages/server/src}/api/src/routes/sessions.ts +0 -0
  64. /package/{src → packages/server/src}/api/src/routes/setup.ts +0 -0
  65. /package/{src → packages/server/src}/api/src/routes/skills.ts +0 -0
  66. /package/{src → packages/server/src}/api/src/routes/speech.ts +0 -0
  67. /package/{src → packages/server/src}/api/src/routes/supervision.ts +0 -0
  68. /package/{src → packages/server/src}/api/src/routes/sync.ts +0 -0
  69. /package/{src → packages/server/src}/api/src/server.ts +0 -0
  70. /package/{src → packages/server/src}/api/src/services/activity-stream.ts +0 -0
  71. /package/{src → packages/server/src}/api/src/services/activity-tracker.ts +0 -0
  72. /package/{src → packages/server/src}/api/src/services/database.ts +0 -0
  73. /package/{src → packages/server/src}/api/src/services/git-save.ts +0 -0
  74. /package/{src → packages/server/src}/api/src/services/job-manager.ts +0 -0
  75. /package/{src → packages/server/src}/api/src/services/knowledge-importer.ts +0 -0
  76. /package/{src → packages/server/src}/api/src/services/markdown-parser.ts +0 -0
  77. /package/{src → packages/server/src}/api/src/services/port-registry.ts +0 -0
  78. /package/{src → packages/server/src}/api/src/services/preferences.ts +0 -0
  79. /package/{src → packages/server/src}/api/src/services/pricing.ts +0 -0
  80. /package/{src → packages/server/src}/api/src/services/scaffold.ts +0 -0
  81. /package/{src → packages/server/src}/api/src/services/session-store.ts +0 -0
  82. /package/{src → packages/server/src}/api/src/services/team-recommender.ts +0 -0
  83. /package/{src → packages/server/src}/api/src/services/token-ledger.ts +0 -0
  84. /package/{src → packages/server/src}/api/src/services/wave-messages.ts +0 -0
  85. /package/{src → packages/server/src}/api/src/utils/role-level.ts +0 -0
  86. /package/{templates → packages/server/templates}/company.md.tmpl +0 -0
  87. /package/{templates → packages/server/templates}/gitignore.tmpl +0 -0
  88. /package/{templates → packages/server/templates}/roles.md.tmpl +0 -0
  89. /package/{templates → packages/server/templates}/skills/_manifest.json +0 -0
  90. /package/{templates → packages/server/templates}/skills/agent-browser/SKILL.md +0 -0
  91. /package/{templates → packages/server/templates}/skills/agent-browser/meta.json +0 -0
  92. /package/{templates → packages/server/templates}/skills/akb-linter/SKILL.md +0 -0
  93. /package/{templates → packages/server/templates}/skills/akb-linter/meta.json +0 -0
  94. /package/{templates → packages/server/templates}/skills/knowledge-gate/SKILL.md +0 -0
  95. /package/{templates → packages/server/templates}/skills/knowledge-gate/meta.json +0 -0
  96. /package/{templates → packages/server/templates}/teams/agency.json +0 -0
  97. /package/{templates → packages/server/templates}/teams/research.json +0 -0
  98. /package/{templates → packages/server/templates}/teams/startup.json +0 -0
  99. /package/{src/tui → packages/tui/src}/api.ts +0 -0
  100. /package/{src/tui → packages/tui/src}/app.tsx +0 -0
  101. /package/{src/tui → packages/tui/src}/components/CommandMode.tsx +0 -0
  102. /package/{src/tui → packages/tui/src}/components/OrgTree.tsx +0 -0
  103. /package/{src/tui → packages/tui/src}/components/PanelMode.tsx +0 -0
  104. /package/{src/tui → packages/tui/src}/components/SetupWizard.tsx +0 -0
  105. /package/{src/tui → packages/tui/src}/components/StatusBar.tsx +0 -0
  106. /package/{src/tui → packages/tui/src}/components/StreamView.tsx +0 -0
  107. /package/{src/tui → packages/tui/src}/hooks/useApi.ts +0 -0
  108. /package/{src/tui → packages/tui/src}/hooks/useCommand.ts +0 -0
  109. /package/{src/tui → packages/tui/src}/hooks/useSSE.ts +0 -0
  110. /package/{src/tui → packages/tui/src}/index.tsx +0 -0
  111. /package/{src/tui → packages/tui/src}/store.ts +0 -0
  112. /package/{src/tui → packages/tui/src}/theme.ts +0 -0
  113. /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 (agent finished or idle wave), wake it up
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
- // Check if there are still running C-Level sessions for this wave
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 (runningChildren.length > 0) {
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
- // Continuous Improvement Mode: don't stop restart supervisor to ask C-Levels for improvements
804
- console.log(`[Supervisor] Wave ${state.waveId} iteration complete. Continuous mode ON restarting for next improvement cycle.`);
805
- state.crashCount = 0;
806
- this.scheduleRestart(state, 5_000);
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
- .filter(e => !e.parentSessionId || !sessions.has(e.parentSessionId))
321
- .map(e => ({
322
- sessionId: e.sessionId,
323
- roleId: e.roleId,
324
- roleName: e.roleId.toUpperCase(),
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`;