PMFlow/docs/superpowers/specs/2026-04-17-unified-project-...

817 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Unified Project AI Handoff 成功案例验收包
## 目的
这份文档把 `统一 Project Operating Context + 三路 handoff` 的 12 条黄金场景收成可直接过目的“成功案例包”。
本文聚焦三件事:
1. 每条 case 的最小输入
2. 从哪里入站、经过哪些主链、按什么步骤验
3. 成功时应该看到的结构化输出与最终用户可见输出
## 使用边界
- 本文是“成功目标样例 + 验收步骤”文档,不等于 12 条都已经做完人工业务走读。
- 文中的结构化输出与最终输出,表示该 case 验收成功时应看到的目标结果。
- 自动化证据以 [2026-04-17-feishu-problem-inventory.md](/Users/yuuuu/Documents/AIProjectManger/.worktrees/codex/unified-project-ai-handoff/docs/superpowers/specs/2026-04-17-feishu-problem-inventory.md) 为真源。
- 飞书前台验收默认先看“后端最终出站”:
- 非交互 QA 看 `qa_direct` 文本
- 强交互 requirement / draft 看 action card payload
- 所有 Feishu 场景都必须经过 `feishu_reply_outbox`;如果没有看到 outbox,就不能算飞书链路验收成功。
## 通用验收步骤
每条 case 都按同一顺序验:
1. 准备前置上下文
2. 用真实入站面发送最小输入
3. 先看结构化输出是否命中预期主链
4. 再看最终用户可见输出是否符合合同
5. 记录 pass / fail 与证据
每条 case 至少记录这 6 项:
- 输入
- 入站面
- 主链
- 结构化输出
- 最终输出
- 结果判定
## Feishu 场景统一 Transport 合同
凡是本文中真实入站面包含 Feishu 的 case,除了看业务主链,还必须额外确认 transport 层。
### 非交互 QA 文本
必经链路:
```text
message.receive_v1
-> handle_message_event()
-> _handle_message_receive()
-> capture_from_feishu()
-> _route_scene_reply()
-> _build_feishu_qa_direct_reply_text()
-> _safe_reply_text()
-> FeishuReplyOutboxMixin._enqueue_reply_outbox()
-> feishu_reply_outbox
```
必验点:
- `reply_route=qa_direct`
- `feishu_reply_outbox.message_text` 是纯文本
- 文本里包含成功案例要求的正文 / 当前项目 / 外部判断 / 下一步
### 交互卡片
必经链路:
```text
message.receive_v1 或 card.action.trigger
-> handle_message_event()
-> _handle_message_receive() / _handle_card_action_trigger()
-> _route_scene_reply()
-> _build_action_card_from_capture_result()
-> _safe_reply_card()
-> FeishuReplyOutboxMixin._enqueue_reply_outbox()
-> feishu_reply_outbox
```
必验点:
- `reply_route=qa_action_card` 或对应 card action 路由
- `feishu_reply_outbox.message_text` 以 `__pmflow_feishu_card__:` 开头
- 解码后的 card payload 保留正确 `action` 和 `work_item_id`
---
## Case 01
### 场景
纯内部系统问题
### 前置条件
- 当前在项目页或带 `project_id=17` 的 Web 问答入口
- 无 active requirement-plan / draft confirm 干扰
### 输入
- 用户原话:`当前 PMFlow 里 MCP 模板有几个?`
- 真实入站面:Web `POST /rag/ask`
示例请求:
```json
{
"question": "当前 PMFlow 里 MCP 模板有几个?",
"asked_via": "pmflow_manual",
"project_id": 17,
"action_mode": "read"
}
```
### 详细步骤
1. 从项目页问答入口发起问题。
2. 查看 `/rag/ask` 返回 payload 中的 `route_decision`。
3. 确认 `route_mode=internal`,且没有被转成 external。
4. 查看最终 `answer` 是否以 `内部知识库查询:` 开头。
### 成功结构化输出
```json
{
"route_decision": {
"route_mode": "internal",
"effective_source_policy": "internal"
}
}
```
### 成功最终输出
```text
内部知识库查询:当前 PMFlow 里共有 2 个 MCP 模板。
```
### 成功判定
- 命中 internal 主链
- 不混入外部来源摘要
- 不是泛化聊天式说明
---
## Case 02
### 场景
纯外部事实问题
### 前置条件
- 普通 QA 线程
- 无 active requirement-plan
### 输入
- 用户原话:`OpenAI Docs 最近有没有新的 Responses API 最佳实践?`
- 真实入站面:Web `POST /rag/ask`
示例请求:
```json
{
"question": "OpenAI Docs 最近有没有新的 Responses API 最佳实践?",
"asked_via": "pmflow_manual",
"action_mode": "read"
}
```
### 详细步骤
1. 从 Web QA 入口提交问题。
2. 检查 `route_decision.route_mode` 是否为 `external`。
3. 检查 external 证据是否经过 fail-closed 过滤。
4. 查看最终回答是否保持 `外部实时查询:` 合同。
### 成功结构化输出
```json
{
"route_decision": {
"route_mode": "external",
"effective_source_policy": "external"
}
}
```
### 成功最终输出
```text
外部实时查询:OpenAI Docs 最近补充了新的 Responses API 最佳实践说明。
```
### 成功判定
- 只能走 external 主链
- internal 来源不得混进 external 正文
---
## Case 03
### 场景
先判断,再给 plan 入口
### 前置条件
- 当前项目明确
- repo 摘要可用
- 允许 external lane 构建 `fit_brief`
### 输入
- 用户原话:`OpenAI Docs 这种能力适不适合我们当前项目?`
- 真实入站面:
- Web `POST /rag/ask`
- Feishu `message.receive_v1`
Web 示例请求:
```json
{
"question": "OpenAI Docs 这种能力适不适合我们当前项目?",
"asked_via": "pmflow_manual",
"project_id": 12,
"source_chat_id": "chat-12",
"action_mode": "read"
}
```
Feishu 最小入站:
```json
{
"message_type": "text",
"chat_id": "chat-12",
"sender_open_id": "ou-alpha",
"message_id": "om-case-03",
"content": "OpenAI Docs 这种能力适不适合我们当前项目?"
}
```
### 详细步骤
1. 从 Web 或 Feishu 发问题。
2. 检查 `project_operating_context` 是否带上当前项目。
3. 检查 `execution_recipe.plan_handoff_mode` 是否为 `offer_after_answer`。
4. 检查 `handoff_mode / can_enter_plan / recommended_next_step`。
5. Feishu 侧检查 `_route_scene_reply()` 是否走到 `qa_direct`。
6. Feishu 侧检查 `feishu_reply_outbox.message_text` 是否为最终出站文本。
7. Feishu 侧再看最终 `qa_direct` 文本是否已经变成围绕当前项目实体的自然分析回复,而不是标签摘要。
### 成功结构化输出
```json
{
"project_operating_context": {
"current_project_id": 12,
"fit_scope": "current_project_and_repo",
"decision_strategy": "answer_then_offer_plan"
},
"execution_recipe": {
"primary_lane": "external",
"fit_brief_required": true,
"external_evidence_required": true,
"plan_handoff_mode": "offer_after_answer"
},
"handoff_mode": "offer_after_answer",
"can_enter_plan": true,
"recommended_next_step": "如果你要,我可以基于当前结论进入 plan,继续把需求范围和下一步收清楚。"
}
```
### 成功最终输出
Web:
```text
外部实时查询:目前更像背景相关,建议先验证它是否能映射到当前项目正式对象。
```
Feishu:
```text
外部实时查询:目前更像背景相关,建议先验证它是否能映射到当前项目正式对象。
结合当前项目看,Alpha 是一个 AI 项目管理工作台,当前仓库负责统一项目上下文与 AI handoff。就这次判断来说,这条外部信息目前更像背景相关,还不能直接映射到当前正式对象,所以现在更适合作为参考,而不是直接当成已经命中当前项目的正式变化。
如果你要,我可以基于当前结论进入 plan,继续把需求范围和下一步收清楚。
```
### 成功判定
- 先给判断
- 再给 plan 入口
- 不得首轮直接切到 plan 主链
---
## Case 04
### 场景
显式强锚点直接进 plan
### 前置条件
- 上一轮已经给出结论
- 当前消息显式 reply 到上一轮结论
### 输入
- 用户原话:`那按这个推进`
- 真实入站面:Feishu `message.receive_v1` reply/thread
示例入站:
```json
{
"message_id": "om-case-04",
"reply_to_message_id": "om-case-03-bot",
"chat_id": "chat-12",
"content": "那按这个推进"
}
```
### 详细步骤
1. 用 reply/thread 回复上一条结论消息。
2. 检查 canonical thread context 是否恢复成功。
3. 检查 `execution_recipe.plan_handoff_mode` 是否命中 `explicit_plan_now`,或直接回到 active requirement context。
4. 检查 `_route_scene_reply()` 后是否进入 card 或 follow-up 对应出站路径。
5. 检查最终 Feishu 回包是否通过 `feishu_reply_outbox` 出站。
6. 检查输出是否继续 requirement-plan,而不是普通 QA。
### 成功结构化输出
```json
{
"execution_recipe": {
"plan_handoff_mode": "explicit_plan_now"
}
}
```
### 成功最终输出
```text
AI PMO 需求收敛:我先按这条结论继续收敛需求范围。
```
### 成功判定
- 直接 handoff 到 plan
- 不是重新解释 internal / external 模式
---
## Case 05
### 场景
reply/thread 明确续问
### 前置条件
- 机器人上一条已给出解释性回复
- 当前消息显式 reply 到该条回复
### 输入
- 用户原话:`你刚才这条说的“模型路由”具体指哪层?`
- 真实入站面:Feishu reply/thread 或 QA session follow-up
### 详细步骤
1. 对上一条机器人解释消息做显式回复。
2. 检查 follow-up anchor 是否优先命中 reply anchor。
3. 检查问题是否被重写到上一条回复对象。
4. 确认没有被旧 `last_subject` 抢走。
### 成功结构化输出
```json
{
"anchor_mode": "reply_anchor",
"resolved_anchor": {
"source_message_id": "om-prev-answer"
}
}
```
### 成功最终输出
```text
内部知识库查询:这里说的“模型路由”指的是 PMFlow 本地路由层里的 tier / provider / response contract 决策层。
```
### 成功判定
- 续问对象来自 reply anchor
- 不是把它当新问题重开
---
## Case 06
### 场景
同群同人但切新话题
### 前置条件
- 同一群聊、同一用户
- 上一轮刚结束 requirement-plan
### 输入
- 用户原话:`另外,现在 PMFlow 里 signal source onboarding 做到哪了?`
- 真实入站面:Feishu `message.receive_v1`
### 详细步骤
1. 在同一群里直接发一条新的事实问题。
2. 检查 session inheritance guard 是否拦住旧 requirement-plan 续写。
3. 检查显式 signal-source 请求是否避开 draft/plan 劫持。
4. 检查最终回答是否回到 internal query。
### 成功结构化输出
```json
{
"route_decision": {
"route_mode": "internal"
}
}
```
### 成功最终输出
```text
内部知识库查询:当前 PMFlow 的 signal source onboarding 已支持受控接入与运行时探测。
```
### 成功判定
- 不得继续沿 requirement-plan 解释
- 必须识别为新的 internal query
---
## Case 07
### 场景
旧 active context 劫持新问题
### 前置条件
- 上一轮挂着 `pending_clarification(kind=query_route_selection)`
- 当前用户显式改题
### 输入
- 用户原话:`不是说那个,我现在问 external gateway。`
- 真实入站面:Feishu `message.receive_v1` 或 Web QA session
### 详细步骤
1. 先制造一个 query-route 澄清状态。
2. 再发送这条显式改题消息。
3. 检查 ingress 是否调用 `WorkflowSemanticPlanner` 看 `topic_switch=new`。
4. 检查旧 `pending_clarification` 是否被立即清空。
5. 确认没有继续调用旧 session 的澄清协议消费链。
### 成功结构化输出
```json
{
"pending_clarification": null,
"next_route": "query.answer"
}
```
### 成功最终输出
```text
外部实时查询:external gateway 当前负责统一外部查询计划、provider 分发和结果校验。
```
### 成功判定
- 旧 `pending_clarification` 必须失效
- 当前消息必须作为新 query 进入主链
---
## Case 08
### 场景
明明该继续 plan,却掉回 query.answer
### 前置条件
- active requirement-plan 正在进行
- 上一轮刚发 A/B/C 选项
### 输入
- 用户原话:`我选 C`
- 真实入站面:Feishu `message.receive_v1`
### 详细步骤
1. 先进入 requirement-plan 并发出 A/B/C。
2. 再发送 `我选 C`。
3. 检查 active requirement context 是否命中当前 option map。
4. 确认协议解析直接 bypass 普通 QA semantic gate。
5. 查看最终输出是否继续 requirement-plan follow-up。
### 成功结构化输出
```json
{
"reply_template_key": "requirement_plan_followup",
"parsed_choice": "C"
}
```
### 成功最终输出
```text
C. 混合增强路径
⭐ 当前我更推荐:C
```
### 成功判定
- 不得掉回普通 QA
- 必须继续当前 requirement-plan 主链
---
## Case 09
### 场景
明明只是问结论,却被直接拉进 plan
### 前置条件
- 无 active plan
- 无显式 reply/thread 强锚点
### 输入
- 用户原话:`这个方案适不适合我们现在这套 PMFlow?`
- 真实入站面:Web `POST /rag/ask` 或 Feishu `message.receive_v1`
### 详细步骤
1. 直接发一条适配性判断问题。
2. 检查 `execution_recipe.plan_handoff_mode`。
3. 确认首轮还是 `offer_after_answer`,不是 `explicit_plan_now`。
4. 确认最终输出先给结论,再给下一步。
### 成功结构化输出
```json
{
"execution_recipe": {
"plan_handoff_mode": "offer_after_answer"
},
"recommended_next_step": "如果你要,我可以基于当前结论进入 plan。"
}
```
### 成功最终输出
```text
外部实时查询:目前适合作为背景参考,但还需要验证它与当前 PMFlow 正式对象的映射关系。
```
### 成功判定
- 首轮不能直接进入 plan
- 必须保留“先答结论,再给下一步”
---
## Case 10
### 场景
卡片动作和纯文本回复不一致
### 前置条件
- 同一 requirement thread 同时支持按钮和文本协议
- 当前 active `work_item_id` 已存在
### 输入
- 文本输入:`选 B`
- 卡片输入:点击 `B` 按钮
- 真实入站面:
- 文本:Feishu `message.receive_v1`
- 卡片:Feishu `card.action.trigger`
### 详细步骤
1. 在同一 requirement 轮次里保留当前 action card。
2. 用文本发 `选 B`。
3. 再用卡片点击 `B`。
4. 检查两条路径是否都命中同一 `work_item_id` 和 canonical thread context。
5. 检查文本和卡片两条路径都写入 `feishu_reply_outbox`。
6. 检查最终都继续 requirement-plan。
### 成功结构化输出
```json
{
"work_item_id": 88,
"action": "plan_choice",
"choice": "B"
}
```
### 成功最终输出
```json
{
"header": {
"title": {
"content": "AI PMO 需求收敛"
}
},
"elements": [
{
"tag": "action",
"actions": [
{
"value": {
"action": "plan_choice",
"choice": "B",
"work_item_id": 88
}
}
]
}
]
}
```
### 成功判定
- 文本和按钮都命中同一 requirement thread
- 不能出现“一条续上、一条掉链”
---
## Case 11
### 场景
Web 与 Feishu 对同一句话不同链路
### 前置条件
- 当前项目一致
- Web 与 Feishu 都从同一项目语境发问
### 输入
- 用户原话:`这个适合吗`
- 真实入站面:
- Web `POST /rag/ask`
- Feishu `message.receive_v1`
### 详细步骤
1. 用同一句话分别从 Web 和 Feishu 发起问题。
2. 对比两边的 `project_operating_context`。
3. 对比两边的 `execution_recipe` 和 `handoff_mode`。
4. Web 看结构化 meta。
5. Feishu 检查 `_route_scene_reply()` 是否进入 `qa_direct`,并写入 `feishu_reply_outbox`。
6. Feishu 再看最终 `qa_direct` 文本。
### 成功结构化输出
```json
{
"execution_recipe": {
"plan_handoff_mode": "offer_after_answer"
},
"handoff_mode": "offer_after_answer",
"can_enter_plan": true
}
```
### 成功最终输出
Web:
```text
外部实时查询:目前更像背景相关,还不能直接映射到当前正式对象。
```
Feishu:
```text
外部实时查询:目前更像背景相关,还不能直接映射到当前正式对象。
结合当前项目看,Alpha 是一个 AI 项目管理工作台,当前仓库负责统一项目上下文与 AI handoff。就这次判断来说,这条外部信息目前更像背景相关,还不能直接映射到当前正式对象,所以现在更适合作为参考,而不是直接当成已经命中当前项目的正式变化。
如果你要,我可以基于当前结论进入 plan,继续把需求范围和下一步收清楚。
```
### 成功判定
- 两边都必须是 `offer_after_answer`
- 不能一边进 plan、一边留在普通 QA
---
## Case 12
### 场景
多候选 meta follow-up 需要澄清
### 前置条件
- 最近 5 分钟内有 2 条候选回复
- 当前问题无法唯一锚定某一条
### 输入
- 第一轮:`那你刚才那条是哪个模型回复的?`
- 第二轮:`第二条`
- 真实入站面:Feishu / QA session follow-up
### 详细步骤
1. 先制造两个最近候选回复。
2. 发送第一轮 meta follow-up。
3. 检查是否返回 `ambiguous_recent_reply` 和编号候选。
4. 再发送 `第二条`。
5. 检查是否恢复 `original_question` 并绑定正确 `selected_qa_id`。
### 成功结构化输出
第一轮:
```json
{
"clarification_code": "ambiguous_recent_reply",
"clarification_candidates": [
{"index": 1},
{"index": 2}
]
}
```
第二轮:
```json
{
"selected_qa_id": 102,
"effective_question": "我问你这个是哪个模型执行回复的?"
}
```
### 成功最终输出
第一轮:
```text
我找到两条可能对应的回复:
1. ...
2. ...
请告诉我是第几条。
```
第二轮:
```text
内部知识库查询:你选的是第二条,那条回复由当前 query.answer 主链执行。
```
### 成功判定
- 第一轮必须先澄清
- 第二轮必须支持中文编号恢复原问题
---
## 建议过目顺序
如果你是要先审文档,再决定人工走读顺序,建议按这 4 条先看:
1. Case 03:先判断,再给 plan 入口
2. Case 07:旧澄清失效
3. Case 08:A/B/C 继续 requirement-plan
4. Case 11:Web / Feishu 同句一致
这 4 条最能看出 unified handoff 现在是不是真的站住了。