@yeaft/webchat-agent 1.0.76 → 1.0.78

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/crew/session.js DELETED
@@ -1,648 +0,0 @@
1
- /**
2
- * Crew Session — 核心数据结构、角色展开和 Session 生命周期管理
3
- */
4
- import { promises as fs } from 'fs';
5
- import { join, isAbsolute } from 'path';
6
- import ctx from '../context.js';
7
- import { getMessages } from '../crew-i18n.js';
8
- import { initWorktrees } from './worktree.js';
9
- import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd, backupMemoryContent } from './shared-dir.js';
10
- import {
11
- loadCrewIndex, upsertCrewIndex, removeFromCrewIndex,
12
- loadSessionMeta, saveSessionMeta, loadSessionMessages, getMaxShardIndex
13
- } from './persistence.js';
14
- import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
15
- import { preloadSlashCommands } from '../conversation.js';
16
- import { sanitizeKanbanFile } from './task-files.js';
17
-
18
- // =====================================================================
19
- // Data Structures
20
- // =====================================================================
21
-
22
- /** @type {Map<string, CrewSession>} */
23
- export const crewSessions = new Map();
24
-
25
- // =====================================================================
26
- // Role Multi-Instance Expansion
27
- // =====================================================================
28
-
29
- const SHORT_PREFIX = {
30
- developer: 'dev',
31
- reviewer: 'rev',
32
- 'product-reviewer': 'prev'
33
- };
34
-
35
- const EXPANDABLE_ROLES = new Set(['developer', 'reviewer', 'product-reviewer']);
36
-
37
- /**
38
- * 展开角色列表:count > 1 的执行者角色展开为多个实例
39
- */
40
- export function expandRoles(roles) {
41
- const devRole = roles.find(r => r.name === 'developer');
42
- const devCount = devRole?.count > 1 ? devRole.count : 1;
43
-
44
- const expanded = [];
45
- for (const role of roles) {
46
- const isExpandable = EXPANDABLE_ROLES.has(role.name);
47
- const count = isExpandable ? devCount : 1;
48
-
49
- if (count <= 1) {
50
- expanded.push({
51
- ...role,
52
- roleType: role.name,
53
- groupIndex: isExpandable ? 1 : 0
54
- });
55
- } else {
56
- const prefix = SHORT_PREFIX[role.name] || role.name;
57
- for (let i = 1; i <= count; i++) {
58
- expanded.push({
59
- ...role,
60
- name: `${prefix}-${i}`,
61
- displayName: `${role.displayName}-${i}`,
62
- roleType: role.name,
63
- groupIndex: i,
64
- count: undefined
65
- });
66
- }
67
- }
68
- }
69
- return expanded;
70
- }
71
-
72
- /** Format role label */
73
- export function roleLabel(r) {
74
- return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
75
- }
76
-
77
- // =====================================================================
78
- // Path Validation
79
- // =====================================================================
80
-
81
- function isValidProjectDir(dir) {
82
- if (!dir || typeof dir !== 'string') return false;
83
- if (!isAbsolute(dir)) return false;
84
- if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(dir)) return false;
85
- return true;
86
- }
87
-
88
- // =====================================================================
89
- // Session Lifecycle
90
- // =====================================================================
91
-
92
- /**
93
- * 生成角色配置的规范化签名(sorted role name list)
94
- * 用于比较新旧 session 的角色配置是否一致
95
- */
96
- export function getRolesSignature(roles) {
97
- if (!roles || roles.length === 0) return '';
98
- const names = roles.map(r => r.name).sort();
99
- return names.join(',');
100
- }
101
-
102
- /**
103
- * 查找指定 projectDir 的已有 crew session
104
- * 返回 { sessionId, source, roles } 或 null
105
- */
106
- async function findExistingSessionByProjectDir(projectDir) {
107
- const normalizedDir = projectDir.replace(/\/+$/, '');
108
-
109
- for (const [, session] of crewSessions) {
110
- if (session.projectDir.replace(/\/+$/, '') === normalizedDir
111
- && session.status !== 'completed') {
112
- return {
113
- sessionId: session.id,
114
- source: 'active',
115
- roles: Array.from(session.roles.values())
116
- };
117
- }
118
- }
119
-
120
- const index = await loadCrewIndex();
121
- const agentId = ctx.CONFIG?.agentName || null;
122
- const match = index.find(e =>
123
- e.projectDir.replace(/\/+$/, '') === normalizedDir
124
- && (!agentId || !e.agentId || e.agentId === agentId)
125
- && e.status !== 'completed'
126
- );
127
-
128
- if (match) {
129
- const meta = await loadSessionMeta(match.sharedDir);
130
- if (meta) return { sessionId: match.sessionId, source: 'index', roles: meta.roles || [] };
131
- await removeFromCrewIndex(match.sessionId);
132
- }
133
-
134
- return null;
135
- }
136
-
137
- /**
138
- * 创建 Crew Session
139
- */
140
- export async function createCrewSession(msg) {
141
- const {
142
- sessionId,
143
- projectDir,
144
- sharedDir: sharedDirRel,
145
- name,
146
- roles: rawRoles = [],
147
- teamType = 'dev',
148
- language = 'zh-CN',
149
- userId,
150
- username
151
- } = msg;
152
-
153
- // 同目录检查:如果已有同目录的 session,比较角色配置
154
- const existingSession = await findExistingSessionByProjectDir(projectDir);
155
- if (existingSession) {
156
- const newRoles = expandRoles(rawRoles);
157
- const newSig = getRolesSignature(newRoles);
158
- const oldSig = getRolesSignature(existingSession.roles);
159
-
160
- if (newSig === oldSig) {
161
- // 角色配置相同,安全 auto-resume
162
- console.log(`[Crew] Found existing session for ${projectDir}: ${existingSession.sessionId}, roles match, auto-resuming`);
163
- await resumeCrewSession({ sessionId: existingSession.sessionId, userId, username });
164
- return;
165
- }
166
-
167
- // 角色配置不同 → 清理旧 session,用新配置走正常创建流程
168
- console.log(`[Crew] Roles changed for ${projectDir}: old=[${oldSig}] new=[${newSig}], discarding old session ${existingSession.sessionId}`);
169
- if (existingSession.source === 'active') {
170
- crewSessions.delete(existingSession.sessionId);
171
- }
172
- await removeFromCrewIndex(existingSession.sessionId);
173
- }
174
-
175
- const roles = expandRoles(rawRoles);
176
- const sharedDir = sharedDirRel?.startsWith('/')
177
- ? sharedDirRel
178
- : join(projectDir, sharedDirRel || '.crew');
179
- const decisionMaker = roles.find(r => r.isDecisionMaker)?.name || roles[0]?.name || null;
180
-
181
- // 尝试读取旧 session.json,合并统计数据(deleteCrewDir 保留了该文件)
182
- const oldMeta = await loadSessionMeta(sharedDir);
183
-
184
- const session = {
185
- id: sessionId,
186
- projectDir,
187
- sharedDir,
188
- name: name || '',
189
- roles: new Map(roles.map(r => [r.name, r])),
190
- roleStates: new Map(),
191
- decisionMaker,
192
- status: 'initializing',
193
- round: oldMeta?.round || 0,
194
- costUsd: oldMeta?.costUsd || 0,
195
- totalInputTokens: oldMeta?.totalInputTokens || 0,
196
- totalOutputTokens: oldMeta?.totalOutputTokens || 0,
197
- messageHistory: [],
198
- uiMessages: [],
199
- humanMessageQueue: [],
200
- waitingHumanContext: null,
201
- pendingRoutes: [],
202
- features: new Map((oldMeta?.features || []).map(f => [f.taskId, f])),
203
- _completedTaskIds: new Set(oldMeta?._completedTaskIds || []),
204
- initProgress: null,
205
- userId,
206
- username,
207
- agentId: ctx.CONFIG?.agentName || null,
208
- teamType,
209
- language,
210
- createdAt: oldMeta?.createdAt || Date.now()
211
- };
212
-
213
- if (oldMeta) {
214
- console.log(`[Crew] Merged stats from previous session: round=${session.round}, cost=$${session.costUsd.toFixed(4)}, inputTokens=${session.totalInputTokens}, outputTokens=${session.totalOutputTokens}`);
215
- // 恢复旧消息历史(deleteCrewDir 保留了 messages*.json)
216
- const loaded = await loadSessionMessages(sharedDir);
217
- if (loaded.messages.length > 0) {
218
- session.uiMessages = loaded.messages;
219
- console.log(`[Crew] Restored ${loaded.messages.length} messages from previous session`);
220
- }
221
- }
222
-
223
- crewSessions.set(sessionId, session);
224
-
225
- // Startup self-heal: rewrite kanban.md once to drop any historical dirty
226
- // rows (placeholders, bare ids, multi-line summaries) even if no further
227
- // task-update happens this session.
228
- sanitizeKanbanFile(session).catch(e =>
229
- console.warn('[Crew] Kanban startup migration failed:', e.message)
230
- );
231
-
232
- // 如果有旧消息,检查是否有更早的分片
233
- const hasOlderMessages = oldMeta ? await getMaxShardIndex(sharedDir) > 0 : false;
234
-
235
- sendCrewMessage({
236
- type: 'crew_session_created',
237
- sessionId,
238
- projectDir,
239
- sharedDir,
240
- name: name || '',
241
- roles: roles.map(r => ({
242
- name: r.name,
243
- displayName: r.displayName,
244
- icon: r.icon,
245
- description: r.description,
246
- isDecisionMaker: r.isDecisionMaker || false,
247
- model: r.model,
248
- roleType: r.roleType,
249
- groupIndex: r.groupIndex
250
- })),
251
- decisionMaker,
252
- userId,
253
- username,
254
- // 旧消息(recreate 时保留的历史)
255
- uiMessages: session.uiMessages.length > 0 ? session.uiMessages : undefined,
256
- hasOlderMessages: hasOlderMessages || undefined
257
- });
258
-
259
- sendStatusUpdate(session);
260
-
261
- try {
262
- session.initProgress = 'roles';
263
- sendStatusUpdate(session);
264
- await initSharedDir(sharedDir, roles, projectDir, language);
265
-
266
- const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
267
- if (groupIndices.length > 0) {
268
- session.initProgress = 'worktrees';
269
- sendStatusUpdate(session);
270
- }
271
- const worktreeMap = await initWorktrees(projectDir, roles);
272
-
273
- for (const role of roles) {
274
- if (role.groupIndex > 0 && worktreeMap.has(role.groupIndex)) {
275
- role.workDir = worktreeMap.get(role.groupIndex);
276
- await writeRoleClaudeMd(sharedDir, role, language, roles);
277
- }
278
- }
279
-
280
- await upsertCrewIndex(session);
281
- await saveSessionMeta(session);
282
-
283
- if (session.status === 'initializing') {
284
- session.status = 'running';
285
- }
286
- session.initProgress = null;
287
- sendStatusUpdate(session);
288
- } catch (e) {
289
- console.error('[Crew] Session initialization failed:', e);
290
- if (session.status === 'initializing') {
291
- session.status = 'running';
292
- }
293
- session.initProgress = null;
294
- sendStatusUpdate(session);
295
- sendCrewMessage({
296
- type: 'crew_output',
297
- sessionId,
298
- roleName: 'system',
299
- roleIcon: 'S',
300
- roleDisplayName: '系统',
301
- content: `工作环境初始化失败: ${e.message}`,
302
- isTurnEnd: true
303
- });
304
- }
305
-
306
- // ★ Preload project-level skills for crew session input autocomplete
307
- preloadSlashCommands(projectDir, sessionId).catch(() => {});
308
-
309
- return session;
310
- }
311
-
312
- // =====================================================================
313
- // List & Resume Sessions
314
- // =====================================================================
315
-
316
- /**
317
- * 列出所有 crew sessions
318
- */
319
- export async function handleListCrewSessions(msg) {
320
- const { requestId, _requestClientId } = msg;
321
- const index = await loadCrewIndex();
322
-
323
- const agentId = ctx.CONFIG?.agentName || null;
324
- const filtered = agentId
325
- ? index.filter(e => !e.agentId || e.agentId === agentId)
326
- : index;
327
-
328
- const sessions = filtered.map(entry => {
329
- const active = crewSessions.get(entry.sessionId);
330
- return {
331
- ...entry,
332
- active: !!active,
333
- status: active ? active.status : 'stopped'
334
- };
335
- });
336
-
337
- ctx.sendToServer({
338
- type: 'crew_sessions_list',
339
- requestId,
340
- _requestClientId,
341
- sessions
342
- });
343
- }
344
-
345
- /**
346
- * 检查工作目录下是否存在 .crew 目录
347
- */
348
- export async function handleCheckCrewExists(msg) {
349
- const { projectDir, requestId, _requestClientId } = msg;
350
- if (!projectDir || !isValidProjectDir(projectDir)) {
351
- ctx.sendToServer({
352
- type: 'crew_exists_result',
353
- requestId,
354
- _requestClientId,
355
- exists: false,
356
- error: 'projectDir is required'
357
- });
358
- return;
359
- }
360
-
361
- const crewDir = join(projectDir, '.crew');
362
- try {
363
- const stat = await fs.stat(crewDir);
364
- if (stat.isDirectory()) {
365
- let sessionInfo = null;
366
- try {
367
- const sessionPath = join(crewDir, 'session.json');
368
- const data = await fs.readFile(sessionPath, 'utf-8');
369
- sessionInfo = JSON.parse(data);
370
- } catch {}
371
- ctx.sendToServer({
372
- type: 'crew_exists_result',
373
- requestId,
374
- _requestClientId,
375
- exists: true,
376
- projectDir,
377
- sessionInfo
378
- });
379
- } else {
380
- ctx.sendToServer({
381
- type: 'crew_exists_result',
382
- requestId,
383
- _requestClientId,
384
- exists: false,
385
- projectDir
386
- });
387
- }
388
- } catch {
389
- ctx.sendToServer({
390
- type: 'crew_exists_result',
391
- requestId,
392
- _requestClientId,
393
- exists: false,
394
- projectDir
395
- });
396
- }
397
- }
398
-
399
- /**
400
- * 删除 Crew 定义文件(模板/角色配置),保留所有用户数据和工作产出
401
- *
402
- * 删除: CLAUDE.md(共享模板)、roles/(角色模板)
403
- * 清空: sessions/ 下的文件(旧角色的 Claude Code session IDs,已失效)
404
- * 清除: crew-index 中的旧 entry(防止 createCrewSession 走 resume 而非 create)
405
- * 保留: context/、session.json、messages*.json 及任何其他生成文件(截图、设计文档等)
406
- */
407
- export async function handleDeleteCrewDir(msg) {
408
- const { projectDir } = msg;
409
- if (!isValidProjectDir(projectDir)) return;
410
- const crewDir = join(projectDir, '.crew');
411
- try {
412
- // 提取并备份记忆内容(删除前)
413
- await backupMemoryContent(crewDir);
414
-
415
- // 删除 Crew 模板定义
416
- await fs.rm(join(crewDir, 'CLAUDE.md'), { force: true }).catch(() => {});
417
- await fs.rm(join(crewDir, 'roles'), { recursive: true, force: true }).catch(() => {});
418
-
419
- // 清空 sessions/ 内容(旧角色的 session IDs 已失效),保留目录本身
420
- const sessionsDir = join(crewDir, 'sessions');
421
- try {
422
- const sessionFiles = await fs.readdir(sessionsDir);
423
- await Promise.all(
424
- sessionFiles.map(f => fs.rm(join(sessionsDir, f), { recursive: true, force: true }).catch(() => {}))
425
- );
426
- } catch { /* sessions/ may not exist */ }
427
-
428
- // 清除 crew-index 中的旧 entry(不删文件),确保新建时走 create → loadSessionMeta 合并统计
429
- const normalizedDir = projectDir.replace(/\/+$/, '');
430
- const index = await loadCrewIndex();
431
- const match = index.find(e => e.projectDir.replace(/\/+$/, '') === normalizedDir);
432
- if (match) {
433
- await removeFromCrewIndex(match.sessionId);
434
- console.log(`[Crew] Cleared index entry for ${projectDir} (sessionId: ${match.sessionId})`);
435
- }
436
- } catch {}
437
- }
438
-
439
- /**
440
- * 恢复已停止的 crew session
441
- */
442
- export async function resumeCrewSession(msg) {
443
- const { sessionId, userId, username } = msg;
444
-
445
- if (crewSessions.has(sessionId)) {
446
- const session = crewSessions.get(sessionId);
447
- const roles = Array.from(session.roles.values());
448
- if ((!session.uiMessages || session.uiMessages.length === 0) && session.sharedDir) {
449
- const loaded = await loadSessionMessages(session.sharedDir);
450
- session.uiMessages = loaded.messages;
451
- }
452
- const cleanedMessages = (session.uiMessages || []).map(m => {
453
- const { _streaming, ...rest } = m;
454
- return rest;
455
- });
456
- const hasOlderMessages = await getMaxShardIndex(session.sharedDir) > 0;
457
-
458
- sendCrewMessage({
459
- type: 'crew_session_restored',
460
- sessionId,
461
- projectDir: session.projectDir,
462
- sharedDir: session.sharedDir,
463
- name: session.name || '',
464
- roles: roles.map(r => ({
465
- name: r.name, displayName: r.displayName, icon: r.icon,
466
- description: r.description, isDecisionMaker: r.isDecisionMaker || false,
467
- groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
468
- })),
469
- decisionMaker: session.decisionMaker,
470
- userId: session.userId,
471
- username: session.username,
472
- uiMessages: cleanedMessages,
473
- hasOlderMessages
474
- });
475
- sendStatusUpdate(session);
476
- // ★ Preload project-level skills for crew session input autocomplete
477
- preloadSlashCommands(session.projectDir, sessionId).catch(() => {});
478
- return;
479
- }
480
-
481
- const index = await loadCrewIndex();
482
- const indexEntry = index.find(e => e.sessionId === sessionId);
483
- if (!indexEntry) {
484
- console.warn(`[Crew] resumeCrewSession: session ${sessionId} not found in index`);
485
- sendCrewMessage({ type: 'crew_session_restore_failed', sessionId, message: 'Crew session not found in index' });
486
- return;
487
- }
488
-
489
- const meta = await loadSessionMeta(indexEntry.sharedDir);
490
- if (!meta) {
491
- console.warn(`[Crew] resumeCrewSession: session.json not found at ${indexEntry.sharedDir}`);
492
- sendCrewMessage({ type: 'crew_session_restore_failed', sessionId, message: 'Crew session metadata not found' });
493
- return;
494
- }
495
-
496
- const roles = meta.roles || [];
497
- // Migration: strip claudeMd from legacy session.json (now persisted in per-role CLAUDE.md files)
498
- for (const r of roles) delete r.claudeMd;
499
- if (roles.length === 0) {
500
- console.warn(`[Crew] resumeCrewSession: session ${sessionId} has empty roles in session.json`);
501
- }
502
- const decisionMaker = meta.decisionMaker || roles[0]?.name || null;
503
- const session = {
504
- id: sessionId,
505
- projectDir: meta.projectDir,
506
- sharedDir: meta.sharedDir || indexEntry.sharedDir,
507
- name: meta.name || '',
508
- roles: new Map(roles.map(r => [r.name, r])),
509
- roleStates: new Map(),
510
- decisionMaker,
511
- status: 'waiting_human',
512
- round: meta.round || 0,
513
- costUsd: meta.costUsd || 0,
514
- totalInputTokens: meta.totalInputTokens || 0,
515
- totalOutputTokens: meta.totalOutputTokens || 0,
516
- messageHistory: [],
517
- uiMessages: [],
518
- humanMessageQueue: [],
519
- waitingHumanContext: null,
520
- pendingRoutes: [],
521
- features: new Map((meta.features || []).map(f => [f.taskId, f])),
522
- _completedTaskIds: new Set(meta._completedTaskIds || []),
523
- userId: userId || meta.userId,
524
- username: username || meta.username,
525
- agentId: meta.agentId || ctx.CONFIG?.agentName || null,
526
- teamType: meta.teamType || 'dev',
527
- language: meta.language || 'zh-CN',
528
- createdAt: meta.createdAt || Date.now()
529
- };
530
- crewSessions.set(sessionId, session);
531
-
532
- // Startup self-heal on resume as well
533
- sanitizeKanbanFile(session).catch(e =>
534
- console.warn('[Crew] Kanban startup migration failed:', e.message)
535
- );
536
-
537
- const loaded = await loadSessionMessages(session.sharedDir);
538
- session.uiMessages = loaded.messages;
539
-
540
- sendCrewMessage({
541
- type: 'crew_session_restored',
542
- sessionId,
543
- projectDir: session.projectDir,
544
- sharedDir: session.sharedDir,
545
- name: session.name || '',
546
- roles: roles.map(r => ({
547
- name: r.name, displayName: r.displayName, icon: r.icon,
548
- description: r.description, isDecisionMaker: r.isDecisionMaker || false,
549
- groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
550
- })),
551
- decisionMaker,
552
- userId: session.userId,
553
- username: session.username,
554
- uiMessages: session.uiMessages,
555
- hasOlderMessages: loaded.hasOlderMessages
556
- });
557
- sendStatusUpdate(session);
558
-
559
- await upsertCrewIndex(session);
560
- await saveSessionMeta(session);
561
-
562
- // ★ Preload project-level skills for crew session input autocomplete
563
- preloadSlashCommands(session.projectDir, sessionId).catch(() => {});
564
-
565
- console.log(`[Crew] Session ${sessionId} resumed, waiting for human input`);
566
- }
567
-
568
- /**
569
- * 更新 crew session 的 name 和/或 roles 配置
570
- * roles 变更时:重新展开角色、初始化新 worktrees、更新 CLAUDE.md、通知前端
571
- */
572
- export async function handleUpdateCrewSession(msg) {
573
- const { sessionId, name, roles: newRolesConfig } = msg;
574
- const session = crewSessions.get(sessionId);
575
- if (!session) {
576
- console.warn(`[Crew] Session not found for update: ${sessionId}`);
577
- return;
578
- }
579
- if (name !== undefined) session.name = name;
580
-
581
- // Handle roles update (count changes, etc.)
582
- if (newRolesConfig && Array.isArray(newRolesConfig) && newRolesConfig.length > 0) {
583
- const newExpanded = expandRoles(newRolesConfig);
584
- const oldRoleNames = new Set(session.roles.keys());
585
- const newRoleNames = new Set(newExpanded.map(r => r.name));
586
-
587
- // Stop and clean up removed roles
588
- for (const oldName of oldRoleNames) {
589
- if (!newRoleNames.has(oldName)) {
590
- const roleState = session.roleStates.get(oldName);
591
- if (roleState) {
592
- if (roleState.abortController) {
593
- roleState.abortController.abort();
594
- }
595
- session.roleStates.delete(oldName);
596
- }
597
- session.roles.delete(oldName);
598
- console.log(`[Crew] Role removed during update: ${oldName}`);
599
- }
600
- }
601
-
602
- // Add new roles and update existing ones
603
- for (const r of newExpanded) {
604
- if (!oldRoleNames.has(r.name)) {
605
- // Brand new role — add it
606
- session.roles.set(r.name, r);
607
- console.log(`[Crew] Role added during update: ${r.name} (${r.displayName})`);
608
- } else {
609
- // Existing role — update metadata (displayName, icon, etc.) but preserve roleState
610
- // claudeMd is NOT updated here — it's managed via CLAUDE.md files, not through edit session
611
- const existing = session.roles.get(r.name);
612
- existing.displayName = r.displayName;
613
- existing.icon = r.icon;
614
- existing.description = r.description;
615
- existing.isDecisionMaker = r.isDecisionMaker;
616
- existing.roleType = r.roleType;
617
- existing.groupIndex = r.groupIndex;
618
- }
619
- }
620
-
621
- // Update decision maker
622
- const dm = newExpanded.find(r => r.isDecisionMaker);
623
- if (dm) session.decisionMaker = dm.name;
624
-
625
- // Initialize worktrees for any new dev groups
626
- const allRoles = Array.from(session.roles.values());
627
- const worktreeMap = await initWorktrees(session.projectDir, allRoles);
628
- for (const role of allRoles) {
629
- if (role.groupIndex > 0 && worktreeMap.has(role.groupIndex) && !role.workDir) {
630
- role.workDir = worktreeMap.get(role.groupIndex);
631
- }
632
- }
633
-
634
- // Regenerate shared CLAUDE.md and role-specific CLAUDE.md files
635
- await updateSharedClaudeMd(session);
636
- for (const role of allRoles) {
637
- if (role.groupIndex > 0 && role.workDir) {
638
- await writeRoleClaudeMd(session.sharedDir, role, session.language || 'zh-CN', allRoles);
639
- }
640
- }
641
-
642
- // Notify frontend about role changes
643
- sendStatusUpdate(session);
644
- }
645
-
646
- await saveSessionMeta(session);
647
- await upsertCrewIndex(session);
648
- }