tycono 0.3.44 → 0.3.45-beta.1
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 +1 -1
- package/src/api/src/create-server.ts +1 -1
- package/src/api/src/engine/context-assembler.ts +4 -4
- package/src/api/src/services/claude-md-manager.ts +5 -4
- package/src/api/src/services/file-reader.ts +2 -2
- package/src/api/src/services/preset-loader.ts +2 -2
- package/src/api/src/services/scaffold.ts +12 -11
- package/src/api/src/services/team-recommender.ts +382 -0
- package/src/web/dist/assets/{index-C6r_vHBI.js → index-BJyiMGkM.js} +3 -3
- package/src/web/dist/assets/{index-Czp8wshq.js → index-DDPzbp9E.js} +1 -1
- package/src/web/dist/assets/{preview-app-CMGFfqT-.js → preview-app-DZ6WxhDc.js} +1 -1
- package/src/web/dist/index.html +1 -1
- package/src/web/dist/tyconoforge.js +1 -0
- package/templates/CLAUDE.md.tmpl +46 -45
package/package.json
CHANGED
|
@@ -94,7 +94,7 @@ function handleImportKnowledge(req: http.IncomingMessage, res: http.ServerRespon
|
|
|
94
94
|
|
|
95
95
|
export function createHttpServer(): http.Server {
|
|
96
96
|
// Only cleanup/ensure if a company is already initialized (avoid creating dirs in CWD)
|
|
97
|
-
if (COMPANY_ROOT && fs.existsSync(path.join(COMPANY_ROOT, 'CLAUDE.md'))) {
|
|
97
|
+
if (COMPANY_ROOT && fs.existsSync(path.join(COMPANY_ROOT, 'knowledge', 'CLAUDE.md'))) {
|
|
98
98
|
ensureClaudeMd(COMPANY_ROOT);
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -300,8 +300,8 @@ function loadCompanyRules(companyRoot: string): string | null {
|
|
|
300
300
|
parts.push(fs.readFileSync(claudeMdPath, 'utf-8'));
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
// 2. User custom rules (
|
|
304
|
-
const customPath = path.join(companyRoot, '
|
|
303
|
+
// 2. User custom rules (knowledge/custom-rules.md — user owned, git tracked)
|
|
304
|
+
const customPath = path.join(companyRoot, 'knowledge', 'custom-rules.md');
|
|
305
305
|
if (fs.existsSync(customPath)) {
|
|
306
306
|
const custom = fs.readFileSync(customPath, 'utf-8').trim();
|
|
307
307
|
if (custom) {
|
|
@@ -401,7 +401,7 @@ domain: tech|market|process|strategy|financial|competitor|general
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
function loadSkillMd(companyRoot: string, roleId: string): string | null {
|
|
404
|
-
const skillPath = path.join(companyRoot, '.claude', 'skills', roleId, 'SKILL.md');
|
|
404
|
+
const skillPath = path.join(companyRoot, 'knowledge', '.claude', 'skills', roleId, 'SKILL.md');
|
|
405
405
|
if (!fs.existsSync(skillPath)) return null;
|
|
406
406
|
return fs.readFileSync(skillPath, 'utf-8');
|
|
407
407
|
}
|
|
@@ -411,7 +411,7 @@ function loadSharedSkills(companyRoot: string, skillIds?: string[]): string | nu
|
|
|
411
411
|
|
|
412
412
|
const sections: string[] = [];
|
|
413
413
|
for (const skillId of skillIds) {
|
|
414
|
-
const skillPath = path.join(companyRoot, '.claude', 'skills', '_shared', skillId, 'SKILL.md');
|
|
414
|
+
const skillPath = path.join(companyRoot, 'knowledge', '.claude', 'skills', '_shared', skillId, 'SKILL.md');
|
|
415
415
|
if (!fs.existsSync(skillPath)) continue;
|
|
416
416
|
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
417
417
|
// Extract just the key sections (skip frontmatter)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Version tracking via .tycono/rules-version
|
|
6
6
|
* - Auto-regeneration on version mismatch (server startup)
|
|
7
7
|
* - Backup of pre-existing CLAUDE.md (first time only)
|
|
8
|
-
* - Stub creation for
|
|
8
|
+
* - Stub creation for knowledge/custom-rules.md
|
|
9
9
|
*/
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
@@ -42,13 +42,14 @@ function generateClaudeMd(version: string): string {
|
|
|
42
42
|
*
|
|
43
43
|
* Called on server startup. Compares .tycono/rules-version with package version.
|
|
44
44
|
* If different, regenerates CLAUDE.md from template (safe because CLAUDE.md
|
|
45
|
-
* contains 0% user data — all user customization is in
|
|
45
|
+
* contains 0% user data — all user customization is in knowledge/custom-rules.md).
|
|
46
46
|
*/
|
|
47
47
|
export function ensureClaudeMd(companyRoot: string): void {
|
|
48
48
|
const tyconoDir = path.join(companyRoot, '.tycono');
|
|
49
49
|
const rulesVersionPath = path.join(tyconoDir, 'rules-version');
|
|
50
|
-
const claudeMdPath = path.join(companyRoot, 'CLAUDE.md');
|
|
51
|
-
const
|
|
50
|
+
const claudeMdPath = path.join(companyRoot, 'knowledge', 'CLAUDE.md');
|
|
51
|
+
const knowledgeDir = path.join(companyRoot, 'knowledge');
|
|
52
|
+
const customRulesPath = path.join(knowledgeDir, 'custom-rules.md');
|
|
52
53
|
const backupPath = path.join(tyconoDir, 'CLAUDE.md.backup');
|
|
53
54
|
|
|
54
55
|
// Skip if not initialized (no .tycono/ directory)
|
|
@@ -5,10 +5,10 @@ import { glob } from 'glob';
|
|
|
5
5
|
|
|
6
6
|
function findCompanyRoot(): string {
|
|
7
7
|
if (process.env.COMPANY_ROOT) return process.env.COMPANY_ROOT;
|
|
8
|
-
// Walk up from cwd to find CLAUDE.md (
|
|
8
|
+
// Walk up from cwd to find knowledge/CLAUDE.md (project root marker)
|
|
9
9
|
let dir = process.cwd();
|
|
10
10
|
while (dir !== path.dirname(dir)) {
|
|
11
|
-
if (fs.existsSync(path.join(dir, 'CLAUDE.md'))) return dir;
|
|
11
|
+
if (fs.existsSync(path.join(dir, 'knowledge', 'CLAUDE.md'))) return dir;
|
|
12
12
|
dir = path.dirname(dir);
|
|
13
13
|
}
|
|
14
14
|
return process.cwd();
|
|
@@ -90,7 +90,7 @@ function loadPresetFromDir(presetDir: string): LoadedPreset | null {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Load all presets from
|
|
93
|
+
* Load all presets from knowledge/presets/ + auto-generated default.
|
|
94
94
|
* Returns [default, ...installed] — default is always first.
|
|
95
95
|
*/
|
|
96
96
|
export function loadPresets(companyRoot: string): LoadedPreset[] {
|
|
@@ -112,7 +112,7 @@ export function loadPresets(companyRoot: string): LoadedPreset[] {
|
|
|
112
112
|
|
|
113
113
|
presets.push(defaultPreset);
|
|
114
114
|
|
|
115
|
-
// 2. Installed presets from
|
|
115
|
+
// 2. Installed presets from knowledge/presets/{name}/preset.yaml
|
|
116
116
|
const presetsDir = path.join(companyRoot, PRESETS_DIR);
|
|
117
117
|
if (fs.existsSync(presetsDir)) {
|
|
118
118
|
const entries = fs.readdirSync(presetsDir, { withFileTypes: true });
|
|
@@ -262,13 +262,13 @@ function renderTemplate(template: string, vars: Record<string, string>): string
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
|
-
* Copy a skill from templates/skills/ to the target AKB's
|
|
265
|
+
* Copy a skill from templates/skills/ to the target AKB's knowledge/.claude/skills/_shared/
|
|
266
266
|
*/
|
|
267
267
|
function installSkill(root: string, skillId: string): boolean {
|
|
268
268
|
const srcDir = path.join(TEMPLATES_DIR, 'skills', skillId);
|
|
269
269
|
if (!fs.existsSync(srcDir)) return false;
|
|
270
270
|
|
|
271
|
-
const destDir = path.join(root, '.claude', 'skills', '_shared', skillId);
|
|
271
|
+
const destDir = path.join(root, 'knowledge', '.claude', 'skills', '_shared', skillId);
|
|
272
272
|
fs.mkdirSync(destDir, { recursive: true });
|
|
273
273
|
|
|
274
274
|
// Copy SKILL.md
|
|
@@ -317,32 +317,33 @@ export function scaffold(config: ScaffoldConfig): string[] {
|
|
|
317
317
|
'knowledge', 'knowledge/roles', 'knowledge/projects',
|
|
318
318
|
'knowledge/architecture', 'knowledge/methodologies',
|
|
319
319
|
'knowledge/decisions', 'knowledge/presets',
|
|
320
|
+
'knowledge/.claude/skills', 'knowledge/.claude/skills/_shared',
|
|
320
321
|
'.tycono/waves', '.tycono/sessions', '.tycono/standup',
|
|
321
322
|
'.tycono/activity-streams', '.tycono/cost', '.tycono/activity',
|
|
322
|
-
'.
|
|
323
|
+
'.tycono',
|
|
324
|
+
'apps',
|
|
323
325
|
];
|
|
324
326
|
for (const dir of dirs) {
|
|
325
327
|
fs.mkdirSync(path.join(root, dir), { recursive: true });
|
|
326
328
|
created.push(dir + '/');
|
|
327
329
|
}
|
|
328
330
|
|
|
329
|
-
// Write CLAUDE.md —
|
|
331
|
+
// Write CLAUDE.md — knowledge/ only (AI agent's cwd)
|
|
330
332
|
const claudeTmpl = loadTemplate('CLAUDE.md.tmpl');
|
|
331
333
|
const pkgVersion = getPackageVersion();
|
|
332
334
|
const claudeContent = claudeTmpl.replaceAll('{{VERSION}}', pkgVersion);
|
|
333
|
-
fs.writeFileSync(path.join(root, 'CLAUDE.md'), claudeContent);
|
|
334
335
|
fs.writeFileSync(path.join(root, 'knowledge', 'CLAUDE.md'), claudeContent);
|
|
335
|
-
created.push('
|
|
336
|
+
created.push('knowledge/CLAUDE.md');
|
|
336
337
|
|
|
337
338
|
// Write .tycono/rules-version
|
|
338
339
|
fs.writeFileSync(path.join(root, '.tycono', 'rules-version'), pkgVersion);
|
|
339
340
|
created.push('.tycono/rules-version');
|
|
340
341
|
|
|
341
|
-
// Write
|
|
342
|
-
const customRulesPath = path.join(root, '
|
|
342
|
+
// Write knowledge/custom-rules.md (empty stub — user owned, git tracked)
|
|
343
|
+
const customRulesPath = path.join(root, 'knowledge', 'custom-rules.md');
|
|
343
344
|
if (!fs.existsSync(customRulesPath)) {
|
|
344
345
|
fs.writeFileSync(customRulesPath, `# Custom Rules\n\n> Company-specific rules, constraints, and processes.\n> This file is owned by you — Tycono will never overwrite it.\n\n<!-- Add your custom rules below -->\n`);
|
|
345
|
-
created.push('
|
|
346
|
+
created.push('knowledge/custom-rules.md');
|
|
346
347
|
}
|
|
347
348
|
|
|
348
349
|
// Write knowledge/company.md
|
|
@@ -365,7 +366,7 @@ export function scaffold(config: ScaffoldConfig): string[] {
|
|
|
365
366
|
|
|
366
367
|
// Write .tycono/config.json (engine + API key + codeRoot)
|
|
367
368
|
const slug = config.companyName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'my-company';
|
|
368
|
-
const defaultCodeRoot = path.join(
|
|
369
|
+
const defaultCodeRoot = path.join(root, 'apps');
|
|
369
370
|
if (config.apiKey) {
|
|
370
371
|
const companyConfig: CompanyConfig = {
|
|
371
372
|
engine: 'direct-api',
|
|
@@ -484,7 +485,7 @@ export function scaffold(config: ScaffoldConfig): string[] {
|
|
|
484
485
|
|
|
485
486
|
function createRole(root: string, role: TeamRole): void {
|
|
486
487
|
const roleDir = path.join(root, 'knowledge', 'roles', role.id);
|
|
487
|
-
const skillDir = path.join(root, '.claude', 'skills', role.id);
|
|
488
|
+
const skillDir = path.join(root, 'knowledge', '.claude', 'skills', role.id);
|
|
488
489
|
const journalDir = path.join(roleDir, 'journal');
|
|
489
490
|
|
|
490
491
|
fs.mkdirSync(roleDir, { recursive: true });
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* team-recommender.ts — Recommend team composition for a Wave directive
|
|
3
|
+
*
|
|
4
|
+
* Analyzes the directive text + org tree to suggest optimal team size.
|
|
5
|
+
* Uses Haiku for AI classification, falls back to keyword heuristic.
|
|
6
|
+
*
|
|
7
|
+
* Returns ranked options: Quick / Standard / Full + custom saved teams.
|
|
8
|
+
*/
|
|
9
|
+
import { buildOrgTree, getSubordinates, getDescendants, type OrgTree, type OrgNode } from '../engine/org-tree.js';
|
|
10
|
+
import { readConfig } from './company-config.js';
|
|
11
|
+
import { COMPANY_ROOT } from './file-reader.js';
|
|
12
|
+
import { ClaudeCliProvider } from '../engine/llm-adapter.js';
|
|
13
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
/* ─── Types ──────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
export interface TeamOption {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
roles: string[]; // role IDs to include as targetRoles
|
|
24
|
+
roleDetails: { id: string; name: string; level: string; subordinates: string[] }[];
|
|
25
|
+
totalAgents: number; // total agents including subordinates
|
|
26
|
+
estimatedSpeed: 'fast' | 'medium' | 'slow';
|
|
27
|
+
recommended?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TeamRecommendation {
|
|
31
|
+
directive: string;
|
|
32
|
+
analysis: {
|
|
33
|
+
domains: string[]; // detected domains: code, design, planning, testing, business
|
|
34
|
+
complexity: 'simple' | 'moderate' | 'complex';
|
|
35
|
+
reasoning: string;
|
|
36
|
+
};
|
|
37
|
+
options: TeamOption[];
|
|
38
|
+
customTeams: SavedTeam[];
|
|
39
|
+
recommendedId: string; // ID of the recommended option
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SavedTeam {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
roles: string[];
|
|
46
|
+
createdAt: string;
|
|
47
|
+
usageCount: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TeamPresetsFile {
|
|
51
|
+
teams: SavedTeam[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ─── Constants ──────────────────────────────── */
|
|
55
|
+
|
|
56
|
+
const TEAM_PRESETS_PATH = () => path.join(COMPANY_ROOT, '.tycono', 'team-presets.json');
|
|
57
|
+
|
|
58
|
+
const CLASSIFY_SYSTEM = `You analyze a CEO directive and determine what domains of work it requires.
|
|
59
|
+
|
|
60
|
+
Reply with a JSON object (no markdown, no explanation):
|
|
61
|
+
{
|
|
62
|
+
"domains": ["code", "design"],
|
|
63
|
+
"complexity": "moderate",
|
|
64
|
+
"reasoning": "Needs implementation + visual design but scope is clear"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Domain options: "code", "design", "planning", "testing", "business", "research", "writing"
|
|
68
|
+
Complexity options: "simple" (one person can do it), "moderate" (2-3 roles needed), "complex" (full team coordination)
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
"버그 고쳐" → {"domains":["code"],"complexity":"simple","reasoning":"Single code fix, engineer only"}
|
|
72
|
+
"랜딩페이지 만들어" → {"domains":["code","design"],"complexity":"moderate","reasoning":"Frontend + design needed"}
|
|
73
|
+
"타워 디펜스 게임 만들어" → {"domains":["code","design","planning"],"complexity":"moderate","reasoning":"Game needs balance design + visual + code"}
|
|
74
|
+
"Q2 사업 전략 수립하고 신규 기능 개발해" → {"domains":["code","design","planning","business"],"complexity":"complex","reasoning":"Cross-domain: business strategy + product development"}
|
|
75
|
+
"README 업데이트해" → {"domains":["writing"],"complexity":"simple","reasoning":"Documentation only, single agent task"}`;
|
|
76
|
+
|
|
77
|
+
/* ─── Domain → Role Mapping ──────────────────── */
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Map detected domains to required role capabilities.
|
|
81
|
+
* This uses the org tree to find which roles handle which domains.
|
|
82
|
+
*/
|
|
83
|
+
function mapDomainsToRoles(domains: string[], orgTree: OrgTree): Set<string> {
|
|
84
|
+
const needed = new Set<string>();
|
|
85
|
+
|
|
86
|
+
// Domain → role mapping based on typical role responsibilities
|
|
87
|
+
const DOMAIN_ROLES: Record<string, string[]> = {
|
|
88
|
+
code: ['engineer', 'cto'],
|
|
89
|
+
design: ['designer'],
|
|
90
|
+
planning: ['pm'],
|
|
91
|
+
testing: ['qa'],
|
|
92
|
+
business: ['cbo', 'data-analyst'],
|
|
93
|
+
research: ['cbo', 'data-analyst'],
|
|
94
|
+
writing: ['engineer'], // simple writing = engineer can handle
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
for (const domain of domains) {
|
|
98
|
+
const candidates = DOMAIN_ROLES[domain] ?? [];
|
|
99
|
+
for (const roleId of candidates) {
|
|
100
|
+
if (orgTree.nodes.has(roleId)) {
|
|
101
|
+
needed.add(roleId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return needed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build team options from org tree based on analysis.
|
|
111
|
+
*/
|
|
112
|
+
function buildOptions(
|
|
113
|
+
orgTree: OrgTree,
|
|
114
|
+
neededRoles: Set<string>,
|
|
115
|
+
complexity: 'simple' | 'moderate' | 'complex',
|
|
116
|
+
): { options: TeamOption[]; recommendedId: string } {
|
|
117
|
+
const ceo = orgTree.nodes.get('ceo')!;
|
|
118
|
+
const cLevelIds = ceo.children;
|
|
119
|
+
const options: TeamOption[] = [];
|
|
120
|
+
|
|
121
|
+
// Helper: build role details for an option
|
|
122
|
+
const buildRoleDetails = (roleIds: string[]) => {
|
|
123
|
+
return roleIds.map(id => {
|
|
124
|
+
const node = orgTree.nodes.get(id);
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
name: node?.name ?? id,
|
|
128
|
+
level: node?.level ?? 'member',
|
|
129
|
+
subordinates: getSubordinates(orgTree, id),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const totalAgents = (roleIds: string[]) => {
|
|
135
|
+
let count = 0;
|
|
136
|
+
for (const id of roleIds) {
|
|
137
|
+
count += 1 + getDescendants(orgTree, id).length;
|
|
138
|
+
}
|
|
139
|
+
return count;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ── Quick: minimum viable team (just the most needed C-Level) ──
|
|
143
|
+
// Find the C-Level that covers the most needed roles
|
|
144
|
+
let quickCLevel: string | null = null;
|
|
145
|
+
let maxCoverage = 0;
|
|
146
|
+
|
|
147
|
+
for (const cId of cLevelIds) {
|
|
148
|
+
const descendants = new Set([cId, ...getDescendants(orgTree, cId)]);
|
|
149
|
+
const coverage = [...neededRoles].filter(r => descendants.has(r)).length;
|
|
150
|
+
if (coverage > maxCoverage) {
|
|
151
|
+
maxCoverage = coverage;
|
|
152
|
+
quickCLevel = cId;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (quickCLevel) {
|
|
157
|
+
const quickRoles = [quickCLevel];
|
|
158
|
+
options.push({
|
|
159
|
+
id: 'quick',
|
|
160
|
+
name: '⚡ Quick',
|
|
161
|
+
description: `${orgTree.nodes.get(quickCLevel)?.name ?? quickCLevel} 팀만 투입. 빠르고 저비용.`,
|
|
162
|
+
roles: quickRoles,
|
|
163
|
+
roleDetails: buildRoleDetails(quickRoles),
|
|
164
|
+
totalAgents: totalAgents(quickRoles),
|
|
165
|
+
estimatedSpeed: 'fast',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Standard: C-Levels that cover all needed roles ──
|
|
170
|
+
const standardCLevels: string[] = [];
|
|
171
|
+
const coveredRoles = new Set<string>();
|
|
172
|
+
|
|
173
|
+
for (const cId of cLevelIds) {
|
|
174
|
+
const descendants = new Set([cId, ...getDescendants(orgTree, cId)]);
|
|
175
|
+
const covers = [...neededRoles].filter(r => descendants.has(r) && !coveredRoles.has(r));
|
|
176
|
+
if (covers.length > 0) {
|
|
177
|
+
standardCLevels.push(cId);
|
|
178
|
+
covers.forEach(r => coveredRoles.add(r));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (standardCLevels.length > 0 && standardCLevels.length < cLevelIds.length) {
|
|
183
|
+
options.push({
|
|
184
|
+
id: 'standard',
|
|
185
|
+
name: '⭐ Standard',
|
|
186
|
+
description: `필요 역할만 투입. ${standardCLevels.map(id => orgTree.nodes.get(id)?.name ?? id).join(' + ')}.`,
|
|
187
|
+
roles: standardCLevels,
|
|
188
|
+
roleDetails: buildRoleDetails(standardCLevels),
|
|
189
|
+
totalAgents: totalAgents(standardCLevels),
|
|
190
|
+
estimatedSpeed: 'medium',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Full: all C-Levels ──
|
|
195
|
+
options.push({
|
|
196
|
+
id: 'full',
|
|
197
|
+
name: '🔥 Full Team',
|
|
198
|
+
description: '전체 팀 투입. 느리지만 가장 정교한 결과.',
|
|
199
|
+
roles: cLevelIds,
|
|
200
|
+
roleDetails: buildRoleDetails(cLevelIds),
|
|
201
|
+
totalAgents: totalAgents(cLevelIds),
|
|
202
|
+
estimatedSpeed: 'slow',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Determine recommendation
|
|
206
|
+
let recommendedId: string;
|
|
207
|
+
if (complexity === 'simple' && options.find(o => o.id === 'quick')) {
|
|
208
|
+
recommendedId = 'quick';
|
|
209
|
+
} else if (complexity === 'complex') {
|
|
210
|
+
recommendedId = 'full';
|
|
211
|
+
} else {
|
|
212
|
+
recommendedId = options.find(o => o.id === 'standard')?.id ?? options[0].id;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Mark recommended
|
|
216
|
+
for (const opt of options) {
|
|
217
|
+
opt.recommended = opt.id === recommendedId;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { options, recommendedId };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ─── AI Classification ──────────────────────── */
|
|
224
|
+
|
|
225
|
+
interface AnalysisResult {
|
|
226
|
+
domains: string[];
|
|
227
|
+
complexity: 'simple' | 'moderate' | 'complex';
|
|
228
|
+
reasoning: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function classifyDirective(text: string): Promise<AnalysisResult> {
|
|
232
|
+
try {
|
|
233
|
+
const config = readConfig(COMPANY_ROOT);
|
|
234
|
+
const engine = config.engine || process.env.EXECUTION_ENGINE || 'claude-cli';
|
|
235
|
+
|
|
236
|
+
let reply: string;
|
|
237
|
+
if (engine === 'claude-cli') {
|
|
238
|
+
const provider = new ClaudeCliProvider({ model: 'claude-haiku-4-5-20251001' });
|
|
239
|
+
const response = await provider.chat(
|
|
240
|
+
CLASSIFY_SYSTEM,
|
|
241
|
+
[{ role: 'user', content: text }],
|
|
242
|
+
);
|
|
243
|
+
reply = response.content.find(c => c.type === 'text')?.text?.trim() ?? '';
|
|
244
|
+
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
245
|
+
const client = new Anthropic();
|
|
246
|
+
const response = await client.messages.create({
|
|
247
|
+
model: 'claude-haiku-4-5-20251001',
|
|
248
|
+
max_tokens: 200,
|
|
249
|
+
system: CLASSIFY_SYSTEM,
|
|
250
|
+
messages: [{ role: 'user', content: text }],
|
|
251
|
+
});
|
|
252
|
+
reply = (response.content[0] as { type: 'text'; text: string }).text.trim();
|
|
253
|
+
} else {
|
|
254
|
+
return classifyDirectiveFallback(text);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Parse JSON response
|
|
258
|
+
const parsed = JSON.parse(reply) as AnalysisResult;
|
|
259
|
+
if (Array.isArray(parsed.domains) && parsed.complexity && parsed.reasoning) {
|
|
260
|
+
return parsed;
|
|
261
|
+
}
|
|
262
|
+
return classifyDirectiveFallback(text);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.warn('[TeamRecommender] AI classification failed, using fallback:', err);
|
|
265
|
+
return classifyDirectiveFallback(text);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function classifyDirectiveFallback(text: string): AnalysisResult {
|
|
270
|
+
const t = text.toLowerCase();
|
|
271
|
+
const domains: string[] = [];
|
|
272
|
+
|
|
273
|
+
// Keyword-based domain detection
|
|
274
|
+
if (/코드|구현|개발|빌드|build|implement|develop|fix|bug|api|서버|deploy|refactor/.test(t)) domains.push('code');
|
|
275
|
+
if (/디자인|design|ui|ux|css|스타일|비주얼|visual|레이아웃/.test(t)) domains.push('design');
|
|
276
|
+
if (/기획|plan|prd|스펙|spec|기능\s*정의|요구사항|밸런/.test(t)) domains.push('planning');
|
|
277
|
+
if (/테스트|test|qa|검증|verify|플레이테스트/.test(t)) domains.push('testing');
|
|
278
|
+
if (/사업|business|매출|revenue|시장|market|경쟁|전략|strategy|pricing/.test(t)) domains.push('business');
|
|
279
|
+
if (/조사|research|분석|analy|리서치/.test(t)) domains.push('research');
|
|
280
|
+
if (/문서|doc|readme|작성|write|블로그|blog/.test(t)) domains.push('writing');
|
|
281
|
+
|
|
282
|
+
// Fallback: if no domain detected, assume code
|
|
283
|
+
if (domains.length === 0) domains.push('code');
|
|
284
|
+
|
|
285
|
+
// Complexity based on domain count and text length
|
|
286
|
+
let complexity: 'simple' | 'moderate' | 'complex';
|
|
287
|
+
if (domains.length <= 1 && t.length < 50) {
|
|
288
|
+
complexity = 'simple';
|
|
289
|
+
} else if (domains.length >= 3 || t.length > 200) {
|
|
290
|
+
complexity = 'complex';
|
|
291
|
+
} else {
|
|
292
|
+
complexity = 'moderate';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
domains,
|
|
297
|
+
complexity,
|
|
298
|
+
reasoning: `Detected ${domains.length} domain(s) via keyword matching: ${domains.join(', ')}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ─── Custom Teams CRUD ──────────────────────── */
|
|
303
|
+
|
|
304
|
+
function readTeamPresets(): TeamPresetsFile {
|
|
305
|
+
const filePath = TEAM_PRESETS_PATH();
|
|
306
|
+
if (!fs.existsSync(filePath)) return { teams: [] };
|
|
307
|
+
try {
|
|
308
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as TeamPresetsFile;
|
|
309
|
+
} catch {
|
|
310
|
+
return { teams: [] };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function writeTeamPresets(data: TeamPresetsFile): void {
|
|
315
|
+
const filePath = TEAM_PRESETS_PATH();
|
|
316
|
+
const dir = path.dirname(filePath);
|
|
317
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
318
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function getSavedTeams(): SavedTeam[] {
|
|
322
|
+
return readTeamPresets().teams;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function saveCustomTeam(name: string, roles: string[]): SavedTeam {
|
|
326
|
+
const data = readTeamPresets();
|
|
327
|
+
const team: SavedTeam = {
|
|
328
|
+
id: `custom-${Date.now()}`,
|
|
329
|
+
name,
|
|
330
|
+
roles,
|
|
331
|
+
createdAt: new Date().toISOString(),
|
|
332
|
+
usageCount: 0,
|
|
333
|
+
};
|
|
334
|
+
data.teams.push(team);
|
|
335
|
+
writeTeamPresets(data);
|
|
336
|
+
return team;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function deleteCustomTeam(teamId: string): boolean {
|
|
340
|
+
const data = readTeamPresets();
|
|
341
|
+
const idx = data.teams.findIndex(t => t.id === teamId);
|
|
342
|
+
if (idx === -1) return false;
|
|
343
|
+
data.teams.splice(idx, 1);
|
|
344
|
+
writeTeamPresets(data);
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function incrementTeamUsage(teamId: string): void {
|
|
349
|
+
const data = readTeamPresets();
|
|
350
|
+
const team = data.teams.find(t => t.id === teamId);
|
|
351
|
+
if (team) {
|
|
352
|
+
team.usageCount += 1;
|
|
353
|
+
writeTeamPresets(data);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ─── Main: Recommend ────────────────────────── */
|
|
358
|
+
|
|
359
|
+
export async function recommendTeam(directive: string): Promise<TeamRecommendation> {
|
|
360
|
+
// 1. Build org tree
|
|
361
|
+
const orgTree = buildOrgTree(COMPANY_ROOT);
|
|
362
|
+
|
|
363
|
+
// 2. Classify directive
|
|
364
|
+
const analysis = await classifyDirective(directive);
|
|
365
|
+
|
|
366
|
+
// 3. Map domains to needed roles
|
|
367
|
+
const neededRoles = mapDomainsToRoles(analysis.domains, orgTree);
|
|
368
|
+
|
|
369
|
+
// 4. Build team options
|
|
370
|
+
const { options, recommendedId } = buildOptions(orgTree, neededRoles, analysis.complexity);
|
|
371
|
+
|
|
372
|
+
// 5. Load custom teams
|
|
373
|
+
const customTeams = getSavedTeams();
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
directive,
|
|
377
|
+
analysis,
|
|
378
|
+
options,
|
|
379
|
+
customTeams,
|
|
380
|
+
recommendedId,
|
|
381
|
+
};
|
|
382
|
+
}
|