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.
- package/bin/cli.js +35 -0
- package/bin/server.ts +160 -0
- package/package.json +50 -0
- package/src/api/package.json +31 -0
- package/src/api/src/create-app.ts +90 -0
- package/src/api/src/create-server.ts +251 -0
- package/src/api/src/engine/agent-loop.ts +738 -0
- package/src/api/src/engine/authority-validator.ts +149 -0
- package/src/api/src/engine/context-assembler.ts +912 -0
- package/src/api/src/engine/index.ts +27 -0
- package/src/api/src/engine/knowledge-gate.ts +365 -0
- package/src/api/src/engine/llm-adapter.ts +304 -0
- package/src/api/src/engine/org-tree.ts +270 -0
- package/src/api/src/engine/role-lifecycle.ts +369 -0
- package/src/api/src/engine/runners/claude-cli.ts +796 -0
- package/src/api/src/engine/runners/direct-api.ts +66 -0
- package/src/api/src/engine/runners/index.ts +30 -0
- package/src/api/src/engine/runners/types.ts +95 -0
- package/src/api/src/engine/skill-template.ts +134 -0
- package/src/api/src/engine/tools/definitions.ts +201 -0
- package/src/api/src/engine/tools/executor.ts +611 -0
- package/src/api/src/routes/active-sessions.ts +134 -0
- package/src/api/src/routes/coins.ts +153 -0
- package/src/api/src/routes/company.ts +57 -0
- package/src/api/src/routes/cost.ts +141 -0
- package/src/api/src/routes/engine.ts +220 -0
- package/src/api/src/routes/execute.ts +1075 -0
- package/src/api/src/routes/git.ts +211 -0
- package/src/api/src/routes/knowledge.ts +378 -0
- package/src/api/src/routes/operations.ts +309 -0
- package/src/api/src/routes/preferences.ts +63 -0
- package/src/api/src/routes/presets.ts +123 -0
- package/src/api/src/routes/projects.ts +82 -0
- package/src/api/src/routes/quests.ts +41 -0
- package/src/api/src/routes/roles.ts +112 -0
- package/src/api/src/routes/save.ts +152 -0
- package/src/api/src/routes/sessions.ts +288 -0
- package/src/api/src/routes/setup.ts +437 -0
- package/src/api/src/routes/skills.ts +357 -0
- package/src/api/src/routes/speech.ts +959 -0
- package/src/api/src/routes/supervision.ts +136 -0
- package/src/api/src/routes/sync.ts +165 -0
- package/src/api/src/server.ts +59 -0
- package/src/api/src/services/activity-stream.ts +184 -0
- package/src/api/src/services/activity-tracker.ts +115 -0
- package/src/api/src/services/claude-md-manager.ts +94 -0
- package/src/api/src/services/company-config.ts +115 -0
- package/src/api/src/services/database.ts +77 -0
- package/src/api/src/services/digest-engine.ts +313 -0
- package/src/api/src/services/execution-manager.ts +1036 -0
- package/src/api/src/services/file-reader.ts +77 -0
- package/src/api/src/services/git-save.ts +614 -0
- package/src/api/src/services/job-manager.ts +16 -0
- package/src/api/src/services/knowledge-importer.ts +466 -0
- package/src/api/src/services/markdown-parser.ts +173 -0
- package/src/api/src/services/port-registry.ts +222 -0
- package/src/api/src/services/preferences.ts +150 -0
- package/src/api/src/services/preset-loader.ts +149 -0
- package/src/api/src/services/pricing.ts +34 -0
- package/src/api/src/services/scaffold.ts +546 -0
- package/src/api/src/services/session-store.ts +340 -0
- package/src/api/src/services/supervisor-heartbeat.ts +897 -0
- package/src/api/src/services/team-recommender.ts +382 -0
- package/src/api/src/services/token-ledger.ts +127 -0
- package/src/api/src/services/wave-messages.ts +194 -0
- package/src/api/src/services/wave-multiplexer.ts +356 -0
- package/src/api/src/services/wave-tracker.ts +359 -0
- package/src/api/src/utils/role-level.ts +31 -0
- package/src/core/scaffolder.ts +620 -0
- package/src/shared/types.ts +224 -0
- package/templates/CLAUDE.md.tmpl +239 -0
- package/templates/company.md.tmpl +17 -0
- package/templates/gitignore.tmpl +28 -0
- package/templates/roles.md.tmpl +8 -0
- package/templates/skills/_manifest.json +23 -0
- package/templates/skills/agent-browser/SKILL.md +159 -0
- package/templates/skills/agent-browser/meta.json +19 -0
- package/templates/skills/akb-linter/SKILL.md +125 -0
- package/templates/skills/akb-linter/meta.json +12 -0
- package/templates/skills/knowledge-gate/SKILL.md +120 -0
- package/templates/skills/knowledge-gate/meta.json +12 -0
- package/templates/teams/agency.json +58 -0
- package/templates/teams/research.json +58 -0
- 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
|
+
}
|