@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.
- package/conductor/conductor-claude.js +148 -129
- package/conductor/index.js +12 -10
- package/conductor/persistence.js +172 -136
- package/conductor/session.js +118 -265
- package/conductor/ui-messages.js +53 -52
- package/conductor/worktree.js +89 -82
- package/conductor.js +21 -32
- package/connection/message-router.js +10 -32
- package/package.json +1 -1
package/conductor/worktree.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Conductor — Worktree
|
|
2
|
+
* Conductor — Worktree Management (V5)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
19
|
+
* Create a worktree for a task.
|
|
20
|
+
* Called at task creation time.
|
|
20
21
|
*
|
|
21
|
-
* @param {string}
|
|
22
|
-
* @param {string}
|
|
23
|
-
* @param {string}
|
|
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'] -
|
|
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
|
|
29
|
+
export async function createTaskWorktree(workDir, taskId, taskDir, options = {}) {
|
|
29
30
|
const { baseBranch = 'HEAD' } = options;
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
|
|
32
|
+
const wtDir = join(taskDir, 'worktree');
|
|
33
|
+
const branch = `conductor/${taskId}`;
|
|
33
34
|
|
|
34
|
-
|
|
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
|
-
|
|
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:
|
|
60
|
+
await execFile('git', ['branch', branch, baseBranch], { cwd: workDir });
|
|
55
61
|
} catch {
|
|
56
|
-
//
|
|
62
|
+
// Branch already exists, ignore
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
//
|
|
60
|
-
await execFile('git', ['worktree', 'add', wtDir, branch], { cwd:
|
|
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 ${
|
|
65
|
-
|
|
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
|
-
*
|
|
78
|
+
* Clean up a task's worktree
|
|
71
79
|
*
|
|
72
|
-
* @param {string}
|
|
73
|
-
* @param {string}
|
|
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
|
|
76
|
-
const
|
|
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(
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
108
|
+
console.warn(`[Conductor/Worktree] Failed to delete branch ${branch}:`, e.message);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
113
|
+
* Clean up all conductor worktrees under a project
|
|
114
114
|
*
|
|
115
|
-
* @param {string}
|
|
116
|
-
* @param {string} conductorDir - .conductor 目录路径
|
|
115
|
+
* @param {string} workDir - Project root directory
|
|
117
116
|
*/
|
|
118
|
-
export async function cleanupAllConductorWorktrees(
|
|
119
|
-
const
|
|
117
|
+
export async function cleanupAllConductorWorktrees(workDir) {
|
|
118
|
+
const conductorDir = join(workDir, '.conductor');
|
|
120
119
|
|
|
121
|
-
if (!(await directoryExists(
|
|
120
|
+
if (!(await directoryExists(conductorDir))) return;
|
|
122
121
|
|
|
123
122
|
try {
|
|
124
|
-
const
|
|
125
|
-
for (const
|
|
126
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
handleListConductorSessions,
|
|
18
|
-
resumeConductorSession,
|
|
10
|
+
getConductor,
|
|
11
|
+
initConductor,
|
|
19
12
|
handleConductorUserInput,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
stopConductorSession,
|
|
23
|
-
clearConductorSession
|
|
13
|
+
stopConductor,
|
|
14
|
+
clearConductor
|
|
24
15
|
} from './conductor/session.js';
|
|
25
16
|
|
|
26
|
-
//
|
|
27
|
-
export {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 '
|
|
264
|
-
await
|
|
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 '
|
|
280
|
-
await
|
|
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 '
|
|
296
|
-
await
|
|
273
|
+
case 'clear_conductor':
|
|
274
|
+
await clearConductor();
|
|
297
275
|
break;
|
|
298
276
|
|
|
299
277
|
case 'conductor_load_history':
|
|
300
|
-
await
|
|
278
|
+
await handleConductorHistory(msg);
|
|
301
279
|
break;
|
|
302
280
|
|
|
303
281
|
// Port proxy
|