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,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
|
+
}
|