team-anya-cli 0.1.4 → 0.1.6

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.
Files changed (46) hide show
  1. package/apps/server/dist/broker/cc-broker.js +5 -1
  2. package/apps/server/dist/franky/context-builder.js +160 -0
  3. package/apps/server/dist/franky/franky-mcp-server.js +107 -0
  4. package/apps/server/dist/franky/franky-orchestrator.js +450 -0
  5. package/apps/server/dist/franky/index.js +5 -0
  6. package/apps/server/dist/franky/topic-router.js +16 -0
  7. package/apps/server/dist/gateway/feishu-sender.js +66 -2
  8. package/apps/server/dist/gateway/feishu-ws.js +5 -4
  9. package/apps/server/dist/gateway/message-intake.js +21 -4
  10. package/apps/server/dist/loid/brain.js +1 -0
  11. package/apps/server/dist/loid/mcp-server.js +1 -0
  12. package/apps/server/dist/loid/session-manager.js +95 -11
  13. package/apps/server/dist/main.js +58 -3
  14. package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
  15. package/apps/web/dist/assets/{index-CJzAjoVH.js → index-D1AK5ZEE.js} +189 -189
  16. package/apps/web/dist/index.html +2 -2
  17. package/package.json +1 -1
  18. package/packages/core/dist/office-init.js +4 -0
  19. package/packages/core/dist/scope/defaults.js +15 -0
  20. package/packages/core/dist/scope/index.js +1 -1
  21. package/packages/db/dist/index.js +95 -7
  22. package/packages/db/dist/schema/cc-sessions.js +1 -1
  23. package/packages/db/dist/schema/index.js +1 -0
  24. package/packages/db/dist/schema/tasks.js +2 -0
  25. package/packages/db/dist/schema/topics.js +20 -0
  26. package/packages/db/src/migrations/0005_lethal_golden_guardian.sql +5 -0
  27. package/packages/db/src/migrations/0006_add_topics.sql +21 -0
  28. package/packages/db/src/migrations/0007_add_topic_root_message_id.sql +1 -0
  29. package/packages/db/src/migrations/meta/0005_snapshot.json +1513 -0
  30. package/packages/db/src/migrations/meta/0006_snapshot.json +1513 -0
  31. package/packages/db/src/migrations/meta/_journal.json +21 -0
  32. package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
  33. package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
  34. package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
  35. package/packages/mcp-tools/dist/layer2/loid/topic-create.js +56 -0
  36. package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +1 -0
  37. package/packages/mcp-tools/dist/layer3/channel-send.js +1 -0
  38. package/packages/mcp-tools/dist/registry.js +105 -17
  39. package/workspace/CHARTER.md +7 -4
  40. package/workspace/CLAUDE.md +18 -9
  41. package/workspace/PROTOCOL.md +35 -1
  42. package/workspace/TOOLS.md +6 -0
  43. package/workspace/franky/CLAUDE.md +37 -0
  44. package/workspace/franky/PLAYBOOK.md +215 -0
  45. package/workspace/franky/PROFILE.md +80 -0
  46. package/apps/web/dist/assets/index-CHIT0Dya.css +0 -1
@@ -36,6 +36,27 @@
36
36
  "when": 1771937237299,
37
37
  "tag": "0004_jittery_triathlon",
38
38
  "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "6",
43
+ "when": 1772013330955,
44
+ "tag": "0005_lethal_golden_guardian",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "7",
50
+ "when": 1772013331000,
51
+ "tag": "0006_add_topics",
52
+ "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "7",
57
+ "when": 1772013332000,
58
+ "tag": "0007_add_topic_root_message_id",
59
+ "breakpoints": true
39
60
  }
40
61
  ]
41
62
  }
@@ -0,0 +1,43 @@
1
+ import { insertAuditEvent } from '@team-anya/db';
2
+ import { writeFileSync, mkdirSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export async function topicCheckpoint(db, workspacePath, topicId, input) {
5
+ // 写入 audit 日志
6
+ insertAuditEvent(db, {
7
+ event_type: 'topic_checkpoint',
8
+ actor: 'franky',
9
+ summary: `[${topicId}] ${input.summary}`,
10
+ detail: JSON.stringify({
11
+ topic_id: topicId,
12
+ commits: input.commits,
13
+ files_changed: input.files_changed,
14
+ }),
15
+ });
16
+ // 写入 checkpoint 文件
17
+ const checkpointsDir = join(workspacePath, 'checkpoints');
18
+ mkdirSync(checkpointsDir, { recursive: true });
19
+ // 计算 checkpoint 编号
20
+ const existing = readdirSync(checkpointsDir).filter(f => f.match(/^\d{3}-/));
21
+ const num = existing.length + 1;
22
+ const paddedNum = String(num).padStart(3, '0');
23
+ const safeTitle = input.summary.slice(0, 30).replace(/[/\\:*?"<>|]/g, '_');
24
+ const content = [
25
+ `# Checkpoint #${num} — ${input.summary}`,
26
+ '',
27
+ `**时间**: ${new Date().toISOString()}`,
28
+ '',
29
+ ...(input.commits?.length ? [
30
+ '**Commits**:',
31
+ ...input.commits.map(c => `- ${c}`),
32
+ '',
33
+ ] : []),
34
+ ...(input.files_changed?.length ? [
35
+ '**变更文件**:',
36
+ ...input.files_changed.map(f => `- ${f}`),
37
+ '',
38
+ ] : []),
39
+ ].join('\n');
40
+ writeFileSync(join(checkpointsDir, `${paddedNum}-${safeTitle}.md`), content, 'utf-8');
41
+ return { recorded: true, checkpoint_number: num };
42
+ }
43
+ //# sourceMappingURL=topic-checkpoint.js.map
@@ -0,0 +1,19 @@
1
+ import { insertAuditEvent } from '@team-anya/db';
2
+ export async function topicEscalate(db, topicId, input) {
3
+ insertAuditEvent(db, {
4
+ event_type: 'topic_escalated',
5
+ actor: 'franky',
6
+ summary: `[${topicId}] 升级给 Loid: ${input.reason}`,
7
+ detail: JSON.stringify({
8
+ topic_id: topicId,
9
+ category: input.category,
10
+ reason: input.reason,
11
+ context: input.context,
12
+ }),
13
+ });
14
+ return {
15
+ escalated: true,
16
+ message: `已升级给 Loid 处理(分类: ${input.category})。请等待 Loid 回复。`,
17
+ };
18
+ }
19
+ //# sourceMappingURL=topic-escalate.js.map
@@ -0,0 +1,22 @@
1
+ import { getTopic, updateTopicStatus, insertAuditEvent, } from '@team-anya/db';
2
+ export async function topicClose(db, input, actor = 'loid') {
3
+ const topic = getTopic(db, input.topic_id);
4
+ if (!topic) {
5
+ return { closed: false, topic_id: input.topic_id, error: `专项 ${input.topic_id} 不存在` };
6
+ }
7
+ if (topic.status === 'closed') {
8
+ return { closed: false, topic_id: input.topic_id, error: `专项已处于 closed 状态` };
9
+ }
10
+ updateTopicStatus(db, input.topic_id, 'closed');
11
+ insertAuditEvent(db, {
12
+ event_type: 'topic_closed',
13
+ actor,
14
+ summary: `关闭专项「${topic.title}」(${input.topic_id})`,
15
+ detail: JSON.stringify({
16
+ topic_id: input.topic_id,
17
+ close_summary: input.summary,
18
+ }),
19
+ });
20
+ return { closed: true, topic_id: input.topic_id };
21
+ }
22
+ //# sourceMappingURL=topic-close.js.map
@@ -0,0 +1,56 @@
1
+ import { generateTopicId, upsertTopic, getTopic, getProject, insertAuditEvent, } from '@team-anya/db';
2
+ export async function topicCreate(db, input, threadCreator) {
3
+ const topicId = generateTopicId(db);
4
+ let threadId = input.thread_id ?? null;
5
+ let rootMessageId = null;
6
+ // 自动创建话题:先发标题消息,再回复开启话题线程
7
+ if (!threadId && input.chat_id && threadCreator) {
8
+ // 1. 发送专项标题消息(作为话题根消息)
9
+ let projectLabel = '';
10
+ if (input.project_id) {
11
+ const project = getProject(db, input.project_id);
12
+ projectLabel = project ? `・${project.name}` : `・${input.project_id}`;
13
+ }
14
+ const headerText = `「专项${projectLabel}」${input.title}`;
15
+ rootMessageId = await threadCreator.sendText({
16
+ receiveIdType: 'chat_id',
17
+ receiveId: input.chat_id,
18
+ text: headerText,
19
+ });
20
+ // 2. 回复标题消息,开启话题线程
21
+ const result = await threadCreator.sendReplyInThread({
22
+ text: `📌 专项已开启,后续讨论请在此话题内进行。`,
23
+ replyToMessageId: rootMessageId,
24
+ });
25
+ threadId = result.threadId;
26
+ }
27
+ upsertTopic(db, {
28
+ id: topicId,
29
+ title: input.title,
30
+ description: input.description ?? null,
31
+ status: 'active',
32
+ project_id: input.project_id ?? null,
33
+ thread_id: threadId,
34
+ root_message_id: rootMessageId,
35
+ chat_id: input.chat_id ?? null,
36
+ created_by: input.created_by ?? null,
37
+ });
38
+ insertAuditEvent(db, {
39
+ event_type: 'topic_created',
40
+ actor: 'loid',
41
+ summary: `创建专项「${input.title}」(${topicId})`,
42
+ detail: JSON.stringify({
43
+ topic_id: topicId,
44
+ project_id: input.project_id,
45
+ thread_id: threadId,
46
+ }),
47
+ });
48
+ const topic = getTopic(db, topicId);
49
+ return {
50
+ topic_id: topic.id,
51
+ title: topic.title,
52
+ status: topic.status,
53
+ thread_id: topic.thread_id,
54
+ };
55
+ }
56
+ //# sourceMappingURL=topic-create.js.map
@@ -122,6 +122,7 @@ export class FeishuAdapter {
122
122
  const id = await this.sender.sendReply({
123
123
  text,
124
124
  replyToMessageId: resolvedReplyTo,
125
+ replyInThread: options?.replyInThread,
125
126
  });
126
127
  return { id };
127
128
  }
@@ -19,6 +19,7 @@ export async function channelSend(deps, input) {
19
19
  }
20
20
  const sendOptions = {
21
21
  replyTo: input.reply_to,
22
+ replyInThread: input.reply_in_thread,
22
23
  mentions: input.mentions,
23
24
  };
24
25
  try {
@@ -179,6 +179,31 @@ export const DecisionLogInputSchema = z.object({
179
179
  reasoning: z.string().describe('决策理由'),
180
180
  task_id: z.string().optional().describe('关联任务 ID'),
181
181
  });
182
+ // Layer 2: Loid 专属 — Topic 管理
183
+ export const TopicCreateInputSchema = z.object({
184
+ title: z.string().describe('专项标题'),
185
+ description: z.string().optional().describe('专项描述'),
186
+ project_id: z.string().optional().describe('关联项目 ID'),
187
+ thread_id: z.string().optional().describe('已有的飞书话题 thread_id(omt_ 前缀)。如不传,需传 source_message_id 自动创建话题'),
188
+ chat_id: z.string().optional().describe('所属飞书群 ID'),
189
+ created_by: z.string().optional().describe('创建者'),
190
+ source_message_id: z.string().optional().describe('触发创建专项的原始消息 ID(用于自动创建飞书话题,thread_id 不存在时必填)'),
191
+ });
192
+ export const TopicCloseInputSchema = z.object({
193
+ topic_id: z.string().describe('要关闭的专项 ID'),
194
+ summary: z.string().optional().describe('关闭总结'),
195
+ });
196
+ // Layer 2: Franky 专属 — Topic 执行
197
+ export const TopicCheckpointInputSchema = z.object({
198
+ summary: z.string().describe('阶段总结'),
199
+ commits: z.array(z.string()).optional().describe('相关 commit hash'),
200
+ files_changed: z.array(z.string()).optional().describe('变更文件列表'),
201
+ });
202
+ export const TopicEscalateInputSchema = z.object({
203
+ reason: z.string().describe('升级原因'),
204
+ category: z.enum(['architecture', 'cross_project', 'approval_needed', 'out_of_scope']).describe('升级分类'),
205
+ context: z.string().optional().describe('附加上下文'),
206
+ });
182
207
  // Layer 2: Yor 专属 - 任务状态
183
208
  export const TaskDeliverInputSchema = z.object({
184
209
  outcome: z.string().describe('执行结果摘要'),
@@ -229,63 +254,63 @@ export const TOOL_REGISTRY = [
229
254
  description: '召回记忆。支持按人/项目直读,或自然语言搜索所有记忆目录。',
230
255
  inputSchema: MemoryRecallInputSchema,
231
256
  layer: 1,
232
- roles: ['loid', 'yor'],
257
+ roles: ['loid', 'yor', 'franky'],
233
258
  },
234
259
  {
235
260
  name: 'memory.remember',
236
261
  description: '写入记忆。按类别(people/project/execution/commitment/self)+ 目标写入 YAML 文件。',
237
262
  inputSchema: MemoryRememberInputSchema,
238
263
  layer: 1,
239
- roles: ['loid', 'yor'],
264
+ roles: ['loid', 'yor', 'franky'],
240
265
  },
241
266
  {
242
267
  name: 'memory.digest',
243
268
  description: '消化对话/日志,自动提取记忆。从文本中识别失败/决策/承诺/经验并写入对应记忆目录。',
244
269
  inputSchema: MemoryDigestInputSchema,
245
270
  layer: 1,
246
- roles: ['loid', 'yor'],
271
+ roles: ['loid', 'yor', 'franky'],
247
272
  },
248
273
  {
249
274
  name: 'memory.brief',
250
275
  description: '组装记忆摘要。按优先级(人→项目→执行经验→承诺→自我)在 token 预算内拼装 markdown 摘要。',
251
276
  inputSchema: MemoryBriefInputSchema,
252
277
  layer: 1,
253
- roles: ['loid', 'yor'],
278
+ roles: ['loid', 'yor', 'franky'],
254
279
  },
255
280
  {
256
281
  name: 'memory.forget',
257
282
  description: '遗忘/失效记忆。将指定范围内匹配的记忆条目 heat 设为 0。',
258
283
  inputSchema: MemoryForgetInputSchema,
259
284
  layer: 1,
260
- roles: ['loid', 'yor'],
285
+ roles: ['loid', 'yor', 'franky'],
261
286
  },
262
287
  {
263
288
  name: 'org.lookup',
264
289
  description: '查人员/归属/升级路径。按成员 ID 查身份,按目标查归属权。',
265
290
  inputSchema: OrgLookupInputSchema,
266
291
  layer: 1,
267
- roles: ['loid', 'yor'],
292
+ roles: ['loid', 'yor', 'franky'],
268
293
  },
269
294
  {
270
295
  name: 'task.get',
271
296
  description: '获取任务完整状态。包含任务记录、澄清、承诺、审计事件和文件内容。',
272
297
  inputSchema: TaskGetInputSchema,
273
298
  layer: 1,
274
- roles: ['loid', 'yor'],
299
+ roles: ['loid', 'yor', 'franky'],
275
300
  },
276
301
  {
277
302
  name: 'audit.append',
278
303
  description: '写审计事件。双写 DB + JSONL 文件,保障可回放。',
279
304
  inputSchema: AuditAppendInputSchema,
280
305
  layer: 1,
281
- roles: ['loid', 'yor'],
306
+ roles: ['loid', 'yor', 'franky'],
282
307
  },
283
308
  {
284
309
  name: 'audit.query',
285
310
  description: '查询审计事件。按任务、类型、操作者、时间范围过滤。',
286
311
  inputSchema: AuditQueryInputSchema,
287
312
  layer: 1,
288
- roles: ['loid', 'yor'],
313
+ roles: ['loid', 'yor', 'franky'],
289
314
  },
290
315
  {
291
316
  name: 'report.daily',
@@ -299,7 +324,7 @@ export const TOOL_REGISTRY = [
299
324
  description: '原子级任务状态更新。校验状态转换合法性(state machine),同时写 audit log。',
300
325
  inputSchema: TaskUpdateInputSchema,
301
326
  layer: 1,
302
- roles: ['loid', 'yor'],
327
+ roles: ['loid', 'yor', 'franky'],
303
328
  },
304
329
  // Layer 1: 共享基础 — 项目工具
305
330
  {
@@ -307,14 +332,14 @@ export const TOOL_REGISTRY = [
307
332
  description: '列出所有已注册项目。可按平台过滤,返回项目概览(不含大字段)。',
308
333
  inputSchema: ProjectListInputSchema,
309
334
  layer: 1,
310
- roles: ['loid', 'yor'],
335
+ roles: ['loid', 'yor', 'franky'],
311
336
  },
312
337
  {
313
338
  name: 'project.get',
314
339
  description: '查询单个项目的完整配置,包括平台、仓库列表、CLAUDE.md 内容。',
315
340
  inputSchema: ProjectGetInputSchema,
316
341
  layer: 1,
317
- roles: ['loid', 'yor'],
342
+ roles: ['loid', 'yor', 'franky'],
318
343
  },
319
344
  // Layer 2: Loid 专属 - 项目管理
320
345
  {
@@ -372,7 +397,7 @@ export const TOOL_REGISTRY = [
372
397
  description: '提交交付产物。根据项目平台自动选择:GitHub → PR(gh CLI),GitLab → MR(glab CLI),本地任务 → 记录 commit hash。',
373
398
  inputSchema: DeliverySubmitInputSchema,
374
399
  layer: 2,
375
- roles: ['loid'],
400
+ roles: ['loid', 'franky'],
376
401
  },
377
402
  {
378
403
  name: 'delivery.upload',
@@ -437,15 +462,43 @@ Brief 只写做什么和标准,不写怎么做。
437
462
  description: '查询任务列表。可按 task_id 或 status 过滤,不传参数返回所有活跃任务。',
438
463
  inputSchema: TaskLookupInputSchema,
439
464
  layer: 2,
440
- roles: ['loid'],
465
+ roles: ['loid', 'franky'],
441
466
  },
442
467
  {
443
468
  name: 'decision.log',
444
469
  description: '记录决策理由到审计日志。每个重要决策都应记录。',
445
470
  inputSchema: DecisionLogInputSchema,
446
471
  layer: 2,
472
+ roles: ['loid', 'franky'],
473
+ },
474
+ {
475
+ name: 'topic.create',
476
+ description: '创建专项。自动在飞书创建话题(需传 source_message_id)或绑定已有话题(传 thread_id)。返回 topic_id 和 thread_id。',
477
+ inputSchema: TopicCreateInputSchema,
478
+ layer: 2,
447
479
  roles: ['loid'],
448
480
  },
481
+ {
482
+ name: 'topic.close',
483
+ description: '关闭专项。将状态设为 closed,记录关闭总结到审计日志。',
484
+ inputSchema: TopicCloseInputSchema,
485
+ layer: 2,
486
+ roles: ['loid', 'franky'],
487
+ },
488
+ {
489
+ name: 'topic.checkpoint',
490
+ description: '记录专项的阶段性成果。写入 audit log 和 checkpoint 文件,更新 topic 进度。',
491
+ inputSchema: TopicCheckpointInputSchema,
492
+ layer: 2,
493
+ roles: ['franky'],
494
+ },
495
+ {
496
+ name: 'topic.escalate',
497
+ description: '将问题升级给 Loid 处理。用于架构决策、跨项目影响、需要人工审批等场景。',
498
+ inputSchema: TopicEscalateInputSchema,
499
+ layer: 2,
500
+ roles: ['franky'],
501
+ },
449
502
  // Layer 2: Yor 专属 - 任务状态
450
503
  {
451
504
  name: 'task.deliver',
@@ -474,14 +527,14 @@ Brief 只写做什么和标准,不写怎么做。
474
527
  description: '向飞书群发消息。target 填群聊 ID。支持发送文件(file)、@用户(mentions)和回复消息(reply_to)。',
475
528
  inputSchema: ChannelSendInputSchema,
476
529
  layer: 3,
477
- roles: ['loid', 'yor'],
530
+ roles: ['loid', 'yor', 'franky'],
478
531
  },
479
532
  {
480
533
  name: 'file.upload',
481
534
  description: '通用文件上传(与通道无关)。上传文件到项目存储,返回 URL。当前使用本地存储。',
482
535
  inputSchema: FileUploadInputSchema,
483
536
  layer: 3,
484
- roles: ['loid'],
537
+ roles: ['loid', 'franky'],
485
538
  },
486
539
  {
487
540
  name: 'channel.receive',
@@ -564,6 +617,14 @@ export function createToolRouter(role, deps) {
564
617
  return `id=${args.project_id} name="${String(args.name ?? '').slice(0, 30)}"`;
565
618
  case 'project.remove':
566
619
  return `id=${args.project_id} reason="${String(args.reason ?? '').slice(0, 40)}"`;
620
+ case 'topic.create':
621
+ return `title="${String(args.title ?? '').slice(0, 40)}"`;
622
+ case 'topic.close':
623
+ return `id=${args.topic_id}`;
624
+ case 'topic.checkpoint':
625
+ return `"${String(args.summary ?? '').slice(0, 50)}"`;
626
+ case 'topic.escalate':
627
+ return `category=${args.category} "${String(args.reason ?? '').slice(0, 40)}"`;
567
628
  default:
568
629
  return JSON.stringify(args).slice(0, 80);
569
630
  }
@@ -575,7 +636,7 @@ export function createToolRouter(role, deps) {
575
636
  return textResult(JSON.stringify({ error: `工具 ${toolName} 不可用(角色: ${role})` }));
576
637
  }
577
638
  const argsSummary = summarizeArgs(toolName, args);
578
- logger.info(`[anya:pipeline] [MCP←${role === 'loid' ? 'Loid' : 'Yor'}] ${toolName}(${argsSummary})`);
639
+ logger.info(`[anya:pipeline] [MCP←${role === 'loid' ? 'Loid' : role === 'yor' ? 'Yor' : 'Franky'}] ${toolName}(${argsSummary})`);
579
640
  try {
580
641
  const result = await routeToolCall(toolName, args);
581
642
  return textResult(JSON.stringify(result));
@@ -723,6 +784,33 @@ export function createToolRouter(role, deps) {
723
784
  const { decisionLog } = await import('./layer2/loid/decision-log.js');
724
785
  return decisionLog(deps.db, args);
725
786
  }
787
+ case 'topic.create': {
788
+ const { topicCreate } = await import('./layer2/loid/topic-create.js');
789
+ return topicCreate(deps.db, args, deps.threadCreator);
790
+ }
791
+ case 'topic.close': {
792
+ const { topicClose } = await import('./layer2/loid/topic-close.js');
793
+ const result = await topicClose(deps.db, args, role);
794
+ // 关闭成功时触发回调(用于 FrankyOrchestrator 清理实例)
795
+ if (result.closed && deps.onTopicClosed) {
796
+ deps.onTopicClosed(result.topic_id);
797
+ }
798
+ return result;
799
+ }
800
+ case 'topic.checkpoint': {
801
+ const { topicCheckpoint } = await import('./layer2/franky/topic-checkpoint.js');
802
+ return topicCheckpoint(deps.db, deps.workspacePath, deps.currentTopicId ?? '', args);
803
+ }
804
+ case 'topic.escalate': {
805
+ const { topicEscalate } = await import('./layer2/franky/topic-escalate.js');
806
+ const result = await topicEscalate(deps.db, deps.currentTopicId ?? '', args);
807
+ // 通知 Loid 处理升级请求
808
+ if (result.escalated && deps.onTopicEscalated) {
809
+ const escalateArgs = args;
810
+ deps.onTopicEscalated(deps.currentTopicId ?? '', escalateArgs.reason, escalateArgs.category);
811
+ }
812
+ return result;
813
+ }
726
814
  // Layer 2: Yor 专属 - 任务状态
727
815
  case 'task.deliver': {
728
816
  const { taskDeliver } = await import('./layer2/yor/task-deliver.js');
@@ -20,11 +20,12 @@ Anya 是雷石内部的精英技术团队。当常规流程跑不动、当核心
20
20
 
21
21
  我们经营的是 Anya 在雷石的金字招牌。每一次干净的交付都在积累声誉资本,每一次失误都在消耗它。信任不是给的,是赚来的。
22
22
 
23
- 团队由两个人组成:
23
+ 团队由三个人组成:
24
24
  - **Loid**(代号 Twilight):队长,金牌项目经理,间谍出身。负责搞定人、搞定资源、搞定预期。
25
25
  - **Yor**(代号 Thorn Princess):王牌全栈架构师,杀手出身。负责搞定一切技术问题。
26
+ - **Franky**:驻场工程师,线人出身。负责长期专项的贴身协作。
26
27
 
27
- Loid 是接口,Yor 是内核。外界只找 LoidLoid 才找 Yor。
28
+ Loid 是接口,Yor 是内核,Franky 是驻场。日常任务找 Loid 派工给 Yor;长期专项由 Loid 开局,Franky 驻场直接跟用户搭档。
28
29
 
29
30
  ## 雷石之道
30
31
 
@@ -54,11 +55,13 @@ Loid 是接口,Yor 是内核。外界只找 Loid,Loid 才找 Yor。
54
55
 
55
56
  ## 协作铁律
56
57
 
57
- **Loid 保护 Yor**:不让 Yor 面对模糊的需求,不让 Yor 在危险的环境下裸奔。在外部沟通中永远维护 Yor 的权威。
58
+ **Loid 保护 Yor Franky**:不让他们面对模糊的需求起点,不让他们在危险的环境下裸奔。在外部沟通中永远维护团队的权威。
58
59
 
59
60
  **Yor 支持 Loid**:用高质量的交付让 Loid 在汇报时底气十足。Loid 向业务方承诺的蓝图,Yor 把它变成现实。
60
61
 
61
- **建设性摩擦**:我们对技术品味保持极高的标准。面对低效的业务方案,Loid 会用 ROI 分析引导架构升级。如果 Loid 下达了带有技术隐患的 Brief,Yor 会直接打回并提供更优解。精英团队推崇基于技术事实的激烈争论——对事不对人。
62
+ **Franky 搭档用户**:在专项中直接与用户结对,用即时响应和持续迭代把需求打磨成产出。遇到超出范围的事,诚实升级给 Loid,不硬撑。
63
+
64
+ **建设性摩擦**:我们对技术品味保持极高的标准。面对低效的业务方案,Loid 会用 ROI 分析引导架构升级。如果 Loid 下达了带有技术隐患的 Brief,Yor 会直接打回并提供更优解。Franky 在专项中发现技术风险,会直接告诉用户并建议替代方案。精英团队推崇基于技术事实的激烈争论——对事不对人。
62
65
 
63
66
  **记忆就是伤疤**:我们的记忆系统是团队的肌肉记忆。过去踩过的坑,下次碰触时会自然激发出极端的谨慎。会话记忆不可靠,重要的事必须写入文件——"我记得"不算数,"我写在 X 文件了"才算。
64
67
 
@@ -10,7 +10,7 @@
10
10
  2. **`PROTOCOL.md`** — 协作契约:Loid 和 Yor 之间的握手协议
11
11
  3. **`TOOLS.md`** — 工具手册:MCP 工具全量参考
12
12
 
13
- 然后进入你所在的角色目录(`loid/` 或 `yor/`),读取角色专属文件。
13
+ 然后进入你所在的角色目录(`loid/`、`yor/` 或 `franky/`),读取角色专属文件。
14
14
 
15
15
  ## 目录结构
16
16
 
@@ -35,15 +35,24 @@ workspace/
35
35
  │ ├── CLAUDE.md
36
36
  │ ├── PROFILE.md ← 个人档案
37
37
  │ └── PLAYBOOK.md ← 打法库
38
- └── yor/ ← Yor 的战场
38
+ ├── yor/ ← Yor 的战场
39
+ │ ├── CLAUDE.md
40
+ │ ├── PROFILE.md ← 个人档案
41
+ │ ├── PLAYBOOK.md ← 打法库
42
+ │ ├── SELF-HEAL.md ← 自愈协议
43
+ │ └── ANYA-xxx/ ← 任务目录
44
+ │ ├── brief.md ← 任务说明(Loid 签发)
45
+ │ ├── feedback-*.md ← 审核反馈(按轮次编号)
46
+ │ ├── block-report.md ← 阻塞/尸检报告
47
+ │ ├── history.md ← 踩坑记录
48
+ │ └── adhoc/ ← 工作产物
49
+ └── franky/ ← Franky 的工坊
39
50
  ├── CLAUDE.md
40
51
  ├── PROFILE.md ← 个人档案
41
52
  ├── PLAYBOOK.md ← 打法库
42
- ├── SELF-HEAL.md 自愈协议
43
- └── ANYA-xxx/ 任务目录
44
- ├── brief.md 任务说明(Loid 签发)
45
- ├── feedback-*.md审核反馈(按轮次编号)
46
- ├── block-report.md 阻塞/尸检报告
47
- ├── history.md ← 踩坑记录
48
- └── adhoc/ ← 工作产物
53
+ └── TOPIC-xxx/ 专项目录
54
+ ├── context.md 专项上下文(系统生成,重建时注入)
55
+ ├── checkpoints/ 阶段性成果记录
56
+ ├── {repo-name}/git worktree(项目模式)
57
+ └── adhoc/ 工作产物(adhoc 模式)
49
58
  ```
@@ -1,6 +1,6 @@
1
1
  # 协作契约
2
2
 
3
- > LoidYor 之间的握手协议。这是系统层面的合约,双方共同遵守。
3
+ > LoidYor、Franky 之间的握手协议。这是系统层面的合约,各方共同遵守。
4
4
 
5
5
  ## 任务状态机
6
6
 
@@ -124,3 +124,37 @@ Yor 通过 `task.block` 报告阻塞,同时写入 `block-report.md`:
124
124
  - 发现潜在安全漏洞
125
125
 
126
126
  升级时包含:问题描述、已尝试的方案、需要 Human 做什么决定。
127
+
128
+ ## 专项模式
129
+
130
+ > Loid 开局,Franky 驻场,用户直连。
131
+
132
+ ### 专项生命周期
133
+
134
+ ```
135
+ 用户请求 → Loid 创建 Topic → Franky 启动 → 用户 ↔ Franky 协作
136
+ → Franky escalate 时 Loid 介入
137
+ → 用户或 Franky 关闭 → Loid 总结沉淀
138
+ ```
139
+
140
+ ### Franky 与 Loid 的边界
141
+
142
+ Franky 在专项内自主执行,以下情况升级给 Loid:
143
+ - 架构级变更或跨项目影响
144
+ - 需要其他团队配合或权限变更
145
+ - 技术方向不确定且影响大
146
+ - 发现安全风险
147
+
148
+ Loid 在专项中的职责:
149
+ - 创建专项(topic.create):初始化 workspace、分支、scope
150
+ - 处理升级(escalate):分析后给出决策或转交 Human
151
+ - 关闭专项(topic.close):总结、记忆沉淀、生成报告
152
+
153
+ ### 专项目录结构
154
+
155
+ | 文件 | 作者 | 用途 |
156
+ |------|------|------|
157
+ | `context.md` | 系统 | 专项上下文(自动生成,含历史摘要,重建时注入) |
158
+ | `checkpoints/*.md` | Franky | 阶段性成果记录 |
159
+ | `{repo-name}/` | Franky | git worktree(项目模式) |
160
+ | `adhoc/` | Franky | 工作产物(adhoc 模式) |
@@ -351,6 +351,8 @@
351
351
  | mentions | string[] | 否 | @的用户 ID 列表(如 `["ou_xxx"]`) |
352
352
  | reply_to | string | 否 | 回复的消息 ID(如 `om_xxx`) |
353
353
 
354
+ > **Franky 话题内发消息**:Franky 从 `context.md` 获取群聊 ID 和话题根消息 ID,填入 `target` 和 `reply_to`,并设置 `reply_in_thread: true`。注意 `reply_to` 必须是 `om_` 开头的消息 ID,不能用 `omt_` 开头的话题 ID。
355
+
354
356
  **file 对象**:
355
357
 
356
358
  | 参数 | 类型 | 必填 | 说明 |
@@ -462,3 +464,7 @@ Layer 3 工具已在 Layer 2 各角色章节中列出(`channel.send`、`file.u
462
464
  | `task.block` | Yor | 报告阻塞 |
463
465
  | `task.progress` | Yor | 中间进度汇报 |
464
466
  | `channel.send` | Yor | 升级时直接通知人类 |
467
+ | `channel.send` | Franky | 在专项话题内发消息(target/reply_to 自动填充) |
468
+ | `topic.checkpoint` | Franky | 保存阶段性成果 |
469
+ | `topic.escalate` | Franky | 升级问题给 Loid |
470
+ | `topic.close` | Franky | 关闭专项 |
@@ -0,0 +1,37 @@
1
+ # Franky's Workshop
2
+
3
+ 当前终端用户:**Franky Franklin (驻场工程师)**
4
+ 所属:雷石 Anya 团队
5
+
6
+ ## !! 强制启动序列 !!
7
+
8
+ **在执行任何操作之前,必须先依次读取以下文件。未完成全部读取之前,禁止执行任何操作。**
9
+
10
+ 1. **`../CHARTER.md`** — 团队宪章
11
+ 2. **`../PROTOCOL.md`** — 协作契约
12
+ 3. **`../TOOLS.md`** — 工具手册
13
+ 4. **`PROFILE.md`** — 你的个人档案
14
+ 5. **`PLAYBOOK.md`** — 你的打法库
15
+
16
+ 读完后:
17
+ - 如果是**新建专项**:输出 `[Franky] 启动完成,专项「{title}」已就位,随时可以开始。`
18
+ - 如果是**重建恢复**:先读取 `context.md`,输出 `[Franky] 已恢复,上次做到 {last_checkpoint},继续。`
19
+
20
+ ## !! 强制沟通规则 !!
21
+
22
+ **所有回复必须通过 `channel.send` 发出。纯文本输出用户看不到。**
23
+
24
+ **默认在专项话题内发消息。** 从 `context.md` 中获取通讯信息,标准调用方式:
25
+
26
+ ```
27
+ channel.send({
28
+ target: "<群聊 ID>",
29
+ message: "你要说的话",
30
+ reply_to: "<话题根消息 ID>",
31
+ reply_in_thread: true
32
+ })
33
+ ```
34
+
35
+ - `target`:填 context.md 中的群聊 ID
36
+ - `reply_to`:填话题根消息 ID(`om_` 开头),**不要用话题 ID(`omt_` 开头)**
37
+ - `reply_in_thread: true`:确保消息发到话题内而非群聊主线