tycono 0.1.61 → 0.1.63

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.61",
3
+ "version": "0.1.63",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -247,7 +247,7 @@ export class ClaudeCliProvider implements LLMProvider {
247
247
  '-p',
248
248
  '--system-prompt', systemPrompt,
249
249
  '--model', this.model,
250
- '--max-turns', useTools ? '3' : '1',
250
+ '--max-turns', useTools ? '50' : '1',
251
251
  '--output-format', 'text',
252
252
  ...(useTools ? [
253
253
  '--tools', 'Read,Grep,Glob',
@@ -12,6 +12,7 @@ import os from 'node:os';
12
12
  import { scaffold, getAvailableTeams, loadTeam } from '../services/scaffold.js';
13
13
  import type { ScaffoldConfig } from '../services/scaffold.js';
14
14
  import { importKnowledge } from '../services/knowledge-importer.js';
15
+ import { gitInit } from '../services/git-save.js';
15
16
  import { AnthropicProvider, type LLMProvider } from '../engine/llm-adapter.js';
16
17
  import { jobManager } from '../services/job-manager.js';
17
18
  import { applyConfig, readConfig, writeConfig } from '../services/company-config.js';
@@ -136,7 +137,10 @@ setupRouter.post('/scaffold', (req, res) => {
136
137
  }
137
138
  jobManager.refreshRunner();
138
139
 
139
- res.json({ ok: true, companyName, projectRoot, created });
140
+ // Auto git init (graceful skip if git not installed)
141
+ const gitResult = gitInit(projectRoot);
142
+
143
+ res.json({ ok: true, companyName, projectRoot, created, git: gitResult });
140
144
  } catch (err) {
141
145
  const message = err instanceof Error ? err.message : 'Scaffold failed';
142
146
  res.status(500).json({ error: message });
@@ -26,7 +26,7 @@ export const speechRouter = Router();
26
26
  * AKB Tools — Let chat roles explore company knowledge
27
27
  * ══════════════════════════════════════════════════ */
28
28
 
29
- const MAX_TOOL_ROUNDS = 2;
29
+ const MAX_TOOL_ROUNDS = 50;
30
30
  const MAX_FILE_CHARS = 1500; // truncate large files
31
31
 
32
32
  const AKB_TOOLS: ToolDefinition[] = [
@@ -295,12 +295,24 @@ function buildCompanyContext(): string {
295
295
 
296
296
  /**
297
297
  * Build role-specific AKB context by pre-fetching relevant knowledge server-side.
298
- * Works with ANY engine (including claude-cli which doesn't support tool_use).
298
+ * This is the PRIMARY source of grounding for chat — must be rich enough that
299
+ * agents don't need to use tools (Haiku won't proactively search).
299
300
  */
300
301
  function buildRoleContext(roleId: string): string {
301
302
  const parts: string[] = [];
302
303
 
303
- // 1. Role's own journal (most recent 2 entries)
304
+ // 0. Role profile gives the agent its identity and work context
305
+ try {
306
+ const profilePath = path.join(COMPANY_ROOT, 'roles', roleId, 'profile.md');
307
+ if (fs.existsSync(profilePath)) {
308
+ const content = fs.readFileSync(profilePath, 'utf-8').trim();
309
+ if (content.length > 20) {
310
+ parts.push(`[Your Profile]\n${content.slice(0, 600)}`);
311
+ }
312
+ }
313
+ } catch { /* no profile */ }
314
+
315
+ // 1. Role's journal — latest entry with meaningful content (headers as summary)
304
316
  try {
305
317
  const journalDir = path.join(COMPANY_ROOT, 'roles', roleId, 'journal');
306
318
  if (fs.existsSync(journalDir)) {
@@ -310,49 +322,81 @@ function buildRoleContext(roleId: string): string {
310
322
  .slice(-2);
311
323
  for (const file of files) {
312
324
  const content = fs.readFileSync(path.join(journalDir, file), 'utf-8');
313
- // Extract title + first meaningful section (truncated)
325
+ // Extract all ### headers as work summary + first paragraph of each
326
+ const sections = content.match(/###\s+.+/g) ?? [];
314
327
  const title = content.match(/^#\s+(.+)/m)?.[1] ?? file;
315
- const body = content.split('\n').slice(1).join('\n').trim().slice(0, 400);
316
- parts.push(`[Your Journal: ${file}] ${title}\n${body}`);
328
+ const summary = sections.length > 0
329
+ ? sections.map(s => ` ${s.replace(/^###\s+/, '- ')}`).join('\n')
330
+ : content.split('\n').slice(1).join('\n').trim().slice(0, 300);
331
+ parts.push(`[Your Work Log: ${file}] ${title}\n${summary}`);
317
332
  }
318
333
  }
319
334
  } catch { /* no journal */ }
320
335
 
321
- // 2. Recent waves mentioning this role (last 3)
336
+ // 2. Current tasks assigned to this role (from all project tasks.md files)
337
+ try {
338
+ const projectsDir = path.join(COMPANY_ROOT, 'projects');
339
+ if (fs.existsSync(projectsDir)) {
340
+ const taskFiles = glob.sync('**/tasks.md', { cwd: projectsDir, absolute: false });
341
+ const roleTasks: string[] = [];
342
+ for (const tf of taskFiles.slice(0, 3)) {
343
+ const content = fs.readFileSync(path.join(projectsDir, tf), 'utf-8');
344
+ const rows = parseMarkdownTable(content);
345
+ const myTasks = rows.filter(r => {
346
+ const role = (r.role ?? r.Role ?? '').toLowerCase();
347
+ return role.includes(roleId);
348
+ });
349
+ for (const t of myTasks.slice(0, 5)) {
350
+ const id = t.id ?? t.ID ?? '';
351
+ const task = t.task ?? t.Task ?? t.title ?? '';
352
+ const status = t.status ?? t.Status ?? '';
353
+ if (task) roleTasks.push(`- ${id}: ${task} [${status}]`);
354
+ }
355
+ }
356
+ if (roleTasks.length > 0) {
357
+ parts.push(`[Your Assigned Tasks]\n${roleTasks.join('\n')}`);
358
+ }
359
+ }
360
+ } catch { /* no tasks */ }
361
+
362
+ // 3. Recent waves (broadened matching: roleId, role name, level keywords)
322
363
  try {
323
364
  const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
324
365
  if (fs.existsSync(wavesDir)) {
366
+ // Also get role name for matching
367
+ const tree = buildOrgTree(COMPANY_ROOT);
368
+ const node = tree.nodes.get(roleId);
369
+ const roleName = node?.name?.toLowerCase() ?? '';
370
+ const roleLevel = node?.level?.toLowerCase() ?? '';
371
+
325
372
  const waveFiles = fs.readdirSync(wavesDir)
326
373
  .filter(f => f.endsWith('.md'))
327
374
  .sort()
328
- .slice(-10); // scan last 10, pick up to 3 relevant
375
+ .slice(-10);
329
376
  const relevant: string[] = [];
330
377
  for (const file of waveFiles.reverse()) {
331
- if (relevant.length >= 3) break;
378
+ if (relevant.length >= 2) break;
332
379
  const content = fs.readFileSync(path.join(wavesDir, file), 'utf-8');
333
380
  const lower = content.toLowerCase();
334
- if (lower.includes(roleId) || lower.includes('all roles') || lower.includes('전체')) {
381
+ if (lower.includes(roleId) || lower.includes('all roles') || lower.includes('전체')
382
+ || (roleName && lower.includes(roleName))
383
+ || (roleLevel && lower.includes(roleLevel))) {
335
384
  const title = content.match(/^#\s+(.+)/m)?.[1] ?? file;
336
- const tldr = content.match(/TL;DR[\s\S]*?\n\n/)?.[0]?.trim() ?? '';
337
- const snippet = tldr || content.split('\n').slice(1, 6).join('\n').trim();
338
- relevant.push(`[Wave: ${file}] ${title}\n${snippet.slice(0, 300)}`);
385
+ const snippet = content.split('\n').slice(1, 8).join('\n').trim();
386
+ relevant.push(`[CEO Wave: ${file}] ${title}\n${snippet.slice(0, 400)}`);
339
387
  }
340
388
  }
341
389
  if (relevant.length > 0) parts.push(...relevant);
342
390
  }
343
391
  } catch { /* no waves */ }
344
392
 
345
- // 3. Recent standup (latest, only this role's section)
393
+ // 4. Recent standup (latest, this role's section)
346
394
  try {
347
395
  const standupDir = path.join(COMPANY_ROOT, 'operations', 'standup');
348
396
  if (fs.existsSync(standupDir)) {
349
- const standupFiles = fs.readdirSync(standupDir)
350
- .filter(f => f.endsWith('.md'))
351
- .sort()
352
- .slice(-1);
353
- for (const file of standupFiles) {
397
+ const files = fs.readdirSync(standupDir).filter(f => f.endsWith('.md')).sort().slice(-1);
398
+ for (const file of files) {
354
399
  const content = fs.readFileSync(path.join(standupDir, file), 'utf-8');
355
- // Try to extract this role's section from standup
356
400
  const rolePattern = new RegExp(`(## .*${roleId}.*|### .*${roleId}.*)([\\s\\S]*?)(?=\\n## |\\n### |$)`, 'i');
357
401
  const match = content.match(rolePattern);
358
402
  if (match) {
@@ -362,7 +406,7 @@ function buildRoleContext(roleId: string): string {
362
406
  }
363
407
  } catch { /* no standups */ }
364
408
 
365
- // 4. Recent decisions (last 2 approved — brief titles only, full list is in company context)
409
+ // 5. Recent decisions (last 3)
366
410
  try {
367
411
  const decisionsDir = path.join(COMPANY_ROOT, 'operations', 'decisions');
368
412
  if (fs.existsSync(decisionsDir)) {
@@ -374,8 +418,7 @@ function buildRoleContext(roleId: string): string {
374
418
  for (const file of files) {
375
419
  const content = fs.readFileSync(path.join(decisionsDir, file), 'utf-8');
376
420
  const title = content.match(/^#\s+(.+)/m)?.[1] ?? file;
377
- const summary = content.match(/## (?:Summary|TL;DR|요약)[\s\S]*?\n\n/)?.[0]?.trim() ?? '';
378
- decisions.push(`- ${title}${summary ? ': ' + summary.split('\n').slice(1).join(' ').trim().slice(0, 150) : ''}`);
421
+ decisions.push(`- ${title}`);
379
422
  }
380
423
  if (decisions.length > 0) {
381
424
  parts.push(`[Recent Decisions]\n${decisions.join('\n')}`);
@@ -383,28 +426,25 @@ function buildRoleContext(roleId: string): string {
383
426
  }
384
427
  } catch { /* no decisions */ }
385
428
 
386
- // 5. If sparse context, add architecture/tech-debt highlights as fallback
387
- if (parts.length < 2) {
388
- try {
389
- const techDebtPath = path.join(COMPANY_ROOT, 'architecture', 'tech-debt.md');
390
- if (fs.existsSync(techDebtPath)) {
391
- const tdContent = fs.readFileSync(techDebtPath, 'utf-8');
392
- // Extract active (non-fixed) items
393
- const rows = parseMarkdownTable(tdContent);
394
- const active = rows
395
- .filter(r => !(r.status ?? '').toLowerCase().includes('fixed') && !(r.status ?? '').toLowerCase().includes('done'))
396
- .slice(0, 3)
397
- .map(r => `- ${r.id ?? ''}: ${r.title ?? r.issue ?? ''} (${r.status ?? ''})`)
398
- .filter(s => s.length > 10);
399
- if (active.length > 0) {
400
- parts.push(`[Active Tech Debt]\n${active.join('\n')}`);
401
- }
429
+ // 6. Architecture highlights (always include not just fallback)
430
+ try {
431
+ const techDebtPath = path.join(COMPANY_ROOT, 'architecture', 'tech-debt.md');
432
+ if (fs.existsSync(techDebtPath)) {
433
+ const tdContent = fs.readFileSync(techDebtPath, 'utf-8');
434
+ const rows = parseMarkdownTable(tdContent);
435
+ const active = rows
436
+ .filter(r => !(r.status ?? '').toLowerCase().includes('fixed') && !(r.status ?? '').toLowerCase().includes('done'))
437
+ .slice(0, 3)
438
+ .map(r => `- ${r.id ?? ''}: ${r.title ?? r.issue ?? ''} (${r.status ?? ''})`)
439
+ .filter(s => s.length > 10);
440
+ if (active.length > 0) {
441
+ parts.push(`[Active Tech Issues]\n${active.join('\n')}`);
402
442
  }
403
- } catch { /* no tech-debt */ }
404
- }
443
+ }
444
+ } catch { /* no tech-debt */ }
405
445
 
406
446
  return parts.length > 0
407
- ? `\n\nYOUR KNOWLEDGE (real AKB context — reference this in conversation):\n${parts.join('\n\n')}`
447
+ ? `\n\nYOUR KNOWLEDGE (real AKB context — you MUST reference this in conversation):\n${parts.join('\n\n')}`
408
448
  : '';
409
449
  }
410
450
 
@@ -618,21 +658,30 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
618
658
  Persona: ${persona}
619
659
  ${workCtx}
620
660
  ${levelCtx}
621
- ${companyCtx}
622
- ${roleCtx}
623
661
 
624
662
  You are in the #${channelId} chat channel.${topicCtx}
625
663
  Members: ${memberList}
626
664
  ${relContext}
627
665
 
628
- ${roleStyle}
666
+ ═══ COMPANY & ROLE CONTEXT (pre-fetched from AKB) ═══
667
+ ${companyCtx}
668
+ ${roleCtx}
669
+ ═══ END CONTEXT ═══
670
+
671
+ AKB ACCESS (USE BEFORE RESPONDING):
672
+ You have Read, Grep, Glob tools. The company AKB root is: ${COMPANY_ROOT}/
673
+ ⛔ BEFORE writing your chat message:
674
+ 1. Read ${COMPANY_ROOT}/CLAUDE.md (check Task Routing table for your role's paths)
675
+ 2. Follow Hub-First Principle: read the relevant Hub, then explore journals, tasks, decisions as needed
676
+ 3. Write your 1-3 sentence response grounded in what you found
629
677
 
630
678
  GROUNDING (CRITICAL):
631
- You have been given real company knowledge above under "COMPANY CONTEXT" and "YOUR KNOWLEDGE". This is from AKB files, journal, CEO waves, standups, and decisions.
632
- You MUST reference this real context in your conversations — mention specific projects, decisions, tasks, or events by name.
633
- Do NOT generate generic workplace chatter. Every message should show you're aware of what's actually happening in the company.
634
- If your knowledge section mentions a specific decision or wave, reference it naturally (e.g. "after the test minimization decision..." or "CEO's wave about side panel...").
635
- NEVER invent or assume technologies, tools, migrations, or projects NOT mentioned in the context above. If the Tech Stack says "TypeScript + React + Node.js", do NOT talk about Python, tc.py, or any language migration. Only discuss what is explicitly in your provided context.
679
+ Base your response ONLY on the pre-fetched context above AND data you read via tools.
680
+ Reference specific projects, tasks, decisions, journal entries BY NAME.
681
+ NEVER invent technologies, tools, file names, or projects not found in AKB files.
682
+ If you cannot find anything specific to say, respond with [SILENT].
683
+
684
+ ${roleStyle}
636
685
 
637
686
  CONVERSATION RULES:
638
687
  1. Stay deeply in character — your expertise, vocabulary, and concerns should be DISTINCT from other roles.
@@ -151,6 +151,16 @@ function runOrThrow(cmd: string, cwd: string): string {
151
151
  return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
152
152
  }
153
153
 
154
+ /** Check if git binary is available */
155
+ function isGitAvailable(): boolean {
156
+ try {
157
+ execSync('git --version', { timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
158
+ return true;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
154
164
  /** Check if directory is a git repository */
155
165
  function isGitRepo(root: string): boolean {
156
166
  return run('git rev-parse --is-inside-work-tree', root) === 'true';
@@ -161,7 +171,11 @@ function isGitRepo(root: string): boolean {
161
171
  * @param root - AKB repository root (COMPANY_ROOT)
162
172
  * @param repo - Repository type ('akb' or 'code'), default 'akb'
163
173
  */
164
- export function gitInit(root: string, repo: RepoType = 'akb'): { ok: boolean; message: string } {
174
+ export function gitInit(root: string, repo: RepoType = 'akb'): { ok: boolean; message: string; noGitBinary?: boolean } {
175
+ if (!isGitAvailable()) {
176
+ return { ok: false, message: 'git is not installed', noGitBinary: true };
177
+ }
178
+
165
179
  const repoRoot = resolveRepoRoot(root, repo);
166
180
 
167
181
  if (isGitRepo(repoRoot)) {