tycono-server 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/bin/cli.js +35 -0
  2. package/bin/server.ts +160 -0
  3. package/package.json +50 -0
  4. package/src/api/package.json +31 -0
  5. package/src/api/src/create-app.ts +90 -0
  6. package/src/api/src/create-server.ts +251 -0
  7. package/src/api/src/engine/agent-loop.ts +738 -0
  8. package/src/api/src/engine/authority-validator.ts +149 -0
  9. package/src/api/src/engine/context-assembler.ts +912 -0
  10. package/src/api/src/engine/index.ts +27 -0
  11. package/src/api/src/engine/knowledge-gate.ts +365 -0
  12. package/src/api/src/engine/llm-adapter.ts +304 -0
  13. package/src/api/src/engine/org-tree.ts +270 -0
  14. package/src/api/src/engine/role-lifecycle.ts +369 -0
  15. package/src/api/src/engine/runners/claude-cli.ts +796 -0
  16. package/src/api/src/engine/runners/direct-api.ts +66 -0
  17. package/src/api/src/engine/runners/index.ts +30 -0
  18. package/src/api/src/engine/runners/types.ts +95 -0
  19. package/src/api/src/engine/skill-template.ts +134 -0
  20. package/src/api/src/engine/tools/definitions.ts +201 -0
  21. package/src/api/src/engine/tools/executor.ts +611 -0
  22. package/src/api/src/routes/active-sessions.ts +134 -0
  23. package/src/api/src/routes/coins.ts +153 -0
  24. package/src/api/src/routes/company.ts +57 -0
  25. package/src/api/src/routes/cost.ts +141 -0
  26. package/src/api/src/routes/engine.ts +220 -0
  27. package/src/api/src/routes/execute.ts +1075 -0
  28. package/src/api/src/routes/git.ts +211 -0
  29. package/src/api/src/routes/knowledge.ts +378 -0
  30. package/src/api/src/routes/operations.ts +309 -0
  31. package/src/api/src/routes/preferences.ts +63 -0
  32. package/src/api/src/routes/presets.ts +123 -0
  33. package/src/api/src/routes/projects.ts +82 -0
  34. package/src/api/src/routes/quests.ts +41 -0
  35. package/src/api/src/routes/roles.ts +112 -0
  36. package/src/api/src/routes/save.ts +152 -0
  37. package/src/api/src/routes/sessions.ts +288 -0
  38. package/src/api/src/routes/setup.ts +437 -0
  39. package/src/api/src/routes/skills.ts +357 -0
  40. package/src/api/src/routes/speech.ts +959 -0
  41. package/src/api/src/routes/supervision.ts +136 -0
  42. package/src/api/src/routes/sync.ts +165 -0
  43. package/src/api/src/server.ts +59 -0
  44. package/src/api/src/services/activity-stream.ts +184 -0
  45. package/src/api/src/services/activity-tracker.ts +115 -0
  46. package/src/api/src/services/claude-md-manager.ts +94 -0
  47. package/src/api/src/services/company-config.ts +115 -0
  48. package/src/api/src/services/database.ts +77 -0
  49. package/src/api/src/services/digest-engine.ts +313 -0
  50. package/src/api/src/services/execution-manager.ts +1036 -0
  51. package/src/api/src/services/file-reader.ts +77 -0
  52. package/src/api/src/services/git-save.ts +614 -0
  53. package/src/api/src/services/job-manager.ts +16 -0
  54. package/src/api/src/services/knowledge-importer.ts +466 -0
  55. package/src/api/src/services/markdown-parser.ts +173 -0
  56. package/src/api/src/services/port-registry.ts +222 -0
  57. package/src/api/src/services/preferences.ts +150 -0
  58. package/src/api/src/services/preset-loader.ts +149 -0
  59. package/src/api/src/services/pricing.ts +34 -0
  60. package/src/api/src/services/scaffold.ts +546 -0
  61. package/src/api/src/services/session-store.ts +340 -0
  62. package/src/api/src/services/supervisor-heartbeat.ts +897 -0
  63. package/src/api/src/services/team-recommender.ts +382 -0
  64. package/src/api/src/services/token-ledger.ts +127 -0
  65. package/src/api/src/services/wave-messages.ts +194 -0
  66. package/src/api/src/services/wave-multiplexer.ts +356 -0
  67. package/src/api/src/services/wave-tracker.ts +359 -0
  68. package/src/api/src/utils/role-level.ts +31 -0
  69. package/src/core/scaffolder.ts +620 -0
  70. package/src/shared/types.ts +224 -0
  71. package/templates/CLAUDE.md.tmpl +239 -0
  72. package/templates/company.md.tmpl +17 -0
  73. package/templates/gitignore.tmpl +28 -0
  74. package/templates/roles.md.tmpl +8 -0
  75. package/templates/skills/_manifest.json +23 -0
  76. package/templates/skills/agent-browser/SKILL.md +159 -0
  77. package/templates/skills/agent-browser/meta.json +19 -0
  78. package/templates/skills/akb-linter/SKILL.md +125 -0
  79. package/templates/skills/akb-linter/meta.json +12 -0
  80. package/templates/skills/knowledge-gate/SKILL.md +120 -0
  81. package/templates/skills/knowledge-gate/meta.json +12 -0
  82. package/templates/teams/agency.json +58 -0
  83. package/templates/teams/research.json +58 -0
  84. package/templates/teams/startup.json +58 -0
@@ -0,0 +1,270 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+
5
+ /* ─── Types ──────────────────────────────────── */
6
+
7
+ export interface Authority {
8
+ autonomous: string[];
9
+ needsApproval: string[];
10
+ }
11
+
12
+ export interface KnowledgeAccess {
13
+ reads: string[];
14
+ writes: string[];
15
+ }
16
+
17
+ export interface RoleSource {
18
+ id: string;
19
+ sync: 'auto' | 'manual' | 'off';
20
+ forked_at?: string;
21
+ upstream_version?: string;
22
+ }
23
+
24
+ export interface HeartbeatConfig {
25
+ enabled: boolean;
26
+ intervalSec: number; // default 120
27
+ maxTicks: number; // default 60
28
+ }
29
+
30
+ export interface OrgNode {
31
+ id: string;
32
+ name: string;
33
+ level: 'c-level' | 'member';
34
+ reportsTo: string;
35
+ children: string[];
36
+ persona: string;
37
+ authority: Authority;
38
+ knowledge: KnowledgeAccess;
39
+ reports: { daily: string; weekly: string };
40
+ skills?: string[];
41
+ model?: string;
42
+ source?: RoleSource;
43
+ heartbeat?: HeartbeatConfig;
44
+ }
45
+
46
+ export interface OrgTree {
47
+ root: string;
48
+ nodes: Map<string, OrgNode>;
49
+ }
50
+
51
+ /* ─── Raw YAML shape ─────────────────────────── */
52
+
53
+ interface RawRoleYaml {
54
+ id: string;
55
+ name: string;
56
+ level: string;
57
+ reports_to: string;
58
+ persona: string;
59
+ authority?: {
60
+ autonomous?: string[];
61
+ needs_approval?: string[];
62
+ };
63
+ knowledge?: {
64
+ reads?: string[];
65
+ writes?: string[];
66
+ };
67
+ reports?: {
68
+ daily?: string;
69
+ weekly?: string;
70
+ };
71
+ skills?: string[];
72
+ model?: string;
73
+ source?: {
74
+ id?: string;
75
+ sync?: string;
76
+ forked_at?: string;
77
+ upstream_version?: string;
78
+ };
79
+ heartbeat?: {
80
+ enabled?: boolean;
81
+ intervalSec?: number;
82
+ maxTicks?: number;
83
+ };
84
+ }
85
+
86
+ /* ─── Build ──────────────────────────────────── */
87
+
88
+ export function buildOrgTree(companyRoot: string, presetId?: string): OrgTree {
89
+ const rolesDir = path.join(companyRoot, 'knowledge', 'roles');
90
+ const tree: OrgTree = { root: 'ceo', nodes: new Map() };
91
+
92
+ // CEO is implicit (not a role.yaml file)
93
+ tree.nodes.set('ceo', {
94
+ id: 'ceo',
95
+ name: 'CEO',
96
+ level: 'c-level',
97
+ reportsTo: '',
98
+ children: [],
99
+ persona: '',
100
+ authority: { autonomous: [], needsApproval: [] },
101
+ knowledge: { reads: ['*'], writes: ['*'] },
102
+ reports: { daily: '', weekly: '' },
103
+ });
104
+
105
+ // Collect role directories to scan: base roles/ + preset roles/
106
+ const roleDirs: string[] = [];
107
+ if (fs.existsSync(rolesDir)) roleDirs.push(rolesDir);
108
+
109
+ // If preset specified, also scan preset's roles directory
110
+ if (presetId && presetId !== 'default') {
111
+ const presetRolesDir = path.join(companyRoot, 'knowledge', 'presets', presetId, 'roles');
112
+ if (fs.existsSync(presetRolesDir)) roleDirs.push(presetRolesDir);
113
+ }
114
+
115
+ // Read all role.yaml files from all role directories
116
+ for (const dir of roleDirs) {
117
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ if (!entry.isDirectory()) continue;
120
+ const yamlPath = path.join(dir, entry.name, 'role.yaml');
121
+ if (!fs.existsSync(yamlPath)) continue;
122
+
123
+ try {
124
+ const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as RawRoleYaml;
125
+ const nodeId = raw.id || entry.name;
126
+
127
+ // Skip if already loaded (base roles take precedence over preset roles)
128
+ if (tree.nodes.has(nodeId)) continue;
129
+
130
+ const node: OrgNode = {
131
+ id: nodeId,
132
+ name: raw.name || entry.name,
133
+ level: (raw.level as OrgNode['level']) || 'member',
134
+ reportsTo: (raw.reports_to || 'ceo').toLowerCase(),
135
+ children: [],
136
+ persona: raw.persona || '',
137
+ authority: {
138
+ autonomous: raw.authority?.autonomous ?? [],
139
+ needsApproval: raw.authority?.needs_approval ?? [],
140
+ },
141
+ knowledge: {
142
+ reads: raw.knowledge?.reads ?? [],
143
+ writes: raw.knowledge?.writes ?? [],
144
+ },
145
+ reports: {
146
+ daily: raw.reports?.daily ?? '',
147
+ weekly: raw.reports?.weekly ?? '',
148
+ },
149
+ skills: raw.skills,
150
+ model: raw.model,
151
+ source: raw.source ? {
152
+ id: raw.source.id || '',
153
+ sync: (raw.source.sync as RoleSource['sync']) || 'manual',
154
+ forked_at: raw.source.forked_at,
155
+ upstream_version: raw.source.upstream_version,
156
+ } : undefined,
157
+ heartbeat: raw.heartbeat ? {
158
+ enabled: raw.heartbeat.enabled ?? false,
159
+ intervalSec: raw.heartbeat.intervalSec ?? 120,
160
+ maxTicks: raw.heartbeat.maxTicks ?? 60,
161
+ } : undefined,
162
+ };
163
+ tree.nodes.set(node.id, node);
164
+ } catch {
165
+ // Skip malformed YAML
166
+ }
167
+ }
168
+ }
169
+
170
+ // Wire up children from reportsTo
171
+ for (const [id, node] of tree.nodes) {
172
+ if (id === 'ceo') continue;
173
+ const parent = tree.nodes.get(node.reportsTo);
174
+ if (parent) {
175
+ parent.children.push(id);
176
+ }
177
+ }
178
+
179
+ return tree;
180
+ }
181
+
182
+ /* ─── Queries ────────────────────────────────── */
183
+
184
+ /** Direct reports */
185
+ export function getSubordinates(tree: OrgTree, roleId: string): string[] {
186
+ return tree.nodes.get(roleId)?.children ?? [];
187
+ }
188
+
189
+ /** All descendants (recursive) */
190
+ export function getDescendants(tree: OrgTree, roleId: string): string[] {
191
+ const result: string[] = [];
192
+ const stack = [...getSubordinates(tree, roleId)];
193
+ while (stack.length > 0) {
194
+ const id = stack.pop()!;
195
+ result.push(id);
196
+ stack.push(...getSubordinates(tree, id));
197
+ }
198
+ return result;
199
+ }
200
+
201
+ /** Chain from role up to CEO: [roleId, ..., ceo] */
202
+ export function getChainOfCommand(tree: OrgTree, roleId: string): string[] {
203
+ const chain: string[] = [];
204
+ let current = roleId;
205
+ const visited = new Set<string>();
206
+ while (current && !visited.has(current)) {
207
+ visited.add(current);
208
+ chain.push(current);
209
+ const node = tree.nodes.get(current);
210
+ if (!node || !node.reportsTo) break;
211
+ current = node.reportsTo;
212
+ }
213
+ return chain;
214
+ }
215
+
216
+ /** Can source dispatch a task to target? */
217
+ export function canDispatchTo(tree: OrgTree, source: string, target: string): boolean {
218
+ if (source === target) return false;
219
+
220
+ // CEO can dispatch to direct reports only
221
+ if (source === 'ceo') {
222
+ return tree.nodes.get(target)?.reportsTo === 'ceo';
223
+ }
224
+
225
+ // Others can dispatch to anyone in their subtree
226
+ const descendants = getDescendants(tree, source);
227
+ return descendants.includes(target);
228
+ }
229
+
230
+ /** Can source consult (ask a question to) target? Peers, direct manager, or subordinates. */
231
+ export function canConsult(tree: OrgTree, source: string, target: string): boolean {
232
+ if (source === target) return false;
233
+ const sourceNode = tree.nodes.get(source);
234
+ const targetNode = tree.nodes.get(target);
235
+ if (!sourceNode || !targetNode) return false;
236
+
237
+ // 1. Peers — same parent
238
+ if (sourceNode.reportsTo === targetNode.reportsTo) return true;
239
+
240
+ // 2. Direct manager
241
+ if (sourceNode.reportsTo === target) return true;
242
+
243
+ // 3. Subordinates (same as dispatch scope)
244
+ const descendants = getDescendants(tree, source);
245
+ return descendants.includes(target);
246
+ }
247
+
248
+ /** Refresh tree (re-read all role.yaml files) */
249
+ export function refreshOrgTree(companyRoot: string, presetId?: string): OrgTree {
250
+ return buildOrgTree(companyRoot, presetId);
251
+ }
252
+
253
+ /** Get a human-readable org chart string for context injection */
254
+ export function formatOrgChart(tree: OrgTree, perspective?: string): string {
255
+ const lines: string[] = [];
256
+
257
+ function render(nodeId: string, indent: number): void {
258
+ const node = tree.nodes.get(nodeId);
259
+ if (!node) return;
260
+ const marker = perspective === nodeId ? ' ← YOU' : '';
261
+ const prefix = indent === 0 ? '' : ' '.repeat(indent) + '└─ ';
262
+ lines.push(`${prefix}${node.name} (${node.id})${marker}`);
263
+ for (const childId of node.children) {
264
+ render(childId, indent + 1);
265
+ }
266
+ }
267
+
268
+ render('ceo', 0);
269
+ return lines.join('\n');
270
+ }
@@ -0,0 +1,369 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { buildOrgTree, type OrgNode, type OrgTree, type RoleSource } from './org-tree.js';
5
+ import { generateSkillMd } from './skill-template.js';
6
+
7
+ /* ─── Types ──────────────────────────────────── */
8
+
9
+ export interface SkillContentDef {
10
+ frontmatter: Record<string, unknown>;
11
+ body: string;
12
+ }
13
+
14
+ export interface SkillExportDef {
15
+ primary: SkillContentDef | null;
16
+ shared: Array<{ id: string } & SkillContentDef>;
17
+ }
18
+
19
+ export interface RoleDefinition {
20
+ id: string;
21
+ name: string;
22
+ level: 'c-level' | 'member';
23
+ reportsTo: string;
24
+ persona: string;
25
+ skills?: string[];
26
+ source?: RoleSource;
27
+ skillContent?: SkillExportDef;
28
+ authority: {
29
+ autonomous: string[];
30
+ needsApproval: string[];
31
+ };
32
+ knowledge: {
33
+ reads: string[];
34
+ writes: string[];
35
+ };
36
+ reports: {
37
+ daily: string;
38
+ weekly: string;
39
+ };
40
+ }
41
+
42
+ export interface RoleValidationResult {
43
+ valid: boolean;
44
+ issues: Array<{
45
+ severity: 'error' | 'warning';
46
+ message: string;
47
+ file: string;
48
+ }>;
49
+ }
50
+
51
+ /* ─── Role Lifecycle Manager ─────────────────── */
52
+
53
+ export class RoleLifecycleManager {
54
+ constructor(private companyRoot: string) {}
55
+
56
+ /**
57
+ * Create a new Role: role.yaml + SKILL.md + profile.md + journal/
58
+ */
59
+ async createRole(def: RoleDefinition): Promise<void> {
60
+ const roleDir = path.join(this.companyRoot, 'knowledge', 'roles', def.id);
61
+ const skillDir = path.join(this.companyRoot, '.claude', 'skills', def.id);
62
+ const journalDir = path.join(roleDir, 'journal');
63
+
64
+ // 1. Create directories
65
+ fs.mkdirSync(roleDir, { recursive: true });
66
+ fs.mkdirSync(journalDir, { recursive: true });
67
+ fs.mkdirSync(skillDir, { recursive: true });
68
+
69
+ // 2. Write role.yaml
70
+ const yamlContent = this.buildRoleYaml(def);
71
+ fs.writeFileSync(path.join(roleDir, 'role.yaml'), yamlContent);
72
+
73
+ // 3. Write profile.md
74
+ const profileContent = this.buildProfile(def);
75
+ fs.writeFileSync(path.join(roleDir, 'profile.md'), profileContent);
76
+
77
+ // 4. Generate SKILL.md (Level 1 template)
78
+ const orgNode = this.defToOrgNode(def);
79
+ const skillContent = generateSkillMd(orgNode);
80
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
81
+
82
+ // 4b. Store에서 온 skillContent가 있으면 덮어쓰기
83
+ if (def.skillContent?.primary) {
84
+ const content = serializeSkillMd(def.skillContent.primary);
85
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
86
+ }
87
+
88
+ // 4c. Shared skills 설치 (이미 있으면 건너뜀)
89
+ if (def.skillContent?.shared) {
90
+ for (const shared of def.skillContent.shared) {
91
+ const sharedDir = path.join(this.companyRoot, '.claude', 'skills', '_shared', shared.id);
92
+ const sharedSkillPath = path.join(sharedDir, 'SKILL.md');
93
+ if (!fs.existsSync(sharedSkillPath)) {
94
+ fs.mkdirSync(sharedDir, { recursive: true });
95
+ fs.writeFileSync(sharedSkillPath, serializeSkillMd(shared));
96
+ }
97
+ }
98
+ }
99
+
100
+ // 5. Update roles.md Hub
101
+ this.addToRolesHub(def);
102
+ }
103
+
104
+ /**
105
+ * Update an existing Role's definition
106
+ */
107
+ async updateRole(id: string, changes: Partial<RoleDefinition>): Promise<void> {
108
+ const yamlPath = path.join(this.companyRoot, 'knowledge', 'roles', id, 'role.yaml');
109
+ if (!fs.existsSync(yamlPath)) {
110
+ throw new Error(`Role not found: ${id}`);
111
+ }
112
+
113
+ // Read current
114
+ const current = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as Record<string, unknown>;
115
+
116
+ // Apply changes
117
+ if (changes.name !== undefined) current.name = changes.name;
118
+ if (changes.level !== undefined) current.level = changes.level;
119
+ if (changes.reportsTo !== undefined) current.reports_to = changes.reportsTo;
120
+ if (changes.persona !== undefined) current.persona = changes.persona;
121
+ if (changes.skills !== undefined) current.skills = changes.skills;
122
+ if (changes.authority !== undefined) {
123
+ current.authority = {
124
+ autonomous: changes.authority.autonomous,
125
+ needs_approval: changes.authority.needsApproval,
126
+ };
127
+ }
128
+ if (changes.knowledge !== undefined) {
129
+ current.knowledge = {
130
+ reads: changes.knowledge.reads,
131
+ writes: changes.knowledge.writes,
132
+ };
133
+ }
134
+ if (changes.reports !== undefined) {
135
+ current.reports = changes.reports;
136
+ }
137
+ if (changes.source !== undefined) {
138
+ current.source = changes.source;
139
+ }
140
+
141
+ fs.writeFileSync(yamlPath, YAML.stringify(current));
142
+ }
143
+
144
+ /**
145
+ * Remove a Role and all its files
146
+ */
147
+ async removeRole(id: string): Promise<void> {
148
+ const roleDir = path.join(this.companyRoot, 'knowledge', 'roles', id);
149
+ const skillDir = path.join(this.companyRoot, '.claude', 'skills', id);
150
+
151
+ if (fs.existsSync(roleDir)) {
152
+ fs.rmSync(roleDir, { recursive: true });
153
+ }
154
+ if (fs.existsSync(skillDir)) {
155
+ fs.rmSync(skillDir, { recursive: true });
156
+ }
157
+
158
+ // Remove from roles.md Hub
159
+ this.removeFromRolesHub(id);
160
+ }
161
+
162
+ /**
163
+ * Regenerate SKILL.md from role.yaml (Level 1 template)
164
+ */
165
+ async regenerateSkill(id: string): Promise<void> {
166
+ const tree = buildOrgTree(this.companyRoot);
167
+ const node = tree.nodes.get(id);
168
+ if (!node) throw new Error(`Role not found in org tree: ${id}`);
169
+
170
+ const skillDir = path.join(this.companyRoot, '.claude', 'skills', id);
171
+ fs.mkdirSync(skillDir, { recursive: true });
172
+
173
+ const skillContent = generateSkillMd(node);
174
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
175
+ }
176
+
177
+ /**
178
+ * Validate Role integrity: check all required files exist
179
+ */
180
+ validateRole(id: string): RoleValidationResult {
181
+ const issues: RoleValidationResult['issues'] = [];
182
+
183
+ const roleDir = path.join(this.companyRoot, 'knowledge', 'roles', id);
184
+ const yamlPath = path.join(roleDir, 'role.yaml');
185
+ const profilePath = path.join(roleDir, 'profile.md');
186
+ const journalDir = path.join(roleDir, 'journal');
187
+ const skillPath = path.join(this.companyRoot, '.claude', 'skills', id, 'SKILL.md');
188
+
189
+ if (!fs.existsSync(yamlPath)) {
190
+ issues.push({ severity: 'error', message: 'role.yaml missing', file: yamlPath });
191
+ }
192
+
193
+ if (!fs.existsSync(skillPath)) {
194
+ issues.push({ severity: 'error', message: 'SKILL.md missing', file: skillPath });
195
+ }
196
+
197
+ if (!fs.existsSync(profilePath)) {
198
+ issues.push({ severity: 'warning', message: 'profile.md missing', file: profilePath });
199
+ }
200
+
201
+ if (!fs.existsSync(journalDir)) {
202
+ issues.push({ severity: 'warning', message: 'journal/ directory missing', file: journalDir });
203
+ }
204
+
205
+ // Check role.yaml has required fields
206
+ if (fs.existsSync(yamlPath)) {
207
+ try {
208
+ const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as Record<string, unknown>;
209
+ if (!raw.id) issues.push({ severity: 'error', message: 'role.yaml missing "id" field', file: yamlPath });
210
+ if (!raw.name) issues.push({ severity: 'error', message: 'role.yaml missing "name" field', file: yamlPath });
211
+ if (!raw.reports_to) issues.push({ severity: 'warning', message: 'role.yaml missing "reports_to" field', file: yamlPath });
212
+ if (!raw.persona) issues.push({ severity: 'warning', message: 'role.yaml missing "persona" field', file: yamlPath });
213
+ } catch {
214
+ issues.push({ severity: 'error', message: 'role.yaml is not valid YAML', file: yamlPath });
215
+ }
216
+ }
217
+
218
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
219
+ }
220
+
221
+ /**
222
+ * Validate all roles in the organization
223
+ */
224
+ validateAll(): Map<string, RoleValidationResult> {
225
+ const results = new Map<string, RoleValidationResult>();
226
+ const rolesDir = path.join(this.companyRoot, 'knowledge', 'roles');
227
+
228
+ if (!fs.existsSync(rolesDir)) return results;
229
+
230
+ const entries = fs.readdirSync(rolesDir, { withFileTypes: true });
231
+ for (const entry of entries) {
232
+ if (!entry.isDirectory()) continue;
233
+ if (entry.name === 'roles.md') continue; // Hub file, not a role dir
234
+
235
+ const yamlPath = path.join(rolesDir, entry.name, 'role.yaml');
236
+ if (fs.existsSync(yamlPath)) {
237
+ results.set(entry.name, this.validateRole(entry.name));
238
+ }
239
+ }
240
+
241
+ return results;
242
+ }
243
+
244
+ /* ─── Private helpers ──────────────────────── */
245
+
246
+ private defToOrgNode(def: RoleDefinition): OrgNode {
247
+ return {
248
+ id: def.id,
249
+ name: def.name,
250
+ level: def.level,
251
+ reportsTo: def.reportsTo,
252
+ children: [],
253
+ persona: def.persona,
254
+ authority: {
255
+ autonomous: def.authority.autonomous,
256
+ needsApproval: def.authority.needsApproval,
257
+ },
258
+ knowledge: {
259
+ reads: def.knowledge.reads,
260
+ writes: def.knowledge.writes,
261
+ },
262
+ reports: def.reports,
263
+ skills: def.skills,
264
+ source: def.source,
265
+ };
266
+ }
267
+
268
+ private buildRoleYaml(def: RoleDefinition): string {
269
+ const obj: Record<string, unknown> = {
270
+ id: def.id,
271
+ name: def.name,
272
+ level: def.level,
273
+ reports_to: def.reportsTo,
274
+ persona: def.persona,
275
+ };
276
+ if (def.skills?.length) {
277
+ obj.skills = def.skills;
278
+ }
279
+ if (def.source) {
280
+ obj.source = def.source;
281
+ }
282
+ obj.authority = {
283
+ autonomous: def.authority.autonomous,
284
+ needs_approval: def.authority.needsApproval,
285
+ };
286
+ obj.knowledge = {
287
+ reads: def.knowledge.reads,
288
+ writes: def.knowledge.writes,
289
+ };
290
+ obj.reports = def.reports;
291
+ return YAML.stringify(obj);
292
+ }
293
+
294
+ private buildProfile(def: RoleDefinition): string {
295
+ return `# ${def.name}
296
+
297
+ > ${def.persona.trim().split('\n')[0]}
298
+
299
+ ## 기본 정보
300
+
301
+ | 항목 | 내용 |
302
+ |------|------|
303
+ | ID | ${def.id} |
304
+ | 직급 | ${def.level} |
305
+ | 보고 대상 | ${def.reportsTo} |
306
+
307
+ ## Persona
308
+
309
+ ${def.persona}
310
+
311
+ ## Authority
312
+
313
+ ### 자율
314
+ ${def.authority.autonomous.map((a) => `- ${a}`).join('\n')}
315
+
316
+ ### 승인 필요
317
+ ${def.authority.needsApproval.map((a) => `- ${a}`).join('\n')}
318
+ `;
319
+ }
320
+
321
+ private addToRolesHub(def: RoleDefinition): void {
322
+ const hubPath = path.join(this.companyRoot, 'knowledge', 'roles', 'roles.md');
323
+ if (!fs.existsSync(hubPath)) return;
324
+
325
+ const content = fs.readFileSync(hubPath, 'utf-8');
326
+ if (content.includes(`| ${def.id} |`)) {
327
+ return; // Already exists
328
+ }
329
+
330
+ const row = `| ${def.name} | ${def.id} | ${def.level} | ${def.reportsTo} | Active |`;
331
+ const updatedContent = content.trimEnd() + '\n' + row + '\n';
332
+ fs.writeFileSync(hubPath, updatedContent);
333
+ }
334
+
335
+ private removeFromRolesHub(id: string): void {
336
+ const hubPath = path.join(this.companyRoot, 'knowledge', 'roles', 'roles.md');
337
+ if (!fs.existsSync(hubPath)) return;
338
+
339
+ const content = fs.readFileSync(hubPath, 'utf-8');
340
+ const lines = content.split('\n').filter((line) => {
341
+ if (!line.includes('|')) return true;
342
+ const cells = line.split('|').map((c) => c.trim());
343
+ return !cells.some((c) => c === id);
344
+ });
345
+ fs.writeFileSync(hubPath, lines.join('\n'));
346
+ }
347
+
348
+ }
349
+
350
+ /* ─── Helpers ──────────────────────────────── */
351
+
352
+ function serializeSkillMd(skill: SkillContentDef): string {
353
+ const fm = skill.frontmatter;
354
+ const yamlLines: string[] = [];
355
+ if (fm.name) yamlLines.push(`name: ${fm.name}`);
356
+ if (fm.description) yamlLines.push(`description: ${JSON.stringify(fm.description)}`);
357
+ if (fm.allowedTools && Array.isArray(fm.allowedTools)) {
358
+ yamlLines.push(`allowedTools:\n${(fm.allowedTools as string[]).map(t => ` - ${t}`).join('\n')}`);
359
+ }
360
+ if (fm.model) yamlLines.push(`model: ${fm.model}`);
361
+ if (fm.tags && Array.isArray(fm.tags)) {
362
+ yamlLines.push(`tags:\n${(fm.tags as string[]).map(t => ` - ${t}`).join('\n')}`);
363
+ }
364
+
365
+ if (yamlLines.length === 0) {
366
+ return skill.body;
367
+ }
368
+ return `---\n${yamlLines.join('\n')}\n---\n\n${skill.body}`;
369
+ }