tycono 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/src/create-server.ts +9 -3
- package/src/api/src/engine/context-assembler.ts +25 -6
- package/src/api/src/engine/role-lifecycle.ts +0 -34
- package/src/api/src/routes/knowledge.ts +13 -13
- package/src/api/src/routes/setup.ts +3 -2
- package/src/api/src/services/activity-stream.ts +8 -5
- package/src/api/src/services/activity-tracker.ts +9 -6
- package/src/api/src/services/claude-md-manager.ts +93 -0
- package/src/api/src/services/file-reader.ts +7 -1
- package/src/api/src/services/scaffold.ts +24 -14
- package/src/api/src/services/session-store.ts +19 -8
- package/templates/CLAUDE.md.tmpl +18 -13
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ import { skillsRouter } from './routes/skills.js';
|
|
|
32
32
|
import { importKnowledge } from './services/knowledge-importer.js';
|
|
33
33
|
import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
|
|
34
34
|
import { readConfig } from './services/company-config.js';
|
|
35
|
+
import { ensureClaudeMd } from './services/claude-md-manager.js';
|
|
35
36
|
|
|
36
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
37
38
|
const __dirname = path.dirname(__filename);
|
|
@@ -102,7 +103,11 @@ function handleImportKnowledge(req: http.IncomingMessage, res: http.ServerRespon
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export function createHttpServer(): http.Server {
|
|
105
|
-
|
|
106
|
+
// Only cleanup/ensure if a company is already initialized (avoid creating dirs in CWD)
|
|
107
|
+
if (COMPANY_ROOT && fs.existsSync(path.join(COMPANY_ROOT, 'CLAUDE.md'))) {
|
|
108
|
+
cleanupStaleActivities();
|
|
109
|
+
ensureClaudeMd(COMPANY_ROOT);
|
|
110
|
+
}
|
|
106
111
|
|
|
107
112
|
const app = createExpressApp();
|
|
108
113
|
|
|
@@ -166,8 +171,9 @@ export function createExpressApp(): express.Application {
|
|
|
166
171
|
let companyName: string | null = null;
|
|
167
172
|
if (initialized) {
|
|
168
173
|
try {
|
|
169
|
-
|
|
170
|
-
const
|
|
174
|
+
// Read company name from company/company.md (user-owned data)
|
|
175
|
+
const companyMdPath = path.join(COMPANY_ROOT, 'company', 'company.md');
|
|
176
|
+
const content = fs.readFileSync(companyMdPath, 'utf-8');
|
|
171
177
|
const match = content.match(/^#\s+(.+)/m);
|
|
172
178
|
if (match) companyName = match[1].trim();
|
|
173
179
|
} catch { /* ignore */ }
|
|
@@ -164,14 +164,33 @@ export function assembleContext(
|
|
|
164
164
|
/* ─── Section Builders ───────────────────────── */
|
|
165
165
|
|
|
166
166
|
function loadCompanyRules(companyRoot: string): string | null {
|
|
167
|
+
const parts: string[] = [];
|
|
168
|
+
|
|
169
|
+
// 1. System rules (CLAUDE.md — Tycono managed)
|
|
167
170
|
const claudeMdPath = path.join(companyRoot, 'CLAUDE.md');
|
|
168
|
-
if (
|
|
171
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
172
|
+
parts.push(fs.readFileSync(claudeMdPath, 'utf-8'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. User custom rules (.tycono/custom-rules.md — user owned)
|
|
176
|
+
const customPath = path.join(companyRoot, '.tycono', 'custom-rules.md');
|
|
177
|
+
if (fs.existsSync(customPath)) {
|
|
178
|
+
const custom = fs.readFileSync(customPath, 'utf-8').trim();
|
|
179
|
+
if (custom) {
|
|
180
|
+
parts.push('---\n\n## Company Custom Rules\n\n' + custom);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Company info (company/company.md — user owned)
|
|
185
|
+
const companyMdPath = path.join(companyRoot, 'company', 'company.md');
|
|
186
|
+
if (fs.existsSync(companyMdPath)) {
|
|
187
|
+
const companyInfo = fs.readFileSync(companyMdPath, 'utf-8').trim();
|
|
188
|
+
if (companyInfo) {
|
|
189
|
+
parts.push('---\n\n## Company Info\n\n' + companyInfo);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
169
192
|
|
|
170
|
-
|
|
171
|
-
// Hub-first principle, and other navigation info that roles need to work effectively.
|
|
172
|
-
// Previously we extracted only AKB + Git rules, but that stripped out the most
|
|
173
|
-
// practically useful parts (routing table, folder structure, skill principle).
|
|
174
|
-
return fs.readFileSync(claudeMdPath, 'utf-8');
|
|
193
|
+
return parts.length > 0 ? parts.join('\n\n') : null;
|
|
175
194
|
}
|
|
176
195
|
|
|
177
196
|
function buildOrgContextSection(orgTree: OrgTree, node: OrgNode): string {
|
|
@@ -99,9 +99,6 @@ export class RoleLifecycleManager {
|
|
|
99
99
|
|
|
100
100
|
// 5. Update roles.md Hub
|
|
101
101
|
this.addToRolesHub(def);
|
|
102
|
-
|
|
103
|
-
// 6. Update CLAUDE.md org table
|
|
104
|
-
this.addToClaudeMdOrgTable(def);
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
/**
|
|
@@ -160,8 +157,6 @@ export class RoleLifecycleManager {
|
|
|
160
157
|
|
|
161
158
|
// Remove from roles.md Hub
|
|
162
159
|
this.removeFromRolesHub(id);
|
|
163
|
-
// Remove from CLAUDE.md org table
|
|
164
|
-
this.removeFromClaudeMdOrgTable(id);
|
|
165
160
|
}
|
|
166
161
|
|
|
167
162
|
/**
|
|
@@ -337,25 +332,6 @@ ${def.authority.needsApproval.map((a) => `- ${a}`).join('\n')}
|
|
|
337
332
|
fs.writeFileSync(hubPath, updatedContent);
|
|
338
333
|
}
|
|
339
334
|
|
|
340
|
-
private addToClaudeMdOrgTable(def: RoleDefinition): void {
|
|
341
|
-
const claudeMdPath = path.join(this.companyRoot, 'CLAUDE.md');
|
|
342
|
-
if (!fs.existsSync(claudeMdPath)) return;
|
|
343
|
-
|
|
344
|
-
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
345
|
-
if (content.includes(`| **${def.name}**`)) {
|
|
346
|
-
return; // Already exists
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const row = `| **${def.name}** | AI (${def.id}) | ${def.level} | ${def.reportsTo.toUpperCase()} | Active |`;
|
|
350
|
-
|
|
351
|
-
const orgSectionMatch = content.match(/## (?:조직|Organization)[\s\S]*?\n(\|[^\n]*\n)+/);
|
|
352
|
-
if (orgSectionMatch) {
|
|
353
|
-
const insertPos = (orgSectionMatch.index ?? 0) + orgSectionMatch[0].length;
|
|
354
|
-
const updated = content.slice(0, insertPos) + row + '\n' + content.slice(insertPos);
|
|
355
|
-
fs.writeFileSync(claudeMdPath, updated);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
335
|
private removeFromRolesHub(id: string): void {
|
|
360
336
|
const hubPath = path.join(this.companyRoot, 'roles', 'roles.md');
|
|
361
337
|
if (!fs.existsSync(hubPath)) return;
|
|
@@ -369,16 +345,6 @@ ${def.authority.needsApproval.map((a) => `- ${a}`).join('\n')}
|
|
|
369
345
|
fs.writeFileSync(hubPath, lines.join('\n'));
|
|
370
346
|
}
|
|
371
347
|
|
|
372
|
-
private removeFromClaudeMdOrgTable(id: string): void {
|
|
373
|
-
const claudeMdPath = path.join(this.companyRoot, 'CLAUDE.md');
|
|
374
|
-
if (!fs.existsSync(claudeMdPath)) return;
|
|
375
|
-
|
|
376
|
-
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
377
|
-
const lines = content.split('\n').filter((line) => {
|
|
378
|
-
return !line.includes(`(${id})`) || !line.startsWith('|');
|
|
379
|
-
});
|
|
380
|
-
fs.writeFileSync(claudeMdPath, lines.join('\n'));
|
|
381
|
-
}
|
|
382
348
|
}
|
|
383
349
|
|
|
384
350
|
/* ─── Helpers ──────────────────────────────── */
|
|
@@ -13,8 +13,8 @@ import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
|
13
13
|
|
|
14
14
|
export const knowledgeRouter = Router();
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
function knowledgeDir(): string { return path.join(COMPANY_ROOT, 'knowledge'); }
|
|
17
|
+
function companyRoot(): string { return COMPANY_ROOT; }
|
|
18
18
|
|
|
19
19
|
/* ─── Helpers ─────────────────────────────────────── */
|
|
20
20
|
|
|
@@ -61,13 +61,13 @@ function inferCategory(filePath: string, tags: string[]): string {
|
|
|
61
61
|
|
|
62
62
|
knowledgeRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
|
|
63
63
|
try {
|
|
64
|
-
if (!fs.existsSync(companyRoot)) {
|
|
64
|
+
if (!fs.existsSync(companyRoot())) {
|
|
65
65
|
res.json([]);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const files = glob.sync('**/*.{md,html}', {
|
|
70
|
-
cwd: companyRoot,
|
|
70
|
+
cwd: companyRoot(),
|
|
71
71
|
ignore: [
|
|
72
72
|
'node_modules/**', '.claude/**', '.obsidian/**', '.tycono/**', '.git/**',
|
|
73
73
|
'**/node_modules/**',
|
|
@@ -82,7 +82,7 @@ knowledgeRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
|
|
|
82
82
|
.sort();
|
|
83
83
|
|
|
84
84
|
const docs = files.map((f) => {
|
|
85
|
-
const absPath = path.join(companyRoot, f);
|
|
85
|
+
const absPath = path.join(companyRoot(), f);
|
|
86
86
|
let raw = '';
|
|
87
87
|
try { raw = fs.readFileSync(absPath, 'utf-8'); } catch { return null; }
|
|
88
88
|
|
|
@@ -165,9 +165,9 @@ knowledgeRouter.post('/', (req: Request, res: Response, next: NextFunction) => {
|
|
|
165
165
|
// Sanitize filename
|
|
166
166
|
const safeName = filename.replace(/[^a-zA-Z0-9가-힣_\-. ]/g, '').replace(/\s+/g, '-');
|
|
167
167
|
const fullName = safeName.endsWith('.md') ? safeName : `${safeName}.md`;
|
|
168
|
-
const absPath = path.join(knowledgeDir, fullName);
|
|
168
|
+
const absPath = path.join(knowledgeDir(), fullName);
|
|
169
169
|
|
|
170
|
-
if (!absPath.startsWith(knowledgeDir)) {
|
|
170
|
+
if (!absPath.startsWith(knowledgeDir())) {
|
|
171
171
|
res.status(403).json({ error: 'Forbidden' });
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
@@ -206,8 +206,8 @@ knowledgeRouter.put('/{*path}', (req: Request, res: Response, next: NextFunction
|
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
const absPath = path.join(companyRoot, docId);
|
|
210
|
-
if (!absPath.startsWith(companyRoot)) {
|
|
209
|
+
const absPath = path.join(companyRoot(), docId);
|
|
210
|
+
if (!absPath.startsWith(companyRoot())) {
|
|
211
211
|
res.status(403).json({ error: 'Forbidden' });
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
@@ -248,8 +248,8 @@ knowledgeRouter.delete('/{*path}', (req: Request, res: Response, next: NextFunct
|
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
const absPath = path.join(companyRoot, docId);
|
|
252
|
-
if (!absPath.startsWith(companyRoot)) {
|
|
251
|
+
const absPath = path.join(companyRoot(), docId);
|
|
252
|
+
if (!absPath.startsWith(companyRoot())) {
|
|
253
253
|
res.status(403).json({ error: 'Forbidden' });
|
|
254
254
|
return;
|
|
255
255
|
}
|
|
@@ -277,10 +277,10 @@ knowledgeRouter.get('/{*path}', (req: Request, res: Response, next: NextFunction
|
|
|
277
277
|
return;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
const absPath = path.join(knowledgeDir, docId);
|
|
280
|
+
const absPath = path.join(knowledgeDir(), docId);
|
|
281
281
|
|
|
282
282
|
// Security: ensure path stays within knowledgeDir
|
|
283
|
-
if (!absPath.startsWith(knowledgeDir)) {
|
|
283
|
+
if (!absPath.startsWith(knowledgeDir())) {
|
|
284
284
|
res.status(403).json({ error: 'Forbidden' });
|
|
285
285
|
return;
|
|
286
286
|
}
|
|
@@ -16,6 +16,7 @@ import { AnthropicProvider, type LLMProvider } from '../engine/llm-adapter.js';
|
|
|
16
16
|
import { jobManager } from '../services/job-manager.js';
|
|
17
17
|
import { applyConfig, readConfig, writeConfig } from '../services/company-config.js';
|
|
18
18
|
import { mergePreferences } from '../services/preferences.js';
|
|
19
|
+
import { setCompanyRoot } from '../services/file-reader.js';
|
|
19
20
|
|
|
20
21
|
export const setupRouter = Router();
|
|
21
22
|
|
|
@@ -122,7 +123,7 @@ setupRouter.post('/scaffold', (req, res) => {
|
|
|
122
123
|
try {
|
|
123
124
|
const created = scaffold(config);
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
setCompanyRoot(projectRoot);
|
|
126
127
|
// Load config.json written by scaffold and apply to process.env
|
|
127
128
|
const scaffoldConfig = applyConfig(projectRoot);
|
|
128
129
|
// Save codeRoot if provided
|
|
@@ -217,7 +218,7 @@ setupRouter.post('/connect-akb', (req, res) => {
|
|
|
217
218
|
if (match) companyName = match[1].trim();
|
|
218
219
|
} catch { /* ignore */ }
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
setCompanyRoot(resolved);
|
|
221
222
|
|
|
222
223
|
// Load existing config.json if present
|
|
223
224
|
const config = readConfig(resolved);
|
|
@@ -24,16 +24,19 @@ export interface ActivityEvent {
|
|
|
24
24
|
|
|
25
25
|
/* ─── Constants ──────────────────────────── */
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
function streamsDir(): string {
|
|
28
|
+
return path.join(COMPANY_ROOT, 'operations', 'activity-streams');
|
|
29
|
+
}
|
|
28
30
|
|
|
29
31
|
function ensureDir(): void {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const dir = streamsDir();
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
function streamPath(jobId: string): string {
|
|
36
|
-
return path.join(
|
|
39
|
+
return path.join(streamsDir(), `${jobId}.jsonl`);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/* ─── Subscriber type ────────────────────── */
|
|
@@ -149,7 +152,7 @@ export class ActivityStream {
|
|
|
149
152
|
/** List all stream files (job IDs) */
|
|
150
153
|
static listAll(): string[] {
|
|
151
154
|
ensureDir();
|
|
152
|
-
return fs.readdirSync(
|
|
155
|
+
return fs.readdirSync(streamsDir())
|
|
153
156
|
.filter(f => f.endsWith('.jsonl'))
|
|
154
157
|
.map(f => f.replace('.jsonl', ''));
|
|
155
158
|
}
|
|
@@ -2,7 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { COMPANY_ROOT } from './file-reader.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
function activityDir(): string {
|
|
6
|
+
return path.join(COMPANY_ROOT, 'operations', 'activity');
|
|
7
|
+
}
|
|
6
8
|
|
|
7
9
|
export interface RoleActivity {
|
|
8
10
|
roleId: string;
|
|
@@ -14,12 +16,13 @@ export interface RoleActivity {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function activityPath(roleId: string): string {
|
|
17
|
-
return path.join(
|
|
19
|
+
return path.join(activityDir(), `${roleId}.json`);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function ensureDir(): void {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const dir = activityDir();
|
|
24
|
+
if (!fs.existsSync(dir)) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -71,10 +74,10 @@ export function getActivity(roleId: string): RoleActivity | null {
|
|
|
71
74
|
|
|
72
75
|
export function getAllActivities(): RoleActivity[] {
|
|
73
76
|
ensureDir();
|
|
74
|
-
const files = fs.readdirSync(
|
|
77
|
+
const files = fs.readdirSync(activityDir()).filter(f => f.endsWith('.json'));
|
|
75
78
|
return files.map(f => {
|
|
76
79
|
try {
|
|
77
|
-
return JSON.parse(fs.readFileSync(path.join(
|
|
80
|
+
return JSON.parse(fs.readFileSync(path.join(activityDir(), f), 'utf-8'));
|
|
78
81
|
} catch {
|
|
79
82
|
return null;
|
|
80
83
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-md-manager.ts — CLAUDE.md lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* CLAUDE.md is 100% Tycono-managed. This module handles:
|
|
5
|
+
* - Version tracking via .tycono/rules-version
|
|
6
|
+
* - Auto-regeneration on version mismatch (server startup)
|
|
7
|
+
* - Backup of pre-existing CLAUDE.md (first time only)
|
|
8
|
+
* - Stub creation for .tycono/custom-rules.md
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '../../../../templates');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the current package version from package.json
|
|
20
|
+
*/
|
|
21
|
+
function getPackageVersion(): string {
|
|
22
|
+
const pkgPath = path.resolve(__dirname, '../../../../package.json');
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
25
|
+
return pkg.version || '0.0.0';
|
|
26
|
+
} catch {
|
|
27
|
+
return '0.0.0';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate CLAUDE.md content from template with version marker
|
|
33
|
+
*/
|
|
34
|
+
function generateClaudeMd(version: string): string {
|
|
35
|
+
const tmplPath = path.join(TEMPLATES_DIR, 'CLAUDE.md.tmpl');
|
|
36
|
+
const template = fs.readFileSync(tmplPath, 'utf-8');
|
|
37
|
+
return template.replaceAll('{{VERSION}}', version);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ensure CLAUDE.md is up-to-date with the current package version.
|
|
42
|
+
*
|
|
43
|
+
* Called on server startup. Compares .tycono/rules-version with package version.
|
|
44
|
+
* If different, regenerates CLAUDE.md from template (safe because CLAUDE.md
|
|
45
|
+
* contains 0% user data — all user customization is in .tycono/custom-rules.md).
|
|
46
|
+
*/
|
|
47
|
+
export function ensureClaudeMd(companyRoot: string): void {
|
|
48
|
+
const tyconoDir = path.join(companyRoot, '.tycono');
|
|
49
|
+
const rulesVersionPath = path.join(tyconoDir, 'rules-version');
|
|
50
|
+
const claudeMdPath = path.join(companyRoot, 'CLAUDE.md');
|
|
51
|
+
const customRulesPath = path.join(tyconoDir, 'custom-rules.md');
|
|
52
|
+
const backupPath = path.join(tyconoDir, 'CLAUDE.md.backup');
|
|
53
|
+
|
|
54
|
+
// Skip if not initialized (no .tycono/ directory)
|
|
55
|
+
if (!fs.existsSync(tyconoDir)) return;
|
|
56
|
+
|
|
57
|
+
const currentVersion = getPackageVersion();
|
|
58
|
+
|
|
59
|
+
// Read stored version
|
|
60
|
+
let storedVersion = '0.0.0';
|
|
61
|
+
if (fs.existsSync(rulesVersionPath)) {
|
|
62
|
+
storedVersion = fs.readFileSync(rulesVersionPath, 'utf-8').trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Skip if already up-to-date
|
|
66
|
+
if (storedVersion === currentVersion) return;
|
|
67
|
+
|
|
68
|
+
// Backup existing CLAUDE.md (first time only — don't overwrite previous backup)
|
|
69
|
+
if (fs.existsSync(claudeMdPath) && !fs.existsSync(backupPath)) {
|
|
70
|
+
fs.copyFileSync(claudeMdPath, backupPath);
|
|
71
|
+
console.log(`[CLAUDE.md] Backed up existing CLAUDE.md to .tycono/CLAUDE.md.backup`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Regenerate CLAUDE.md from template
|
|
75
|
+
const content = generateClaudeMd(currentVersion);
|
|
76
|
+
fs.writeFileSync(claudeMdPath, content);
|
|
77
|
+
|
|
78
|
+
// Update rules-version
|
|
79
|
+
fs.writeFileSync(rulesVersionPath, currentVersion);
|
|
80
|
+
|
|
81
|
+
// Create custom-rules.md stub if not exists
|
|
82
|
+
if (!fs.existsSync(customRulesPath)) {
|
|
83
|
+
fs.writeFileSync(customRulesPath, `# Custom Rules
|
|
84
|
+
|
|
85
|
+
> Company-specific rules, constraints, and processes.
|
|
86
|
+
> This file is owned by you — Tycono will never overwrite it.
|
|
87
|
+
|
|
88
|
+
<!-- Add your custom rules below -->
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`[CLAUDE.md] System rules updated to v${currentVersion} (was v${storedVersion})`);
|
|
93
|
+
}
|
|
@@ -14,7 +14,13 @@ function findCompanyRoot(): string {
|
|
|
14
14
|
return process.cwd();
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export let COMPANY_ROOT = findCompanyRoot();
|
|
18
|
+
|
|
19
|
+
/** Update COMPANY_ROOT at runtime (e.g. after scaffold picks a new location) */
|
|
20
|
+
export function setCompanyRoot(root: string): void {
|
|
21
|
+
COMPANY_ROOT = root;
|
|
22
|
+
process.env.COMPANY_ROOT = root;
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
function resolve(...segments: string[]): string {
|
|
20
26
|
return path.resolve(COMPANY_ROOT, ...segments);
|
|
@@ -13,6 +13,16 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
13
13
|
const __dirname = path.dirname(__filename);
|
|
14
14
|
const TEMPLATES_DIR = path.resolve(__dirname, '../../../../templates');
|
|
15
15
|
|
|
16
|
+
function getPackageVersion(): string {
|
|
17
|
+
const pkgPath = path.resolve(__dirname, '../../../../package.json');
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
20
|
+
return pkg.version || '0.0.0';
|
|
21
|
+
} catch {
|
|
22
|
+
return '0.0.0';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export interface ScaffoldConfig {
|
|
17
27
|
companyName: string;
|
|
18
28
|
description: string;
|
|
@@ -155,11 +165,23 @@ export function scaffold(config: ScaffoldConfig): string[] {
|
|
|
155
165
|
created.push(dir + '/');
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
// Write CLAUDE.md
|
|
168
|
+
// Write CLAUDE.md (no variable substitution — 100% Tycono managed)
|
|
159
169
|
const claudeTmpl = loadTemplate('CLAUDE.md.tmpl');
|
|
160
|
-
|
|
170
|
+
const pkgVersion = getPackageVersion();
|
|
171
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), claudeTmpl.replaceAll('{{VERSION}}', pkgVersion));
|
|
161
172
|
created.push('CLAUDE.md');
|
|
162
173
|
|
|
174
|
+
// Write .tycono/rules-version
|
|
175
|
+
fs.writeFileSync(path.join(root, '.tycono', 'rules-version'), pkgVersion);
|
|
176
|
+
created.push('.tycono/rules-version');
|
|
177
|
+
|
|
178
|
+
// Write .tycono/custom-rules.md (empty stub — user owned)
|
|
179
|
+
const customRulesPath = path.join(root, '.tycono', 'custom-rules.md');
|
|
180
|
+
if (!fs.existsSync(customRulesPath)) {
|
|
181
|
+
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`);
|
|
182
|
+
created.push('.tycono/custom-rules.md');
|
|
183
|
+
}
|
|
184
|
+
|
|
163
185
|
// Write company/company.md
|
|
164
186
|
const companyTmpl = loadTemplate('company.md.tmpl');
|
|
165
187
|
fs.writeFileSync(path.join(root, 'company', 'company.md'), renderTemplate(companyTmpl, vars));
|
|
@@ -310,16 +332,4 @@ function createRole(root: string, role: TeamRole): void {
|
|
|
310
332
|
fs.writeFileSync(rolesHubPath, hubContent.trimEnd() + '\n' + row + '\n');
|
|
311
333
|
}
|
|
312
334
|
|
|
313
|
-
// Append to CLAUDE.md org table
|
|
314
|
-
const claudeMdPath = path.join(root, 'CLAUDE.md');
|
|
315
|
-
if (fs.existsSync(claudeMdPath)) {
|
|
316
|
-
const claudeContent = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
317
|
-
const orgRow = `| **${role.name}** | AI (${role.id}) | ${role.level} | ${role.reportsTo} | Active |`;
|
|
318
|
-
const orgMatch = claudeContent.match(/(## Organization[\s\S]*?\n(\|[^\n]*\n)+)/);
|
|
319
|
-
if (orgMatch) {
|
|
320
|
-
const insertPos = (orgMatch.index ?? 0) + orgMatch[0].length;
|
|
321
|
-
const updated = claudeContent.slice(0, insertPos) + orgRow + '\n' + claudeContent.slice(insertPos);
|
|
322
|
-
fs.writeFileSync(claudeMdPath, updated);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
335
|
}
|
|
@@ -26,16 +26,19 @@ export interface Session {
|
|
|
26
26
|
|
|
27
27
|
/* ─── Session directory ─────────────────── */
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
function sessionsDir(): string {
|
|
30
|
+
return path.join(COMPANY_ROOT, 'operations', 'sessions');
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
function ensureDir(): void {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const dir = sessionsDir();
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
function sessionPath(id: string): string {
|
|
38
|
-
return path.join(
|
|
41
|
+
return path.join(sessionsDir(), `${id}.json`);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/* ─── Debounced write ───────────────────── */
|
|
@@ -69,21 +72,27 @@ const cache = new Map<string, Session>();
|
|
|
69
72
|
|
|
70
73
|
function loadAll(): void {
|
|
71
74
|
ensureDir();
|
|
72
|
-
const files = fs.readdirSync(
|
|
75
|
+
const files = fs.readdirSync(sessionsDir()).filter((f) => f.endsWith('.json'));
|
|
73
76
|
for (const file of files) {
|
|
74
77
|
try {
|
|
75
|
-
const data = JSON.parse(fs.readFileSync(path.join(
|
|
78
|
+
const data = JSON.parse(fs.readFileSync(path.join(sessionsDir(), file), 'utf-8')) as Session;
|
|
76
79
|
cache.set(data.id, data);
|
|
77
80
|
} catch { /* skip corrupted */ }
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
//
|
|
82
|
-
|
|
84
|
+
// Lazy load: defer until first access (avoids creating dirs in CWD before scaffold)
|
|
85
|
+
let loaded = false;
|
|
86
|
+
function ensureLoaded(): void {
|
|
87
|
+
if (loaded) return;
|
|
88
|
+
loaded = true;
|
|
89
|
+
loadAll();
|
|
90
|
+
}
|
|
83
91
|
|
|
84
92
|
/* ─── Public API ────────────────────────── */
|
|
85
93
|
|
|
86
94
|
export function createSession(roleId: string, mode: 'talk' | 'do' = 'talk'): Session {
|
|
95
|
+
ensureLoaded();
|
|
87
96
|
const id = `ses-${roleId}-${Date.now()}`;
|
|
88
97
|
const now = new Date().toISOString();
|
|
89
98
|
const session: Session = {
|
|
@@ -102,10 +111,12 @@ export function createSession(roleId: string, mode: 'talk' | 'do' = 'talk'): Ses
|
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
export function getSession(id: string): Session | undefined {
|
|
114
|
+
ensureLoaded();
|
|
105
115
|
return cache.get(id);
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
export function listSessions(): Omit<Session, 'messages'>[] {
|
|
119
|
+
ensureLoaded();
|
|
109
120
|
return Array.from(cache.values())
|
|
110
121
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
111
122
|
.map(({ messages: _, ...meta }) => meta);
|
package/templates/CLAUDE.md.tmpl
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Company Rules
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Organization
|
|
8
|
-
|
|
9
|
-
| Role | Assignee | Level | Reports To | Status |
|
|
10
|
-
|------|----------|-------|------------|--------|
|
|
3
|
+
> Powered by [Tycono](https://tycono.ai) — AI Company Operating Platform
|
|
11
4
|
|
|
12
5
|
---
|
|
13
6
|
|
|
@@ -47,6 +40,12 @@
|
|
|
47
40
|
Every folder has a Hub file (`{folder-name}.md`) as its entry point.
|
|
48
41
|
Check the Task Routing table above to find the right Hub, then read it first.
|
|
49
42
|
|
|
43
|
+
### Custom Rules (CRITICAL)
|
|
44
|
+
|
|
45
|
+
> ⛔ **Before starting work, check if `.tycono/custom-rules.md` exists and read it.**
|
|
46
|
+
> This file contains company-specific rules, constraints, and processes.
|
|
47
|
+
> If the file doesn't exist, ignore this section.
|
|
48
|
+
|
|
50
49
|
### Knowledge Gate
|
|
51
50
|
|
|
52
51
|
> **Before creating a new document, search existing docs first.**
|
|
@@ -162,10 +161,15 @@ After completing any task:
|
|
|
162
161
|
## Folder Structure
|
|
163
162
|
|
|
164
163
|
```
|
|
165
|
-
{
|
|
166
|
-
+-- CLAUDE.md <- AI entry point
|
|
164
|
+
{company}/
|
|
165
|
+
+-- CLAUDE.md <- AI entry point (Tycono managed)
|
|
166
|
+
+-- .tycono/
|
|
167
|
+
| +-- config.json <- Engine settings
|
|
168
|
+
| +-- preferences.json <- UI preferences
|
|
169
|
+
| +-- custom-rules.md <- Company custom rules (user owned)
|
|
170
|
+
| +-- rules-version <- Current CLAUDE.md version
|
|
167
171
|
+-- company/
|
|
168
|
-
| +-- company.md <- Mission, vision
|
|
172
|
+
| +-- company.md <- Mission, vision, company info
|
|
169
173
|
+-- roles/
|
|
170
174
|
| +-- roles.md <- Role listing (Hub)
|
|
171
175
|
| +-- {role-id}/
|
|
@@ -189,4 +193,5 @@ After completing any task:
|
|
|
189
193
|
|
|
190
194
|
---
|
|
191
195
|
|
|
192
|
-
|
|
196
|
+
<!-- tycono:managed v{{VERSION}} — This file is managed by Tycono. Do not edit manually. -->
|
|
197
|
+
<!-- Company-specific rules go in .tycono/custom-rules.md -->
|