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.
- package/README.md +191 -162
- package/bin/tycono.ts +42 -10
- package/package.json +21 -15
- package/packages/server/bin/cli.js +35 -0
- package/packages/server/bin/server.ts +183 -0
- package/{src → packages/server/src}/api/src/create-server.ts +11 -3
- package/{src → packages/server/src}/api/src/engine/agent-loop.ts +30 -7
- package/{src → packages/server/src}/api/src/engine/context-assembler.ts +122 -57
- package/{src → packages/server/src}/api/src/engine/llm-adapter.ts +10 -7
- package/{src → packages/server/src}/api/src/engine/org-tree.ts +43 -3
- package/{src → packages/server/src}/api/src/engine/runners/claude-cli.ts +37 -15
- package/{src → packages/server/src}/api/src/engine/runners/types.ts +6 -0
- package/{src → packages/server/src}/api/src/engine/tools/executor.ts +65 -9
- package/{src → packages/server/src}/api/src/routes/execute.ts +221 -17
- package/packages/server/src/api/src/services/claude-md-manager.ts +190 -0
- package/{src → packages/server/src}/api/src/services/company-config.ts +1 -0
- package/{src → packages/server/src}/api/src/services/digest-engine.ts +4 -1
- package/packages/server/src/api/src/services/dispatch-classifier.ts +179 -0
- package/{src → packages/server/src}/api/src/services/execution-manager.ts +227 -21
- package/{src → packages/server/src}/api/src/services/file-reader.ts +4 -1
- package/packages/server/src/api/src/services/preset-loader.ts +310 -0
- package/{src → packages/server/src}/api/src/services/supervisor-heartbeat.ts +89 -9
- package/{src → packages/server/src}/api/src/services/wave-multiplexer.ts +18 -8
- package/{src → packages/server/src}/api/src/services/wave-tracker.ts +25 -0
- package/packages/server/src/core/scaffolder.ts +620 -0
- package/{src → packages/server/src}/shared/types.ts +3 -1
- package/packages/server/templates/CLAUDE.md.tmpl +152 -0
- package/packages/server/templates/agentic-knowledge-base.md +355 -0
- package/src/api/src/services/claude-md-manager.ts +0 -94
- package/src/api/src/services/preset-loader.ts +0 -149
- package/templates/CLAUDE.md.tmpl +0 -239
- /package/{src/web → packages/pixel}/dist/assets/index-BJyiMGkM.js +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-BOuHc64o.css +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-DDPzbp9E.js +0 -0
- /package/{src/web → packages/pixel}/dist/assets/index-DVKWFwwK.css +0 -0
- /package/{src/web → packages/pixel}/dist/assets/preview-app-DZ6WxhDc.js +0 -0
- /package/{src/web → packages/pixel}/dist/index.html +0 -0
- /package/{src/web → packages/pixel}/dist/tyconoforge.js +0 -0
- /package/{src → packages/server/src}/api/package.json +0 -0
- /package/{src → packages/server/src}/api/src/create-app.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/authority-validator.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/index.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/knowledge-gate.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/role-lifecycle.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/runners/direct-api.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/runners/index.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/skill-template.ts +0 -0
- /package/{src → packages/server/src}/api/src/engine/tools/definitions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/active-sessions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/coins.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/company.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/cost.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/engine.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/git.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/knowledge.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/operations.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/preferences.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/presets.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/projects.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/quests.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/roles.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/save.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/sessions.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/setup.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/skills.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/speech.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/supervision.ts +0 -0
- /package/{src → packages/server/src}/api/src/routes/sync.ts +0 -0
- /package/{src → packages/server/src}/api/src/server.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/activity-stream.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/activity-tracker.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/database.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/git-save.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/job-manager.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/knowledge-importer.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/markdown-parser.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/port-registry.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/preferences.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/pricing.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/scaffold.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/session-store.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/team-recommender.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/token-ledger.ts +0 -0
- /package/{src → packages/server/src}/api/src/services/wave-messages.ts +0 -0
- /package/{src → packages/server/src}/api/src/utils/role-level.ts +0 -0
- /package/{templates → packages/server/templates}/company.md.tmpl +0 -0
- /package/{templates → packages/server/templates}/gitignore.tmpl +0 -0
- /package/{templates → packages/server/templates}/roles.md.tmpl +0 -0
- /package/{templates → packages/server/templates}/skills/_manifest.json +0 -0
- /package/{templates → packages/server/templates}/skills/agent-browser/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/agent-browser/meta.json +0 -0
- /package/{templates → packages/server/templates}/skills/akb-linter/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/akb-linter/meta.json +0 -0
- /package/{templates → packages/server/templates}/skills/knowledge-gate/SKILL.md +0 -0
- /package/{templates → packages/server/templates}/skills/knowledge-gate/meta.json +0 -0
- /package/{templates → packages/server/templates}/teams/agency.json +0 -0
- /package/{templates → packages/server/templates}/teams/research.json +0 -0
- /package/{templates → packages/server/templates}/teams/startup.json +0 -0
- /package/{src/tui → packages/tui/src}/api.ts +0 -0
- /package/{src/tui → packages/tui/src}/app.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/CommandMode.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/OrgTree.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/PanelMode.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/SetupWizard.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/StatusBar.tsx +0 -0
- /package/{src/tui → packages/tui/src}/components/StreamView.tsx +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useApi.ts +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useCommand.ts +0 -0
- /package/{src/tui → packages/tui/src}/hooks/useSSE.ts +0 -0
- /package/{src/tui → packages/tui/src}/index.tsx +0 -0
- /package/{src/tui → packages/tui/src}/store.ts +0 -0
- /package/{src/tui → packages/tui/src}/theme.ts +0 -0
- /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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
'--
|
|
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
|
|
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
|
|
112
|
-
|
|
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
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
418
|
+
const targetRoleId = String(input.roleId ?? '');
|
|
399
419
|
const task = String(input.task ?? '');
|
|
400
420
|
|
|
401
|
-
if (!
|
|
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(
|
|
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
|
-
//
|
|
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
|