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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const VERSION = JSON.parse(
|
|
11
|
+
fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')
|
|
12
|
+
).version;
|
|
13
|
+
|
|
14
|
+
function printHelp(): void {
|
|
15
|
+
console.log(`
|
|
16
|
+
tycono-server v${VERSION}
|
|
17
|
+
|
|
18
|
+
AI team orchestration server. Headless by default.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
tycono-server [path] Start API server (default: current directory)
|
|
22
|
+
tycono-server --help Show this help
|
|
23
|
+
tycono-server --version Show version
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
tycono-server Start in current directory
|
|
27
|
+
tycono-server ./my-company Start with company directory
|
|
28
|
+
PORT=3001 tycono-server Use specific port
|
|
29
|
+
|
|
30
|
+
Environment:
|
|
31
|
+
PORT Server port (default: auto-detect free port)
|
|
32
|
+
COMPANY_ROOT Company directory (default: current directory)
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findFreePort(): Promise<number> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const server = net.createServer();
|
|
39
|
+
server.listen(0, () => {
|
|
40
|
+
const addr = server.address();
|
|
41
|
+
if (addr && typeof addr === 'object') {
|
|
42
|
+
const port = addr.port;
|
|
43
|
+
server.close(() => resolve(port));
|
|
44
|
+
} else {
|
|
45
|
+
reject(new Error('Could not find free port'));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
server.on('error', reject);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectEngine(): string {
|
|
53
|
+
try {
|
|
54
|
+
execSync('claude --version', { stdio: 'pipe' });
|
|
55
|
+
return 'claude-cli';
|
|
56
|
+
} catch {}
|
|
57
|
+
if (process.env.ANTHROPIC_API_KEY) return 'direct-api';
|
|
58
|
+
return 'none';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function main(args: string[]): Promise<void> {
|
|
62
|
+
const command = args[0];
|
|
63
|
+
|
|
64
|
+
if (command === '--help' || command === '-h') {
|
|
65
|
+
printHelp();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (command === '--version' || command === '-v') {
|
|
70
|
+
console.log(VERSION);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Load .env
|
|
75
|
+
const dotenvPath = path.resolve(process.cwd(), '.env');
|
|
76
|
+
if (fs.existsSync(dotenvPath)) {
|
|
77
|
+
const { config } = await import('dotenv');
|
|
78
|
+
config({ path: dotenvPath });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Company root
|
|
82
|
+
if (command && !command.startsWith('-')) {
|
|
83
|
+
process.env.COMPANY_ROOT = path.resolve(command);
|
|
84
|
+
}
|
|
85
|
+
if (!process.env.COMPANY_ROOT) {
|
|
86
|
+
process.env.COMPANY_ROOT = process.cwd();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for knowledge/ subdirectory pattern
|
|
90
|
+
const knowledgeDir = path.join(process.env.COMPANY_ROOT, 'knowledge');
|
|
91
|
+
if (fs.existsSync(path.join(knowledgeDir, 'CLAUDE.md'))) {
|
|
92
|
+
// Company root is the parent of knowledge/
|
|
93
|
+
} else if (!fs.existsSync(path.join(process.env.COMPANY_ROOT, 'CLAUDE.md'))) {
|
|
94
|
+
// Scan one level deep
|
|
95
|
+
try {
|
|
96
|
+
const entries = fs.readdirSync(process.env.COMPANY_ROOT, { withFileTypes: true });
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
99
|
+
if (fs.existsSync(path.join(process.env.COMPANY_ROOT, entry.name, '.tycono', 'config.json'))) {
|
|
100
|
+
process.env.COMPANY_ROOT = path.join(process.env.COMPANY_ROOT, entry.name);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.env.NODE_ENV = 'production';
|
|
108
|
+
|
|
109
|
+
// Detect engine
|
|
110
|
+
const engine = detectEngine();
|
|
111
|
+
process.env.EXECUTION_ENGINE = engine;
|
|
112
|
+
|
|
113
|
+
// Port
|
|
114
|
+
const port = process.env.PORT ? Number(process.env.PORT) : await findFreePort();
|
|
115
|
+
process.env.PORT = String(port);
|
|
116
|
+
|
|
117
|
+
// Company name
|
|
118
|
+
let companyName = 'My Company';
|
|
119
|
+
try {
|
|
120
|
+
const claudeMdPath = path.join(process.env.COMPANY_ROOT, 'CLAUDE.md');
|
|
121
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
122
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
123
|
+
const match = content.match(/^#\s+(.+)/m);
|
|
124
|
+
if (match) companyName = match[1].trim();
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
|
|
128
|
+
const url = `http://localhost:${port}`;
|
|
129
|
+
|
|
130
|
+
console.log(` tycono-server v${VERSION}`);
|
|
131
|
+
console.log(` API: ${url}`);
|
|
132
|
+
console.log(` Company: ${companyName}`);
|
|
133
|
+
console.log(` Engine: ${engine}`);
|
|
134
|
+
console.log(` PID: ${process.pid}`);
|
|
135
|
+
|
|
136
|
+
// Write headless.json for discovery
|
|
137
|
+
const headlessInfo = { port, pid: process.pid, url, startedAt: new Date().toISOString() };
|
|
138
|
+
const headlessPath = path.join(process.env.COMPANY_ROOT, '.tycono', 'headless.json');
|
|
139
|
+
try {
|
|
140
|
+
fs.mkdirSync(path.dirname(headlessPath), { recursive: true });
|
|
141
|
+
fs.writeFileSync(headlessPath, JSON.stringify(headlessInfo, null, 2));
|
|
142
|
+
} catch {}
|
|
143
|
+
|
|
144
|
+
// Start server
|
|
145
|
+
const { createHttpServer } = await import('../src/api/src/create-server.js');
|
|
146
|
+
const server = createHttpServer();
|
|
147
|
+
|
|
148
|
+
const host = process.env.HOST || '0.0.0.0';
|
|
149
|
+
server.listen(port, host);
|
|
150
|
+
|
|
151
|
+
// Graceful shutdown with active wave guard (BUG-CONCURRENT protection)
|
|
152
|
+
let forceShutdown = false;
|
|
153
|
+
let forceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
154
|
+
|
|
155
|
+
const doShutdown = () => {
|
|
156
|
+
console.log('\n Shutting down...');
|
|
157
|
+
try { fs.unlinkSync(headlessPath); } catch {}
|
|
158
|
+
server.close(() => process.exit(0));
|
|
159
|
+
setTimeout(() => process.exit(1), 5000);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const shutdown = async () => {
|
|
163
|
+
try {
|
|
164
|
+
const { getActiveWaveCount } = await import('../src/api/src/create-server.js');
|
|
165
|
+
const activeCount = getActiveWaveCount();
|
|
166
|
+
|
|
167
|
+
if (activeCount > 0 && !forceShutdown) {
|
|
168
|
+
console.log(`\n ⚠️ ${activeCount} active wave(s) running. Press Ctrl+C again within 5s to force shutdown.`);
|
|
169
|
+
forceShutdown = true;
|
|
170
|
+
forceTimer = setTimeout(() => { forceShutdown = false; }, 5000);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// If we can't check, proceed with shutdown
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
178
|
+
doShutdown();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
process.on('SIGINT', shutdown);
|
|
182
|
+
process.on('SIGTERM', doShutdown);
|
|
183
|
+
}
|
|
@@ -39,11 +39,18 @@ import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
|
|
|
39
39
|
import { readConfig } from './services/company-config.js';
|
|
40
40
|
import { ensureClaudeMd } from './services/claude-md-manager.js';
|
|
41
41
|
|
|
42
|
+
import { supervisorHeartbeat } from './services/supervisor-heartbeat.js';
|
|
43
|
+
|
|
42
44
|
const __filename = fileURLToPath(import.meta.url);
|
|
43
45
|
const __dirname = path.dirname(__filename);
|
|
44
46
|
const isProd = process.env.NODE_ENV === 'production';
|
|
45
47
|
const corsOrigin = isProd ? true : /^http:\/\/localhost:\d+$/;
|
|
46
48
|
|
|
49
|
+
/** Get count of active waves (for shutdown guard) */
|
|
50
|
+
export function getActiveWaveCount(): number {
|
|
51
|
+
return supervisorHeartbeat.listActive().length;
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
/* ─── Raw HTTP handler for import-knowledge SSE ─── */
|
|
48
55
|
|
|
49
56
|
function handleImportKnowledge(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
@@ -101,7 +108,8 @@ export function createHttpServer(): http.Server {
|
|
|
101
108
|
const app = createExpressApp();
|
|
102
109
|
|
|
103
110
|
const server = http.createServer((req, res) => {
|
|
104
|
-
const
|
|
111
|
+
const rawUrl = req.url ?? '';
|
|
112
|
+
const url = rawUrl.split('?')[0]; // Strip query string for route matching
|
|
105
113
|
const method = req.method ?? '';
|
|
106
114
|
|
|
107
115
|
// GET /api/waves/active — restore active waves after refresh
|
|
@@ -150,8 +158,8 @@ export function createHttpServer(): http.Server {
|
|
|
150
158
|
return;
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
// Non-SSE exec/jobs endpoints (GET, DELETE)
|
|
154
|
-
if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs')) && (method === 'GET' || method === 'DELETE')) {
|
|
161
|
+
// Non-SSE exec/jobs/waves endpoints (GET, DELETE)
|
|
162
|
+
if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url.startsWith('/api/waves/')) && (method === 'GET' || method === 'DELETE')) {
|
|
155
163
|
setExecCors(req, res);
|
|
156
164
|
handleExecRequest(req, res);
|
|
157
165
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnthropicProvider, type LLMProvider, type LLMMessage, type ToolResult, type MessageContent } from './llm-adapter.js';
|
|
1
|
+
import { AnthropicProvider, type LLMProvider, type LLMMessage, type ToolResult, type MessageContent, type SystemPrompt } from './llm-adapter.js';
|
|
2
2
|
import { type OrgTree, getSubordinates } from './org-tree.js';
|
|
3
3
|
import { assembleContext, type TeamStatus } from './context-assembler.js';
|
|
4
4
|
import { validateDispatch, validateConsult } from './authority-validator.js';
|
|
@@ -30,6 +30,7 @@ export interface AgentConfig {
|
|
|
30
30
|
attachments?: ImageAttachment[]; // Image attachments for vision
|
|
31
31
|
targetRoles?: string[]; // Selective dispatch scope
|
|
32
32
|
presetId?: string; // Wave-scoped preset for knowledge injection
|
|
33
|
+
priorDispatches?: Array<{ roleId: string; task: string; result: string }>; // Handoff summary
|
|
33
34
|
// Callbacks
|
|
34
35
|
onText?: (text: string) => void;
|
|
35
36
|
onToolExec?: (name: string, input: Record<string, unknown>) => void;
|
|
@@ -163,11 +164,32 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
163
164
|
const llm = config.llm ?? new AnthropicProvider();
|
|
164
165
|
|
|
165
166
|
// 1. Assemble context
|
|
166
|
-
const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus: config.teamStatus, targetRoles: config.targetRoles, presetId: config.presetId });
|
|
167
|
+
const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus: config.teamStatus, targetRoles: config.targetRoles, presetId: config.presetId, priorDispatches: config.priorDispatches });
|
|
167
168
|
|
|
168
169
|
// Trace: capture assembled prompt for debugging
|
|
169
170
|
config.onPromptAssembled?.(context.systemPrompt, task);
|
|
170
171
|
|
|
172
|
+
// Build cached system prompt from structured sections (for Anthropic prompt caching)
|
|
173
|
+
const cachedPrompt: SystemPrompt = (() => {
|
|
174
|
+
const blocks: Array<{ type: 'text'; text: string; cache_control?: { type: 'ephemeral' } }> = [];
|
|
175
|
+
let staticBuf = '';
|
|
176
|
+
for (const s of context.sections) {
|
|
177
|
+
if (s.cacheable) {
|
|
178
|
+
staticBuf += (staticBuf ? '\n\n---\n\n' : '') + s.text;
|
|
179
|
+
} else {
|
|
180
|
+
if (staticBuf) {
|
|
181
|
+
blocks.push({ type: 'text', text: staticBuf, cache_control: { type: 'ephemeral' } });
|
|
182
|
+
staticBuf = '';
|
|
183
|
+
}
|
|
184
|
+
blocks.push({ type: 'text', text: s.text });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (staticBuf) {
|
|
188
|
+
blocks.push({ type: 'text', text: staticBuf, cache_control: { type: 'ephemeral' } });
|
|
189
|
+
}
|
|
190
|
+
return blocks.length > 0 ? blocks : context.systemPrompt;
|
|
191
|
+
})();
|
|
192
|
+
|
|
171
193
|
// 2. Determine tools
|
|
172
194
|
const subordinates = getSubordinates(orgTree, roleId);
|
|
173
195
|
const hasBash = !readOnly && !!config.codeRoot;
|
|
@@ -221,6 +243,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
221
243
|
sessionId: config.sessionId,
|
|
222
244
|
model: config.model,
|
|
223
245
|
tokenLedger: config.tokenLedger,
|
|
246
|
+
priorDispatches: dispatches, // Handoff: pass accumulated results to sub-agent
|
|
224
247
|
onText: (text) => onText?.(`[${targetRoleId}] ${text}`),
|
|
225
248
|
onToolExec,
|
|
226
249
|
});
|
|
@@ -323,7 +346,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
323
346
|
}
|
|
324
347
|
|
|
325
348
|
// Call LLM
|
|
326
|
-
const response = await llm.chat(
|
|
349
|
+
const response = await llm.chat(cachedPrompt, messages, tools, abortSignal);
|
|
327
350
|
totalInput += response.usage.inputTokens;
|
|
328
351
|
totalOutput += response.usage.outputTokens;
|
|
329
352
|
|
|
@@ -496,7 +519,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
496
519
|
if (abortSignal?.aborted) break;
|
|
497
520
|
turns++;
|
|
498
521
|
|
|
499
|
-
const supResponse = await llm.chat(
|
|
522
|
+
const supResponse = await llm.chat(cachedPrompt, messages, tools, abortSignal);
|
|
500
523
|
totalInput += supResponse.usage.inputTokens;
|
|
501
524
|
totalOutput += supResponse.usage.outputTokens;
|
|
502
525
|
config.tokenLedger?.record({
|
|
@@ -576,7 +599,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
576
599
|
|
|
577
600
|
if (turns < maxTurns) {
|
|
578
601
|
turns++;
|
|
579
|
-
const verifyResponse = await llm.chat(
|
|
602
|
+
const verifyResponse = await llm.chat(cachedPrompt, messages, tools, abortSignal);
|
|
580
603
|
totalInput += verifyResponse.usage.inputTokens;
|
|
581
604
|
totalOutput += verifyResponse.usage.outputTokens;
|
|
582
605
|
config.tokenLedger?.record({
|
|
@@ -622,7 +645,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
622
645
|
|
|
623
646
|
if (turns < maxTurns) {
|
|
624
647
|
turns++;
|
|
625
|
-
const summaryResponse = await llm.chat(
|
|
648
|
+
const summaryResponse = await llm.chat(cachedPrompt, messages, tools, abortSignal);
|
|
626
649
|
totalInput += summaryResponse.usage.inputTokens;
|
|
627
650
|
totalOutput += summaryResponse.usage.outputTokens;
|
|
628
651
|
config.tokenLedger?.record({
|
|
@@ -676,7 +699,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
676
699
|
if (abortSignal?.aborted) break;
|
|
677
700
|
turns++;
|
|
678
701
|
|
|
679
|
-
const postKResponse = await llm.chat(
|
|
702
|
+
const postKResponse = await llm.chat(cachedPrompt, messages, tools, abortSignal);
|
|
680
703
|
totalInput += postKResponse.usage.inputTokens;
|
|
681
704
|
totalOutput += postKResponse.usage.outputTokens;
|
|
682
705
|
config.tokenLedger?.record({
|
|
@@ -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 { readPreferences } from '../services/preferences.js';
|
|
4
5
|
import { readConfig, resolveCodeRoot } from '../services/company-config.js';
|
|
@@ -14,8 +15,15 @@ import { extractKeywords, searchRelatedDocs } from './knowledge-gate.js';
|
|
|
14
15
|
|
|
15
16
|
/* ─── Types ──────────────────────────────────── */
|
|
16
17
|
|
|
18
|
+
export interface PromptSection {
|
|
19
|
+
text: string;
|
|
20
|
+
cacheable: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export interface AssembledContext {
|
|
18
24
|
systemPrompt: string;
|
|
25
|
+
/** Structured sections for prompt caching — cacheable sections get cache_control */
|
|
26
|
+
sections: PromptSection[];
|
|
19
27
|
task: string;
|
|
20
28
|
sourceRole: string;
|
|
21
29
|
targetRole: string;
|
|
@@ -51,60 +59,62 @@ export function assembleContext(
|
|
|
51
59
|
task: string,
|
|
52
60
|
sourceRole: string,
|
|
53
61
|
orgTree: OrgTree,
|
|
54
|
-
options?: { teamStatus?: TeamStatus; targetRoles?: string[]; presetId?: string },
|
|
62
|
+
options?: { teamStatus?: TeamStatus; targetRoles?: string[]; presetId?: string; priorDispatches?: Array<{ roleId: string; task: string; result: string }> },
|
|
55
63
|
): AssembledContext {
|
|
56
64
|
const node = orgTree.nodes.get(roleId);
|
|
57
65
|
if (!node) {
|
|
58
66
|
throw new Error(`Role not found in org tree: ${roleId}`);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
const
|
|
69
|
+
const taggedSections: PromptSection[] = [];
|
|
70
|
+
// Helper: push with cacheability tag
|
|
71
|
+
const pushSection = (text: string, cacheable: boolean) => taggedSections.push({ text, cacheable });
|
|
62
72
|
|
|
63
|
-
// 1. Company Rules (CLAUDE.md + custom-rules.md + company.md)
|
|
73
|
+
// 1. Company Rules (CLAUDE.md + custom-rules.md + company.md) — CACHEABLE
|
|
64
74
|
const companyRules = loadCompanyRules(companyRoot);
|
|
65
75
|
if (companyRules) {
|
|
66
|
-
|
|
76
|
+
pushSection(companyRules, true);
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
// 2. Org Context
|
|
70
|
-
|
|
79
|
+
// 2. Org Context — CACHEABLE (static per role)
|
|
80
|
+
pushSection(buildOrgContextSection(orgTree, node), true);
|
|
71
81
|
|
|
72
|
-
// 3. Role Persona
|
|
73
|
-
|
|
82
|
+
// 3. Role Persona — CACHEABLE
|
|
83
|
+
pushSection(buildPersonaSection(node), true);
|
|
74
84
|
|
|
75
|
-
// 4. Authority Rules
|
|
76
|
-
|
|
85
|
+
// 4. Authority Rules — CACHEABLE
|
|
86
|
+
pushSection(buildAuthoritySection(node), true);
|
|
77
87
|
|
|
78
|
-
// 5. Knowledge Scope
|
|
79
|
-
|
|
88
|
+
// 5. Knowledge Scope — CACHEABLE
|
|
89
|
+
pushSection(buildKnowledgeSection(node), true);
|
|
80
90
|
|
|
81
|
-
// 6. SKILL.md (Role-specific + equipped shared skills)
|
|
91
|
+
// 6. SKILL.md (Role-specific + equipped shared skills) — CACHEABLE
|
|
82
92
|
const skillContent = loadSkillMd(companyRoot, roleId);
|
|
83
93
|
if (skillContent) {
|
|
84
|
-
|
|
94
|
+
pushSection('# Skills & Tools\n\n' + skillContent, true);
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
// 6b. Shared Skills (from role.yaml skills field)
|
|
97
|
+
// 6b. Shared Skills (from role.yaml skills field) — CACHEABLE
|
|
88
98
|
const sharedSkills = loadSharedSkills(companyRoot, node.skills);
|
|
89
99
|
if (sharedSkills) {
|
|
90
|
-
|
|
100
|
+
pushSection('# Equipped Skills\n\n' + sharedSkills, true);
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
// 7. Hub Docs (요약)
|
|
103
|
+
// 7. Hub Docs (요약) — CACHEABLE
|
|
94
104
|
const hubSummary = loadHubSummaries(companyRoot, node);
|
|
95
105
|
if (hubSummary) {
|
|
96
|
-
|
|
106
|
+
pushSection('# Reference Documents\n\n' + hubSummary, true);
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
// 8. CEO Decisions (전사 공지)
|
|
109
|
+
// 8. CEO Decisions (전사 공지) — CACHEABLE
|
|
100
110
|
const ceoDecisions = loadCeoDecisions(companyRoot);
|
|
101
111
|
if (ceoDecisions) {
|
|
102
|
-
|
|
112
|
+
pushSection('# CEO Decisions (전사 공지)\n\n' + ceoDecisions, true);
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
// 9. Code Root (코드 프로젝트 경로)
|
|
115
|
+
// 9. Code Root (코드 프로젝트 경로) — CACHEABLE (static per company)
|
|
106
116
|
const codeRoot = resolveCodeRoot(companyRoot);
|
|
107
|
-
|
|
117
|
+
pushSection(`# Code Project
|
|
108
118
|
|
|
109
119
|
The code repository is located at: \`${codeRoot}\` (env: $TYCONO_CODE_ROOT)
|
|
110
120
|
The AKB (knowledge) directory is at: \`${companyRoot}\` (env: $TYCONO_AKB_ROOT)
|
|
@@ -115,19 +125,36 @@ Use the code repository path for all source code work (reading, writing, buildin
|
|
|
115
125
|
- Your cwd is already set to the code repository. When creating worktrees, use relative paths or \`$TYCONO_CODE_ROOT\`.
|
|
116
126
|
- **NEVER run \`git worktree add\` in \`$TYCONO_AKB_ROOT\`** — the AKB directory is not a code repository.
|
|
117
127
|
- Recommended worktree path: \`$TYCONO_CODE_ROOT/.worktrees/{branch-name}\`
|
|
118
|
-
- Example: \`git worktree add .worktrees/feature-xyz -b feature/xyz\` (from cwd, which is already code repo)
|
|
128
|
+
- Example: \`git worktree add .worktrees/feature-xyz -b feature/xyz\` (from cwd, which is already code repo)`, true);
|
|
119
129
|
|
|
120
|
-
// 10. Pre-Knowledging: 작업 관련 문서 자동 탐색
|
|
130
|
+
// 10. Pre-Knowledging: 작업 관련 문서 자동 탐색 — NOT cacheable (task-dependent)
|
|
121
131
|
const preKSection = buildPreKnowledgingSection(companyRoot, task);
|
|
122
132
|
if (preKSection) {
|
|
123
|
-
|
|
133
|
+
pushSection(preKSection, false);
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
// 11. Preset Knowledge (wave-scoped preset docs)
|
|
136
|
+
// 11. Preset Knowledge (wave-scoped preset docs) — CACHEABLE (static per wave)
|
|
127
137
|
if (options?.presetId && options.presetId !== 'default') {
|
|
128
138
|
const presetKnowledge = loadPresetKnowledge(companyRoot, options.presetId);
|
|
129
139
|
if (presetKnowledge) {
|
|
130
|
-
|
|
140
|
+
pushSection('# Preset Knowledge\n\n' + presetKnowledge, true);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 12. Handoff Summary — NOT cacheable (dynamic per dispatch)
|
|
145
|
+
if (options?.priorDispatches && options.priorDispatches.length > 0) {
|
|
146
|
+
const sameRole = options.priorDispatches.filter(d => d.roleId === roleId);
|
|
147
|
+
if (sameRole.length > 0) {
|
|
148
|
+
const summaries = sameRole.map((d, i) =>
|
|
149
|
+
`### Session ${i + 1}\n**Task**: ${d.task.slice(0, 200)}\n**Result**: ${d.result.slice(0, 500)}`
|
|
150
|
+
).join('\n\n');
|
|
151
|
+
pushSection(`# Previous Session Results (Handoff Summary)
|
|
152
|
+
|
|
153
|
+
This role has been dispatched ${sameRole.length} time(s) before in this wave. **DO NOT repeat completed work.**
|
|
154
|
+
|
|
155
|
+
${summaries}
|
|
156
|
+
|
|
157
|
+
> Build on these results. Skip investigation that was already done. Focus on NEW or UNFINISHED items only.`, false);
|
|
131
158
|
}
|
|
132
159
|
}
|
|
133
160
|
|
|
@@ -140,23 +167,23 @@ Use the code repository path for all source code work (reading, writing, buildin
|
|
|
140
167
|
subordinates = subordinates.filter(id => options.targetRoles!.includes(id));
|
|
141
168
|
}
|
|
142
169
|
|
|
143
|
-
// Supervision prompt (SV-11, SV-12: C-Level heartbeat mode)
|
|
170
|
+
// Supervision prompt (SV-11, SV-12: C-Level heartbeat mode) — CACHEABLE
|
|
144
171
|
const heartbeatEnabled = node.heartbeat?.enabled === true;
|
|
145
172
|
if (heartbeatEnabled && subordinates.length > 0) {
|
|
146
|
-
|
|
173
|
+
pushSection(buildSupervisionSection(node), true);
|
|
147
174
|
}
|
|
148
175
|
|
|
149
|
-
// Knowledge consistency management (C-Level responsibility)
|
|
176
|
+
// Knowledge consistency management (C-Level responsibility) — CACHEABLE
|
|
150
177
|
if (node.level === 'c-level') {
|
|
151
|
-
|
|
178
|
+
pushSection(buildKnowledgeManagementSection(roleId), true);
|
|
152
179
|
}
|
|
153
180
|
|
|
154
|
-
// Dispatch 도구 안내 (하위 Role이 있는 경우)
|
|
181
|
+
// Dispatch 도구 안내 (하위 Role이 있는 경우) — NOT cacheable (teamStatus varies)
|
|
155
182
|
if (subordinates.length > 0) {
|
|
156
|
-
|
|
183
|
+
pushSection(buildDispatchSection(orgTree, roleId, subordinates, options?.teamStatus), false);
|
|
157
184
|
} else if (node.level === 'c-level') {
|
|
158
185
|
// C-level with no subordinates — clarify authority boundaries
|
|
159
|
-
|
|
186
|
+
pushSection(`# Team Structure
|
|
160
187
|
|
|
161
188
|
⚠️ **You have no direct reports.** You are an individual contributor at the C-level.
|
|
162
189
|
|
|
@@ -164,21 +191,21 @@ Use the code repository path for all source code work (reading, writing, buildin
|
|
|
164
191
|
- You CAN consult other roles for information (see Consult section below)
|
|
165
192
|
- You MUST do the work yourself — research, analyze, write, decide
|
|
166
193
|
- If implementation requires another role (e.g., engineering work), recommend it to CEO
|
|
167
|
-
- Make decisions within your authority autonomously — do NOT ask CEO for decisions you can make yourself
|
|
194
|
+
- Make decisions within your authority autonomously — do NOT ask CEO for decisions you can make yourself`, true);
|
|
168
195
|
}
|
|
169
196
|
|
|
170
|
-
// Consult 도구 안내
|
|
197
|
+
// Consult 도구 안내 — CACHEABLE
|
|
171
198
|
const consultSection = buildConsultSection(orgTree, roleId);
|
|
172
199
|
if (consultSection) {
|
|
173
|
-
|
|
200
|
+
pushSection(consultSection, true);
|
|
174
201
|
}
|
|
175
202
|
|
|
176
|
-
// Language preference
|
|
203
|
+
// Language preference — CACHEABLE
|
|
177
204
|
const prefs = readPreferences(companyRoot);
|
|
178
205
|
const lang = prefs.language && prefs.language !== 'auto' ? prefs.language : 'en';
|
|
179
206
|
const langNames: Record<string, string> = { en: 'English', ko: 'Korean (한국어)', ja: 'Japanese (日本語)' };
|
|
180
207
|
const langName = langNames[lang] ?? lang;
|
|
181
|
-
|
|
208
|
+
pushSection(`# Language (CRITICAL)
|
|
182
209
|
|
|
183
210
|
You MUST respond in **${langName}**.
|
|
184
211
|
|
|
@@ -190,10 +217,10 @@ This applies to ALL output without exception:
|
|
|
190
217
|
- Git commit messages and PR descriptions
|
|
191
218
|
|
|
192
219
|
Code (variable names, comments in code) may remain in English for readability.
|
|
193
|
-
Everything else MUST be in ${langName}
|
|
220
|
+
Everything else MUST be in ${langName}.`, true);
|
|
194
221
|
|
|
195
|
-
// Execution behavior rules
|
|
196
|
-
|
|
222
|
+
// Execution behavior rules — CACHEABLE
|
|
223
|
+
pushSection(`# Execution Rules (CRITICAL)
|
|
197
224
|
|
|
198
225
|
## Interpreting Tasks
|
|
199
226
|
- A [CEO Wave] is a directive from the CEO. Interpret it based on your role's expertise.
|
|
@@ -229,12 +256,13 @@ Everything else MUST be in ${langName}.`);
|
|
|
229
256
|
- Use a descriptive commit message: \`git commit -m "type(scope): description"\`
|
|
230
257
|
- Common types: feat, fix, refactor, test, chore
|
|
231
258
|
- This ensures your work is not lost in uncommitted changes.
|
|
232
|
-
- Do NOT push — just commit locally. Your superior or the system handles push/PR
|
|
259
|
+
- Do NOT push — just commit locally. Your superior or the system handles push/PR.`, true);
|
|
233
260
|
|
|
234
|
-
const systemPrompt =
|
|
261
|
+
const systemPrompt = taggedSections.map(s => s.text).join('\n\n---\n\n');
|
|
235
262
|
|
|
236
263
|
return {
|
|
237
264
|
systemPrompt,
|
|
265
|
+
sections: taggedSections,
|
|
238
266
|
task,
|
|
239
267
|
sourceRole,
|
|
240
268
|
targetRole: roleId,
|
|
@@ -271,22 +299,43 @@ ${docList}
|
|
|
271
299
|
}
|
|
272
300
|
|
|
273
301
|
/**
|
|
274
|
-
* Load knowledge docs from a preset's knowledge/ directory.
|
|
302
|
+
* Load knowledge docs from a preset/agency's knowledge/ directory.
|
|
303
|
+
* 2-Layer Knowledge: merges docs from multiple sources (deduplicated by filename).
|
|
304
|
+
*
|
|
305
|
+
* Search order:
|
|
306
|
+
* 1. {companyRoot}/knowledge/presets/{presetId}/knowledge/ (legacy/local presets)
|
|
307
|
+
* 2. {companyRoot}/.tycono/agencies/{presetId}/knowledge/ (local agency install)
|
|
308
|
+
* 3. ~/.tycono/agencies/{presetId}/knowledge/ (global agency install)
|
|
309
|
+
*
|
|
275
310
|
* Returns concatenated content (capped at 2000 chars per doc).
|
|
276
311
|
*/
|
|
277
312
|
function loadPresetKnowledge(companyRoot: string, presetId: string): string | null {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
313
|
+
const candidateDirs = [
|
|
314
|
+
path.join(companyRoot, 'knowledge', 'presets', presetId, 'knowledge'),
|
|
315
|
+
path.join(companyRoot, '.tycono', 'agencies', presetId, 'knowledge'),
|
|
316
|
+
path.join(os.homedir(), '.tycono', 'agencies', presetId, 'knowledge'),
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
// Collect docs from all sources, deduplicate by filename (first wins)
|
|
320
|
+
const seenFiles = new Set<string>();
|
|
281
321
|
const parts: string[] = [];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
322
|
+
|
|
323
|
+
for (const knowledgeDir of candidateDirs) {
|
|
324
|
+
if (!fs.existsSync(knowledgeDir)) continue;
|
|
325
|
+
try {
|
|
326
|
+
const entries = fs.readdirSync(knowledgeDir).filter(f => f.endsWith('.md'));
|
|
327
|
+
for (const file of entries) {
|
|
328
|
+
if (seenFiles.has(file)) continue; // deduplicate: first source wins
|
|
329
|
+
seenFiles.add(file);
|
|
330
|
+
if (seenFiles.size > 10) break; // Cap at 10 docs total
|
|
331
|
+
|
|
332
|
+
const content = fs.readFileSync(path.join(knowledgeDir, file), 'utf-8');
|
|
333
|
+
const preview = content.slice(0, 2000);
|
|
334
|
+
parts.push(`## ${file}\n\n${preview}${content.length > 2000 ? '\n\n... (truncated)' : ''}`);
|
|
335
|
+
}
|
|
336
|
+
} catch { /* ignore unreadable dirs */ }
|
|
337
|
+
if (seenFiles.size >= 10) break;
|
|
338
|
+
}
|
|
290
339
|
|
|
291
340
|
return parts.length > 0 ? parts.join('\n\n---\n\n') : null;
|
|
292
341
|
}
|
|
@@ -606,7 +655,23 @@ python3 "$SUPERVISION_CMD" watch ses-aaa${subordinates.length > 1 ? ',ses-bbb' :
|
|
|
606
655
|
### ⛔ CRITICAL Rules
|
|
607
656
|
- **NEVER re-dispatch the same task.** If --check shows RUNNING, just keep polling.
|
|
608
657
|
- **NEVER dispatch and immediately finish.** The dispatch→check→review loop must continue until ALL work is complete.
|
|
609
|
-
- **Save the job ID** from each dispatch to use with --check
|
|
658
|
+
- **Save the job ID** from each dispatch to use with --check.
|
|
659
|
+
|
|
660
|
+
### ⛔ Amend-First Rule (COST CRITICAL)
|
|
661
|
+
When a subordinate needs follow-up work on the SAME topic, **amend instead of re-dispatch**.
|
|
662
|
+
Re-dispatch creates a new session that reloads ALL context from scratch (expensive).
|
|
663
|
+
Amend sends instructions to the existing session — near-zero additional cost.
|
|
664
|
+
|
|
665
|
+
\`\`\`bash
|
|
666
|
+
# ❌ WRONG: re-dispatch (wastes tokens reloading context)
|
|
667
|
+
python3 "$DISPATCH_CMD" ${exampleSubId} "fix the bug again"
|
|
668
|
+
|
|
669
|
+
# ✅ CORRECT: amend existing session (keeps context, near-zero cost)
|
|
670
|
+
python3 "$SUPERVISION_CMD" amend ses-xxx "Critic found issue: [details]. Fix it."
|
|
671
|
+
\`\`\`
|
|
672
|
+
|
|
673
|
+
**When to amend**: Follow-up on same files/code the role already touched.
|
|
674
|
+
**When to dispatch new**: Genuinely unrelated new task scope.`;
|
|
610
675
|
|
|
611
676
|
// C-level roles get mandatory delegation rules
|
|
612
677
|
if (isCLevel) {
|