@yeaft/webchat-agent 0.1.225 → 0.1.227

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.
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Conductor — Worktree 管理
2
+ * Conductor — Worktree Management (V5)
3
3
  *
4
- * Conductor 的 worktree 策略与 Crew V1 不同:
5
- * - Crew V1: 每个 dev group 一个 worktree
6
- * - Conductor: 每个并行执行线程(thread) 一个 worktree
4
+ * V5 strategy: each task gets ONE worktree at creation time.
5
+ * Path: {workDir}/.conductor/task-N/worktree/
7
6
  *
8
- * worktree 位于 .conductor/tasks/task-N/worktrees/thread-N/
9
- * 复用 crew/worktree.js 的 git 操作模式
7
+ * This prevents file conflicts when multiple tasks work on the same project.
8
+ * - Read-write actors (coding/testing/review) → cwd = worktree
9
+ * - Read-only actors (planning/discussion/design) → cwd = {workDir}
10
10
  */
11
11
  import { promises as fs } from 'fs';
12
12
  import { join, resolve } from 'path';
@@ -16,117 +16,117 @@ import { promisify } from 'util';
16
16
  const execFile = promisify(execFileCb);
17
17
 
18
18
  /**
19
- * 为 task 的执行线程创建 worktree
19
+ * Create a worktree for a task.
20
+ * Called at task creation time.
20
21
  *
21
- * @param {string} projectDir - 主项目目录(git 仓库根目录)
22
- * @param {string} taskDir - task 目录路径 (.conductor/tasks/task-N)
23
- * @param {string} threadId - 线程 ID (e.g. 't1', 't2')
22
+ * @param {string} workDir - Project root directory (git repo root)
23
+ * @param {string} taskId - Task ID (e.g. 'task-m1234-abcd')
24
+ * @param {string} taskDir - Full task directory path ({workDir}/.conductor/{taskId})
24
25
  * @param {object} [options]
25
- * @param {string} [options.baseBranch='HEAD'] - 基于哪个 ref 创建
26
- * @returns {Promise<string>} worktree 路径
26
+ * @param {string} [options.baseBranch='HEAD'] - Base ref for the worktree
27
+ * @returns {Promise<string>} worktree path
27
28
  */
28
- export async function createThreadWorktree(projectDir, taskDir, threadId, options = {}) {
29
+ export async function createTaskWorktree(workDir, taskId, taskDir, options = {}) {
29
30
  const { baseBranch = 'HEAD' } = options;
30
31
 
31
- const worktreeBase = join(taskDir, 'worktrees');
32
- await fs.mkdir(worktreeBase, { recursive: true });
32
+ const wtDir = join(taskDir, 'worktree');
33
+ const branch = `conductor/${taskId}`;
33
34
 
34
- const wtDir = join(worktreeBase, threadId);
35
- const branch = `conductor/${extractTaskId(taskDir)}-${threadId}`;
36
-
37
- // 检查是否已存在
35
+ // Check if worktree already exists
38
36
  const exists = await directoryExists(wtDir);
39
37
  if (exists) {
40
- // 检查 git 是否认识它
41
- const known = await isKnownWorktree(projectDir, wtDir);
38
+ const known = await isKnownWorktree(workDir, wtDir);
42
39
  if (known) {
43
40
  console.log(`[Conductor/Worktree] Already exists: ${wtDir}`);
44
41
  return wtDir;
45
42
  }
46
- // 孤立目录,清理后重建
43
+ // Orphaned directory, clean up and recreate
47
44
  console.warn(`[Conductor/Worktree] Orphaned dir, removing: ${wtDir}`);
48
45
  await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
49
46
  }
50
47
 
48
+ // Check if this is a git repo
49
+ const isGitRepo = await directoryExists(join(workDir, '.git'));
50
+ if (!isGitRepo) {
51
+ // Not a git repo — just create a regular directory (no worktree isolation possible)
52
+ console.warn(`[Conductor/Worktree] ${workDir} is not a git repo, creating plain directory`);
53
+ await fs.mkdir(wtDir, { recursive: true });
54
+ return wtDir;
55
+ }
56
+
51
57
  try {
52
- // 创建分支
58
+ // Create branch (ignore if already exists)
53
59
  try {
54
- await execFile('git', ['branch', branch, baseBranch], { cwd: projectDir });
60
+ await execFile('git', ['branch', branch, baseBranch], { cwd: workDir });
55
61
  } catch {
56
- // 分支已存在,忽略
62
+ // Branch already exists, ignore
57
63
  }
58
64
 
59
- // 创建 worktree
60
- await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: projectDir });
65
+ // Create worktree
66
+ await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: workDir });
61
67
  console.log(`[Conductor/Worktree] Created: ${wtDir} on branch ${branch}`);
62
68
  return wtDir;
63
69
  } catch (e) {
64
- console.error(`[Conductor/Worktree] Failed to create ${threadId}:`, e.message);
65
- throw e;
70
+ console.error(`[Conductor/Worktree] Failed to create worktree for ${taskId}:`, e.message);
71
+ // Fallback: create plain directory so task can still function
72
+ await fs.mkdir(wtDir, { recursive: true }).catch(() => {});
73
+ return wtDir;
66
74
  }
67
75
  }
68
76
 
69
77
  /**
70
- * 清理 task 的全部 worktrees
78
+ * Clean up a task's worktree
71
79
  *
72
- * @param {string} projectDir - 主项目目录
73
- * @param {string} taskDir - task 目录路径
80
+ * @param {string} workDir - Project root directory
81
+ * @param {string} taskId - Task ID
82
+ * @param {string} taskDir - Full task directory path
74
83
  */
75
- export async function cleanupTaskWorktrees(projectDir, taskDir) {
76
- const worktreeBase = join(taskDir, 'worktrees');
84
+ export async function cleanupTaskWorktree(workDir, taskId, taskDir) {
85
+ const wtDir = join(taskDir, 'worktree');
86
+ const branch = `conductor/${taskId}`;
77
87
 
78
- if (!(await directoryExists(worktreeBase))) {
88
+ if (!(await directoryExists(wtDir))) return;
89
+
90
+ const isGitRepo = await directoryExists(join(workDir, '.git'));
91
+ if (!isGitRepo) {
92
+ // Plain directory, just remove
93
+ await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
79
94
  return;
80
95
  }
81
96
 
82
97
  try {
83
- const entries = await fs.readdir(worktreeBase);
84
- for (const entry of entries) {
85
- const wtDir = join(worktreeBase, entry);
86
- const branch = `conductor/${extractTaskId(taskDir)}-${entry}`;
87
-
88
- try {
89
- await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: projectDir });
90
- console.log(`[Conductor/Worktree] Removed: ${wtDir}`);
91
- } catch (e) {
92
- console.warn(`[Conductor/Worktree] Failed to remove ${wtDir}:`, e.message);
93
- }
94
-
95
- try {
96
- await execFile('git', ['branch', '-D', branch], { cwd: projectDir });
97
- console.log(`[Conductor/Worktree] Deleted branch: ${branch}`);
98
- } catch (e) {
99
- console.warn(`[Conductor/Worktree] Failed to delete branch ${branch}:`, e.message);
100
- }
101
- }
98
+ await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: workDir });
99
+ console.log(`[Conductor/Worktree] Removed: ${wtDir}`);
100
+ } catch (e) {
101
+ console.warn(`[Conductor/Worktree] Failed to remove ${wtDir}:`, e.message);
102
+ }
102
103
 
103
- // 尝试删除 worktrees 目录
104
- try {
105
- await fs.rmdir(worktreeBase);
106
- } catch { /* 目录不空,忽略 */ }
104
+ try {
105
+ await execFile('git', ['branch', '-D', branch], { cwd: workDir });
106
+ console.log(`[Conductor/Worktree] Deleted branch: ${branch}`);
107
107
  } catch (e) {
108
- console.error(`[Conductor/Worktree] Failed to cleanup ${taskDir}:`, e.message);
108
+ console.warn(`[Conductor/Worktree] Failed to delete branch ${branch}:`, e.message);
109
109
  }
110
110
  }
111
111
 
112
112
  /**
113
- * 清理整个 conductor 下的全部 worktrees
113
+ * Clean up all conductor worktrees under a project
114
114
  *
115
- * @param {string} projectDir - 主项目目录
116
- * @param {string} conductorDir - .conductor 目录路径
115
+ * @param {string} workDir - Project root directory
117
116
  */
118
- export async function cleanupAllConductorWorktrees(projectDir, conductorDir) {
119
- const tasksDir = join(conductorDir, 'tasks');
117
+ export async function cleanupAllConductorWorktrees(workDir) {
118
+ const conductorDir = join(workDir, '.conductor');
120
119
 
121
- if (!(await directoryExists(tasksDir))) return;
120
+ if (!(await directoryExists(conductorDir))) return;
122
121
 
123
122
  try {
124
- const taskEntries = await fs.readdir(tasksDir);
125
- for (const taskEntry of taskEntries) {
126
- const taskDir = join(tasksDir, taskEntry);
123
+ const entries = await fs.readdir(conductorDir);
124
+ for (const entry of entries) {
125
+ if (!entry.startsWith('task-')) continue;
126
+ const taskDir = join(conductorDir, entry);
127
127
  const stat = await fs.stat(taskDir).catch(() => null);
128
128
  if (stat?.isDirectory()) {
129
- await cleanupTaskWorktrees(projectDir, taskDir);
129
+ await cleanupTaskWorktree(workDir, entry, taskDir);
130
130
  }
131
131
  }
132
132
  } catch (e) {
@@ -134,13 +134,30 @@ export async function cleanupAllConductorWorktrees(projectDir, conductorDir) {
134
134
  }
135
135
  }
136
136
 
137
+ /**
138
+ * Get the worktree path for a task (for cwd routing)
139
+ */
140
+ export function getTaskWorktreePath(taskDir) {
141
+ return join(taskDir, 'worktree');
142
+ }
143
+
144
+ /**
145
+ * Determine actor cwd based on specialty:
146
+ * - Read-write actors → worktree
147
+ * - Read-only actors → workDir (main project directory)
148
+ */
149
+ export function getActorCwd(workDir, taskDir, specialty) {
150
+ const readWriteSpecialties = new Set(['coding', 'testing', 'review']);
151
+ if (readWriteSpecialties.has(specialty)) {
152
+ return getTaskWorktreePath(taskDir);
153
+ }
154
+ return workDir;
155
+ }
156
+
137
157
  // =====================================================================
138
158
  // Helpers
139
159
  // =====================================================================
140
160
 
141
- /**
142
- * 检查目录是否存在
143
- */
144
161
  async function directoryExists(dir) {
145
162
  try {
146
163
  await fs.access(dir);
@@ -150,9 +167,6 @@ async function directoryExists(dir) {
150
167
  }
151
168
  }
152
169
 
153
- /**
154
- * 检查目录是否是 git 认识的 worktree
155
- */
156
170
  async function isKnownWorktree(projectDir, wtDir) {
157
171
  const normalizedWtDir = resolve(wtDir);
158
172
  try {
@@ -166,10 +180,3 @@ async function isKnownWorktree(projectDir, wtDir) {
166
180
  } catch { /* ignore */ }
167
181
  return false;
168
182
  }
169
-
170
- /**
171
- * 从 taskDir 路径中提取 taskId(取最后一个路径段)
172
- */
173
- function extractTaskId(taskDir) {
174
- return taskDir.split(/[\\/]/).filter(Boolean).pop() || 'unknown';
175
- }
package/conductor.js CHANGED
@@ -1,46 +1,35 @@
1
1
  /**
2
- * Conductor Mode — 自适应项目执行引擎
2
+ * Conductor Mode — V5 Entry Point
3
3
  *
4
- * V2 的核心入口。与 crew.js 并存,不替代。
5
- *
6
- * 本文件聚合并重新导出 conductor 子模块的公共 API,
7
- * 供 message-router.js 消费。
8
- *
9
- * hideConductorSession / handleLoadConductorHistory 需要跨模块依赖,
10
- * 在这里注入,避免 persistence.js ↔ session.js 循环引用。
4
+ * Aggregates and re-exports conductor sub-module public API.
5
+ * Singleton model: one Conductor per Agent.
11
6
  */
12
7
 
13
- // Session 核心
8
+ // Session core (singleton)
14
9
  export {
15
- conductorSessions,
16
- createConductorSession,
17
- handleListConductorSessions,
18
- resumeConductorSession,
10
+ getConductor,
11
+ initConductor,
19
12
  handleConductorUserInput,
20
- handleUpdateWorkDir,
21
- handleUpdateConductorSession,
22
- stopConductorSession,
23
- clearConductorSession
13
+ stopConductor,
14
+ clearConductor
24
15
  } from './conductor/session.js';
25
16
 
26
- // 持久化(原始导出)
27
- export { loadConductorIndex } from './conductor/persistence.js';
28
-
29
- // 依赖注入包装:hideConductorSession
30
- import { conductorSessions } from './conductor/session.js';
31
- import { sendConductorMessage } from './conductor/ui-messages.js';
32
- import {
33
- hideConductorSession as _hideConductorSession,
34
- handleLoadConductorHistory as _handleLoadConductorHistory
17
+ // Persistence
18
+ export {
19
+ handleLoadConductorHistory,
20
+ loadState as loadConductorState,
21
+ ensureConductorHome
35
22
  } from './conductor/persistence.js';
36
23
 
37
- export async function hideConductorSession(sessionId) {
38
- return _hideConductorSession(sessionId, conductorSessions);
39
- }
24
+ // UI messages
25
+ import { sendConductorMessage } from './conductor/ui-messages.js';
26
+ import { getConductor } from './conductor/session.js';
27
+ import { handleLoadConductorHistory as _handleLoadConductorHistory } from './conductor/persistence.js';
40
28
 
41
- export async function handleLoadConductorHistory(msg) {
42
- return _handleLoadConductorHistory(msg, conductorSessions, sendConductorMessage);
29
+ // Dependency injection wrapper for history loading
30
+ export async function handleConductorHistory(msg) {
31
+ return _handleLoadConductorHistory(msg, getConductor(), sendConductorMessage);
43
32
  }
44
33
 
45
- // 信号量
34
+ // Semaphore
46
35
  export { globalSemaphore } from './conductor/semaphore.js';
@@ -21,11 +21,9 @@ import {
21
21
  handleLoadCrewHistory, handleCheckCrewContext
22
22
  } from '../crew.js';
23
23
  import {
24
- createConductorSession, handleListConductorSessions,
25
- resumeConductorSession, handleConductorUserInput,
26
- handleUpdateWorkDir, handleUpdateConductorSession,
27
- stopConductorSession, clearConductorSession,
28
- hideConductorSession, handleLoadConductorHistory
24
+ initConductor, handleConductorUserInput,
25
+ stopConductor, clearConductor,
26
+ handleConductorHistory
29
27
  } from '../conductor.js';
30
28
  import { sendToServer, flushMessageBuffer } from './buffer.js';
31
29
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
@@ -260,44 +258,24 @@ export async function handleMessage(msg) {
260
258
  break;
261
259
 
262
260
  // Conductor (V2) messages
263
- case 'create_conductor_session':
264
- await createConductorSession(msg);
265
- break;
266
-
267
- case 'list_conductor_sessions':
268
- await handleListConductorSessions(msg);
269
- break;
270
-
271
- case 'resume_conductor_session':
272
- await resumeConductorSession(msg);
261
+ case 'open_conductor':
262
+ await initConductor(msg);
273
263
  break;
274
264
 
275
265
  case 'conductor_user_input':
276
266
  await handleConductorUserInput(msg);
277
267
  break;
278
268
 
279
- case 'conductor_update_workdir':
280
- await handleUpdateWorkDir(msg);
281
- break;
282
-
283
- case 'update_conductor_session':
284
- await handleUpdateConductorSession(msg);
285
- break;
286
-
287
- case 'stop_conductor_session':
288
- await stopConductorSession(msg.sessionId);
289
- break;
290
-
291
- case 'clear_conductor_session':
292
- await clearConductorSession(msg.sessionId);
269
+ case 'stop_conductor':
270
+ await stopConductor();
293
271
  break;
294
272
 
295
- case 'delete_conductor_session':
296
- await hideConductorSession(msg.sessionId);
273
+ case 'clear_conductor':
274
+ await clearConductor();
297
275
  break;
298
276
 
299
277
  case 'conductor_load_history':
300
- await handleLoadConductorHistory(msg);
278
+ await handleConductorHistory(msg);
301
279
  break;
302
280
 
303
281
  // Port proxy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.225",
3
+ "version": "0.1.227",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",