tycono 0.3.45-beta.2 → 0.3.45-beta.3

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 (113) hide show
  1. package/README.md +191 -162
  2. package/bin/tycono.ts +42 -10
  3. package/package.json +21 -15
  4. package/packages/server/bin/cli.js +35 -0
  5. package/packages/server/bin/server.ts +183 -0
  6. package/{src → packages/server/src}/api/src/create-server.ts +11 -3
  7. package/{src → packages/server/src}/api/src/engine/agent-loop.ts +30 -7
  8. package/{src → packages/server/src}/api/src/engine/context-assembler.ts +122 -57
  9. package/{src → packages/server/src}/api/src/engine/llm-adapter.ts +10 -7
  10. package/{src → packages/server/src}/api/src/engine/org-tree.ts +43 -3
  11. package/{src → packages/server/src}/api/src/engine/runners/claude-cli.ts +37 -15
  12. package/{src → packages/server/src}/api/src/engine/runners/types.ts +6 -0
  13. package/{src → packages/server/src}/api/src/engine/tools/executor.ts +65 -9
  14. package/{src → packages/server/src}/api/src/routes/execute.ts +221 -17
  15. package/packages/server/src/api/src/services/claude-md-manager.ts +190 -0
  16. package/{src → packages/server/src}/api/src/services/company-config.ts +1 -0
  17. package/{src → packages/server/src}/api/src/services/digest-engine.ts +4 -1
  18. package/packages/server/src/api/src/services/dispatch-classifier.ts +179 -0
  19. package/{src → packages/server/src}/api/src/services/execution-manager.ts +227 -21
  20. package/{src → packages/server/src}/api/src/services/file-reader.ts +4 -1
  21. package/packages/server/src/api/src/services/preset-loader.ts +310 -0
  22. package/{src → packages/server/src}/api/src/services/supervisor-heartbeat.ts +89 -9
  23. package/{src → packages/server/src}/api/src/services/wave-multiplexer.ts +18 -8
  24. package/{src → packages/server/src}/api/src/services/wave-tracker.ts +25 -0
  25. package/packages/server/src/core/scaffolder.ts +620 -0
  26. package/{src → packages/server/src}/shared/types.ts +3 -1
  27. package/packages/server/templates/CLAUDE.md.tmpl +152 -0
  28. package/packages/server/templates/agentic-knowledge-base.md +355 -0
  29. package/src/api/src/services/claude-md-manager.ts +0 -94
  30. package/src/api/src/services/preset-loader.ts +0 -149
  31. package/templates/CLAUDE.md.tmpl +0 -239
  32. /package/{src/web → packages/pixel}/dist/assets/index-BJyiMGkM.js +0 -0
  33. /package/{src/web → packages/pixel}/dist/assets/index-BOuHc64o.css +0 -0
  34. /package/{src/web → packages/pixel}/dist/assets/index-DDPzbp9E.js +0 -0
  35. /package/{src/web → packages/pixel}/dist/assets/index-DVKWFwwK.css +0 -0
  36. /package/{src/web → packages/pixel}/dist/assets/preview-app-DZ6WxhDc.js +0 -0
  37. /package/{src/web → packages/pixel}/dist/index.html +0 -0
  38. /package/{src/web → packages/pixel}/dist/tyconoforge.js +0 -0
  39. /package/{src → packages/server/src}/api/package.json +0 -0
  40. /package/{src → packages/server/src}/api/src/create-app.ts +0 -0
  41. /package/{src → packages/server/src}/api/src/engine/authority-validator.ts +0 -0
  42. /package/{src → packages/server/src}/api/src/engine/index.ts +0 -0
  43. /package/{src → packages/server/src}/api/src/engine/knowledge-gate.ts +0 -0
  44. /package/{src → packages/server/src}/api/src/engine/role-lifecycle.ts +0 -0
  45. /package/{src → packages/server/src}/api/src/engine/runners/direct-api.ts +0 -0
  46. /package/{src → packages/server/src}/api/src/engine/runners/index.ts +0 -0
  47. /package/{src → packages/server/src}/api/src/engine/skill-template.ts +0 -0
  48. /package/{src → packages/server/src}/api/src/engine/tools/definitions.ts +0 -0
  49. /package/{src → packages/server/src}/api/src/routes/active-sessions.ts +0 -0
  50. /package/{src → packages/server/src}/api/src/routes/coins.ts +0 -0
  51. /package/{src → packages/server/src}/api/src/routes/company.ts +0 -0
  52. /package/{src → packages/server/src}/api/src/routes/cost.ts +0 -0
  53. /package/{src → packages/server/src}/api/src/routes/engine.ts +0 -0
  54. /package/{src → packages/server/src}/api/src/routes/git.ts +0 -0
  55. /package/{src → packages/server/src}/api/src/routes/knowledge.ts +0 -0
  56. /package/{src → packages/server/src}/api/src/routes/operations.ts +0 -0
  57. /package/{src → packages/server/src}/api/src/routes/preferences.ts +0 -0
  58. /package/{src → packages/server/src}/api/src/routes/presets.ts +0 -0
  59. /package/{src → packages/server/src}/api/src/routes/projects.ts +0 -0
  60. /package/{src → packages/server/src}/api/src/routes/quests.ts +0 -0
  61. /package/{src → packages/server/src}/api/src/routes/roles.ts +0 -0
  62. /package/{src → packages/server/src}/api/src/routes/save.ts +0 -0
  63. /package/{src → packages/server/src}/api/src/routes/sessions.ts +0 -0
  64. /package/{src → packages/server/src}/api/src/routes/setup.ts +0 -0
  65. /package/{src → packages/server/src}/api/src/routes/skills.ts +0 -0
  66. /package/{src → packages/server/src}/api/src/routes/speech.ts +0 -0
  67. /package/{src → packages/server/src}/api/src/routes/supervision.ts +0 -0
  68. /package/{src → packages/server/src}/api/src/routes/sync.ts +0 -0
  69. /package/{src → packages/server/src}/api/src/server.ts +0 -0
  70. /package/{src → packages/server/src}/api/src/services/activity-stream.ts +0 -0
  71. /package/{src → packages/server/src}/api/src/services/activity-tracker.ts +0 -0
  72. /package/{src → packages/server/src}/api/src/services/database.ts +0 -0
  73. /package/{src → packages/server/src}/api/src/services/git-save.ts +0 -0
  74. /package/{src → packages/server/src}/api/src/services/job-manager.ts +0 -0
  75. /package/{src → packages/server/src}/api/src/services/knowledge-importer.ts +0 -0
  76. /package/{src → packages/server/src}/api/src/services/markdown-parser.ts +0 -0
  77. /package/{src → packages/server/src}/api/src/services/port-registry.ts +0 -0
  78. /package/{src → packages/server/src}/api/src/services/preferences.ts +0 -0
  79. /package/{src → packages/server/src}/api/src/services/pricing.ts +0 -0
  80. /package/{src → packages/server/src}/api/src/services/scaffold.ts +0 -0
  81. /package/{src → packages/server/src}/api/src/services/session-store.ts +0 -0
  82. /package/{src → packages/server/src}/api/src/services/team-recommender.ts +0 -0
  83. /package/{src → packages/server/src}/api/src/services/token-ledger.ts +0 -0
  84. /package/{src → packages/server/src}/api/src/services/wave-messages.ts +0 -0
  85. /package/{src → packages/server/src}/api/src/utils/role-level.ts +0 -0
  86. /package/{templates → packages/server/templates}/company.md.tmpl +0 -0
  87. /package/{templates → packages/server/templates}/gitignore.tmpl +0 -0
  88. /package/{templates → packages/server/templates}/roles.md.tmpl +0 -0
  89. /package/{templates → packages/server/templates}/skills/_manifest.json +0 -0
  90. /package/{templates → packages/server/templates}/skills/agent-browser/SKILL.md +0 -0
  91. /package/{templates → packages/server/templates}/skills/agent-browser/meta.json +0 -0
  92. /package/{templates → packages/server/templates}/skills/akb-linter/SKILL.md +0 -0
  93. /package/{templates → packages/server/templates}/skills/akb-linter/meta.json +0 -0
  94. /package/{templates → packages/server/templates}/skills/knowledge-gate/SKILL.md +0 -0
  95. /package/{templates → packages/server/templates}/skills/knowledge-gate/meta.json +0 -0
  96. /package/{templates → packages/server/templates}/teams/agency.json +0 -0
  97. /package/{templates → packages/server/templates}/teams/research.json +0 -0
  98. /package/{templates → packages/server/templates}/teams/startup.json +0 -0
  99. /package/{src/tui → packages/tui/src}/api.ts +0 -0
  100. /package/{src/tui → packages/tui/src}/app.tsx +0 -0
  101. /package/{src/tui → packages/tui/src}/components/CommandMode.tsx +0 -0
  102. /package/{src/tui → packages/tui/src}/components/OrgTree.tsx +0 -0
  103. /package/{src/tui → packages/tui/src}/components/PanelMode.tsx +0 -0
  104. /package/{src/tui → packages/tui/src}/components/SetupWizard.tsx +0 -0
  105. /package/{src/tui → packages/tui/src}/components/StatusBar.tsx +0 -0
  106. /package/{src/tui → packages/tui/src}/components/StreamView.tsx +0 -0
  107. /package/{src/tui → packages/tui/src}/hooks/useApi.ts +0 -0
  108. /package/{src/tui → packages/tui/src}/hooks/useCommand.ts +0 -0
  109. /package/{src/tui → packages/tui/src}/hooks/useSSE.ts +0 -0
  110. /package/{src/tui → packages/tui/src}/index.tsx +0 -0
  111. /package/{src/tui → packages/tui/src}/store.ts +0 -0
  112. /package/{src/tui → packages/tui/src}/theme.ts +0 -0
  113. /package/{src/tui → packages/tui/src}/utils/markdown.tsx +0 -0
@@ -56,9 +56,12 @@ export interface ChatOptions {
56
56
  maxTokens?: number;
57
57
  }
58
58
 
59
+ /** System prompt: plain string or structured blocks with cache_control for prompt caching */
60
+ export type SystemPrompt = string | Array<{ type: 'text'; text: string; cache_control?: { type: 'ephemeral' } }>;
61
+
59
62
  export interface LLMProvider {
60
63
  chat(
61
- systemPrompt: string,
64
+ systemPrompt: SystemPrompt,
62
65
  messages: LLMMessage[],
63
66
  tools?: ToolDefinition[],
64
67
  signal?: AbortSignal,
@@ -66,7 +69,7 @@ export interface LLMProvider {
66
69
  ): Promise<LLMResponse>;
67
70
 
68
71
  chatStream?(
69
- systemPrompt: string,
72
+ systemPrompt: SystemPrompt,
70
73
  messages: LLMMessage[],
71
74
  tools: ToolDefinition[] | undefined,
72
75
  callbacks: StreamCallbacks,
@@ -90,7 +93,7 @@ export class AnthropicProvider implements LLMProvider {
90
93
  * Send a message and get a complete response (non-streaming)
91
94
  */
92
95
  async chat(
93
- systemPrompt: string,
96
+ systemPrompt: SystemPrompt,
94
97
  messages: LLMMessage[],
95
98
  tools?: ToolDefinition[],
96
99
  signal?: AbortSignal,
@@ -99,7 +102,7 @@ export class AnthropicProvider implements LLMProvider {
99
102
  const params: Anthropic.MessageCreateParamsNonStreaming = {
100
103
  model: this.model,
101
104
  max_tokens: options?.maxTokens ?? 8192,
102
- system: systemPrompt,
105
+ system: systemPrompt as Anthropic.MessageCreateParams['system'],
103
106
  messages: messages.map((m) => ({
104
107
  role: m.role,
105
108
  content: m.content as Anthropic.MessageCreateParams['messages'][0]['content'],
@@ -130,7 +133,7 @@ export class AnthropicProvider implements LLMProvider {
130
133
  * Send a message with streaming (for SSE)
131
134
  */
132
135
  async chatStream(
133
- systemPrompt: string,
136
+ systemPrompt: SystemPrompt,
134
137
  messages: LLMMessage[],
135
138
  tools: ToolDefinition[] | undefined,
136
139
  callbacks: StreamCallbacks,
@@ -139,7 +142,7 @@ export class AnthropicProvider implements LLMProvider {
139
142
  model: this.model,
140
143
  max_tokens: 8192,
141
144
  stream: true,
142
- system: systemPrompt,
145
+ system: systemPrompt as Anthropic.MessageCreateParams['system'],
143
146
  messages: messages.map((m) => ({
144
147
  role: m.role,
145
148
  content: m.content as Anthropic.MessageCreateParams['messages'][0]['content'],
@@ -257,7 +260,7 @@ export class ClaudeCliProvider implements LLMProvider {
257
260
  '--output-format', 'text',
258
261
  ...(useTools ? [
259
262
  '--tools', 'Read,Grep,Glob',
260
- '--dangerously-skip-permissions',
263
+ '--permission-mode', process.env.TYCONO_PERMISSION_MODE || 'bypassPermissions',
261
264
  ] : []),
262
265
  userText,
263
266
  ];
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import YAML from 'yaml';
4
5
 
@@ -83,6 +84,17 @@ interface RawRoleYaml {
83
84
  };
84
85
  }
85
86
 
87
+ /* ─── Default Roles (fallback when no roles/ directory) ── */
88
+
89
+ const DEFAULT_ROLES: Array<{ id: string; name: string; level: 'c-level' | 'member'; reportsTo: string; persona: string }> = [
90
+ { id: 'cto', name: 'CTO', level: 'c-level', reportsTo: 'ceo', persona: 'Chief Technology Officer. Leads technical architecture and manages Engineer + QA.' },
91
+ { id: 'cbo', name: 'CBO', level: 'c-level', reportsTo: 'ceo', persona: 'Chief Business Officer. Leads product vision and manages PM + Designer.' },
92
+ { id: 'engineer', name: 'Engineer', level: 'member', reportsTo: 'cto', persona: 'Software Engineer. Writes working code.' },
93
+ { id: 'qa', name: 'QA', level: 'member', reportsTo: 'cto', persona: 'QA Engineer. Tests and validates.' },
94
+ { id: 'pm', name: 'PM', level: 'member', reportsTo: 'cbo', persona: 'Product Manager. Writes specs and requirements.' },
95
+ { id: 'designer', name: 'Designer', level: 'member', reportsTo: 'cbo', persona: 'UI/UX Designer.' },
96
+ ];
97
+
86
98
  /* ─── Build ──────────────────────────────────── */
87
99
 
88
100
  export function buildOrgTree(companyRoot: string, presetId?: string): OrgTree {
@@ -106,10 +118,17 @@ export function buildOrgTree(companyRoot: string, presetId?: string): OrgTree {
106
118
  const roleDirs: string[] = [];
107
119
  if (fs.existsSync(rolesDir)) roleDirs.push(rolesDir);
108
120
 
109
- // If preset specified, also scan preset's roles directory
121
+ // If preset specified, also scan preset/agency roles directories (2-Layer Knowledge)
122
+ // Search order: local presets > local agencies > global agencies
110
123
  if (presetId && presetId !== 'default') {
111
- const presetRolesDir = path.join(companyRoot, 'knowledge', 'presets', presetId, 'roles');
112
- if (fs.existsSync(presetRolesDir)) roleDirs.push(presetRolesDir);
124
+ const presetRoleCandidates = [
125
+ path.join(companyRoot, 'knowledge', 'presets', presetId, 'roles'),
126
+ path.join(companyRoot, '.tycono', 'agencies', presetId, 'roles'),
127
+ path.join(os.homedir(), '.tycono', 'agencies', presetId, 'roles'),
128
+ ];
129
+ for (const candidateDir of presetRoleCandidates) {
130
+ if (fs.existsSync(candidateDir)) roleDirs.push(candidateDir);
131
+ }
113
132
  }
114
133
 
115
134
  // Read all role.yaml files from all role directories
@@ -167,6 +186,27 @@ export function buildOrgTree(companyRoot: string, presetId?: string): OrgTree {
167
186
  }
168
187
  }
169
188
 
189
+ // Fallback: if no C-Level roles found, use built-in defaults
190
+ const hasCLevel = Array.from(tree.nodes.values()).some(
191
+ n => n.id !== 'ceo' && n.level === 'c-level'
192
+ );
193
+ if (!hasCLevel) {
194
+ for (const def of DEFAULT_ROLES) {
195
+ if (tree.nodes.has(def.id)) continue;
196
+ tree.nodes.set(def.id, {
197
+ id: def.id,
198
+ name: def.name,
199
+ level: def.level,
200
+ reportsTo: def.reportsTo,
201
+ children: [],
202
+ persona: def.persona,
203
+ authority: { autonomous: [], needsApproval: [] },
204
+ knowledge: { reads: [], writes: [] },
205
+ reports: { daily: '', weekly: '' },
206
+ });
207
+ }
208
+ }
209
+
170
210
  // Wire up children from reportsTo
171
211
  for (const [id, node] of tree.nodes) {
172
212
  if (id === 'ceo') continue;
@@ -407,7 +407,7 @@ else:
407
407
  */
408
408
  export class ClaudeCliRunner implements ExecutionRunner {
409
409
  execute(config: RunnerConfig, callbacks: RunnerCallbacks): RunnerHandle {
410
- const { companyRoot, roleId, task, sourceRole, orgTree, readOnly = false, teamStatus, attachments, targetRoles, presetId } = config;
410
+ const { companyRoot, roleId, task, sourceRole, orgTree, readOnly = false, teamStatus, attachments, targetRoles, presetId, priorDispatches } = config;
411
411
 
412
412
  // Note: Claude CLI doesn't support inline image attachments.
413
413
  // Images will be ignored with a warning if passed.
@@ -416,7 +416,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
416
416
  }
417
417
 
418
418
  // 1. Context Assembly
419
- const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus, targetRoles, presetId });
419
+ const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus, targetRoles, presetId, priorDispatches });
420
420
 
421
421
  // Trace: capture assembled prompt for debugging
422
422
  callbacks.onPromptAssembled?.(context.systemPrompt, task);
@@ -471,18 +471,31 @@ export class ClaudeCliRunner implements ExecutionRunner {
471
471
 
472
472
  // 6. CLI args 구성
473
473
  const maxTurns = config.maxTurns ?? 25;
474
- const args = [
475
- '-p',
476
- '--system-prompt', fs.readFileSync(promptFile, 'utf-8'),
477
- '--output-format', 'stream-json',
478
- '--verbose',
479
- '--dangerously-skip-permissions',
480
- '--model', config.model ?? 'claude-opus-4-6',
481
- '--max-turns', String(maxTurns),
482
- '--mcp-config', mcpConfig,
483
- '--strict-mcp-config',
484
- taskPrompt,
485
- ];
474
+ const isResume = !!config.cliSessionId;
475
+ const args: string[] = isResume
476
+ ? [
477
+ '--resume', config.cliSessionId!,
478
+ '-p',
479
+ '--output-format', 'stream-json',
480
+ '--verbose',
481
+ '--permission-mode', process.env.TYCONO_PERMISSION_MODE || 'bypassPermissions',
482
+ '--max-turns', String(maxTurns),
483
+ '--mcp-config', mcpConfig,
484
+ '--strict-mcp-config',
485
+ taskPrompt,
486
+ ]
487
+ : [
488
+ '-p',
489
+ '--system-prompt', fs.readFileSync(promptFile, 'utf-8'),
490
+ '--output-format', 'stream-json',
491
+ '--verbose',
492
+ '--permission-mode', process.env.TYCONO_PERMISSION_MODE || 'bypassPermissions',
493
+ '--model', config.model ?? 'claude-opus-4-6',
494
+ '--max-turns', String(maxTurns),
495
+ '--mcp-config', mcpConfig,
496
+ '--strict-mcp-config',
497
+ taskPrompt,
498
+ ];
486
499
 
487
500
  // Disallow Agent and Task tools to force use of dispatch bridge
488
501
  // For roles with subordinates (C-Level), also disallow Edit/Write to enforce delegation
@@ -526,7 +539,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
526
539
  cleanEnv.TYCONO_CODE_ROOT = codeRoot;
527
540
  cleanEnv.TYCONO_AKB_ROOT = companyRoot;
528
541
  cleanEnv.TYCONO_KNOWLEDGE_ROOT = knowledgeDir;
529
- console.log(`[Runner] Spawning claude -p: role=${roleId}, model=${modelName}, maxTurns=${maxTurns}, sessionId=${config.sessionId}, cwd=${cwd}, subordinates=[${subordinates.join(',')}]`);
542
+ console.log(`[Runner] Spawning claude ${isResume ? '--resume ' + config.cliSessionId + ' ' : ''}-p: role=${roleId}, model=${modelName}, maxTurns=${maxTurns}, sessionId=${config.sessionId}, cwd=${cwd}, subordinates=[${subordinates.join(',')}]`);
530
543
 
531
544
  const proc = spawn('claude', args, {
532
545
  cwd,
@@ -538,6 +551,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
538
551
  let turnCount = 0;
539
552
  let totalInput = 0;
540
553
  let totalOutput = 0;
554
+ let capturedCliSessionId: string | undefined;
541
555
  const toolCalls: RunnerResult['toolCalls'] = [];
542
556
  const dispatches: RunnerResult['dispatches'] = [];
543
557
  const tokenLedger = getTokenLedger(companyRoot);
@@ -567,6 +581,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
567
581
  totalTokens: { input: totalInput, output: totalOutput },
568
582
  toolCalls,
569
583
  dispatches,
584
+ cliSessionId: capturedCliSessionId,
570
585
  });
571
586
  }
572
587
  }, 5000);
@@ -592,6 +607,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
592
607
  // JobManager.startJob() now auto-emits dispatch:start on parent stream.
593
608
  },
594
609
  incrementTurn: () => { turnCount++; callbacks.onTurnComplete?.(turnCount); },
610
+ captureCliSessionId: (id) => { capturedCliSessionId = id; },
595
611
  recordTokens: (input, out) => {
596
612
  totalInput += input;
597
613
  totalOutput += out;
@@ -694,6 +710,7 @@ interface StreamHandlers {
694
710
  addToolCall: (name: string, input?: Record<string, unknown>) => void;
695
711
  incrementTurn: () => void;
696
712
  recordTokens?: (inputTokens: number, outputTokens: number) => void;
713
+ captureCliSessionId?: (id: string) => void;
697
714
  }
698
715
 
699
716
  function processStreamEvent(
@@ -755,6 +772,11 @@ function processStreamEvent(
755
772
  handlers.recordTokens(inputTk, outputTk);
756
773
  }
757
774
  }
775
+ // Capture CLI session ID for --resume support
776
+ const sid = event.session_id ?? (event as Record<string, unknown>).sessionId;
777
+ if (typeof sid === 'string' && handlers.captureCliSessionId) {
778
+ handlers.captureCliSessionId(sid);
779
+ }
758
780
  break;
759
781
  }
760
782
 
@@ -47,6 +47,10 @@ export interface RunnerConfig {
47
47
  env?: Record<string, string>;
48
48
  /** Wave-scoped preset ID for knowledge injection */
49
49
  presetId?: string;
50
+ /** Handoff summary: prior dispatch results in this wave (for context carry-over) */
51
+ priorDispatches?: Array<{ roleId: string; task: string; result: string }>;
52
+ /** CLI session ID for --resume (context continuity across turn limits) */
53
+ cliSessionId?: string;
50
54
  /** SV-7: Supervision — abort a running session */
51
55
  onAbortSession?: (sessionId: string) => boolean;
52
56
  /** SV-6: Supervision — amend a running session */
@@ -75,6 +79,8 @@ export interface RunnerResult {
75
79
  totalTokens: { input: number; output: number };
76
80
  toolCalls: Array<{ name: string; input?: Record<string, unknown> }>;
77
81
  dispatches: Array<{ roleId: string; task: string; result?: string }>;
82
+ /** CLI session ID captured from stream-json result event (for --resume) */
83
+ cliSessionId?: string;
78
84
  }
79
85
 
80
86
  /* ─── Handle (for abort support) ──────────────── */
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { execSync } from 'node:child_process';
4
5
  import { glob } from 'glob';
@@ -55,7 +56,7 @@ export async function executeTool(
55
56
  case 'bash_execute':
56
57
  return bashExecute(id, input, codeRoot ?? companyRoot);
57
58
  case 'dispatch':
58
- return await dispatchTask(id, input, onDispatch);
59
+ return await dispatchTask(id, input, onDispatch, options);
59
60
  case 'consult':
60
61
  return await consultTask(id, input, onConsult);
61
62
  case 'heartbeat_watch':
@@ -73,6 +74,22 @@ export async function executeTool(
73
74
  }
74
75
  }
75
76
 
77
+ /* ─── 2-Layer Knowledge: allowed read paths ──── */
78
+
79
+ /**
80
+ * For READ operations, agents may access agency-bundled knowledge
81
+ * in addition to the user's companyRoot (knowledge/).
82
+ * Write operations remain restricted to companyRoot only.
83
+ */
84
+ function isAllowedReadPath(absolute: string, companyRoot: string): boolean {
85
+ const allowedPaths = [
86
+ companyRoot,
87
+ path.join(companyRoot, '.tycono', 'agencies'),
88
+ path.join(os.homedir(), '.tycono', 'agencies'),
89
+ ];
90
+ return allowedPaths.some(p => absolute.startsWith(p));
91
+ }
92
+
76
93
  /* ─── Tool Implementations ───────────────────── */
77
94
 
78
95
  function readFile(
@@ -95,8 +112,8 @@ function readFile(
95
112
 
96
113
  const absolute = path.resolve(companyRoot, filePath);
97
114
 
98
- // Security: prevent path traversal
99
- if (!absolute.startsWith(companyRoot)) {
115
+ // Security: prevent path traversal (2-Layer: allow agency knowledge paths for reads)
116
+ if (!isAllowedReadPath(absolute, companyRoot)) {
100
117
  return { tool_use_id: id, content: 'Error: path traversal not allowed', is_error: true };
101
118
  }
102
119
 
@@ -134,7 +151,8 @@ function listFiles(
134
151
  }
135
152
 
136
153
  const absolute = path.resolve(companyRoot, directory);
137
- if (!absolute.startsWith(companyRoot)) {
154
+ // 2-Layer: allow agency knowledge paths for reads
155
+ if (!isAllowedReadPath(absolute, companyRoot)) {
138
156
  return { tool_use_id: id, content: 'Error: path traversal not allowed', is_error: true };
139
157
  }
140
158
 
@@ -168,7 +186,8 @@ function searchFiles(
168
186
  }
169
187
 
170
188
  const absolute = path.resolve(companyRoot, directory);
171
- if (!absolute.startsWith(companyRoot)) {
189
+ // 2-Layer: allow agency knowledge paths for reads
190
+ if (!isAllowedReadPath(absolute, companyRoot)) {
172
191
  return { tool_use_id: id, content: 'Error: path traversal not allowed', is_error: true };
173
192
  }
174
193
 
@@ -394,19 +413,44 @@ async function dispatchTask(
394
413
  id: string,
395
414
  input: Record<string, unknown>,
396
415
  onDispatch?: (roleId: string, task: string) => Promise<string>,
416
+ options?: ToolExecutorOptions,
397
417
  ): Promise<ToolResult> {
398
- const roleId = String(input.roleId ?? '');
418
+ const targetRoleId = String(input.roleId ?? '');
399
419
  const task = String(input.task ?? '');
400
420
 
401
- if (!roleId || !task) {
421
+ if (!targetRoleId || !task) {
402
422
  return { tool_use_id: id, content: 'Error: roleId and task are required', is_error: true };
403
423
  }
404
424
 
405
425
  if (!onDispatch) {
426
+ // Emit dispatch:error — dispatch not available
427
+ if (options?.sessionId) {
428
+ const stream = ActivityStream.getOrCreate(options.sessionId, options.roleId);
429
+ stream.emit('dispatch:error', options.roleId, {
430
+ sourceRole: options.roleId,
431
+ targetRole: targetRoleId,
432
+ error: 'dispatch not available in this context',
433
+ timestamp: Date.now(),
434
+ });
435
+ }
406
436
  return { tool_use_id: id, content: 'Error: dispatch not available in this context', is_error: true };
407
437
  }
408
438
 
409
- const result = await onDispatch(roleId, task);
439
+ const result = await onDispatch(targetRoleId, task);
440
+
441
+ // Detect dispatch rejection and emit dispatch:error event
442
+ if (result.startsWith('Dispatch rejected:') || result.startsWith('[DISPATCH BLOCKED]')) {
443
+ if (options?.sessionId) {
444
+ const stream = ActivityStream.getOrCreate(options.sessionId, options.roleId);
445
+ stream.emit('dispatch:error', options.roleId, {
446
+ sourceRole: options.roleId,
447
+ targetRole: targetRoleId,
448
+ error: result,
449
+ timestamp: Date.now(),
450
+ });
451
+ }
452
+ }
453
+
410
454
  return { tool_use_id: id, content: result };
411
455
  }
412
456
 
@@ -496,7 +540,10 @@ async function heartbeatWatch(
496
540
  unsubscribers.push(() => stream.unsubscribe(handler));
497
541
  }
498
542
 
499
- // Wait for duration or early return
543
+ // Pre-compute waveId for directive checking during poll loop
544
+ const waveIdForPoll = findWaveIdForSessions(sessionIds);
545
+
546
+ // Wait for duration or early return (also breaks on pending CEO directive)
500
547
  await new Promise<void>((resolve) => {
501
548
  const timeout = setTimeout(resolve, durationSec * 1000);
502
549
  const checkInterval = setInterval(() => {
@@ -504,6 +551,15 @@ async function heartbeatWatch(
504
551
  clearTimeout(timeout);
505
552
  clearInterval(checkInterval);
506
553
  resolve();
554
+ return;
555
+ }
556
+ // Early-return on pending CEO directive — don't block for 180s when user is waiting
557
+ if (waveIdForPoll && supervisorHeartbeat.getPendingDirectives(waveIdForPoll).length > 0) {
558
+ earlyReturn = true;
559
+ clearTimeout(timeout);
560
+ clearInterval(checkInterval);
561
+ resolve();
562
+ return;
507
563
  }
508
564
  }, 500); // Check every 500ms
509
565
  // Ensure cleanup even if early