@yeaft/webchat-agent 0.1.236 → 0.1.238

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.
@@ -21,6 +21,7 @@ import {
21
21
  getConductorHome, ensureConductorHome,
22
22
  loadState, saveState, updateTaskInState, initTaskDir
23
23
  } from './persistence.js';
24
+ import { startTaskExecution, forwardToTask } from './task-runner.js';
24
25
 
25
26
  // =====================================================================
26
27
  // Claude Session ID Persistence
@@ -142,7 +143,15 @@ export async function createConductorClaude(conductor) {
142
143
  cwd: conductorHome,
143
144
  permissionMode: 'bypassPermissions',
144
145
  abort: abortController.signal,
145
- appendSystemPrompt: systemPrompt
146
+ appendSystemPrompt: systemPrompt,
147
+ // Conductor Claude is a lightweight classifier — restrict heavy tools
148
+ // Only allow: Read, Glob, Grep (for context awareness), WebFetch, WebSearch
149
+ disallowedTools: [
150
+ 'Write', 'Edit', 'NotebookEdit',
151
+ 'Bash', 'Agent',
152
+ 'EnterPlanMode', 'ExitPlanMode',
153
+ 'EnterWorktree', 'ExitWorktree'
154
+ ]
146
155
  };
147
156
 
148
157
  if (savedSessionId) {
@@ -191,6 +200,7 @@ export async function sendToConductor(conductor, content) {
191
200
 
192
201
  state.turnActive = true;
193
202
  state.accumulatedText = '';
203
+ state._lastUserContent = content; // Preserve original user input for task description
194
204
  state.inputStream.enqueue({
195
205
  type: 'user',
196
206
  message: { role: 'user', content: fullContent }
@@ -328,6 +338,18 @@ async function processConductorOutput(conductor, conductorQuery) {
328
338
  for (const ct of createTasks) {
329
339
  const taskId = `task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
330
340
 
341
+ // Notify frontend: task is being created (worktree may take a few seconds)
342
+ sendConductorOutput(conductor, 'task_creating', null, {
343
+ taskId, taskTitle: ct.title
344
+ });
345
+ sendConductorMessage({
346
+ type: 'conductor_task_creating',
347
+ taskId,
348
+ title: ct.title,
349
+ workDir: ct.workDir,
350
+ scenario: ct.scenario
351
+ });
352
+
331
353
  // Create task directory at {workDir}/.conductor/{taskId}/ (includes worktree)
332
354
  let worktreePath = null;
333
355
  if (ct.workDir) {
@@ -339,10 +361,15 @@ async function processConductorOutput(conductor, conductorQuery) {
339
361
  }
340
362
  }
341
363
 
364
+ // Extract user's original message as task description
365
+ // (the content that triggered this CREATE_TASK)
366
+ const userDescription = state._lastUserContent || ct.title;
367
+
342
368
  // Register in state.json
343
369
  const taskEntry = {
344
370
  taskId,
345
371
  title: ct.title,
372
+ description: userDescription,
346
373
  workDir: ct.workDir,
347
374
  scenario: ct.scenario,
348
375
  status: 'created',
@@ -367,6 +394,11 @@ async function processConductorOutput(conductor, conductorQuery) {
367
394
  });
368
395
 
369
396
  console.log(`[Conductor] Task created: ${taskId} — ${ct.title} @ ${ct.workDir} (${ct.scenario})`);
397
+
398
+ // Start Orchestrator-driven execution (fire-and-forget)
399
+ startTaskExecution(conductor, taskEntry).catch(e =>
400
+ console.error(`[Conductor] Task execution failed for ${taskId}:`, e.message)
401
+ );
370
402
  }
371
403
 
372
404
  // Handle FORWARD_TASK
@@ -381,6 +413,11 @@ async function processConductorOutput(conductor, conductorQuery) {
381
413
  });
382
414
  task.lastUpdate = Date.now();
383
415
 
416
+ // Forward to Orchestrator (which will relay to active actors)
417
+ forwardToTask(ft.taskId, ft.message).catch(e =>
418
+ console.warn(`[Conductor] Failed to forward to orchestrator ${ft.taskId}:`, e.message)
419
+ );
420
+
384
421
  sendConductorOutput(conductor, 'task_forwarded', null, {
385
422
  taskId: ft.taskId
386
423
  });
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Conductor — Module Entry
3
3
  *
4
- * Exports three core subsystems:
4
+ * Exports core subsystems:
5
5
  * 1. Personas — 18 persona definitions (4 scenarios)
6
6
  * 2. Specialties — specialty behavior definitions
7
7
  * 3. CLAUDE.md Generator — dynamic 3-section generation
8
8
  * 4. Actor — Claude instance create/manage/release
9
9
  * 5. Worktree — per-task worktree management
10
+ * 6. Task Runner — Orchestrator ↔ Actor bridge layer
10
11
  */
11
12
 
12
13
  // ---- Personas ----
@@ -61,5 +62,15 @@ export {
61
62
  cleanupTaskWorktree,
62
63
  cleanupAllConductorWorktrees,
63
64
  getTaskWorktreePath,
64
- getActorCwd
65
+ getActorCwd,
66
+ READ_WRITE_SPECIALTIES
65
67
  } from './worktree.js';
68
+
69
+ // ---- Task Runner (Orchestrator ↔ Actor bridge) ----
70
+ export {
71
+ startTaskExecution,
72
+ stopTaskExecution,
73
+ stopAllTaskExecutions,
74
+ forwardToTask,
75
+ getOrchestrator
76
+ } from './task-runner.js';
@@ -53,6 +53,18 @@ export class Orchestrator {
53
53
 
54
54
  /** @type {Map<string, Task>} */
55
55
  this.tasks = new Map();
56
+
57
+ /** Abort flag — checked at each phase boundary in _runStateMachine */
58
+ this._aborted = false;
59
+ }
60
+
61
+ /**
62
+ * Abort the orchestrator — stops the state machine loop at the next phase boundary.
63
+ * Does NOT release actors (that's handled by task-runner's stopTaskExecution).
64
+ */
65
+ abort() {
66
+ this._aborted = true;
67
+ this.log('Orchestrator aborted');
56
68
  }
57
69
 
58
70
  // ===================================================================
@@ -63,15 +75,16 @@ export class Orchestrator {
63
75
  * 创建并启动新 Task
64
76
  *
65
77
  * @param {object} params
78
+ * @param {string} [params.taskId] — 外部指定的 taskId(来自 Conductor),不提供则自动生成
66
79
  * @param {string} params.title
67
80
  * @param {string} params.description
68
81
  * @param {string} params.scenario — 'dev' | 'writing' | 'trading' | 'video'
69
82
  * @param {string} params.workDir
70
83
  * @returns {Task}
71
84
  */
72
- async startTask({ title, description, scenario, workDir }) {
85
+ async startTask({ taskId: externalTaskId, title, description, scenario, workDir }) {
73
86
  const flow = getFlow(scenario); // 验证 scenario 合法性
74
- const task = createTask({ title, description, scenario, workDir });
87
+ const task = createTask({ taskId: externalTaskId, title, description, scenario, workDir });
75
88
  this.tasks.set(task.taskId, task);
76
89
  this._notify(task);
77
90
 
@@ -114,13 +127,33 @@ export class Orchestrator {
114
127
 
115
128
  /**
116
129
  * 用户消息注入(Task 对话页直接交互时)
117
- * TODO: 后续实现用户消息处理
130
+ *
131
+ * 将 Conductor 转发的用户消息注入到正在运行的 task:
132
+ * - 如果 task 有活跃的 actor,发送给所有活跃 actor
133
+ * - 如果没有活跃 actor(phase 之间),存入 inbox 等待下一个 actor
118
134
  */
119
135
  async handleUserMessage(taskId, message) {
120
136
  const task = this.tasks.get(taskId);
121
137
  if (!task) return;
122
138
  this.log(`User message for ${taskId}: ${message.substring(0, 100)}...`);
123
- // 用户消息处理逻辑由 Conductor 引擎层实现
139
+
140
+ // Store in inbox for actors in subsequent phases
141
+ if (!task.inbox) task.inbox = [];
142
+ task.inbox.push({ from: 'user', content: message, timestamp: Date.now() });
143
+ task.updatedAt = Date.now();
144
+
145
+ // If there are active actors, forward to them
146
+ if (task.activeActors.length > 0) {
147
+ for (const actor of task.activeActors) {
148
+ try {
149
+ this.actorManager.sendMessage?.(actor.instanceId, message);
150
+ } catch (e) {
151
+ this.log(`Failed to send user message to actor ${actor.instanceId}: ${e.message}`);
152
+ }
153
+ }
154
+ }
155
+
156
+ this._notify(task);
124
157
  }
125
158
 
126
159
  // ===================================================================
@@ -132,6 +165,15 @@ export class Orchestrator {
132
165
  */
133
166
  async _runStateMachine(task, flow) {
134
167
  while (task.phase !== Phase.DONE && task.phase !== Phase.FAILED) {
168
+ // Check abort flag at each phase boundary
169
+ if (this._aborted) {
170
+ this.log(`Task ${task.taskId} aborted`);
171
+ task.error = 'Task aborted';
172
+ transitionPhase(task, Phase.FAILED);
173
+ this._notify(task);
174
+ break;
175
+ }
176
+
135
177
  this.log(`Task ${task.taskId} entering phase: ${task.phase}`);
136
178
 
137
179
  switch (task.phase) {
@@ -16,6 +16,7 @@ import {
16
16
  sendConductorMessage, sendConductorOutput, sendStatusUpdate, recordUserMessage
17
17
  } from './ui-messages.js';
18
18
  import { createConductorClaude, sendToConductor, stopConductorClaude } from './conductor-claude.js';
19
+ import { stopAllTaskExecutions } from './task-runner.js';
19
20
 
20
21
  // =====================================================================
21
22
  // Singleton Conductor Instance
@@ -164,6 +165,7 @@ export async function stopConductor() {
164
165
  if (!conductor) return;
165
166
 
166
167
  conductor.status = 'stopped';
168
+ await stopAllTaskExecutions();
167
169
  await stopConductorClaude(conductor);
168
170
 
169
171
  sendConductorOutput(conductor, 'system', {
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Conductor — Task Runner (Bridge Layer)
3
+ *
4
+ * Bridges the gap between Conductor Claude (task creation) and
5
+ * Orchestrator (state machine) + Actor (Claude instances).
6
+ *
7
+ * Responsibilities:
8
+ * 1. Create actorManager adapter that translates Orchestrator's interface
9
+ * to Actor module's actual parameter format
10
+ * 2. Instantiate Orchestrator and start task execution
11
+ * 3. Relay status changes and actor output to frontend via UI messages
12
+ * 4. Handle task completion / failure cleanup
13
+ */
14
+ import { createOrchestrator } from './orchestrator.js';
15
+ import {
16
+ createActor, sendToActor, releaseActor, releaseAllActors
17
+ } from './actor.js';
18
+ import { getTaskDir, updateTaskInState } from './persistence.js';
19
+ import {
20
+ sendConductorMessage, sendConductorOutput, sendStatusUpdate
21
+ } from './ui-messages.js';
22
+ import { READ_WRITE_SPECIALTIES } from './worktree.js';
23
+
24
+ // =====================================================================
25
+ // Per-task Orchestrator registry
26
+ // =====================================================================
27
+
28
+ /** @type {Map<string, Orchestrator>} taskId → Orchestrator */
29
+ const orchestrators = new Map();
30
+
31
+ /**
32
+ * Get an active orchestrator for a task
33
+ * @param {string} taskId
34
+ * @returns {Orchestrator|null}
35
+ */
36
+ export function getOrchestrator(taskId) {
37
+ return orchestrators.get(taskId) || null;
38
+ }
39
+
40
+ // =====================================================================
41
+ // ActorManager Adapter
42
+ // =====================================================================
43
+
44
+ /**
45
+ * Create an actorManager adapter that translates Orchestrator's
46
+ * createActor(persona, specialty, context) interface into Actor module's
47
+ * createActor({ personaId, specialtyId, ... }) format.
48
+ *
49
+ * Orchestrator calls:
50
+ * actorManager.createActor(
51
+ * { id: personaId, name: personaName },
52
+ * specialty, // string like 'coding', 'planning', etc.
53
+ * { taskId, actorInstanceId, instruction, workDir, memoryPath, ... }
54
+ * ) → Promise<ActorResult>
55
+ *
56
+ * Actor.js expects:
57
+ * createActor({
58
+ * personaId, specialtyId, taskId, taskDir, taskContext,
59
+ * worktreePath, onOutput, onComplete, onError
60
+ * }) → Promise<ActorInstance>
61
+ *
62
+ * The adapter wraps actor creation + message sending + result collection
63
+ * into a single Promise<ActorResult> that the Orchestrator awaits.
64
+ */
65
+ function createActorManagerAdapter(conductor, taskEntry) {
66
+ const { taskId, workDir, worktreePath } = taskEntry;
67
+ const taskDir = getTaskDir(workDir, taskId);
68
+
69
+ return {
70
+ /**
71
+ * Create an actor, send it instructions, and wait for its result.
72
+ * @returns {Promise<ActorResult>}
73
+ */
74
+ async createActor(persona, specialty, context) {
75
+ const actorWorktreePath = READ_WRITE_SPECIALTIES.has(specialty)
76
+ ? worktreePath
77
+ : null;
78
+
79
+ // Create a deferred promise for actor completion
80
+ let resolveResult, rejectResult;
81
+ const resultPromise = new Promise((resolve, reject) => {
82
+ resolveResult = resolve;
83
+ rejectResult = reject;
84
+ });
85
+
86
+ const instance = await createActor({
87
+ personaId: persona.id,
88
+ specialtyId: specialty,
89
+ taskId,
90
+ taskDir,
91
+ taskContext: {
92
+ description: taskEntry.description || '',
93
+ instruction: context.instruction || context.description || '',
94
+ memoryPath: context.memoryPath || null,
95
+ planPath: context.planPath || null,
96
+ scenario: taskEntry.scenario,
97
+ workDir
98
+ },
99
+ threadId: context.threadId || null,
100
+ worktreePath: actorWorktreePath,
101
+ onOutput: (instanceId, message) => {
102
+ // Relay actor output to frontend in real-time
103
+ sendConductorOutput(conductor, 'text', message, {
104
+ taskId,
105
+ persona: persona.name || persona.id,
106
+ specialty
107
+ });
108
+ },
109
+ onComplete: (instanceId, result) => {
110
+ console.log(`[TaskRunner] Actor ${instanceId} completed for task ${taskId}`);
111
+
112
+ // Accumulate actor cost into conductor totals
113
+ if (result.costUsd) conductor.costUsd += result.costUsd;
114
+ if (result.inputTokens) conductor.totalInputTokens += result.inputTokens;
115
+ if (result.outputTokens) conductor.totalOutputTokens += result.outputTokens;
116
+
117
+ // Update tracking: decrement active count
118
+ if (taskEntry.activeActors) {
119
+ const idx = taskEntry.activeActors.indexOf(instanceId);
120
+ if (idx !== -1) taskEntry.activeActors.splice(idx, 1);
121
+ }
122
+ conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
123
+ sendStatusUpdate(conductor);
124
+
125
+ // Release actor from registry to prevent memory leak
126
+ releaseActor(taskId, instanceId).catch(e =>
127
+ console.warn(`[TaskRunner] Failed to auto-release actor ${instanceId}:`, e.message)
128
+ );
129
+
130
+ resolveResult(result);
131
+ },
132
+ onError: (instanceId, error) => {
133
+ console.error(`[TaskRunner] Actor ${instanceId} failed for task ${taskId}:`, error.message);
134
+
135
+ // Update tracking: decrement active count
136
+ if (taskEntry.activeActors) {
137
+ const idx = taskEntry.activeActors.indexOf(instanceId);
138
+ if (idx !== -1) taskEntry.activeActors.splice(idx, 1);
139
+ }
140
+ conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
141
+ sendStatusUpdate(conductor);
142
+
143
+ // Release actor from registry on error too
144
+ releaseActor(taskId, instanceId).catch(e =>
145
+ console.warn(`[TaskRunner] Failed to auto-release failed actor ${instanceId}:`, e.message)
146
+ );
147
+
148
+ rejectResult(error);
149
+ }
150
+ });
151
+
152
+ // Update conductor's active claude count
153
+ conductor.activeClaudes = (conductor.activeClaudes || 0) + 1;
154
+
155
+ // Update task entry's activeActors for status tracking
156
+ if (!taskEntry.activeActors) taskEntry.activeActors = [];
157
+ taskEntry.activeActors.push(instance.instanceId);
158
+ sendStatusUpdate(conductor);
159
+
160
+ // Send the instruction to the actor to start work
161
+ const instruction = context.instruction || context.description || taskEntry.description || '';
162
+ if (instruction) {
163
+ sendToActor(taskId, instance.instanceId, instruction);
164
+ }
165
+
166
+ return resultPromise;
167
+ },
168
+
169
+ /**
170
+ * Release an actor instance.
171
+ */
172
+ releaseActor(instanceId) {
173
+ releaseActor(taskId, instanceId).catch(e =>
174
+ console.warn(`[TaskRunner] Failed to release actor ${instanceId}:`, e.message)
175
+ );
176
+
177
+ // Update tracking
178
+ if (taskEntry.activeActors) {
179
+ const idx = taskEntry.activeActors.indexOf(instanceId);
180
+ if (idx !== -1) taskEntry.activeActors.splice(idx, 1);
181
+ }
182
+ conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
183
+ },
184
+
185
+ /**
186
+ * Send a message to an active actor (for user message forwarding).
187
+ */
188
+ sendMessage(instanceId, message) {
189
+ sendToActor(taskId, instanceId, message);
190
+ }
191
+ };
192
+ }
193
+
194
+ // =====================================================================
195
+ // Task Execution Entry Point
196
+ // =====================================================================
197
+
198
+ /**
199
+ * Start task execution via Orchestrator.
200
+ *
201
+ * Called after initTaskDir completes (folder + worktree ready).
202
+ * This is fire-and-forget — the orchestrator runs asynchronously.
203
+ *
204
+ * @param {object} conductor — Conductor singleton instance
205
+ * @param {object} taskEntry — Task registry entry from conductor.tasks Map
206
+ */
207
+ export async function startTaskExecution(conductor, taskEntry) {
208
+ const { taskId, title, workDir, scenario, description } = taskEntry;
209
+
210
+ console.log(`[TaskRunner] Starting execution for task ${taskId}: "${title}" [${scenario}]`);
211
+
212
+ // Update task status
213
+ taskEntry.status = 'running';
214
+ taskEntry.lastUpdate = Date.now();
215
+
216
+ // Create actorManager adapter
217
+ const actorManager = createActorManagerAdapter(conductor, taskEntry);
218
+
219
+ // Create Orchestrator instance
220
+ const orchestrator = createOrchestrator({
221
+ actorManager,
222
+
223
+ onStatusChange: (serializedTask) => {
224
+ // Sync orchestrator state back to conductor's task entry
225
+ taskEntry.status = serializedTask.phase;
226
+ taskEntry.currentStep = `${serializedTask.phase}${serializedTask.currentStepIndex >= 0 ? ' step ' + serializedTask.currentStepIndex : ''}`;
227
+ taskEntry.activeActors = serializedTask.activeActors.map(a => a.instanceId);
228
+ taskEntry.lastUpdate = Date.now();
229
+
230
+ // Persist to state.json (async, non-blocking)
231
+ updateTaskInState(taskId, taskEntry).catch(e =>
232
+ console.warn(`[TaskRunner] Failed to persist task ${taskId}:`, e.message)
233
+ );
234
+
235
+ // Push status to frontend
236
+ sendConductorMessage({
237
+ type: 'conductor_task_status',
238
+ taskId,
239
+ task: {
240
+ ...taskEntry,
241
+ phase: serializedTask.phase,
242
+ complexity: serializedTask.complexity,
243
+ plan: serializedTask.plan,
244
+ activeActors: serializedTask.activeActors,
245
+ completedActors: serializedTask.completedActors,
246
+ iterations: serializedTask.iterations
247
+ }
248
+ });
249
+ sendStatusUpdate(conductor);
250
+ },
251
+
252
+ onLog: (...args) => {
253
+ console.log('[TaskRunner/Orchestrator]', ...args);
254
+ }
255
+ });
256
+
257
+ // Register orchestrator
258
+ orchestrators.set(taskId, orchestrator);
259
+
260
+ // Start the task (async — orchestrator runs its state machine internally)
261
+ try {
262
+ await orchestrator.startTask({
263
+ taskId,
264
+ title,
265
+ description: description || title,
266
+ scenario,
267
+ workDir
268
+ });
269
+
270
+ console.log(`[TaskRunner] Orchestrator started for task ${taskId}`);
271
+ } catch (err) {
272
+ console.error(`[TaskRunner] Failed to start orchestrator for task ${taskId}:`, err.message);
273
+
274
+ taskEntry.status = 'failed';
275
+ taskEntry.error = err.message;
276
+ taskEntry.lastUpdate = Date.now();
277
+
278
+ await updateTaskInState(taskId, taskEntry).catch(() => {});
279
+
280
+ sendConductorOutput(conductor, 'system', {
281
+ message: { role: 'assistant', content: `Task "${title}" failed to start: ${err.message}` }
282
+ });
283
+ sendStatusUpdate(conductor);
284
+
285
+ // Cleanup
286
+ orchestrators.delete(taskId);
287
+ await releaseAllActors(taskId, { force: true }).catch(() => {});
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Forward a user message to a running task's orchestrator.
293
+ *
294
+ * @param {string} taskId
295
+ * @param {string} message
296
+ */
297
+ export async function forwardToTask(taskId, message) {
298
+ const orchestrator = orchestrators.get(taskId);
299
+ if (!orchestrator) {
300
+ console.warn(`[TaskRunner] Cannot forward to task ${taskId}: no active orchestrator`);
301
+ return;
302
+ }
303
+
304
+ await orchestrator.handleUserMessage(taskId, message);
305
+ }
306
+
307
+ /**
308
+ * Stop a running task's orchestrator and release all its actors.
309
+ *
310
+ * @param {string} taskId
311
+ */
312
+ export async function stopTaskExecution(taskId) {
313
+ const orchestrator = orchestrators.get(taskId);
314
+ if (!orchestrator) return;
315
+
316
+ console.log(`[TaskRunner] Stopping task ${taskId}`);
317
+
318
+ // Abort the orchestrator state machine (stops at next phase boundary)
319
+ orchestrator.abort();
320
+ orchestrators.delete(taskId);
321
+
322
+ // Release all actors for this task
323
+ await releaseAllActors(taskId, { force: true }).catch(e =>
324
+ console.warn(`[TaskRunner] Error releasing actors for ${taskId}:`, e.message)
325
+ );
326
+ }
327
+
328
+ /**
329
+ * Stop all running task orchestrators (used during conductor shutdown).
330
+ */
331
+ export async function stopAllTaskExecutions() {
332
+ const taskIds = [...orchestrators.keys()];
333
+ for (const taskId of taskIds) {
334
+ await stopTaskExecution(taskId);
335
+ }
336
+ }
package/conductor/task.js CHANGED
@@ -58,14 +58,15 @@ export function generateTaskId() {
58
58
  * 创建 Task 实例
59
59
  *
60
60
  * @param {object} params
61
+ * @param {string} [params.taskId] — 外部指定的 taskId(来自 Conductor),不提供则自动生成
61
62
  * @param {string} params.title — 任务标题
62
63
  * @param {string} params.description — 任务描述(用户原始输入 or Conductor 转译)
63
64
  * @param {string} params.scenario — 场景: dev / writing / trading / video
64
65
  * @param {string} params.workDir — 工作目录(创建时绑定)
65
66
  * @returns {Task}
66
67
  */
67
- export function createTask({ title, description, scenario, workDir }) {
68
- const taskId = generateTaskId();
68
+ export function createTask({ taskId: externalTaskId, title, description, scenario, workDir }) {
69
+ const taskId = externalTaskId || generateTaskId();
69
70
  const now = Date.now();
70
71
 
71
72
  return {
@@ -20,7 +20,7 @@ export function sendConductorMessage(msg) {
20
20
 
21
21
  /**
22
22
  * Send Conductor Claude output to frontend
23
- * outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_created' | 'task_forwarded'
23
+ * outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_creating' | 'task_created' | 'task_forwarded'
24
24
  */
25
25
  export function sendConductorOutput(conductor, outputType, rawMessage, extra = {}) {
26
26
  sendConductorMessage({
@@ -64,6 +64,13 @@ export function sendConductorOutput(conductor, outputType, rawMessage, extra = {
64
64
  conductor.uiMessages.push({
65
65
  source: 'conductor', type: 'system', content: text, timestamp: Date.now()
66
66
  });
67
+ } else if (outputType === 'task_creating') {
68
+ conductor.uiMessages.push({
69
+ source: 'conductor', type: 'task_creating',
70
+ taskId: extra.taskId, taskTitle: extra.taskTitle,
71
+ content: `Creating task: ${extra.taskTitle}...`,
72
+ timestamp: Date.now()
73
+ });
67
74
  } else if (outputType === 'task_created') {
68
75
  conductor.uiMessages.push({
69
76
  source: 'conductor', type: 'task_created',
@@ -15,6 +15,12 @@ import { promisify } from 'util';
15
15
 
16
16
  const execFile = promisify(execFileCb);
17
17
 
18
+ /**
19
+ * Specialties that need read-write access (use worktree as cwd).
20
+ * Shared constant — imported by task-runner.js.
21
+ */
22
+ export const READ_WRITE_SPECIALTIES = new Set(['coding', 'testing', 'review']);
23
+
18
24
  /**
19
25
  * Create a worktree for a task.
20
26
  * Called at task creation time.
@@ -147,8 +153,7 @@ export function getTaskWorktreePath(taskDir) {
147
153
  * - Read-only actors → workDir (main project directory)
148
154
  */
149
155
  export function getActorCwd(workDir, taskDir, specialty) {
150
- const readWriteSpecialties = new Set(['coding', 'testing', 'review']);
151
- if (readWriteSpecialties.has(specialty)) {
156
+ if (READ_WRITE_SPECIALTIES.has(specialty)) {
152
157
  return getTaskWorktreePath(taskDir);
153
158
  }
154
159
  return workDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.236",
3
+ "version": "0.1.238",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",