pi-multi-viewers 0.8.3 → 0.9.1

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/AGENTS.md CHANGED
@@ -68,8 +68,8 @@ tests/ 测试(unittest discover tests)
68
68
 
69
69
  | 档 | 唤醒命令 | 用途 |
70
70
  |---|---|---|
71
- | **mc-tools**(默认) | 四个 `--no-*` + `-e <MC 的 subagent-entry.js>` | 给 agents **按需检索项目背景**(`ctx_search`)——背景蒸馏机制已移除,这是其补充通道。**允许而非要求 MC**:找不到 MC → 降级为零扩展 + 一行可见说明(`ctx_search` 本次不可用)|
72
- | **none** | 四个 `--no-*` | 零扩展、**零依赖**(无 MC 的机器/CI 用这档)|
71
+ | **mc-tools**(默认) | 四个 `--no-*` + `-e <MC 的 subagent-entry.js>` + `-e <pi-mcp-adapter 入口>` | 给 agents **按需检索**(`ctx_search` 查项目历史、web_search 等查外部)——协议模板里带一句「需要项目历史时用 ctx_search」的条件指引(仅在工具真到位时出现)。**降级/严格/生效值语义见 docs/design.md 决策 20 的语义清单**(唯一权威段) |
72
+ | **none** | 四个 `--no-*` | 零扩展、**零依赖**(无 MC 的机器/CI 用这档) |
73
73
  | **all** | 不加任何 `--no-*`(pi 默认发现)| A/B 实验与显式 opt-in |
74
74
 
75
75
  为什么**不能**用插件全档(`all`):两类插件在**我们这种 session 形态**上都是分钟级负担、
@@ -83,12 +83,11 @@ tests/ 测试(unittest discover tests)
83
83
  **mc-tools 档的实测**:entry **只注册工具、不装 hook** → historian 0/6 ✓(生产 0/3 ✓);
84
84
  `ctx_search` 实测可用 ✓;成本**未测得显著差异**(受控探针 n 小、组内方差>组间差 ✗;
85
85
  生产基线:本场 strict=1、n=19,唤醒启动段中位 **0.68s**、收尾中位 0.04s ✓)。
86
- **入口解析 fail-fast**(`meeting_fs.resolve_mc_tools_entry`:从 pi 的 packages 找 MC 包 →
87
- 读它声明的扩展入口 → 取同目录的 subagent-entry.js);缺 MC 时**可见降级**为零扩展。
88
- **依赖边界**:mc-tools 档**允许而非要求** MC——缺 MC 时降级为零扩展,且**可见**
89
- (打印一行"本次按零扩展运行:ctx_search 不可用";无静默铁律);`none` 档零依赖
90
- (无 MC 的机器/CI 显式选它)。**测试/探针保真**:设 `MV_MC_TOOLS_STRICT=1` →
91
- 缺 MC 即报错退出(否则测试可能在"没装 MC"下通过而 ctx_search 从未生效)。
86
+ **入口解析**:`meeting_fs` 从 pi 的 packages 找包 → 读它自己声明的 `pi.extensions`
87
+ (不硬编码布局)→ `resolve_mc_tools_entry`(取同目录 subagent-entry.js)与
88
+ `resolve_mcp_adapter_entry`(取声明的入口本身)。**降级/严格模式/生效值语义**
89
+ 见 docs/design.md 决策 20 的语义清单——本文件不复述(本周刚付过一次漂移的账)。
90
+
92
91
  **主 pi 完全不受影响**(只改我们 spawn 的 agent 进程命令行;主 pi 的 MC/历史学家照常)。
93
92
 
94
93
  **关键约定**:pi sessions 目录编码 = `--` + 去首尾斜杠内斜杠换 `-` + `--`
package/README.md CHANGED
@@ -1,12 +1,30 @@
1
1
  # pi-multi-viewers
2
2
 
3
- 多视角协同分析(Pi 插件):把主 pi session **fork** 成 N 个视角 agent,
4
- 各带一份视角任务书(效率/简单/铁律/……),在 meeting 协议下交锋、
5
- 修正、收敛,产出一份共识结果。
3
+ **多视角协同分析(Pi 插件)**:把当前 pi 会话 fork 成 N 个视角 agent,让它们带着你的真实上下文互相交锋,产出共识与分歧。
6
4
 
7
- 与 [pi-agents-helper](https://github.com/maxdai/pi-agents-helper)(多方
8
- 讨论达成共识,agent 无主上下文)平行演化;共享 meeting 协议核心
9
- (core/fs/engine),差异在初始化层。
5
+ ## 它想解决什么问题
6
+
7
+ 需要考虑多种因素或者准则时,LLM 容易出现**逐渐忽略其中一部分因素**的问题。
8
+
9
+ 对策是**给每个因素一条独立的会话**:一个视角 agent 只关注一个因素(效率 / 简单 / 铁律 / …),
10
+ 各带一份视角任务书与独立发言配额,并且必须对其它视角的观点表态(认同或反驳,都要用自己的论据)。
11
+ 这样**只要这些会话存在,对应因素就不会被忽略**——关注不靠提醒模型"别忘了 X",
12
+ 而是让每个 X 有一个独立的载体。
13
+
14
+ 讨论由代码驱动的 meeting 协议推进(发言配额 → 冻结 → 轮转表态 → 共识收束),
15
+ 最终产出 `result.md`:**共识结论 + 明确否决项(含理由与重估触发条件)+ 各自保留的分歧**。
16
+
17
+ ## 什么时候值得跑
18
+
19
+ **当一个问题需要长思考、并且需要在长思考过程中保持若干个视角的关注时**,可以尝试使用。
20
+
21
+ (反过来说:查一个事实、跑一条命令、一两分钟能自己确认的问题,直接问 pi 更快。)
22
+
23
+ ## 代价
24
+
25
+ 一次分析通常 **12–35 分钟**(3 视角、20–50 次唤醒,正常情况)。这个区间**不含**异常外溢:
26
+ provider 连续失败或扩展异常时可能显著更久(历史场次里出现过 55 分钟与 73 分钟)。
27
+ 它换来的不是"更快",而是"多几个独立立场 + 一份可复查的分歧记录"。
10
28
 
11
29
  ## 核心机制(2026-09-09/10 实测验证,见 docs/examples/first-experiment)
12
30
 
@@ -67,16 +85,20 @@ ls ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh
67
85
 
68
86
  ## 用法
69
87
 
70
- ```
71
- /multi-viewers-setup # ① 建视角(首次使用先跑这个;prompt:先建议 → 你定 → 落盘 → 给你审)
72
- /multi-viewers "<主题>" # ② 分析(extension:生成 spec → 弹窗门禁 → 启动 → 预填观看命令)
73
- /multi-viewers-say "<文本>" # ③ 插话(分析进行中;extension:零 LLM 直接写入 human 消息)
74
- /multi-viewers-finish # ④ 收尾(extension:查状态 → 确认 → 清理,报告随清理打印)
75
- ```
88
+ **接口总表**(pi 内 1 个 prompt + 3 个命令;终端侧另有等价 CLI):
76
89
 
77
- 四个 pi 命令入口,按使用顺序排列。**②③④ 是 extension**(流程完全由代码执行、
78
- 零 LLM:跑命令、门禁弹窗、观看命令预填、状态判据都走退出码/机器标记行,
79
- 不靠 LLM 转述);**① 是 prompt**——写视角是内容工作,本就需要 LLM 参与。
90
+ | 入口 | 形态 | 作用 |
91
+ |---|---|---|
92
+ | `/multi-viewers-setup` | prompt | 建视角(建议 → 你定 → `--set-viewer` 落盘 → 给你审) |
93
+ | `/multi-viewers "<主题>"` | extension | 分析:prepare → **暂停点弹窗** → start → 交付观看命令 |
94
+ | `/multi-viewers-finish` | extension | 收尾:status → 确认 → cleanup(报告随清理打印并落盘) |
95
+ | `/multi-viewers-say "<文本>"` | extension | 插话(human 消息,各视角可见可回应) |
96
+ | `scripts/mv.sh <子命令>` | CLI | 终端侧等价入口(`--prepare` / `--start` / `--status` / `--view` / `--say` / `--report` / `--wait` / `--cleanup` / `--viewers` / `--set-viewer`)——pi 内命令内部也走它 |
97
+
98
+ 按使用顺序:先 `/multi-viewers-setup` 建视角(一次就够),之后 `/multi-viewers "<主题>"` 跑分析,
99
+ 分析进行中用 `/multi-viewers-say` 插话,结束后 `/multi-viewers-finish` 收尾。
100
+ **后三个是 extension**(流程完全由代码执行、零 LLM:跑命令、门禁弹窗、观看命令交付、状态判据
101
+ 都走退出码/机器标记行,不靠 LLM 转述);**`setup` 是 prompt**——写视角是内容工作,本就需要 LLM 参与。
80
102
 
81
103
  **目录可以省略**:`--view`/`--say`/`--status`/`--report`/`--wait`/`--cleanup`
82
104
  不带目录时自动定位"本 session 当前分析"(只匹配 `mv-<sessionId>-*` 最新;**未匹配
@@ -98,8 +120,9 @@ scripts/mv.sh --start <spec目录> # 启动(自动挂载主 sessi
98
120
  # 可选:--fork-mode compaction|budget|full(见上表;一般不调)
99
121
  # 可选:--max-meeting 15 --max-rr 7 --stall-timeout 600
100
122
  # (配额:建环境时固化进 protocol.json,之后不可改;meeting 配额是"每 agent")
101
- # 可选:--extension-policy mc-tools|none|all(默认 mc-tools = agents 带 MC 的只读检索工具
102
- # ctx_search;none = 零扩展、零依赖;all = 走 pi 默认发现。缺 MC 时 mc-tools 可见降级)
123
+ # 可选:--extension-policy mc-tools|none|all(默认 mc-tools = 零扩展 + 两份只读工具入口:
124
+ # MC 的 ctx_search 与 MCP adapter 的 web 检索等;none = 零扩展、零依赖;
125
+ # all = 走 pi 默认发现。两份入口各自独立,缺谁少谁且可见降级)
103
126
  # 高级:--agents "a,b" 起一次性视角(不建 viewers/ 时用;prompt 入口不传它)
104
127
 
105
128
  # 观看:--start 会输出可直接执行的 !! 流式观看命令(复制执行)
@@ -111,7 +134,7 @@ scripts/mv.sh --view # 一次性增量查看(主 pi
111
134
  # 插话 / 状态 / 收尾(目录可省略——自动定位本 session 当前分析)
112
135
  scripts/mv.sh --say "<文本>" # 插话(命令行形态;pi 内用 /multi-viewers-say)
113
136
  scripts/mv.sh --status # 状态 + 路径(取值与含义以该命令输出为准)
114
- scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM/档位对照;冷路径,不持久化)
137
+ scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM/档位对照;本命令不落盘——cleanup 会留存一份)
115
138
  scripts/mv.sh --cleanup # 收尾(result.md + 报告都留存到 <dir>-*.md/.txt)
116
139
  scripts/mv.sh --viewers # 列出+校验当前项目 viewers/(只读;建视角时用)
117
140
  scripts/mv.sh --set-viewer <名字> # 新建视角文件(正文从 stdin 读;只新建不覆盖)
@@ -156,6 +179,9 @@ frontmatter 字段、写文件路径、独立参与者纪律。
156
179
  | 完全一次性(项目还没建 viewers/) | `mv.sh --prepare "<主题>" --agents "a,b"`(wrapper 高级用法) |
157
180
 
158
181
  ## 架构
182
+ 与 [pi-agents-helper](https://github.com/maxdai/pi-agents-helper)(多方
183
+ 讨论达成共识,agent 无主上下文)平行演化;共享 meeting 协议核心
184
+ (core/fs/engine),差异在初始化层。
159
185
 
160
186
  ```
161
187
  meeting_core.py 纯判定(冻结级联/RR/聚合 + 状态机词汇常量)
package/docs/design.md CHANGED
@@ -495,7 +495,7 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
495
495
 
496
496
  | 档 | 唤醒命令 | 语义 |
497
497
  |---|---|---|
498
- | `mc-tools`(默认) | 四个 `--no-*` + `-e <MC subagent-entry.js>`(找不到 MC 时退化为四个 `--no-*`)| 只要 MC 的**只读检索工具**(`ctx_search`);**允许而非要求** MC |
498
+ | `mc-tools`(默认) | 四个 `--no-*` + `-e <MC subagent-entry.js>` + `-e <pi-mcp-adapter 入口>`(任一份入口缺失即**部分降级**:缺谁少谁、都可见;全缺 = 等价 none)
499
499
  | `none` | 四个 `--no-*` | 零扩展:最快、**零依赖** |
500
500
  | `all` | 不加任何 `--no-*` | pi 默认发现(A/B 与显式 opt-in)|
501
501
 
@@ -517,25 +517,34 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
517
517
  spawn cwd 全截断("无 MC 的机器"上产出残缺命令)。**登记行**:首唤打
518
518
  `扩展策略: 声明=X 生效=Y strict=0|1 [降级原因=…]`——报告据此给"声明 vs 生效"
519
519
  (E2;与"档位"同型),否则降级只在 loop log 里、产品面看不见。
520
-
521
- **依赖边界(用户 2026-09-14 定)**:mc-tools **允许而非要求** MC——缺 MC 时
522
- **降级为零扩展并按 none 运行**,但**必须可见**(打印一行"mc-tools 档未生效
523
- (原因)——本次按零扩展运行:ctx_search 不可用";无静默铁律)。
524
- `none` 零依赖(无 MC 的机器/CI 显式选它)。**死代码纪律**:`--extensions`
525
- 别名与 `extensions: true` 历史字段(只存在约 1 天)**已删净**(无移除条件的
526
- 兼容层不留——同 `--pure` 先例);扩展策略只有一个入口:`--extension-policy`
520
+
521
+ **使用指引(2026-09-25)**:协议模板里有一节「需要项目历史时」告诉 agents何时用 `ctx_search`(并写明"项目文件优先、记忆可能过期")——**它只在工具真会到位时出现**(策略为 mc-tools 且入口可解析),降级/零扩展时整节消失(不留空指引)。起因:e2e25 自然使用观察里三 agent 自发调用 **0 次**;加装指引后需在下一次"任务书不提工具"的场次里复测计数(utility 无客观判据,只做计数 + 抽看)。
522
+ **两份入口(2026-09-25 用户裁决 B)**:`mc-tools` 除 MC 的只读检索工具外,再显式
523
+ `-e` 加载 **pi-mcp-adapter**(web_search / web_reader / zread 等 MCP 工具)。
524
+ **语义清单(唯一权威段,别处引用不复述)**:
525
+ ① 两份入口**各自独立降级**(缺谁少谁)、**都可见**、**允许而非要求**——缺入口
526
+ 不阻断分析(机器上没装其中之一照样能跑);
527
+ ② `MV_MC_TOOLS_STRICT=1`(测试/探针保真)→ **任一**入口缺失即报错退出
528
+ (否则测试可能在"没装某入口"的环境里通过,而该工具从未生效);
529
+ ③ 生效值语义:**部分降级仍 `生效=mc-tools`**(只是少了那份 `-e`),
530
+ 两份全失才 `生效=none`;
531
+ ④ 为什么必须显式 `-e`:`--no-extensions` 关的是**扩展发现**,显式路径照常生效
532
+ (pi `--help` 原文)——不加载就等于 agents 完全失去该能力(MCP 那侧 = 失去
533
+ 联网检索)。
534
+ 不新增档位(保持简单)。`none` 零依赖(无 MC/adapter 的机器/CI 显式选它)。
535
+
536
+ **依赖边界**:见上方清单 ①(允许而非要求、缺谁少谁、都可见)——本段不再复述。
537
+ **死代码纪律**:`--extensions` 别名与 `extensions: true` 历史字段(只存在约 1 天)
538
+ **已删净**(无移除条件的兼容层不留);扩展策略只有一个入口:`--extension-policy`
527
539
  + 协议字段 `extensionPolicy`。
528
- **测试/探针保真开关**:`MV_MC_TOOLS_STRICT=1` → 缺 MC 即报错退出。
529
- 为什么需要它(测试阶段语义):降级虽可见,但"没降级"这件事在测试里必须可断言——
530
- 否则测试可能在"没装 MC"的环境下通过,而 `ctx_search` 从未生效
531
- (测试环境准确性优先;成版行为 = 允许降级)。
532
540
 
533
541
  **主 pi 不受影响**(只改我们 spawn 的 agent 命令行)。
534
542
 
535
- **代价**:agents 用 pi 内置 read/write/edit/bash/grep/glob;无 `ctx_*` 与知识
536
- 注入。零扩展真场里三视角自述:"内置工具胜任本任务、未因缺工具放弃或简化检查
537
- (符号级导航多 2–3 步/文件)"——**自述 ≠ 测量**,但两次真场(e2e20、本场)
538
- 均抓到真问题(F1/S1 等),无质量下降证据。
543
+ **`none` 档的代价**(该档专属,不是默认档的代价):agents 用 pi 内置
544
+ read/write/edit/bash/grep/glob,且**没有** `ctx_*` 检索与 MCP 工具;零扩展真场里
545
+ 三视角自述"内置工具胜任本任务、未因缺工具放弃或简化检查(符号级导航多 2–3
546
+ 步/文件)"——**自述 ≠ 测量**,但两次真场(e2e20、本场)均抓到真问题(F1/S1 等),
547
+ 无质量下降证据。默认档 `mc-tools` 则带两份只读工具入口(见上方清单)。
539
548
 
540
549
  **随此退役的机制**(删净、不留死代码):`KEEP_EXTENSIONS`、
541
550
  `resolve_extension_entries`、`--pure`(语义反转为默认)、以及决策 19 那整套
@@ -558,7 +567,7 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
558
567
  `/multi-viewers-finish` 由 prompt 改为 extension 命令——prepare → **门禁 = 暂停点**
559
568
  (`ui.confirm`:标题点明「暂停中,可在其它窗口修改 spec」,正文带 spec 路径 +
560
569
  文件清单;用户在**其它窗口**编辑该目录,改完点「确认」继续,取消则保留 spec)
561
- → start → **观看命令预填输入框**(`ctx.ui.setEditorText` + notify 各一份,用户按 Enter 即执行);收尾 status →
570
+ → start → **交付观看命令**(四个通道,见下方角色表,用户按 Enter 即执行);收尾 status →
562
571
  确认 → cleanup。**为什么**:prompt 靠 LLM 逐步执行,每步都可能漏(实测:观看
563
572
  命令漏传 2 次、失败判据曾靠读中文报错文本、目录路径曾靠 LLM 记忆);代码执行
564
573
  则天然不遗漏。**与 CLI 的契约 = 机器标记行**(`[prepare] spec=` / `[start] dir=` /
@@ -591,7 +600,7 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
591
600
  |---|---|---|---|
592
601
  | `setEditorText` 预填 | 仅 TUI | 0 | TUI 便利(能直接回车跑) |
593
602
  | `notify` | 全模式 | 0 | 即时反馈(pi-web 关掉弹窗即消失) |
594
- | `pi.sendMessage`(custom_message) | 全模式 | ~百 token(主 session)+ 随 fork 进每场分析 | 持久留痕(pi-web 渲染为折叠块) |
603
+ | `pi.sendMessage`(custom_message) | 全模式 | ~百 token(主 session)+ 随 fork 进每场分析 | 持久留痕(pi-web 渲染为折叠块、需点击;起 0.5.19 实时出现) |
595
604
  | `ctx.ui.setWidget` | 全模式 | 0(纯 UI) | 运行期常驻可见(一眼看到、不需点击) |
596
605
  三条注记:① `sendMessage` 的 custom_message **会随 fork 进每场分析各视角的上下文**
597
606
  (fork 源在首唤由主 session 条目构建,不做类型过滤)——~2 行/场,有界;不为它加
@@ -0,0 +1,161 @@
1
+ <!-- 存档:docs/reviews/2026-09-25-patch-audit-review.md
2
+ 来源:一次真实多视角分析的 result.md 原文(未删改,仅加本头与下方说明)。
3
+ 分析场次目录已随 cleanup 删除;文中消息编号不可再核验,仅作溯源线索
4
+ (与代码注释引用约定一致:行为以自描述为准)。 -->
5
+
6
+ # 存档说明
7
+
8
+ - **主题**:审阅本项目 **0.8.0 → 0.9.0** 这一周的改动(extension 三命令与并流修复、报告落盘、
9
+ `--set-viewer`、默认扩展策略、README 重写)是否存在**补丁堆叠 / 复杂度失配 / 职责边界**问题
10
+ - **场次**:`mv-mv-main-20260925-175841`(3 视角:效率 / 简单 / 铁律;真实 pi 讨论;
11
+ `forkMode=budget`、扩展策略 `mc-tools`(两份入口 ✓、声明=生效)、`maxMeeting=15`;
12
+ 墙钟 20m51s、22 条消息、共识收敛)
13
+ - **判定**:**没有补丁堆叠** ✓ —— 真正减代码的三处(spawn 样板合一、`--viewers`/`--set-viewer`
14
+ 共用校验、删 no-op)方向正确;四通道交付、报告落盘两段 fail-open、②′ 指纹、解析层重构
15
+ 均被判**不违规**(§9 防翻案)
16
+ - **真问题两类**(都是"改了一处、复述没跟上"):
17
+ - **F1(中)文档漂移**:默认策略已改为"两份入口各自独立降级",但 **6 处**复述未同步
18
+ (`AGENTS.md` 4 处、`design.md` 4 处、`README.md` 1 处、测试 docstring 引用**已不存在的
19
+ 旧日志文案**)→ 收敛为「语义清单 = 唯一权威段」+ 各处指针
20
+ - **F2/S3(低-中)名实不符**:`_viewer_set_errors` 自称"唯一组合点 + 数量 ≥2",实测皆不成立
21
+ → 改名 `viewer_entry_errors`(entry vs set)+ docstring 改述,**保留写后复校**
22
+ (删它依赖"无并发"假设,不成立)
23
+ - **F4(低)契约只有注释没有机制**:cleanup 的"不得裸 `print(`" → 立为本仓**首条源码结构断言**
24
+ (AST,找不到函数即红)
25
+ - **S2** 降级日志第二行是复述 → 并入登记行(`降级原因=部分/完全:…`)
26
+ - **S4/F3** 两处"工具在不在"的解析时点 → 维持 + 注释(窗口/后果/重估触发)
27
+ - **本场同时是两项验证**:① "任务书不提工具"的自然使用复测 —— `ctx_search` 自发调用 **0 次**
28
+ (与 e2e25 基线相同;**但本场不能证伪那句指引**:任务是审阅这一周改动,而这段历史**就在
29
+ fork 窗口里**,需要检索的场景根本没出现 —— 正面结果是**没有**引发低价值调用);② 收尾列
30
+ 0s / −4s / −7s ⇒ MCP adapter **没有** AFT 那种尾巴(探针结论在真场复现)
31
+ - **落地**:`6c024be`(481 python + 48 harness 全绿;含 F2 改名、S2 合并、F4 AST 断言、
32
+ F1 文档收敛)。报告另记"明确不做"7 项与 3 项备查(不判违规)
33
+
34
+ # 0.8.0 → 0.9.0 审阅 · 三方共识结果
35
+
36
+ 参与者:效率 / 简单 / 铁律(3 视角,真实 pi 讨论;`forkMode=budget`、`extensionPolicy=mc-tools`、`maxMeeting=15`、`maxRR=7`)。
37
+ 主题:审阅本项目 0.8.0 → 0.9.0 这一周的改动(extension 三命令与并流修复、报告落盘、`--set-viewer`、默认扩展策略、README 重写)是否存在补丁堆叠、复杂度失配或职责边界问题。**只提意见,不改代码。**
38
+
39
+ ## 0. 结论摘要
40
+
41
+ 1. **本批没有补丁堆叠**。真正减代码的三处(`shared.ts` 三处 spawn 样板合一、`--viewers`/`--set-viewer` 共用 `_validate_and_print_viewers`、删三个 `getArgumentCompletions` no-op)方向正确;四通道交付、报告落盘的两段 fail-open、②′ 指纹、解析层重构,以及本批三处修复(效率收下)均被判**不违规**(§9,防翻案)。
42
+ 2. **真正的两类问题是"文档漂移"与"契约比实现大"**,不是实现复杂度:
43
+ - **F1(设计符合度,中)**:默认策略已实现为"两份入口各自独立降级",但文档 **6 处**未同步(含一处测试 docstring 引用**已不存在**的旧日志文案)→ 按"删复述引权威"收敛到单点 + 指针(§2)。
44
+ - **F2/S3(设计符合度,低-中)**:`_viewer_set_errors` 名实不符(docstring 自称"唯一组合点 + 数量 ≥2",实测不查数量、组合点另有 2 处)→ 改名 `viewer_entry_errors` + 改述,函数体最小改法(§3)。
45
+ - **F4(职责边界,低)**:`cleanup_discussion` 的"无裸 `print`"契约只有注释没有机制 → 做 **AST 断言**(方式 1),契约收窄、找不到函数即红(§6)。
46
+ 3. **两项维持现状 + 注释**:S4/F3 两个解析时点(setup 承诺 / wake 实况)保留并写明窗口与重估触发条件(§5);S2 降级日志合并进登记行 + 修 14 空格缩进(§4)。
47
+ 4. **明确不做**:S1 通道文案合一(触碰时提常量、不搬函数、不删 notify);`require_two` 参数;S4 的自条件句;F3 的快照传值;F5 脚本改写(只记录);为 TOCTOU 加锁;AST 方式 2(扫 `sys.stdout`)。
48
+ 5. **一句话**:本批 0.8.0 → 0.9.0 的改动没有补丁堆叠;共识修复全部是"让文档/契约与其唯一实现对齐"的低风险收敛,运行时零变化;另有一条可选的 AST 结构断言(本仓首个源码结构断言类别)。
49
+
50
+ ---
51
+
52
+ ## 1. 审查基准(先钉死)
53
+
54
+ | 项 | 事实 |
55
+ |---|---|
56
+ | 区间 | `v0.8.0..v0.9.0` = **16 提交**、24 文件、+1513 / −269 行 |
57
+ | 审查树 | **HEAD `80a34b5`** = `v0.9.0`(`0ca8a24`)+ 1 提交「协议加『需要项目历史时』一节」——讨论全程 HEAD 未变 |
58
+ | 主题点名五项 | extension 三命令(0.8.0 主体 `dfe71cc`;本区间修复 `69a415a`/`e92eacc`/`0b28256`);报告落盘(`e06bceb`);`--set-viewer`(`e06bceb`);默认扩展策略(`d55793d`);README 重写(`bb1ec92`/`f4b67e1`) |
59
+ | 测试口径 | 当前树:479 python + 48 harness 断言;F4 的增量账按全量 314s 折算 |
60
+ | 场次数字 | 本场(175841)与上场(153113)两份报告的 per-wake「启动 / 收尾」列用于 E1 对比(§10) |
61
+
62
+ ## 2. F1 文档漂移:默认策略降级语义改了,复述没跟(设计符合度,中)
63
+
64
+ ### 2.1 事实(已核实)
65
+
66
+ - 实现:`meeting_loop.py:336–361` 两份入口**各自**解析与降级;`:355–359` strict = **任一**入口缺失即 raise;`:364–374` 登记行 + 降级日志为"部分/完全未生效…"。
67
+ - 旧日志文案(`本次按零扩展运行:ctx_search 不可用`)**在实现中已不存在**(grep 0 命中)。
68
+ - 未同步处(同一批 `d55793d` 表行/新增段改了、相邻句没改):
69
+ - `AGENTS.md:71`(表行尾"找不到 MC → 降级为零扩展 + 一行可见说明")、`:87`("缺 MC 时可见降级为零扩展")、`:89`(引用旧文案)、`:91`("缺 MC 即报错退出");
70
+ - `docs/design.md:512–513`、`:525–526`("降级为零扩展并按 none 运行"+ 旧文案)、`:531`("缺 MC 即报错")、`:564`("setEditorText + notify 各一份",与同节 `:593–599` 四通道表矛盾);
71
+ - `README.md:137`(`--report`"冷路径,**不持久化**",与下一行 cleanup"报告都留存"打架);
72
+ - `tests/test_meeting_loop.py:857–861`(docstring"缺 MC → 降级为零扩展"+ 旧文案引用;**resultWriter 收尾补查新增**,属"全仓删净"同一动作)。
73
+
74
+ ### 2.2 收敛方案(三方一致)
75
+
76
+ 1. **先补权威段**(`design.md` 决策 20「两份入口」段)为**四行清单**,缺条可见:
77
+ ① 两份入口各自独立降级(缺谁少谁)+ 都可见 + **允许而非要求**(缺入口不阻断分析,不可丢);
78
+ ② `MV_MC_TOOLS_STRICT=1` → **任一**入口缺失即报错退出;
79
+ ③ 生效值语义:部分降级仍 `生效=mc-tools`(只是少了 `-e`),全失才 `none`;
80
+ ④ 为什么必须显式 `-e`(`--no-extensions` 关的是发现,显式路径照常生效)。
81
+ 2. **四行齐全 ⇒ 才可删相邻复述段**;`AGENTS.md:71/87/89/91` 压成一句 + 指路("语义见 design.md 决策 20")——反复成本从 6 处降到 1 处(本周已实付一次漂移)。
82
+ 3. 旧日志文案**全仓删净**(含 `tests/test_meeting_loop.py:861`)。
83
+ 4. `README.md:137` 改"本命令不落盘(cleanup 会留存一份)";`design.md:564` 删括注(四通道表是唯一列表处)。
84
+ 5. **验收**:旧文案全仓 grep 0 + 四行清单齐全;**docs-only,不重跑全量测试**(效率账)。排除项:`spec_gen.py:345` 与 `meeting_loop` 同实现,不是漂移点。
85
+
86
+ ## 3. F2/S3 `_viewer_set_errors` 名实不符(设计符合度,低-中)
87
+
88
+ ### 3.1 事实(可执行复现)
89
+
90
+ - `spec_gen.py:495` docstring:"集合级校验的**唯一组合点**:整组名字 + 空正文 + **数量 ≥2**"。
91
+ - 实测:`_viewer_set_errors(['甲'], [])` → `None`(**不报数量**);`viewer_set_error(['甲'], [])` → 报"至少需要 2 个";`spec_gen.py:506` 的 `if empty else None` 使 gap 分支在该组合内**不可达**。
92
+ - "唯一"不成立:另有 `spec_gen.py:550`(`_snapshot_viewers`)与 `start_discussion.py:244` 两处组合。
93
+ - 风险:后来者按文档当全量校验用 → 静默漏 ≥2;两处调用点(`mv_cli.py:189/228`)恰好不需要 ≥2,所以当前无错。
94
+
95
+ ### 3.2 收敛(三方一致)
96
+
97
+ - 改名 **`viewer_entry_errors`**(`entry` vs `set` 正编码"条目级 vs 集合级"),**去掉前导下划线**(它已被 CLI 跨模块当稳定契约用);`_discover_viewers` 的下划线属既有债,**不夹带**。
98
+ - docstring 改为:"名字 + 空正文;≥2 由启动路径单独判;CLI 允许单视角是合法中间状态"。
99
+ - 函数体**最小改法**:保留 `if empty` 保护与组合,不动 `viewer_set_error`、不拆新函数。
100
+ - **不采纳** `require_two` 参数方案(会把组合复制回两个调用点;提案方已自行撤回)。
101
+ - **保留写后复校**:`cmd_set_viewer` 顺序 = 写前校验 → `sys.stdin.read()`(交互下可阻塞任意长)→ 写 → 写后复校;删写后复校依赖"无并发"假设,不成立(`viewers/` 是本产品明确可边跑边改的目录);"绝无半成功"严格说只覆盖**静态**情形,并发窗口属不可避免 TOCTOU(不加锁)。
102
+
103
+ ## 4. S2 降级日志合并(共识)
104
+
105
+ - `meeting_loop.py:364–374`:登记行已含 `生效=` + `降级原因=`(原因逐入口点名);第二行 + 两个三元表达式是复述。
106
+ - 改法:把"**部分/完全**"标进登记行原因字段(如 `降级原因=部分:…`),**删第二行**;顺带修 `:371` 的 14 空格缩进(同级语句应 12)。
107
+ - 解析面:`observability.py:644–645` 的 regex 捕获行尾 → 契约行格式不变、解析不受影响;效率账:只在降级场打印(正常场 0 次),运行时差 = 0(**不计作效率项**)。
108
+
109
+ ## 5. S4/F3 两个解析时点(维持 + 注释)
110
+
111
+ - `spec_gen.py:344–349`(setup 决定 prompt 是否插「需要项目历史时」节)与 `meeting_loop.py:336–361`(wake 实况)各自回答"工具在不在";窗口 = `--prepare` … `--start` 分离路径(默认 extension 流程背靠背,秒级)。
112
+ - 裁决:**维持**现状;在 `spec_gen` 注释补三句:这是**第二次解析**、窗口与后果(承诺可能失真,保守方向无害)、重估触发条件(出现第三份入口 / prompt 需点名第二个工具 → 入口表单点化回 `meeting_fs`)。
113
+ - **拒绝**自条件句(把可用性判断推给 agent 试错,与"不下空指令"口径冲突;且"只在工具真到位时出现"是用户 2026-09-25 的决定)。
114
+ - **拒绝**快照传值(省 <100ms/场,却引入"快照过期"新失效面;最坏失真 = 3 agent × 1 次失败调用,有界)。
115
+
116
+ ## 6. F4 AST 断言(职责边界,低)——做
117
+
118
+ - 现状:`start_discussion.py:436–437` 写"(该函数内不得出现裸 `print(`,**可 grep 校验**)"——是契约,无机制;`rmtree` 必达的真实保证是 `:504` 的 `finally`(docstring 的归因需改准:print 包装保证的是"显示失败不上抛 ⇒ rc=0、后续段不跳过")。
119
+ - **做,方式 1(三方一致)**:
120
+ - 契约收窄为"**不得直接调用 `print(`**"(与断言同宽,不假装锁全;方式 2 扫 `sys.stdout` 不做——已核实三个函数体内 `sys.*` 属性访问 0 处,属理论缺口);
121
+ - 通过条件 = **找到 `cleanup_discussion` ∧ 其 AST 子树内无 `Call(func=Name('print'))`**;
122
+ - **找不到函数即红**(改名/移动后退化成"永远绿的假契约"比没有更糟);
123
+ - 残差(报告段若提成助手须扩范围)写进测试注释;`:437` 删"可 grep 校验"、指向测试文件(文件级指针,避免测试改名即腐烂)。
124
+ - 账(效率核):一次性 ~15 min、每轮增量 <30ms(占全量 314s 的 <0.01%)、**O(1) 于发射点数**(行为用例是 O(发射点数),而"以后还会加发射点"是已知趋势)。
125
+ - 备注:本仓**首个源码结构断言**(`tests/` 无先例,已 grep);与函数名耦合 = 可接受的显式成本;行为用例(display-failure 等)保留为互补,不替代;`main`/`setup_environment` 另有裸 print(已核实),模块级禁令不可行,函数级限定是唯一合理形态。
126
+
127
+ ## 7. S1 通道文案(不做)
128
+
129
+ - `index.ts:98–124` 四通道各有**有意差异**(notify"TUI 下已预填"、widget 插话/收尾引导、setEditorText 纯命令、sendMessage 带 dir),每通道已被 harness 锁定含 WATCH(`tests/extension_harness.ts:253–275`);"四处说同一件事"不是应然。
130
+ - 裁决:**不做 `deliverWatch` 合一**(运行时差 0;把 4 个副作用藏进一个函数反增间接层、调用点看不出发了几个通道);若将来触碰这批文案,把 2–3 个共享片段提成常量即可。
131
+ - **不删 notify**:UI 调用成本 ≈0,属产品取舍(未决);不以"效率"为由删。
132
+
133
+ ## 8. F5 archive 脚本与备查项
134
+
135
+ - `scripts/archive-result.sh`:**记录、不催改**。若未来触碰,收益 = python 化后测试可直接 `import`、去掉"复制脚本到临时仓库"装置(`tests/test_archive_script.py:21–31` 的成因:脚本自推仓库根,曾把两份存档写进真仓库)。
136
+ - **备查(不判违规、本轮不改)**:`cleanup_discussion` 三层 try + `lines is not None`(可压平,可选);`shared.ts` 的 `{rc}`/`{ok}` 二返回形状(收益极小);`mv_cli` 复用 `spec_gen._discover_viewers`(既有命名债,本次不夹带)。
137
+
138
+ ## 9. 明确不判违规(防翻案)
139
+
140
+ 1. **四通道交付**(`index.ts:98–124`,清面板 `:194`):客户端能力不对称下的**最小并集**——无分支、三条 0 上下文成本、一条有界(`custom_message` 随 fork 进每场分析 ~2 行,已记录 `design.md:599`)。重估条件:通告文本增长 / 出现第二类 `custom_message`。
141
+ 2. **报告落盘 + 两段 fail-open**:显示层失败不上抛;产物留存失败**允许**阻断(唯一例外,产物权威位在待删目录内);`rmtree` 在 `finally`(`start_discussion.py:504`)。
142
+ 3. **②′ 指纹**(`run_tests.sh:70` 清指纹、`:95` 仅 rc==0 写回):归一为 rc 单一判据 → "指纹存在 ⇒ 最近同源真跑全绿"成立,无残留假绿路径。
143
+ 4. **解析层重构**:`_packages_of`/`_declared_extensions` + 两个 resolver,无第二份路径推理;`spec_gen` 复用同一 resolver(v0.9.0 的正确方向)。
144
+ 5. **本批三处修复 + 报告落盘**(效率收下):消掉"残留目录/rc≠0"、"指纹假绿"、"半成功写入"三类人工成本;报告以 <5ms/场换掉"重跑 20–40 分钟取数"。
145
+
146
+ ## 10. 效率账与两条收尾测量(不阻塞结论)
147
+
148
+ - 量级:上场 28m42s 中 provider error 38 次 ≈ 5m17s ≈ 18% 墙钟;本轮所有改动合计 <1% 量级。**效率优先级(非本项目标):provider 重试成因 > 唤醒次数 > 其余。**
149
+ - **E1(零成本)**:上场(153113,单入口)vs 本场(175841,双入口)的 per-wake「启动 / 收尾」中位对比——两场唯一唤醒路径差异 = 第二份 `-e` ⇒ 得 MCP 入口的每唤醒固定成本上界(×46 次)。探针此前只测到"含 3–5 次搜索往返 16–23s、**收尾 0.0s**"。
150
+ - **E2(本场即首场)**:`ctx_search` 指引(`80a34b5`)的复测——数三 agent 自发调用次数(基线 e2e25 = 0;e2e24 的 9 次全是任务书点名诱导)+ 是否改变结论。**止损阈值**:调用数 ≥3 且决定性帮助 = 0,或出现 1 次因过期记忆导致的错误结论 ⇒ **回滚**(~10 min:删一节 + 改 3 条断言)。
151
+
152
+ ## 11. 收敛过程(消息索引)
153
+
154
+ - **自由讨论**:简单/0001 复杂度盘点(S1–S4 + 5 项不必改)→ 铁律/0001 三铁律五项发现(F1–F5 + 5 项不判违规)并回应 S1–S4 → 简单/0002 撤回 `require_two`、接受 S4 反对、建议 AST → 铁律/0002 修正 S1 测试前提(四通道各有 WATCH 断言、差异有意)+ 反对删写后复校 → 简单/0003 两处撤回(R1/R2)+ S1 降级 → 铁律/0003 命名去消费者 / 权威段完整性 / AST 新类别 → 简单/0004 命名定稿 `viewer_entry_errors` +"找不到即红" → 铁律/0004 F4 落地口径 → 简单/0005 删"可 grep 校验"+ R2 教训只进 result → 铁律/0005 通过条件与前置顺序确认 → 双方 freezing。
155
+ - **效率裁决(效率/0001)**:F1 单点收敛(排第一批、docs-only 不跑全量)、F4 做(方式 1)、S1 不做、S2 非效率项、S3/S4 维持或最小改;附 E1/E2 与止损阈值。
156
+ - **RR 表态**:效率/0004 → 简单/0009 → 铁律/0008,三份均 `pass`,无异议——共识闭合。
157
+ - **方法论留档(只进本 result,不进设计文档)**:判定某段逻辑"不必要"时,必须先声明它依赖的假设(本次实例 = 写后复校依赖"无并发",该假设不成立)。
158
+
159
+ ## 12. 一句话结论
160
+
161
+ **本批 0.8.0 → 0.9.0 的改动没有补丁堆叠;真正的问题是"改了一处、复述没跟上"的文档漂移(F1)与三处"契约/文档说得比实现大"(F2/S3、F4 的"可 grep 校验")——共识修复全部是让文档与契约和其唯一实现对齐的低风险收敛(外加一条 AST 结构断言),运行时零变化。**
@@ -38,6 +38,7 @@
38
38
  | `2026-09-25-multi-viewers-extension-review.md` | 把 `/multi-viewers` 改成 extension 这次的实现(extension 代码 / CLI 机器标记行契约 / 文档同步) | **P1:`stalled` 被当成 running → 让用户等一个永不到来的收尾**(唯一行为错误);P2 `[result]` 只在 done 打印;P3 头注释与暂停点自相矛盾;P4 `/root/pi-multi-viewers` 单机死回退;P5 两扩展逐字重复 ≈35 行且已漂移(→ 合并为一单元三命令);P6 取消提示缺 sid 提醒 + **扩展消费端零仓库内测试**;首用另暴露:主题带引号、观看命令只有预填一个出口 | 本批(合并 + P1–P6 + 首用两项 + harness 进仓库) |
39
39
  | `2026-09-25-multi-viewers-postfix-review.md` | 复验 0.8.0 的 extension 合并与 P1–P6(含 harness 覆盖审查) | **P1–P6 逐条到位、合并净简化**;新抓 **漏 A:`run_tests.sh --reuse` 的错误成功信号**(harness 失败仍算绿 → 命中旧绿 + exit 0,修法 ②′ 清指纹 + rc==0 才写回);D2 通知里的不实断言(pi-web 忽略 `setEditorText`);B1/B2 契约前缀与不可执行出路;7 类现存分支零覆盖 + sid 注入与 percent-encoding 两装置缺口;D1/D3 文档漂移;S1–S3 简化 | `69a415a`(+ `e92eacc` 第三交付出口) |
40
40
  | `2026-09-25-extension-mechanisms-review.md` | 复验 0.8.2 三处机制(报告落盘 / `--set-viewer` / 观看命令通道)+ 测试覆盖与断言强度 | **① 显示层失败会跳过清理**(BrokenPipe 逃逸 → rmtree 被跳过、目录残留;修法 `_print_best_effort` + rmtree 进 finally ⇒ 清理必达);**② `--set-viewer` 半成功**(校验在写之后 → rc≠0 但文件已写入;改 B′ 校验前移);③ 通道模型由「三出口」收敛为四通道角色表,并证实 custom_message 随 fork 进每场上下文;文档三处「不持久化」复述、断言偏弱、prompt 复述、fail-open 宽窄不对称 | `51a4535` |
41
+ | `2026-09-25-patch-audit-review.md` | 审阅 0.8.0 → 0.9.0 一周改动是否有补丁堆叠 / 复杂度失配 / 职责边界问题 | **判定:没有补丁堆叠**;真问题是**文档漂移 F1**(两份入口降级语义改了、6 处复述没跟)、**名实不符 F2/S3**(`_viewer_set_errors` 自称唯一组合点+数量≥2,皆不成立)、**契约只有注释 F4**(cleanup 裸 print 禁令 → 本仓首条 AST 结构断言);顺带:自然使用复测 `ctx_search` 0 次(不可证伪那句指引)、MCP adapter 无收尾尾巴 | `6c024be` |
41
42
 
42
43
  ## 环境口径(读报告时的背景)
43
44
 
@@ -20,7 +20,8 @@
20
20
  * ① `ctx.ui.setEditorText` 预填输入框——TUI 便利(能直接回车跑);pi-web 忽略
21
21
  * ② `notify`——即时反馈;pi-web 上关掉弹窗即消失
22
22
  * ③ `pi.sendMessage`(custom_message)——**持久留痕**,跨重启仍在;但 pi-web 渲染为
23
- * **折叠的** `multi-viewers (click to expand)`,且随 fork 进入每场分析上下文
23
+ * **折叠的** `multi-viewers (click to expand)`,需点击展开(pi-web 0.5.19 起**实时出现**,
24
+ * 此前只在重载后可见);且随 fork 进入每场分析上下文
24
25
  * ④ `ctx.ui.setWidget`——**运行期常驻可见**(一眼看到、不需点击;MC 待办用的同一通道)
25
26
  */
26
27
 
@@ -104,8 +105,8 @@ export default function register(pi: any) {
104
105
  content: `多视角分析已启动:${dir}\n观看命令(复制执行,不进 LLM):\n${watch}`,
105
106
  display: true,
106
107
  });
107
- // 常驻面板:pi-web 实测把 custom_message 渲染成折叠的 `multi-viewers (click to expand)`,
108
- // 且历史条目未必实时刷新 → 观看命令还需要一条**一眼可见、不需点击**的常驻出口。
108
+ // 常驻面板:custom_message 在 pi-web 是**折叠块**(要点击才展开;0.5.19 起实时出现)
109
+ // → 观看命令还需要一条**一眼可见、不需点击**的常驻出口。
109
110
  // setWidget 正是这个语义(pi-web 注释:"Persistent widget panel … Not a popup";
110
111
  // 主 pi 的 magic-context 待办面板用的就是它)。
111
112
  ctx.ui.setWidget("multi-viewers", [
package/meeting_fs.py CHANGED
@@ -126,32 +126,24 @@ def _package_dir(source, agent_dir):
126
126
  return cand if os.path.isdir(cand) else None
127
127
 
128
128
 
129
- def resolve_mc_tools_entry(agent_dir=None):
130
- """解析 MC 的**只读工具入口**(`dist/subagent-entry.js`)——"mc-tools" 档用。
131
-
132
- 为什么这样解析而不硬编码路径:MC 自己就是用"主入口的**兄弟文件**"
133
- (其源码 `resolveSiblingEntryPath("subagent-entry.js")`)定位它。我们的
134
- 等价做法 = 从 pi 的注册表(settings.json.packages)找到 MC 包目录 → 读它
135
- package.json 声明的扩展入口(`pi.extensions[0]`)→ 取同目录下的
136
- subagent-entry.js。
129
+ def _packages_of(pkg_name, agent_dir=None):
130
+ """在 pi 的 packages 里找声明了 `pkg_name` 的包:返回 (candidates, err)。
137
131
 
138
- **失败语义**:任一步缺失返回 `(None, 原因)`——**由调用方按策略决定**:
139
- loop 在生产态做**可见降级**(打印一行说明后按零扩展运行 ✓ 无静默),
140
- 在严格态(`MV_MC_TOOLS_STRICT=1`,测试/探针保真)直接报错。
132
+ candidates = [(source, pkg_dir)],按注册顺序(可能有多个候选——包名相同但
133
+ 来源不同);err = 读注册表失败的原因(此时 candidates 为空)。
141
134
 
142
- 依赖边界(决策 20):mc-tools **允许而非要求** MC——缺 MC 即降级为零扩展;
143
- `none` 档零依赖(无 MC 的机器/CI 显式选它)。
135
+ **匹配按裸包名精确相等**(F5):不能用子串——`in` 会把
136
+ `@cortexkit/pi-magic-context-legacy` 也命中。npm 源直接比裸名;路径源读
137
+ 它的 package.json.name。**遍历全部候选**(F6:首个匹配不可解析时继续看
138
+ 后面的候选,否则"装着也报没装",文案误导)。
144
139
  """
145
140
  agent_dir = agent_dir or pi_agent_dir()
146
141
  try:
147
142
  with open(os.path.join(agent_dir, "settings.json"), encoding="utf-8") as f:
148
143
  pkgs = json.load(f).get("packages") or []
149
144
  except (OSError, ValueError) as e:
150
- return None, f"读不到 pi 的 packages({agent_dir}/settings.json): {e}"
151
- # F5:按**裸包名精确相等**识别(npm 源直接比;路径源读其 package.json.name);
152
- # F6:遍历**全部**候选、取首个可解析(此前首个匹配失败即停,装着 MC 也会
153
- # 报"没装",文案误导)
154
- seen = [] # 候选(source 字符串)——用于错误文案,说明"试过哪些"
145
+ return [], f"读不到 pi 的 packages({agent_dir}/settings.json): {e}"
146
+ out = []
155
147
  for entry in pkgs:
156
148
  source = _entry_source(entry)
157
149
  if not source:
@@ -167,32 +159,80 @@ def resolve_mc_tools_entry(agent_dir=None):
167
159
  name = json.load(f).get("name") or ""
168
160
  except (OSError, ValueError):
169
161
  name = ""
170
- if name != MC_PACKAGE:
171
- continue
172
- seen.append(source)
162
+ if name == pkg_name:
163
+ out.append((source, pkg_dir))
164
+ return out, ""
165
+
166
+
167
+ def _declared_extensions(pkg_name, agent_dir=None):
168
+ """包名 → (pkg_dir, exts, err):包目录 + 它自己声明的扩展入口(已归一为列表)。
169
+
170
+ exts 来自包 package.json 的 `pi.extensions`(判据**由上游声明**,不硬编码布局);
171
+ 字符串形态归一为单元素列表(F7:直接取首字符会解析出错误路径)。
172
+ """
173
+ cands, err = _packages_of(pkg_name, agent_dir)
174
+ if err:
175
+ return None, [], err
176
+ if not cands:
177
+ return None, [], f"packages 里没有 {pkg_name}(pi install npm:{pkg_name})"
178
+ last = ""
179
+ for source, pkg_dir in cands:
173
180
  try:
174
181
  with open(os.path.join(pkg_dir, "package.json"), encoding="utf-8") as f:
175
182
  man = json.load(f)
176
183
  except (OSError, ValueError) as e:
177
- return None, f"读不到 {MC_PACKAGE} 的 package.json: {e}"
184
+ last = f"读不到 {pkg_name} 的 package.json: {e}"
185
+ continue
178
186
  exts = (man.get("pi") or {}).get("extensions")
179
- if isinstance(exts, str): # F7:字符串形态(取首字符会解析错路径)
187
+ if isinstance(exts, str):
180
188
  exts = [exts]
181
189
  if not isinstance(exts, list) or not exts or not all(
182
190
  isinstance(x, str) and x for x in exts):
183
- return None, f"{MC_PACKAGE} 的 pi.extensions 形态不可用(版本不兼容?)"
184
- cand = os.path.normpath(
185
- os.path.join(pkg_dir, os.path.dirname(exts[0]), "subagent-entry.js"))
186
- if os.path.isfile(cand):
187
- return cand, ""
188
- # 该候选不可解析 → 继续看后面的候选(F6)
189
- last_missing = os.path.relpath(cand, pkg_dir)
190
- if seen:
191
- return None, (f"{MC_PACKAGE} 的只读工具入口不存在({last_missing})"
192
- f"——上游版本可能改了布局")
193
- return None, (f"packages 里没有可解析的 {MC_PACKAGE}——装它"
194
- f"(pi install npm:{MC_PACKAGE}),或改用零扩展档:"
195
- f"--extension-policy none")
191
+ last = f"{pkg_name} 的 pi.extensions 形态不可用(版本不兼容?)"
192
+ continue
193
+ return pkg_dir, exts, ""
194
+ return None, [], (last or f"{pkg_name} 的扩展入口不可用")
195
+
196
+
197
+ def resolve_mc_tools_entry(agent_dir=None):
198
+ """解析 MC 的**只读工具入口**(`dist/subagent-entry.js`)——"mc-tools" 档用。
199
+
200
+ 路径推理:MC 自己就是用"主入口的**兄弟文件**"(其源码
201
+ `resolveSiblingEntryPath("subagent-entry.js")`)定位它;我们等价地读它声明的
202
+ 扩展入口(`pi.extensions[0]`),再取同目录下的 subagent-entry.js。
203
+
204
+ 失败语义(与 resolve_mcp_adapter_entry 同):任一步缺失返回 `(None, 原因)`
205
+ ——**由调用方按策略决定**:loop 生产态做**可见降级**,严格态
206
+ (`MV_MC_TOOLS_STRICT=1`)直接报错。
207
+ """
208
+ pkg_dir, exts, err = _declared_extensions(MC_PACKAGE, agent_dir)
209
+ if err:
210
+ return None, err
211
+ cand = os.path.normpath(
212
+ os.path.join(pkg_dir, os.path.dirname(exts[0]), "subagent-entry.js"))
213
+ if os.path.isfile(cand):
214
+ return cand, ""
215
+ return None, (f"{MC_PACKAGE} 的只读工具入口不存在"
216
+ f"({os.path.relpath(cand, pkg_dir)})——上游版本可能改了布局")
217
+
218
+
219
+ def resolve_mcp_adapter_entry(agent_dir=None):
220
+ """解析 MCP adapter 的扩展入口(它声明的 `pi.extensions[0]`)——mc-tools 第二份。
221
+
222
+ 为什么需要:MCP 工具(web_search / web_reader / zread…)由 pi-mcp-adapter
223
+ 提供,而 `--no-extensions` 关掉的是**扩展发现**——显式 `-e` 路径照常生效
224
+ (pi --help 原文)。不显式加载 = agents 完全没有联网检索能力。
225
+
226
+ 与 MC 的差别:这里要的**就是主入口本身**(它注册 MCP 工具),不取兄弟文件。
227
+ """
228
+ pkg_dir, exts, err = _declared_extensions(MCP_ADAPTER_PACKAGE, agent_dir)
229
+ if err:
230
+ return None, err
231
+ cand = os.path.normpath(os.path.join(pkg_dir, exts[0]))
232
+ if os.path.isfile(cand):
233
+ return cand, ""
234
+ return None, (f"{MCP_ADAPTER_PACKAGE} 声明的入口不存在"
235
+ f"({os.path.relpath(cand, pkg_dir)})")
196
236
 
197
237
 
198
238
  def pi_agent_dir():
@@ -752,7 +792,7 @@ def parse_log_nameonly(output):
752
792
  # agents 需要主项目背景(背景蒸馏机制已移除),这是它的补充通道;
753
793
  # 该入口**只注册工具、不装 hook** → historian/压缩不在其中
754
794
  # (受控实测 historian 0/6、ctx_search 可用;成本未测得显著差异)。
755
- # 代价:本档**允许而非要求** MC(缺则可见降级为零扩展;严格模式见
795
+ # 代价:本档两份入口**允许而非要求**(缺谁少谁、都可见降级;严格模式见
756
796
  # MC_TOOLS_STRICT_ENV)
757
797
  # none : 零扩展——最快、**零依赖**(不依赖任何扩展;无 MC 的机器/CI 用这档)
758
798
  # all : 走 pi 默认扩展发现(A/B 实验与显式 opt-in 用)
@@ -760,7 +800,8 @@ def parse_log_nameonly(output):
760
800
  # 默认档看 DEFAULT_EXTENSION_POLICY)
761
801
  EXTENSION_POLICIES = ("mc-tools", "none", "all")
762
802
  DEFAULT_EXTENSION_POLICY = "mc-tools"
763
- # mc-tools 档**允许**(而非要求)MC:找不到 MC 时降级为零扩展,但必须**可见**
803
+ # mc-tools 档**允许而非要求**两份入口(MC 的 ctx_search、MCP adapter 的 web 工具):
804
+ # 缺谁少谁、都必须**可见**;语义清单见 docs/design.md 决策 20
764
805
  # (打印一行说明 `ctx_search` 本次不可用)——无静默铁律。
765
806
  # 测试/探针要保真(确认"本场确实带着 ctx_search 在跑")时,用环境变量把它变严格:
766
807
  # MV_MC_TOOLS_STRICT=1 → 解析失败即报错退出(测试环境准确性优先,用户 2026-09-14 定)
@@ -778,6 +819,10 @@ def mc_tools_strict():
778
819
  # 入口是它的内部文件,路径解析见 resolve_mc_tools_entry 的 docstring)
779
820
  MC_PACKAGE = "@cortexkit/pi-magic-context"
780
821
 
822
+ # MCP 工具(web_search / web_reader / zread…)的提供者——mc-tools 档的第二份入口。
823
+ # 名字**由 pi 的注册表给**(settings.json.packages),这里只做精确匹配用。
824
+ MCP_ADAPTER_PACKAGE = "pi-mcp-adapter"
825
+
781
826
  FORK_MODES = ("budget", "compaction", "full")
782
827
  DEFAULT_FORK_MODE = "budget"
783
828
  #
package/meeting_loop.py CHANGED
@@ -330,29 +330,47 @@ def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
330
330
  if extension_policy == "none":
331
331
  cmd += no_ext
332
332
  elif extension_policy == "mc-tools":
333
- entry, err = meeting_fs.resolve_mc_tools_entry()
334
- if entry:
335
- cmd += no_ext + ["-e", entry]
336
- elif meeting_fs.mc_tools_strict():
337
- # 严格模式(测试/探针保真):缺 MC 即响,不降级——否则测试可能在
338
- # "没装 MC"的环境里通过,而 ctx_search 从未生效
339
- log(agent, f"[fatal] mc-tools 档入口解析失败(严格模式):{err}")
340
- raise RuntimeError(f"mc-tools 档不可用: {err}")
341
- else:
342
- # mc-tools **允许**而非要求 MC:缺 MC → 降级零扩展,但**可见**
343
- effective_policy = "none"
344
- downgrade_reason = err
345
- cmd += no_ext
333
+ # mc-tools = 零扩展 + 显式加载**两份只读工具入口**(2026-09-25 用户裁定 B:
334
+ # 并入默认档,不再新增档位——保持简单):
335
+ # ① MC 的 subagent-entry(只注册工具、不装 hook)→ ctx_search
336
+ # ② MCP adapter(web_search / web_reader / zread 等 MCP 工具)
337
+ # 为什么必须显式 -e:`--no-extensions` 关的是**发现**,显式路径照常生效
338
+ # (pi --help 原文);MCP 工具此前因发现被关而对 agents 完全不可用。
339
+ # 两份入口**各自独立**降级(允许而非要求)——缺哪个就少哪个,都**可见**。
340
+ resolved = [] # [(label, entry)]
341
+ missing = [] # [(label, err)]
342
+ for label, resolver in (
343
+ ("ctx_search(MC 只读检索)", meeting_fs.resolve_mc_tools_entry),
344
+ ("MCP 工具(web_search 等)", meeting_fs.resolve_mcp_adapter_entry)):
345
+ entry, err = resolver()
346
+ if entry:
347
+ resolved.append((label, entry))
348
+ else:
349
+ missing.append((label, err))
350
+ cmd += no_ext
351
+ for _label, entry in resolved:
352
+ cmd += ["-e", entry]
353
+ if missing:
354
+ reason = ";".join(f"{label} 不可用({err})" for label, err in missing)
355
+ if meeting_fs.mc_tools_strict():
356
+ # 严格模式(测试/探针保真):缺入口即响,不降级——否则测试可能在
357
+ # "没装某入口"的环境里通过,而该工具从未生效
358
+ log(agent, f"[fatal] mc-tools 档入口解析失败(严格模式):{reason}")
359
+ raise RuntimeError(f"mc-tools 档不可用: {reason}")
360
+ # 允许而非要求:缺入口 → 少一份 -e,但**可见**
361
+ downgrade_reason = reason
362
+ effective_policy = ("mc-tools" if resolved else "none")
346
363
  # "all":不加任何 --no-*(走 pi 默认发现)
347
364
  if first_wake:
348
365
  # 登记行(观测面的稳定字段;报告据此给"声明 vs 生效")。只在首唤打:
349
366
  # 策略在一次运行内不变,变了也是配置错误(重跑即可)。
350
- log(agent, f"扩展策略: 声明={extension_policy} 生效={effective_policy}"
351
- f" strict={int(meeting_fs.mc_tools_strict())}"
352
- + (f" 降级原因={downgrade_reason}" if downgrade_reason else ""))
367
+ # 降级时把"部分/完全"标进原因字段(S2:第二行是复述,已删——
368
+ # 报告只解析本行,`observability._report_extension_line` 的 regex 匹配行尾)。
369
+ reason = ""
353
370
  if downgrade_reason:
354
- log(agent, f"mc-tools 档未生效({downgrade_reason})——本次按零扩展"
355
- f"运行:ctx_search 不可用")
371
+ reason = f" 降级原因={'部分' if resolved else '完全'}:{downgrade_reason}"
372
+ log(agent, f"扩展策略: 声明={extension_policy} 生效={effective_policy}"
373
+ f" strict={int(meeting_fs.mc_tools_strict())}{reason}")
356
374
  model = cfg.get("model") or ""
357
375
  if model:
358
376
  cmd += ["--model", model]
package/mv_cli.py CHANGED
@@ -186,7 +186,7 @@ def _validate_and_print_viewers(vdir):
186
186
  notes.append("空:没有视角内容")
187
187
  suffix = f"({';'.join(notes)})" if notes else ""
188
188
  print(f" {n}.md{suffix}")
189
- err = spec_gen._viewer_set_errors(names, empty)
189
+ err = spec_gen.viewer_entry_errors(names, empty)
190
190
  if err:
191
191
  fail_verbatim(err)
192
192
  gap = spec_gen.viewers_count_gap(names)
@@ -225,7 +225,7 @@ def cmd_set_viewer(args):
225
225
  # (不守卫会让 validate_participants(None) 抛 TypeError——首次建视角即命中)。
226
226
  names, _briefs, empty = spec_gen._discover_viewers(vdir)
227
227
  if names:
228
- err = spec_gen._viewer_set_errors(names, empty)
228
+ err = spec_gen.viewer_entry_errors(names, empty)
229
229
  if err:
230
230
  fail_verbatim(f"{err}\n(修正 viewers/ 后再建新视角——本次未写入任何文件)")
231
231
  body = sys.stdin.read().strip()
package/observability.py CHANGED
@@ -655,7 +655,7 @@ def _report_extension_line(base, out):
655
655
  line += f",降级:{reason.strip()}" if reason else ""
656
656
  line += ")"
657
657
  if d != e:
658
- line += " ⚠ 生效≠声明(降级:ctx_search 不可用)"
658
+ line += " ⚠ 生效≠声明(降级:部分工具不可用——见登记行原因)"
659
659
  out.append(line)
660
660
  else:
661
661
  out.append(f"扩展策略:声明 {declared} | 生效 n/a(日志中无登记行)")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "description": "Multi-perspective analysis for Pi: fork the main session into N perspective agents over the meeting protocol.",
5
5
  "type": "module",
6
6
  "private": false,
package/spec_gen.py CHANGED
@@ -311,7 +311,7 @@ def gen_spec_skeleton(spec_dir, participants, topic=None, background=None,
311
311
 
312
312
 
313
313
  def gen_agents_md(args, agent, participants, spec_background=None,
314
- main_pi_cwd=None):
314
+ main_pi_cwd=None, extension_policy=None):
315
315
  """meeting 协议 AGENTS.md(共享协议 + background;身份/立场在 agent
316
316
  定义/question.md)。
317
317
 
@@ -338,6 +338,23 @@ def gen_agents_md(args, agent, participants, spec_background=None,
338
338
  f"查找:如有需要可查看相关文件以获取\n比本背景更详细的信息。\n")
339
339
  else:
340
340
  cwd_section = ""
341
+ # **这是「工具在不在」的第二次解析**(第一次在 setup 这里、第二次在 wake 实况
342
+ # meeting_loop);窗口 = `--prepare` … `--start` 分离路径(默认 extension 流程
343
+ # 背靠背,秒级)。两者不一致的后果:prompt 承诺了而 wake 没给(agent 一次失败
344
+ # 调用,有界无害)或反之(少一句指引)。**重估触发**:出现第三份入口,或 prompt
345
+ # 需点名第二个工具 → 把入口表提成 `meeting_fs` 的单点再消费。
346
+ # 历史检索节:**只在工具真的会到位时**才出现(2026-09-25 用户定)——
347
+ # 否则 agents 会去找一个不存在的工具("无静默/不下空指令")。判据 = 策略允许
348
+ # (mc-tools)**且**入口可解析(与 meeting_loop 的解析同一实现,不各写一套)。
349
+ policy = extension_policy or meeting_fs.DEFAULT_EXTENSION_POLICY
350
+ if policy == "mc-tools" and meeting_fs.resolve_mc_tools_entry()[0]:
351
+ history_section = (
352
+ "\n## 需要项目历史时\n\n"
353
+ "你的上下文来自发起分析的会话,覆盖不到更早的决策与实测记录。这类问题可以用\n"
354
+ "`ctx_search` 检索本项目的历史记忆。项目文件(代码/文档)优先——记忆可能落后于\n"
355
+ "代码,冲突时以文件为准。\n")
356
+ else:
357
+ history_section = ""
341
358
  out = tpl.format(
342
359
  AGENT_NAME=agent,
343
360
  N=str(len(participants)),
@@ -345,6 +362,7 @@ def gen_agents_md(args, agent, participants, spec_background=None,
345
362
  SAMPLE_OTHER=sample,
346
363
  BACKGROUND=background,
347
364
  MAIN_PI_CWD_SECTION=cwd_section,
365
+ HISTORY_SECTION=history_section,
348
366
  )
349
367
  return out
350
368
 
@@ -478,14 +496,18 @@ def viewer_set_error(names, empty, where="viewers/"):
478
496
  return f"错误: {gap}" if gap else None
479
497
 
480
498
 
481
- def _viewer_set_errors(names, empty, where="viewers/"):
482
- """集合级校验的**唯一组合点**:整组名字 + 空正文 + 数量 ≥2。
499
+ def viewer_entry_errors(names, empty, where="viewers/"):
500
+ """**条目级**校验的组合入口:整组名字 + 空正文(**不查数量**)。
483
501
 
484
- 为什么单独存在:`--viewers`(只读检查)与 `--set-viewer`(写前校验)必须
485
- 用**同一套**判据——同一套规则曾在两处漂移过(文案与检查项不一致)。
486
- `if empty` 是必要保护:否则 viewers/ 里只有一个**合法**视角时,
487
- `viewer_set_error` 会因为数量不足而报错,把"建第 2 个视角"判成非法。
488
- `names` 为空(目录缺失/无 .md)返回 None——那是"还没有视角",不是错误。
502
+ 为什么改名(2026-09-25 评审批 F2/S3):原名 `_viewer_set_errors` 的 docstring 自称
503
+ "集合级校验的唯一组合点 + 数量 ≥2",实测两件都不成立——它不查数量(`if empty`
504
+ 保护 + `viewer_set_error` 只在有空正文时才顺带报数量,那条分支在本组合内不可达),
505
+ 而且 `_snapshot_viewers` 与 `start` 路径各有自己的组合。**名实不符的风险**是后来者
506
+ 按文档当全量校验用 → 静默漏掉 ≥2。
507
+ 现在名字只说它做的事:`entry`(条目级)而非 `set`(集合级);**≥2 由启动路径单独判**
508
+ (`viewer_set_error` / `viewers_count_gap`),CLI 允许单视角是合法中间状态。
509
+ 去掉前导下划线:它已被 `mv_cli` 跨模块当稳定契约用(`_discover_viewers` 的下划线
510
+ 属既有的命名债,本批不夹带)。
489
511
  """
490
512
  if not names:
491
513
  return None
@@ -391,7 +391,8 @@ def setup_environment(args, participants, base, spec_dir=None,
391
391
  workdir = os.path.join(base, f"work-{p}")
392
392
  with open(os.path.join(workdir, "AGENTS.md"), "w") as f:
393
393
  f.write(gen_agents_md(args, p, participants, spec_background,
394
- main_pi_cwd=os.getcwd()))
394
+ main_pi_cwd=os.getcwd(),
395
+ extension_policy=args.extension_policy))
395
396
  mv = models[p] # 归一后必有条目(见上方归一循环)
396
397
  with open(os.path.join(workdir, ".pi/agent", f"{p}.md"), "w") as f:
397
398
  f.write(gen_agent_def(p, participants, {p: mv[0]} if mv[0] else None,
@@ -429,12 +430,16 @@ def _print_best_effort(*args, **kwargs):
429
430
 
430
431
  为什么需要:`cleanup_discussion` 的输出可能在管道关闭时抛
431
432
  `BrokenPipeError`(`| head`、终端断开、CI 截断)——实测复现:异常从 print
432
- 逃逸 → **`rmtree` 被跳过**,目录残留且 rc≠0,即"该清理的没清理"
433
- (2026-09-25 评审批 ①(c))。显示层从来不是主职责,失败只能被忽略。
434
-
435
- 契约:`cleanup_discussion` 的**全部 stdout 都走本函数**(该函数内不得出现
436
- 裸 `print(`,可 grep 校验);于是不变量成立——**rmtree 必达**,唯一例外
437
- 是产物留存真失败(那在 `_preserve_result_md` 里冒泡,见其注释)。
433
+ 逃逸 → **报告段之后的一切被跳过**(含删目录),目录残留且 rc≠0,即"该清理的
434
+ 没清理"(2026-09-25 评审批 ①(c))。显示层从来不是主职责,失败只能被忽略。
435
+
436
+ 契约:`cleanup_discussion` 的**全部 stdout 都走本函数**——具体是"该函数 AST
437
+ 子树内不得直接 `Call(Name('print'))`",由 `tests/test_cleanup_contract.py`
438
+ 断言(找不到该函数即红;本批起本仓有源码结构断言这一类别)。
439
+ 本函数保证的是**显示层失败不上抛**(⇒ rc=0、后续段不跳过);**"删目录必达"
440
+ 的真正保证是 `cleanup_discussion` 里的 `try/finally`**——两者别混为一谈
441
+ (原 docstring 把 rmtree 必达归因到本函数,归因错了)。
442
+ 唯一允许阻断删除的失败 = 产物留存真失败(`_preserve_result_md` 里冒泡)。
438
443
  """
439
444
  try:
440
445
  print(*args, **kwargs)
@@ -603,7 +608,7 @@ def main():
603
608
  choices=list(meeting_fs.EXTENSION_POLICIES),
604
609
  default=meeting_fs.DEFAULT_EXTENSION_POLICY,
605
610
  help="agents 的扩展策略:mc-tools=默认,只要 MC 的只读检索工具 "
606
- "ctx_search(缺 MC 时可见降级为零扩展);none=零扩展(零依赖);"
611
+ "ctx_search + MCP 工具(缺谁少谁、可见降级);none=零扩展(零依赖);"
607
612
  "all=走 pi 默认发现")
608
613
  parser.add_argument("--start", action="store_true", help="创建后启动讨论")
609
614
  parser.add_argument("--skip-setup", action="store_true",
@@ -5,6 +5,7 @@
5
5
 
6
6
  ## 背景
7
7
  {BACKGROUND}
8
+ {HISTORY_SECTION}
8
9
 
9
10
  ## 你被唤醒时做什么
10
11