@yeaft/webchat-agent 0.1.248 → 0.1.250
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/connection/message-router.js +0 -26
- package/crew/role-query.js +8 -9
- package/index.js +0 -2
- package/package.json +1 -1
- package/conductor/actor.js +0 -478
- package/conductor/claudemd-generator.js +0 -194
- package/conductor/conductor-claude.js +0 -480
- package/conductor/index.js +0 -65
- package/conductor/orchestrator-flows.js +0 -498
- package/conductor/orchestrator.js +0 -888
- package/conductor/persistence.js +0 -297
- package/conductor/personas/dev.js +0 -97
- package/conductor/personas/index.js +0 -77
- package/conductor/personas/trading.js +0 -92
- package/conductor/personas/video.js +0 -75
- package/conductor/personas/writing.js +0 -82
- package/conductor/semaphore.js +0 -73
- package/conductor/session.js +0 -210
- package/conductor/specialties/common.js +0 -350
- package/conductor/specialties/index.js +0 -135
- package/conductor/specialties/trading.js +0 -165
- package/conductor/specialties/video.js +0 -175
- package/conductor/specialties/writing.js +0 -120
- package/conductor/task.js +0 -333
- package/conductor/ui-messages.js +0 -170
- package/conductor/worktree.js +0 -182
- package/conductor.js +0 -35
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conductor — 三段式 CLAUDE.md 生成器
|
|
3
|
-
*
|
|
4
|
-
* CLAUDE.md = 人物基底(persona base) + specialty 行为(specialty behavior) + 任务上下文(task context)
|
|
5
|
-
*
|
|
6
|
-
* 核心原则:一个实例一个文件。每个 persona × specialty 组合生成独立的 CLAUDE.md。
|
|
7
|
-
* Torvalds 做 planning 和 Torvalds 做 coding 是两个完全不同的文件。
|
|
8
|
-
*/
|
|
9
|
-
import { getPersonaById } from './personas/index.js';
|
|
10
|
-
import {
|
|
11
|
-
getSpecialty,
|
|
12
|
-
getThinkingMode,
|
|
13
|
-
getPersonalizedBehavior,
|
|
14
|
-
getOutputFormat,
|
|
15
|
-
formatToolRules
|
|
16
|
-
} from './specialties/index.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 生成 Actor 的 CLAUDE.md 内容
|
|
20
|
-
*
|
|
21
|
-
* @param {object|string} personaOrId - persona 对象 或 persona ID
|
|
22
|
-
* @param {string} specialtyId - specialty ID (e.g. 'planning', 'coding', 'review')
|
|
23
|
-
* @param {object} taskContext - 任务上下文
|
|
24
|
-
* @param {string} taskContext.taskId - 任务 ID
|
|
25
|
-
* @param {string} taskContext.description - 任务描述
|
|
26
|
-
* @param {string} [taskContext.memoryPath] - memory.md 路径
|
|
27
|
-
* @param {string} [taskContext.planPath] - plan.json 路径
|
|
28
|
-
* @param {string} [taskContext.worktreePath] - worktree 路径 (coding actor 用)
|
|
29
|
-
* @param {string} [taskContext.reviewTarget] - 审查目标描述 (review actor 用)
|
|
30
|
-
* @param {string} [taskContext.assignedStep] - 分配的执行步骤 (coding actor 用)
|
|
31
|
-
* @param {string} [taskContext.scenario] - 场景名称
|
|
32
|
-
* @param {object} [taskContext.extra] - 额外上下文(自由格式)
|
|
33
|
-
* @returns {string} 完整的 CLAUDE.md 内容
|
|
34
|
-
*/
|
|
35
|
-
export function generateActorCLAUDEmd(personaOrId, specialtyId, taskContext = {}) {
|
|
36
|
-
// 解析 persona
|
|
37
|
-
const persona = typeof personaOrId === 'string'
|
|
38
|
-
? getPersonaById(personaOrId)
|
|
39
|
-
: personaOrId;
|
|
40
|
-
|
|
41
|
-
if (!persona) {
|
|
42
|
-
throw new Error(`Persona not found: ${personaOrId}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const specialty = getSpecialty(specialtyId);
|
|
46
|
-
if (!specialty) {
|
|
47
|
-
throw new Error(`Specialty not found: ${specialtyId}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 三段生成
|
|
51
|
-
const section1 = generatePersonaBase(persona, specialty);
|
|
52
|
-
const section2 = generateSpecialtyBehavior(persona, specialty);
|
|
53
|
-
const section3 = generateTaskContext(persona, specialty, taskContext);
|
|
54
|
-
|
|
55
|
-
return `${section1}
|
|
56
|
-
|
|
57
|
-
${section2}
|
|
58
|
-
|
|
59
|
-
${section3}`.trim();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// =====================================================================
|
|
63
|
-
// 第一段:人物基底
|
|
64
|
-
// =====================================================================
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 生成人物基底部分
|
|
68
|
-
*/
|
|
69
|
-
function generatePersonaBase(persona, specialty) {
|
|
70
|
-
return `# 你是 ${persona.name} — 此刻你在做 ${specialty.displayName}
|
|
71
|
-
|
|
72
|
-
${persona.base}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// =====================================================================
|
|
76
|
-
// 第二段:Specialty 行为(结合人物个性定制)
|
|
77
|
-
// =====================================================================
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 生成 specialty 行为部分
|
|
81
|
-
*/
|
|
82
|
-
function generateSpecialtyBehavior(persona, specialty) {
|
|
83
|
-
const thinkingMode = getThinkingMode(specialty.id);
|
|
84
|
-
const thinkingLabel = thinkingMode === 'divergent' ? '发散思维' : '收敛执行';
|
|
85
|
-
|
|
86
|
-
const personalizedBehavior = getPersonalizedBehavior(specialty.id, persona);
|
|
87
|
-
const outputFormat = getOutputFormat(specialty.id);
|
|
88
|
-
const toolRules = formatToolRules(specialty.id);
|
|
89
|
-
|
|
90
|
-
return `## 作为 ${specialty.displayName} 的你(${thinkingLabel})
|
|
91
|
-
${personalizedBehavior}
|
|
92
|
-
|
|
93
|
-
## 输出要求
|
|
94
|
-
${outputFormat}
|
|
95
|
-
|
|
96
|
-
## 工具使用
|
|
97
|
-
${toolRules}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// =====================================================================
|
|
101
|
-
// 第三段:任务上下文(动态注入)
|
|
102
|
-
// =====================================================================
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 生成任务上下文部分
|
|
106
|
-
*/
|
|
107
|
-
function generateTaskContext(persona, specialty, ctx) {
|
|
108
|
-
const lines = ['## Task 上下文'];
|
|
109
|
-
|
|
110
|
-
if (ctx.taskId) {
|
|
111
|
-
lines.push(`- 任务 ID:${ctx.taskId}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (ctx.description) {
|
|
115
|
-
lines.push(`- 任务描述:${ctx.description}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (ctx.scenario) {
|
|
119
|
-
lines.push(`- 场景:${ctx.scenario}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (ctx.assignedStep) {
|
|
123
|
-
lines.push(`- 执行步骤:${ctx.assignedStep}`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (ctx.reviewTarget) {
|
|
127
|
-
lines.push(`- 审查目标:${ctx.reviewTarget}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (ctx.memoryPath) {
|
|
131
|
-
lines.push(`- 共享记忆:${ctx.memoryPath}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (ctx.planPath) {
|
|
135
|
-
lines.push(`- 计划文件:${ctx.planPath}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// worktree 路径和纪律规则(coding actor 专用)
|
|
139
|
-
if (ctx.worktreePath) {
|
|
140
|
-
lines.push('');
|
|
141
|
-
lines.push('## Worktree 纪律');
|
|
142
|
-
lines.push(`- 代码工作目录:${ctx.worktreePath}`);
|
|
143
|
-
lines.push('- 所有代码操作必须在此目录内');
|
|
144
|
-
lines.push('- 禁止在主目录或 main 分支修改代码');
|
|
145
|
-
lines.push('- 完成后通过回调通知 Orchestrator');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 额外上下文
|
|
149
|
-
if (ctx.extra) {
|
|
150
|
-
lines.push('');
|
|
151
|
-
lines.push('## 补充信息');
|
|
152
|
-
if (typeof ctx.extra === 'string') {
|
|
153
|
-
lines.push(ctx.extra);
|
|
154
|
-
} else {
|
|
155
|
-
for (const [key, value] of Object.entries(ctx.extra)) {
|
|
156
|
-
lines.push(`- ${key}:${value}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 通用结尾指令
|
|
162
|
-
lines.push('');
|
|
163
|
-
lines.push('## 工作完成后');
|
|
164
|
-
lines.push('完成工作后,请输出你的产出结果。Orchestrator 会接收并处理。');
|
|
165
|
-
|
|
166
|
-
return lines.join('\n');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 生成 Actor 实例 ID
|
|
171
|
-
*
|
|
172
|
-
* @param {string} specialtyId - specialty ID
|
|
173
|
-
* @param {string} personaId - persona ID
|
|
174
|
-
* @param {string} taskId - task ID
|
|
175
|
-
* @param {string} [threadId] - 并行线程 ID(可选)
|
|
176
|
-
* @returns {string} 实例 ID,如 "planning-jobs-task-1" 或 "coding-torvalds-task-1-t1"
|
|
177
|
-
*/
|
|
178
|
-
export function generateActorInstanceId(specialtyId, personaId, taskId, threadId) {
|
|
179
|
-
const base = `${specialtyId}-${personaId}-${taskId}`;
|
|
180
|
-
return threadId ? `${base}-${threadId}` : base;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* 生成 Actor 的目录名(用于 .conductor/tasks/task-N/actors/ 下)
|
|
185
|
-
*
|
|
186
|
-
* @param {string} specialtyId
|
|
187
|
-
* @param {string} personaId
|
|
188
|
-
* @param {string} [threadId]
|
|
189
|
-
* @returns {string} 目录名,如 "planning-jobs" 或 "coding-torvalds-t1"
|
|
190
|
-
*/
|
|
191
|
-
export function generateActorDirName(specialtyId, personaId, threadId) {
|
|
192
|
-
const base = `${specialtyId}-${personaId}`;
|
|
193
|
-
return threadId ? `${base}-${threadId}` : base;
|
|
194
|
-
}
|
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conductor — Conductor Claude Instance (V5)
|
|
3
|
-
*
|
|
4
|
-
* Conductor Claude is a persistent Claude Code process that:
|
|
5
|
-
* 1. Classifies messages (Q&A / create task / forward to task / status)
|
|
6
|
-
* 2. Answers simple questions
|
|
7
|
-
* 3. Never does task analysis or step decomposition
|
|
8
|
-
*
|
|
9
|
-
* cwd = Conductor Home (~/.config/yeaft-agent/.conductor/)
|
|
10
|
-
* State.json summary injected into systemPrompt for task awareness.
|
|
11
|
-
*/
|
|
12
|
-
import { query, Stream } from '../sdk/index.js';
|
|
13
|
-
import { promises as fs } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
import { globalSemaphore } from './semaphore.js';
|
|
16
|
-
import {
|
|
17
|
-
sendConductorMessage, sendConductorOutput,
|
|
18
|
-
endConductorStreaming, sendStatusUpdate
|
|
19
|
-
} from './ui-messages.js';
|
|
20
|
-
import {
|
|
21
|
-
getConductorHome, ensureConductorHome,
|
|
22
|
-
loadState, saveState, updateTaskInState, initTaskDir
|
|
23
|
-
} from './persistence.js';
|
|
24
|
-
|
|
25
|
-
// =====================================================================
|
|
26
|
-
// Claude Session ID Persistence
|
|
27
|
-
// =====================================================================
|
|
28
|
-
|
|
29
|
-
async function saveConductorSessionId(claudeSessionId) {
|
|
30
|
-
const dir = await ensureConductorHome();
|
|
31
|
-
const filePath = join(dir, 'claude-session.json');
|
|
32
|
-
await fs.writeFile(filePath, JSON.stringify({
|
|
33
|
-
claudeSessionId, savedAt: Date.now()
|
|
34
|
-
}));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function loadConductorSessionId() {
|
|
38
|
-
const filePath = join(getConductorHome(), 'claude-session.json');
|
|
39
|
-
try {
|
|
40
|
-
const data = JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
41
|
-
return data.claudeSessionId || null;
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function clearConductorSessionId() {
|
|
48
|
-
const filePath = join(getConductorHome(), 'claude-session.json');
|
|
49
|
-
try { await fs.unlink(filePath); } catch { /* ok */ }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// =====================================================================
|
|
53
|
-
// System Prompt
|
|
54
|
-
// =====================================================================
|
|
55
|
-
|
|
56
|
-
function buildConductorSystemPrompt(conductor) {
|
|
57
|
-
// Build task summaries from in-memory tasks map
|
|
58
|
-
const taskLines = [];
|
|
59
|
-
for (const [taskId, t] of conductor.tasks) {
|
|
60
|
-
const actors = (t.activeActors || []).join(', ');
|
|
61
|
-
taskLines.push(`- ${taskId}: ${t.title} [${t.status}${t.currentStep ? ' ' + t.currentStep : ''}] workDir=${t.workDir} scenario=${t.scenario}${actors ? ' actors=' + actors : ''}`);
|
|
62
|
-
}
|
|
63
|
-
const taskSummaries = taskLines.join('\n');
|
|
64
|
-
|
|
65
|
-
return `# Conductor — 交响乐指挥台
|
|
66
|
-
|
|
67
|
-
你是 Conductor,项目的总指挥。你的职责是:
|
|
68
|
-
1. **分类消息**:判断用户的输入是简单问答还是需要创建任务
|
|
69
|
-
2. **创建任务**:通过 CREATE_TASK 指令创建新任务(必须指定 workDir 和 scenario)
|
|
70
|
-
3. **转发消息**:通过 FORWARD_TASK 将用户消息传递给已有任务
|
|
71
|
-
4. **回答问题**:直接回答简单的问题
|
|
72
|
-
5. **汇报状态**:用户询问进度时汇总所有任务状态
|
|
73
|
-
|
|
74
|
-
## 你绝对不做的事
|
|
75
|
-
- 不分析任务难度
|
|
76
|
-
- 不拆解任务步骤
|
|
77
|
-
- 不评估需要几个人
|
|
78
|
-
- 不写代码
|
|
79
|
-
|
|
80
|
-
## 当前活跃任务
|
|
81
|
-
${taskSummaries || '(无)'}
|
|
82
|
-
|
|
83
|
-
## 指令格式
|
|
84
|
-
|
|
85
|
-
### 创建任务
|
|
86
|
-
当用户的需求需要实际的开发/分析/设计工作时,输出:
|
|
87
|
-
|
|
88
|
-
\`\`\`
|
|
89
|
-
---CREATE_TASK---
|
|
90
|
-
title: <任务标题>
|
|
91
|
-
workDir: <项目工作路径>
|
|
92
|
-
scenario: <dev|writing|trading|video>
|
|
93
|
-
---END_CREATE_TASK---
|
|
94
|
-
\`\`\`
|
|
95
|
-
|
|
96
|
-
### 转发消息到已有任务
|
|
97
|
-
当用户的消息是针对某个已有任务的补充说明时,输出:
|
|
98
|
-
|
|
99
|
-
\`\`\`
|
|
100
|
-
---FORWARD_TASK---
|
|
101
|
-
taskId: <task-id>
|
|
102
|
-
message: <要转发的内容>
|
|
103
|
-
---END_FORWARD_TASK---
|
|
104
|
-
\`\`\`
|
|
105
|
-
|
|
106
|
-
### 回答简单问题
|
|
107
|
-
直接用文本回答,不需要任何指令块。
|
|
108
|
-
|
|
109
|
-
## 规则
|
|
110
|
-
- 每条用户消息只需要一种响应:创建任务 / 转发 / 直接回答
|
|
111
|
-
- 创建任务时只给标题,不要描述实现步骤
|
|
112
|
-
- 创建任务时必须指定 workDir(用户提到的项目路径)和 scenario(场景类型)
|
|
113
|
-
- 当消息包含 [工作路径: xxx] 时,直接使用该路径作为 workDir 创建 CREATE_TASK,不要再询问 workDir
|
|
114
|
-
- 当消息包含 [工作路径: xxx] 时,这是用户明确的意图——立刻创建任务,不要做任何分析
|
|
115
|
-
- 如果用户没指定 workDir 或 scenario,先询问
|
|
116
|
-
- 如果用户问"进度怎样",直接汇总上面的任务列表状态
|
|
117
|
-
- 保持回复简洁
|
|
118
|
-
- 用中文回复`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// =====================================================================
|
|
122
|
-
// Conductor Claude Instance
|
|
123
|
-
// =====================================================================
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Create or rebuild Conductor Claude instance
|
|
127
|
-
*/
|
|
128
|
-
export async function createConductorClaude(conductor) {
|
|
129
|
-
const conductorHome = await ensureConductorHome();
|
|
130
|
-
|
|
131
|
-
// Acquire semaphore
|
|
132
|
-
const release = await globalSemaphore.acquire();
|
|
133
|
-
conductor._conductorSemRelease = release;
|
|
134
|
-
conductor.activeClaudes = (conductor.activeClaudes || 0) + 1;
|
|
135
|
-
|
|
136
|
-
const inputStream = new Stream();
|
|
137
|
-
const abortController = new AbortController();
|
|
138
|
-
const systemPrompt = buildConductorSystemPrompt(conductor);
|
|
139
|
-
|
|
140
|
-
// Try to resume existing Claude session
|
|
141
|
-
const savedSessionId = await loadConductorSessionId();
|
|
142
|
-
|
|
143
|
-
const queryOptions = {
|
|
144
|
-
cwd: conductorHome,
|
|
145
|
-
permissionMode: 'bypassPermissions',
|
|
146
|
-
abort: abortController.signal,
|
|
147
|
-
appendSystemPrompt: systemPrompt
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (savedSessionId) {
|
|
151
|
-
queryOptions.resume = savedSessionId;
|
|
152
|
-
console.log(`[Conductor] Resuming Claude with sessionId: ${savedSessionId}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const conductorQuery = query({
|
|
156
|
-
prompt: inputStream,
|
|
157
|
-
options: queryOptions
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
conductor.conductorState = {
|
|
161
|
-
query: conductorQuery,
|
|
162
|
-
inputStream,
|
|
163
|
-
abortController,
|
|
164
|
-
claudeSessionId: savedSessionId,
|
|
165
|
-
accumulatedText: '',
|
|
166
|
-
turnActive: false,
|
|
167
|
-
lastCostUsd: 0,
|
|
168
|
-
lastInputTokens: 0,
|
|
169
|
-
lastOutputTokens: 0
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// Start output processing loop
|
|
173
|
-
processConductorOutput(conductor, conductorQuery);
|
|
174
|
-
|
|
175
|
-
return conductor.conductorState;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Send message to Conductor Claude
|
|
180
|
-
*/
|
|
181
|
-
export async function sendToConductor(conductor, content, workDir) {
|
|
182
|
-
let state = conductor.conductorState;
|
|
183
|
-
|
|
184
|
-
if (!state || !state.query || !state.inputStream) {
|
|
185
|
-
state = await createConductorClaude(conductor);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Inject workDir prefix so Conductor knows the project path
|
|
189
|
-
let fullContent = content;
|
|
190
|
-
if (workDir) {
|
|
191
|
-
fullContent = `[工作路径: ${workDir}]\n${content}`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Inject task context before user message
|
|
195
|
-
const taskContext = buildTaskContext(conductor);
|
|
196
|
-
if (taskContext) {
|
|
197
|
-
fullContent = `${fullContent}\n\n---\n<conductor-context>\n${taskContext}\n</conductor-context>`;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
state.turnActive = true;
|
|
201
|
-
state.accumulatedText = '';
|
|
202
|
-
state.inputStream.enqueue({
|
|
203
|
-
type: 'user',
|
|
204
|
-
message: { role: 'user', content: fullContent }
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
sendStatusUpdate(conductor);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function buildTaskContext(conductor) {
|
|
211
|
-
if (conductor.tasks.size === 0) return null;
|
|
212
|
-
const lines = ['活跃任务:'];
|
|
213
|
-
for (const [taskId, t] of conductor.tasks) {
|
|
214
|
-
const actors = (t.activeActors || []).join(', ');
|
|
215
|
-
lines.push(`- ${taskId}: ${t.title} [${t.status}] workDir=${t.workDir} scenario=${t.scenario}${actors ? ' actors=' + actors : ''}`);
|
|
216
|
-
}
|
|
217
|
-
return lines.join('\n');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// =====================================================================
|
|
221
|
-
// Output Processing
|
|
222
|
-
// =====================================================================
|
|
223
|
-
|
|
224
|
-
// Parse CREATE_TASK instruction (V5: includes workDir and scenario)
|
|
225
|
-
function parseCreateTask(text) {
|
|
226
|
-
const regex = /---CREATE_TASK---\s*\n([\s\S]*?)---END_CREATE_TASK---/g;
|
|
227
|
-
const tasks = [];
|
|
228
|
-
let match;
|
|
229
|
-
while ((match = regex.exec(text)) !== null) {
|
|
230
|
-
const block = match[1];
|
|
231
|
-
const titleMatch = block.match(/^title:\s*(.+)/im);
|
|
232
|
-
const workDirMatch = block.match(/^workDir:\s*(.+)/im);
|
|
233
|
-
const scenarioMatch = block.match(/^scenario:\s*(.+)/im);
|
|
234
|
-
if (titleMatch) {
|
|
235
|
-
tasks.push({
|
|
236
|
-
title: titleMatch[1].trim(),
|
|
237
|
-
workDir: workDirMatch ? workDirMatch[1].trim() : '',
|
|
238
|
-
scenario: scenarioMatch ? scenarioMatch[1].trim() : 'dev'
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return tasks;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Parse FORWARD_TASK instruction
|
|
246
|
-
function parseForwardTask(text) {
|
|
247
|
-
const regex = /---FORWARD_TASK---\s*\n([\s\S]*?)---END_FORWARD_TASK---/g;
|
|
248
|
-
const forwards = [];
|
|
249
|
-
let match;
|
|
250
|
-
while ((match = regex.exec(text)) !== null) {
|
|
251
|
-
const block = match[1];
|
|
252
|
-
const taskIdMatch = block.match(/^taskId:\s*(.+)/im);
|
|
253
|
-
// message may be multi-line: capture from "message:" to end of block
|
|
254
|
-
const messageMatch = block.match(/^message:\s*([\s\S]*)$/im);
|
|
255
|
-
if (taskIdMatch) {
|
|
256
|
-
forwards.push({
|
|
257
|
-
taskId: taskIdMatch[1].trim(),
|
|
258
|
-
message: messageMatch ? messageMatch[1].trim() : ''
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return forwards;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Process Conductor Claude streaming output
|
|
267
|
-
*/
|
|
268
|
-
async function processConductorOutput(conductor, conductorQuery) {
|
|
269
|
-
const state = conductor.conductorState;
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
for await (const message of conductorQuery) {
|
|
273
|
-
if (conductor.status === 'stopped') break;
|
|
274
|
-
|
|
275
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
276
|
-
state.claudeSessionId = message.session_id;
|
|
277
|
-
console.log(`[Conductor] Claude session: ${message.session_id}`);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (message.type === 'assistant') {
|
|
282
|
-
const content = message.message?.content;
|
|
283
|
-
if (content) {
|
|
284
|
-
if (typeof content === 'string') {
|
|
285
|
-
state.accumulatedText += content;
|
|
286
|
-
sendConductorOutput(conductor, 'text', message);
|
|
287
|
-
} else if (Array.isArray(content)) {
|
|
288
|
-
let hasText = false;
|
|
289
|
-
for (const block of content) {
|
|
290
|
-
if (block.type === 'text') {
|
|
291
|
-
state.accumulatedText += block.text;
|
|
292
|
-
hasText = true;
|
|
293
|
-
} else if (block.type === 'tool_use') {
|
|
294
|
-
endConductorStreaming(conductor);
|
|
295
|
-
sendConductorOutput(conductor, 'tool_use', message);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (hasText) {
|
|
299
|
-
sendConductorOutput(conductor, 'text', message);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
} else if (message.type === 'user') {
|
|
304
|
-
sendConductorOutput(conductor, 'tool_result', message);
|
|
305
|
-
} else if (message.type === 'result') {
|
|
306
|
-
// Turn completed
|
|
307
|
-
console.log('[Conductor] Turn completed');
|
|
308
|
-
endConductorStreaming(conductor);
|
|
309
|
-
|
|
310
|
-
// Update costs
|
|
311
|
-
if (message.total_cost_usd != null) {
|
|
312
|
-
const costDelta = message.total_cost_usd - state.lastCostUsd;
|
|
313
|
-
if (costDelta > 0) conductor.costUsd += costDelta;
|
|
314
|
-
state.lastCostUsd = message.total_cost_usd;
|
|
315
|
-
}
|
|
316
|
-
if (message.usage) {
|
|
317
|
-
const inputDelta = (message.usage.input_tokens || 0) - (state.lastInputTokens || 0);
|
|
318
|
-
const outputDelta = (message.usage.output_tokens || 0) - (state.lastOutputTokens || 0);
|
|
319
|
-
if (inputDelta > 0) conductor.totalInputTokens += inputDelta;
|
|
320
|
-
if (outputDelta > 0) conductor.totalOutputTokens += outputDelta;
|
|
321
|
-
state.lastInputTokens = message.usage.input_tokens || 0;
|
|
322
|
-
state.lastOutputTokens = message.usage.output_tokens || 0;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Persist Claude session ID
|
|
326
|
-
if (state.claudeSessionId) {
|
|
327
|
-
saveConductorSessionId(state.claudeSessionId)
|
|
328
|
-
.catch(e => console.warn('[Conductor] Failed to save sessionId:', e.message));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Parse instructions
|
|
332
|
-
const createTasks = parseCreateTask(state.accumulatedText);
|
|
333
|
-
const forwardTasks = parseForwardTask(state.accumulatedText);
|
|
334
|
-
|
|
335
|
-
// Handle CREATE_TASK
|
|
336
|
-
for (const ct of createTasks) {
|
|
337
|
-
const taskId = `task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
338
|
-
|
|
339
|
-
// Create task directory at {workDir}/.conductor/{taskId}/ (includes worktree)
|
|
340
|
-
let worktreePath = null;
|
|
341
|
-
if (ct.workDir) {
|
|
342
|
-
try {
|
|
343
|
-
const result = await initTaskDir(ct.workDir, taskId);
|
|
344
|
-
worktreePath = result.worktreePath || null;
|
|
345
|
-
} catch (e) {
|
|
346
|
-
console.warn(`[Conductor] Failed to create task dir: ${e.message}`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Register in state.json
|
|
351
|
-
const taskEntry = {
|
|
352
|
-
taskId,
|
|
353
|
-
title: ct.title,
|
|
354
|
-
workDir: ct.workDir,
|
|
355
|
-
scenario: ct.scenario,
|
|
356
|
-
status: 'created',
|
|
357
|
-
activeActors: [],
|
|
358
|
-
currentStep: '',
|
|
359
|
-
worktreePath,
|
|
360
|
-
lastUpdate: Date.now()
|
|
361
|
-
};
|
|
362
|
-
conductor.tasks.set(taskId, taskEntry);
|
|
363
|
-
|
|
364
|
-
await updateTaskInState(taskId, taskEntry).catch(e =>
|
|
365
|
-
console.warn('[Conductor] Failed to update state.json:', e.message)
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
sendConductorOutput(conductor, 'task_created', null, {
|
|
369
|
-
taskId, taskTitle: ct.title
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
sendConductorMessage({
|
|
373
|
-
type: 'conductor_task_created',
|
|
374
|
-
task: taskEntry
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
console.log(`[Conductor] Task created: ${taskId} — ${ct.title} @ ${ct.workDir} (${ct.scenario})`);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Handle FORWARD_TASK
|
|
381
|
-
for (const ft of forwardTasks) {
|
|
382
|
-
const task = conductor.tasks.get(ft.taskId);
|
|
383
|
-
if (task) {
|
|
384
|
-
if (!task.inbox) task.inbox = [];
|
|
385
|
-
task.inbox.push({
|
|
386
|
-
from: 'conductor',
|
|
387
|
-
content: ft.message,
|
|
388
|
-
timestamp: Date.now()
|
|
389
|
-
});
|
|
390
|
-
task.lastUpdate = Date.now();
|
|
391
|
-
|
|
392
|
-
sendConductorOutput(conductor, 'task_forwarded', null, {
|
|
393
|
-
taskId: ft.taskId
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
sendConductorMessage({
|
|
397
|
-
type: 'conductor_task_message',
|
|
398
|
-
taskId: ft.taskId,
|
|
399
|
-
message: ft.message
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
console.log(`[Conductor] Forwarded to ${ft.taskId}: ${ft.message.substring(0, 60)}`);
|
|
403
|
-
} else {
|
|
404
|
-
console.warn(`[Conductor] Unknown task: ${ft.taskId}`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
state.accumulatedText = '';
|
|
409
|
-
state.turnActive = false;
|
|
410
|
-
|
|
411
|
-
sendConductorMessage({ type: 'conductor_turn_completed' });
|
|
412
|
-
sendStatusUpdate(conductor);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
} catch (error) {
|
|
416
|
-
if (error.name === 'AbortError') {
|
|
417
|
-
console.log('[Conductor] Claude aborted');
|
|
418
|
-
} else {
|
|
419
|
-
console.error('[Conductor] Claude error:', error.message);
|
|
420
|
-
endConductorStreaming(conductor);
|
|
421
|
-
state.turnActive = false;
|
|
422
|
-
state.query = null;
|
|
423
|
-
state.inputStream = null;
|
|
424
|
-
state.accumulatedText = '';
|
|
425
|
-
|
|
426
|
-
sendConductorMessage({
|
|
427
|
-
type: 'conductor_error',
|
|
428
|
-
error: error.message.substring(0, 500)
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
conductor.status = 'waiting_user';
|
|
432
|
-
sendStatusUpdate(conductor);
|
|
433
|
-
}
|
|
434
|
-
} finally {
|
|
435
|
-
if (conductor._conductorSemRelease) {
|
|
436
|
-
conductor._conductorSemRelease();
|
|
437
|
-
conductor._conductorSemRelease = null;
|
|
438
|
-
conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Stop Conductor Claude
|
|
445
|
-
*/
|
|
446
|
-
export async function stopConductorClaude(conductor) {
|
|
447
|
-
const state = conductor.conductorState;
|
|
448
|
-
if (!state) return;
|
|
449
|
-
|
|
450
|
-
if (state.claudeSessionId) {
|
|
451
|
-
await saveConductorSessionId(state.claudeSessionId)
|
|
452
|
-
.catch(e => console.warn('[Conductor] Failed to save sessionId:', e.message));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (state.abortController) {
|
|
456
|
-
state.abortController.abort();
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
state.query = null;
|
|
460
|
-
state.inputStream = null;
|
|
461
|
-
state.turnActive = false;
|
|
462
|
-
conductor.conductorState = null;
|
|
463
|
-
|
|
464
|
-
if (conductor._conductorSemRelease) {
|
|
465
|
-
conductor._conductorSemRelease();
|
|
466
|
-
conductor._conductorSemRelease = null;
|
|
467
|
-
conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Clear Conductor Claude (force new conversation)
|
|
473
|
-
*/
|
|
474
|
-
export async function clearConductorClaude(conductor) {
|
|
475
|
-
await stopConductorClaude(conductor);
|
|
476
|
-
await clearConductorSessionId();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Export parse functions for testing
|
|
480
|
-
export { parseCreateTask, parseForwardTask };
|