@yeaft/webchat-agent 1.0.77 → 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.
@@ -9,11 +9,7 @@ export const BUFFERABLE_TYPES = new Set([
9
9
  'turn_completed', 'conversation_closed',
10
10
  'session_id_update', 'compact_status', 'slash_commands_update',
11
11
  'background_task_started', 'background_task_output',
12
- 'subagent_started', 'subagent_message', 'subagent_completed',
13
- 'crew_output', 'crew_status', 'crew_turn_completed',
14
- 'crew_session_created', 'crew_session_restored', 'crew_human_needed',
15
- 'crew_role_added', 'crew_role_removed',
16
- 'crew_role_compact', 'crew_context_usage'
12
+ 'subagent_started', 'subagent_message', 'subagent_completed'
17
13
  ]);
18
14
 
19
15
  function bufferMessage(msg, reason) {
@@ -27,7 +23,7 @@ function bufferMessage(msg, reason) {
27
23
  return 'buffered';
28
24
  }
29
25
  // Buffer full: drop oldest non-status messages to make room
30
- const dropIdx = ctx.messageBuffer.findIndex(m => m.type !== 'crew_status' && m.type !== 'turn_completed');
26
+ const dropIdx = ctx.messageBuffer.findIndex(m => m.type !== 'turn_completed');
31
27
  if (dropIdx >= 0) {
32
28
  ctx.messageBuffer.splice(dropIdx, 1);
33
29
  ctx.messageBuffer.push(msg);
@@ -3,11 +3,7 @@
3
3
  *
4
4
  * task-330c lint guard:
5
5
  * ⚠️ DO NOT introduce greedy `text.replace(/---ROUTE---[\s\S]*$/g, '')`
6
- * style strips on routed message payloads. Crew ROUTE stripping is
7
- * owned EXCLUSIVELY by `agent/crew/routing.js` `parseRoutes()` which
8
- * returns `{routes, displayBody}` with exact ranges removed. A second
9
- * strip here would re-process already-cleaned text and risks both
10
- * double-strip artefacts and the trailing-prose bug fixed by task-328.
6
+ * style strips on routed message payloads.
11
7
  */
12
8
  import ctx from '../context.js';
13
9
  import { decodeKey } from '../encryption.js';
@@ -26,12 +22,6 @@ import {
26
22
  sendConversationList, handleBtwQuestion, preloadSlashCommands,
27
23
  handlePingSession
28
24
  } from '../conversation.js';
29
- import {
30
- createCrewSession, handleCrewHumanInput, handleCrewControl,
31
- addRoleToSession, removeRoleFromSession,
32
- handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex,
33
- handleLoadCrewHistory, handleCheckCrewContext
34
- } from '../crew.js';
35
25
  import { sendToServer, flushMessageBuffer } from './buffer.js';
36
26
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
37
27
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
@@ -118,10 +108,6 @@ export async function handleMessage(msg) {
118
108
  preloadSlashCommands().catch(() => {});
119
109
  break;
120
110
 
121
- case 'check_crew_context':
122
- handleCheckCrewContext(msg);
123
- break;
124
-
125
111
  case 'resume_conversation':
126
112
  await resumeConversation(msg);
127
113
  break;
@@ -262,55 +248,6 @@ export async function handleMessage(msg) {
262
248
  handleAskUserAnswer(msg);
263
249
  break;
264
250
 
265
- // Crew (multi-agent) messages
266
- case 'create_crew_session':
267
- await createCrewSession(msg);
268
- break;
269
-
270
- case 'crew_human_input':
271
- await handleCrewHumanInput(msg);
272
- break;
273
-
274
- case 'crew_control':
275
- await handleCrewControl(msg);
276
- break;
277
-
278
- case 'crew_add_role':
279
- await addRoleToSession(msg);
280
- break;
281
-
282
- case 'crew_remove_role':
283
- await removeRoleFromSession(msg);
284
- break;
285
-
286
- case 'list_crew_sessions':
287
- await handleListCrewSessions(msg);
288
- break;
289
-
290
- case 'check_crew_exists':
291
- await handleCheckCrewExists(msg);
292
- break;
293
-
294
- case 'delete_crew_dir':
295
- await handleDeleteCrewDir(msg);
296
- break;
297
-
298
- case 'resume_crew_session':
299
- await resumeCrewSession(msg);
300
- break;
301
-
302
- case 'delete_crew_session':
303
- await removeFromCrewIndex(msg.sessionId);
304
- (await import('../conversation.js')).sendConversationList();
305
- break;
306
-
307
- case 'update_crew_session':
308
- await (await import('../crew.js')).handleUpdateCrewSession(msg);
309
- break;
310
-
311
- case 'crew_load_history':
312
- await handleLoadCrewHistory(msg);
313
- break;
314
251
 
315
252
  // Port proxy
316
253
  case 'proxy_request':
package/conversation.js CHANGED
@@ -5,7 +5,6 @@ import ctx from './context.js';
5
5
  import { query } from './sdk/index.js';
6
6
  import { loadSessionHistory } from './history.js';
7
7
  import { startClaudeQuery } from './claude.js';
8
- import { crewSessions } from './crew.js';
9
8
  import { getProvider, DEFAULT_PROVIDER, isValidProvider } from './providers/index.js';
10
9
 
11
10
  // 不支持的斜杠命令(真正需要交互式 CLI 的命令)
@@ -191,7 +190,7 @@ function prestartClaude(conversationId, workDir, resumeSessionId) {
191
190
  *
192
191
  * @param {string} [workDir] - Project directory (default: agent workDir)
193
192
  * @param {string} [targetId] - conversationId to key the update to
194
- * ('__preload__' for agent-level, or crewSessionId)
193
+ * ('__preload__' for agent-level)
195
194
  */
196
195
  export async function preloadSlashCommands(workDir, targetId = '__preload__') {
197
196
  const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
@@ -283,7 +282,7 @@ export function parseSlashCommand(message) {
283
282
  return { type: null, message };
284
283
  }
285
284
 
286
- // 发送 conversation 列表(仅含活跃 crew sessions;历史 Crew 索引按需通过 list_crew_sessions 加载)
285
+ // 发送 conversation 列表。
287
286
  export async function sendConversationList() {
288
287
  const list = [];
289
288
  for (const [id, state] of ctx.conversations) {
@@ -305,19 +304,6 @@ export async function sendConversationList() {
305
304
  };
306
305
  list.push(entry);
307
306
  }
308
- // 追加活跃 crew sessions。历史 Crew 索引可能触发磁盘读取,保持按需加载,
309
- // 由显式 list_crew_sessions 请求处理,避免普通 Chat/Yeaft 列表提前加载 Crew。
310
- for (const [id, session] of crewSessions) {
311
- list.push({
312
- id,
313
- workDir: session.projectDir,
314
- createdAt: session.createdAt,
315
- processing: session.status === 'running',
316
- userId: session.userId,
317
- username: session.username,
318
- type: 'crew',
319
- });
320
- }
321
307
  ctx.sendToServer({
322
308
  type: 'conversation_list',
323
309
  conversations: list
@@ -1046,19 +1032,16 @@ export function handleAskUserAnswer(msg) {
1046
1032
  }
1047
1033
 
1048
1034
  /**
1049
- * Handle /btw side question — supports multi-turn and Crew mode.
1035
+ * Handle /btw side question.
1050
1036
  *
1051
1037
  * Multi-turn: First question forks the session (forkSession: true).
1052
1038
  * Subsequent questions resume the forked session (no fork).
1053
1039
  * The forked session ID is captured from system init and returned in btw_done.
1054
- *
1055
- * Crew mode: If conversationId is a crew session, use the decision maker's
1056
- * claudeSessionId as the base for forking.
1057
1040
  */
1058
1041
  export async function handleBtwQuestion(msg) {
1059
1042
  const { conversationId, question, btwSessionId } = msg;
1060
1043
 
1061
- // 1. Find the base session — Chat or Crew decision maker
1044
+ // 1. Find the base chat session.
1062
1045
  let baseSessionId = null;
1063
1046
  let workDir = null;
1064
1047
 
@@ -1066,17 +1049,6 @@ export async function handleBtwQuestion(msg) {
1066
1049
  if (chatState?.claudeSessionId) {
1067
1050
  baseSessionId = chatState.claudeSessionId;
1068
1051
  workDir = chatState.workDir;
1069
- } else {
1070
- // Crew mode: find decision maker's session
1071
- const crewSession = crewSessions.get(conversationId);
1072
- if (crewSession) {
1073
- const dmName = crewSession.decisionMaker;
1074
- const dmState = dmName ? crewSession.roleStates.get(dmName) : null;
1075
- if (dmState?.claudeSessionId) {
1076
- baseSessionId = dmState.claudeSessionId;
1077
- workDir = crewSession.projectDir;
1078
- }
1079
- }
1080
1052
  }
1081
1053
 
1082
1054
  if (!baseSessionId) {
package/history.js CHANGED
@@ -65,8 +65,8 @@ export function getWorkDirFromProjectFolder(projectFolderPath, folderName) {
65
65
 
66
66
  // 获取指定目录的历史会话列表
67
67
  export async function getHistorySessions(workDir) {
68
- // 过滤掉 crew 角色目录的 sessions
69
- if (workDir && /[/\\]\.crew[/\\]roles[/\\]/.test(workDir)) {
68
+ const retiredCollabRoleDirPattern = new RegExp('[/\\\\]\\\\.' + 'crew' + '[/\\\\]roles[/\\\\]');
69
+ if (workDir && retiredCollabRoleDirPattern.test(workDir)) {
70
70
  return [];
71
71
  }
72
72
 
package/index.js CHANGED
@@ -121,12 +121,6 @@ async function detectCapabilities() {
121
121
  const pty = await loadNodePty();
122
122
  if (pty) capabilities.push('terminal');
123
123
 
124
- // Crew mode requires Claude CLI
125
- try {
126
- const { getDefaultClaudeCodePath } = await import('./sdk/utils.js');
127
- const claudePath = getDefaultClaudeCodePath();
128
- if (claudePath) capabilities.push('crew');
129
- } catch {}
130
124
 
131
125
  console.log(`[Capabilities] Detected: ${capabilities.join(', ')}`);
132
126
  return capabilities;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.77",
3
+ "version": "1.0.78",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -62,7 +62,7 @@ export async function listFolders() {
62
62
  let stats;
63
63
  try { stats = statSync(entryPath); } catch { continue; }
64
64
  if (!stats.isDirectory()) continue;
65
- if (entry.includes('--crew-roles-')) continue;
65
+ if (entry.includes('--' + 'crew' + '-roles-')) continue;
66
66
 
67
67
  const originalPath = getWorkDirFromProjectFolder(entryPath, entry);
68
68
  let sessionCount = 0;
@@ -16,7 +16,7 @@
16
16
  * synthesized [Uploaded files] suffix) for the LLM call.
17
17
  *
18
18
  * Inputs (`files`) come from the server-side resolver in
19
- * `client-conversation.js` / `client-crew.js`: each entry is
19
+ * `client-conversation.js`: each entry is
20
20
  * `{ name, mimeType, data: <base64>, isImage }` — the `pendingFiles`
21
21
  * `fileId` was already consumed by the server before `forwardToAgent`.
22
22
  *
@@ -33,7 +33,7 @@
33
33
  * Field name is `media_type` (snake_case) to match the Anthropic
34
34
  * Messages API spec — Anthropic adapter forwards user-content blocks
35
35
  * verbatim, so the on-the-wire form must already be correct here.
36
- * `crew/routing.js` / `workbench/transfer.js` emit the same shape.
36
+ * `workbench/transfer.js` emits the same shape.
37
37
  * - `failed`: list of `{ name, error }` for entries that could not
38
38
  * be persisted (disk full, bad base64, ...). The caller surfaces
39
39
  * this so the UI can tell the user *which* file blew up rather
package/yeaft/config.js CHANGED
@@ -434,7 +434,7 @@ export function loadConfig(overrides = {}) {
434
434
  projectDocMaxBytes: overrides.projectDocMaxBytes ?? jsonConfig.projectDocMaxBytes ?? DEFAULTS.projectDocMaxBytes,
435
435
 
436
436
  // task-318: Yeaft runtime caps. `yeaft` is a nested section so we
437
- // don't pollute the flat config namespace used by chat/crew code.
437
+ // don't pollute the flat config namespace used by chat code.
438
438
  yeaft: normaliseYeaftSection(jsonConfig.yeaft),
439
439
 
440
440
  // Legacy fields (null when using config.json)
@@ -12,10 +12,7 @@
12
12
  *
13
13
  * task-330c lint guard:
14
14
  * ⚠️ DO NOT introduce greedy `text.replace(/---ROUTE---[\s\S]*$/g, '')`
15
- * style strips on incoming/outgoing message bodies. Crew ROUTE
16
- * stripping is owned EXCLUSIVELY by `agent/crew/routing.js`
17
- * `parseRoutes()` which returns `{routes, displayBody}` with exact
18
- * ranges removed.
15
+ * style strips on incoming/outgoing message bodies.
19
16
  */
20
17
 
21
18
  import { delimiter, join } from 'node:path';
@@ -3591,7 +3588,7 @@ async function runYeaftSessionSend(msg) {
3591
3588
 
3592
3589
  // ── Attachments (images + files) ───────────────────────────────
3593
3590
  // Server has already resolved fileId → { name, mimeType, data:base64,
3594
- // isImage } via the same path crew uses (client-conversation.js relay
3591
+ // isImage } via the client-conversation.js relay
3595
3592
  // for `yeaft_*`). We persist files to disk under the agent's CWD so
3596
3593
  // file-tools (file-read / bash) can pick them up with relative paths,
3597
3594
  // and we build per-image content blocks for the LLM call. The
@@ -1,154 +0,0 @@
1
- /**
2
- * Crew — built-in role actions (task-330a §B)
3
- *
4
- * These two actions replace the "PM 给自己发闭环消息" anti-pattern that
5
- * task-330a §A now rejects at the routing layer:
6
- *
7
- * • taskClose(session, { taskId, summary, fromRole })
8
- * — directly mark the task complete on the kanban + broadcast a
9
- * status card via sendCrewMessage. No ROUTE round-trip.
10
- *
11
- * • roleStandby(session, { role, reason, fromRole })
12
- * — flip a role's runtime state to 'standby' and broadcast.
13
- * Persistence to .crew/context/role-states.json is a 330d
14
- * concern — we only define the in-memory contract here.
15
- *
16
- * Both functions are pure server-side helpers. They do NOT consume a
17
- * routing turn (no session.round++, no dispatchToRole). Callers that
18
- * choose to expose them as Claude tools (via canCallTool / mcp) must
19
- * wire that separately — task-330a defines the contract; tool-binding
20
- * lands in 330d alongside the role-state persistence layer.
21
- *
22
- * Red lines (per PM dispatch):
23
- * - Do not mutate routing protocol shape (.routes / displayBody).
24
- * - Do not break task-319/328 (parser tests stay green).
25
- * - Old session replay must keep working — we only add new keys to
26
- * session.roleStates[role], never remove.
27
- */
28
-
29
- import { sendCrewMessage, sendStatusUpdate } from './ui-messages.js';
30
- import { updateKanban, appendChangelog, updateFeatureIndex, isValidTaskId } from './task-files.js';
31
-
32
- /** Valid standby reasons — extend with care; consumers may grep for these. */
33
- export const STANDBY_REASONS = Object.freeze([
34
- 'task_closed', 'awaiting_input', 'manual', 'idle', 'paused',
35
- ]);
36
-
37
- /**
38
- * Mark a task as complete on the kanban + broadcast a status card.
39
- *
40
- * Mirrors the side-effects that role-output.js performs when it detects a
41
- * completed TASKS block, so the two paths converge on the same kanban
42
- * state regardless of which one fires.
43
- *
44
- * @param {object} session
45
- * @param {{ taskId: string, summary?: string, fromRole?: string }} params
46
- * @returns {Promise<{ ok: boolean, taskId: string, reason?: string }>}
47
- */
48
- export async function taskClose(session, { taskId, summary, fromRole }) {
49
- if (!session) {
50
- return { ok: false, taskId: taskId || null, reason: 'no_session' };
51
- }
52
- if (!taskId || !isValidTaskId(taskId)) {
53
- return { ok: false, taskId: taskId || null, reason: 'invalid_task_id' };
54
- }
55
-
56
- // Mark in the in-memory completion set so future kanban rebuilds keep it.
57
- if (!session._completedTaskIds) session._completedTaskIds = new Set();
58
- const wasAlreadyCompleted = session._completedTaskIds.has(taskId);
59
- session._completedTaskIds.add(taskId);
60
-
61
- const feature = session.features?.get(taskId);
62
- const taskTitle = feature?.taskTitle || taskId;
63
- const cleanSummary = (summary && String(summary).trim()) || '已完成';
64
-
65
- // Persist to the kanban file (best-effort; warns on failure).
66
- try {
67
- await updateKanban(session, { taskId, completed: true, summary: cleanSummary });
68
- } catch (e) {
69
- console.warn(`[Crew] taskClose: updateKanban failed for ${taskId}:`, e.message);
70
- }
71
-
72
- // Append to features index + changelog only on first close (idempotent).
73
- if (!wasAlreadyCompleted) {
74
- updateFeatureIndex(session)
75
- .catch(e => console.warn('[Crew] taskClose: updateFeatureIndex failed:', e.message));
76
- appendChangelog(session, taskId, taskTitle)
77
- .catch(e => console.warn(`[Crew] taskClose: appendChangelog failed for ${taskId}:`, e.message));
78
- }
79
-
80
- // Broadcast status card so the UI reflects the close immediately.
81
- try {
82
- sendCrewMessage({
83
- type: 'crew_task_closed',
84
- sessionId: session.id,
85
- taskId,
86
- taskTitle,
87
- summary: cleanSummary,
88
- fromRole: fromRole || null,
89
- timestamp: Date.now(),
90
- });
91
- sendStatusUpdate(session);
92
- } catch (e) {
93
- console.warn(`[Crew] taskClose: broadcast failed for ${taskId}:`, e.message);
94
- }
95
-
96
- return { ok: true, taskId };
97
- }
98
-
99
- /**
100
- * Flip a role into 'standby' (in-memory) and broadcast.
101
- *
102
- * 330a defines the contract; 330d wires the .crew/context/role-states.json
103
- * persistence. We DO mutate `session.roleStates[role].standby` here so the
104
- * UI / status pipeline can reflect the change immediately, and 330d can
105
- * snapshot it on the next debounced save.
106
- *
107
- * @param {object} session
108
- * @param {{ role: string, reason?: string, fromRole?: string }} params
109
- * @returns {{ ok: boolean, role: string, reason?: string }}
110
- */
111
- export function roleStandby(session, { role, reason, fromRole }) {
112
- if (!session) {
113
- return { ok: false, role: role || null, reason: 'no_session' };
114
- }
115
- if (!role || typeof role !== 'string') {
116
- return { ok: false, role: role || null, reason: 'invalid_role' };
117
- }
118
- if (!session.roles || !session.roles.has(role)) {
119
- return { ok: false, role, reason: 'unknown_role' };
120
- }
121
-
122
- const normalizedReason = reason && STANDBY_REASONS.includes(reason)
123
- ? reason
124
- : 'manual';
125
-
126
- // Ensure the roleState bucket exists (cold-start safe).
127
- let roleState = session.roleStates?.get?.(role);
128
- if (!roleState) {
129
- roleState = {};
130
- session.roleStates?.set?.(role, roleState);
131
- }
132
- // New keys only — never delete legacy fields (replay safety).
133
- roleState.standby = {
134
- reason: normalizedReason,
135
- since: Date.now(),
136
- setBy: fromRole || null,
137
- };
138
-
139
- try {
140
- sendCrewMessage({
141
- type: 'crew_role_standby',
142
- sessionId: session.id,
143
- role,
144
- reason: normalizedReason,
145
- fromRole: fromRole || null,
146
- timestamp: Date.now(),
147
- });
148
- sendStatusUpdate(session);
149
- } catch (e) {
150
- console.warn(`[Crew] roleStandby: broadcast failed for ${role}:`, e.message);
151
- }
152
-
153
- return { ok: true, role, reason: normalizedReason };
154
- }
@@ -1,171 +0,0 @@
1
- /**
2
- * Crew Context Loader — detect and parse .crew/ directory structure.
3
- *
4
- * Reads .crew/CLAUDE.md, session.json, per-role CLAUDE.md files,
5
- * kanban, and feature files. Used by the frontend to detect whether
6
- * a project has a Crew setup and display role metadata.
7
- */
8
-
9
- import { join } from 'path';
10
- import { readFileSync, existsSync, readdirSync } from 'fs';
11
- import ctx from '../context.js';
12
-
13
- const MAX_CLAUDE_MD_LEN = 8192;
14
-
15
- /**
16
- * Read a file, returning null on any error.
17
- * @param {string} filePath
18
- * @returns {string|null}
19
- */
20
- function readFileOrNull(filePath) {
21
- try {
22
- if (!existsSync(filePath)) return null;
23
- return readFileSync(filePath, 'utf-8');
24
- } catch {
25
- return null;
26
- }
27
- }
28
-
29
- /**
30
- * Deduplicate Crew roles by roleType.
31
- * Crew may have dev-1, dev-2, dev-3 — collapse to a single "dev" entry.
32
- * Attaches per-role CLAUDE.md content.
33
- */
34
- function deduplicateRoles(sessionRoles, roleClaudes) {
35
- const byType = new Map();
36
- const merged = [];
37
-
38
- for (const r of sessionRoles) {
39
- const type = r.roleType || r.name;
40
- const claudeMd = roleClaudes[r.name] || '';
41
-
42
- if (byType.has(type)) continue;
43
- byType.set(type, true);
44
-
45
- const name = type;
46
- let displayName = r.displayName || name;
47
- displayName = displayName.replace(/-\d+$/, '');
48
-
49
- merged.push({
50
- name,
51
- displayName,
52
- icon: r.icon || '',
53
- description: r.description || '',
54
- claudeMd: claudeMd.substring(0, MAX_CLAUDE_MD_LEN),
55
- roleType: type,
56
- isDecisionMaker: !!r.isDecisionMaker,
57
- });
58
- }
59
-
60
- return merged;
61
- }
62
-
63
- /**
64
- * Load .crew context from a project directory.
65
- * Returns null if .crew/ doesn't exist.
66
- */
67
- export function loadCrewContext(projectDir) {
68
- const crewDir = join(projectDir, '.crew');
69
- if (!existsSync(crewDir)) return null;
70
-
71
- // 1. Shared CLAUDE.md
72
- const sharedClaudeMd = readFileOrNull(join(crewDir, 'CLAUDE.md')) || '';
73
-
74
- // 2. session.json → roles, teamType, language, features
75
- let sessionRoles = [];
76
- let teamType = 'dev';
77
- let language = 'zh-CN';
78
- let sessionFeatures = [];
79
- const sessionPath = join(crewDir, 'session.json');
80
- const sessionJson = readFileOrNull(sessionPath);
81
- if (sessionJson) {
82
- try {
83
- const session = JSON.parse(sessionJson);
84
- if (Array.isArray(session.roles)) sessionRoles = session.roles;
85
- if (session.teamType) teamType = session.teamType;
86
- if (session.language) language = session.language;
87
- if (Array.isArray(session.features)) sessionFeatures = session.features;
88
- } catch {
89
- // Invalid JSON — ignore
90
- }
91
- }
92
-
93
- // 3. Per-role CLAUDE.md from .crew/roles/*/CLAUDE.md
94
- const roleClaudes = {};
95
- const rolesDir = join(crewDir, 'roles');
96
- if (existsSync(rolesDir)) {
97
- try {
98
- const roleDirs = readdirSync(rolesDir, { withFileTypes: true })
99
- .filter(d => d.isDirectory())
100
- .map(d => d.name);
101
- for (const dirName of roleDirs) {
102
- const md = readFileOrNull(join(rolesDir, dirName, 'CLAUDE.md'));
103
- if (md) roleClaudes[dirName] = md;
104
- }
105
- } catch {
106
- // Permission error or similar — ignore
107
- }
108
- }
109
-
110
- // 4. Merge roles: deduplicate by roleType, attach claudeMd
111
- const roles = deduplicateRoles(sessionRoles, roleClaudes);
112
-
113
- // 5. Kanban
114
- const kanban = readFileOrNull(join(crewDir, 'context', 'kanban.md')) || '';
115
-
116
- // 6. Feature files from context/features/*.md
117
- const features = [];
118
- const featuresDir = join(crewDir, 'context', 'features');
119
- if (existsSync(featuresDir)) {
120
- try {
121
- const files = readdirSync(featuresDir)
122
- .filter(f => f.endsWith('.md') && f !== 'index.md')
123
- .sort();
124
- for (const f of files) {
125
- const content = readFileOrNull(join(featuresDir, f));
126
- if (content) {
127
- features.push({ name: f.replace('.md', ''), content });
128
- }
129
- }
130
- } catch {
131
- // ignore
132
- }
133
- }
134
-
135
- return { sharedClaudeMd, roles, kanban, features, teamType, language, sessionFeatures };
136
- }
137
-
138
- /**
139
- * Handle check_crew_context message — check if a project has .crew/ setup
140
- * and return role metadata for the frontend.
141
- */
142
- export function handleCheckCrewContext(msg) {
143
- const { projectDir, requestId } = msg;
144
- if (!projectDir) {
145
- ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
146
- return;
147
- }
148
- const crewContext = loadCrewContext(projectDir);
149
- if (!crewContext) {
150
- ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
151
- return;
152
- }
153
- // Return a safe subset for the frontend (no full claudeMd content, just metadata)
154
- ctx.sendToServer({
155
- type: 'crew_context_result',
156
- requestId,
157
- found: true,
158
- roles: crewContext.roles.map(r => ({
159
- name: r.name,
160
- displayName: r.displayName,
161
- icon: r.icon,
162
- description: r.description,
163
- roleType: r.roleType,
164
- isDecisionMaker: r.isDecisionMaker,
165
- hasClaudeMd: !!(r.claudeMd && r.claudeMd.length > 0),
166
- })),
167
- teamType: crewContext.teamType,
168
- language: crewContext.language,
169
- featureCount: crewContext.features.length,
170
- });
171
- }