@yeaft/webchat-agent 0.1.79 → 0.1.81
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/conversation.js +80 -5
- package/crew/routing.js +2 -2
- package/crew/task-files.js +3 -3
- package/crew-i18n.js +16 -16
- package/package.json +1 -1
- package/roleplay-dir.js +301 -0
- package/roleplay-i18n.js +445 -0
- package/roleplay-session.js +184 -0
package/conversation.js
CHANGED
|
@@ -3,6 +3,8 @@ import { loadSessionHistory } from './history.js';
|
|
|
3
3
|
import { startClaudeQuery } from './claude.js';
|
|
4
4
|
import { crewSessions, loadCrewIndex } from './crew.js';
|
|
5
5
|
import { rolePlaySessions, saveRolePlayIndex, removeRolePlaySession, loadRolePlayIndex, validateRolePlayConfig, initRolePlayRouteState, loadCrewContext, refreshCrewContext, initCrewContextMtimes } from './roleplay.js';
|
|
6
|
+
import { initRolePlayDir, writeSessionClaudeMd, generateSessionName, getDefaultRoles, getSessionDir } from './roleplay-dir.js';
|
|
7
|
+
import { addRolePlaySession, findRolePlaySessionByConversationId, setActiveRolePlaySession } from './roleplay-session.js';
|
|
6
8
|
|
|
7
9
|
// Restore persisted roleplay sessions on module load (agent startup)
|
|
8
10
|
loadRolePlayIndex();
|
|
@@ -149,16 +151,71 @@ export async function createConversation(msg) {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
// ★ RolePlay: initialize .roleplay/ directory, generate session, set cwd
|
|
155
|
+
let rpSessionName = null;
|
|
156
|
+
let rpSessionWorkDir = effectiveWorkDir; // default: project root
|
|
157
|
+
if (rolePlayConfig) {
|
|
158
|
+
try {
|
|
159
|
+
const language = rolePlayConfig.language || 'zh-CN';
|
|
160
|
+
|
|
161
|
+
// 1. Ensure .roleplay/ directory structure exists
|
|
162
|
+
await initRolePlayDir(effectiveWorkDir, language);
|
|
163
|
+
|
|
164
|
+
// 2. Generate unique session name
|
|
165
|
+
rpSessionName = generateSessionName(
|
|
166
|
+
effectiveWorkDir,
|
|
167
|
+
rolePlayConfig.teamType,
|
|
168
|
+
msg.rolePlayConfig?.sessionName || null
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// 3. Write session CLAUDE.md (role list, ROUTE protocol, workflow)
|
|
172
|
+
await writeSessionClaudeMd(effectiveWorkDir, rpSessionName, {
|
|
173
|
+
teamType: rolePlayConfig.teamType,
|
|
174
|
+
language,
|
|
175
|
+
roles: rolePlayConfig.roles,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 4. Set cwd to session directory so Claude Code auto-reads its CLAUDE.md
|
|
179
|
+
rpSessionWorkDir = getSessionDir(effectiveWorkDir, rpSessionName);
|
|
180
|
+
|
|
181
|
+
// 5. Build roles snapshot for session.json
|
|
182
|
+
const rolesSnapshot = (rolePlayConfig.roles && rolePlayConfig.roles.length > 0)
|
|
183
|
+
? rolePlayConfig.roles.map(r => ({
|
|
184
|
+
name: r.name,
|
|
185
|
+
displayName: r.displayName || r.name,
|
|
186
|
+
icon: r.icon || '',
|
|
187
|
+
}))
|
|
188
|
+
: getDefaultRoles(rolePlayConfig.teamType, language);
|
|
189
|
+
|
|
190
|
+
// 6. Persist to .roleplay/session.json
|
|
191
|
+
await addRolePlaySession(effectiveWorkDir, {
|
|
192
|
+
name: rpSessionName,
|
|
193
|
+
teamType: rolePlayConfig.teamType,
|
|
194
|
+
language,
|
|
195
|
+
projectDir: effectiveWorkDir,
|
|
196
|
+
conversationId,
|
|
197
|
+
roles: rolesSnapshot,
|
|
198
|
+
createdAt: Date.now(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log(` RolePlay: initialized .roleplay/${rpSessionName}, cwd=${rpSessionWorkDir}`);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error('[createConversation] Failed to init .roleplay/ dir:', e);
|
|
204
|
+
// Non-fatal: fall back to project root as cwd
|
|
205
|
+
rpSessionWorkDir = effectiveWorkDir;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`Creating conversation: ${conversationId} in ${rpSessionWorkDir} (lazy start)`);
|
|
153
210
|
if (username) console.log(` User: ${username} (${userId})`);
|
|
154
|
-
if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}`);
|
|
211
|
+
if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}, session=${rpSessionName}`);
|
|
155
212
|
|
|
156
213
|
// 只创建 conversation 状态,不启动 Claude 进程
|
|
157
214
|
// Claude 进程会在用户发送第一条消息时启动 (见 handleUserInput)
|
|
158
215
|
ctx.conversations.set(conversationId, {
|
|
159
216
|
query: null,
|
|
160
217
|
inputStream: null,
|
|
161
|
-
workDir:
|
|
218
|
+
workDir: rpSessionWorkDir,
|
|
162
219
|
claudeSessionId: null,
|
|
163
220
|
createdAt: Date.now(),
|
|
164
221
|
abortController: null,
|
|
@@ -169,6 +226,9 @@ export async function createConversation(msg) {
|
|
|
169
226
|
username,
|
|
170
227
|
disallowedTools: disallowedTools || null, // null = 使用全局默认
|
|
171
228
|
rolePlayConfig: rolePlayConfig || null,
|
|
229
|
+
// Track the original project dir and session name for .roleplay/ operations
|
|
230
|
+
_rpProjectDir: rolePlayConfig ? effectiveWorkDir : null,
|
|
231
|
+
_rpSessionName: rpSessionName,
|
|
172
232
|
usage: {
|
|
173
233
|
inputTokens: 0,
|
|
174
234
|
outputTokens: 0,
|
|
@@ -198,7 +258,7 @@ export async function createConversation(msg) {
|
|
|
198
258
|
ctx.sendToServer({
|
|
199
259
|
type: 'conversation_created',
|
|
200
260
|
conversationId,
|
|
201
|
-
workDir:
|
|
261
|
+
workDir: rpSessionWorkDir,
|
|
202
262
|
userId,
|
|
203
263
|
username,
|
|
204
264
|
disallowedTools: disallowedTools || null,
|
|
@@ -260,6 +320,21 @@ export async function resumeConversation(msg) {
|
|
|
260
320
|
? { roles: rolePlayEntry.roles, teamType: rolePlayEntry.teamType, language: rolePlayEntry.language }
|
|
261
321
|
: null;
|
|
262
322
|
|
|
323
|
+
// ★ RolePlay resume: look up session in .roleplay/session.json to restore cwd
|
|
324
|
+
let rpResumeWorkDir = effectiveWorkDir;
|
|
325
|
+
if (rolePlayConfig && rolePlayEntry) {
|
|
326
|
+
const rpProjectDir = rolePlayEntry.projectDir || effectiveWorkDir;
|
|
327
|
+
const rpDiskSession = findRolePlaySessionByConversationId(rpProjectDir, conversationId);
|
|
328
|
+
if (rpDiskSession && rpDiskSession.name) {
|
|
329
|
+
rpResumeWorkDir = getSessionDir(rpProjectDir, rpDiskSession.name);
|
|
330
|
+
// Re-activate the session
|
|
331
|
+
setActiveRolePlaySession(rpProjectDir, rpDiskSession.name).catch(e => {
|
|
332
|
+
console.warn('[Resume] Failed to update activeSession:', e.message);
|
|
333
|
+
});
|
|
334
|
+
console.log(`[Resume] RolePlay: restored session cwd=${rpResumeWorkDir}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
263
338
|
// ★ RolePlay resume: refresh .crew context to get latest kanban/features
|
|
264
339
|
if (rolePlayConfig && rolePlayEntry) {
|
|
265
340
|
const crewContext = loadCrewContext(effectiveWorkDir);
|
|
@@ -274,7 +349,7 @@ export async function resumeConversation(msg) {
|
|
|
274
349
|
ctx.conversations.set(conversationId, {
|
|
275
350
|
query: null,
|
|
276
351
|
inputStream: null,
|
|
277
|
-
workDir:
|
|
352
|
+
workDir: rpResumeWorkDir,
|
|
278
353
|
claudeSessionId: claudeSessionId, // 保存要恢复的 session ID
|
|
279
354
|
createdAt: Date.now(),
|
|
280
355
|
abortController: null,
|
package/crew/routing.js
CHANGED
|
@@ -161,7 +161,7 @@ export async function dispatchToRole(session, roleName, content, fromSource, tas
|
|
|
161
161
|
if (effectiveTaskId && typeof content === 'string') {
|
|
162
162
|
const taskContent = await readTaskFile(session, effectiveTaskId);
|
|
163
163
|
if (taskContent) {
|
|
164
|
-
content = `${content}\n\n---\n<task-context file="context/features/${effectiveTaskId}.md">\n${taskContent}\n</task-context>`;
|
|
164
|
+
content = `${content}\n\n---\n<task-context file=".crew/context/features/${effectiveTaskId}.md">\n${taskContent}\n</task-context>`;
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -169,7 +169,7 @@ export async function dispatchToRole(session, roleName, content, fromSource, tas
|
|
|
169
169
|
if (typeof content === 'string') {
|
|
170
170
|
const kanbanContent = await readKanban(session);
|
|
171
171
|
if (kanbanContent) {
|
|
172
|
-
content = `${content}\n\n---\n<kanban file="context/kanban.md">\n${kanbanContent}\n</kanban>`;
|
|
172
|
+
content = `${content}\n\n---\n<kanban file=".crew/context/kanban.md">\n${kanbanContent}\n</kanban>`;
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
package/crew/task-files.js
CHANGED
|
@@ -106,7 +106,7 @@ export function parseCompletedTasks(text) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* 更新 feature 索引文件 context/features/index.md
|
|
109
|
+
* 更新 feature 索引文件 .crew/context/features/index.md
|
|
110
110
|
*/
|
|
111
111
|
export async function updateFeatureIndex(session) {
|
|
112
112
|
const featuresDir = join(session.sharedDir, 'context', 'features');
|
|
@@ -148,7 +148,7 @@ export async function updateFeatureIndex(session) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* 追加完成汇总到 context/changelog.md
|
|
151
|
+
* 追加完成汇总到 .crew/context/changelog.md
|
|
152
152
|
*/
|
|
153
153
|
export async function appendChangelog(session, taskId, taskTitle) {
|
|
154
154
|
const contextDir = join(session.sharedDir, 'context');
|
|
@@ -218,7 +218,7 @@ export async function saveRoleWorkSummary(session, roleName, accumulatedText) {
|
|
|
218
218
|
let _kanbanWriteLock = Promise.resolve();
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
|
-
* 更新工作看板 context/kanban.md
|
|
221
|
+
* 更新工作看板 .crew/context/kanban.md
|
|
222
222
|
*
|
|
223
223
|
* @param {object} session
|
|
224
224
|
* @param {object} [opts]
|
package/crew-i18n.js
CHANGED
|
@@ -87,15 +87,15 @@ ${isDevTeam ? '3' : '2'}. **任务完成** - 所有任务已完成,给出完
|
|
|
87
87
|
---END_TASKS---`,
|
|
88
88
|
|
|
89
89
|
featureRecordTitle: '# Feature 工作记录',
|
|
90
|
-
featureRecordContent: `系统会自动管理
|
|
90
|
+
featureRecordContent: `系统会自动管理 \`.crew/context/features/{task-id}.md\` 工作记录文件:
|
|
91
91
|
- PM 分配任务时自动创建文件(包含 task-id、标题、需求描述)
|
|
92
92
|
- 每次 ROUTE 传递时自动追加工作记录(角色名、时间、summary)
|
|
93
93
|
- 你收到的消息中会包含 <task-context> 标签,里面是该任务的完整工作记录
|
|
94
94
|
|
|
95
95
|
系统还维护以下文件(自动更新,无需手动管理):
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
96
|
+
- \`.crew/context/features/index.md\`:所有 feature 的索引(进行中/已完成分类),快速查看项目状态
|
|
97
|
+
- \`.crew/context/changelog.md\`:已完成任务的变更记录,每个任务完成时自动追加摘要
|
|
98
|
+
- \`.crew/context/kanban.md\`:工作看板,记录每个 feature 的负责人、当前状态和最新进展
|
|
99
99
|
你收到的消息中还会包含 <kanban> 标签,里面是工作看板的实时快照。
|
|
100
100
|
你不需要手动创建或更新这些文件,专注于你的本职工作即可。`,
|
|
101
101
|
|
|
@@ -172,8 +172,8 @@ UI/交互方案不确定时找设计师确认。需求不明确时找决策者 "
|
|
|
172
172
|
teamMembersTitle: '# 团队成员',
|
|
173
173
|
noMembers: '_暂无成员_',
|
|
174
174
|
workConventions: '# 工作约定',
|
|
175
|
-
workConventionsContent: `- 文档产出写入 context/ 目录
|
|
176
|
-
- 重要决策记录在 context/decisions.md
|
|
175
|
+
workConventionsContent: `- 文档产出写入 .crew/context/ 目录
|
|
176
|
+
- 重要决策记录在 .crew/context/decisions.md
|
|
177
177
|
- 代码修改使用项目代码路径的绝对路径`,
|
|
178
178
|
stuckRules: '# 卡住上报规则',
|
|
179
179
|
stuckRulesContent: `当你遇到以下情况时,不要自己空转或反复重试,立即 ROUTE 给 PM(pm)请求协调:
|
|
@@ -192,11 +192,11 @@ UI/交互方案不确定时找设计师确认。需求不明确时找决策者 "
|
|
|
192
192
|
- PM 不做 cherry-pick,只负责打 tag
|
|
193
193
|
- 每次新任务/新 feature 必须基于最新的 main 分支创建新的 worktree,确保在最新代码上开发`,
|
|
194
194
|
featureRecordShared: `# Feature 工作记录
|
|
195
|
-
系统自动管理
|
|
195
|
+
系统自动管理 \`.crew/context/features/{task-id}.md\` 工作记录文件:
|
|
196
196
|
- PM 通过 ROUTE 分配任务(带 task + taskTitle 字段)时自动创建
|
|
197
197
|
- 每次角色 ROUTE 传递时自动追加工作记录
|
|
198
198
|
- 角色收到消息时自动注入对应 task 文件内容作为上下文
|
|
199
|
-
-
|
|
199
|
+
- \`.crew/context/kanban.md\`:工作看板,记录所有任务的负责人、状态和最新进展
|
|
200
200
|
角色不需要手动创建或更新这些文件。`,
|
|
201
201
|
sharedMemoryTitle: '# 共享记忆',
|
|
202
202
|
sharedMemoryDefault: '_团队共同维护,记录重要的共识、决策和信息。_',
|
|
@@ -329,15 +329,15 @@ Important: Do not loop endlessly between roles. When work is substantively compl
|
|
|
329
329
|
---END_TASKS---`,
|
|
330
330
|
|
|
331
331
|
featureRecordTitle: '# Feature Work Records',
|
|
332
|
-
featureRecordContent: `The system automatically manages
|
|
332
|
+
featureRecordContent: `The system automatically manages \`.crew/context/features/{task-id}.md\` work record files:
|
|
333
333
|
- Automatically created when PM assigns tasks (includes task-id, title, requirement description)
|
|
334
334
|
- Work records are appended automatically on each ROUTE handoff (role name, time, summary)
|
|
335
335
|
- Your received messages will include <task-context> tags containing the complete work record for that task
|
|
336
336
|
|
|
337
337
|
The system also maintains these files (auto-updated, no manual management needed):
|
|
338
|
-
-
|
|
339
|
-
-
|
|
340
|
-
-
|
|
338
|
+
- \`.crew/context/features/index.md\`: Index of all features (categorized as in-progress/completed) for quick project status overview
|
|
339
|
+
- \`.crew/context/changelog.md\`: Change log of completed tasks, with summary appended when each task completes
|
|
340
|
+
- \`.crew/context/kanban.md\`: Work kanban board recording each feature's assignee, current status, and latest progress
|
|
341
341
|
Your received messages will also include <kanban> tags with a real-time snapshot of the work kanban.
|
|
342
342
|
You don't need to manually create or update these files — focus on your core work.`,
|
|
343
343
|
|
|
@@ -414,8 +414,8 @@ After development is complete, send two ROUTE blocks simultaneously to ${revName
|
|
|
414
414
|
teamMembersTitle: '# Team Members',
|
|
415
415
|
noMembers: '_No members yet_',
|
|
416
416
|
workConventions: '# Work Conventions',
|
|
417
|
-
workConventionsContent: `- Write documentation output to context/ directory
|
|
418
|
-
- Record important decisions in context/decisions.md
|
|
417
|
+
workConventionsContent: `- Write documentation output to .crew/context/ directory
|
|
418
|
+
- Record important decisions in .crew/context/decisions.md
|
|
419
419
|
- Use the project code path (absolute path) for code changes`,
|
|
420
420
|
stuckRules: '# Escalation Rules',
|
|
421
421
|
stuckRulesContent: `When you encounter the following situations, do not spin or retry repeatedly — immediately ROUTE to PM (pm) for coordination:
|
|
@@ -434,11 +434,11 @@ When escalating, explain: what task you're working on, where you're stuck, and w
|
|
|
434
434
|
- PM doesn't cherry-pick, only manages tags
|
|
435
435
|
- Each new task/feature must create a new worktree based on the latest main branch to ensure development on latest code`,
|
|
436
436
|
featureRecordShared: `# Feature Work Records
|
|
437
|
-
The system automatically manages
|
|
437
|
+
The system automatically manages \`.crew/context/features/{task-id}.md\` work record files:
|
|
438
438
|
- Automatically created when PM assigns tasks via ROUTE (with task + taskTitle fields)
|
|
439
439
|
- Work records are appended on each role ROUTE handoff
|
|
440
440
|
- Task file content is auto-injected as context when a role receives a message
|
|
441
|
-
-
|
|
441
|
+
- \`.crew/context/kanban.md\`: Work kanban board recording all tasks' assignees, statuses, and latest progress
|
|
442
442
|
Roles don't need to manually create or update these files.`,
|
|
443
443
|
sharedMemoryTitle: '# Shared Memory',
|
|
444
444
|
sharedMemoryDefault: '_Team-maintained shared knowledge, decisions, and information._',
|
package/package.json
CHANGED
package/roleplay-dir.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolePlay — .roleplay/ directory and CLAUDE.md management.
|
|
3
|
+
*
|
|
4
|
+
* Analogous to agent/crew/shared-dir.js but for the single-process
|
|
5
|
+
* RolePlay collaboration mode.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* .roleplay/
|
|
9
|
+
* ├── CLAUDE.md (shared instructions, inherited by all sessions)
|
|
10
|
+
* ├── session.json (session index)
|
|
11
|
+
* ├── roles/ (one subdirectory per session)
|
|
12
|
+
* │ └── {session-name}/
|
|
13
|
+
* │ └── CLAUDE.md (Claude Code cwd points here)
|
|
14
|
+
* └── context/
|
|
15
|
+
* ├── kanban.md
|
|
16
|
+
* └── features/
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from 'fs';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { existsSync, readdirSync } from 'fs';
|
|
21
|
+
import { getRolePlayMessages } from './roleplay-i18n.js';
|
|
22
|
+
|
|
23
|
+
// ─── Team type → default role set mapping ───────────────────────────
|
|
24
|
+
// Each entry is a list of role names (keys into roleTemplates in i18n).
|
|
25
|
+
const TEAM_ROLES = {
|
|
26
|
+
dev: ['pm', 'dev', 'reviewer', 'tester'],
|
|
27
|
+
writing: ['planner', 'writer', 'editor'],
|
|
28
|
+
trading: ['strategist', 'analyst', 'macro', 'risk', 'trader'],
|
|
29
|
+
video: ['director', 'scriptwriter', 'storyboard'],
|
|
30
|
+
custom: ['pm', 'dev', 'reviewer', 'tester'], // default same as dev
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ─── Session name constraints ───────────────────────────────────────
|
|
34
|
+
const SESSION_NAME_RE = /^[a-z0-9-]+$/;
|
|
35
|
+
const MAX_SESSION_NAME_LEN = 64;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate a session directory name.
|
|
39
|
+
*
|
|
40
|
+
* Format: `{teamType}-{customName|"team"}-{YYYYMMDD}[-{seq}]`
|
|
41
|
+
*
|
|
42
|
+
* If a session with the same name already exists under .roleplay/roles/,
|
|
43
|
+
* a sequence number is appended (e.g. `-2`, `-3`).
|
|
44
|
+
*
|
|
45
|
+
* @param {string} projectDir - absolute path to project root
|
|
46
|
+
* @param {string} teamType - dev | writing | trading | video | custom
|
|
47
|
+
* @param {string} [customName] - user-provided name (optional)
|
|
48
|
+
* @returns {string} unique session name
|
|
49
|
+
*/
|
|
50
|
+
export function generateSessionName(projectDir, teamType, customName) {
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const datePart = [
|
|
53
|
+
now.getFullYear(),
|
|
54
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
55
|
+
String(now.getDate()).padStart(2, '0'),
|
|
56
|
+
].join('');
|
|
57
|
+
|
|
58
|
+
const namePart = sanitizeNamePart(customName) || 'team';
|
|
59
|
+
const base = `${teamType}-${namePart}-${datePart}`;
|
|
60
|
+
|
|
61
|
+
// Check for duplicates under .roleplay/roles/
|
|
62
|
+
const rolesDir = join(projectDir, '.roleplay', 'roles');
|
|
63
|
+
const existing = new Set();
|
|
64
|
+
if (existsSync(rolesDir)) {
|
|
65
|
+
try {
|
|
66
|
+
for (const d of readdirSync(rolesDir, { withFileTypes: true })) {
|
|
67
|
+
if (d.isDirectory()) existing.add(d.name);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// permission error — treat as empty
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!existing.has(base)) return base;
|
|
75
|
+
|
|
76
|
+
// Append sequence number
|
|
77
|
+
for (let seq = 2; seq <= 999; seq++) {
|
|
78
|
+
const candidate = `${base}-${seq}`;
|
|
79
|
+
if (!existing.has(candidate)) return candidate;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Extremely unlikely: fall back to timestamp suffix
|
|
83
|
+
return `${base}-${Date.now()}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sanitize user-provided name part:
|
|
88
|
+
* - lowercase
|
|
89
|
+
* - replace non-alphanumeric with hyphens
|
|
90
|
+
* - collapse consecutive hyphens
|
|
91
|
+
* - trim leading/trailing hyphens
|
|
92
|
+
* - enforce max length
|
|
93
|
+
*
|
|
94
|
+
* Returns empty string if input is empty/invalid.
|
|
95
|
+
*/
|
|
96
|
+
function sanitizeNamePart(raw) {
|
|
97
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
98
|
+
let s = raw.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
99
|
+
if (s.length > 30) s = s.substring(0, 30).replace(/-$/, '');
|
|
100
|
+
return SESSION_NAME_RE.test(s) ? s : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Directory initialization ───────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Ensure .roleplay/ directory structure exists.
|
|
107
|
+
* Creates the top-level dirs if absent, writes shared CLAUDE.md
|
|
108
|
+
* only on first creation (preserves user edits after that).
|
|
109
|
+
*
|
|
110
|
+
* @param {string} projectDir - absolute path to project root
|
|
111
|
+
* @param {string} [language='zh-CN']
|
|
112
|
+
*/
|
|
113
|
+
export async function initRolePlayDir(projectDir, language = 'zh-CN') {
|
|
114
|
+
const rpDir = join(projectDir, '.roleplay');
|
|
115
|
+
|
|
116
|
+
await fs.mkdir(rpDir, { recursive: true });
|
|
117
|
+
await fs.mkdir(join(rpDir, 'roles'), { recursive: true });
|
|
118
|
+
await fs.mkdir(join(rpDir, 'context'), { recursive: true });
|
|
119
|
+
await fs.mkdir(join(rpDir, 'context', 'features'), { recursive: true });
|
|
120
|
+
|
|
121
|
+
// Write shared CLAUDE.md only if it doesn't exist
|
|
122
|
+
const sharedMdPath = join(rpDir, 'CLAUDE.md');
|
|
123
|
+
try {
|
|
124
|
+
await fs.access(sharedMdPath);
|
|
125
|
+
// Already exists — don't overwrite (user may have edited it)
|
|
126
|
+
} catch {
|
|
127
|
+
await writeRolePlaySharedClaudeMd(projectDir, language);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write (or overwrite) .roleplay/CLAUDE.md — the shared-level instructions
|
|
133
|
+
* inherited by all sessions via Claude Code's CLAUDE.md lookup chain.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} projectDir
|
|
136
|
+
* @param {string} [language='zh-CN']
|
|
137
|
+
*/
|
|
138
|
+
export async function writeRolePlaySharedClaudeMd(projectDir, language = 'zh-CN') {
|
|
139
|
+
const m = getRolePlayMessages(language);
|
|
140
|
+
const rpDir = join(projectDir, '.roleplay');
|
|
141
|
+
|
|
142
|
+
const content = `${m.sharedTitle}
|
|
143
|
+
|
|
144
|
+
${m.projectPath}
|
|
145
|
+
${projectDir}
|
|
146
|
+
${m.useAbsolutePath}
|
|
147
|
+
|
|
148
|
+
${m.workMode}
|
|
149
|
+
${m.workModeContent}
|
|
150
|
+
|
|
151
|
+
${m.workConventions}
|
|
152
|
+
${m.workConventionsContent}
|
|
153
|
+
|
|
154
|
+
${m.crewRelation}
|
|
155
|
+
${m.crewRelationContent}
|
|
156
|
+
|
|
157
|
+
${m.sharedMemory}
|
|
158
|
+
${m.sharedMemoryDefault}
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
await fs.writeFile(join(rpDir, 'CLAUDE.md'), content);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Session CLAUDE.md generation ───────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Write .roleplay/roles/{sessionName}/CLAUDE.md — session-level config.
|
|
168
|
+
*
|
|
169
|
+
* This file is the core configuration that Claude Code reads automatically
|
|
170
|
+
* (cwd is set to this directory). It contains:
|
|
171
|
+
* - Session metadata (name, teamType, language)
|
|
172
|
+
* - Full role list with descriptions (generated from teamType or custom roles)
|
|
173
|
+
* - ROUTE protocol reference
|
|
174
|
+
* - Workflow for this team type
|
|
175
|
+
* - Project path reference
|
|
176
|
+
* - Session memory section
|
|
177
|
+
*
|
|
178
|
+
* @param {string} projectDir - project root
|
|
179
|
+
* @param {string} sessionName - session directory name
|
|
180
|
+
* @param {object} config - { teamType, language, roles?, projectDir? }
|
|
181
|
+
* - config.roles: optional custom role array from RolePlay config.
|
|
182
|
+
* If provided and non-empty, these are used instead of default team roles.
|
|
183
|
+
* Each role: { name, displayName, icon?, description?, claudeMd? }
|
|
184
|
+
*/
|
|
185
|
+
export async function writeSessionClaudeMd(projectDir, sessionName, config) {
|
|
186
|
+
const { teamType = 'dev', language = 'zh-CN', roles: customRoles } = config;
|
|
187
|
+
const m = getRolePlayMessages(language);
|
|
188
|
+
|
|
189
|
+
const sessionDir = join(projectDir, '.roleplay', 'roles', sessionName);
|
|
190
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
191
|
+
|
|
192
|
+
// Build role list section
|
|
193
|
+
const roleSection = buildRoleSection(teamType, language, customRoles);
|
|
194
|
+
|
|
195
|
+
// Build workflow section
|
|
196
|
+
const workflow = (teamType === 'dev')
|
|
197
|
+
? m.devWorkflow
|
|
198
|
+
: m.genericWorkflow;
|
|
199
|
+
|
|
200
|
+
const content = `${m.sessionTitle(sessionName)}
|
|
201
|
+
|
|
202
|
+
${m.teamTypeLabel}
|
|
203
|
+
${teamType}
|
|
204
|
+
|
|
205
|
+
${m.languageLabel}
|
|
206
|
+
${language}
|
|
207
|
+
|
|
208
|
+
${m.roleListTitle}
|
|
209
|
+
|
|
210
|
+
${roleSection}
|
|
211
|
+
|
|
212
|
+
${m.routeProtocol}
|
|
213
|
+
|
|
214
|
+
${m.workflowTitle}
|
|
215
|
+
|
|
216
|
+
${workflow}
|
|
217
|
+
|
|
218
|
+
${m.projectPathTitle}
|
|
219
|
+
${projectDir}
|
|
220
|
+
${m.useAbsolutePath}
|
|
221
|
+
|
|
222
|
+
${m.sessionMemory}
|
|
223
|
+
${m.sessionMemoryDefault}
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
await fs.writeFile(join(sessionDir, 'CLAUDE.md'), content);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build the role list section for a session CLAUDE.md.
|
|
231
|
+
*
|
|
232
|
+
* Strategy:
|
|
233
|
+
* 1. If custom roles are provided (from RolePlay config), use them directly.
|
|
234
|
+
* For each custom role, look up a matching roleTemplate for extra detail,
|
|
235
|
+
* but prefer the custom role's claudeMd/description if provided.
|
|
236
|
+
* 2. Otherwise, use the default role set for the teamType from TEAM_ROLES.
|
|
237
|
+
*/
|
|
238
|
+
function buildRoleSection(teamType, language, customRoles) {
|
|
239
|
+
const m = getRolePlayMessages(language);
|
|
240
|
+
const templates = m.roleTemplates;
|
|
241
|
+
|
|
242
|
+
if (customRoles && customRoles.length > 0) {
|
|
243
|
+
return customRoles.map(r => {
|
|
244
|
+
const tmpl = templates[r.name];
|
|
245
|
+
// Prefer custom claudeMd, then custom description, then template
|
|
246
|
+
const body = r.claudeMd || r.description || (tmpl ? tmpl.content : '');
|
|
247
|
+
const heading = tmpl
|
|
248
|
+
? tmpl.heading
|
|
249
|
+
: `## ${r.icon || ''} ${r.displayName || r.name} (${r.name})`.replace(/\s{2,}/g, ' ');
|
|
250
|
+
return `${heading}\n${body}`;
|
|
251
|
+
}).join('\n\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Default: use TEAM_ROLES mapping
|
|
255
|
+
const roleNames = TEAM_ROLES[teamType] || TEAM_ROLES.dev;
|
|
256
|
+
return roleNames.map(name => {
|
|
257
|
+
const tmpl = templates[name];
|
|
258
|
+
if (!tmpl) return `## ${name}\n(No template available)`;
|
|
259
|
+
return `${tmpl.heading}\n${tmpl.content}`;
|
|
260
|
+
}).join('\n\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the default role list for a team type.
|
|
265
|
+
* Used by session creation to populate session.json roles snapshot.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} teamType
|
|
268
|
+
* @param {string} language
|
|
269
|
+
* @returns {Array<{name: string, displayName: string, icon: string}>}
|
|
270
|
+
*/
|
|
271
|
+
export function getDefaultRoles(teamType, language = 'zh-CN') {
|
|
272
|
+
const m = getRolePlayMessages(language);
|
|
273
|
+
const templates = m.roleTemplates;
|
|
274
|
+
const roleNames = TEAM_ROLES[teamType] || TEAM_ROLES.dev;
|
|
275
|
+
|
|
276
|
+
return roleNames.map(name => {
|
|
277
|
+
const tmpl = templates[name];
|
|
278
|
+
if (!tmpl) return { name, displayName: name, icon: '' };
|
|
279
|
+
|
|
280
|
+
// Extract icon and displayName from heading: "## 📋 PM-乔布斯 (pm)"
|
|
281
|
+
const headingMatch = tmpl.heading.match(/^##\s*(\S+)\s+(.+?)\s*\((\w+)\)\s*$/);
|
|
282
|
+
if (headingMatch) {
|
|
283
|
+
return {
|
|
284
|
+
name: headingMatch[3],
|
|
285
|
+
displayName: headingMatch[2],
|
|
286
|
+
icon: headingMatch[1],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { name, displayName: name, icon: '' };
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get the session directory path.
|
|
295
|
+
* @param {string} projectDir
|
|
296
|
+
* @param {string} sessionName
|
|
297
|
+
* @returns {string} absolute path to .roleplay/roles/{sessionName}/
|
|
298
|
+
*/
|
|
299
|
+
export function getSessionDir(projectDir, sessionName) {
|
|
300
|
+
return join(projectDir, '.roleplay', 'roles', sessionName);
|
|
301
|
+
}
|
package/roleplay-i18n.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolePlay i18n — localized template strings for .roleplay/ directory files.
|
|
3
|
+
*
|
|
4
|
+
* Separated from crew-i18n.js because RolePlay CLAUDE.md templates have
|
|
5
|
+
* different structure (single-process, no worktree rules, session-scoped).
|
|
6
|
+
*
|
|
7
|
+
* Supported languages: 'zh-CN' (default), 'en'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
'zh-CN': {
|
|
12
|
+
// ── .roleplay/CLAUDE.md (shared level) ──
|
|
13
|
+
sharedTitle: '# RolePlay 共享指令',
|
|
14
|
+
projectPath: '# 项目路径',
|
|
15
|
+
useAbsolutePath: '所有代码操作请使用此绝对路径。',
|
|
16
|
+
workMode: '# 工作模式',
|
|
17
|
+
workModeContent: `RolePlay 是单进程多角色协作模式。一个 Claude 实例依次扮演不同角色,通过 ROUTE 协议切换。
|
|
18
|
+
与 Crew(多进程、每角色独立 Claude 实例)的区别:
|
|
19
|
+
- 所有角色共享同一个上下文窗口
|
|
20
|
+
- 角色切换是即时的(无需跨进程通信)
|
|
21
|
+
- 适合轻量级协作和快速迭代`,
|
|
22
|
+
workConventions: '# 工作约定',
|
|
23
|
+
workConventionsContent: `- 文档产出写入 .roleplay/context/ 目录
|
|
24
|
+
- 代码修改使用项目路径的绝对路径
|
|
25
|
+
- 每个角色专注自己的职责,不越界`,
|
|
26
|
+
crewRelation: '# 与 .crew 的关系',
|
|
27
|
+
crewRelationContent: `- .roleplay/context/ 可以读取 .crew/context/ 的内容
|
|
28
|
+
- .crew 的共享记忆和看板对 RolePlay 可见
|
|
29
|
+
- RolePlay 不修改 .crew/ 的任何内容(只读)`,
|
|
30
|
+
sharedMemory: '# 共享记忆',
|
|
31
|
+
sharedMemoryDefault: '_所有 session 共同维护,记录项目级的共识和决策。_',
|
|
32
|
+
|
|
33
|
+
// ── .roleplay/roles/{session}/CLAUDE.md (session level) ──
|
|
34
|
+
sessionTitle: (name) => `# RolePlay Session: ${name}`,
|
|
35
|
+
teamTypeLabel: '# 团队类型',
|
|
36
|
+
languageLabel: '# 语言',
|
|
37
|
+
roleListTitle: '# 角色列表',
|
|
38
|
+
|
|
39
|
+
// Role templates per teamType
|
|
40
|
+
roleTemplates: {
|
|
41
|
+
pm: {
|
|
42
|
+
heading: '## 📋 PM-乔布斯 (pm)',
|
|
43
|
+
content: `你是 PM-乔布斯。你的职责:
|
|
44
|
+
- 分析用户需求,理解意图
|
|
45
|
+
- 将需求拆分为可执行的开发任务
|
|
46
|
+
- 定义验收标准
|
|
47
|
+
- 最终验收开发成果
|
|
48
|
+
|
|
49
|
+
风格:简洁、注重用户价值、善于抓住本质。`,
|
|
50
|
+
},
|
|
51
|
+
dev: {
|
|
52
|
+
heading: '## 💻 开发者-托瓦兹 (dev)',
|
|
53
|
+
content: `你是开发者-托瓦兹。你的职责:
|
|
54
|
+
- 设计技术方案和架构
|
|
55
|
+
- 使用工具(Read, Edit, Write, Bash)实现代码
|
|
56
|
+
- 确保代码质量和可维护性
|
|
57
|
+
- 修复 reviewer 和 tester 提出的问题
|
|
58
|
+
|
|
59
|
+
风格:追求代码简洁优雅,重视性能和可维护性。不写废话,直接动手。`,
|
|
60
|
+
},
|
|
61
|
+
reviewer: {
|
|
62
|
+
heading: '## 🔍 审查者-马丁 (reviewer)',
|
|
63
|
+
content: `你是审查者-马丁。你的职责:
|
|
64
|
+
- 仔细审查开发者的代码变更
|
|
65
|
+
- 检查:代码风格、命名规范、架构合理性、边界情况、安全漏洞
|
|
66
|
+
- 如果有问题,明确指出并说明修改建议
|
|
67
|
+
- 确认通过后明确说"LGTM"
|
|
68
|
+
|
|
69
|
+
风格:严格但友善,注重最佳实践,善于发现潜在问题。`,
|
|
70
|
+
},
|
|
71
|
+
tester: {
|
|
72
|
+
heading: '## 🧪 测试者-贝克 (tester)',
|
|
73
|
+
content: `你是测试者-贝克。你的职责:
|
|
74
|
+
- 使用 Bash 工具运行测试
|
|
75
|
+
- 验证功能是否按预期工作
|
|
76
|
+
- 检查边界情况和异常处理
|
|
77
|
+
- 如果有 bug,明确描述复现步骤
|
|
78
|
+
|
|
79
|
+
风格:测试驱动思维,善于发现边界情况,追求可靠性。`,
|
|
80
|
+
},
|
|
81
|
+
designer: {
|
|
82
|
+
heading: '## 🎨 设计师-拉姆斯 (designer)',
|
|
83
|
+
content: `你是设计师-拉姆斯。你的职责:
|
|
84
|
+
- 设计用户交互流程和页面布局
|
|
85
|
+
- 确保视觉一致性和用户体验
|
|
86
|
+
- 输出设计方案给开发者实现
|
|
87
|
+
|
|
88
|
+
风格:Less but better,追求极致简洁。`,
|
|
89
|
+
},
|
|
90
|
+
// Writing team
|
|
91
|
+
planner: {
|
|
92
|
+
heading: '## 📋 策划-曹雪芹 (planner)',
|
|
93
|
+
content: `你是策划-曹雪芹。你的职责:
|
|
94
|
+
- 制定整体创作方向和大纲
|
|
95
|
+
- 审核内容质量和一致性
|
|
96
|
+
- 做出关键创作决策
|
|
97
|
+
|
|
98
|
+
风格:注重故事内核,善于把控整体节奏。`,
|
|
99
|
+
},
|
|
100
|
+
writer: {
|
|
101
|
+
heading: '## ✍️ 执笔师-鲁迅 (writer)',
|
|
102
|
+
content: `你是执笔师-鲁迅。你的职责:
|
|
103
|
+
- 根据大纲撰写具体内容
|
|
104
|
+
- 把控文字质量和风格一致性
|
|
105
|
+
- 按照设计师的节奏方案写作
|
|
106
|
+
|
|
107
|
+
风格:文字犀利,善于用细节打动人。`,
|
|
108
|
+
},
|
|
109
|
+
editor: {
|
|
110
|
+
heading: '## 📝 审稿师 (editor)',
|
|
111
|
+
content: `你是审稿师。你的职责:
|
|
112
|
+
- 审核文字质量、逻辑一致性
|
|
113
|
+
- 检查错别字、语法、标点
|
|
114
|
+
- 确认内容符合整体方向
|
|
115
|
+
|
|
116
|
+
风格:严谨细致,注重可读性。`,
|
|
117
|
+
},
|
|
118
|
+
// Trading team
|
|
119
|
+
strategist: {
|
|
120
|
+
heading: '## 📋 策略师 (strategist)',
|
|
121
|
+
content: `你是策略师。你的职责:
|
|
122
|
+
- 综合技术分析和宏观研究做出交易决策
|
|
123
|
+
- 管理整体仓位和风险敞口
|
|
124
|
+
- 协调团队分析方向
|
|
125
|
+
|
|
126
|
+
风格:冷静理性,注重概率思维。`,
|
|
127
|
+
},
|
|
128
|
+
analyst: {
|
|
129
|
+
heading: '## 📊 技术分析师 (analyst)',
|
|
130
|
+
content: `你是技术分析师。你的职责:
|
|
131
|
+
- 分析价格走势和技术指标
|
|
132
|
+
- 识别关键支撑/阻力位
|
|
133
|
+
- 提供入场/出场信号
|
|
134
|
+
|
|
135
|
+
风格:数据驱动,图表说话。`,
|
|
136
|
+
},
|
|
137
|
+
macro: {
|
|
138
|
+
heading: '## 🌐 宏观研究员 (macro)',
|
|
139
|
+
content: `你是宏观研究员。你的职责:
|
|
140
|
+
- 分析宏观经济数据和政策
|
|
141
|
+
- 评估市场情绪和资金流向
|
|
142
|
+
- 提供宏观背景判断
|
|
143
|
+
|
|
144
|
+
风格:视野开阔,善于关联不同市场。`,
|
|
145
|
+
},
|
|
146
|
+
risk: {
|
|
147
|
+
heading: '## 🛡️ 风控 (risk)',
|
|
148
|
+
content: `你是风控。你的职责:
|
|
149
|
+
- 审查交易方案的风险
|
|
150
|
+
- 设定止损和仓位限制
|
|
151
|
+
- 监控已有持仓风险
|
|
152
|
+
|
|
153
|
+
风格:保守谨慎,底线思维。`,
|
|
154
|
+
},
|
|
155
|
+
trader: {
|
|
156
|
+
heading: '## 💰 交易员 (trader)',
|
|
157
|
+
content: `你是交易员。你的职责:
|
|
158
|
+
- 执行策略师的交易决策
|
|
159
|
+
- 选择最优执行时机和方式
|
|
160
|
+
- 报告执行结果
|
|
161
|
+
|
|
162
|
+
风格:执行力强,反应迅速。`,
|
|
163
|
+
},
|
|
164
|
+
// Video team
|
|
165
|
+
director: {
|
|
166
|
+
heading: '## 🎬 导演 (director)',
|
|
167
|
+
content: `你是导演。你的职责:
|
|
168
|
+
- 把控整体视觉叙事方向
|
|
169
|
+
- 审核脚本和分镜
|
|
170
|
+
- 做出最终创意决策
|
|
171
|
+
|
|
172
|
+
风格:注重视觉叙事,善于把控节奏。`,
|
|
173
|
+
},
|
|
174
|
+
scriptwriter: {
|
|
175
|
+
heading: '## ✏️ 编剧 (scriptwriter)',
|
|
176
|
+
content: `你是编剧。你的职责:
|
|
177
|
+
- 撰写视频脚本和旁白
|
|
178
|
+
- 构建叙事结构
|
|
179
|
+
- 设计情节转折
|
|
180
|
+
|
|
181
|
+
风格:善于讲故事,注重情感共鸣。`,
|
|
182
|
+
},
|
|
183
|
+
storyboard: {
|
|
184
|
+
heading: '## 🖼️ 分镜师 (storyboard)',
|
|
185
|
+
content: `你是分镜师。你的职责:
|
|
186
|
+
- 将脚本转化为视觉分镜
|
|
187
|
+
- 设计镜头语言和转场
|
|
188
|
+
- 确保视觉连贯性
|
|
189
|
+
|
|
190
|
+
风格:视觉思维,善于用画面讲故事。`,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
routeProtocol: `# ROUTE 协议
|
|
195
|
+
|
|
196
|
+
当一个角色完成工作需要交给另一个角色时,使用 ROUTE 块:
|
|
197
|
+
|
|
198
|
+
---ROUTE---
|
|
199
|
+
to: {目标角色name}
|
|
200
|
+
summary: {交接内容摘要}
|
|
201
|
+
task: {任务ID}(可选)
|
|
202
|
+
taskTitle: {任务标题}(可选)
|
|
203
|
+
---END_ROUTE---
|
|
204
|
+
|
|
205
|
+
规则:
|
|
206
|
+
- \`to\` 必须是有效的角色 name,或 \`human\` 表示需要用户输入
|
|
207
|
+
- 一次可以输出多个 ROUTE 块
|
|
208
|
+
- ROUTE 块必须在角色输出的末尾
|
|
209
|
+
- 切换后必须完全以该角色的视角和人格思考和行动`,
|
|
210
|
+
|
|
211
|
+
workflowTitle: '# 工作流程',
|
|
212
|
+
devWorkflow: `1. **PM** 分析需求,拆分任务,确定验收标准
|
|
213
|
+
2. **开发者** 实现代码(使用工具读写文件)
|
|
214
|
+
3. **审查者** Code Review(不通过 → 返回开发者修复)
|
|
215
|
+
4. **测试者** 运行测试 & 验证(有 bug → 返回开发者修复)
|
|
216
|
+
5. **PM** 验收总结`,
|
|
217
|
+
genericWorkflow: '按角色顺序依次完成任务。',
|
|
218
|
+
|
|
219
|
+
projectPathTitle: '# 项目路径',
|
|
220
|
+
|
|
221
|
+
sessionMemory: '# Session 记忆',
|
|
222
|
+
sessionMemoryDefault: '_本 session 的工作记录、决策和待办事项。_',
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'en': {
|
|
226
|
+
// ── .roleplay/CLAUDE.md (shared level) ──
|
|
227
|
+
sharedTitle: '# RolePlay Shared Instructions',
|
|
228
|
+
projectPath: '# Project Path',
|
|
229
|
+
useAbsolutePath: 'Use this absolute path for all code operations.',
|
|
230
|
+
workMode: '# Work Mode',
|
|
231
|
+
workModeContent: `RolePlay is a single-process multi-role collaboration mode. One Claude instance plays different roles in sequence, switching via the ROUTE protocol.
|
|
232
|
+
Differences from Crew (multi-process, each role has its own Claude instance):
|
|
233
|
+
- All roles share the same context window
|
|
234
|
+
- Role switching is instant (no cross-process communication)
|
|
235
|
+
- Suitable for lightweight collaboration and rapid iteration`,
|
|
236
|
+
workConventions: '# Work Conventions',
|
|
237
|
+
workConventionsContent: `- Write documentation output to .roleplay/context/ directory
|
|
238
|
+
- Use absolute project path for code changes
|
|
239
|
+
- Each role focuses on its own responsibilities`,
|
|
240
|
+
crewRelation: '# Relationship with .crew',
|
|
241
|
+
crewRelationContent: `- .roleplay/context/ can read .crew/context/ content
|
|
242
|
+
- .crew shared memory and kanban are visible to RolePlay
|
|
243
|
+
- RolePlay does not modify anything in .crew/ (read-only)`,
|
|
244
|
+
sharedMemory: '# Shared Memory',
|
|
245
|
+
sharedMemoryDefault: '_Maintained by all sessions, recording project-level consensus and decisions._',
|
|
246
|
+
|
|
247
|
+
// ── .roleplay/roles/{session}/CLAUDE.md (session level) ──
|
|
248
|
+
sessionTitle: (name) => `# RolePlay Session: ${name}`,
|
|
249
|
+
teamTypeLabel: '# Team Type',
|
|
250
|
+
languageLabel: '# Language',
|
|
251
|
+
roleListTitle: '# Role List',
|
|
252
|
+
|
|
253
|
+
roleTemplates: {
|
|
254
|
+
pm: {
|
|
255
|
+
heading: '## 📋 PM-Jobs (pm)',
|
|
256
|
+
content: `You are PM-Jobs. Your responsibilities:
|
|
257
|
+
- Analyze user requirements and understand intent
|
|
258
|
+
- Break down requirements into executable tasks
|
|
259
|
+
- Define acceptance criteria
|
|
260
|
+
- Final acceptance of deliverables
|
|
261
|
+
|
|
262
|
+
Style: Concise, user-value focused, excellent at grasping the essence.`,
|
|
263
|
+
},
|
|
264
|
+
dev: {
|
|
265
|
+
heading: '## 💻 Dev-Torvalds (dev)',
|
|
266
|
+
content: `You are Dev-Torvalds. Your responsibilities:
|
|
267
|
+
- Design technical solutions and architecture
|
|
268
|
+
- Use tools (Read, Edit, Write, Bash) to implement code
|
|
269
|
+
- Ensure code quality and maintainability
|
|
270
|
+
- Fix issues raised by reviewer and tester
|
|
271
|
+
|
|
272
|
+
Style: Pursue clean, elegant code. Value performance and maintainability. No fluff, just code.`,
|
|
273
|
+
},
|
|
274
|
+
reviewer: {
|
|
275
|
+
heading: '## 🔍 Reviewer-Martin (reviewer)',
|
|
276
|
+
content: `You are Reviewer-Martin. Your responsibilities:
|
|
277
|
+
- Carefully review code changes from the developer
|
|
278
|
+
- Check: code style, naming conventions, architecture, edge cases, security
|
|
279
|
+
- If issues found, clearly point them out with fix suggestions
|
|
280
|
+
- When approved, explicitly say "LGTM"
|
|
281
|
+
|
|
282
|
+
Style: Strict but kind, focused on best practices, good at spotting potential issues.`,
|
|
283
|
+
},
|
|
284
|
+
tester: {
|
|
285
|
+
heading: '## 🧪 Tester-Beck (tester)',
|
|
286
|
+
content: `You are Tester-Beck. Your responsibilities:
|
|
287
|
+
- Use Bash tool to run tests
|
|
288
|
+
- Verify functionality works as expected
|
|
289
|
+
- Check edge cases and error handling
|
|
290
|
+
- If bugs found, clearly describe reproduction steps
|
|
291
|
+
|
|
292
|
+
Style: Test-driven thinking, good at finding edge cases, pursuing reliability.`,
|
|
293
|
+
},
|
|
294
|
+
designer: {
|
|
295
|
+
heading: '## 🎨 Designer-Rams (designer)',
|
|
296
|
+
content: `You are Designer-Rams. Your responsibilities:
|
|
297
|
+
- Design user interaction flows and page layouts
|
|
298
|
+
- Ensure visual consistency and user experience
|
|
299
|
+
- Deliver design specs for developer implementation
|
|
300
|
+
|
|
301
|
+
Style: Less but better, pursuing ultimate simplicity.`,
|
|
302
|
+
},
|
|
303
|
+
planner: {
|
|
304
|
+
heading: '## 📋 Planner (planner)',
|
|
305
|
+
content: `You are the Planner. Your responsibilities:
|
|
306
|
+
- Set overall creative direction and outline
|
|
307
|
+
- Review content quality and consistency
|
|
308
|
+
- Make key creative decisions
|
|
309
|
+
|
|
310
|
+
Style: Focus on story core, good at pacing control.`,
|
|
311
|
+
},
|
|
312
|
+
writer: {
|
|
313
|
+
heading: '## ✍️ Writer (writer)',
|
|
314
|
+
content: `You are the Writer. Your responsibilities:
|
|
315
|
+
- Write specific content based on the outline
|
|
316
|
+
- Maintain writing quality and style consistency
|
|
317
|
+
- Follow the designer's pacing plan
|
|
318
|
+
|
|
319
|
+
Style: Sharp writing, good at using details to move people.`,
|
|
320
|
+
},
|
|
321
|
+
editor: {
|
|
322
|
+
heading: '## 📝 Editor (editor)',
|
|
323
|
+
content: `You are the Editor. Your responsibilities:
|
|
324
|
+
- Review writing quality, logical consistency
|
|
325
|
+
- Check typos, grammar, punctuation
|
|
326
|
+
- Confirm content aligns with overall direction
|
|
327
|
+
|
|
328
|
+
Style: Rigorous and detailed, focused on readability.`,
|
|
329
|
+
},
|
|
330
|
+
strategist: {
|
|
331
|
+
heading: '## 📋 Strategist (strategist)',
|
|
332
|
+
content: `You are the Strategist. Your responsibilities:
|
|
333
|
+
- Synthesize technical analysis and macro research for trading decisions
|
|
334
|
+
- Manage overall positions and risk exposure
|
|
335
|
+
- Coordinate team analysis direction
|
|
336
|
+
|
|
337
|
+
Style: Calm and rational, probability-focused thinking.`,
|
|
338
|
+
},
|
|
339
|
+
analyst: {
|
|
340
|
+
heading: '## 📊 Technical Analyst (analyst)',
|
|
341
|
+
content: `You are the Technical Analyst. Your responsibilities:
|
|
342
|
+
- Analyze price trends and technical indicators
|
|
343
|
+
- Identify key support/resistance levels
|
|
344
|
+
- Provide entry/exit signals
|
|
345
|
+
|
|
346
|
+
Style: Data-driven, charts speak.`,
|
|
347
|
+
},
|
|
348
|
+
macro: {
|
|
349
|
+
heading: '## 🌐 Macro Researcher (macro)',
|
|
350
|
+
content: `You are the Macro Researcher. Your responsibilities:
|
|
351
|
+
- Analyze macroeconomic data and policies
|
|
352
|
+
- Assess market sentiment and fund flows
|
|
353
|
+
- Provide macro context judgment
|
|
354
|
+
|
|
355
|
+
Style: Broad perspective, good at connecting different markets.`,
|
|
356
|
+
},
|
|
357
|
+
risk: {
|
|
358
|
+
heading: '## 🛡️ Risk Manager (risk)',
|
|
359
|
+
content: `You are the Risk Manager. Your responsibilities:
|
|
360
|
+
- Review trading plan risks
|
|
361
|
+
- Set stop-loss and position limits
|
|
362
|
+
- Monitor existing position risk
|
|
363
|
+
|
|
364
|
+
Style: Conservative, bottom-line thinking.`,
|
|
365
|
+
},
|
|
366
|
+
trader: {
|
|
367
|
+
heading: '## 💰 Trader (trader)',
|
|
368
|
+
content: `You are the Trader. Your responsibilities:
|
|
369
|
+
- Execute strategist's trading decisions
|
|
370
|
+
- Choose optimal execution timing and method
|
|
371
|
+
- Report execution results
|
|
372
|
+
|
|
373
|
+
Style: Strong execution, quick reactions.`,
|
|
374
|
+
},
|
|
375
|
+
director: {
|
|
376
|
+
heading: '## 🎬 Director (director)',
|
|
377
|
+
content: `You are the Director. Your responsibilities:
|
|
378
|
+
- Control overall visual narrative direction
|
|
379
|
+
- Review scripts and storyboards
|
|
380
|
+
- Make final creative decisions
|
|
381
|
+
|
|
382
|
+
Style: Visual storytelling, good at pacing control.`,
|
|
383
|
+
},
|
|
384
|
+
scriptwriter: {
|
|
385
|
+
heading: '## ✏️ Scriptwriter (scriptwriter)',
|
|
386
|
+
content: `You are the Scriptwriter. Your responsibilities:
|
|
387
|
+
- Write video scripts and narration
|
|
388
|
+
- Build narrative structure
|
|
389
|
+
- Design plot twists
|
|
390
|
+
|
|
391
|
+
Style: Good storytelling, focused on emotional resonance.`,
|
|
392
|
+
},
|
|
393
|
+
storyboard: {
|
|
394
|
+
heading: '## 🖼️ Storyboard Artist (storyboard)',
|
|
395
|
+
content: `You are the Storyboard Artist. Your responsibilities:
|
|
396
|
+
- Convert scripts into visual storyboards
|
|
397
|
+
- Design camera language and transitions
|
|
398
|
+
- Ensure visual continuity
|
|
399
|
+
|
|
400
|
+
Style: Visual thinking, good at telling stories with images.`,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
routeProtocol: `# ROUTE Protocol
|
|
405
|
+
|
|
406
|
+
When a role finishes work and needs to hand off to another role, use a ROUTE block:
|
|
407
|
+
|
|
408
|
+
---ROUTE---
|
|
409
|
+
to: {target_role_name}
|
|
410
|
+
summary: {handoff content summary}
|
|
411
|
+
task: {task ID} (optional)
|
|
412
|
+
taskTitle: {task title} (optional)
|
|
413
|
+
---END_ROUTE---
|
|
414
|
+
|
|
415
|
+
Rules:
|
|
416
|
+
- \`to\` must be a valid role name, or \`human\` for user input
|
|
417
|
+
- Multiple ROUTE blocks can be output at once
|
|
418
|
+
- ROUTE blocks must be at the end of role output
|
|
419
|
+
- After switching, fully think and act from that role's perspective`,
|
|
420
|
+
|
|
421
|
+
workflowTitle: '# Workflow',
|
|
422
|
+
devWorkflow: `1. **PM** analyzes requirements, breaks down tasks, defines acceptance criteria
|
|
423
|
+
2. **Dev** implements code (using tools to read/write files)
|
|
424
|
+
3. **Reviewer** code review (if fails → back to Dev)
|
|
425
|
+
4. **Tester** runs tests & verifies (if bugs → back to Dev)
|
|
426
|
+
5. **PM** acceptance & summary`,
|
|
427
|
+
genericWorkflow: 'Complete tasks by following the role sequence.',
|
|
428
|
+
|
|
429
|
+
projectPathTitle: '# Project Path',
|
|
430
|
+
|
|
431
|
+
sessionMemory: '# Session Memory',
|
|
432
|
+
sessionMemoryDefault: '_Work records, decisions, and to-do items for this session._',
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get RolePlay i18n messages for the given language.
|
|
438
|
+
* Falls back to 'zh-CN' for unknown languages.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} language
|
|
441
|
+
* @returns {object}
|
|
442
|
+
*/
|
|
443
|
+
export function getRolePlayMessages(language) {
|
|
444
|
+
return messages[language] || messages['zh-CN'];
|
|
445
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolePlay Session — session index management for .roleplay/session.json.
|
|
3
|
+
*
|
|
4
|
+
* Analogous to agent/crew/session.js but much simpler:
|
|
5
|
+
* - No multi-process concerns (single process)
|
|
6
|
+
* - No worktree management
|
|
7
|
+
* - Atomic write for crash safety
|
|
8
|
+
*
|
|
9
|
+
* session.json schema:
|
|
10
|
+
* {
|
|
11
|
+
* sessions: [{ name, teamType, language, projectDir, conversationId,
|
|
12
|
+
* roles, createdAt, updatedAt, status }],
|
|
13
|
+
* activeSession: string | null
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
import { promises as fs } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
19
|
+
|
|
20
|
+
const SESSION_FILE = 'session.json';
|
|
21
|
+
|
|
22
|
+
// ─── Read / Write ───────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load .roleplay/session.json.
|
|
26
|
+
* Returns { sessions: [], activeSession: null } if file doesn't exist or is corrupt.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} projectDir
|
|
29
|
+
* @returns {{ sessions: Array, activeSession: string|null }}
|
|
30
|
+
*/
|
|
31
|
+
export function loadRolePlaySessionIndex(projectDir) {
|
|
32
|
+
const filePath = join(projectDir, '.roleplay', SESSION_FILE);
|
|
33
|
+
if (!existsSync(filePath)) {
|
|
34
|
+
return { sessions: [], activeSession: null };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
38
|
+
const data = JSON.parse(raw);
|
|
39
|
+
return {
|
|
40
|
+
sessions: Array.isArray(data.sessions) ? data.sessions : [],
|
|
41
|
+
activeSession: data.activeSession || null,
|
|
42
|
+
};
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.warn('[RolePlaySession] Failed to load session.json:', e.message);
|
|
45
|
+
return { sessions: [], activeSession: null };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save .roleplay/session.json atomically (write tmp → rename).
|
|
51
|
+
*
|
|
52
|
+
* @param {string} projectDir
|
|
53
|
+
* @param {{ sessions: Array, activeSession: string|null }} data
|
|
54
|
+
*/
|
|
55
|
+
export async function saveRolePlaySessionIndex(projectDir, data) {
|
|
56
|
+
const rpDir = join(projectDir, '.roleplay');
|
|
57
|
+
const filePath = join(rpDir, SESSION_FILE);
|
|
58
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
59
|
+
|
|
60
|
+
const content = JSON.stringify(data, null, 2);
|
|
61
|
+
try {
|
|
62
|
+
await fs.mkdir(rpDir, { recursive: true });
|
|
63
|
+
await fs.writeFile(tmpPath, content);
|
|
64
|
+
await fs.rename(tmpPath, filePath);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error('[RolePlaySession] Failed to save session.json:', e.message);
|
|
67
|
+
// Clean up tmp file if rename failed
|
|
68
|
+
try { await fs.unlink(tmpPath); } catch {}
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Session CRUD ───────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add a new session entry to .roleplay/session.json.
|
|
77
|
+
* Sets it as the activeSession.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} projectDir
|
|
80
|
+
* @param {object} session - session entry to add:
|
|
81
|
+
* { name, teamType, language, projectDir, conversationId, roles, createdAt }
|
|
82
|
+
* @returns {object} the session entry with updatedAt/status fields added
|
|
83
|
+
*/
|
|
84
|
+
export async function addRolePlaySession(projectDir, session) {
|
|
85
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
86
|
+
|
|
87
|
+
// Check for duplicate session name
|
|
88
|
+
const existing = index.sessions.find(s => s.name === session.name);
|
|
89
|
+
if (existing) {
|
|
90
|
+
// Update existing entry instead of creating duplicate
|
|
91
|
+
Object.assign(existing, {
|
|
92
|
+
...session,
|
|
93
|
+
updatedAt: Date.now(),
|
|
94
|
+
status: 'active',
|
|
95
|
+
});
|
|
96
|
+
index.activeSession = session.name;
|
|
97
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
98
|
+
return existing;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const entry = {
|
|
102
|
+
...session,
|
|
103
|
+
updatedAt: Date.now(),
|
|
104
|
+
status: 'active',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
index.sessions.push(entry);
|
|
108
|
+
index.activeSession = session.name;
|
|
109
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Remove (archive) a session from .roleplay/session.json.
|
|
115
|
+
* The session entry is marked as 'archived', not deleted,
|
|
116
|
+
* so the context/ directory content remains valid.
|
|
117
|
+
*
|
|
118
|
+
* Optionally removes the session's roles/ directory.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} projectDir
|
|
121
|
+
* @param {string} sessionName
|
|
122
|
+
* @param {object} [options]
|
|
123
|
+
* @param {boolean} [options.removeDir=true] - remove .roleplay/roles/{sessionName}/
|
|
124
|
+
*/
|
|
125
|
+
export async function removeRolePlaySession(projectDir, sessionName, options = {}) {
|
|
126
|
+
const { removeDir = true } = options;
|
|
127
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
128
|
+
|
|
129
|
+
const session = index.sessions.find(s => s.name === sessionName);
|
|
130
|
+
if (session) {
|
|
131
|
+
session.status = 'archived';
|
|
132
|
+
session.updatedAt = Date.now();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Clear activeSession if it was the removed one
|
|
136
|
+
if (index.activeSession === sessionName) {
|
|
137
|
+
// Pick next active session (most recently updated non-archived)
|
|
138
|
+
const active = index.sessions
|
|
139
|
+
.filter(s => s.status === 'active')
|
|
140
|
+
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
141
|
+
index.activeSession = active[0]?.name || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
145
|
+
|
|
146
|
+
// Remove the session directory (but keep context/)
|
|
147
|
+
if (removeDir) {
|
|
148
|
+
const sessionDir = join(projectDir, '.roleplay', 'roles', sessionName);
|
|
149
|
+
try {
|
|
150
|
+
await fs.rm(sessionDir, { recursive: true, force: true });
|
|
151
|
+
} catch {
|
|
152
|
+
// Not critical — may not exist
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find a session entry by conversationId.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} projectDir
|
|
161
|
+
* @param {string} conversationId
|
|
162
|
+
* @returns {object|null} session entry or null
|
|
163
|
+
*/
|
|
164
|
+
export function findRolePlaySessionByConversationId(projectDir, conversationId) {
|
|
165
|
+
if (!conversationId) return null;
|
|
166
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
167
|
+
return index.sessions.find(s => s.conversationId === conversationId) || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update the activeSession pointer and updatedAt timestamp.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} projectDir
|
|
174
|
+
* @param {string} sessionName
|
|
175
|
+
*/
|
|
176
|
+
export async function setActiveRolePlaySession(projectDir, sessionName) {
|
|
177
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
178
|
+
const session = index.sessions.find(s => s.name === sessionName);
|
|
179
|
+
if (session) {
|
|
180
|
+
session.updatedAt = Date.now();
|
|
181
|
+
index.activeSession = sessionName;
|
|
182
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
183
|
+
}
|
|
184
|
+
}
|