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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- cleanupStaleActivities();
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
- const claudeMdPath = path.join(COMPANY_ROOT, 'CLAUDE.md');
170
- const content = fs.readFileSync(claudeMdPath, 'utf-8');
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 (!fs.existsSync(claudeMdPath)) return null;
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
- // Give the full CLAUDE.md it contains the routing table, folder structure,
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
- const knowledgeDir = path.join(COMPANY_ROOT, 'knowledge');
17
- const companyRoot = COMPANY_ROOT;
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
- process.env.COMPANY_ROOT = projectRoot;
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
- process.env.COMPANY_ROOT = resolved;
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
- const STREAMS_DIR = path.join(COMPANY_ROOT, 'operations', 'activity-streams');
27
+ function streamsDir(): string {
28
+ return path.join(COMPANY_ROOT, 'operations', 'activity-streams');
29
+ }
28
30
 
29
31
  function ensureDir(): void {
30
- if (!fs.existsSync(STREAMS_DIR)) {
31
- fs.mkdirSync(STREAMS_DIR, { recursive: true });
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(STREAMS_DIR, `${jobId}.jsonl`);
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(STREAMS_DIR)
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
- const ACTIVITY_DIR = path.join(COMPANY_ROOT, 'operations', 'activity');
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(ACTIVITY_DIR, `${roleId}.json`);
19
+ return path.join(activityDir(), `${roleId}.json`);
18
20
  }
19
21
 
20
22
  function ensureDir(): void {
21
- if (!fs.existsSync(ACTIVITY_DIR)) {
22
- fs.mkdirSync(ACTIVITY_DIR, { recursive: true });
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(ACTIVITY_DIR).filter(f => f.endsWith('.json'));
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(ACTIVITY_DIR, f), 'utf-8'));
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 const COMPANY_ROOT = findCompanyRoot();
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
- fs.writeFileSync(path.join(root, 'CLAUDE.md'), renderTemplate(claudeTmpl, vars));
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
- const SESSIONS_DIR = path.join(COMPANY_ROOT, 'operations', 'sessions');
29
+ function sessionsDir(): string {
30
+ return path.join(COMPANY_ROOT, 'operations', 'sessions');
31
+ }
30
32
 
31
33
  function ensureDir(): void {
32
- if (!fs.existsSync(SESSIONS_DIR)) {
33
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
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(SESSIONS_DIR, `${id}.json`);
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(SESSIONS_DIR).filter((f) => f.endsWith('.json'));
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(SESSIONS_DIR, file), 'utf-8')) as Session;
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
- // Load on startup
82
- loadAll();
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);
@@ -1,13 +1,6 @@
1
- # {{COMPANY_NAME}}
1
+ # Company Rules
2
2
 
3
- > {{DESCRIPTION}}
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
- {{COMPANY_NAME}}/
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
- *Powered by [tycono](https://github.com/seongsu-kang/the-company)*
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 -->