Skip to content

Commit 2341641

Browse files
bdjasonzjsclaude
andcommitted
feat(p1-c1): 类型契约 + ChatContext.taskType + pending 测试矩阵
P1 commit #1 (spec v0.4.1 §7) — 类型层 + schema 字段铺底,**无产品逻辑改动**。 所有运行时测试都是 `it.todo` 占位,对应 product commit (#4 / deepcoldy#5) 才打开。 ## 类型 / Schema ### src/services/chat-context-store.ts - 新增 `export type TaskType = 'prd' | 'bug' | 'misc'` - `ChatContext.taskType?: TaskType`(向后兼容:旧 JSON 不写此字段; caller 没传 → undefined → JSON.stringify 不写出 → 旧 reader 不知道新字段) - `CreateOpts.taskType?: TaskType` + `create()` body 写入 ### src/services/group-creator.ts - `CreateGroupOpts.chatContext?: { taskType / relatedRefs / activeTodoRefs / rules / participants / parentDigest }` — types-only - 注释明确:(a) participants 由 caller (Playbook) 推导后传入, group-creator 只透传不推导;(b) idempotencyKey 故意不加,幂等主责 完全在 Playbook + spawn-idempotency-store(v0.4 妹妹 #2) - **body 无任何改动**(commit #4 才接 chatContext → dispatchChatCreated) ### src/im/lark/chat-created-handler.ts - `DispatchChatCreatedOpts` 加 rich fields(relatedRefs / activeTodoRefs / rules / parentDigest / taskType)— types-only - **body 无任何改动**(commit deepcoldy#5 才写入 ChatContext) ## 测试 ### test/group-creator-chatcontext.test.ts(新) - 6 tests:C1 真跑(类型 backward compat) + C2/C3/C9/C10/C-PP-pass `it.todo` - C4/C5/C6/C7/C7b/C8(v0.1-v0.3 老 case)按 v0.4 妹妹 #2 已挪到 spawn-idempotency-store / Playbook 测试 ### test/dispatch-chat-created-rich-context.test.ts(新) - 5 tests:1 个类型断言真跑(rich opts 可编译) + D1/D2/D3/D-PP `it.todo` ## 验证 - `pnpm tsc --noEmit` ✅ - focused 3 文件 / 63 pass + 9 todo / 0 fail - 全量 vitest 2579 pass / 6 fail(全部跟 commit #1 无关:3 个老 CLI binary 缺失测试 + 3 个 EMFILE 系统 fd 耗尽,错误堆栈在 `src/workflows/fanout.ts:93` 系统 watch() 直接抛,跟 schema 改动无关) - **向后兼容回归 e2e**(真飞书建群): - `botmux create-group --bot 克劳德 --name "..."` → Lark 真群建成功 - 写出 ChatContext.json 12 个旧字段全在 + `taskType` 字段缺席 (caller 没传 → JSON 不写)= 老 reader 完全感知不到 schema 变化 - dashboard `/api/topology` HTTP 200,新群被收录 (originType=bot_spawned) - 测试群已归档清理 ## 边界遵守(妹妹 review v0.1-v0.4.1 后五版定下) - group-creator 只建群 + dispatch,不知道 idempotency 概念 - Playbook 独占任务模板 + 权限校验 + 幂等(commit deepcoldy#6 实现) - CLI 是 IPC 薄壳(commit deepcoldy#7 实现) - session 真凭证从 session-store 反查,CLI 不能伪造(commit deepcoldy#6/deepcoldy#8 实现) - BOTMUX_SESSION_ID env 由 worker-spawner 唯一注入(commit deepcoldy#8 实现) 下一步: commit #2 (config mainTopicChatId + 同步 ChatTopology.rootChatId) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cbd881c commit 2341641

5 files changed

Lines changed: 157 additions & 0 deletions

File tree

src/im/lark/chat-created-handler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ export interface DispatchChatCreatedOpts {
134134
parentChatId?: string | null;
135135
purpose?: string;
136136
participants?: { openId: string; role: string }[];
137+
/** P1 main-bot mode (spec v0.4.1 §1.3) — richer ChatContext fields
138+
* plumbed through from group-creator / Playbook so the FIRST welcome
139+
* card is complete. dispatchChatCreated body writes these straight
140+
* into ChatContext.create() (chat-context-store has承载位 already).
141+
* Implementation is wired in commit #5, here is types-only (commit #1). */
142+
relatedRefs?: string[];
143+
activeTodoRefs?: string[];
144+
rules?: string[];
145+
parentDigest?: string;
146+
taskType?: 'prd' | 'bug' | 'misc';
137147
}
138148

139149
/**

src/services/chat-context-store.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export interface ChatContextInherited {
3939
* board and skip escalation rule evaluation (R1-R5 / R6). */
4040
export type ChatStatus = 'active' | 'archived';
4141

42+
/** Task category for chats spawned by the main bot via
43+
* `MainBotPlaybook.spawnSubTask`. Drives dashboard filtering + template
44+
* selection (rules/welcome-card wording). undefined for non-task chats
45+
* (p2p / human-created / legacy bot_spawned without a task type). */
46+
export type TaskType = 'prd' | 'bug' | 'misc';
47+
4248
export interface ChatContext {
4349
chatId: string;
4450
/** One-line summary of what this chat is for. */
@@ -62,6 +68,9 @@ export interface ChatContext {
6268
status?: ChatStatus;
6369
/** ISO timestamp when archived (null/undefined when status='active'). */
6470
archivedAt?: string | null;
71+
/** P1: task category — set by MainBotPlaybook.spawnSubTask, used by
72+
* dashboard filter + template logic. undefined for non-task chats. */
73+
taskType?: TaskType;
6574
/** ISO timestamp of last write (for cache invalidation). */
6675
updatedAt: string;
6776
}
@@ -126,6 +135,8 @@ export interface CreateOpts {
126135
activeTodoRefs?: string[];
127136
rules?: string[];
128137
injectionPolicy?: InjectionPolicy;
138+
/** P1: optional task category for MainBotPlaybook-spawned chats. */
139+
taskType?: TaskType;
129140
}
130141

131142
/**
@@ -157,6 +168,7 @@ export function create(chatId: string, opts: CreateOpts): ChatContext {
157168
injectionPolicy: opts.injectionPolicy ?? 'eager',
158169
status: 'active',
159170
archivedAt: null,
171+
taskType: opts.taskType,
160172
updatedAt: new Date().toISOString(),
161173
};
162174
write(ctx);

src/services/group-creator.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,33 @@ export interface CreateGroupOpts {
4343
/** P0/4 main-bot mode: one-line purpose statement. Defaults to a placeholder
4444
* that the L2 缇蕾 scout replaces on its next tick. */
4545
purpose?: string;
46+
/**
47+
* P1 main-bot mode (spec v0.4.1 §1.1) — richer ChatContext fields,
48+
* passed straight through to `dispatchChatCreated()` so the FIRST
49+
* welcome card is complete (no "send empty card, then update" anti-
50+
* pattern). Optional: callers that don't care (legacy `/group`,
51+
* dashboard `build-group`) can omit and get current purpose-only
52+
* behavior.
53+
*
54+
* Note: `participants` here is what caller already resolved
55+
* (MainBotPlaybook is responsible for推导 bot ref → openId+role).
56+
* group-creator does NOT auto-推导 — it只透传给 dispatchChatCreated.
57+
*
58+
* `idempotencyKey` is deliberately absent (v0.4 妹妹 #2): idempotency
59+
* 主责完全在 MainBotPlaybook + spawn-idempotency-store, group-creator
60+
* 不知道幂等概念。
61+
*
62+
* Wiring (commit #4): pass these into `dispatchChatCreated` opts.
63+
* Commit #1 (this commit) is types-only — no body change yet.
64+
*/
65+
chatContext?: {
66+
taskType?: 'prd' | 'bug' | 'misc';
67+
relatedRefs?: string[];
68+
activeTodoRefs?: string[];
69+
rules?: string[];
70+
participants?: Array<{ openId: string; role: string }>;
71+
parentDigest?: string;
72+
};
4673
}
4774

4875
export interface CreateGroupResult {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Test matrix for `DispatchChatCreatedOpts` richer fields persisting into
3+
* ChatContext + welcome card on FIRST dispatch (no "send empty card, then
4+
* update" anti-pattern).
5+
*
6+
* Spec: docs/superpowers/plans/2026-05-24-p1-main-bot-subtask-spawn.md §1.5
7+
*
8+
* **Commit #1 (this commit)** — types-only. All runtime assertions are
9+
* `it.todo` placeholders; opened in commit #5 when dispatchChatCreated
10+
* body actually persists the new fields.
11+
*
12+
* Run: pnpm vitest run test/dispatch-chat-created-rich-context.test.ts
13+
*/
14+
import { describe, it, expect } from 'vitest';
15+
import type { DispatchChatCreatedOpts } from '../src/im/lark/chat-created-handler.js';
16+
17+
describe('dispatchChatCreated: rich context plumbing', () => {
18+
describe('Types — opts contract', () => {
19+
it('DispatchChatCreatedOpts accepts the richer fields (compile-time only)', () => {
20+
const opts: DispatchChatCreatedOpts = {
21+
chatId: 'oc_compile_test',
22+
larkAppId: 'cli_x',
23+
originType: 'bot_spawned',
24+
parentChatId: 'oc_parent',
25+
purpose: 'analyse PRD',
26+
participants: [{ openId: 'ou_a', role: 'main bot' }],
27+
relatedRefs: ['https://wiki/p1'],
28+
activeTodoRefs: ['om_root_msg'],
29+
rules: ['先读 PRD 全文'],
30+
parentDigest: 'parent chat 24h digest text',
31+
taskType: 'prd',
32+
};
33+
// Compile-time success is the assertion. Runtime sanity:
34+
expect(opts.taskType).toBe('prd');
35+
expect(opts.rules?.length).toBe(1);
36+
});
37+
});
38+
39+
describe('D1 — ChatContext.read() after dispatch has all rich fields', () => {
40+
it.todo('dispatch with rich opts persists taskType / rules / relatedRefs / activeTodoRefs / parentDigest into ChatContext');
41+
});
42+
43+
describe('D2 — first welcome card sees rich ctx (mock sendContextCard)', () => {
44+
it.todo('first welcome card render receives ctx containing rules + relatedRefs + activeTodoRefs + participants — not empty defaults');
45+
});
46+
47+
describe('D3 — second dispatch is idempotent (no rewrite/resend)', () => {
48+
it.todo('same chatId second dispatch hits ChatContext.create idempotency, no second card sent');
49+
});
50+
51+
describe('D-PP — three-bot participants array persists fully', () => {
52+
it.todo('participants=三 bot 数组 → ChatContext.participants length=3, all three openIds present');
53+
});
54+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Test matrix for `CreateGroupOpts.chatContext` plumbing.
3+
*
4+
* Spec: docs/superpowers/plans/2026-05-24-p1-main-bot-subtask-spawn.md §1.5
5+
*
6+
* **Commit #1 (this commit)** — types-only, body not yet implemented.
7+
* Most cases are `it.todo` placeholders that real product commits
8+
* (#4 `createGroupWithBots` body, #5 `dispatchChatCreated` body) open up.
9+
* Only C1 (existing-behavior regression) runs for real now.
10+
*
11+
* Run: pnpm vitest run test/group-creator-chatcontext.test.ts
12+
*/
13+
import { describe, it, expect } from 'vitest';
14+
import type { CreateGroupOpts } from '../src/services/group-creator.js';
15+
16+
describe('group-creator: chatContext plumbing', () => {
17+
describe('C1 — backward compat: chatContext omitted', () => {
18+
it('CreateGroupOpts.chatContext is optional in the type', () => {
19+
// Type-level assertion: omitting chatContext compiles.
20+
const opts: CreateGroupOpts = {
21+
creatorLarkAppId: 'cli_x',
22+
larkAppIds: ['cli_y'],
23+
name: 'legacy /group call without chatContext',
24+
};
25+
expect(opts.chatContext).toBeUndefined();
26+
});
27+
});
28+
29+
describe('C2 — chatContext fields plumb to dispatchChatCreated', () => {
30+
it.todo('createGroupWithBots forwards chatContext.taskType/rules/relatedRefs to dispatchChatCreated (spy)');
31+
});
32+
33+
describe('C3 — parentDigest + sourceChatId → ChatContext.inheritedFrom.parentDigest', () => {
34+
it.todo('chatContext.parentDigest with sourceChatId writes both into ChatContext.inheritedFrom');
35+
});
36+
37+
describe('C9 — transferOwnerTo conditional', () => {
38+
it.todo('omitting transferOwnerTo skips the transfer call (no Lark transferChatOwner)');
39+
});
40+
41+
describe('C10 — notifyOwnerOpenId conditional', () => {
42+
it.todo('omitting notifyOwnerOpenId skips the @-mention notify call (no sendMessage to owner)');
43+
});
44+
45+
describe('C-PP-pass — Playbook-resolved participants pass through unchanged', () => {
46+
it.todo('chatContext.participants array (already resolved by caller) is forwarded to dispatchChatCreated verbatim — group-creator does no derivation');
47+
});
48+
49+
// The following cases used to live here in spec v0.1-v0.3, but per spec
50+
// v0.4 they belong to the idempotency-store / Playbook test files
51+
// because group-creator no longer knows about idempotency at all:
52+
// C4 / C5 / C6 / C7 / C7b / C8 → test/spawn-idempotency-store.test.ts
53+
// → test/main-bot-playbook-spawn-subtask.test.ts
54+
});

0 commit comments

Comments
 (0)