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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch-classifier.ts — Role당 1세션 Invariant
|
|
3
|
+
*
|
|
4
|
+
* Dispatch 시 같은 wave 내 같은 role의 기존 세션을 찾아:
|
|
5
|
+
* - active → amend (기존 세션에 추가 지시)
|
|
6
|
+
* - done → amend (이어서 작업)
|
|
7
|
+
* - error N회 → new (fresh start)
|
|
8
|
+
* - 없음 → new (첫 생성)
|
|
9
|
+
*
|
|
10
|
+
* Haiku classifier 제거 — deterministic 판단.
|
|
11
|
+
* BUG-FORKBOMB: CEO 무한 dispatch 루프 구조적 차단.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { COMPANY_ROOT } from './file-reader.js';
|
|
16
|
+
import { listSessions } from './session-store.js';
|
|
17
|
+
import { executionManager } from './execution-manager.js';
|
|
18
|
+
|
|
19
|
+
/* ─── Types ──────────────────────────── */
|
|
20
|
+
|
|
21
|
+
export interface DispatchDecision {
|
|
22
|
+
ts: string;
|
|
23
|
+
waveId: string;
|
|
24
|
+
roleId: string;
|
|
25
|
+
sourceRole: string;
|
|
26
|
+
newTask: string;
|
|
27
|
+
prevSessionId: string;
|
|
28
|
+
decision: 'amend' | 'new';
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ─── Constants ──────────────────────── */
|
|
33
|
+
|
|
34
|
+
const ERROR_THRESHOLD = 3; // error 3회 초과 시 fresh session 허용
|
|
35
|
+
|
|
36
|
+
/* ─── Session Finder ─────────────────── */
|
|
37
|
+
|
|
38
|
+
export interface PrevSessionInfo {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
task: string;
|
|
41
|
+
status: string;
|
|
42
|
+
cliSessionId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 같은 wave 내에서 같은 role의 active 세션을 찾는다.
|
|
47
|
+
*/
|
|
48
|
+
export function findActiveSession(waveId: string, roleId: string): PrevSessionInfo | null {
|
|
49
|
+
const session = listSessions().find(
|
|
50
|
+
s => s.waveId === waveId && s.roleId === roleId && (s.status === 'active' || s.status === 'awaiting_input'),
|
|
51
|
+
);
|
|
52
|
+
if (!session) return null;
|
|
53
|
+
|
|
54
|
+
const exec = executionManager.getActiveExecution(session.id);
|
|
55
|
+
return {
|
|
56
|
+
sessionId: session.id,
|
|
57
|
+
task: exec?.task ?? '',
|
|
58
|
+
status: session.status,
|
|
59
|
+
cliSessionId: exec?.cliSessionId,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 같은 wave 내에서 같은 role의 가장 최근 done 세션을 찾는다.
|
|
65
|
+
*/
|
|
66
|
+
export function findPrevDoneSession(waveId: string, roleId: string): PrevSessionInfo | null {
|
|
67
|
+
const sessions = listSessions().filter(
|
|
68
|
+
s => s.waveId === waveId && s.roleId === roleId && (s.status === 'done' || s.status === 'closed'),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (sessions.length === 0) return null;
|
|
72
|
+
|
|
73
|
+
const latest = sessions[sessions.length - 1];
|
|
74
|
+
const exec = executionManager.getCompletedExecution(latest.id);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
sessionId: latest.id,
|
|
78
|
+
task: exec?.task ?? '',
|
|
79
|
+
status: latest.status,
|
|
80
|
+
cliSessionId: exec?.cliSessionId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 같은 wave 내에서 같은 role의 error 세션 수를 센다.
|
|
86
|
+
*/
|
|
87
|
+
function countErrorSessions(waveId: string, roleId: string): number {
|
|
88
|
+
return listSessions().filter(
|
|
89
|
+
s => s.waveId === waveId && s.roleId === roleId && s.status === 'error',
|
|
90
|
+
).length;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ─── Decision Logger ─────────────────── */
|
|
94
|
+
|
|
95
|
+
function logDecision(decision: DispatchDecision): void {
|
|
96
|
+
const logDir = path.join(COMPANY_ROOT, '.tycono');
|
|
97
|
+
const logPath = path.join(logDir, 'dispatch-decisions.jsonl');
|
|
98
|
+
try {
|
|
99
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
100
|
+
fs.appendFileSync(logPath, JSON.stringify(decision) + '\n');
|
|
101
|
+
} catch {
|
|
102
|
+
// Non-critical — don't crash on log failure
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ─── Main Decision Function ──────────── */
|
|
107
|
+
|
|
108
|
+
export interface AutoAmendResult {
|
|
109
|
+
action: 'amend' | 'new';
|
|
110
|
+
prevSessionId?: string;
|
|
111
|
+
reason: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* dispatch 요청 시 기존 세션 reuse 여부를 deterministic하게 판단.
|
|
116
|
+
*
|
|
117
|
+
* Role당 1세션 Invariant:
|
|
118
|
+
* 1. active 세션 있음 → amend (대기 후 추가 지시)
|
|
119
|
+
* 2. done 세션 있음 → amend (이어서)
|
|
120
|
+
* 3. error N회 초과 → new (fresh start)
|
|
121
|
+
* 4. 세션 없음 → new (첫 생성)
|
|
122
|
+
*/
|
|
123
|
+
export async function decideDispatchOrAmend(
|
|
124
|
+
waveId: string | undefined,
|
|
125
|
+
roleId: string,
|
|
126
|
+
sourceRole: string,
|
|
127
|
+
newTask: string,
|
|
128
|
+
): Promise<AutoAmendResult> {
|
|
129
|
+
// No wave context → always new dispatch
|
|
130
|
+
if (!waveId) {
|
|
131
|
+
return { action: 'new', reason: 'no-wave-context' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. Active session → amend (BUG-FORKBOMB fix: 기존에는 'new' 반환하여 fork bomb 유발)
|
|
135
|
+
const active = findActiveSession(waveId, roleId);
|
|
136
|
+
if (active) {
|
|
137
|
+
const decision: DispatchDecision = {
|
|
138
|
+
ts: new Date().toISOString(),
|
|
139
|
+
waveId, roleId, sourceRole, newTask: newTask.slice(0, 200),
|
|
140
|
+
prevSessionId: active.sessionId,
|
|
141
|
+
decision: 'amend', reason: 'role-already-active',
|
|
142
|
+
};
|
|
143
|
+
logDecision(decision);
|
|
144
|
+
console.log(`[Dispatch] ${roleId}: AMEND (active session ${active.sessionId})`);
|
|
145
|
+
return { action: 'amend', prevSessionId: active.sessionId, reason: 'role-already-active' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. Done session → amend (이어서 작업)
|
|
149
|
+
const prev = findPrevDoneSession(waveId, roleId);
|
|
150
|
+
if (prev) {
|
|
151
|
+
// 2a. Error threshold 체크: error가 많으면 fresh start
|
|
152
|
+
const errorCount = countErrorSessions(waveId, roleId);
|
|
153
|
+
if (errorCount >= ERROR_THRESHOLD) {
|
|
154
|
+
const decision: DispatchDecision = {
|
|
155
|
+
ts: new Date().toISOString(),
|
|
156
|
+
waveId, roleId, sourceRole, newTask: newTask.slice(0, 200),
|
|
157
|
+
prevSessionId: prev.sessionId,
|
|
158
|
+
decision: 'new', reason: `error-threshold-${errorCount}`,
|
|
159
|
+
};
|
|
160
|
+
logDecision(decision);
|
|
161
|
+
console.log(`[Dispatch] ${roleId}: NEW (${errorCount} errors >= ${ERROR_THRESHOLD} threshold)`);
|
|
162
|
+
return { action: 'new', reason: `error-threshold-${errorCount}` };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const decision: DispatchDecision = {
|
|
166
|
+
ts: new Date().toISOString(),
|
|
167
|
+
waveId, roleId, sourceRole, newTask: newTask.slice(0, 200),
|
|
168
|
+
prevSessionId: prev.sessionId,
|
|
169
|
+
decision: 'amend', reason: 'prev-session-done',
|
|
170
|
+
};
|
|
171
|
+
logDecision(decision);
|
|
172
|
+
console.log(`[Dispatch] ${roleId}: AMEND (done session ${prev.sessionId})`);
|
|
173
|
+
return { action: 'amend', prevSessionId: prev.sessionId, reason: 'prev-session-done' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 3. No session → new dispatch
|
|
177
|
+
console.log(`[Dispatch] ${roleId}: NEW (first dispatch in wave)`);
|
|
178
|
+
return { action: 'new', reason: 'no-prev-session' };
|
|
179
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
3
4
|
import { COMPANY_ROOT } from './file-reader.js';
|
|
4
5
|
import { ActivityStream, type ActivityEvent } from './activity-stream.js';
|
|
5
6
|
import { buildOrgTree } from '../engine/org-tree.js';
|
|
@@ -42,6 +43,8 @@ export interface Execution {
|
|
|
42
43
|
knowledgeDebt?: KnowledgeDebtItem[];
|
|
43
44
|
ports?: PortAllocation;
|
|
44
45
|
traceId?: string;
|
|
46
|
+
/** CLI session ID for --resume (captured from Claude CLI result event) */
|
|
47
|
+
cliSessionId?: string;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export interface StartExecutionParams {
|
|
@@ -56,6 +59,8 @@ export interface StartExecutionParams {
|
|
|
56
59
|
targetRoles?: string[];
|
|
57
60
|
sessionId: string;
|
|
58
61
|
attachments?: ImageAttachment[];
|
|
62
|
+
/** CLI session ID for --resume (context continuity across turn limits) */
|
|
63
|
+
cliSessionId?: string;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
/* ─── Helpers ────────────────────────────── */
|
|
@@ -77,6 +82,29 @@ function hasQuestion(output: string): boolean {
|
|
|
77
82
|
return /\?\s*$/.test(lastBlock) || /할까요|해볼까요|어떨까요|확인.*필요/.test(lastBlock);
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
/* ─── [APPROVAL_NEEDED] Detection ─── */
|
|
86
|
+
|
|
87
|
+
const APPROVAL_TAGS = /\[APPROVAL_NEEDED\]|\[CEO_DECISION\]|\[DECISION_REQUIRED\]/;
|
|
88
|
+
|
|
89
|
+
function extractApprovalQuestion(output: string): string | null {
|
|
90
|
+
const idx = output.search(APPROVAL_TAGS);
|
|
91
|
+
if (idx === -1) return null;
|
|
92
|
+
// Extract text after the tag until end or next section marker
|
|
93
|
+
const afterTag = output.slice(idx);
|
|
94
|
+
const lines = afterTag.split('\n');
|
|
95
|
+
// Take up to 10 lines after the tag line for context
|
|
96
|
+
const relevant = lines.slice(0, 10).join('\n').trim();
|
|
97
|
+
return relevant || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sendApprovalNotification(roleId: string, question: string): void {
|
|
101
|
+
try {
|
|
102
|
+
const title = `Tycono: ${roleId} needs approval`;
|
|
103
|
+
const msg = question.replace(/["\\\n]/g, ' ').slice(0, 200);
|
|
104
|
+
execSync(`osascript -e 'display notification "${msg}" with title "${title}" sound name "Ping"'`);
|
|
105
|
+
} catch { /* ignore on non-macOS */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
80
108
|
function isExecActive(status: ExecStatus): boolean {
|
|
81
109
|
return status === 'running' || status === 'awaiting_input';
|
|
82
110
|
}
|
|
@@ -102,6 +130,7 @@ class ExecutionManager {
|
|
|
102
130
|
private runner = createRunner();
|
|
103
131
|
private nextId = 1;
|
|
104
132
|
private executionCreatedListeners = new Set<(exec: Execution) => void>();
|
|
133
|
+
private pendingAmendments = new Map<string, string[]>(); // sessionId → queued tasks
|
|
105
134
|
|
|
106
135
|
setRunner(newRunner: ExecutionRunner): void {
|
|
107
136
|
this.runner = newRunner;
|
|
@@ -246,6 +275,24 @@ class ExecutionManager {
|
|
|
246
275
|
...(execution.ports.hmr && { VITE_HMR_PORT: String(execution.ports.hmr) }),
|
|
247
276
|
} : {};
|
|
248
277
|
|
|
278
|
+
// Handoff summary: collect prior dispatch results for this wave
|
|
279
|
+
const priorDispatches: Array<{ roleId: string; task: string; result: string }> = [];
|
|
280
|
+
const execSession = getSession(params.sessionId);
|
|
281
|
+
if (execSession?.waveId) {
|
|
282
|
+
for (const [, exec] of this.executions) {
|
|
283
|
+
if (exec.status === 'done' && exec.result && exec.sessionId !== params.sessionId) {
|
|
284
|
+
const s = getSession(exec.sessionId);
|
|
285
|
+
if (s?.waveId === execSession.waveId) {
|
|
286
|
+
priorDispatches.push({
|
|
287
|
+
roleId: exec.roleId,
|
|
288
|
+
task: exec.task,
|
|
289
|
+
result: exec.result.output?.slice(0, 500) ?? '',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
249
296
|
const handle = this.runner.execute(
|
|
250
297
|
{
|
|
251
298
|
companyRoot: COMPANY_ROOT,
|
|
@@ -262,6 +309,8 @@ class ExecutionManager {
|
|
|
262
309
|
presetId,
|
|
263
310
|
codeRoot: resolveCodeRoot(COMPANY_ROOT),
|
|
264
311
|
attachments: params.attachments,
|
|
312
|
+
cliSessionId: params.cliSessionId,
|
|
313
|
+
priorDispatches,
|
|
265
314
|
env: {
|
|
266
315
|
...process.env,
|
|
267
316
|
...portEnv,
|
|
@@ -299,12 +348,16 @@ class ExecutionManager {
|
|
|
299
348
|
});
|
|
300
349
|
}
|
|
301
350
|
},
|
|
302
|
-
onDispatch: (subRoleId, subTask) => {
|
|
351
|
+
onDispatch: async (subRoleId, subTask) => {
|
|
303
352
|
if (params.targetRoles && params.targetRoles.length > 0) {
|
|
304
353
|
if (!params.targetRoles.includes(subRoleId)) {
|
|
354
|
+
const errorMsg = `Dispatch to ${subRoleId} blocked — not in active target scope for this wave.`;
|
|
305
355
|
console.warn(`[ExecMgr] Dispatch blocked: ${params.roleId} → ${subRoleId} (not in targetRoles)`);
|
|
306
|
-
execution.stream.emit('
|
|
307
|
-
|
|
356
|
+
execution.stream.emit('dispatch:error', params.roleId, {
|
|
357
|
+
sourceRole: params.roleId,
|
|
358
|
+
targetRole: subRoleId,
|
|
359
|
+
error: errorMsg,
|
|
360
|
+
timestamp: Date.now(),
|
|
308
361
|
});
|
|
309
362
|
return;
|
|
310
363
|
}
|
|
@@ -314,6 +367,32 @@ class ExecutionManager {
|
|
|
314
367
|
const parentSession = getSession(execution.sessionId);
|
|
315
368
|
const parentWaveId = parentSession?.waveId;
|
|
316
369
|
|
|
370
|
+
// BUG-FORKBOMB: Role당 1세션 invariant — active/done 세션 있으면 amend
|
|
371
|
+
if (parentWaveId) {
|
|
372
|
+
const { decideDispatchOrAmend } = await import('./dispatch-classifier.js');
|
|
373
|
+
const decision = await decideDispatchOrAmend(parentWaveId, subRoleId, params.roleId, subTask);
|
|
374
|
+
|
|
375
|
+
if (decision.action === 'amend' && decision.prevSessionId) {
|
|
376
|
+
console.log(`[ExecMgr] AMEND instead of dispatch: ${subRoleId} → ${decision.prevSessionId} (${decision.reason})`);
|
|
377
|
+
|
|
378
|
+
if (decision.reason === 'role-already-active') {
|
|
379
|
+
// Active session — queue amendment
|
|
380
|
+
this.queueAmendment(decision.prevSessionId, `[FOLLOW-UP from ${params.roleId}] ${subTask}`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Done session — continue
|
|
385
|
+
const amended = this.continueSession(
|
|
386
|
+
decision.prevSessionId,
|
|
387
|
+
`[FOLLOW-UP from ${params.roleId}] ${subTask}`,
|
|
388
|
+
params.roleId,
|
|
389
|
+
);
|
|
390
|
+
if (amended) return;
|
|
391
|
+
// continueSession failed — fall through to new dispatch
|
|
392
|
+
console.warn(`[ExecMgr] continueSession failed for ${decision.prevSessionId}, creating new session`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
317
396
|
const childSession = createSession(subRoleId, {
|
|
318
397
|
mode: 'do',
|
|
319
398
|
source: 'dispatch',
|
|
@@ -424,6 +503,7 @@ class ExecutionManager {
|
|
|
424
503
|
handle.promise
|
|
425
504
|
.then((result: RunnerResult) => {
|
|
426
505
|
execution.result = result;
|
|
506
|
+
if (result.cliSessionId) execution.cliSessionId = result.cliSessionId;
|
|
427
507
|
|
|
428
508
|
const costUsd = estimateCost(
|
|
429
509
|
result.totalTokens.input,
|
|
@@ -449,6 +529,34 @@ class ExecutionManager {
|
|
|
449
529
|
|
|
450
530
|
const targetRole = resolveTargetRole(params.sourceRole, params.parentSessionId, this.executions);
|
|
451
531
|
|
|
532
|
+
// ── [APPROVAL_NEEDED] detection — notify user when agent is blocked ──
|
|
533
|
+
const approvalQuestion = extractApprovalQuestion(result.output);
|
|
534
|
+
if (approvalQuestion) {
|
|
535
|
+
console.log(`[Approval] ${params.roleId} (${execution.sessionId}) output contains approval tag`);
|
|
536
|
+
execution.stream.emit('approval:needed', params.roleId, {
|
|
537
|
+
roleId: params.roleId,
|
|
538
|
+
sessionId: execution.sessionId,
|
|
539
|
+
question: approvalQuestion,
|
|
540
|
+
timestamp: Date.now(),
|
|
541
|
+
});
|
|
542
|
+
sendApprovalNotification(params.roleId, approvalQuestion);
|
|
543
|
+
|
|
544
|
+
// BUG-APPROVAL belt-and-suspenders: directly notify supervisor (don't rely solely on stream)
|
|
545
|
+
// This ensures approval state is set even if stream watcher was lost (e.g., stream closed by cleanup)
|
|
546
|
+
if (params.roleId === 'ceo') {
|
|
547
|
+
const session = getSession(execution.sessionId);
|
|
548
|
+
if (session?.waveId) {
|
|
549
|
+
import('./supervisor-heartbeat.js').then(({ supervisorHeartbeat }) => {
|
|
550
|
+
const state = supervisorHeartbeat.getState(session.waveId!);
|
|
551
|
+
if (state && state.status !== 'awaiting_approval') {
|
|
552
|
+
console.log(`[Approval] Direct supervisor notification: wave ${session.waveId} → awaiting_approval`);
|
|
553
|
+
state.status = 'awaiting_approval';
|
|
554
|
+
}
|
|
555
|
+
}).catch(() => { /* avoid circular import crash */ });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
452
560
|
if (hardLimitReached) {
|
|
453
561
|
execution.status = 'awaiting_input';
|
|
454
562
|
execution.targetRole = targetRole;
|
|
@@ -460,15 +568,32 @@ class ExecutionManager {
|
|
|
460
568
|
targetRole,
|
|
461
569
|
reason: 'turn_limit',
|
|
462
570
|
});
|
|
571
|
+
|
|
572
|
+
// Auto-continue on turn limit: resume with --resume for context continuity
|
|
573
|
+
// Delay slightly to allow stream event to propagate
|
|
574
|
+
setTimeout(() => {
|
|
575
|
+
console.log(`[Harness] Auto-continuing ${params.roleId} (${execution.sessionId}) after turn limit`);
|
|
576
|
+
this.continueSession(execution.sessionId, '턴 한도에 도달했습니다. 이전 작업을 이어서 계속 진행하세요.');
|
|
577
|
+
}, 3_000);
|
|
463
578
|
} else if (!params.isContinuation && hasQuestion(result.output)) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
execution.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
579
|
+
// CEO supervisor should auto-continue instead of hanging on awaiting_input
|
|
580
|
+
// (subordinates may have completed while CEO was running — CEO needs to synthesize results)
|
|
581
|
+
const session = getSession(execution.sessionId);
|
|
582
|
+
if (session?.roleId === 'ceo' && session?.source === 'wave') {
|
|
583
|
+
console.log(`[Harness] CEO supervisor hasQuestion — auto-continuing to synthesize results`);
|
|
584
|
+
setTimeout(() => {
|
|
585
|
+
this.continueSession(execution.sessionId, 'All dispatched sessions have completed. Synthesize the results from your team and provide a final briefing.');
|
|
586
|
+
}, 3_000);
|
|
587
|
+
} else {
|
|
588
|
+
execution.status = 'awaiting_input';
|
|
589
|
+
execution.targetRole = targetRole;
|
|
590
|
+
execution.stream.emit('msg:awaiting_input', params.roleId, {
|
|
591
|
+
...doneData,
|
|
592
|
+
question: result.output.trim().split('\n').slice(-5).join('\n'),
|
|
593
|
+
awaitingInput: true,
|
|
594
|
+
targetRole,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
472
597
|
} else {
|
|
473
598
|
const changedMdFiles = result.toolCalls
|
|
474
599
|
.filter(tc => (tc.name === 'write_file' || tc.name === 'edit_file') && tc.input && typeof tc.input.path === 'string')
|
|
@@ -503,6 +628,20 @@ class ExecutionManager {
|
|
|
503
628
|
this.finalizeSessionMessage(execution, 'done', result);
|
|
504
629
|
}
|
|
505
630
|
|
|
631
|
+
// Emit dispatch:done on parent's stream (monni VOC: parent needs completion signal)
|
|
632
|
+
if (params.parentSessionId) {
|
|
633
|
+
const parentExec = this.getActiveExecution(params.parentSessionId);
|
|
634
|
+
if (parentExec) {
|
|
635
|
+
parentExec.stream.emit('dispatch:done', parentExec.roleId, {
|
|
636
|
+
targetRoleId: params.roleId,
|
|
637
|
+
childSessionId: params.sessionId,
|
|
638
|
+
output: result.output.slice(-1000),
|
|
639
|
+
turns: result.turns,
|
|
640
|
+
tokens: result.totalTokens,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
506
645
|
if (!params.parentSessionId && result) {
|
|
507
646
|
const totalTokens = (result.totalTokens?.input ?? 0) + (result.totalTokens?.output ?? 0);
|
|
508
647
|
const bonus = Math.min(2000, Math.max(500, Math.round(totalTokens / 500)));
|
|
@@ -568,11 +707,18 @@ class ExecutionManager {
|
|
|
568
707
|
|
|
569
708
|
// OOM prevention: remove completed execution from memory after delay
|
|
570
709
|
// (delay allows getActiveExecution to find it briefly for multiplexer/recovery)
|
|
710
|
+
// BUG-APPROVAL fix: Don't close stream if a continuation is running on the same session
|
|
711
|
+
// (closing the stream kills watcher subscribers, breaking supervisor event delivery)
|
|
571
712
|
setTimeout(() => {
|
|
572
713
|
this.executions.delete(execution.id);
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
714
|
+
// Only close stream if no other active execution shares this session
|
|
715
|
+
const hasActiveSibling = [...this.executions.values()].some(
|
|
716
|
+
e => e.sessionId === execution.sessionId && e.id !== execution.id && (e.status === 'running' || e.status === 'awaiting_input'),
|
|
717
|
+
);
|
|
718
|
+
if (!hasActiveSibling) {
|
|
719
|
+
execution.stream.close();
|
|
720
|
+
}
|
|
721
|
+
}, 300_000).unref(); // 5 min — prevents HTTP 410 on dispatch --check
|
|
576
722
|
});
|
|
577
723
|
}
|
|
578
724
|
|
|
@@ -656,6 +802,11 @@ class ExecutionManager {
|
|
|
656
802
|
if (session.roleId !== 'ceo' || session.source !== 'wave') {
|
|
657
803
|
updateSession(execution.sessionId, { status: 'done' });
|
|
658
804
|
}
|
|
805
|
+
|
|
806
|
+
// Process queued amendments (BUG-FORKBOMB: role당 1세션 invariant)
|
|
807
|
+
if (status === 'done') {
|
|
808
|
+
this.processPendingAmendments(execution.sessionId);
|
|
809
|
+
}
|
|
659
810
|
}
|
|
660
811
|
|
|
661
812
|
private cleanupOrphanedChildren(parentSessionId: string): void {
|
|
@@ -779,6 +930,19 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
|
|
|
779
930
|
return this.recoverExecutionFromStream(sessionId);
|
|
780
931
|
}
|
|
781
932
|
|
|
933
|
+
/** Find the latest completed execution for a session (for auto-amend lookup) */
|
|
934
|
+
getCompletedExecution(sessionId: string): { task: string; cliSessionId?: string } | undefined {
|
|
935
|
+
let latest: Execution | undefined;
|
|
936
|
+
for (const exec of this.executions.values()) {
|
|
937
|
+
if (exec.sessionId === sessionId && exec.status === 'done') {
|
|
938
|
+
if (!latest || exec.createdAt > latest.createdAt) {
|
|
939
|
+
latest = exec;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return latest ? { task: latest.task, cliSessionId: latest.cliSessionId } : undefined;
|
|
944
|
+
}
|
|
945
|
+
|
|
782
946
|
listExecutions(filter?: { status?: ExecStatus; roleId?: string; active?: boolean }): Array<{
|
|
783
947
|
id: string;
|
|
784
948
|
type: ExecType;
|
|
@@ -856,14 +1020,23 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
|
|
|
856
1020
|
exec.stream.emit('msg:reply', exec.roleId, { response, responderRole: effectiveResponder, isFollowUp });
|
|
857
1021
|
|
|
858
1022
|
const prevOutput = exec.result?.output ?? '';
|
|
859
|
-
const
|
|
860
|
-
? prevOutput.slice(-2000)
|
|
861
|
-
: prevOutput;
|
|
1023
|
+
const hasCliSession = !!exec.cliSessionId;
|
|
862
1024
|
|
|
863
1025
|
const responderLabel = effectiveResponder === 'ceo' ? 'CEO' : effectiveResponder.toUpperCase();
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1026
|
+
let continuationTask: string;
|
|
1027
|
+
if (hasCliSession) {
|
|
1028
|
+
// --resume preserves full conversation context — no need to repeat output
|
|
1029
|
+
continuationTask = isFollowUp
|
|
1030
|
+
? `[CEO Follow-up Directive]\n${response}`
|
|
1031
|
+
: `[${responderLabel} Response — continue where you left off]\n${response}`;
|
|
1032
|
+
} else {
|
|
1033
|
+
const contextSummary = prevOutput.length > 2000
|
|
1034
|
+
? prevOutput.slice(-2000)
|
|
1035
|
+
: prevOutput;
|
|
1036
|
+
continuationTask = isFollowUp
|
|
1037
|
+
? `[CEO Follow-up Directive]\n${response}\n\n[Previous context — your earlier report follows]\n${contextSummary}`
|
|
1038
|
+
: `[Continuation — previous output follows]\n${contextSummary}\n\n[${responderLabel} Response]\n${response}`;
|
|
1039
|
+
}
|
|
867
1040
|
|
|
868
1041
|
const newExec = this.startExecution({
|
|
869
1042
|
type: exec.type,
|
|
@@ -873,11 +1046,44 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
|
|
|
873
1046
|
parentSessionId: exec.parentSessionId,
|
|
874
1047
|
isContinuation: !isFollowUp,
|
|
875
1048
|
sessionId: exec.sessionId, // Same session → same stream
|
|
1049
|
+
// Pass CLI session ID for --resume (preserves Claude conversation context)
|
|
1050
|
+
cliSessionId: exec.cliSessionId,
|
|
876
1051
|
});
|
|
877
1052
|
|
|
878
1053
|
return newExec;
|
|
879
1054
|
}
|
|
880
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* Queue an amendment for a running session.
|
|
1058
|
+
* Will be processed when the current execution completes.
|
|
1059
|
+
*/
|
|
1060
|
+
queueAmendment(sessionId: string, task: string): void {
|
|
1061
|
+
const queue = this.pendingAmendments.get(sessionId) ?? [];
|
|
1062
|
+
queue.push(task);
|
|
1063
|
+
this.pendingAmendments.set(sessionId, queue);
|
|
1064
|
+
console.log(`[Dispatch] Queued amendment for ${sessionId} (${queue.length} pending)`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Process pending amendments after execution completes.
|
|
1069
|
+
* Called from finalization logic.
|
|
1070
|
+
*/
|
|
1071
|
+
processPendingAmendments(sessionId: string): void {
|
|
1072
|
+
const queue = this.pendingAmendments.get(sessionId);
|
|
1073
|
+
if (!queue || queue.length === 0) return;
|
|
1074
|
+
|
|
1075
|
+
const task = queue.shift()!;
|
|
1076
|
+
if (queue.length === 0) {
|
|
1077
|
+
this.pendingAmendments.delete(sessionId);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
console.log(`[Dispatch] Processing queued amendment for ${sessionId} (${queue.length} remaining)`);
|
|
1081
|
+
// Use setTimeout to avoid recursive call stack during finalization
|
|
1082
|
+
setTimeout(() => {
|
|
1083
|
+
this.continueSession(sessionId, task);
|
|
1084
|
+
}, 100);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
881
1087
|
getActiveExecutionForRole(roleId: string): Execution | undefined {
|
|
882
1088
|
for (const exec of this.executions.values()) {
|
|
883
1089
|
if (exec.roleId === roleId && isExecActive(exec.status)) {
|
|
@@ -957,7 +1163,7 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
|
|
|
957
1163
|
setTimeout(() => {
|
|
958
1164
|
this.executions.delete(execution.id);
|
|
959
1165
|
execution.stream.close();
|
|
960
|
-
},
|
|
1166
|
+
}, 300_000).unref(); // 5 min — prevents HTTP 410 on dispatch --check
|
|
961
1167
|
}
|
|
962
1168
|
|
|
963
1169
|
return execution;
|
|
@@ -5,9 +5,12 @@ import { glob } from 'glob';
|
|
|
5
5
|
|
|
6
6
|
function findCompanyRoot(): string {
|
|
7
7
|
if (process.env.COMPANY_ROOT) return process.env.COMPANY_ROOT;
|
|
8
|
-
// Walk up from cwd to find
|
|
8
|
+
// Walk up from cwd to find CLAUDE.md (supports both layouts)
|
|
9
|
+
// - Claude Code standard: CLAUDE.md at project root
|
|
10
|
+
// - Tycono scaffold: knowledge/CLAUDE.md
|
|
9
11
|
let dir = process.cwd();
|
|
10
12
|
while (dir !== path.dirname(dir)) {
|
|
13
|
+
if (fs.existsSync(path.join(dir, 'CLAUDE.md'))) return dir;
|
|
11
14
|
if (fs.existsSync(path.join(dir, 'knowledge', 'CLAUDE.md'))) return dir;
|
|
12
15
|
dir = path.dirname(dir);
|
|
13
16
|
}
|