pi-multi-viewers 0.9.0 → 0.10.0

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
@@ -30,10 +30,11 @@ scripts/pi-probe.sh LLM 探针(跑 pi + 登记新 session → 残留检查器
30
30
  scripts/check-residue.sh 残留检查(session/进程/目录三类;增删 scripts/ 时同步本节)
31
31
  scripts/archive-result.sh 归档 result.md(机械部分:逐字复制+校验/存档头骨架/索引行/删源副本)
32
32
  mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers/set-viewer)
33
- extensions/multi-viewers/ 【单一扩展单元】index.ts = 三命令 + shared.ts = 助手
33
+ extensions/multi-viewers/ 【单一扩展单元】index.ts = 四命令 + shared.ts = 助手
34
34
  /multi-viewers 分析入口(prepare→暂停点弹窗→start→预填+打印观看命令)
35
35
  /multi-viewers-finish 收尾(status→确认→cleanup)
36
36
  /multi-viewers-say 插话(零 LLM,直接 spawn human_sayer.py)
37
+ /multi-viewers-config 启动参数默认值(零 LLM,调 --set-default)
37
38
  ⚠ extensions/ 平级禁放 .ts 助手(加载器会把平级文件当独立扩展)
38
39
  prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→**mv.sh --set-viewer** 落盘→给审)
39
40
  # 那四条本来写进 prompt 的纪律(命名/不覆盖/非空/校验)改由命令保证
@@ -68,8 +69,8 @@ tests/ 测试(unittest discover tests)
68
69
 
69
70
  | 档 | 唤醒命令 | 用途 |
70
71
  |---|---|---|
71
- | **mc-tools**(默认) | 四个 `--no-*` + `-e <MC 的 subagent-entry.js>` + `-e <pi-mcp-adapter 入口>` | 给 agents **按需检索项目背景**(`ctx_search`)——背景蒸馏机制已移除,这是其补充通道。**允许而非要求 MC**:找不到 MC → 降级为零扩展 + 一行可见说明(`ctx_search` 本次不可用)|
72
- | **none** | 四个 `--no-*` | 零扩展、**零依赖**(无 MC 的机器/CI 用这档)|
72
+ | **mc-tools**(默认) | 四个 `--no-*` + `-e <MC 的 subagent-entry.js>` + `-e <pi-mcp-adapter 入口>` | 给 agents **按需检索**(`ctx_search` 查项目历史、web_search 等查外部)——协议模板里带一句「需要项目历史时用 ctx_search」的条件指引(仅在工具真到位时出现)。**降级/严格/生效值语义见 docs/design.md 决策 20 的语义清单**(唯一权威段) |
73
+ | **none** | 四个 `--no-*` | 零扩展、**零依赖**(无 MC 的机器/CI 用这档) |
73
74
  | **all** | 不加任何 `--no-*`(pi 默认发现)| A/B 实验与显式 opt-in |
74
75
 
75
76
  为什么**不能**用插件全档(`all`):两类插件在**我们这种 session 形态**上都是分钟级负担、
@@ -83,12 +84,11 @@ tests/ 测试(unittest discover tests)
83
84
  **mc-tools 档的实测**:entry **只注册工具、不装 hook** → historian 0/6 ✓(生产 0/3 ✓);
84
85
  `ctx_search` 实测可用 ✓;成本**未测得显著差异**(受控探针 n 小、组内方差>组间差 ✗;
85
86
  生产基线:本场 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 的 ctx_search、MCP adapter 的 web_search 等)——**允许而非要求**,缺谁少谁、都**可见**
89
- (打印一行"本次按零扩展运行:ctx_search 不可用";无静默铁律);`none` 档零依赖
90
- (无 MC 的机器/CI 显式选它)。**测试/探针保真**:设 `MV_MC_TOOLS_STRICT=1` →
91
- 缺 MC 即报错退出(否则测试可能在"没装 MC"下通过而 ctx_search 从未生效)。
87
+ **入口解析**:`meeting_fs` 从 pi 的 packages 找包 → 读它自己声明的 `pi.extensions`
88
+ (不硬编码布局)→ `resolve_mc_tools_entry`(取同目录 subagent-entry.js)与
89
+ `resolve_mcp_adapter_entry`(取声明的入口本身)。**降级/严格模式/生效值语义**
90
+ 见 docs/design.md 决策 20 的语义清单——本文件不复述(本周刚付过一次漂移的账)。
91
+
92
92
  **主 pi 完全不受影响**(只改我们 spawn 的 agent 进程命令行;主 pi 的 MC/历史学家照常)。
93
93
 
94
94
  **关键约定**:pi sessions 目录编码 = `--` + 去首尾斜杠内斜杠换 `-` + `--`
@@ -235,11 +235,11 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
235
235
 
236
236
  ## 安装/发版状态(2026-09-11)
237
237
 
238
- - **当前形态**:prompt × 1(multi-viewers-setup 建视角)+ extension × 1(一单元注册三命令)
239
- ——一个扩展单元内注册三命令(`multi-viewers` 分析 / `multi-viewers-finish` 收尾 /
240
- `multi-viewers-say` 插话;前两个靠 CLI 机器标记行取值 `[prepare] spec=` /
241
- `[start] dir=` / `[start] watch=` / `[status]`,后者零 LLM 直接 spawn human_sayer.py;
242
- 目录发现 = `<cwd>/mv-<sessionId>-*` 最新——**无兜底**:未匹配即报错 rc 1)+ wrapper。
238
+ - **当前形态**:prompt × 1(multi-viewers-setup 建视角)+ extension × 1(一单元注册四命令)
239
+ ——一个扩展单元内注册四命令(`multi-viewers` 分析 / `multi-viewers-finish` 收尾 /
240
+ `multi-viewers-say` 插话 / `multi-viewers-config` 启动参数默认值);前两个靠 CLI
241
+ 机器标记行取值(`[prepare] spec=` / `[start] dir=` / `[start] watch=` / `[status]`),
242
+ say 与 config 零 LLM 直接转发给 `human_sayer.py` / `mv_cli`;
243
243
  扩展层测试:`tests/extension_harness.ts`(真扩展代码 + 假 python3 装置,零 LLM;
244
244
  `run_tests.sh` 自动带上——缺 bun 可见跳过、`MV_REQUIRE_BUN=1` 严格)。
245
245
  **npm 已发布**(版本以 `package.json` / registry 为准)。
@@ -261,7 +261,9 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
261
261
  - **核验法**(照上游约定,不用命令行长度判断):
262
262
  `readlink -f ~/.pi/agent/npm/node_modules/pi-multi-viewers` 指向仓库根,
263
263
  且该路径下 `scripts/mv.sh` 存在。
264
- - **registry 核验别只看 `/latest`**(实测踩两次):npm 的 abbreviated 元数据
265
- (`registry.npmjs.org/<pkg>/latest`)有缓存,发布后可能持续返回旧版本;
266
- **权威判据 = 完整文档的 `dist-tags`**:
267
- `curl -s https://registry.npmjs.org/pi-multi-viewers | python3 -c "import json,sys;print(json.load(sys.stdin)['dist-tags'])"`
264
+ - **registry 传播有滞后,别把「读不到」当「没发成功」**(实测三次不同的滞后形态):
265
+ `/latest`(abbreviated)可能持续返回旧版本;完整文档的 `dist-tags` 也可能滞后;
266
+ 本次 0.9.1 **两个读端点都还是旧值**(版本专属端点一度 404),而发布其实已被接受。
267
+ **权威判据 = 本地发布日志**:`~/.npm/_logs/<最新>-debug-0.log` 里 `PUT https://registry.npmjs.org/<pkg> 202`
268
+ + `exit 0` + `info ok`(被接受);随后再用**版本专属端点**确认已可读:
269
+ `curl -s -o /dev/null -w '%{http_code}\n' https://registry.npmjs.org/<pkg>/<version>`(200 = 已上架)
package/README.md CHANGED
@@ -93,6 +93,7 @@ ls ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh
93
93
  | `/multi-viewers "<主题>"` | extension | 分析:prepare → **暂停点弹窗** → start → 交付观看命令 |
94
94
  | `/multi-viewers-finish` | extension | 收尾:status → 确认 → cleanup(报告随清理打印并落盘) |
95
95
  | `/multi-viewers-say "<文本>"` | extension | 插话(human 消息,各视角可见可回应) |
96
+ | `/multi-viewers-config [<键> <值>]` | extension | 查看/修改**启动参数默认值**(零 LLM) |
96
97
  | `scripts/mv.sh <子命令>` | CLI | 终端侧等价入口(`--prepare` / `--start` / `--status` / `--view` / `--say` / `--report` / `--wait` / `--cleanup` / `--viewers` / `--set-viewer`)——pi 内命令内部也走它 |
97
98
 
98
99
  按使用顺序:先 `/multi-viewers-setup` 建视角(一次就够),之后 `/multi-viewers "<主题>"` 跑分析,
@@ -134,12 +135,30 @@ scripts/mv.sh --view # 一次性增量查看(主 pi
134
135
  # 插话 / 状态 / 收尾(目录可省略——自动定位本 session 当前分析)
135
136
  scripts/mv.sh --say "<文本>" # 插话(命令行形态;pi 内用 /multi-viewers-say)
136
137
  scripts/mv.sh --status # 状态 + 路径(取值与含义以该命令输出为准)
137
- scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM/档位对照;冷路径,不持久化)
138
+ scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM/档位对照;本命令不落盘——cleanup 会留存一份)
138
139
  scripts/mv.sh --cleanup # 收尾(result.md + 报告都留存到 <dir>-*.md/.txt)
139
140
  scripts/mv.sh --viewers # 列出+校验当前项目 viewers/(只读;建视角时用)
140
141
  scripts/mv.sh --set-viewer <名字> # 新建视角文件(正文从 stdin 读;只新建不覆盖)
142
+ scripts/mv.sh --set-default [<键> <值>] # 启动参数默认值(无参数=查看)
141
143
  ```
142
144
 
145
+ ## 启动参数与默认值
146
+
147
+ 配额这类参数可以在**跑之前**设成默认值,之后每次生成 spec 都会沿用:
148
+
149
+ ```
150
+ /multi-viewers-config max-meeting 20 # 设默认值(等价 mv.sh --set-default max-meeting 20)
151
+ /multi-viewers-config # 查看当前默认值(哪些来自配置文件、哪些是内置)
152
+ ```
153
+
154
+ **取值优先级**(后者覆盖前者):内置默认 → 你设的默认值 → `spec/startup.md`
155
+ (每次分析生成,**你能看也能改** = 只影响本轮)→ `--start` 的显式 flag(临时覆盖一次)。
156
+ 生效值与来源在启动时打印;运行期唯一权威始终是分析环境里的 `protocol.json`
157
+ (loop 每轮只读它,中途不可改)。
158
+
159
+ 可设的键:`max-meeting`(meeting 阶段每 agent 发言配额,默认 15)、
160
+ `max-rr`(RR 轮次配额,默认 7)、`stall-timeout`(无进展超时秒数,默认 600)。
161
+
143
162
  ## 视角文件写什么(`viewers/<视角名>.md`)
144
163
 
145
164
  建视角**推荐**走 `/multi-viewers-setup`(交互式:先给候选建议 → 你定建哪几个 →
package/docs/design.md CHANGED
@@ -517,26 +517,34 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
517
517
  spawn cwd 全截断("无 MC 的机器"上产出残缺命令)。**登记行**:首唤打
518
518
  `扩展策略: 声明=X 生效=Y strict=0|1 [降级原因=…]`——报告据此给"声明 vs 生效"
519
519
  (E2;与"档位"同型),否则降级只在 loop log 里、产品面看不见。
520
- **两份入口(2026-09-25 用户裁决 B)**:`mc-tools` 除 MC 的只读检索工具外,再显式 `-e` 加载 **pi-mcp-adapter**(web_search / web_reader / zread 等 MCP 工具)——`--no-extensions` 关的是**扩展发现**,显式路径照常生效(pi `--help` 原文),不加载就等于 agents 完全失去联网检索能力。两份入口**各自独立降级**(缺谁少谁、都可见),不新增档位(保持简单)。
521
-
522
- **依赖边界(用户 2026-09-14 定)**:mc-tools **允许而非要求** MC——缺 MC 时
523
- **降级为零扩展并按 none 运行**,但**必须可见**(打印一行"mc-tools 档未生效
524
- (原因)——本次按零扩展运行:ctx_search 不可用";无静默铁律)。
525
- `none` 零依赖(无 MC 的机器/CI 显式选它)。**死代码纪律**:`--extensions`
526
- 别名与 `extensions: true` 历史字段(只存在约 1 天)**已删净**(无移除条件的
527
- 兼容层不留——同 `--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`
528
539
  + 协议字段 `extensionPolicy`。
529
- **测试/探针保真开关**:`MV_MC_TOOLS_STRICT=1` → 缺 MC 即报错退出。
530
- 为什么需要它(测试阶段语义):降级虽可见,但"没降级"这件事在测试里必须可断言——
531
- 否则测试可能在"没装 MC"的环境下通过,而 `ctx_search` 从未生效
532
- (测试环境准确性优先;成版行为 = 允许降级)。
533
540
 
534
541
  **主 pi 不受影响**(只改我们 spawn 的 agent 命令行)。
535
542
 
536
- **代价**:agents 用 pi 内置 read/write/edit/bash/grep/glob;无 `ctx_*` 与知识
537
- 注入。零扩展真场里三视角自述:"内置工具胜任本任务、未因缺工具放弃或简化检查
538
- (符号级导航多 2–3 步/文件)"——**自述 ≠ 测量**,但两次真场(e2e20、本场)
539
- 均抓到真问题(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` 则带两份只读工具入口(见上方清单)。
540
548
 
541
549
  **随此退役的机制**(删净、不留死代码):`KEEP_EXTENSIONS`、
542
550
  `resolve_extension_entries`、`--pure`(语义反转为默认)、以及决策 19 那整套
@@ -559,7 +567,7 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
559
567
  `/multi-viewers-finish` 由 prompt 改为 extension 命令——prepare → **门禁 = 暂停点**
560
568
  (`ui.confirm`:标题点明「暂停中,可在其它窗口修改 spec」,正文带 spec 路径 +
561
569
  文件清单;用户在**其它窗口**编辑该目录,改完点「确认」继续,取消则保留 spec)
562
- → start → **观看命令预填输入框**(`ctx.ui.setEditorText` + notify 各一份,用户按 Enter 即执行);收尾 status →
570
+ → start → **交付观看命令**(四个通道,见下方角色表,用户按 Enter 即执行);收尾 status →
563
571
  确认 → cleanup。**为什么**:prompt 靠 LLM 逐步执行,每步都可能漏(实测:观看
564
572
  命令漏传 2 次、失败判据曾靠读中文报错文本、目录路径曾靠 LLM 记忆);代码执行
565
573
  则天然不遗漏。**与 CLI 的契约 = 机器标记行**(`[prepare] spec=` / `[start] dir=` /
@@ -631,6 +639,22 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
631
639
 
632
640
  ---
633
641
 
642
+
643
+ 23. **启动参数默认值走「配置文件 → spec → protocol.json」**(2026-09-27 用户定):
644
+ `/multi-viewers-config <键> <值>`(= `mv.sh --set-default`)改的是**默认值**,
645
+ 存在 pi agent 目录的 `multi-viewers.json`(用户级;键名与 CLI flag 同名)。
646
+ **取值优先级(唯一实现在 `spec_gen.resolve_startup`)**:
647
+ `--start` 显式 flag > `spec/startup.md` > 默认值配置 > 内置默认。
648
+ 为什么经 spec 而不是直接进 `protocol.json`:spec 是**用户审阅的产物**——
649
+ 把值写进 `spec/startup.md` 让"这次到底用多少"在暂停点可见可改(= 本轮的
650
+ "特别指定"),运行期权威仍是 `protocol.json`(loop 每轮只读它,中途不可改)。
651
+ **踩过的坑(用户点名要确认的那件事)**:`--start` 三个 flag 原先
652
+ `default=DEFAULT_*`,而 `/multi-viewers` 这条路径不带 flag → argparse 默认值
653
+ **无条件覆盖**任何偏好(永远 15/7/600)。修法 = 默认值改 `None`,让"没指定"
654
+ 与"指定成默认值"可区分;四层优先级由 `tests/test_startup_defaults.py` 锁
655
+ (含"谁都没设 → 内置默认"这条反例断言)。生效值与来源在启动时逐键打印
656
+ (`max-meeting=20(默认值配置)`),不存在静默覆盖。
657
+
634
658
  ## 四、记录项(不修;每项必带**现在就能用的观测点**)
635
659
 
636
660
  | 项 | 实测/性质(口径) | 触发条件(观测点) |
@@ -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
 
@@ -233,4 +233,22 @@ export default function register(pi: any) {
233
233
  }
234
234
  },
235
235
  });
236
+
237
+ // ------------------------------------------------------ 启动参数默认值
238
+ // 零 LLM:直接调 `mv.sh --set-default`(写法与校验都在 python 侧单点实现)。
239
+ // 语义:这里改的是**默认值**;spec/startup.md 或 --start 的显式 flag 优先。
240
+ pi.registerCommand("multi-viewers-config", {
241
+ description: "查看/修改启动参数默认值(max-meeting / max-rr / stall-timeout)",
242
+ argumentHint: "[<键> <值>]",
243
+ handler: async (args: string, ctx: any) => {
244
+ const rest = args.trim().split(/\s+/).filter(Boolean);
245
+ const { rc, output } = await runCli(
246
+ ["--set-default", ...rest], ctx.cwd, ctx.sessionManager.getSessionId(),
247
+ );
248
+ ctx.ui.notify(
249
+ output.trim() || (rc === 0 ? "已处理" : "修改失败"),
250
+ rc === 0 ? "success" : "error",
251
+ );
252
+ },
253
+ });
236
254
  }
package/meeting_fs.py CHANGED
@@ -74,6 +74,99 @@ DEFAULT_STALL_TIMEOUT = 600
74
74
  # 显式档位),不是把默认值挪到便宜侧。
75
75
  DEFAULT_THINKING = "max"
76
76
 
77
+ # ---------------------------------------------------------------
78
+ # 启动参数的「默认值」配置(用户级)——`/multi-viewers-config` 与
79
+ # `mv.sh --set-default` 写的就是它
80
+ # ---------------------------------------------------------------
81
+ # 为什么需要:配额此前只能在 `--start` 那一刻用命令行 flag 指定,而
82
+ # `/multi-viewers` 这条主路径不带 flag → 永远拿 argparse 的 default。
83
+ # 用户要的是"设一次默认值,以后每次生成 spec 就沿用"(2026-09-27 定)。
84
+ #
85
+ # **取值优先级(唯一实现见 spec_gen.resolve_startup)**:
86
+ # 命令行显式 flag > spec/startup.md > 本配置文件 > 内置默认
87
+ # 运行时权威仍是 protocol.json(--start 固化;loop 只读它、不接 flag)。
88
+ #
89
+ # 文件位置 = pi agent 目录下(`$PI_CODING_AGENT_DIR` 或 `~/.pi/agent`)——
90
+ # 复用 `pi_agent_dir()` 单一实现,测试靠改该环境变量重定向(不新增测试专用开关)。
91
+ # 形状:`{"max-meeting": 20, "max-rr": 10, "stall-timeout": 600}`
92
+ # (键名与 CLI flag 同名,少一层映射)。
93
+ STARTUP_DEFAULTS = { # 键名 → 内置默认(引用上方常量,不重复字面量)
94
+ "max-meeting": DEFAULT_MAX_MEETING,
95
+ "max-rr": DEFAULT_MAX_RR,
96
+ "stall-timeout": DEFAULT_STALL_TIMEOUT,
97
+ }
98
+
99
+
100
+ def startup_config_path(agent_dir=None):
101
+ """用户级启动参数配置文件的路径(单一实现)。"""
102
+ return os.path.join(agent_dir or pi_agent_dir(), "multi-viewers.json")
103
+
104
+
105
+ def read_startup_config(agent_dir=None):
106
+ """读用户级配置 → (overrides, error)。
107
+
108
+ 只保留**已知键**且值合法(正整数)——未知键忽略但**可见**(warning),
109
+ 坏文件返回 ({} , 原因) 由调用方决定如何提示(fail-open,不阻断分析)。
110
+ """
111
+ path = startup_config_path(agent_dir)
112
+ if not os.path.exists(path):
113
+ return {}, ""
114
+ try:
115
+ with open(path, encoding="utf-8") as f:
116
+ raw = json.load(f)
117
+ except (OSError, ValueError) as e:
118
+ return {}, f"读不到/解析失败 {path}: {e}"
119
+ if not isinstance(raw, dict):
120
+ return {}, f"{path} 顶层不是对象"
121
+ out, unknown = {}, []
122
+ for k, v in raw.items():
123
+ if k not in STARTUP_DEFAULTS:
124
+ unknown.append(k)
125
+ continue
126
+ try:
127
+ n = int(v)
128
+ except (TypeError, ValueError):
129
+ unknown.append(k)
130
+ continue
131
+ if n < 1:
132
+ unknown.append(k)
133
+ continue
134
+ out[k] = n
135
+ warn = f"{path} 里这些键被忽略(未知或非法):{', '.join(unknown)}" if unknown else ""
136
+ return out, warn
137
+
138
+
139
+ def write_startup_config(key, value, agent_dir=None):
140
+ """把 `key: value` 写进用户级配置(保留其它键)→ (path, error)。
141
+
142
+ 校验在调用方(`parse_startup_kv`)——本函数只管读改写。
143
+ """
144
+ path = startup_config_path(agent_dir)
145
+ cur, _err = read_startup_config(agent_dir)
146
+ cur[key] = value
147
+ try:
148
+ os.makedirs(os.path.dirname(path), exist_ok=True)
149
+ with open(path, "w", encoding="utf-8") as f:
150
+ json.dump(cur, f, indent=2, ensure_ascii=False)
151
+ f.write("\n")
152
+ except OSError as e:
153
+ return path, f"写不了 {path}: {e}"
154
+ return path, ""
155
+
156
+
157
+ def parse_startup_kv(key, raw):
158
+ """校验 `--set-default <key> <value>` 的入参 → (value, error)。"""
159
+ if key not in STARTUP_DEFAULTS:
160
+ return None, (f"未知的键 {key!r}——合法键:"
161
+ + "、".join(STARTUP_DEFAULTS))
162
+ try:
163
+ n = int(raw)
164
+ except (TypeError, ValueError):
165
+ return None, f"{key} 需要整数,收到 {raw!r}"
166
+ if n < 1:
167
+ return None, f"{key} 需要 ≥1,收到 {n}"
168
+ return n, ""
169
+
77
170
 
78
171
  def _entry_source(entry):
79
172
  """pi 的 packages 条目 → 源字符串(两种形态共用;非字符串形态 → "")。
@@ -792,7 +885,7 @@ def parse_log_nameonly(output):
792
885
  # agents 需要主项目背景(背景蒸馏机制已移除),这是它的补充通道;
793
886
  # 该入口**只注册工具、不装 hook** → historian/压缩不在其中
794
887
  # (受控实测 historian 0/6、ctx_search 可用;成本未测得显著差异)。
795
- # 代价:本档**允许而非要求** MC(缺则可见降级为零扩展;严格模式见
888
+ # 代价:本档两份入口**允许而非要求**(缺谁少谁、都可见降级;严格模式见
796
889
  # MC_TOOLS_STRICT_ENV)
797
890
  # none : 零扩展——最快、**零依赖**(不依赖任何扩展;无 MC 的机器/CI 用这档)
798
891
  # all : 走 pi 默认扩展发现(A/B 实验与显式 opt-in 用)
@@ -800,7 +893,8 @@ def parse_log_nameonly(output):
800
893
  # 默认档看 DEFAULT_EXTENSION_POLICY)
801
894
  EXTENSION_POLICIES = ("mc-tools", "none", "all")
802
895
  DEFAULT_EXTENSION_POLICY = "mc-tools"
803
- # mc-tools 档**允许**(而非要求)MC:找不到 MC 时降级为零扩展,但必须**可见**
896
+ # mc-tools 档**允许而非要求**两份入口(MC 的 ctx_search、MCP adapter 的 web 工具):
897
+ # 缺谁少谁、都必须**可见**;语义清单见 docs/design.md 决策 20
804
898
  # (打印一行说明 `ctx_search` 本次不可用)——无静默铁律。
805
899
  # 测试/探针要保真(确认"本场确实带着 ctx_search 在跑")时,用环境变量把它变严格:
806
900
  # MV_MC_TOOLS_STRICT=1 → 解析失败即报错退出(测试环境准确性优先,用户 2026-09-14 定)
package/meeting_loop.py CHANGED
@@ -364,14 +364,13 @@ def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
364
364
  if first_wake:
365
365
  # 登记行(观测面的稳定字段;报告据此给"声明 vs 生效")。只在首唤打:
366
366
  # 策略在一次运行内不变,变了也是配置错误(重跑即可)。
367
- log(agent, f"扩展策略: 声明={extension_policy} 生效={effective_policy}"
368
- f" strict={int(meeting_fs.mc_tools_strict())}"
369
- + (f" 降级原因={downgrade_reason}" if downgrade_reason else ""))
367
+ # 降级时把"部分/完全"标进原因字段(S2:第二行是复述,已删——
368
+ # 报告只解析本行,`observability._report_extension_line` 的 regex 匹配行尾)。
369
+ reason = ""
370
370
  if downgrade_reason:
371
- log(agent, f"mc-tools 档{'部分' if resolved else '完全'}未生效"
372
- f"({downgrade_reason})——本次按"
373
- f"{'已解析的入口' if resolved else '零扩展'}运行:"
374
- f"缺失的工具在本次分析中不可用")
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}")
375
374
  model = cfg.get("model") or ""
376
375
  if model:
377
376
  cmd += ["--model", model]
package/mv_cli.py CHANGED
@@ -29,6 +29,7 @@ import sys
29
29
  from datetime import datetime
30
30
 
31
31
  import spec_gen # --viewers 复用其单一判据(列举/名字/集合校验)
32
+ import meeting_fs # --set-default 的配置读写(单一实现)
32
33
 
33
34
  HERE = os.path.dirname(os.path.abspath(__file__))
34
35
  PYTHON = os.environ.get("PYTHON") or "python3"
@@ -55,6 +56,7 @@ USAGE = f"""用法:
55
56
  {PROG} --say [dir] "<文本>"
56
57
  {PROG} --viewers # 列出并校验当前项目的 viewers/(只读;建视角时用)
57
58
  {PROG} --set-viewer <名字> # 新建一个视角文件(正文从 stdin 读;只新建不覆盖)
59
+ {PROG} --set-default [<键> <值>] # 启动参数默认值(无参数=查看;键:max-meeting/max-rr/stall-timeout)
58
60
 
59
61
  消费命令的 <dir> 可省略(自动发现本 session 当前分析——按 cwd 下
60
62
  mv-<PI_SESSION_ID>-* 最新;无匹配则报错要求显式传目录)
@@ -186,7 +188,7 @@ def _validate_and_print_viewers(vdir):
186
188
  notes.append("空:没有视角内容")
187
189
  suffix = f"({';'.join(notes)})" if notes else ""
188
190
  print(f" {n}.md{suffix}")
189
- err = spec_gen._viewer_set_errors(names, empty)
191
+ err = spec_gen.viewer_entry_errors(names, empty)
190
192
  if err:
191
193
  fail_verbatim(err)
192
194
  gap = spec_gen.viewers_count_gap(names)
@@ -197,6 +199,43 @@ def _validate_and_print_viewers(vdir):
197
199
  return 0
198
200
 
199
201
 
202
+ def cmd_set_default(args):
203
+ """查看 / 修改**启动参数的默认值**:`--set-default [<键> <值>]`。
204
+
205
+ 语义(用户 2026-09-27 定):这里改的是**默认值**;`spec/startup.md` 或
206
+ `--start` 的显式 flag 属于"特别指定",优先于它(取值优先级唯一实现在
207
+ `spec_gen.resolve_startup`)。无参数 = 打印当前默认值(含内置 fallback
208
+ 与来源),便于自查"我设的值到底生效没有"。
209
+ """
210
+ if not args:
211
+ cfg, warn = meeting_fs.read_startup_config()
212
+ print(f"默认值配置文件: {meeting_fs.startup_config_path()}"
213
+ + ("" if os.path.exists(meeting_fs.startup_config_path()) else "(尚未创建)"))
214
+ for k, builtin in meeting_fs.STARTUP_DEFAULTS.items():
215
+ if k in cfg:
216
+ print(f" {k} = {cfg[k]}(配置文件)")
217
+ else:
218
+ print(f" {k} = {builtin}(内置默认)")
219
+ if warn:
220
+ print(f" ⚠ {warn}", file=sys.stderr)
221
+ print("改法: --set-default <键> <值> | 本轮单独指定: 改 spec/startup.md")
222
+ return 0
223
+ if len(args) != 2:
224
+ fail(f"用法: --set-default <键> <值>(无参数 = 查看当前默认值);"
225
+ f"合法键: {'、'.join(meeting_fs.STARTUP_DEFAULTS)}")
226
+ key, raw = args
227
+ value, err = meeting_fs.parse_startup_kv(key, raw)
228
+ if err:
229
+ fail(err)
230
+ path, err2 = meeting_fs.write_startup_config(key, value)
231
+ if err2:
232
+ fail(err2)
233
+ print(f"已设默认值: {key} = {value}({path})")
234
+ print("以后 `/multi-viewers \"<主题>\"` 生成的 spec 会沿用;"
235
+ "想只给本轮不同 → 改 spec/startup.md 或 --start 时显式给 flag。")
236
+ return 0
237
+
238
+
200
239
  def cmd_set_viewer(args):
201
240
  """新建一个视角文件:`--set-viewer <名字>`,正文**从 stdin 读**。
202
241
 
@@ -225,7 +264,7 @@ def cmd_set_viewer(args):
225
264
  # (不守卫会让 validate_participants(None) 抛 TypeError——首次建视角即命中)。
226
265
  names, _briefs, empty = spec_gen._discover_viewers(vdir)
227
266
  if names:
228
- err = spec_gen._viewer_set_errors(names, empty)
267
+ err = spec_gen.viewer_entry_errors(names, empty)
229
268
  if err:
230
269
  fail_verbatim(f"{err}\n(修正 viewers/ 后再建新视角——本次未写入任何文件)")
231
270
  body = sys.stdin.read().strip()
@@ -470,6 +509,8 @@ def main(argv=None):
470
509
  return cmd_viewers(rest)
471
510
  if cmd == "--set-viewer":
472
511
  return cmd_set_viewer(rest)
512
+ if cmd == "--set-default":
513
+ return cmd_set_default(rest)
473
514
  if cmd == "--status":
474
515
  return cmd_status(rest)
475
516
  if cmd == "--report":
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.9.0",
3
+ "version": "0.10.0",
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
@@ -257,6 +257,9 @@ def gen_spec_skeleton(spec_dir, participants, topic=None, background=None,
257
257
  # README.md:从模板复制(内容不变——模板化,用户 7909)
258
258
  shutil.copyfile(os.path.join(TPL_DIR, "spec-readme.md.tpl"),
259
259
  os.path.join(spec_dir, "README.md"))
260
+ # startup.md:本轮启动参数(= `--prepare` 时解析出的默认值;用户 2026-09-27)
261
+ # —— 写进 spec 的目的是**可见可改**:审阅暂停点里直接改这里 = 只影响本轮。
262
+ write_spec_startup(spec_dir, resolve_startup({})[0])
260
263
  # question.md:第一行说明 + 基本结构模板(用户 7713:提供基本结构)
261
264
  q = [
262
265
  "# question.md——分析起点(话题/立场/待答问题,自由 markdown)。本行是说明行,不会注入。",
@@ -311,7 +314,7 @@ def gen_spec_skeleton(spec_dir, participants, topic=None, background=None,
311
314
 
312
315
 
313
316
  def gen_agents_md(args, agent, participants, spec_background=None,
314
- main_pi_cwd=None):
317
+ main_pi_cwd=None, extension_policy=None):
315
318
  """meeting 协议 AGENTS.md(共享协议 + background;身份/立场在 agent
316
319
  定义/question.md)。
317
320
 
@@ -338,6 +341,23 @@ def gen_agents_md(args, agent, participants, spec_background=None,
338
341
  f"查找:如有需要可查看相关文件以获取\n比本背景更详细的信息。\n")
339
342
  else:
340
343
  cwd_section = ""
344
+ # **这是「工具在不在」的第二次解析**(第一次在 setup 这里、第二次在 wake 实况
345
+ # meeting_loop);窗口 = `--prepare` … `--start` 分离路径(默认 extension 流程
346
+ # 背靠背,秒级)。两者不一致的后果:prompt 承诺了而 wake 没给(agent 一次失败
347
+ # 调用,有界无害)或反之(少一句指引)。**重估触发**:出现第三份入口,或 prompt
348
+ # 需点名第二个工具 → 把入口表提成 `meeting_fs` 的单点再消费。
349
+ # 历史检索节:**只在工具真的会到位时**才出现(2026-09-25 用户定)——
350
+ # 否则 agents 会去找一个不存在的工具("无静默/不下空指令")。判据 = 策略允许
351
+ # (mc-tools)**且**入口可解析(与 meeting_loop 的解析同一实现,不各写一套)。
352
+ policy = extension_policy or meeting_fs.DEFAULT_EXTENSION_POLICY
353
+ if policy == "mc-tools" and meeting_fs.resolve_mc_tools_entry()[0]:
354
+ history_section = (
355
+ "\n## 需要项目历史时\n\n"
356
+ "你的上下文来自发起分析的会话,覆盖不到更早的决策与实测记录。这类问题可以用\n"
357
+ "`ctx_search` 检索本项目的历史记忆。项目文件(代码/文档)优先——记忆可能落后于\n"
358
+ "代码,冲突时以文件为准。\n")
359
+ else:
360
+ history_section = ""
341
361
  out = tpl.format(
342
362
  AGENT_NAME=agent,
343
363
  N=str(len(participants)),
@@ -345,6 +365,7 @@ def gen_agents_md(args, agent, participants, spec_background=None,
345
365
  SAMPLE_OTHER=sample,
346
366
  BACKGROUND=background,
347
367
  MAIN_PI_CWD_SECTION=cwd_section,
368
+ HISTORY_SECTION=history_section,
348
369
  )
349
370
  return out
350
371
 
@@ -369,6 +390,92 @@ def gen_question(topic, stances, background, questions):
369
390
  return "\n".join(lines)
370
391
 
371
392
 
393
+
394
+ def spec_startup_path(spec_dir):
395
+ """spec 里的启动参数文件(`startup.md`)——`--prepare` 写、`--start` 读。"""
396
+ return os.path.join(spec_dir, "startup.md")
397
+
398
+
399
+ _SPEC_STARTUP_HEADER = (
400
+ "# startup.md——本轮的启动参数(键: 值)。本行是说明行,不会注入。\n"
401
+ "#\n"
402
+ "# 这些值由 `--prepare` 按「你的默认值配置」填入;**在这里改 = 只影响本轮**\n"
403
+ "# (相当于\"特别指定\")。删除某行 = 该键回落到默认值配置。\n"
404
+ "# 合法键:max-meeting(meeting 每 agent 发言配额)、max-rr(RR 轮次配额)、\n"
405
+ "# stall-timeout(无进展超时秒数)。值必须是 ≥1 的整数。\n"
406
+ )
407
+
408
+
409
+ def write_spec_startup(spec_dir, values):
410
+ """写 `spec/startup.md`(prepare 时;values = 解析后的默认值)。"""
411
+ lines = [_SPEC_STARTUP_HEADER]
412
+ for k in meeting_fs.STARTUP_DEFAULTS:
413
+ if k in values:
414
+ lines.append(f"{k}: {values[k]}\n")
415
+ with open(spec_startup_path(spec_dir), "w", encoding="utf-8") as f:
416
+ f.write("".join(lines))
417
+
418
+
419
+ def read_spec_startup(spec_dir):
420
+ """读 `spec/startup.md` → (values, error)。
421
+
422
+ 容错:文件不存在 → ({}, "")(旧 spec 兼容,回落默认值);坏行忽略但可见。
423
+ """
424
+ path = spec_startup_path(spec_dir)
425
+ if not os.path.exists(path):
426
+ return {}, ""
427
+ out, bad = {}, []
428
+ try:
429
+ with open(path, encoding="utf-8") as f:
430
+ for raw in f:
431
+ line = raw.strip()
432
+ if not line or line.startswith("#"):
433
+ continue
434
+ if ":" not in line:
435
+ bad.append(line)
436
+ continue
437
+ k, v = (x.strip() for x in line.split(":", 1))
438
+ val, err = meeting_fs.parse_startup_kv(k, v)
439
+ if err:
440
+ bad.append(line)
441
+ continue
442
+ out[k] = val
443
+ except OSError as e:
444
+ return {}, f"读不到 {path}: {e}"
445
+ return out, ("忽略这些行:" + ";".join(bad) if bad else "")
446
+
447
+
448
+ def resolve_startup(cli_values, spec_dir=None):
449
+ """**取值优先级唯一实现** → (values, sources, notes)。
450
+
451
+ 优先级(后者覆盖前者):
452
+ 内置默认 → 用户级配置(`meeting_fs.read_startup_config`)
453
+ → `spec/startup.md` → 命令行显式 flag
454
+
455
+ cli_values:只放**命令行真的给了**的键(未指定 = 不在字典里 ✗)——
456
+ 这正是 argparse 默认值必须改成 None 的原因:默认值会让"没指定"和
457
+ "指定成 15"无法区分,从而静默覆盖用户设的默认值。
458
+ """
459
+ values = dict(meeting_fs.STARTUP_DEFAULTS)
460
+ sources = {k: "内置默认" for k in values}
461
+ notes = []
462
+ cfg, warn = meeting_fs.read_startup_config()
463
+ if warn:
464
+ notes.append(warn)
465
+ for k, v in cfg.items():
466
+ values[k], sources[k] = v, "默认值配置"
467
+ if spec_dir:
468
+ spec_vals, warn2 = read_spec_startup(spec_dir)
469
+ if warn2:
470
+ notes.append(warn2)
471
+ for k, v in spec_vals.items():
472
+ values[k], sources[k] = v, "spec"
473
+ for k, v in (cli_values or {}).items():
474
+ if v is not None:
475
+ values[k], sources[k] = v, "命令行"
476
+ return values, sources, notes
477
+
478
+
372
479
  def gen_protocol(topic, participants, max_meeting, max_rr,
373
480
  extension_policy=meeting_fs.DEFAULT_EXTENSION_POLICY,
374
481
  result_writer=None,
@@ -478,14 +585,18 @@ def viewer_set_error(names, empty, where="viewers/"):
478
585
  return f"错误: {gap}" if gap else None
479
586
 
480
587
 
481
- def _viewer_set_errors(names, empty, where="viewers/"):
482
- """集合级校验的**唯一组合点**:整组名字 + 空正文 + 数量 ≥2。
588
+ def viewer_entry_errors(names, empty, where="viewers/"):
589
+ """**条目级**校验的组合入口:整组名字 + 空正文(**不查数量**)。
483
590
 
484
- 为什么单独存在:`--viewers`(只读检查)与 `--set-viewer`(写前校验)必须
485
- 用**同一套**判据——同一套规则曾在两处漂移过(文案与检查项不一致)。
486
- `if empty` 是必要保护:否则 viewers/ 里只有一个**合法**视角时,
487
- `viewer_set_error` 会因为数量不足而报错,把"建第 2 个视角"判成非法。
488
- `names` 为空(目录缺失/无 .md)返回 None——那是"还没有视角",不是错误。
591
+ 为什么改名(2026-09-25 评审批 F2/S3):原名 `_viewer_set_errors` 的 docstring 自称
592
+ "集合级校验的唯一组合点 + 数量 ≥2",实测两件都不成立——它不查数量(`if empty`
593
+ 保护 + `viewer_set_error` 只在有空正文时才顺带报数量,那条分支在本组合内不可达),
594
+ 而且 `_snapshot_viewers` 与 `start` 路径各有自己的组合。**名实不符的风险**是后来者
595
+ 按文档当全量校验用 → 静默漏掉 ≥2。
596
+ 现在名字只说它做的事:`entry`(条目级)而非 `set`(集合级);**≥2 由启动路径单独判**
597
+ (`viewer_set_error` / `viewers_count_gap`),CLI 允许单视角是合法中间状态。
598
+ 去掉前导下划线:它已被 `mv_cli` 跨模块当稳定契约用(`_discover_viewers` 的下划线
599
+ 属既有的命名债,本批不夹带)。
489
600
  """
490
601
  if not names:
491
602
  return None
@@ -348,11 +348,22 @@ def setup_environment(args, participants, base, spec_dir=None,
348
348
  shutil.rmtree(wa)
349
349
  _clone_work(base, participants[0]) # clone + git 身份 + 建目录
350
350
 
351
+ # 启动参数解析(**唯一实现**在 spec_gen.resolve_startup):命令行显式 > spec >
352
+ # 默认值配置 > 内置默认。三个 flag 的 argparse 默认是 None,所以"没给"不会被
353
+ # 误当成"显式给了默认值"(2026-09-27 用户要求:不能有强制设置配额的操作)。
354
+ startup, startup_src, startup_notes = spec_gen.resolve_startup(
355
+ {"max-meeting": args.max_meeting, "max-rr": args.max_rr,
356
+ "stall-timeout": args.stall_timeout},
357
+ spec_dir=spec_dir)
358
+ for _n in startup_notes:
359
+ print(f"[startup] {_n}")
360
+
351
361
  # 共享配置(work-a 提交,setup commit 进 bare)
352
362
  with open(os.path.join(wa, "protocol.json"), "w") as f:
353
- json.dump(gen_protocol(spec_topic or args.topic, participants, args.max_meeting,
354
- args.max_rr, args.extension_policy, args.result_writer,
355
- args.stall_timeout,
363
+ json.dump(gen_protocol(spec_topic or args.topic, participants,
364
+ startup["max-meeting"],
365
+ startup["max-rr"], args.extension_policy, args.result_writer,
366
+ startup["stall-timeout"],
356
367
  fork_source=getattr(args, "fork_source", None),
357
368
  fork_cwd=os.getcwd(),
358
369
  fork_mode=getattr(args, "fork_mode", meeting_fs.DEFAULT_FORK_MODE)),
@@ -391,7 +402,8 @@ def setup_environment(args, participants, base, spec_dir=None,
391
402
  workdir = os.path.join(base, f"work-{p}")
392
403
  with open(os.path.join(workdir, "AGENTS.md"), "w") as f:
393
404
  f.write(gen_agents_md(args, p, participants, spec_background,
394
- main_pi_cwd=os.getcwd()))
405
+ main_pi_cwd=os.getcwd(),
406
+ extension_policy=args.extension_policy))
395
407
  mv = models[p] # 归一后必有条目(见上方归一循环)
396
408
  with open(os.path.join(workdir, ".pi/agent", f"{p}.md"), "w") as f:
397
409
  f.write(gen_agent_def(p, participants, {p: mv[0]} if mv[0] else None,
@@ -419,7 +431,9 @@ def setup_environment(args, participants, base, spec_dir=None,
419
431
  shutil.copy(os.path.join(HERE, mod), os.path.join(base, mod))
420
432
  rw = args.result_writer or participants[-1]
421
433
  print(f"[setup] 环境就绪: {base}({len(participants)} agents: {', '.join(participants)})")
422
- print(f"[setup] resultWriter={rw}, maxMeeting={args.max_meeting}, maxRR={args.max_rr}, "
434
+ # 生效值 + **来源**一并打印(用户 2026-09-27 的疑虑:默认值有没有被静默覆盖)
435
+ q = " · ".join(f"{k}={startup[k]}({startup_src[k]})" for k in meeting_fs.STARTUP_DEFAULTS)
436
+ print(f"[setup] resultWriter={rw}, 配额 {q}, "
423
437
  f"立场={'有' if (args.stances or spec_dir) else '无'}, "
424
438
  f"extensionPolicy={args.extension_policy}")
425
439
 
@@ -429,12 +443,16 @@ def _print_best_effort(*args, **kwargs):
429
443
 
430
444
  为什么需要:`cleanup_discussion` 的输出可能在管道关闭时抛
431
445
  `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` 里冒泡,见其注释)。
446
+ 逃逸 → **报告段之后的一切被跳过**(含删目录),目录残留且 rc≠0,即"该清理的
447
+ 没清理"(2026-09-25 评审批 ①(c))。显示层从来不是主职责,失败只能被忽略。
448
+
449
+ 契约:`cleanup_discussion` 的**全部 stdout 都走本函数**——具体是"该函数 AST
450
+ 子树内不得直接 `Call(Name('print'))`",由 `tests/test_cleanup_contract.py`
451
+ 断言(找不到该函数即红;本批起本仓有源码结构断言这一类别)。
452
+ 本函数保证的是**显示层失败不上抛**(⇒ rc=0、后续段不跳过);**"删目录必达"
453
+ 的真正保证是 `cleanup_discussion` 里的 `try/finally`**——两者别混为一谈
454
+ (原 docstring 把 rmtree 必达归因到本函数,归因错了)。
455
+ 唯一允许阻断删除的失败 = 产物留存真失败(`_preserve_result_md` 里冒泡)。
438
456
  """
439
457
  try:
440
458
  print(*args, **kwargs)
@@ -578,12 +596,17 @@ def main():
578
596
  parser.add_argument("--questions", default=None, help="待回答问题(|分隔,对齐 RR)")
579
597
  parser.add_argument("--models", default=None, help='JSON: {"a": "provider/model"}')
580
598
  parser.add_argument("--result-writer", default=None, help="resultWriter(默认最后一位参与者)")
581
- parser.add_argument("--max-meeting", type=int, default=meeting_fs.DEFAULT_MAX_MEETING,
582
- help="meeting 阶段发言配额(每 agent)")
583
- parser.add_argument("--max-rr", type=int, default=7, help="RR 阶段轮次配额(starter)")
584
- parser.add_argument("--stall-timeout", type=int,
585
- default=meeting_fs.DEFAULT_STALL_TIMEOUT,
586
- help="无进展超时兜底(秒,默认 600;防 provider API 慢)")
599
+ # 配额三个 flag:**默认值一律 None**(2026-09-27 用户要求)——这样才能区分
600
+ # "命令行没给"(→ 交给 spec/默认值配置/内置默认,见 spec_gen.resolve_startup)
601
+ # 与"命令行显式给了"。此前的 default=DEFAULT_* 会在 `/multi-viewers` 这条
602
+ # 不带 flag 的路径上**无条件覆盖**用户设的默认值(用户的疑虑,已成事实:
603
+ # --start <spec> 无 flag → argparse 15 → protocol.json 15)。
604
+ parser.add_argument("--max-meeting", type=int, default=None,
605
+ help="meeting 阶段发言配额(每 agent;不给则用默认值配置/spec)")
606
+ parser.add_argument("--max-rr", type=int, default=None,
607
+ help="RR 阶段轮次配额(starter;不给则用默认值配置/spec)")
608
+ parser.add_argument("--stall-timeout", type=int, default=None,
609
+ help="无进展超时兜底(秒;不给则用默认值配置/spec)")
587
610
  parser.add_argument("--spec-gen", metavar="DIR", default=None,
588
611
  help="生成 spec 骨架到 DIR(如 --spec-gen myspec/;不需 --dir)")
589
612
  parser.add_argument("--spec", default=None,
@@ -603,7 +626,7 @@ def main():
603
626
  choices=list(meeting_fs.EXTENSION_POLICIES),
604
627
  default=meeting_fs.DEFAULT_EXTENSION_POLICY,
605
628
  help="agents 的扩展策略:mc-tools=默认,只要 MC 的只读检索工具 "
606
- "ctx_search(缺 MC 时可见降级为零扩展);none=零扩展(零依赖);"
629
+ "ctx_search + MCP 工具(缺谁少谁、可见降级);none=零扩展(零依赖);"
607
630
  "all=走 pi 默认发现")
608
631
  parser.add_argument("--start", action="store_true", help="创建后启动讨论")
609
632
  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
 
@@ -46,6 +46,14 @@ fork 机制已让每个 agent 携带发起分析时的对话上下文(默认 b
46
46
 
47
47
  没有就留空。
48
48
 
49
+ ## startup.md —— 启动参数(自动生成,一般不用改)
50
+
51
+ **作用**:本轮的配额等启动参数。`--prepare` 按你设的默认值写好;**直接改这里
52
+ = 只影响本轮**(相当于"特别指定"),删掉某行则该键回落到默认值。
53
+
54
+ 可设:`max-meeting`(meeting 每 agent 发言配额)、`max-rr`(RR 轮次配额)、
55
+ `stall-timeout`(无进展超时秒数)。改默认值:`/multi-viewers-config <键> <值>`。
56
+
49
57
  ## models.md —— 模型配置(可选)
50
58
 
51
59
  每行:`agent名: model, variant`(**两个槽都显式写出**,一眼可见本场跑在