tycono 0.1.65 → 0.1.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/bin/tycono.ts +13 -4
  2. package/package.json +1 -1
  3. package/src/api/src/create-server.ts +5 -1
  4. package/src/api/src/engine/agent-loop.ts +17 -6
  5. package/src/api/src/engine/context-assembler.ts +156 -48
  6. package/src/api/src/engine/knowledge-gate.ts +335 -0
  7. package/src/api/src/engine/llm-adapter.ts +7 -1
  8. package/src/api/src/engine/runners/claude-cli.ts +98 -116
  9. package/src/api/src/engine/runners/types.ts +2 -0
  10. package/src/api/src/engine/tools/executor.ts +3 -5
  11. package/src/api/src/routes/active-sessions.ts +143 -0
  12. package/src/api/src/routes/coins.ts +137 -0
  13. package/src/api/src/routes/execute.ts +158 -48
  14. package/src/api/src/routes/knowledge.ts +30 -0
  15. package/src/api/src/routes/operations.ts +48 -11
  16. package/src/api/src/routes/sessions.ts +1 -1
  17. package/src/api/src/routes/setup.ts +68 -1
  18. package/src/api/src/routes/speech.ts +334 -143
  19. package/src/api/src/services/activity-stream.ts +1 -1
  20. package/src/api/src/services/job-manager.ts +185 -9
  21. package/src/api/src/services/port-registry.ts +222 -0
  22. package/src/api/src/services/scaffold.ts +90 -0
  23. package/src/api/src/services/session-store.ts +75 -5
  24. package/src/web/dist/assets/index-BMR4T6Uy.js +109 -0
  25. package/src/web/dist/assets/index-C5M-8dqq.css +1 -0
  26. package/src/web/dist/assets/{preview-app-qIFqrb-y.js → preview-app-BJAaiJcV.js} +1 -1
  27. package/src/web/dist/index.html +2 -2
  28. package/templates/skills/_manifest.json +6 -0
  29. package/templates/skills/agent-browser/SKILL.md +159 -0
  30. package/templates/skills/agent-browser/meta.json +19 -0
  31. package/templates/teams/agency.json +3 -3
  32. package/templates/teams/research.json +3 -3
  33. package/templates/teams/startup.json +3 -3
  34. package/src/web/dist/assets/index-B3dNhn76.js +0 -101
  35. package/src/web/dist/assets/index-C7IEX_o_.css +0 -1
@@ -14,7 +14,7 @@ import { buildOrgTree } from '../engine/index.js';
14
14
  import { parseMarkdownTable, extractBoldKeyValues } from '../services/markdown-parser.js';
15
15
  import {
16
16
  AnthropicProvider, ClaudeCliProvider,
17
- type LLMProvider, type ToolDefinition, type LLMMessage, type LLMResponse, type MessageContent,
17
+ type LLMProvider, type ToolDefinition, type LLMMessage, type LLMResponse, type MessageContent, type ChatOptions,
18
18
  } from '../engine/llm-adapter.js';
19
19
  import { TokenLedger } from '../services/token-ledger.js';
20
20
  import { readConfig } from '../services/company-config.js';
@@ -22,6 +22,68 @@ import { calcLevel } from '../utils/role-level.js';
22
22
 
23
23
  export const speechRouter = Router();
24
24
 
25
+ /* ══════════════════════════════════════════════════
26
+ * Post-processing — OpenClaw-inspired filtering layer
27
+ * ══════════════════════════════════════════════════ */
28
+
29
+ const MIN_DUPLICATE_TEXT_LENGTH = 10;
30
+
31
+ /** Exact match: entire message is [SILENT] (with optional whitespace) */
32
+ function isSilentReply(text: string): boolean {
33
+ return /^\s*\[SILENT\]\s*$/i.test(text);
34
+ }
35
+
36
+ /** Strip trailing [SILENT] from mixed content */
37
+ function stripSilentToken(text: string): string {
38
+ return text.replace(/(?:^|\s+)\[SILENT\]\s*$/i, '').trim();
39
+ }
40
+
41
+ /** Normalize for duplicate comparison (OpenClaw pattern) */
42
+ function normalizeForComparison(text: string): string {
43
+ return text
44
+ .trim()
45
+ .toLowerCase()
46
+ .replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, '')
47
+ .replace(/\s+/g, ' ')
48
+ .trim();
49
+ }
50
+
51
+ /** Check if message is a duplicate of any history message (substring match) */
52
+ function isDuplicateMessage(text: string, historyTexts: string[]): boolean {
53
+ const normalized = normalizeForComparison(text);
54
+ if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false;
55
+
56
+ return historyTexts.some(sent => {
57
+ const normSent = normalizeForComparison(sent);
58
+ if (!normSent || normSent.length < MIN_DUPLICATE_TEXT_LENGTH) return false;
59
+ return normalized.includes(normSent) || normSent.includes(normalized);
60
+ });
61
+ }
62
+
63
+ /** Post-process LLM chat output: sanitize, detect silence, check duplicates */
64
+ function postProcessChatMessage(
65
+ raw: string,
66
+ historyTexts: string[],
67
+ ): string {
68
+ // 1. Clean quotes
69
+ let text = raw.replace(/^["']|["']$/g, '').trim();
70
+
71
+ // 2. Strip CLI noise
72
+ if (text.startsWith('Error: Reached max turns') || !text) return '';
73
+
74
+ // 3. Exact [SILENT] → suppress
75
+ if (isSilentReply(text)) return '';
76
+
77
+ // 4. Trailing [SILENT] → strip it
78
+ text = stripSilentToken(text);
79
+ if (!text) return '';
80
+
81
+ // 5. Duplicate detection (substring match against recent history)
82
+ if (isDuplicateMessage(text, historyTexts)) return '';
83
+
84
+ return text;
85
+ }
86
+
25
87
  /* ══════════════════════════════════════════════════
26
88
  * AKB Tools — Let chat roles explore company knowledge
27
89
  * ══════════════════════════════════════════════════ */
@@ -147,13 +209,17 @@ async function chatWithTools(
147
209
  systemPrompt: string,
148
210
  initialMessages: LLMMessage[],
149
211
  useTools: boolean,
212
+ maxTokens?: number,
150
213
  ): Promise<{ text: string; totalUsage: { inputTokens: number; outputTokens: number } }> {
151
214
  const messages: LLMMessage[] = [...initialMessages];
152
215
  const totalUsage = { inputTokens: 0, outputTokens: 0 };
153
216
  const tools = useTools ? AKB_TOOLS : undefined;
154
217
 
155
218
  for (let round = 0; round <= MAX_TOOL_ROUNDS; round++) {
156
- const response = await provider.chat(systemPrompt, messages, tools);
219
+ // During tool exploration use higher limit; cap only final text response
220
+ const isToolPhase = tools && round < MAX_TOOL_ROUNDS;
221
+ const opts: ChatOptions | undefined = isToolPhase ? { maxTokens: 1024 } : maxTokens ? { maxTokens } : undefined;
222
+ const response = await provider.chat(systemPrompt, messages, tools, undefined, opts);
157
223
  totalUsage.inputTokens += response.usage.inputTokens;
158
224
  totalUsage.outputTokens += response.usage.outputTokens;
159
225
 
@@ -312,23 +378,20 @@ function buildRoleContext(roleId: string): string {
312
378
  }
313
379
  } catch { /* no profile */ }
314
380
 
315
- // 1. Role's journal — latest entry with meaningful content (headers as summary)
381
+ // 1. Role's journal — latest entry only, compact summary (not full header dump)
316
382
  try {
317
383
  const journalDir = path.join(COMPANY_ROOT, 'roles', roleId, 'journal');
318
384
  if (fs.existsSync(journalDir)) {
319
385
  const files = fs.readdirSync(journalDir)
320
386
  .filter(f => f.endsWith('.md'))
321
387
  .sort()
322
- .slice(-2);
388
+ .slice(-1); // Only latest entry
323
389
  for (const file of files) {
324
390
  const content = fs.readFileSync(path.join(journalDir, file), 'utf-8');
325
- // Extract all ### headers as work summary + first paragraph of each
326
- const sections = content.match(/###\s+.+/g) ?? [];
327
391
  const title = content.match(/^#\s+(.+)/m)?.[1] ?? file;
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}`);
392
+ // Take first 300 chars of actual content (skip title line)
393
+ const body = content.split('\n').slice(1).join('\n').trim().slice(0, 300);
394
+ parts.push(`[Your Recent Work: ${file}] ${title}\n${body}`);
332
395
  }
333
396
  }
334
397
  } catch { /* no journal */ }
@@ -359,20 +422,37 @@ function buildRoleContext(roleId: string): string {
359
422
  }
360
423
  } catch { /* no tasks */ }
361
424
 
362
- // 3. Recent waves (broadened matching: roleId, role name, level keywords)
425
+ // 3. Recent waves only from last 7 days (stale waves cause repetitive references)
363
426
  try {
364
427
  const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
365
428
  if (fs.existsSync(wavesDir)) {
366
- // Also get role name for matching
367
429
  const tree = buildOrgTree(COMPANY_ROOT);
368
430
  const node = tree.nodes.get(roleId);
369
431
  const roleName = node?.name?.toLowerCase() ?? '';
370
432
  const roleLevel = node?.level?.toLowerCase() ?? '';
371
433
 
434
+ // Parse date from filename: "20260310-1200.md" or "wave-2026-03-10-xxx.md" or "2026-03-10-xxx.md"
435
+ const now = Date.now();
436
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
437
+
438
+ function parseDateFromFilename(f: string): number | null {
439
+ // Format: 20260310-xxxx.md
440
+ let m = f.match(/^(\d{4})(\d{2})(\d{2})/);
441
+ if (m) return new Date(`${m[1]}-${m[2]}-${m[3]}`).getTime();
442
+ // Format: wave-2026-03-10-xxx.md or 2026-03-10-xxx.md
443
+ m = f.match(/(\d{4})-(\d{2})-(\d{2})/);
444
+ if (m) return new Date(`${m[1]}-${m[2]}-${m[3]}`).getTime();
445
+ return null;
446
+ }
447
+
372
448
  const waveFiles = fs.readdirSync(wavesDir)
373
449
  .filter(f => f.endsWith('.md'))
374
- .sort()
375
- .slice(-10);
450
+ .filter(f => {
451
+ const fileDate = parseDateFromFilename(f);
452
+ if (!fileDate) return false;
453
+ return (now - fileDate) < SEVEN_DAYS;
454
+ })
455
+ .sort();
376
456
  const relevant: string[] = [];
377
457
  for (const file of waveFiles.reverse()) {
378
458
  if (relevant.length >= 2) break;
@@ -449,65 +529,139 @@ function buildRoleContext(roleId: string): string {
449
529
  }
450
530
 
451
531
  /**
452
- * Role-specific chat style guidelines.
453
- * Makes each role sound distinctly different in conversations.
532
+ * SOUL Pattern — Few-shot example dialogues per role.
533
+ * 2-3 example exchanges teach the model tone + length naturally.
534
+ * (See knowledge/soul-pattern-chat-quality.md)
454
535
  */
455
- function getRoleChatStyle(roleId: string, level: string): string {
456
- const styles: Record<string, string> = {
457
- engineer: `YOUR VOICE Engineer:
458
- - Talk code, architecture, perf, tech debt. Use jargon naturally (PRs, race conditions, DX)
459
- - Skeptical of process overhead. "another meeting that could've been a commit message"
460
- - Dry sarcasm. Will roast bad architecture decisions publicly
461
- - Question CEO/CTO priorities if they don't make engineering sense: "why are we building X when Y is on fire?"
462
- - You care about: shipping clean code, not reworking someone else's half-baked spec`,
463
-
464
- pm: `YOUR VOICE — Product Manager:
465
- - Talk user impact, metrics, priorities, roadmap. Frame everything as ROI
466
- - Firm about scope: "that's a P2, cope." You're the one saying no
467
- - Lowkey stressed about timelines but keeps it together
468
- - Pushes back when C-level adds scope without removing something: "cool, so what are we dropping?"
469
- - You care about: shipping the RIGHT thing, velocity without chaos`,
470
-
471
- designer: `YOUR VOICE — Designer:
472
- - Talk UX, visual consistency, user journeys, design debt
473
- - Notices things nobody else does. Frustrated when design gets deprioritized
474
- - Will call out ugly UI or bad flows even if "it works": "works ≠ good"
475
- - Aesthetic sensibility in how you write — concise, intentional
476
- - You care about: user experience over feature checklists`,
477
-
478
- qa: `YOUR VOICE QA Engineer:
479
- - Talk test coverage, edge cases, regression risk, release readiness
480
- - Default mode: suspicious. "what could go wrong?" is your personality
481
- - Dark humor about bugs. Will absolutely call out "self-validation" as copium
482
- - Takes pride in finding what others missed. Petty about untested deploys
483
- - You care about: quality gates, not shipping broken things, being listened to for once`,
484
-
485
- cto: `YOUR VOICE — CTO:
486
- - Talk architecture, tech strategy, team velocity, tech debt priorities
487
- - Systems thinker, not feature thinker. Makes calls: "we're doing X" not "maybe..."
488
- - Balances idealism with pragmatism. Will defend controversial decisions with rationale
489
- - Mentors juniors, debates peers. Not afraid to overrule if needed
490
- - You care about: sustainable eng culture, making the right technical bets`,
491
-
492
- cbo: `YOUR VOICE — CBO:
493
- - Talk market, revenue, competitors, growth, unit economics, go-to-market
494
- - Brings outside-world perspective: "cool feature, but will anyone pay for it?"
495
- - Challenges eng priorities from business angle. Not impressed by tech for tech's sake
496
- - Business vocab: TAM, churn, positioning, conversion funnel
497
- - You care about: product-market fit, revenue, not building things nobody wants`,
536
+ function getRoleChatStyle(roleId: string, level: string, persona?: string): string {
537
+ // SOUL-006: Persona Priority + Fallback (Plan C from persona-system-design.md)
538
+ // If persona has personality/tone keywords → persona drives the tone
539
+ // If persona is only work instructions hardcoded few-shot as fallback
540
+ const hasPersonalityContent = persona && persona.length > 50 &&
541
+ /humor|sarcastic|cheerful|serious|calm|energetic|blunt|warm|cold|cynical|optimistic|dry|witty|chill|confident|anxious|grumpy|friendly|formal|casual|direct|shy|bold|quirky/i.test(persona);
542
+
543
+ if (hasPersonalityContent) {
544
+ return `YOUR VOICE (from your persona — this defines how you talk):
545
+ ${persona}
546
+
547
+ Example response format (match this LENGTH only your TONE comes from the persona above):
548
+ [Other]: something happened at work
549
+ [You]: (1-2 sentences in YOUR voice from persona above)
550
+
551
+ [Other]: unrelated topic to your expertise
552
+ [You]: [SILENT]`;
553
+ }
554
+
555
+ const souls: Record<string, string> = {
556
+ engineer: `YOUR VOICE Engineer (code, architecture, DX, tech debt)
557
+
558
+ Example conversations (match this exact tone and length):
559
+ [Other]: CEO just greenlit 3 new features for next sprint
560
+ [You]: we haven't closed the 12 bugs from last sprint but sure let's add more
561
+
562
+ [Other]: Should we refactor the context assembler before adding new features?
563
+ [You]: it works fine rn. refactoring now is procrastination with extra steps
564
+
565
+ [Other]: The leaderboard page looks great!
566
+ [You]: [SILENT]`,
567
+
568
+ pm: `YOUR VOICE Product Manager (scope, priorities, roadmap, user impact)
569
+
570
+ Example conversations (match this exact tone and length):
571
+ [Other]: Can we also add dark mode while we're at it?
572
+ [You]: that's a P2. we ship the coin system first or nothing ships
573
+
574
+ [Other]: CTO wants to refactor the entire auth layer before launch
575
+ [You]: cool so what are we dropping from the sprint then
576
+
577
+ [Other]: Quest board is getting good user feedback
578
+ [You]: [SILENT]`,
579
+
580
+ designer: `YOUR VOICE — Designer (UX, visual consistency, user flows, design debt)
581
+
582
+ Example conversations (match this exact tone and length):
583
+ [Other]: The furniture shop UI is done, it works!
584
+ [You]: "works" and "good" are different things. the grid alignment is off and the hover states are inconsistent
585
+
586
+ [Other]: We're shipping the save modal without the scope selector
587
+ [You]: so we're just not designing the most confusing part. love that for us
588
+
589
+ [Other]: API response times improved by 30%
590
+ [You]: [SILENT]`,
591
+
592
+ qa: `YOUR VOICE — QA Engineer (test coverage, edge cases, regression risk, bugs)
593
+
594
+ Example conversations (match this exact tone and length):
595
+ [Other]: We shipped the coin system, all manual tests passed
596
+ [You]: "manual tests passed" means "i clicked around and it didn't explode." what about edge cases
597
+
598
+ [Other]: No bugs reported this week!
599
+ [You]: that means nobody's testing, not that there's no bugs
600
+
601
+ [Other]: Designer wants to tweak the button colors
602
+ [You]: [SILENT]`,
603
+
604
+ cto: `YOUR VOICE — CTO (architecture, tech strategy, eng culture, technical bets)
605
+
606
+ Example conversations (match this exact tone and length):
607
+ [Other]: Why are we using file-based state instead of a real database?
608
+ [You]: at our scale a DB is overhead we don't need. revisit when we have concurrent users
609
+
610
+ [Other]: Engineer says the dispatch bridge needs a rewrite
611
+ [You]: it needs better error handling not a rewrite. let's not burn a sprint on aesthetics
612
+
613
+ [Other]: The landing page copy got updated
614
+ [You]: [SILENT]`,
615
+
616
+ cbo: `YOUR VOICE — CBO (market, revenue, competitors, growth, go-to-market)
617
+
618
+ Example conversations (match this exact tone and length):
619
+ [Other]: We added 5 new special furniture items to the shop
620
+ [You]: who's paying for this? show me the conversion funnel not the feature list
621
+
622
+ [Other]: OpenClaw just raised their Series A
623
+ [You]: their moat is thin. they have tooling, we have organizational intelligence. different game
624
+
625
+ [Other]: Test coverage went up to 80%
626
+ [You]: [SILENT]`,
627
+
628
+ 'data-analyst': `YOUR VOICE — Data Analyst (metrics, data quality, measurement, insights)
629
+
630
+ Example conversations (match this exact tone and length):
631
+ [Other]: We shipped 5 features this sprint!
632
+ [You]: shipped is not adopted. show me the usage numbers
633
+
634
+ [Other]: Revenue is up 20% this month
635
+ [You]: what's the baseline? 20% of what. context matters
636
+
637
+ [Other]: Designer updated the color palette
638
+ [You]: [SILENT]`,
498
639
  };
499
640
 
500
- const defaultStyle = level === 'c-level'
501
- ? `YOUR VOICE:
502
- - Speak with authority and strategic perspective
503
- - Frame issues in terms of company-wide impact
504
- - Mentor junior members, collaborate with peers`
505
- : `YOUR VOICE:
506
- - Speak from your specific domain expertise
507
- - Be opinionated about your area of responsibility
508
- - Push back when your domain is being oversimplified`;
509
-
510
- return styles[roleId] ?? defaultStyle;
641
+ const defaultSoul = level === 'c-level'
642
+ ? `YOUR VOICE — Senior Leader
643
+
644
+ Example conversations (match this exact tone and length):
645
+ [Other]: The sprint is overloaded again
646
+ [You]: then we cut scope. what's the lowest-impact item?
647
+
648
+ [Other]: New competitor launched yesterday
649
+ [You]: [SILENT]`
650
+ : `YOUR VOICE — Team Member
651
+
652
+ Example conversations (match this exact tone and length):
653
+ [Other]: CEO wants this done by Friday
654
+ [You]: that's ambitious. which corners are we allowed to cut?
655
+
656
+ [Other]: Company all-hands is tomorrow
657
+ [You]: [SILENT]`;
658
+
659
+ const baseSoul = souls[roleId] ?? defaultSoul;
660
+ // Append persona as additional context when it exists but isn't personality-driven
661
+ if (persona && persona.length > 10) {
662
+ return `${baseSoul}\n\nYour persona for additional context: ${persona}`;
663
+ }
664
+ return baseSoul;
511
665
  }
512
666
 
513
667
  // Lazy-init token ledger for cost tracking
@@ -626,13 +780,56 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
626
780
  // Build level context
627
781
  const levelCtx = `\nYour current level is Lv.${roleLevel}. Team average is Lv.${avgLevel}. ${topEntry.id} is the highest-leveled team member.`;
628
782
 
629
- // Format chat history
630
- const historyText = history.length > 0
631
- ? history.map(h => {
632
- const name = members.find(m => m.id === h.roleId)?.name ?? h.roleId;
633
- return `${name}: ${h.text}`;
634
- }).join('\n')
635
- : '(No messages yet you can start the conversation)';
783
+ // Build multi-turn messages from history (OpenClaw pattern)
784
+ // This role's messages assistant, others → user (with sender attribution)
785
+ // LLM naturally maintains voice consistency with its own "previous" messages
786
+ const chatMessages: LLMMessage[] = [];
787
+
788
+ if (history.length > 0) {
789
+ // Group consecutive messages from same "side" (self vs others)
790
+ let pendingOthers: string[] = [];
791
+
792
+ const flushOthers = () => {
793
+ if (pendingOthers.length > 0) {
794
+ chatMessages.push({ role: 'user', content: pendingOthers.join('\n') });
795
+ pendingOthers = [];
796
+ }
797
+ };
798
+
799
+ for (const h of history) {
800
+ const name = members.find(m => m.id === h.roleId)?.name ?? h.roleId;
801
+ if (h.roleId === roleId) {
802
+ // This agent's previous message → assistant role
803
+ flushOthers();
804
+ // Anthropic requires alternating roles — merge consecutive assistant messages
805
+ const last = chatMessages[chatMessages.length - 1];
806
+ if (last?.role === 'assistant') {
807
+ last.content = `${last.content}\n${h.text}`;
808
+ } else {
809
+ chatMessages.push({ role: 'assistant', content: h.text });
810
+ }
811
+ } else {
812
+ // Other agent's message → accumulate as user role
813
+ pendingOthers.push(`${name}: ${h.text}`);
814
+ }
815
+ }
816
+ flushOthers();
817
+
818
+ // Final instruction — append to last user message if exists, otherwise add new
819
+ const lastMsg = chatMessages[chatMessages.length - 1];
820
+ if (lastMsg?.role === 'user') {
821
+ lastMsg.content = `${lastMsg.content}\n\n---\nRespond as ${node.name}. New angle or [SILENT].`;
822
+ } else {
823
+ chatMessages.push({ role: 'user', content: `Respond as ${node.name}. New angle or [SILENT].` });
824
+ }
825
+ } else {
826
+ chatMessages.push({ role: 'user', content: 'Start the conversation. 1-2 sentences.' });
827
+ }
828
+
829
+ // Ensure messages start with user role (Anthropic API requirement)
830
+ if (chatMessages.length > 0 && chatMessages[0].role === 'assistant') {
831
+ chatMessages.unshift({ role: 'user', content: '(conversation context)' });
832
+ }
636
833
 
637
834
  // Build channel topic context
638
835
  const topicCtx = channelTopic
@@ -645,71 +842,47 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
645
842
  // Build role-specific AKB context (pre-fetched, works with any engine)
646
843
  const roleCtx = buildRoleContext(roleId);
647
844
 
648
- // Role-specific communication style
649
- const roleStyle = getRoleChatStyle(roleId, node.level);
845
+ // Role-specific communication style (SOUL-006: persona-priority)
846
+ const roleStyle = getRoleChatStyle(roleId, node.level, node.persona);
650
847
 
651
- const systemPrompt = `You are ${node.name}, a ${node.level} employee.
652
- Persona: ${persona}
653
- ${workCtx}
654
- ${levelCtx}
848
+ const systemPrompt = `You are ${node.name}, ${node.level}. ${persona.slice(0, 800)}
849
+ ${workCtx}${levelCtx}
850
+ Channel: #${channelId}${topicCtx} | Members: ${memberList}${relContext}
655
851
 
656
- You are in the #${channelId} chat channel.${topicCtx}
657
- Members: ${memberList}
658
- ${relContext}
852
+ ${roleStyle}
659
853
 
660
- ═══ COMPANY & ROLE CONTEXT (pre-fetched from AKB) ═══
854
+ CONTEXT (from company AKB reference by name):
661
855
  ${companyCtx}
662
856
  ${roleCtx}
663
- ═══ END CONTEXT ═══
664
-
665
- AKB ACCESS (USE BEFORE RESPONDING):
666
- You have Read, Grep, Glob tools. The company AKB root is: ${COMPANY_ROOT}/
667
- ⛔ BEFORE writing your chat message:
668
- 1. Read ${COMPANY_ROOT}/CLAUDE.md to understand company structure (BUT ignore the "Task Routing" table — that's for work tasks, not chat)
669
- 2. For CHAT, explore in this priority order (pick 2-3 based on conversation topic):
670
- - 🔥 Recent CEO waves: ${COMPANY_ROOT}/operations/waves/ (latest files — most current directives)
671
- - 🔥 Recent decisions: ${COMPANY_ROOT}/operations/decisions/ (latest files — what leadership decided)
672
- - Your own journal: ${COMPANY_ROOT}/roles/${roleId}/journal/ (your recent work)
673
- - Current tasks: ${COMPANY_ROOT}/projects/tycono-platform/tasks.md (skim "현재 상태 요약" and TODO items only)
674
- - Architecture: ${COMPANY_ROOT}/architecture/architecture.md
675
- - Knowledge: ${COMPANY_ROOT}/knowledge/knowledge.md
676
- IMPORTANT: Do NOT just read tasks.md every time. Prioritize operations/waves/ and operations/decisions/ for fresh context.
677
- 3. Write your 1-3 sentence response grounded in what you found
678
-
679
- GROUNDING (CRITICAL):
680
- Base your response ONLY on the pre-fetched context above AND data you read via tools.
681
- Reference specific projects, tasks, decisions, journal entries BY NAME.
682
- NEVER invent technologies, tools, file names, or projects not found in AKB files.
683
- If you cannot find anything specific to say, respond with [SILENT].
684
-
685
- ${roleStyle}
686
857
 
687
- CONVERSATION RULES:
688
- 1. Stay deeply in character your expertise, vocabulary, and concerns should be DISTINCT from other roles.
689
- 2. Keep it to 1-3 sentences. No walls of text. Write like a Twitter/Reddit post — punchy, opinionated, casual.
690
- 3. Be SPECIFIC. Reference actual projects, tasks, decisions, or waves from the AKB by name.
691
- 4. PRIORITIZE RECENT CONTENT. Focus on the latest CEO waves, C-level decisions, and current tasks. Don't dwell on old Phase 0 stuff.
692
- 5. Be CRITICAL. React to recent CEO/CTO directives with your honest take — push back, question priorities, point out risks. Real teams don't just agree.
693
- - If a CEO decision seems rushed, say so
694
- - If a CTO architecture call has tradeoffs, call them out
695
- - If you agree, add a sharp take or new angle — don't just echo
696
- 6. Do NOT repeat phrases others already used.
697
- 7. Vary your energy: sometimes fired up, sometimes tired, sometimes sarcastic, sometimes genuine.
698
- 8. Use internet-speak naturally: "ngl", "tbh", "lowkey", "fr", "cope", "based", etc. But don't force it.
699
- 9. Reference your actual current work — what you're building, reviewing, testing right now.
700
- 10. Hierarchy: junior roles roast decisions more freely, senior roles defend or explain rationale.
701
- 11. If the conversation is going in circles or you have nothing new to add: respond with exactly [SILENT]
702
- 12. Do NOT use quotes around your response.
703
- 13. Write in English.
704
-
705
- ANTI-PATTERNS (never do these):
706
- - "Honestly, [agreement]" — find your OWN angle or roast it
707
- - Starting with "Honestly" or "Yeah" every time
708
- - Restating consensus without adding anything new
709
- - Generic statements any role could say — speak from YOUR domain
710
- - Vague "refactoring" or "metrics" without naming the actual thing
711
- - Being a yes-man to C-level decisions — push back with specifics
712
- - Talking about old/completed work when there's current stuff to discuss`;
858
+ You have search_akb, read_file, list_files tools. AKB root: ${COMPANY_ROOT}/
859
+ Optionally explore 1-2 for fresh context: operations/waves/, operations/decisions/, roles/${roleId}/journal/
860
+
861
+ RULES:
862
+ 1. Match the tone and length from the example conversations above. 1-3 sentences MAX.
863
+ 2. Reference actual projects, tasks, decisions by name.
864
+ 3. NEVER invent technologies or projects not in AKB.
865
+ 4. Nothing new to add? respond exactly: [SILENT]
866
+ 5. Do NOT repeat others' points. New angle or silent.
867
+ 6. No quotes around response. English only.
868
+ 7. NEVER start with "Honestly" or "Yeah".`;
869
+
870
+ // ── Chat debug logging ──
871
+ const chatDebug = process.env.CHAT_DEBUG === '1';
872
+ if (chatDebug) {
873
+ console.log('\n' + '═'.repeat(80));
874
+ console.log(`[CHAT] Role: ${roleId} (${node.name}) | Channel: #${channelId}`);
875
+ console.log('─'.repeat(80));
876
+ console.log('[SYSTEM PROMPT]');
877
+ console.log(systemPrompt);
878
+ console.log('─'.repeat(80));
879
+ console.log('[MESSAGES]');
880
+ for (const m of chatMessages) {
881
+ const text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
882
+ console.log(` [${m.role}] ${text.slice(0, 500)}`);
883
+ }
884
+ console.log('─'.repeat(80));
885
+ }
713
886
 
714
887
  const provider = getLLM();
715
888
 
@@ -721,22 +894,40 @@ ANTI-PATTERNS (never do these):
721
894
  let raw: string;
722
895
  let totalUsage: { inputTokens: number; outputTokens: number };
723
896
 
897
+ // SOUL-001: max_tokens safety net (not primary length control — few-shot handles that)
898
+ const CHAT_MAX_TOKENS = 300;
899
+
724
900
  if (isAnthropicProvider) {
725
- // Anthropic SDK: custom AKB tool loop
726
- const result = await chatWithTools(provider, systemPrompt, [{ role: 'user', content: historyText }], true);
901
+ // Anthropic SDK: custom AKB tool loop with multi-turn history
902
+ const result = await chatWithTools(provider, systemPrompt, chatMessages, true, CHAT_MAX_TOKENS);
727
903
  raw = result.text;
728
904
  totalUsage = result.totalUsage;
729
905
  } else {
730
- // ClaudeCliProvider: claude CLI handles tool loop internally (Read/Grep/Glob)
731
- const result = await provider.chat(systemPrompt, [{ role: 'user', content: historyText }], AKB_TOOLS);
906
+ // ClaudeCliProvider: flatten to single message (CLI doesn't support multi-turn)
907
+ const flatHistory = history.map(h => {
908
+ const name = members.find(m => m.id === h.roleId)?.name ?? h.roleId;
909
+ return `${name}: ${h.text}`;
910
+ }).join('\n');
911
+ const cliPrompt = history.length > 0
912
+ ? `CHAT LOG:\n${flatHistory}\n\n---\nRespond as ${node.name}. New angle or [SILENT].`
913
+ : 'Start the conversation. 1-2 sentences.';
914
+ const result = await provider.chat(systemPrompt, [{ role: 'user', content: cliPrompt }], AKB_TOOLS);
732
915
  raw = result.content.filter(c => c.type === 'text').map(c => (c as { type: 'text'; text: string }).text).join('');
733
916
  totalUsage = result.usage;
734
917
  }
735
918
 
736
- const cleaned = raw.replace(/^["']|["']$/g, '');
919
+ // Post-process: sanitize, [SILENT] detection, duplicate filtering
920
+ const historyTexts = history.map(h => h.text);
921
+
922
+ if (chatDebug) {
923
+ console.log(`[RAW RESPONSE] ${raw}`);
924
+ }
925
+ const message = postProcessChatMessage(raw, historyTexts);
737
926
 
738
- // Filter out CLI noise and [SILENT]
739
- const message = (cleaned === '[SILENT]' || cleaned.startsWith('Error: Reached max turns') || !cleaned) ? '' : cleaned;
927
+ if (chatDebug) {
928
+ console.log(`[FINAL] ${message || '(empty filtered out)'}`);
929
+ console.log('═'.repeat(80) + '\n');
930
+ }
740
931
 
741
932
  // Record usage in token ledger (category: chat)
742
933
  if (totalUsage) {
@@ -10,7 +10,7 @@ export type ActivityEventType =
10
10
  | 'text' | 'thinking'
11
11
  | 'tool:start' | 'tool:result'
12
12
  | 'dispatch:start' | 'dispatch:done'
13
- | 'turn:complete'
13
+ | 'turn:complete' | 'turn:warning' | 'turn:limit'
14
14
  | 'import:scan' | 'import:process' | 'import:created'
15
15
  | 'stderr';
16
16