pi-multi-viewers 0.4.0 → 0.5.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
@@ -54,17 +54,31 @@ tests/ 测试(unittest discover tests)
54
54
  移除,fork 使其冗余;background 只写显式边界,不复述对话)。
55
55
 
56
56
  **agent 进程环境**:`GIT_CEILING_DIRECTORIES`(git 上溯防护)——注入点
57
- `meeting_loop._spawn_env`。**扩展策略(决策 20):默认零扩展** ——
58
- `--no-extensions --no-skills --no-prompt-templates --no-themes`,只留 pi 内置
59
- 工具与项目内 AGENTS.md。为什么:两类插件在**我们这种 session 形态**上都是分钟级
60
- 负担、且都在关键路径上(loop 等进程退出才继续)——
57
+ `meeting_loop._spawn_env`。**扩展策略(决策 20):三档**
58
+ (`meeting_fs.EXTENSION_POLICIES` / 协议 `extensionPolicy` / CLI `--extension-policy`):
59
+
60
+ | 档 | 唤醒命令 | 用途 |
61
+ |---|---|---|
62
+ | **mc-tools**(默认) | 四个 `--no-*` + `-e <MC 的 subagent-entry.js>` | 给 agents **按需检索项目背景**(`ctx_search`)——背景蒸馏机制已移除,这是其补充通道。**允许而非要求 MC**:找不到 MC → 降级为零扩展 + 一行可见说明(`ctx_search` 本次不可用)|
63
+ | **none** | 四个 `--no-*` | 零扩展、**零依赖**(无 MC 的机器/CI 用这档)|
64
+ | **all** | 不加任何 `--no-*`(pi 默认发现)| A/B 实验与显式 opt-in |
65
+
66
+ 为什么**不能**用插件全档(`all`):两类插件在**我们这种 session 形态**上都是分钟级负担、
67
+ 且都在关键路径上(loop 等进程退出才继续)——
61
68
  · **AFT**:大 session 上进程退出前多活数分钟(受控对照 445.9s → 0.5s);
62
- · **MC**:它的 historian 对"带大段未处理历史"的 session **每次必失败并立刻重试**
69
+ · **MC 全档**:它的 historian 对"带大段未处理历史"的 session **每次必失败并立刻重试**
63
70
  (受控对照:同输入 **447s → 10.3s,43 倍**)。
64
71
  零扩展**真场实测**:墙钟 12m31s / 每次唤醒 48.1s / 收尾≈0% / historian 0 次。
65
- 加回扩展 = **显式 opt-in**(`--extensions` / 协议 `extensions: true`),且须自证
66
- 净收益(design.md 决策 20 的门槛条款)。**主 pi 完全不受影响**(只改我们 spawn 的
67
- agent 进程命令行;主 pi MC/历史学家照常)。
72
+ **mc-tools 档的实测**:entry **只注册工具、不装 hook** → historian 0/6 ✓(生产 0/3 ✓);
73
+ `ctx_search` 实测可用 ✓;成本**未测得显著差异**(受控探针 n 小、组内方差>组间差 ✗;
74
+ 生产基线:本场 strict=1、n=19,唤醒启动段中位 **0.68s**、收尾中位 0.04s ✓)。
75
+ **入口解析 fail-fast**(`meeting_fs.resolve_mc_tools_entry`:从 pi 的 packages 找 MC 包 →
76
+ 读它声明的扩展入口 → 取同目录的 subagent-entry.js);缺 MC 时**可见降级**为零扩展。
77
+ **依赖边界**:mc-tools 档**允许而非要求** MC——缺 MC 时降级为零扩展,且**可见**
78
+ (打印一行"本次按零扩展运行:ctx_search 不可用";无静默铁律);`none` 档零依赖
79
+ (无 MC 的机器/CI 显式选它)。**测试/探针保真**:设 `MV_MC_TOOLS_STRICT=1` →
80
+ 缺 MC 即报错退出(否则测试可能在"没装 MC"下通过而 ctx_search 从未生效)。
81
+ **主 pi 完全不受影响**(只改我们 spawn 的 agent 进程命令行;主 pi 的 MC/历史学家照常)。
68
82
 
69
83
  **关键约定**:pi sessions 目录编码 = `--` + 去首尾斜杠内斜杠换 `-` + `--`
70
84
  (`/tmp` → `--tmp--`;wrapper 解析 fork 源依赖它,编码错一根横线 = 静默
package/docs/design.md CHANGED
@@ -113,6 +113,7 @@ compaction 的 `firstKeptEntryId` 起 + 其后的条目"——窗口内含 compa
113
113
  | `seconds_by_stopReason` | `{键=stopReason 原值: Δ 合计}` | 同上;**口径 = 相邻条目 Δ 合计**(每次唤醒首条响应**也计入**;跨唤醒空闲不进入 Δ——唤醒 prompt 本身是一条 user 条目,空闲落在"上一唤醒末条 → 本次 user 条目"之间)。⚠️ 曾短暂采用"首响不计时",理由(跨唤醒空闲)经 e2e20 评审批证伪后**已撤回**(见决策 19 的 e2e20 修正段) |
114
114
  | `effective_levels` | `[thinkingLevel…]`(session 侧,去重保序) | `档位:声明 X | 生效 Y | ✓一致 / ⚠不一致` |
115
115
  | (对照)`declared` | `pi-agent.json.thinking` | 同上——声明值与生效值**并列**(此前从没人对照过:探测失败会静默取档,spec 表面正常) |
116
+ | `扩展策略` 行 | `声明 / 生效 / strict / 降级原因` | 声明 = `protocol.extensionPolicy`;生效 = loop log 的**登记行**(首唤打一次:`扩展策略: 声明=X 生效=Y strict=0\|1[ 降级原因=…]`)→ 报告给"声明 vs 生效 + ⚠ 生效≠声明"(与"档位:声明/生效"同型;e2e24 评审 E2)|
116
117
  | `终止` 行 | 分类 + 原料计数 | `终止:共识(RR 全体 pass)|freezing N / all-freezing N / pass N / stall 接管行 N`;分类判据只有事实(bare 的 type 计数 + loop log 的"超时兜底/声明接管"字样)——**不做评分** |
117
118
  | `唤醒构成` 表 | 每次唤醒一行 + 每 agent 合计 | 四端点(spawn / 首事件 / 末事件 / exit)+ 往返数 + `Δ助手` + `Δ工具` + retry + 本唤醒内的 commit;合计给 `跨度 = 启动前 + 事件内 + 收尾` 与平均/往返/retry(e2e23 分析产出,见下) |
118
119
 
@@ -450,8 +451,8 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
450
451
  (具名推导 `agent_config_aft_file`,写入侧与判定侧共用),未注入时打一行
451
452
  **事实**日志;`build_agent_config` 只返回 warnings(目录不再返回——
452
453
  生产端本来就不用),并删掉只有测试在用的 `source_config_home` 参数。
453
- 20. **agent 进程默认零扩展**(2026-09-13 用户定;取代决策 19 的"屏蔽 AFT、
454
- 保留 MC"):
454
+ 20. **agent 进程扩展策略三档(默认 mc-tools)**(2026-09-13 定零扩展、
455
+ 2026-09-14 用户定为默认 mc-tools 并改“允许而非要求”;取代决策 19):
455
456
 
456
457
  **证据(两类插件都在关键路径上,都是分钟级)**:
457
458
  - **AFT**:大 session 上进程退出前多活数分钟——同一份 1.9MB session、同一模型
@@ -466,10 +467,48 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
466
467
  档位对照 ✓。同一机制的对照:e2e19 71.6s·17m19s、e2e20 81.6s·20m34s、
467
468
  e2e21 **330s·1h13m**(historian 风暴 62 次 60 失败)。
468
469
 
469
- **做法**:默认唤醒命令带 `--no-extensions --no-skills --no-prompt-templates
470
- --no-themes`(保留内置工具与项目内 AGENTS.md);加回扩展是**显式 opt-in**
471
- (CLI `--extensions` / 协议字段 `extensions: true`,`gen_protocol` **恒写**该
472
- 字段——默认 `false`)。**主 pi 不受影响**(只改我们 spawn 的 agent 命令行)。
470
+ **做法(三档;值域/默认值的家 = `meeting_fs.EXTENSION_POLICIES` /
471
+ `DEFAULT_EXTENSION_POLICY`,协议字段 `extensionPolicy`,CLI
472
+ `--extension-policy`)**:
473
+
474
+ | 档 | 唤醒命令 | 语义 |
475
+ |---|---|---|
476
+ | `mc-tools`(默认) | 四个 `--no-*` + `-e <MC subagent-entry.js>`(找不到 MC 时退化为四个 `--no-*`)| 只要 MC 的**只读检索工具**(`ctx_search`);**允许而非要求** MC |
477
+ | `none` | 四个 `--no-*` | 零扩展:最快、**零依赖** |
478
+ | `all` | 不加任何 `--no-*` | pi 默认发现(A/B 与显式 opt-in)|
479
+
480
+ **`mc-tools` 档 = 默认档**(2026-09-14 加并定为默认,用户裁定"提供 ctx_search
481
+ 是必要的 background 补充”):**fork 本身是主背景通道**(agent 继承主会话的开发
482
+ 上下文),覆盖面受 ① fork 源内容 ② `budget` 裁剪比例限制;`ctx_search` 是它的
483
+ **兜底**(按需检索记忆/文档/历史)。MC 的
484
+ `subagent-entry.js`(MC **自己**给它的"搜索类子代理"用的入口)**只注册工具、
485
+ 不装任何 hook** → 给 agents 按需检索能力(memories / docs / 历史),
486
+ **不含** historian/压缩/打标。实测:`ctx_search` 可用 ✓;historian 0/6 ✓;
487
+ **成本未测得显著差异**(受控探针 n 小、组内方差 > 组间差 ✗;生产基线:首次真场
488
+ strict=1、n=19 → 唤醒启动段中位 **0.68s**、收尾中位 0.04s ✓)。
489
+ **入口解析 fail-fast**(`resolve_mc_tools_entry`:pi 的 packages → MC 包 →
490
+ 它声明的扩展入口 → 同目录 `subagent-entry.js`);缺 MC 时**可见降级**(严格模式
491
+ `MV_MC_TOOLS_STRICT=1` 才报错退出)。
492
+ **实现纪律(e2e24 评审 S1 的教训)**:三档共用**同一段零扩展前缀**,
493
+ `_build_wake_cmd` **只有一个出口**——降级只是"少追加一个 `-e`"。此前降级
494
+ 分支自拼前缀后提前 `return`,把 model/thinking/system-prompt/`--print` 与
495
+ spawn cwd 全截断("无 MC 的机器"上产出残缺命令)。**登记行**:首唤打
496
+ `扩展策略: 声明=X 生效=Y strict=0|1 [降级原因=…]`——报告据此给"声明 vs 生效"
497
+ (E2;与"档位"同型),否则降级只在 loop log 里、产品面看不见。
498
+
499
+ **依赖边界(用户 2026-09-14 定)**:mc-tools **允许而非要求** MC——缺 MC 时
500
+ **降级为零扩展并按 none 运行**,但**必须可见**(打印一行"mc-tools 档未生效
501
+ (原因)——本次按零扩展运行:ctx_search 不可用";无静默铁律)。
502
+ `none` 零依赖(无 MC 的机器/CI 显式选它)。**死代码纪律**:`--extensions`
503
+ 别名与 `extensions: true` 历史字段(只存在约 1 天)**已删净**(无移除条件的
504
+ 兼容层不留——同 `--pure` 先例);扩展策略只有一个入口:`--extension-policy`
505
+ + 协议字段 `extensionPolicy`。
506
+ **测试/探针保真开关**:`MV_MC_TOOLS_STRICT=1` → 缺 MC 即报错退出。
507
+ 为什么需要它(测试阶段语义):降级虽可见,但"没降级"这件事在测试里必须可断言——
508
+ 否则测试可能在"没装 MC"的环境下通过,而 `ctx_search` 从未生效
509
+ (测试环境准确性优先;成版行为 = 允许降级)。
510
+
511
+ **主 pi 不受影响**(只改我们 spawn 的 agent 命令行)。
473
512
 
474
513
  **代价**:agents 用 pi 内置 read/write/edit/bash/grep/glob;无 `ctx_*` 与知识
475
514
  注入。零扩展真场里三视角自述:"内置工具胜任本任务、未因缺工具放弃或简化检查
@@ -0,0 +1,146 @@
1
+ <!-- 存档:docs/reviews/2026-09-14-e2e24-extension-policy-review.md
2
+ 来源:一次真实多视角分析的 result.md 原文(未删改,仅加本头与下方说明)。
3
+ 分析场次目录已随 --cleanup 删除;文中的消息编号(如 `铁律/0002`)不可再核验,
4
+ 仅作溯源线索(与代码注释引用约定一致:行为以自描述为准)。 -->
5
+
6
+ # 存档说明
7
+
8
+ - **主题**:审阅「扩展策略三档(默认 mc-tools)」的实现与证据链;并如实评估给
9
+ agents 提供 `ctx_search` 的实际价值与风险
10
+ - **场次**:`mv-mv-main-20260914-112746`(3 视角:效率 / 简单 / 铁律;真实 pi 讨论,
11
+ 以 `MV_MC_TOOLS_STRICT=1` 启动——确保本场确实带着 `ctx_search` 在跑)
12
+ - **抓到的问题**:
13
+ - **S1(高,生产可达)**:缺 MC 时的降级分支**提前 return** 截断命令构造 →
14
+ 在"无 MC 的机器"上产出残缺命令(缺 `--model`/`--print`/视角与协议注入、
15
+ spawn cwd 也错)——正是用户要求的"没装 MC 自动切回 none"那条路;
16
+ 两视角独立复现,我随后用零 LLM 复核确认。
17
+ - **S1a**:新增的那条测试只断言 token 有无 → 没抓到 S1(装置判别力不足)。
18
+ - **E2**:报告层没有"扩展策略(声明 vs 生效)"事实 → 无 MC 机器上的降级在
19
+ 产品面**看不见**(当天为确认"MC 生效没有"花了好几轮探针 + DB 查询)。
20
+ - **E1**:我在文档里写的"成本与 none 无差"**超出精度**(可比样本 n=2 vs n=3、
21
+ 组内方差 > 组间差)→ 应表述为"未测得显著差异"。
22
+ - **T1/T2/T3**:默认档翻转后的过时文本;"缺 MC 时响亮失败"与"可见降级"矛盾;
23
+ "agents 没有任何主项目背景通道"与 fork 模型矛盾(fork 本身就是主背景通道)。
24
+ - **F5/F6/F7/S2/F9**:解析链的 5 处缺陷(子串匹配可命中他包 / 首个候选失败即停 /
25
+ `pi.extensions` 字符串形态 / 条目形态解析两处 / pi agent 目录两处实现)。
26
+ - **ctx_search 的价值评估(第一手证据)**:9 次调用**全部由问卷诱导**、
27
+ **0 次决定性帮助**;命中 1 条**过期记忆**(指向已删机制);结构性解释 =
28
+ "检索的边际价值与 fork 新鲜度成反比"。
29
+ - **落地**:`91a1171`(Batch 1 修 S1 + 解析链重构 + 文本同步 + 报告"声明 vs 生效")·
30
+ `b9329fb`(Batch 2 删死代码:`--extensions` 别名 / `extensions: true` 兼容 /
31
+ 启动 loop 的 3 行转发)· 决策 20 同步(含 S1 教训与登记行)。
32
+
33
+ ---
34
+
35
+ # 多视角分析结果:审阅「扩展策略三档(默认 mc-tools)」的实现与证据链;并如实评估给 agents 提供 ctx_search 的实际价值与风险
36
+
37
+ - **参与者**:效率、简单、铁律(3 视角;resultWriter = 铁律)
38
+ - **分析类型**:审阅 + 建议(只读:未改文件、未跑测试、未启动分析)
39
+ - **主题来源**:`mv-spec-20260914-112728/question.md`
40
+ - **结论一句话**:三档机制**方向与分层正确**(值域/解析在 fs、命令与守卫在 loop、写入在 spec_gen、engine 不感知),但有两个必须分开的紧要项——**S1:默认档(mc-tools)的无 MC 降级路径是坏的**(提前 return 截断唤醒命令:缺 prompt/注入、cwd 错,生产可达)应**第一优先修复**;**E2:报告层没有"扩展策略(声明 vs 生效)"事实**,导致"本场到底带没带 ctx_search"只能靠外部考古,列第二。证据链本身有两处需改口(成本声称超精度;"agents 没有任何背景通道"过度陈述);**ctx_search 对"审阅本仓库代码"非必要能力**(9 次调用全部由问卷诱导、零决定性帮助、1 例过期记忆),但成本已实测有界(每唤醒启动中位 0.68s、收尾 0.04s),维持默认档 + 待自然场次数据复核。
41
+
42
+ ---
43
+
44
+ ## 运行元信息(本场)
45
+
46
+ | 项 | 值 | 来源 |
47
+ |---|---|---|
48
+ | 配置身份 | `extensionPolicy=mc-tools`(**首次生产运行**);`forkMode=budget`;`model=commandcode-goat/deepseek/deepseek-v4.1-flash`;`thinking=max`;`maxMeetingRounds=10`;`maxRRRounds=7`;`stallTimeoutSeconds=600` | `protocol.json` |
49
+ | 测试保真 | `MV_MC_TOOLS_STRICT=1` **实测在三个 loop 进程环境中**(不是"以为设了");wake-log 中 `-e <MC>/dist/subagent-entry.js` 在场 | `/proc/*/environ` + wake-logs |
50
+ | **终止原因** | **consensus**(RR 全员 pass,1 轮;非配额、非 stall) | loop 日志(引擎确定性事实) |
51
+ | 墙钟(首末 commit) | **853s ≈ 14.2 分钟**(setup → 铁律/0010;result 提交后略增) | `git log` |
52
+ | 唤醒 / 跨度 | **32 次唤醒**(效率 9 / 简单 12 / 铁律 11)——其中铁律第 11 次为写 result 的收尾唤醒(进行中);**已完成 31 次**的进程跨度合计 **2180s**,均 **70.3s**,max 238s(效率) | loop 日志 `elapsed_ms` |
53
+ | retry / stall | retry 3(每 agent 1 次,11:32 前后同批);stall 0 | loop 日志 |
54
+ | 配额使用 | 消息数:效率 6 / 简单 9 / 铁律 7(上限 10)→ **未撞配额** | 消息统计 |
55
+ | fork 源 | 601 条 / 丢弃 203 条 / est≈79k(字符/3)/ 构建 373ms | loop 日志 |
56
+ | **MC historian(生产验证)** | 三个 agent session 在 MC 账本中的 `subagent_invocations` 行数 **0/3**(+ 早前探针 0/11)——entry"只注册工具、不装 hook"在生产形态获得实证 | `context.db` |
57
+ | ctx_search 用量 | 本 agent 侧 **9 次**(效率 2 / 简单 3 / 铁律 4),**全部落在首唤窗口**;fork 源另含 2 次主会话继承的调用(**不属 agent 用量**——计数须按边界剔除继承历史) | session transcript 按边界核对 |
58
+
59
+ > 口径纪律:ctx_search 的统计只能在"边界之后"计数——fork 源携带的历史里有 2 次主 pi 自己的检索调用,全文件计数会把它们算成 agent 行为(本次核对中实测踩到过这个陷阱)。
60
+
61
+ ---
62
+
63
+ ## 一、确认问题清单
64
+
65
+ | 编号 | 严重度 | 文件:行 | 现象 | 依据 | 建议 |
66
+ |---|---|---|---|---|---|
67
+ | **S1** | **高(生产可达)** | `meeting_loop.py:340-342` | 缺 MC 的降级分支追加四个 `--no-*` 后 **`return cmd, session_dir` 提前截断**命令构造 → 缺 `--model`/`--thinking`/视角+协议 `--append-system-prompt`/`--approve`/`--print`,且 cwd 变成 `pi-sessions` 而非项目目录 | **两视角独立复现**(mock `resolve_mc_tools_entry` 失败):降级 11 项 / none 18 项、逐字相同=False、尾无 `--print`、cwd 错(简单/0001 + 铁律/0002) | 守卫先行 → **单一零扩展前缀** → mc-tools 只决定"能否追加 `-e`" → **删除提前 return** → 单一尾流程;验收四条(见下) |
68
+ | S1a | 中(S1 漏网根因) | `tests/test_meeting_loop.py:788-812` | 测试 docstring 把"命令形态 = 四个 `--no-*`、无 `-e`"写成验收口径;断言只查 token 有无,"等价 none 档"只存在于注释;配置全空值 | 读码 + 两视角核对 | 断言改为"降级返回与 none 返回**逐字同形**(除日志行)+ cwd=项目";配置带**非空** model/thinking/prompt_file;断言注释措辞与断言强度一致 |
69
+ | **E2**(=铁律 F8;效率 E2/E3) | 中 | `observability.py`(无 `extensionPolicy`/`mc-tools` 命中)+ `meeting_loop.py:337-339` + `start_discussion.py:424` | 报告层无"扩展策略(声明 vs 生效)"事实;降级说明只进 `loop-<agent>.log`;产品面只打印**声明**值 → 无 MC 机器上的降级在用户可见面静默;跨场成本/行为无法自动归因(今天为确认"MC 生效没有"付出多轮探针+DB 查询) | grep + 先例对照:thinking 已有"档位对照(声明 vs 生效)" | 报告头**一行**:`声明 / 生效(含真实降级原因)`(fail-open);strict 走 loop **启动时一行日志**(测试语义不进报告);若实施须同步 `docs/design.md` 观测面登记表 |
70
+ | T1 | 中 | `meeting_loop.py:313-314,323`;`start_discussion.py:558-559`;`docs/design.md:453` | 默认档翻转后的过时文本:"none(默认)…为什么默认它"、"代价:本档要求本机装有 MC"、help"none=零扩展(默认)"、"mc-tools…(需要本机装有 MC)"、决策 20 标题"agent 进程默认零扩展" | 常量已是 `mc-tools`(`meeting_fs.py:696`);代码实为"允许而非要求 + 可见降级" | 删除/同步为一句现规则(默认是谁 + 缺 MC 怎么办),规则本体指向 `DEFAULT_EXTENSION_POLICY` / 决策 20 |
71
+ | T2 | 中 | `AGENTS.md:75`、`docs/design.md:486-487`;`meeting_fs.py:94-95`;`tests/test_meeting_loop.py:783` | "缺 MC 时**响亮失败**、不静默退回 none"残留句(与已改的"允许降级"矛盾);`resolve_mc_tools_entry` 契约句"调用方必须报错"与调用方实际可见降级矛盾;测试注释旧契约残留 | 读文件行 | **删残留行为句**(依赖边界句已写正确行为);契约句改为"返回故障原因,由调用方按策略决定报错/可见降级" |
72
+ | T3 | 中 | `docs/design.md:480` | "背景蒸馏机制移除后,agents **没有任何主项目背景**通道"——**与 fork 模型矛盾**:fork 自身就是主背景通道(本场实例:agent fork 到的开发上下文远比检索有用);`AGENTS.md:62` 的"补充通道"措辞才准确 | 读码 + 本场三视角一致同意 | 改为"fork 是主通道;覆盖面受 ① fork 源内容 ② `budget` 裁剪限制;`ctx_search` 为兜底";**result 不得复制绝对句**(本场 question.md 已扩散过一次) |
73
+ | E1(证据) | 中 | `AGENTS.md:73`、`docs/design.md:484`(及 question.md) | "成本与 none 无差(受控 6 次:中位 7.8s vs 9.2s,差在噪音内 ✓)"**超出精度**:可比样本仅 n=2 vs n=3(左组混入 mcprobe2 bootstrap 源 A 臂 3.9s),组内方差 > 组间差 | 探针原始记录(`/tmp/mcprobe2/run.py` 臂定义 + mcprobe3 结果) | 改口为"**未测得显著差异**(n 小、组内方差>组间差)",并补生产基线(见下);长期由「唤醒构成表」启动段序列承担 |
74
+ | F5 | 低 | `meeting_fs.py:110` | `MC_PACKAGE in name` 为**子串匹配**;模拟注册 `@cortexkit/pi-magic-context-legacy`(带 sibling 入口)会被命中 → 潜在**静默加载他包** | 临时目录模拟实测(铁律/0002) | 解析裸包名后 `==` 比较 |
75
+ | F6 | 低(待验) | `meeting_fs.py:109-112` | 首个匹配条目解析失败即 `break`;模拟 `npm:…@0.1.0`(目录缺)+ `npm:…`(有效)→ 报"没有可解析"并忽略有效次条(文案误导) | 临时目录模拟实测 | 遍历全部候选取首个可解析;错误文案列候选 |
76
+ | F7 | 低 | `meeting_fs.py:123` | `pi.extensions` 非列表(字符串)时 `[0]` 取首字符 → 解析错路径后报"版本可能改了布局"(**方向误导,但不静默**) | 模拟实测 | 显式类型校验/兼容字符串 |
77
+ | S2 | 低 | `meeting_fs.py:70-73` 与 `107-110` | packages 条目形态解析写两遍、宽严不一(空白串处理不同) | 读码 | 抽 `_entry_source()`(≤3 行)两处共用 |
78
+ | F9 | 低(预存在) | `meeting_fs.py:133-137` vs `spec_gen.py:20` | pi agent 目录解析**两处实现**(docstring 自认同规则) | 读码 | 收敛一处(spec_gen 引 `meeting_fs.pi_agent_dir`) |
79
+ | S3 | 低 | `start_discussion.py:714-715` | CLI `--extension-policy` 向 loop 的转发**冗余**(`gen_protocol` 恒写协议字段,loop 协议后读覆盖 CLI);例外:旧协议目录无字段 + `--skip-setup` 时转发是唯一通道 | 读码 + grep(tests 无引用) | 删 3 行 + 一行文档("旧目录按默认档")——**需用户点头**(Batch 2) |
80
+ | S5 | 低(决策) | `start_discussion.py:560-562`;`meeting_loop.py:610-611,623-624` | `--extensions` 别名 + `extensions: true` 历史字段兼容(3 处),为只存在约 1 天的字段而设、**无移除条件** | git:`98786d6` 引入 → `47fbcd6` 取代;先例:`--pure` 同日全删 | 现在删,或写明移除条件——**需用户点头**(Batch 2) |
81
+ | E4("不要做") | 低 | `meeting_loop.py:328` → `meeting_fs.resolve_mc_tools_entry`(:85) | 每唤醒 ms 级解析(settings+package.json+目录探测)——**不要为它加缓存** | 失效复杂度(多份 MC/升级/相对路径)> 收益(毫秒) | 以"零机制"字样记入决策,防后手 |
82
+
83
+ **正面实测(职责边界合规)**:entry 的硬排除在生产成立——`ctx_expand` 调不到(`Tool ctx_expand not found`,效率实测);"工具面收窄"不是纸面声明。
84
+
85
+ ## 二、风险与可检测性
86
+
87
+ | 风险 | 触发条件 | 能否被检测 | 检测点 |
88
+ |---|---|---|---|
89
+ | **S1 降级路径缺 prompt** | **默认档 + 无 MC**(或 F6 使装着 MC 的机器走进来) | 修复前:仅 loop 日志的"无产出重试"间接暴露;修复后:命令形状与 none 同形 | P0 修复 + 验收四条 |
90
+ | MC 升级改布局 | `pi install` 升级/换机 | 能(**fail-safe**:解析失败→零扩展,不会加载错版本) | 目前 loop log 一行;E2 落地后报告可见 |
91
+ | F5 子串匹配加载他包 | 存在同前缀包且带 sibling 入口 | **否(静默)** | 无(需 F5 修复) |
92
+ | 行为漂移(工具面/prompt surface) | 后续改动 | 能(事后) | session transcript 按工具名计数 + 唤醒构成表;本场 9 次全在首唤、无异常(n=19),探针 90s 级异常**未复现**(支持"探针装置产物"判断,但单场 n=19,不作定论) |
93
+ | **陈旧知识注入**(ctx_search) | 记忆滞后于代码 | 仅能事后读 session(无自动检测) | 已观测 1 例:简单检索到 `#765`(作用域配置目录)——该机制前一天已删;"计数≠utility"的实例 |
94
+ | MC DB 争用(3 agents + 主 pi 并发写) | 并发 | 未测 | 无 |
95
+ | strict 假阳性(测试/探针忘设严格) | 无 MC 机器 + 忘设 `MV_MC_TOOLS_STRICT` | 修复前:否;修复后:loop 启动一行 `strict=1/0` + 报告降级行 | P1 落地 |
96
+
97
+ ## 三、ctx_search 使用情况与价值评估(附加题 1–4,三视角合并)
98
+
99
+ 1. **用过吗**:用过——本 agent 侧共 **9 次**(效率 2 / 简单 3 / 铁律 4),**全部在首唤窗口**;另 fork 源含 2 次主会话继承调用(不计入)。三家查询词均围绕"本场决策/近期改动"。
100
+ 2. **帮到了吗:0 次决定性帮助**。
101
+ - 代表性结果:效率①问 mc-tools 实测记录 → 返回 6 条"相邻记忆"(score 0.64–0.66)**不含所问事实**;简单三次共 15 条**无一回答查询**,并命中 **1 条过期记忆**(`#765` 指向已删的作用域配置机制——若信任即拿到错误背景);铁律 4 次中 2 次 message 源 **0 结果**,1 次命中相关规则(探针纪律 #752)但原文在仓库文档里更权威。
102
+ - 结构性解释(效率):**ctx_search 的边际价值与 fork 新鲜度成反比**——本类 agents 总是 fork 自刚工作完的主会话,背景已在 fork 窗口内。
103
+ 3. **工具面**:内置 read/bash/edit/write + `ctx_search`(+ `todowrite` 等按配置);`ctx_note`/`ctx_expand` 实测不可调(entry 的硬排除,符合设计);**无工具打扰**——三家均自选动作,未被迫管理上下文;检索是**一次性定向**(全部首唤),不是每唤醒重复成本。
104
+ 4. **诚实评估**:对"审阅本项目代码"这类任务,**ctx_search 可有可无、当前证据偏"非必要"**。依据:① 事实源是文件+git,仓库规模下 read/grep 直接命中;② 它最该发挥的"跨会话背景"场景本场未出现正面样本(且 message 源今日数据未索引到);③ 它有真实的反向风险(陈旧知识注入,已观测实例);④ 能力上界:**它是指针不是阅读替代**(search-only、无下钻,snippet 即全部)。
105
+ - **处置共识**:保持默认 mc-tools(成本已实测有界:每唤醒启动中位 **0.68s**(max 0.75)/ 收尾中位 **0.04s**(max 0.10),n=19,无异常);**回退评估规则 pending**——只在"**自然使用**"(排除问卷/指示诱导)登记,连续 ≥2 场 utility≈0 → 提议回退默认 `none`(mc-tools 留 opt-in);**回退本身属产品决策,需用户点头**。
106
+ - ⚠️ 本场 9 次调用**全部由附加题诱导**(为回答问卷而做),**不构成需求证据**;本场数据同样不能校准该规则。
107
+
108
+ ## 四、只改一处 → **S1**(三方一致;第二处 = E2)
109
+
110
+ **理由**:S1 是本次审阅唯一"**默认档生产可达的功能破损**"——用户显式要求的"没装 MC 自动切换回 none"走的正是这条路径;它同时是**复杂度问题**(四个 `--no-*` 抄三遍 + 短路)与**设计符合度问题**(`docs/design.md:488-492` 承诺"降级为零扩展并**按 none 运行**",实际是残缺命令)。修复**净减代码**(前缀提为单一构造、删提前 return),修改约 15 行 + 验收四条。
111
+ 第二处 = **E2 一行**(报告"声明 vs 生效 + 真实降级原因"):S1 修好后,降级至少是"可用的 none",但"本场带没带 ctx_search"仍需产品面可见。
112
+
113
+ **S1 验收四条(定稿,全部零 LLM)**:
114
+ 1. `_build_wake_cmd` 降级返回与 none 返回**逐字同形**(除日志行)+ cwd=项目;
115
+ 2. 装置配置带**非空** model/thinking/prompt_file(空值会削弱这两项的判别力);
116
+ 3. 在**既有 Popen-mock harness** 上断言 argv/cwd(`tests/test_meeting_loop.py:108-109/137/348`)——**不新增假 pi 装置**(仓库无 stub-pi;新装置判别力≈0、只加失败面);若两层被认为重复,保底保留第 1 条;
117
+ 4. 未知档**直调** `_build_wake_cmd` → raise;同批删 `meeting_loop.py:345` 的 `# pragma: no cover`。
118
+
119
+ ## 五、数据缺口(要测什么、怎么测)
120
+
121
+ | 缺口 | 现状 | 补法 |
122
+ |---|---|---|
123
+ | 降级路径端到端 | 缺(S1 漏网与"允许降级"承诺的共同盲区) | 验收四条(零 LLM、既有装置);**构造层断言必须保留** |
124
+ | `none` 档启动段基线 | 缺(e2e23 早于构成表功能) | 下次任一 none 场自动获得(唤醒构成表启动段);与 0.68s 对照即得 MC 边际 |
125
+ | 自然场次 usage/utility | 本场 9 次被问卷污染,不可校准 | **零机制 join**:fork 裁剪比例(loop log)× 检索计数(transcript)按场登记;**前提**:`--cleanup` 会 `rmtree(base)` 连数据源一起删(`start_discussion.py:458`)→ 须**清理前**把两个数抽进 result 固定小节;join 只检验 usage 侧预测,**utility 仍人工判读** |
126
+ | 跨场漂移(DB 增长/并发) | 无序列 | 连续 ≥3 场记录启动段中位数(报告已含);MC DB 争用未测(单列) |
127
+
128
+ ## 实施批次(三方共识)
129
+
130
+ - **Batch 1(无需用户决策)**:**S1**(守卫先行 + 单一零扩展前缀 + 删提前 return)+ 验收四条 + **文本同步**(T1/T2 删残留句、T3 `design.md:480` 改"主通道+条件限制+兜底"、E1 成本改口)+ **解析链一次重构**(S2/F5/F6/F7/F9;约束:只服务 MC 一个具名包、总长 ~60 行内、不引入推测性抽象)+ F9。
131
+ - **Batch 2(需用户点头)**:S3 转发删除(含"旧目录按默认档"文档)+ S5 兼容别名/历史字段删除(或写明移除条件)。
132
+ - **Batch 3(可选增强)**:E2 报告一行(三字段;strict 走启动日志;同步观测面登记表)。
133
+
134
+ ## 证据分级(防混用)
135
+
136
+ - **机制事实(源码/读码)**:entry 只注册工具不装 hook;S1 截断位置;F5/F6/F7 行为(模拟复现);E2 报告缺字段。
137
+ - **生产实测(本场,strict=1)**:MC historian **0/3**(账本);ctx_search **9 次全首唤**(按边界核对);成本基线 n=19(启动中位 0.68s / 收尾中位 0.04s / 无异常)——**单场样本**,跨场由构成表序列承担。
138
+ - **受控探针**:historian 0/11(两批 11 个探针 session);C 臂 `ctx_search` 真实返回结果;成本探针对照"**未测得显著差异**"(n=2 vs n=3)。
139
+ - **估算(不得当实测用)**:S1 残命令若触发,"每次唤醒最多 600s / 整场报废"——S1 立论不依赖它("缺 prompt 的每次唤醒"已足够)。
140
+ - **推测**:探针 D 臂 90s 异常"因提示词非生产形态"——合理未证;生产 n=19 未复现。
141
+
142
+ ## 过程记录(收敛路径)
143
+
144
+ 铁律/0001 首轮(F1–F9 + 证据分级)→ 简单/0001 独立发现 **S1**(高)→ 效率/0001(E1–E5)→ 铁律/0002 独立复核 S1 并**改选优先序**(撤回 F8 第一)→ 简单/0002、铁律/0003 定 S1 修法(守卫先行/单一前缀)→ 效率/0002、/0003 生产实测(启动/收尾成本)与撤回审计建议 → 铁律/0004 裁定 E2 范围(不并审计计数)+ 指出 usage 度量被问卷污染 → 简单/0005 修法定稿、反对计数进报告、修正验收③(弃假 pi 用 Popen-mock)→ 效率/0005、/0006 全部接受并补零机制 join → 铁律/0005(strict 折中三条件 + `design.md:480` 修正)、/0006(验收③定稿 + n=16 口径)、/0007(零机制 join 的清理前前提 + result 口径承诺)→ 三视角 freezing → RR 全员 pass(效率/0009、简单/0012、铁律/0010)。
145
+
146
+ **遗留(需用户)**:S5 兼容去留;usage 回退规则(pending,待自然场次);E4"零机制"记档。
@@ -30,6 +30,7 @@
30
30
  | `2026-09-13-e2e20-pure-run-review.md` | 审阅 `c2be72d` 的 9 条修复(**该场 agents 零扩展**) | 边界**子串**误判把 fork 历史算进本轮(报告虚高 4–8 倍,也是"Δ 合计 > 墙钟"之谜根因);单趟 strip 让注释含引号的输入解析失败(功能回归);Δ 口径 B 归因被证伪 | `5e02a67` |
31
31
  | `2026-09-13-e2e21-postfix-review.md` | 审阅 `5e02a67`(并验证报告口径修复) | AFT legacy 检测 4 组路径**多一层 `aft/`**(死检查 + 静默假阴性);子串预筛是对设计的错误陈述;`n`/`sec` 分母不一致导致均值低估 | `5cc0b5e` 之后的批 |
32
32
  | `2026-09-13-e2e23-time-breakdown-analysis.md` | 一次分析的**时间构成**(哪些必要、哪些可省) | AFT / MC historian 两笔分钟级开销都不在报告里;反对"不等退出就推进"与"新增常驻机制" | `98786d6` + `c46d996` / `9deefac` |
33
+ | `2026-09-14-e2e24-extension-policy-review.md` | 扩展策略三档(默认 mc-tools)的实现与证据链 + `ctx_search` 价值评估 | **S1:缺 MC 的降级路径提前 return → 命令被截断(缺 model/print/注入、cwd 错)**;S1a 测试判别力不足;E2 报告缺"声明 vs 生效";E1 成本口径超出精度;T1–T3 文本矛盾;F5/F6/F7/S2/F9 解析链缺陷;**ctx_search 9 次调用全为问卷诱导、0 次决定性帮助、命中 1 条过期记忆** | `91a1171`(Batch 1+3)、`b9329fb`(Batch 2 删死代码)|
33
34
 
34
35
  ## 环境口径(读报告时的背景)
35
36
 
@@ -417,3 +417,36 @@ session。用户先发现的("我看到不少 pi 的 session 的残留")。
417
417
 
418
418
  **边界(诚实)**:闸门只挡 `pi-probe.sh` 这条路径——直接调 `pi` 仍可绕过。
419
419
  它的价值是让"绕过"成为**显式动作**(要故意跳过提示),而不是顺手。
420
+
421
+ ### 26. 探针三纪律:源带交接叙事 · 提示词用生产形态 · 数据不干净就说"未测"(2026-09-14 探针跑飞)
422
+
423
+ **触发事件(两次,同一根因家族)**:为验证"MC 的 `subagent-entry` 能否只给
424
+ `ctx_search` 而不带 historian",我写了以 **fork 源**(主会话裁剪)为输入、提示词
425
+ 只有一句"只回复:ok"的探针。结果:
426
+
427
+ | 次 | 现象 | 根因 |
428
+ |---|---|---|
429
+ | 第一次 | 探针进程**扮演我继续做调查**(跑 bash 查进程/session、写 FINDINGS.md、杀掉我的 run.py)→ 跑满数分钟、阻塞我的会话、三臂只跑了半臂 | fork 源**没有注入生产的"切换叙事"**(`append_handoff_turns` 的 2 对对话)→ 历史惯性把模型拉回"上一件任务" |
430
+ | 第二次(修正装置后) | 4 次"fork + entry"里仍有 1 次跑飞(原文:它开始 `echo "=== ① 探针进程是否还在 ==="`、查 fork 源 session 头……);"fork + 零扩展"4 次全干净 | **提示词不是生产形态**:生产 wake prompt 有"身份锚定 + 单一任务 + 禁止监控",我的探针只有一句干话 |
431
+
432
+ **另外的代价**:那批数据我拿去解释机制("MC 43 倍 / 447s vs 10.3s"),后来发现
433
+ 装置有缺陷 → **按用户指正:"你不能没有准确进行测试,就完全靠猜测,那就没有测试
434
+ 的必要了"**。真正干净的证据只有**生产路径的真场**(e2e21 330s/唤醒 + 62 次
435
+ historian vs e2e23 48.1s/唤醒 + historian 0)与 **MC 自己的 DB 账本**。
436
+
437
+ **三纪律(写成条目,不只"下次注意")**:
438
+
439
+ 1. **以 fork 源为基础的探针必须注入交接叙事**(`meeting_fs.append_handoff_turns`
440
+ 或直接用生产 `wake_llm` 路径)。否则探针在结构上"就是我"——它会继续我的任务,
441
+ 而不是回答探针问题。
442
+ 2. **探针的提示词必须用生产形态**(身份锚定 + 单一任务 + 禁止动作),否则"跑飞"
443
+ 这个观察**不可归因**:分不清是被测对象的行为、还是提示词缺了生产防线。
444
+ 3. **探针必须自证范围**:跑完检查"边界之后的新增条目数 ≤ N"、"只出现被允许的
445
+ 工具调用"、"回复就是被要求的那一句"。**做不到这三项 → 该次数据作废**,不许
446
+ 写进结论(宁可写"未测",也不许把装置有缺陷的数字当机制解释)。推论:**引用
447
+ 数字时必须能说清装置**(哪种源、什么提示词、几次重复、硬超时多少)。
448
+
449
+ **落地形态(本次已用)**:干净 `build_bootstrap` 源(结构上不可能跑飞)做功能
450
+ 验证 + fork 源(带交接叙事)做规模/成本验证;每臂硬超时;统计只算**边界之后**的
451
+ 条目(继承历史的 toolCall 不算——第一版踩过);同条件重复 ≥3 次压 provider 方差
452
+ (单样本差异被噪音淹没的实例:同条件 3.9s vs 44s,11 倍)。
package/meeting_fs.py CHANGED
@@ -60,11 +60,131 @@ DEFAULT_STALL_TIMEOUT = 600
60
60
  DEFAULT_THINKING = "max"
61
61
 
62
62
 
63
+ def _entry_source(entry):
64
+ """pi 的 packages 条目 → 源字符串(两种形态共用;非字符串形态 → "")。
65
+
66
+ 形态(实测):裸字符串 `"npm:@scope/name"` / 相对路径 `"../../repo"`;
67
+ 对象形态 `{"source": "..."}`。**单一实现**(此前包目录解析与 MC 匹配各写
68
+ 一遍、宽严不一)。
69
+ """
70
+ if isinstance(entry, str):
71
+ return entry.strip()
72
+ if isinstance(entry, dict):
73
+ src = entry.get("source")
74
+ return src.strip() if isinstance(src, str) else ""
75
+ return ""
76
+
77
+
78
+ def _npm_package_name(source):
79
+ """`npm:` 源 → 裸包名(去版本后缀);非 npm 源 → ""。
80
+
81
+ `npm:@scope/name@1.2.3` → `@scope/name`;`npm:name` → `name`。
82
+ (F5:包匹配必须**按裸包名精确相等**,不能用子串——`in` 会把
83
+ `@cortexkit/pi-magic-context-legacy` 也命中。)
84
+ """
85
+ if not source.startswith("npm:"):
86
+ return ""
87
+ body = source[len("npm:"):]
88
+ if body.startswith("@"): # scoped:@scope/name[@ver]
89
+ at = body.rfind("@")
90
+ return body[:at] if at > 0 else body
91
+ at = body.find("@") # 非 scoped:name[@ver]
92
+ return body[:at] if at > 0 else body
93
+
94
+
95
+ def _package_dir(source, agent_dir):
96
+ """源字符串 → 包目录(不存在 → None)。
97
+
98
+ "npm:@scope/name" → <agent_dir>/npm/node_modules/@scope/name
99
+ "../../repo-name" → 相对 <agent_dir> 解析(绝对路径原样)
100
+ """
101
+ if not source:
102
+ return None
103
+ if source.startswith("npm:"):
104
+ name = _npm_package_name(source)
105
+ if not name:
106
+ return None
107
+ cand = os.path.join(agent_dir, "npm", "node_modules", *name.split("/"))
108
+ else:
109
+ cand = source if os.path.isabs(source) else os.path.join(agent_dir, source)
110
+ cand = os.path.normpath(cand)
111
+ return cand if os.path.isdir(cand) else None
112
+
113
+
114
+ def resolve_mc_tools_entry(agent_dir=None):
115
+ """解析 MC 的**只读工具入口**(`dist/subagent-entry.js`)——"mc-tools" 档用。
116
+
117
+ 为什么这样解析而不硬编码路径:MC 自己就是用"主入口的**兄弟文件**"
118
+ (其源码 `resolveSiblingEntryPath("subagent-entry.js")`)定位它。我们的
119
+ 等价做法 = 从 pi 的注册表(settings.json.packages)找到 MC 包目录 → 读它
120
+ package.json 声明的扩展入口(`pi.extensions[0]`)→ 取同目录下的
121
+ subagent-entry.js。
122
+
123
+ **失败语义**:任一步缺失返回 `(None, 原因)`——**由调用方按策略决定**:
124
+ loop 在生产态做**可见降级**(打印一行说明后按零扩展运行 ✓ 无静默),
125
+ 在严格态(`MV_MC_TOOLS_STRICT=1`,测试/探针保真)直接报错。
126
+
127
+ 依赖边界(决策 20):mc-tools **允许而非要求** MC——缺 MC 即降级为零扩展;
128
+ `none` 档零依赖(无 MC 的机器/CI 显式选它)。
129
+ """
130
+ agent_dir = agent_dir or pi_agent_dir()
131
+ try:
132
+ with open(os.path.join(agent_dir, "settings.json"), encoding="utf-8") as f:
133
+ pkgs = json.load(f).get("packages") or []
134
+ except (OSError, ValueError) as e:
135
+ return None, f"读不到 pi 的 packages({agent_dir}/settings.json): {e}"
136
+ # F5:按**裸包名精确相等**识别(npm 源直接比;路径源读其 package.json.name);
137
+ # F6:遍历**全部**候选、取首个可解析(此前首个匹配失败即停,装着 MC 也会
138
+ # 报"没装",文案误导)
139
+ seen = [] # 候选(source 字符串)——用于错误文案,说明"试过哪些"
140
+ for entry in pkgs:
141
+ source = _entry_source(entry)
142
+ if not source:
143
+ continue
144
+ pkg_dir = _package_dir(source, agent_dir)
145
+ if not pkg_dir:
146
+ continue
147
+ name = _npm_package_name(source)
148
+ if not name: # 路径源:读它的包名(读不到就跳过)
149
+ try:
150
+ with open(os.path.join(pkg_dir, "package.json"),
151
+ encoding="utf-8") as f:
152
+ name = json.load(f).get("name") or ""
153
+ except (OSError, ValueError):
154
+ name = ""
155
+ if name != MC_PACKAGE:
156
+ continue
157
+ seen.append(source)
158
+ try:
159
+ with open(os.path.join(pkg_dir, "package.json"), encoding="utf-8") as f:
160
+ man = json.load(f)
161
+ except (OSError, ValueError) as e:
162
+ return None, f"读不到 {MC_PACKAGE} 的 package.json: {e}"
163
+ exts = (man.get("pi") or {}).get("extensions")
164
+ if isinstance(exts, str): # F7:字符串形态(取首字符会解析错路径)
165
+ exts = [exts]
166
+ if not isinstance(exts, list) or not exts or not all(
167
+ isinstance(x, str) and x for x in exts):
168
+ return None, f"{MC_PACKAGE} 的 pi.extensions 形态不可用(版本不兼容?)"
169
+ cand = os.path.normpath(
170
+ os.path.join(pkg_dir, os.path.dirname(exts[0]), "subagent-entry.js"))
171
+ if os.path.isfile(cand):
172
+ return cand, ""
173
+ # 该候选不可解析 → 继续看后面的候选(F6)
174
+ last_missing = os.path.relpath(cand, pkg_dir)
175
+ if seen:
176
+ return None, (f"{MC_PACKAGE} 的只读工具入口不存在({last_missing})"
177
+ f"——上游版本可能改了布局")
178
+ return None, (f"packages 里没有可解析的 {MC_PACKAGE}——装它"
179
+ f"(pi install npm:{MC_PACKAGE}),或改用零扩展档:"
180
+ f"--extension-policy none")
181
+
182
+
63
183
  def pi_agent_dir():
64
184
  """pi 的 agent 目录(`$PI_CODING_AGENT_DIR` 或 `~/.pi/agent`)——**单一实现**。
65
185
 
66
- spec_gen.PI_AGENT_DIR 同规则;本模块需要它的地方(会话登记日志、
67
- 扩展入口解析)都走本函数,避免两处各拼一次。
186
+ **单一实现**:spec_gen / 本模块(会话登记日志、扩展入口解析)都走本函数
187
+ (F9:此前 spec_gen 自持常量、两处各拼一次;测试改为 patch 环境变量)。
68
188
  """
69
189
  return os.environ.get("PI_CODING_AGENT_DIR") or os.path.expanduser("~/.pi/agent")
70
190
 
@@ -610,6 +730,39 @@ def parse_log_nameonly(output):
610
730
  # 两种语义角色(勿混):FORK_MODES 是**处理哪个模式**(分派仍用字面量);
611
731
  # DEFAULT_FORK_MODE 是**缺省填谁**(仅默认值位置,全仓引此常量)。
612
732
  # 历史值 active/curated(rename 前)按非法值处理(解析入口即报错)。
733
+ # ---- agent 进程的扩展策略(design.md 决策 20)----
734
+ # 三种语义(勿混):EXTENSION_POLICIES 是**合法值域**(分派与值域守卫引它);
735
+ # DEFAULT_EXTENSION_POLICY 是**缺省填谁**(全仓引此常量)。
736
+ # mc-tools : **默认**——只要 MC 的**只读检索工具**(ctx_search)。为什么默认它:
737
+ # agents 需要主项目背景(背景蒸馏机制已移除),这是它的补充通道;
738
+ # 该入口**只注册工具、不装 hook** → historian/压缩不在其中
739
+ # (受控实测 historian 0/6、ctx_search 可用;成本未测得显著差异)。
740
+ # 代价:本档**允许而非要求** MC(缺则可见降级为零扩展;严格模式见
741
+ # MC_TOOLS_STRICT_ENV)
742
+ # none : 零扩展——最快、**零依赖**(不依赖任何扩展;无 MC 的机器/CI 用这档)
743
+ # all : 走 pi 默认扩展发现(A/B 实验与显式 opt-in 用)
744
+ # (顺序只影响 CLI 帮助的罗列——**不承载语义**,勿按下标取值:
745
+ # 默认档看 DEFAULT_EXTENSION_POLICY)
746
+ EXTENSION_POLICIES = ("mc-tools", "none", "all")
747
+ DEFAULT_EXTENSION_POLICY = "mc-tools"
748
+ # mc-tools 档**允许**(而非要求)MC:找不到 MC 时降级为零扩展,但必须**可见**
749
+ # (打印一行说明 `ctx_search` 本次不可用)——无静默铁律。
750
+ # 测试/探针要保真(确认"本场确实带着 ctx_search 在跑")时,用环境变量把它变严格:
751
+ # MV_MC_TOOLS_STRICT=1 → 解析失败即报错退出(测试环境准确性优先,用户 2026-09-14 定)
752
+ MC_TOOLS_STRICT_ENV = "MV_MC_TOOLS_STRICT"
753
+
754
+
755
+ def mc_tools_strict():
756
+ """严格模式?(仅测试/探针用;生产默认 False = 允许降级)
757
+
758
+ 为什么需要它:降级是**可见的**,但"没降级"这件事在测试里必须能被断言——
759
+ 否则一个测试可能"通过"而实际上 ctx_search 从未安装(测试环境准确性)。
760
+ """
761
+ return os.environ.get(MC_TOOLS_STRICT_ENV) == "1"
762
+ # "mc-tools" 档引用的包(该档 = 那个包的能力,故具名引用而非通用机制:
763
+ # 入口是它的内部文件,路径解析见 resolve_mc_tools_entry 的 docstring)
764
+ MC_PACKAGE = "@cortexkit/pi-magic-context"
765
+
613
766
  FORK_MODES = ("budget", "compaction", "full")
614
767
  DEFAULT_FORK_MODE = "budget"
615
768
  #
package/meeting_loop.py CHANGED
@@ -4,7 +4,7 @@
4
4
  复用 meeting_engine 的唯一状态机,只注入"唤醒 pi"的 responder。
5
5
  协议逻辑(锁/配额/级联/信号)全在引擎,此处只做 LLM 交互。
6
6
 
7
- 用法:python3 meeting_loop.py <workdir> <agent> [--extensions]
7
+ 用法:python3 meeting_loop.py <workdir> <agent> [--extension-policy V]
8
8
  (配额/超时从 protocol.json 读——单一事实源;无 CLI 覆盖)
9
9
  """
10
10
 
@@ -289,7 +289,7 @@ def _prepare_fork_session(workdir, agent, sid, fork_source, fork_cwd,
289
289
 
290
290
 
291
291
  def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
292
- session_dir, first_wake, with_extensions, prompt,
292
+ session_dir, first_wake, extension_policy, prompt,
293
293
  fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
294
294
  """组装唤醒命令(#3 拆分,e2e7 评审):返回 (cmd, spawn_cwd)。
295
295
 
@@ -309,19 +309,50 @@ def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
309
309
  else:
310
310
  cmd = ["pi", "--mode", "json", "--session-id", sid,
311
311
  "--session-dir", session_dir]
312
- # ---- agent 进程的扩展策略(design.md 决策 20)----
313
- # **默认 = 零扩展**(关扩展/技能/prompt-template/主题;保留内置工具与项目内
314
- # AGENTS.md)。为什么:两类插件在**我们这种 session 形态**上都是分钟级负担,
315
- # 且都在关键路径上(loop 等进程退出才继续)——
316
- # · AFT:大 session 上进程退出前多活数分钟(受控对照 445.9s 0.5s);
317
- # · MC:它的 historian 对"带着大段未处理历史"的 session **每次必失败并立刻
318
- # 重试**(受控对照:同输入 447s → 10.3s,43 倍)。
319
- # 零扩展实测(真场):墙钟 12m31s / 每次唤醒 48.1s / 收尾≈0% / historian 0 次。
320
- # 加回扩展属**显式 opt-in**(`--extensions`,协议字段 `extensions: true`),
321
- # 且应有净收益账(见 design.md 决策 20 的门槛条款)。
322
- if not with_extensions:
323
- cmd += ["--no-extensions", "--no-skills", "--no-prompt-templates",
324
- "--no-themes"]
312
+ # ---- agent 进程的扩展策略(design.md 决策 20;三档见 meeting_fs)----
313
+ # 三档共用**同一段零扩展前缀**;mc-tools 只决定"能否再追加 -e <MC 入口>"。
314
+ # 为什么这样写:此前 mc-tools 的降级分支自己拼前缀后**提前 return**,把后面的
315
+ # model/thinking/system-prompt/--print 与 spawn cwd 全截断——那条"降级"路径在
316
+ # MC 的机器上产出残缺命令(e2e24 评审 S1;根因 = 前缀抄三遍 + 短路返回)。
317
+ # 现在**只有一个出口**(本函数末尾),降级只是"少追加一个 -e"
318
+ #
319
+ # 三档语义:mc-tools(默认)给 agents `ctx_search`(MC 的只读检索工具;
320
+ # entry 只注册工具、不装 hook → 不带 historian/压缩);none 零扩展(零依赖);
321
+ # all pi 默认发现(A/B 与显式 opt-in,须有净收益账)。
322
+ no_ext = ["--no-extensions", "--no-skills", "--no-prompt-templates",
323
+ "--no-themes"]
324
+ effective_policy = extension_policy
325
+ downgrade_reason = ""
326
+ if extension_policy not in meeting_fs.EXTENSION_POLICIES:
327
+ # 值域守卫在 __main__ 已拦(配置错误不进 engine);直调也要响
328
+ raise RuntimeError(f"未知 extensionPolicy: {extension_policy!r}"
329
+ f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})")
330
+ if extension_policy == "none":
331
+ cmd += no_ext
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
346
+ # "all":不加任何 --no-*(走 pi 默认发现)
347
+ if first_wake:
348
+ # 登记行(观测面的稳定字段;报告据此给"声明 vs 生效")。只在首唤打:
349
+ # 策略在一次运行内不变,变了也是配置错误(重跑即可)。
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 ""))
353
+ if downgrade_reason:
354
+ log(agent, f"mc-tools 档未生效({downgrade_reason})——本次按零扩展"
355
+ f"运行:ctx_search 不可用")
325
356
  model = cfg.get("model") or ""
326
357
  if model:
327
358
  cmd += ["--model", model]
@@ -406,8 +437,9 @@ def _run_wake_proc(cmd, spawn_cwd, workdir, agent):
406
437
  err or "")
407
438
 
408
439
 
409
- def wake_llm(workdir, agent, prompt, with_extensions=False, fork_source=None,
410
- fork_cwd=None,
440
+ def wake_llm(workdir, agent, prompt,
441
+ extension_policy=meeting_fs.DEFAULT_EXTENSION_POLICY,
442
+ fork_source=None, fork_cwd=None,
411
443
  fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
412
444
  """唤醒 pi(fork-only:首唤由本地生成 fork 源 + `--session` 打开,
413
445
  后续 `--session-id` 续接)。返回 (sessionID, returncode)。
@@ -432,7 +464,7 @@ def wake_llm(workdir, agent, prompt, with_extensions=False, fork_source=None,
432
464
  sid = str(uuid.uuid4())
433
465
  cmd, spawn_cwd = _build_wake_cmd(workdir, agent, sid, cfg, fork_source,
434
466
  fork_cwd, session_dir, first_wake,
435
- with_extensions, prompt, fork_mode, topic)
467
+ extension_policy, prompt, fork_mode, topic)
436
468
 
437
469
  log_dir = os.path.join(base, "wake-logs")
438
470
  os.makedirs(log_dir, exist_ok=True)
@@ -509,7 +541,7 @@ def _read_perspective_brief(workdir, agent):
509
541
  return brief or None
510
542
 
511
543
 
512
- def make_responder(with_extensions, fork_source=None, fork_cwd=None,
544
+ def make_responder(extension_policy, fork_source=None, fork_cwd=None,
513
545
  fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
514
546
  """构造真实 LLM responder:唤醒 pi,LLM 写内容文件。
515
547
 
@@ -534,7 +566,7 @@ def make_responder(with_extensions, fork_source=None, fork_cwd=None,
534
566
  if mem_available_mb() < MIN_MEM_MB:
535
567
  log(agent, "内存不足——抛可恢复异常(不代写 freezing,下轮重试)")
536
568
  raise RecoverableWakeError("内存不足")
537
- wake_llm(workdir, agent, prompt, with_extensions,
569
+ wake_llm(workdir, agent, prompt, extension_policy,
538
570
  fork_source=fork_source, fork_cwd=fork_cwd)
539
571
  return True
540
572
  if rr_turn:
@@ -554,7 +586,7 @@ def make_responder(with_extensions, fork_source=None, fork_cwd=None,
554
586
  if mem_available_mb() < MIN_MEM_MB:
555
587
  log(agent, "内存不足——抛可恢复异常(不代写 freezing,下轮重试)")
556
588
  raise RecoverableWakeError("内存不足")
557
- wake_llm(workdir, agent, prompt, with_extensions,
589
+ wake_llm(workdir, agent, prompt, extension_policy,
558
590
  fork_source=fork_source, fork_cwd=fork_cwd,
559
591
  fork_mode=fork_mode, topic=topic)
560
592
  return True
@@ -574,12 +606,16 @@ if __name__ == "__main__":
574
606
  if len(sys.argv) < 3:
575
607
  print("用法: python3 meeting_loop.py <workdir> <agent> "
576
608
  "[--max-meeting N] [--max-rr N] [--stall-timeout S]"
577
- " [--extensions]")
609
+ " [--extension-policy none|mc-tools|all]")
578
610
  sys.exit(1)
579
611
  workdir, agent = sys.argv[1], sys.argv[2]
580
612
  recover_git_lock(workdir, agent)
581
- # 默认零扩展;`--extensions` 显式 opt-in(协议字段优先)
582
- with_extensions = "--extensions" in sys.argv
613
+ # 扩展策略:CLI 先填,协议字段(若有)覆盖——与 forkMode 同款
614
+ extension_policy = meeting_fs.DEFAULT_EXTENSION_POLICY
615
+ if "--extension-policy" in sys.argv:
616
+ i = sys.argv.index("--extension-policy")
617
+ if i + 1 < len(sys.argv):
618
+ extension_policy = sys.argv[i + 1]
583
619
  mm, mr, st = 10, 7, 600
584
620
  # 协议从 **bare HEAD** 读(单一来源 = 共享事实;本地副本 LLM 可改)——
585
621
  # 与 engine participants()/check_status 同一原语
@@ -589,8 +625,14 @@ if __name__ == "__main__":
589
625
  print(f"[fatal] protocol.json 读取失败(bare HEAD 无有效内容): {bare}",
590
626
  flush=True)
591
627
  sys.exit(1)
592
- if proto.get("extensions"):
593
- with_extensions = True
628
+ if proto.get("extensionPolicy"):
629
+ extension_policy = proto["extensionPolicy"]
630
+ if extension_policy not in meeting_fs.EXTENSION_POLICIES:
631
+ # 值域守卫(与 forkMode 同款四层守卫之一:loop 门)——非法值不进 engine
632
+ # 重试路径,直接 fatal 退出(配置错误就该在启动时响)
633
+ print(f"[fatal] 非法 extensionPolicy: {extension_policy!r}"
634
+ f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})", flush=True)
635
+ sys.exit(1)
594
636
  if proto.get("maxMeetingRounds"):
595
637
  mm = proto["maxMeetingRounds"]
596
638
  if proto.get("maxRRRounds"):
@@ -615,7 +657,7 @@ if __name__ == "__main__":
615
657
  sys.exit(1)
616
658
  try:
617
659
  agent_loop(workdir, agent,
618
- make_responder(with_extensions,
660
+ make_responder(extension_policy,
619
661
  fork_source=fork_source,
620
662
  fork_cwd=proto.get("forkCwd") or "",
621
663
  fork_mode=fork_mode_cfg,
package/mv_cli.py CHANGED
@@ -42,6 +42,7 @@ PROG = os.environ.get("MV_INVOKED_AS") or "scripts/mv.sh"
42
42
  USAGE = f"""用法:
43
43
  {PROG} --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
44
44
  {PROG} --start <spec目录> [--fork-mode compaction|budget|full]
45
+ {PROG} --start <spec目录> [--extension-policy none|mc-tools|all]
45
46
  {PROG} --status [dir]
46
47
  {PROG} --report [dir]
47
48
  {PROG} --wait [dir]
package/observability.py CHANGED
@@ -327,6 +327,9 @@ def build_report(base):
327
327
  f"{_dur(d['max_ms'] // 1000)} | rc≠0 {d['fails']} 次")
328
328
 
329
329
  # ---- LLM 运行事实(session 文档化字段;流式预过滤,不整文件解析) ----
330
+ # ---- 扩展策略(声明 vs 生效)----
331
+ _report_extension_line(base, out)
332
+
330
333
  # ---- 终止原因(只报事实与计数)----
331
334
  _report_termination(base, agents, out)
332
335
 
@@ -618,6 +621,46 @@ def _report_termination(base, agents, out):
618
621
  + (" | result.md 已提交" if has_result else ""))
619
622
 
620
623
 
624
+ def _report_extension_line(base, out):
625
+ """扩展策略:**声明 vs 生效**(+ 降级原因)——与"档位:声明 max | 生效 max"同型。
626
+
627
+ 取数(只读已有家,零新增记录):
628
+ · 声明 = `protocol.json.extensionPolicy`(无该字段的旧产物 → n/a)
629
+ · 生效 = loop log 的**登记行**(首唤打一次):
630
+ `扩展策略: 声明=<x> 生效=<y> strict=<0|1>[ 降级原因=<reason>]`
631
+ 为什么需要它:降级本来是**可见的**(loop log 一行),但产品面(报告)看不到
632
+ ——2026-09-14 为确认"MC 生效没有"花了好几轮探针 + DB 查询(e2e24 评审 E2)。
633
+ fail-open:读不到 → 显示 n/a,不报错。
634
+ """
635
+ bare = meeting_fs.bare_of_base(base)
636
+ proto = meeting_fs.read_protocol(bare)
637
+ declared = proto.get("extensionPolicy")
638
+ eff = None
639
+ for f in sorted(glob.glob(os.path.join(base, "loop-*.log"))):
640
+ try:
641
+ txt = open(f, encoding="utf-8", errors="replace").read()
642
+ except OSError:
643
+ continue
644
+ m = re.search(r"扩展策略: 声明=(\S+) 生效=(\S+) strict=(\d)"
645
+ r"(?: 降级原因=(.*))?", txt)
646
+ if m:
647
+ eff = m
648
+ break
649
+ if not declared and not eff:
650
+ out.append("扩展策略:n/a(协议无字段、日志无登记行——多为 2026-09-14 前的产物)")
651
+ return
652
+ if eff:
653
+ d, e, strict, reason = eff.group(1), eff.group(2), eff.group(3), eff.group(4)
654
+ line = f"扩展策略:声明 {d} | 生效 {e}(strict={strict}"
655
+ line += f",降级:{reason.strip()}" if reason else ""
656
+ line += ")"
657
+ if d != e:
658
+ line += " ⚠ 生效≠声明(降级:ctx_search 不可用)"
659
+ out.append(line)
660
+ else:
661
+ out.append(f"扩展策略:声明 {declared} | 生效 n/a(日志中无登记行)")
662
+
663
+
621
664
  def _report_wake_fields(base):
622
665
  """解析 loop-*.log 的登记字段(`elapsed_ms` / `rc`)。
623
666
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.4.0",
3
+ "version": "0.5.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
@@ -17,8 +17,7 @@ import sys
17
17
 
18
18
  HERE = os.path.dirname(os.path.abspath(__file__))
19
19
  TPL_DIR = os.path.join(HERE, "templates")
20
- PI_AGENT_DIR = os.environ.get("PI_CODING_AGENT_DIR",
21
- os.path.expanduser("~/.pi/agent"))
20
+ # pi agent 目录的**单一实现**在 meeting_fs.pi_agent_dir()(F9:此前两处各拼一次)
22
21
  MAX_AGENT_NAME_LEN = 32
23
22
 
24
23
  import meeting_fs
@@ -56,7 +55,7 @@ def _default_model():
56
55
  获取失败(pi 不可用/无 settings)→ 返回 None。
57
56
  """
58
57
  try:
59
- with open(os.path.join(PI_AGENT_DIR, "settings.json")) as f:
58
+ with open(os.path.join(meeting_fs.pi_agent_dir(), "settings.json")) as f:
60
59
  cfg = json.load(f)
61
60
  provider = cfg.get("defaultProvider") or ""
62
61
  model = cfg.get("defaultModel") or ""
@@ -123,7 +122,7 @@ def pi_sessions_dir(cwd):
123
122
  AGENTS.md 明言"编码错一根横线 = 静默解析不到"——风险点不应复制。
124
123
  """
125
124
  enc = "--" + cwd.strip("/").replace("/", "-") + "--"
126
- return os.path.join(PI_AGENT_DIR, "sessions", enc)
125
+ return os.path.join(meeting_fs.pi_agent_dir(), "sessions", enc)
127
126
 
128
127
 
129
128
  def current_session_file():
@@ -370,7 +369,8 @@ def gen_question(topic, stances, background, questions):
370
369
  return "\n".join(lines)
371
370
 
372
371
 
373
- def gen_protocol(topic, participants, max_meeting, max_rr, extensions=False,
372
+ def gen_protocol(topic, participants, max_meeting, max_rr,
373
+ extension_policy=meeting_fs.DEFAULT_EXTENSION_POLICY,
374
374
  result_writer=None,
375
375
  stall_timeout=DEFAULT_STALL_TIMEOUT,
376
376
  fork_source=None, fork_cwd=None,
@@ -388,8 +388,12 @@ def gen_protocol(topic, participants, max_meeting, max_rr, extensions=False,
388
388
  "stallTimeoutSeconds": stall_timeout,
389
389
  "commitPolicy": "one-message-per-commit",
390
390
  }
391
- # 显式写出扩展策略(默认零扩展 = agents 不加载任何外部扩展)
392
- proto["extensions"] = bool(extensions)
391
+ # 显式写出扩展策略(值域与默认值的家在 meeting_fs;此处只写选定值)。
392
+ # 非法值就地报错(不落盘半成品)——与 forkMode 同款:配置错误不进运行时
393
+ if extension_policy not in meeting_fs.EXTENSION_POLICIES:
394
+ raise ValueError(f"非法 extension_policy: {extension_policy!r}"
395
+ f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})")
396
+ proto["extensionPolicy"] = extension_policy
393
397
  if fork_source:
394
398
  # fork 模式(多视角):首唤挂载主 session + cwd=主项目
395
399
  # forkMode 取值域与默认值的定义在 meeting_fs(FORK_MODES /
@@ -3,7 +3,8 @@
3
3
 
4
4
  用法:
5
5
  python3 start_discussion.py --dir mymeet --topic "主题" --agents a,b \
6
- [--stances '{"a": "立场1", "b": "立场2"}'] [--start] [--extensions] \
6
+ [--stances '{"a": "立场1", "b": "立场2"}'] [--start] \
7
+ [--extension-policy none|mc-tools|all] \
7
8
  [--models '{"a": "provider/model"}'] [--max-meeting 10] [--max-rr 7]
8
9
 
9
10
  复杂内容用 spec 规格目录(设计 16,与 CLI 内容参数互斥):
@@ -350,7 +351,7 @@ def setup_environment(args, participants, base, spec_dir=None,
350
351
  # 共享配置(work-a 提交,setup commit 进 bare)
351
352
  with open(os.path.join(wa, "protocol.json"), "w") as f:
352
353
  json.dump(gen_protocol(spec_topic or args.topic, participants, args.max_meeting,
353
- args.max_rr, args.extensions, args.result_writer,
354
+ args.max_rr, args.extension_policy, args.result_writer,
354
355
  args.stall_timeout,
355
356
  fork_source=getattr(args, "fork_source", None),
356
357
  fork_cwd=os.getcwd(),
@@ -420,7 +421,7 @@ def setup_environment(args, participants, base, spec_dir=None,
420
421
  print(f"[setup] 环境就绪: {base}({len(participants)} agents: {', '.join(participants)})")
421
422
  print(f"[setup] resultWriter={rw}, maxMeeting={args.max_meeting}, maxRR={args.max_rr}, "
422
423
  f"立场={'有' if (args.stances or spec_dir) else '无'}, "
423
- f"extensions={args.extensions}")
424
+ f"extensionPolicy={args.extension_policy}")
424
425
 
425
426
 
426
427
  def _preserve_result_md(base):
@@ -512,7 +513,7 @@ from spec_gen import ( # noqa: F401
512
513
  viewer_set_error, gen_agents_md, gen_protocol,
513
514
  pi_sessions_dir, current_session_file, _join_model_ref,
514
515
  _default_model, _detect_pi_model_thinking, resolve_fork_source,
515
- PI_AGENT_DIR, MAX_AGENT_NAME_LEN,
516
+ MAX_AGENT_NAME_LEN,
516
517
  )
517
518
  from observability import ( # noqa: F401
518
519
  _loop_pids, _loops_alive, check_status, build_report,
@@ -550,9 +551,13 @@ def main():
550
551
  "protocol.json,各 agent 首唤由本地生成 fork "
551
552
  "源挂载主上下文;不传 = 从主 pi 环境自动解析"
552
553
  "(PI_SESSION_ID;解析失败明确报错)")
553
- parser.add_argument("--extensions", action="store_true",
554
- help="让 agents 加载外部扩展(默认零扩展:不加载任何"
555
- "外部扩展/技能/prompt-template/主题)")
554
+ parser.add_argument(
555
+ "--extension-policy",
556
+ choices=list(meeting_fs.EXTENSION_POLICIES),
557
+ default=meeting_fs.DEFAULT_EXTENSION_POLICY,
558
+ help="agents 的扩展策略:mc-tools=默认,只要 MC 的只读检索工具 "
559
+ "ctx_search(缺 MC 时可见降级为零扩展);none=零扩展(零依赖);"
560
+ "all=走 pi 默认发现")
556
561
  parser.add_argument("--start", action="store_true", help="创建后启动讨论")
557
562
  parser.add_argument("--skip-setup", action="store_true",
558
563
  help="跳过环境生成,只启动已有环境(需 --dir)")
@@ -704,12 +709,13 @@ def main():
704
709
  workdir = os.path.join(base, f"work-{p}")
705
710
  cmd = [sys.executable, os.path.join(base, "meeting_loop.py"),
706
711
  workdir, p]
707
- if args.extensions:
708
- cmd.append("--extensions")
712
+ # 扩展策略**不转发**:协议字段(gen_protocol 恒写)是唯一权威,
713
+ # 转发只会多一条通道(S3)。例外语义已写进 decision 20:旧协议
714
+ # 目录(无该字段)按默认档启动。
709
715
  # 配额(max-meeting/max-rr/stall-timeout)是环境属性:创建时
710
716
  # 固化在 protocol.json,启动继承(loop 读 protocol 优先)。
711
717
  # 不传 CLI —— 避免无条件覆盖 protocol.json 的固化值
712
- # (审核 C1:配额单一事实源;与 extensions 处理一致)
718
+ # (审核 C1:配额单一事实源;与 extension-policy 处理一致)
713
719
  with open(os.path.join(base, f"loop-{p}.log"), "w") as f:
714
720
  procs.append(subprocess.Popen(cmd, stdout=f,
715
721
  stderr=subprocess.STDOUT,