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,546 @@
1
+ /**
2
+ * scaffold.ts — AKB scaffolding service
3
+ *
4
+ * AKB scaffolding used by the web onboarding wizard.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { execSync } from 'node:child_process';
10
+ import { writeConfig } from './company-config.js';
11
+ import { mergePreferences, type CharacterAppearance } from './preferences.js';
12
+ import type { CompanyConfig } from './company-config.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ const TEMPLATES_DIR = path.resolve(__dirname, '../../../../templates');
17
+
18
+ /* ─── Default Appearances ─── */
19
+
20
+ const DEFAULT_ROLE_APPEARANCES: Record<string, { skinColor: string; hairColor: string; shirtColor: string; pantsColor: string; shoeColor: string; hairStyle: string; outfitStyle: string; accessory: string }> = {
21
+ // Shared across templates
22
+ cto: { skinColor: '#F5CBA7', hairColor: '#2C1810', shirtColor: '#1565C0', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'short', outfitStyle: 'tshirt', accessory: 'glasses' },
23
+ cbo: { skinColor: '#FDEBD0', hairColor: '#1A0A00', shirtColor: '#E65100', pantsColor: '#37474F', shoeColor: '#1A1A1A', hairStyle: 'slicked', outfitStyle: 'suit', accessory: 'lapels' },
24
+ pm: { skinColor: '#FDEBD0', hairColor: '#6D4C41', shirtColor: '#2E7D32', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'bun', outfitStyle: 'tshirt', accessory: 'blush' },
25
+ engineer: { skinColor: '#F5CBA7', hairColor: '#1A1A1A', shirtColor: '#4A148C', pantsColor: '#37474F', shoeColor: '#7B1FA2', hairStyle: 'messy', outfitStyle: 'hoodie', accessory: 'headphones' },
26
+ designer: { skinColor: '#FDEBD0', hairColor: '#AD1457', shirtColor: '#AD1457', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'bob', outfitStyle: 'tshirt', accessory: 'beret' },
27
+ qa: { skinColor: '#F5CBA7', hairColor: '#4E342E', shirtColor: '#00695C', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'short', outfitStyle: 'tshirt', accessory: 'badge' },
28
+ // Startup template
29
+ 'be-engineer': { skinColor: '#F5CBA7', hairColor: '#1A1A1A', shirtColor: '#4A148C', pantsColor: '#37474F', shoeColor: '#7B1FA2', hairStyle: 'messy', outfitStyle: 'hoodie', accessory: 'headphones' },
30
+ 'fe-engineer': { skinColor: '#FDEBD0', hairColor: '#5D4037', shirtColor: '#00838F', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'wavy', outfitStyle: 'tshirt', accessory: 'glasses' },
31
+ po: { skinColor: '#F5CBA7', hairColor: '#6D4C41', shirtColor: '#2E7D32', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'bun', outfitStyle: 'tshirt', accessory: 'blush' },
32
+ // Research template
33
+ 'lead-researcher': { skinColor: '#F5CBA7', hairColor: '#4E342E', shirtColor: '#1565C0', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'short', outfitStyle: 'suit', accessory: 'glasses' },
34
+ 'lead-analyst': { skinColor: '#FDEBD0', hairColor: '#1A0A00', shirtColor: '#6A1B9A', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'slicked', outfitStyle: 'suit', accessory: 'glasses' },
35
+ researcher: { skinColor: '#F5CBA7', hairColor: '#3E2723', shirtColor: '#00695C', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'messy', outfitStyle: 'tshirt', accessory: 'badge' },
36
+ analyst: { skinColor: '#FDEBD0', hairColor: '#4E342E', shirtColor: '#0277BD', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'bob', outfitStyle: 'tshirt', accessory: 'glasses' },
37
+ writer: { skinColor: '#F5CBA7', hairColor: '#6D4C41', shirtColor: '#558B2F', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'wavy', outfitStyle: 'tshirt', accessory: 'blush' },
38
+ editor: { skinColor: '#FDEBD0', hairColor: '#3E2723', shirtColor: '#BF360C', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'short', outfitStyle: 'tshirt', accessory: 'badge' },
39
+ // Agency template
40
+ 'creative-director': { skinColor: '#FDEBD0', hairColor: '#AD1457', shirtColor: '#6A1B9A', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'wavy', outfitStyle: 'tshirt', accessory: 'beret' },
41
+ 'account-director': { skinColor: '#F5CBA7', hairColor: '#1A0A00', shirtColor: '#37474F', pantsColor: '#37474F', shoeColor: '#1A1A1A', hairStyle: 'slicked', outfitStyle: 'suit', accessory: 'lapels' },
42
+ copywriter: { skinColor: '#F5CBA7', hairColor: '#5D4037', shirtColor: '#E65100', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'messy', outfitStyle: 'hoodie', accessory: 'headphones' },
43
+ developer: { skinColor: '#FDEBD0', hairColor: '#1A1A1A', shirtColor: '#1565C0', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'short', outfitStyle: 'hoodie', accessory: 'glasses' },
44
+ strategist: { skinColor: '#F5CBA7', hairColor: '#4E342E', shirtColor: '#00695C', pantsColor: '#37474F', shoeColor: '#212121', hairStyle: 'bun', outfitStyle: 'tshirt', accessory: 'badge' },
45
+ };
46
+
47
+ const AKB_METHODOLOGY_CONTENT = `# Agentic Knowledge Base (AKB)
48
+
49
+ > The canonical reference for AKB — the file-based knowledge protocol for AI agents.
50
+
51
+ ## TL;DR
52
+
53
+ - **Definition**: A file-based knowledge system where AI uses **search (Grep/Glob)** to find and **contextual links** to navigate
54
+ - **Essence**: File-based Lightweight Ontology (Tag = Type, inline links = Edges)
55
+ - **Philosophy**: Optimize documents so AI can find them — don't force AI to follow a rigid protocol
56
+ - **Structure**: Root (CLAUDE.md) → Hub ({folder}.md) → Node (*.md)
57
+ - **Core rules**: 5 writing principles (TL;DR, contextual links, keyword-optimized filenames, atomicity, semantic vs implementation separation)
58
+
59
+ ---
60
+
61
+ ## Architecture
62
+
63
+ AKB follows a 3-layer hierarchy: **Root → Hub → Node**.
64
+
65
+ | Layer | Role | Description |
66
+ |-------|------|-------------|
67
+ | **Root** (CLAUDE.md) | Minimal routing | Auto-injected as system prompt, provides key file paths |
68
+ | **Hub** ({folder}.md) | TOC for humans | Folder overview; AI reads selectively |
69
+ | **Node** (*.md) | Actual information | What AI searches for via Grep/Glob |
70
+
71
+ ## Writing Principles
72
+
73
+ 1. **TL;DR Required** — 3-5 bullet points with bold keywords for Grep search
74
+ 2. **Contextual Links** — Place links inline with context, not in isolated lists
75
+ 3. **Keyword Filenames** — Use descriptive filenames (not notes.md, use market-analysis.md)
76
+ 4. **Atomicity** — One topic per doc, under 200 lines
77
+ 5. **Semantic vs Implementation** — AKB holds "why" and relationships; code repo holds specs and configs
78
+
79
+ ## Design Principle
80
+
81
+ > "Don't try to change AI behavior — optimize documents so AI can find them naturally."
82
+
83
+ If AI found the information it needed and produced a good answer, that's proof AKB is working.
84
+ `;
85
+
86
+ function getPackageVersion(): string {
87
+ const pkgPath = path.resolve(__dirname, '../../../../package.json');
88
+ try {
89
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
90
+ return pkg.version || '0.0.0';
91
+ } catch {
92
+ return '0.0.0';
93
+ }
94
+ }
95
+
96
+ export interface ScaffoldConfig {
97
+ companyName: string;
98
+ description: string;
99
+ apiKey?: string;
100
+ team: 'startup' | 'research' | 'agency' | 'custom';
101
+ projectRoot: string;
102
+ existingProjectPath?: string;
103
+ knowledgePaths?: string[];
104
+ language?: string;
105
+ }
106
+
107
+ interface TeamRole {
108
+ id: string;
109
+ name: string;
110
+ level: string;
111
+ reportsTo: string;
112
+ persona: string;
113
+ defaultSkills?: string[];
114
+ }
115
+
116
+ function loadTemplate(name: string): string {
117
+ return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf-8');
118
+ }
119
+
120
+ export function loadTeam(teamName: string): TeamRole[] {
121
+ const teamPath = path.join(TEMPLATES_DIR, 'teams', `${teamName}.json`);
122
+ if (!fs.existsSync(teamPath)) return [];
123
+ return JSON.parse(fs.readFileSync(teamPath, 'utf-8'));
124
+ }
125
+
126
+ export function getAvailableTeams(): string[] {
127
+ const teamsDir = path.join(TEMPLATES_DIR, 'teams');
128
+ if (!fs.existsSync(teamsDir)) return [];
129
+ return fs.readdirSync(teamsDir)
130
+ .filter(f => f.endsWith('.json'))
131
+ .map(f => f.replace('.json', ''));
132
+ }
133
+
134
+ export interface SkillToolDef {
135
+ package: string;
136
+ binary: string;
137
+ installCmd: string;
138
+ }
139
+
140
+ export interface SkillMeta {
141
+ id: string;
142
+ name: string;
143
+ description: string;
144
+ version: string;
145
+ author: string;
146
+ tags: string[];
147
+ category: string;
148
+ compatibleRoles: string[];
149
+ dependencies: string[];
150
+ files: string[];
151
+ tools?: SkillToolDef[];
152
+ }
153
+
154
+ /**
155
+ * Get available skills from the template registry
156
+ */
157
+ export function getAvailableSkills(): SkillMeta[] {
158
+ const manifestPath = path.join(TEMPLATES_DIR, 'skills', '_manifest.json');
159
+ if (!fs.existsSync(manifestPath)) return [];
160
+
161
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
162
+ const skills: SkillMeta[] = [];
163
+
164
+ for (const entry of manifest.skills) {
165
+ const metaPath = path.join(TEMPLATES_DIR, 'skills', entry.id, 'meta.json');
166
+ if (fs.existsSync(metaPath)) {
167
+ skills.push(JSON.parse(fs.readFileSync(metaPath, 'utf-8')));
168
+ }
169
+ }
170
+
171
+ return skills;
172
+ }
173
+
174
+ /**
175
+ * Check if a CLI binary is available on the system
176
+ */
177
+ function isBinaryInstalled(binary: string): boolean {
178
+ try {
179
+ execSync(`which ${binary}`, { stdio: 'ignore', timeout: 5000 });
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Collect all tools required by a set of skills
188
+ */
189
+ export function getRequiredTools(skillIds: string[]): Array<SkillToolDef & { skillId: string; installed: boolean }> {
190
+ const tools: Array<SkillToolDef & { skillId: string; installed: boolean }> = [];
191
+ const seen = new Set<string>();
192
+
193
+ for (const skillId of skillIds) {
194
+ const metaPath = path.join(TEMPLATES_DIR, 'skills', skillId, 'meta.json');
195
+ if (!fs.existsSync(metaPath)) continue;
196
+
197
+ const meta: SkillMeta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
198
+ if (!meta.tools?.length) continue;
199
+
200
+ for (const tool of meta.tools) {
201
+ if (seen.has(tool.package)) continue;
202
+ seen.add(tool.package);
203
+ tools.push({
204
+ ...tool,
205
+ skillId,
206
+ installed: isBinaryInstalled(tool.binary),
207
+ });
208
+ }
209
+ }
210
+
211
+ return tools;
212
+ }
213
+
214
+ export interface ToolInstallCallbacks {
215
+ onChecking?: (tool: string) => void;
216
+ onInstalling?: (tool: string) => void;
217
+ onInstalled?: (tool: string) => void;
218
+ onSkipped?: (tool: string, reason: string) => void;
219
+ onError?: (tool: string, error: string) => void;
220
+ onDone?: (stats: { installed: number; skipped: number; failed: number }) => void;
221
+ }
222
+
223
+ /**
224
+ * Install CLI tools required by skills
225
+ */
226
+ export function installSkillTools(skillIds: string[], callbacks?: ToolInstallCallbacks): void {
227
+ const tools = getRequiredTools(skillIds);
228
+ let installed = 0;
229
+ let skipped = 0;
230
+ let failed = 0;
231
+
232
+ for (const tool of tools) {
233
+ callbacks?.onChecking?.(tool.package);
234
+
235
+ if (tool.installed) {
236
+ callbacks?.onSkipped?.(tool.package, 'already installed');
237
+ skipped++;
238
+ continue;
239
+ }
240
+
241
+ callbacks?.onInstalling?.(tool.package);
242
+ try {
243
+ execSync(tool.installCmd, { stdio: 'ignore', timeout: 120000 });
244
+ callbacks?.onInstalled?.(tool.package);
245
+ installed++;
246
+ } catch (err) {
247
+ const msg = err instanceof Error ? err.message : 'install failed';
248
+ callbacks?.onError?.(tool.package, msg);
249
+ failed++;
250
+ }
251
+ }
252
+
253
+ callbacks?.onDone?.({ installed, skipped, failed });
254
+ }
255
+
256
+ function renderTemplate(template: string, vars: Record<string, string>): string {
257
+ let result = template;
258
+ for (const [key, value] of Object.entries(vars)) {
259
+ result = result.replaceAll(`{{${key}}}`, value);
260
+ }
261
+ return result;
262
+ }
263
+
264
+ /**
265
+ * Copy a skill from templates/skills/ to the target AKB's knowledge/.claude/skills/_shared/
266
+ */
267
+ function installSkill(root: string, skillId: string): boolean {
268
+ const srcDir = path.join(TEMPLATES_DIR, 'skills', skillId);
269
+ if (!fs.existsSync(srcDir)) return false;
270
+
271
+ const destDir = path.join(root, 'knowledge', '.claude', 'skills', '_shared', skillId);
272
+ fs.mkdirSync(destDir, { recursive: true });
273
+
274
+ // Copy SKILL.md
275
+ const skillMdSrc = path.join(srcDir, 'SKILL.md');
276
+ if (fs.existsSync(skillMdSrc)) {
277
+ fs.copyFileSync(skillMdSrc, path.join(destDir, 'SKILL.md'));
278
+ }
279
+
280
+ return true;
281
+ }
282
+
283
+ /**
284
+ * Collect all unique skills needed by a team's roles and install them
285
+ */
286
+ function installTeamSkills(root: string, roles: TeamRole[]): string[] {
287
+ const installed: string[] = [];
288
+ const skillIds = new Set<string>();
289
+
290
+ for (const role of roles) {
291
+ if (role.defaultSkills) {
292
+ for (const skillId of role.defaultSkills) {
293
+ skillIds.add(skillId);
294
+ }
295
+ }
296
+ }
297
+
298
+ for (const skillId of skillIds) {
299
+ if (installSkill(root, skillId)) {
300
+ installed.push(skillId);
301
+ }
302
+ }
303
+
304
+ return installed;
305
+ }
306
+
307
+ export function scaffold(config: ScaffoldConfig): string[] {
308
+ const root = config.projectRoot;
309
+ const created: string[] = [];
310
+ const vars = {
311
+ COMPANY_NAME: config.companyName,
312
+ DESCRIPTION: config.description,
313
+ };
314
+
315
+ // Create directories
316
+ const dirs = [
317
+ 'knowledge', 'knowledge/roles', 'knowledge/projects',
318
+ 'knowledge/architecture', 'knowledge/methodologies',
319
+ 'knowledge/decisions', 'knowledge/presets',
320
+ 'knowledge/.claude/skills', 'knowledge/.claude/skills/_shared',
321
+ '.tycono/waves', '.tycono/sessions', '.tycono/standup',
322
+ '.tycono/activity-streams', '.tycono/cost', '.tycono/activity',
323
+ '.tycono',
324
+ 'apps',
325
+ ];
326
+ for (const dir of dirs) {
327
+ fs.mkdirSync(path.join(root, dir), { recursive: true });
328
+ created.push(dir + '/');
329
+ }
330
+
331
+ // Write CLAUDE.md — knowledge/ only (AI agent's cwd)
332
+ const claudeTmpl = loadTemplate('CLAUDE.md.tmpl');
333
+ const pkgVersion = getPackageVersion();
334
+ const claudeContent = claudeTmpl.replaceAll('{{VERSION}}', pkgVersion);
335
+ fs.writeFileSync(path.join(root, 'knowledge', 'CLAUDE.md'), claudeContent);
336
+ created.push('knowledge/CLAUDE.md');
337
+
338
+ // Write .tycono/rules-version
339
+ fs.writeFileSync(path.join(root, '.tycono', 'rules-version'), pkgVersion);
340
+ created.push('.tycono/rules-version');
341
+
342
+ // Write knowledge/custom-rules.md (empty stub — user owned, git tracked)
343
+ const customRulesPath = path.join(root, 'knowledge', 'custom-rules.md');
344
+ if (!fs.existsSync(customRulesPath)) {
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`);
346
+ created.push('knowledge/custom-rules.md');
347
+ }
348
+
349
+ // Write knowledge/company.md
350
+ const companyTmpl = loadTemplate('company.md.tmpl');
351
+ fs.writeFileSync(path.join(root, 'knowledge', 'company.md'), renderTemplate(companyTmpl, vars));
352
+ created.push('knowledge/company.md');
353
+
354
+ // Write knowledge/roles/roles.md
355
+ const rolesTmpl = loadTemplate('roles.md.tmpl');
356
+ fs.writeFileSync(path.join(root, 'knowledge', 'roles', 'roles.md'), renderTemplate(rolesTmpl, vars));
357
+ created.push('knowledge/roles/roles.md');
358
+
359
+ // Write .gitignore
360
+ const giTmpl = loadTemplate('gitignore.tmpl');
361
+ const gitignorePath = path.join(root, '.gitignore');
362
+ if (!fs.existsSync(gitignorePath)) {
363
+ fs.writeFileSync(gitignorePath, giTmpl);
364
+ created.push('.gitignore');
365
+ }
366
+
367
+ // Write .tycono/config.json (engine + API key + codeRoot)
368
+ const slug = config.companyName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'my-company';
369
+ const defaultCodeRoot = path.join(root, 'apps');
370
+ if (config.apiKey) {
371
+ const companyConfig: CompanyConfig = {
372
+ engine: 'direct-api',
373
+ apiKey: config.apiKey,
374
+ codeRoot: defaultCodeRoot,
375
+ };
376
+ writeConfig(root, companyConfig);
377
+ created.push('.tycono/config.json');
378
+ // Also write .env for backward compatibility
379
+ fs.writeFileSync(path.join(root, '.env'), `ANTHROPIC_API_KEY=${config.apiKey}\n`);
380
+ created.push('.env');
381
+ } else {
382
+ const companyConfig: CompanyConfig = { engine: 'claude-cli', codeRoot: defaultCodeRoot };
383
+ writeConfig(root, companyConfig);
384
+ created.push('.tycono/config.json');
385
+ }
386
+ // Ensure codeRoot directory exists
387
+ fs.mkdirSync(defaultCodeRoot, { recursive: true });
388
+ created.push(`(code) ${defaultCodeRoot}`);
389
+
390
+ // Save language preference (default: English)
391
+ mergePreferences(root, { language: config.language || 'en' });
392
+
393
+ // Create team roles + install skills
394
+ if (config.team !== 'custom') {
395
+ const roles = loadTeam(config.team);
396
+
397
+ // Install shared skills needed by this team
398
+ const installedSkills = installTeamSkills(root, roles);
399
+ for (const skillId of installedSkills) {
400
+ created.push(`.claude/skills/_shared/${skillId}/`);
401
+ }
402
+
403
+ // Create roles with skill references
404
+ for (const role of roles) {
405
+ createRole(root, role);
406
+ created.push(`knowledge/roles/${role.id}/`);
407
+ }
408
+ }
409
+
410
+ // Hub files
411
+ const hubs: Record<string, string> = {
412
+ 'knowledge/projects/projects.md': `# Projects\n\nProject listing for ${config.companyName}.\n\n| Project | Status | Lead |\n|---------|--------|------|\n`,
413
+ 'knowledge/architecture/architecture.md': `# Architecture\n\nTechnical architecture for ${config.companyName}.\n`,
414
+ 'knowledge/knowledge.md': `# Knowledge Base\n\nDomain knowledge for ${config.companyName}.\n`,
415
+ };
416
+ for (const [filePath, content] of Object.entries(hubs)) {
417
+ const full = path.join(root, filePath);
418
+ if (!fs.existsSync(full)) {
419
+ fs.writeFileSync(full, content);
420
+ created.push(filePath);
421
+ }
422
+ }
423
+
424
+ // Methodology documents
425
+ const methodologiesHub = path.join(root, 'knowledge', 'methodologies', 'methodologies.md');
426
+ if (!fs.existsSync(methodologiesHub)) {
427
+ fs.writeFileSync(methodologiesHub, `# Methodologies\n\n> Frameworks and principles that guide how AI agents work in this organization.\n\n## Documents\n\n| Document | Description |\n|----------|-------------|\n| [agentic-knowledge-base.md](./agentic-knowledge-base.md) | AKB — the file-based knowledge protocol for AI agents |\n\n---\n\n*Managed by: All*\n`);
428
+ created.push('knowledge/methodologies/methodologies.md');
429
+ }
430
+ const akbDoc = path.join(root, 'knowledge', 'methodologies', 'agentic-knowledge-base.md');
431
+ if (!fs.existsSync(akbDoc)) {
432
+ fs.writeFileSync(akbDoc, AKB_METHODOLOGY_CONTENT);
433
+ created.push('knowledge/methodologies/agentic-knowledge-base.md');
434
+ }
435
+
436
+ // Set default appearances for team roles
437
+ if (config.team !== 'custom') {
438
+ const roles = loadTeam(config.team);
439
+ const appearances: Record<string, CharacterAppearance> = {};
440
+ for (const role of roles) {
441
+ const def = DEFAULT_ROLE_APPEARANCES[role.id];
442
+ if (def) appearances[role.id] = def;
443
+ }
444
+ if (Object.keys(appearances).length > 0) {
445
+ mergePreferences(root, { appearances });
446
+ }
447
+ }
448
+
449
+ // Brownfield: note existing project path
450
+ if (config.existingProjectPath) {
451
+ const targetDir = path.join(root, 'knowledge', 'projects', 'existing');
452
+ if (!fs.existsSync(targetDir)) {
453
+ fs.mkdirSync(targetDir, { recursive: true });
454
+ fs.writeFileSync(
455
+ path.join(targetDir, 'prd.md'),
456
+ `# Existing Project\n\nImported from: ${config.existingProjectPath}\n`
457
+ );
458
+ created.push('knowledge/projects/existing/');
459
+ }
460
+ }
461
+
462
+ // Extra knowledge paths
463
+ if (config.knowledgePaths?.length) {
464
+ const knowledgeMd = path.join(root, 'knowledge', 'knowledge.md');
465
+ let content = fs.readFileSync(knowledgeMd, 'utf-8');
466
+ content += '\n## External Knowledge Sources\n\n';
467
+ for (const kp of config.knowledgePaths) {
468
+ content += `- ${kp}\n`;
469
+ }
470
+ fs.writeFileSync(knowledgeMd, content);
471
+ }
472
+
473
+ // Auto-init git for AKB
474
+ const gitDir = path.join(root, '.git');
475
+ if (!fs.existsSync(gitDir)) {
476
+ try {
477
+ execSync('git init', { cwd: root, stdio: 'pipe' });
478
+ execSync('git add -A', { cwd: root, stdio: 'pipe' });
479
+ execSync('git commit -m "Initial commit by Tycono"', { cwd: root, stdio: 'pipe' });
480
+ } catch { /* ignore — git may not be installed */ }
481
+ }
482
+
483
+ return created;
484
+ }
485
+
486
+ function createRole(root: string, role: TeamRole): void {
487
+ const roleDir = path.join(root, 'knowledge', 'roles', role.id);
488
+ const skillDir = path.join(root, 'knowledge', '.claude', 'skills', role.id);
489
+ const journalDir = path.join(roleDir, 'journal');
490
+
491
+ fs.mkdirSync(roleDir, { recursive: true });
492
+ fs.mkdirSync(journalDir, { recursive: true });
493
+ fs.mkdirSync(skillDir, { recursive: true });
494
+
495
+ // Build role.yaml with skills field
496
+ // Use block scalar (|-) for persona to safely handle embedded quotes
497
+ const personaLines = role.persona.split('\n').map((l, i) => i === 0 ? ` ${l}` : ` ${l}`).join('\n');
498
+ const yamlLines = [
499
+ `id: ${role.id}`,
500
+ `name: "${role.name}"`,
501
+ `level: ${role.level}`,
502
+ `reports_to: ${role.reportsTo}`,
503
+ `persona: |-`,
504
+ personaLines,
505
+ ];
506
+
507
+ if (role.defaultSkills?.length) {
508
+ yamlLines.push('skills:');
509
+ for (const skill of role.defaultSkills) {
510
+ yamlLines.push(` - ${skill}`);
511
+ }
512
+ }
513
+
514
+ yamlLines.push(
515
+ 'authority:',
516
+ ' autonomous:',
517
+ ' - Implementation within assigned scope',
518
+ ' needs_approval:',
519
+ ' - Architecture changes',
520
+ 'knowledge:',
521
+ ' reads:',
522
+ ' - projects/',
523
+ ' writes:',
524
+ ' - projects/',
525
+ 'reports:',
526
+ ' daily: standup',
527
+ ' weekly: summary',
528
+ );
529
+
530
+ fs.writeFileSync(path.join(roleDir, 'role.yaml'), yamlLines.join('\n') + '\n');
531
+
532
+ const profile = `# ${role.name}\n\n> ${role.persona}\n\n| Item | Value |\n|------|-------|\n| ID | ${role.id} |\n| Level | ${role.level} |\n| Reports To | ${role.reportsTo} |\n`;
533
+ fs.writeFileSync(path.join(roleDir, 'profile.md'), profile);
534
+
535
+ const skill = `# ${role.name} Skills\n\nSkill definitions for the ${role.name} role.\n`;
536
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skill);
537
+
538
+ // Append to roles.md
539
+ const rolesHubPath = path.join(root, 'knowledge', 'roles', 'roles.md');
540
+ if (fs.existsSync(rolesHubPath)) {
541
+ const hubContent = fs.readFileSync(rolesHubPath, 'utf-8');
542
+ const row = `| ${role.name} | ${role.id} | ${role.level} | ${role.reportsTo} | Active |`;
543
+ fs.writeFileSync(rolesHubPath, hubContent.trimEnd() + '\n' + row + '\n');
544
+ }
545
+
546
+ }