pi-multi-viewers 0.8.0 → 0.8.2

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
@@ -28,13 +28,15 @@ human_sayer.py 【human 通道】插话命令(单次/stdin/交互 -i)
28
28
  scripts/mv.sh 稳定入口 shim(exec mv_cli.py;路径被 prompt/README 引用)
29
29
  scripts/pi-probe.sh LLM 探针(跑 pi + 登记新 session → 残留检查器可追溯)
30
30
  scripts/check-residue.sh 残留检查(session/进程/目录三类;增删 scripts/ 时同步本节)
31
- mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers)
31
+ scripts/archive-result.sh 归档 result.md(机械部分:逐字复制+校验/存档头骨架/索引行/删源副本)
32
+ mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers/set-viewer)
32
33
  extensions/multi-viewers/ 【单一扩展单元】index.ts = 三命令 + shared.ts = 助手
33
34
  /multi-viewers 分析入口(prepare→暂停点弹窗→start→预填+打印观看命令)
34
35
  /multi-viewers-finish 收尾(status→确认→cleanup)
35
36
  /multi-viewers-say 插话(零 LLM,直接 spawn human_sayer.py)
36
37
  ⚠ extensions/ 平级禁放 .ts 助手(加载器会把平级文件当独立扩展)
37
- prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→落盘→给审)
38
+ prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→**mv.sh --set-viewer** 落盘→给审)
39
+ # 那四条本来写进 prompt 的纪律(命名/不覆盖/非空/校验)改由命令保证
38
40
  docs/design.md 设计文档(fork 源模式与规模口径 + 决策记录)
39
41
  package.json npm 包 pi-multi-viewers(pi.prompts 注册;**版本号唯一事实源**)
40
42
  templates/ AGENTS.md.tpl / agent.md.tpl / gitignore.tpl / spec-readme.md.tpl
@@ -233,7 +235,7 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
233
235
 
234
236
  ## 安装/发版状态(2026-09-11)
235
237
 
236
- - **当前形态**:prompt × 1(multi-viewers-setup 建视角)+ extension × 2
238
+ - **当前形态**:prompt × 1(multi-viewers-setup 建视角)+ extension × 1(一单元注册三命令)
237
239
  ——一个扩展单元内注册三命令(`multi-viewers` 分析 / `multi-viewers-finish` 收尾 /
238
240
  `multi-viewers-say` 插话;前两个靠 CLI 机器标记行取值 `[prepare] spec=` /
239
241
  `[start] dir=` / `[start] watch=` / `[status]`,后者零 LLM 直接 spawn human_sayer.py;
package/README.md CHANGED
@@ -112,15 +112,16 @@ scripts/mv.sh --view # 一次性增量查看(主 pi
112
112
  scripts/mv.sh --say "<文本>" # 插话(命令行形态;pi 内用 /multi-viewers-say)
113
113
  scripts/mv.sh --status # 状态 + 路径(取值与含义以该命令输出为准)
114
114
  scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM/档位对照;冷路径,不持久化)
115
- scripts/mv.sh --cleanup # 收尾(result.md 自动留存到 <dir>-result.md)
115
+ scripts/mv.sh --cleanup # 收尾(result.md + 报告都留存到 <dir>-*.md/.txt)
116
116
  scripts/mv.sh --viewers # 列出+校验当前项目 viewers/(只读;建视角时用)
117
+ scripts/mv.sh --set-viewer <名字> # 新建视角文件(正文从 stdin 读;只新建不覆盖)
117
118
  ```
118
119
 
119
120
  ## 视角文件写什么(`viewers/<视角名>.md`)
120
121
 
121
122
  建视角**推荐**走 `/multi-viewers-setup`(交互式:先给候选建议 → 你定建哪几个 →
122
- 落盘 → 展示给你审);也可以手写。写完用 `scripts/mv.sh --viewers` 自查(列出并
123
- 用代码判据校验名字与空正文)。
123
+ `--set-viewer` 落盘 → 展示给你审);也可以手写,写完用 `scripts/mv.sh --viewers`
124
+ 自查(列出并用代码判据校验名字与空正文)。
124
125
 
125
126
  一个视角文件 = **一份视角说明**,纯内容、无格式要求(无 frontmatter、
126
127
  无需标题,**文件名就是全部元数据**)。三个要点(措辞经实验验证):
package/docs/design.md CHANGED
@@ -103,7 +103,7 @@ compaction 的 `firstKeptEntryId` 起 + 其后的条目"——窗口内含 compa
103
103
  | `status-<agent>.json` | loop | `{"sessionID": ...}` | 流程(崩溃恢复) | 是(恢复用) | O(1) |
104
104
  | `pi-sessions/fork-src-*.jsonl` | pi | 文档化 session schema(`usage`/`stopReason`/`timestamp`/`thinkingLevel`) | fork 构建 + `--report` | 否(报告用) | O(MB) 全量 → **禁轮询** |
105
105
  | `result.md`(固定位) | resultWriter loop | 结论文档 | 人 | 是(收尾判据) | — |
106
- | `--report`(视图) | observability | 文本行 | 人(**三个出口**,见下) | **否**(不得升级为验收 gate) | 冷路径一次性 —— **O(session 大小)**:每 agent 读整个 fork-src jsonl(实测 3 × 789KB ≈ 2.4MB/次、50–150ms/次,×3 出口 <0.3s/次分析),**不得进入任何轮询路径**(e2e16 评审量化) |
106
+ | `--report`(视图) | observability | 文本行(`--cleanup` 另落盘 `<base>-report.txt`) | 人(**三个出口**,见下) | **否**(不得升级为验收 gate) | 冷路径一次性 —— **O(session 大小)**:每 agent 读整个 fork-src jsonl(实测 3 × 789KB ≈ 2.4MB/次、50–150ms/次,×3 出口 <0.3s/次分析),**不得进入任何轮询路径**(e2e16 评审量化) |
107
107
 
108
108
  **报告的字段集**(e2e17 评审后定稿,后续增补不计数——字段行以本表为准)——
109
109
  **谓词分组 + 对照 + 事实行**,
@@ -168,7 +168,7 @@ BOUNDARY_TYPE`)——**显式登记"历史(fork 携带)/ 本轮"的分界*
168
168
 
169
169
  **报告的打印位置**:`--report`(手动,任意时刻)+ `--cleanup` 前(自动,
170
170
  删目录前最后一次可读——目录删后 `--report` 不可用)。cleanup 层对报告
171
- fail-open(报告失败不阻断清理,且**打印**失败原因不静默)。
171
+ fail-open(报告失败不阻断清理,且**打印**失败原因不静默)。报告随 `--cleanup` **落盘**一份到 `<base>-report.txt`(与 `-result.md` 同级)——原先只在终端出现一次,目录删掉后无法复查(复盘时长口径时踩到,用户 2026-09-25 定)。
172
172
 
173
173
  消费规则:`meeting_fs.iter_after_boundary` 只产出边界之后的条目;**未找到
174
174
  边界(老产物/手工 session)→ 返回空、按 n/a 处理,不得退回全文扫描**
@@ -567,19 +567,27 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
567
567
  内容起草仍是真 LLM 工作(任务类型/产出形态),故为**两段式**——骨架零 LLM,
568
568
  起草/摘要走普通对话;门禁取消则保留 spec,用户编辑后自行 `mv.sh --start`。
569
569
  **收尾状态语义(2026-09-25 评审)**:`running` 等待;`done | stalled` 并流进
570
- "notify → confirm → cleanup"(`stalled` = 无存活 loop 的静止态,照 running 处理
571
- 会让用户等一个永不到来的收尾);`stopped` 只提示手动清理。`stalled` 用独立文案
572
- (其 result 由清理时保存,不复用 done 的"已留存"路径文案);扩展是唯一守卫
570
+ "notify → confirm → cleanup"(照 running 处理会让用户等一个永不到来的收尾);
571
+ `stopped` 只提示手动清理。**状态定义以 `observability.check_status` 为单一
572
+ 事实源**(`stalled` = 有 result.md、无 concluded、loop 均不存活)——此处不复述其判据。
573
+ `stalled` 用独立文案(其 result 由清理时保存,不复用 done 的"已留存"路径文案);扩展是唯一守卫
573
574
  (CLI 的 cleanup 无状态防护)。**本轮明确不做**(防翻案):`--json` 输出模式、
574
575
  resume 新命令面、env 回退配置、`runCli` 超时、启动路径继续优化(已在 1–2s 地板)、
575
576
  给 stalled 加第三种动作。**契约例外**:扩展作为包内第一方消费者**直连**
576
577
  `mv_cli.py`(实测 shim 61ms vs 直连 58–66ms,性能上零差异;按契约一致性记例外一行)。
577
- **UI 通道按 mode 分级(2026-09-25 实测)**:dialog(`select`/`confirm`/`input`/`editor`)
578
- 全模式可用(RPC/web 走请求-响应子协议、阻塞等用户;不带 `timeout` 即不倒计时,
579
- 暂停点成立);`notify` 全模式可用(TUI = showStatus 行;pi-web = **追加进聊天流的
580
- 常驻行**,非瞬时提示);**`setEditorText` 仅 TUI**——pi-web 忽略(`pi-web/static/app.js`
581
- 注释「set_editor_text … ignored」+ SDK `ui-context.ts` 里是空实现)。⇒ **交付观看命令
582
- 必须有 notify 兜底**(预填只是增强,不能当唯一出口);需要分级时用 `ctx.mode`。
578
+ **建视角流程(`/multi-viewers-setup`,仍是 prompt)**:机械部分下移到
579
+ `mv.sh --set-viewer <名字>`(正文从 stdin 读)——**命名规则 / 不覆盖已有 / 空正文拒绝 /
580
+ 写完校验并回显**四条由命令保证(此前是 prompt 里给 LLM 的纪律,会漏);prompt 只负责
581
+ **看项目给候选 + 内容撰写 + 与用户来回**(那才是 LLM 该做的)。
582
+ **UI 通道按 mode 分级(2026-09-25 实测)**:dialog(`select`/`confirm`/`input`/`editor`)
583
+ 全模式可用(RPC/web 走请求-响应子协议、阻塞等用户;不带 `timeout` 即不倒计时,
584
+ 暂停点成立);`notify` 全模式可用(TUI = showStatus 行;pi-web = **追加进聊天流的
585
+ 常驻行**,非瞬时提示);**`setEditorText` 仅 TUI**——pi-web 忽略(`pi-web/static/app.js`
586
+ 注释「set_editor_text … ignored」+ SDK `ui-context.ts` 里是空实现)。⇒ **交付观看命令
587
+ 必须有 notify 兜底**(预填只是增强,不能当唯一出口);需要分级时用 `ctx.mode`。
588
+ 但 pi-web 实测**关掉 notify 弹窗即消失** ⇒ 观看命令还写一条 `pi.sendMessage`
589
+ (`customType: multi-viewers`、`display: true`)进消息流:持久可回滚复制,
590
+ 代价 = 参与 LLM 上下文的一行(用户 2026-09-25 要求「message 流中也能显示」)。
583
591
  被否决:A(handler 里 `sendUserMessage` 触发 LLM 回合改 spec——时序不可控)、
584
592
  D(拆两条命令——把门禁成本转嫁用户;B1 变体/第三种即现形态)。
585
593
 
@@ -0,0 +1,211 @@
1
+ <!-- 存档:docs/reviews/2026-09-25-multi-viewers-postfix-review.md
2
+ 来源:一次真实多视角分析的 result.md 原文(未删改,仅加本头与下方说明)。
3
+ 分析场次目录已随 cleanup 删除;文中消息编号(如 效率/0012、铁律/0004)不可再核验,
4
+ 仅作溯源线索(与代码注释引用约定一致:行为以自描述为准)。 -->
5
+
6
+ # 存档说明
7
+
8
+ - **主题**:复验 0.8.0 这批改动 —— extension 合并后的实现(一单元三命令)、
9
+ P1–P6 修复是否到位、扩展层 harness 的覆盖是否有漏
10
+ - **场次**:`mv-mv-main-20260925-105614`(3 视角:效率 / 简单 / 铁律;真实 pi 讨论,
11
+ 默认档 `mc-tools`、`forkMode=budget`、`maxMeeting=15`;三方共识收敛)
12
+ - **复验判定**:P1–P6 **逐条到位**;合并(P5)判定**净简化**(`findPackageRoot`
13
+ 全仓 1 定义 1 调用、旧目录不存在);**未触碰唤醒路径** → 分析总时长期望零变化
14
+ - **本场新抓(多数是上一批引入的)**:
15
+ - **漏 A(最重要,接线 bug)**:`run_tests.sh --reuse` 的错误成功信号 ——
16
+ `last_ok()` 只扫任意一行 `^OK`,而 harness 输出追加在 python 的 OK 之后 →
17
+ harness 失败时它仍为真;指纹又无条件写 → 下次 `--reuse` 命中并 exit 0 而日志含失败。
18
+ 修法 **②′**:真跑前清指纹、`rc==0` 才写回(不变式「指纹存在 ∧ 匹配 ⇒ 最近同源真跑全绿」)。
19
+ 本仓落地时做了同款三连红绿验证(全绿→指纹在;环境性失败→rc=1 且指纹已清;随后
20
+ `--reuse` 必须未命中→真跑)
21
+ - **D2**:启动通知「已预填进输入框」在 pi-web 为**不实断言**(`setEditorText` 仅 TUI)
22
+ - **B1**:`mv_cli.py` 两条人类行复用 `[start]` 契约前缀;**B2**:五处出路提示引用裸
23
+ `mv.sh`(不在 PATH 上,出路不可执行)
24
+ - **7 类现存分支零覆盖** + 两项装置缺口:**sid 注入零断言**(漏注入 = say/finish
25
+ 静默定位失败而全绿)、harness 的 `new URL().pathname` 不还 percent-encoding
26
+ - **D1/D3** 文档漂移(AGENTS「extension × 2」;决策 22 复述 stalled 判据)
27
+ - **落地**:`69a415a`(六文件一次收口;harness 28 → 44 断言,含 sid 装置与
28
+ `findPackageRoot` 直断 throw;先红后绿:去掉 sid 注入 → 3 条断言失败)
29
+ - **本场暴露的用户可见事实**(不在代码里):① 门禁暂停点**真的停住**(用户实测);
30
+ ② pi-web 的 `notify` **关掉弹窗即消失** → 后续加第三交付出口
31
+ `pi.sendMessage`(custom_message 进消息流,`e92eacc`)
32
+
33
+ # 0.8.0 复验 · 三方共识结果
34
+
35
+ **主题**:复验 0.8.0 这批改动:extension 合并后的实现(一单元三命令)、P1–P6 修复是否到位、
36
+ 扩展层 harness 的覆盖是否有漏——只提意见,不改代码。
37
+ **参与者**:效率 / 简单 / 铁律(3 视角;meeting 自由讨论 + round-robin 全体 pass)。
38
+ **方法**:只读核对、每项带 `文件:行`;零 LLM 验证(harness 实跑、/tmp 逐字复刻门逻辑复现、
39
+ grep/源码核对);**未修改任何文件、未运行真实分析**。
40
+ **场次**:`mv-mv-main-20260925-105614`(fork 模式 budget;mc-tools 档)。
41
+
42
+ ---
43
+
44
+ ## 0. 结论摘要
45
+
46
+ 1. **P1–P6 行为修复逐条到位,合并(P5)判定为净简化**:`extensions/multi-viewers/` 现为单一
47
+ 单元、三命令(`index.ts` + `shared.ts`),逐字重复消失;**未触碰唤醒路径**
48
+ (48–82s/唤醒的大头一行未改,agent 注入内容未变)→ 分析总时长期望零变化,收益落在
49
+ 人类交互层与维护期(效率/0001)。
50
+ 2. **本场共发现**:2 处新缺陷(漏 A:`--reuse` 错误成功信号;漏 B:P4 加载期分支无覆盖)、
51
+ 3 处设计符合度偏差(D1/D2/D3)、2 处职责边界问题(B1/B2)、7 类 harness 覆盖漏 +
52
+ 2 项装置缺口(sid 断言、percent-encoding)。**其中漏 A 的完整修法 ②′ 是本场最重要产出**
53
+ (三方认账,效率/0012、简单/0012)。
54
+ 3. **②′(铁律 0004 §二)**:`run_tests.sh` 的"失败不写指纹"(原方案②)**不能消除漏 A**——
55
+ 同源码环境性失败会残留旧绿指纹,`--reuse` 仍命中 exit 0 而日志含失败(已复现)。
56
+ 完整修法 = 真跑开始前 `rm -f "$FP_FILE"`、结束 `rc -eq 0` 才写回(:90 现为无条件写)。
57
+ 不变式「指纹存在 ∧ 匹配 ⇒ 最近一次同源真跑全绿」,单一 rc 判据、零文本解析、覆盖中断。
58
+ 4. **净复杂度账为正(变简)**:S1 消 ≈35–50 行同构 spawn 样板、S2/S3 消软分支与 no-op、
59
+ 批量条目多为 1–6 行改动;本批**零新装置**(新增能力仅 = 假 python3 多记一个 env 变量 +
60
+ 导出 1 个函数 + 一个字符串常量)。
61
+ 5. **执行**:六文件一次编辑 → 一次 `/reload` → **约 5 分钟验证**(`run_tests.sh --force` +
62
+ 零 LLM 门禁路径 + ②′ 红绿 ~10s);**不专门真跑**(改动不触唤醒路径/协议注入/分析逻辑)。
63
+
64
+ ---
65
+
66
+ ## 1. 复验判定(0.8.0 本批 = dfe71cc 合并与 P1–P6)
67
+
68
+ | 项 | 判定 | 依据(抽核) |
69
+ |---|---|---|
70
+ | **P1** stalled 并流 | ✓ 到位 | `index.ts` 状态分派:running 早退,`done|stalled` 并流到 confirm→cleanup;harness 断言 stalled 不再落 running |
71
+ | **P2** stalled 独立文案 | ✓ 到位 | stalled 不复用 done 的 result 路径文案;harness 断言不含 done fallback 文本 |
72
+ | **P3** 删与暂停点矛盾的旧注释 | ✓ 到位 | 文件头现为「取消则不启动并保留 spec」,无「先取消再自行 start」旧句 |
73
+ | **P4** 删单机回退、加载期响亮失败 | ✓ 到位 | 全仓 `FALLBACK_ROOT` 0 命中;`shared.ts` 找不到包根即 throw(行动性信息);覆盖缺口见 §2.2 |
74
+ | **P5** 合并为一单元三命令 | ✓ 到位 | `findPackageRoot` 全仓 1 定义 1 调用;旧 `multi-viewers-say/` 目录不存在 |
75
+ | **P6** 取消提示 + harness 进仓库 | ✓ 到位 | 取消提示含「当前 pi session 内」;`tests/extension_harness.ts` 被 `run_tests.sh` 接入(缺 bun 可见跳过、`MV_REQUIRE_BUN=1` 严格、失败置 rc=1) |
76
+
77
+ **复验实测**:harness **28 通过 / 0 失败**(口径更正:效率/0001 的 29 = `grep -c 'check('`
78
+ 含 1 处函数定义;运行汇总为 28);harness 墙钟 **0.113–0.155s**(3 次,本机)——占全量
79
+ (~250s 口径)≈0.05%,"反对 unittest↔harness 并行"由预测变为数据支撑。
80
+
81
+ ---
82
+
83
+ ## 2. 本场新发现(按类别)
84
+
85
+ ### 2.1 漏 A:`--reuse` 的错误成功信号 → 修法 ②′(最重要)
86
+
87
+ - **机制**:`last_ok()` 只扫全日志任意一行 `^OK`(`run_tests.sh:50`),harness 输出**追加**在
88
+ python `OK` 之后 → harness 失败时 `last_ok` 仍真;指纹无条件写(`:90`)→ 下次 `--reuse`
89
+ 命中(`:56-57`)并 **exit 0** 而缓存日志含 harness 失败。
90
+ - **原方案②的残余洞(铁律 0004 复现)**:仅"失败不写指纹"时,**同源码环境性失败**
91
+ (`MV_REQUIRE_BUN=1` 缺 bun / 中断 / harness 并发)会残留上一轮旧绿指纹 → 仍命中 exit 0。
92
+ 已用**逐字复刻的门逻辑**在 /tmp 复现(`→ REUSE 命中:exit 0(而 LAST_LOG 含 harness 失败)`)。
93
+ - **②′**:真跑前 `rm -f "$FP_FILE"`;结束 `rc -eq 0` 才写回。`last_ok` 可留但**不能当 ② 的
94
+ 替代**(它挡不住本反例);②′ 后其定位退为"遗留指纹/篡改面"。
95
+ - **红绿核验(3 条命令,同参数隔离源指纹)**:① module 全绿 → FP 存在;②
96
+ `env PATH=/usr/bin:/bin MV_REQUIRE_BUN=1` 同参数跑 → rc=1 **且 FP 不存在**;③ `--reuse`
97
+ → 必须"未命中→真跑"(现状会命中 exit 0)。
98
+ - **三方裁决**:效率/0012、简单/0012 均认账采纳(补洞 + 单一 rc 判据 + 覆盖中断)。
99
+
100
+ ### 2.2 漏 B:P4 加载期失败分支无覆盖 → 内联(可裁)
101
+
102
+ - `findPackageRoot` 未导出(`shared.ts:29`),harness 内 import 成功即无法触发;子进程负例
103
+ 需**复制** `shared.ts` → 违反「测试对象 = 生产对象」(副本漂移)。
104
+ - **裁定**:导出该函数直调、**只断言 throw**(至多含稳定标识 `pi-multi-viewers`)、不锁散文;
105
+ **残差明账**(锁函数、不锁加载期接线;坏掉症状=三命令消失,响亮)。
106
+ - **内联本批**(不做事后追加:验证是批次级成本,事后追加=第二次验证循环);优先级低,若砍
107
+ 则记账留与下次改动同批(效率/0007 自我更正)。
108
+
109
+ ### 2.3 设计符合度偏差(D1/D2/D3)
110
+
111
+ - **D1**:`AGENTS.md:236`「extension × 2」未随 P5 合并同步(同段已写"一个扩展单元内注册
112
+ 三命令");`git show dfe71cc` 证实该行未动——"改了实现忘改抄本"的漂移。简单认领,改一行。
113
+ - **D2**:启动通知「已预填进输入框,按 Enter 执行」在 **pi-web 为不实断言**
114
+ (决策 22:577-581 已写明 `setEditorText` 仅 TUI;复核 `pi-web/static/app.js:1907`、
115
+ `ui-context.ts:456` 空实现)——与 0.8.0 首用事故(预填被忽略)同源。
116
+ **修法:中性单串**(如「复制执行;TUI 下已预填」),**不引 `ctx.mode` 分支**(无 mock/断言
117
+ 增量,断言体零改动,仅标签措辞随统一改名处理);`setEditorText` 调用保留(TUI 增强)。
118
+ - **D3**:决策 22:570 对 `stalled` 的复述("无存活 loop 的静止态")宽于单一事实源
119
+ (`observability.check_status`:**有 result.md**、无 concluded、loop 均不存活)——按字面
120
+ 把 `stopped` 也涵盖。**扩展文案不改**(正常路径恒真),**决策 22 引用化**。
121
+ - **边界观察(简单/0007 读码定案,不进本批)**:status 判"有结果"看 `git log --all`,而
122
+ `preserve_result_md` 读 `HEAD:result.md`(`meeting_fs.py:1238`)→ 侧分支/删除场景可达性低;
123
+ **将来统一必须偏 loud**(危险方向 = 清理静默丢产物),不得选静默侧。
124
+
125
+ ### 2.4 职责边界问题(B1/B2)
126
+
127
+ - **B1**:`mv_cli.py:368/371` 两条人类行复用 `[start]` 契约前缀且带路径(唯一契约清单外同前缀
128
+ 行)——"方括号=机器契约"被稀释。修法:**去方括号**(非新增 `[spec]` 前缀);已核
129
+ `test_mv_cli.py:312` 只断子串、零测试连锁。
130
+ - **B2**:`index.ts:67/72/86/129/198` 五处出路提示引用裸 `mv.sh`,而 `which mv.sh` 为空、
131
+ README:55 教的是安装绝对路径 → 出路不可执行。修法:`shared.ts` 增**常量**
132
+ `MV_SH = \`bash ${PACKAGE_ROOT}/scripts/mv.sh\``,五处引用(PACKAGE_ROOT 现为模块私有,
133
+ 常量化非抽象层)。
134
+
135
+ ### 2.5 harness 覆盖漏与装置缺口
136
+
137
+ **漏场景(7 类,均为现存分支)**:① MV `!specDir`(rc=0 无 `[prepare] spec=`)② MV
138
+ `!watch` ③ FIN 状态读取失败整支 ④ FIN cleanup 失败 ⑤ SAY find-dir 成功 + sayer 失败
139
+ ⑥ SAY 空输出("插话已发送")⑦ `[start] dir=` 缺失(按 §3-S2 定案 (b) 写作"→ 报错")。
140
+ 补方式按"追加用例"(两处必然例外:S2 改成功夹具、sid 装置改 calls 输出格式);断言名带
141
+ 分支标识(替代另立清单表——清单=第二抄本必漂移);现有断言一并统一改名(纯改名零行为)。
142
+
143
+ **装置缺口(本场新增两项)**:
144
+ - **sid 传递零断言**:假 python3 只记 argv(`harness:25-31`);sid 是 `mv-<sid>-*` 命名与
145
+ find-dir 的唯一钥匙,漏注入 = say/finish **静默定位失败**而 28 条全绿 → 假装置多记
146
+ `$PI_SESSION_ID` + 各命令断言(**升为防静默同级**)。
147
+ - **`HERE = new URL(…).pathname`**(`harness:18`)不还原 percent-encoding——实测把
148
+ `extensions/` 与 harness 复制到 `/tmp/space test/repo` 运行即报
149
+ `Cannot find module '/tmp/space%20test/repo/…'`;改 `fileURLToPath`(与生产 `shared.ts`
150
+ 同处理)。
151
+
152
+ ---
153
+
154
+ ## 3. 冻结的批次清单(六文件、一次收口)
155
+
156
+ **编辑顺序**(依赖者后写;S1 与 harness 无强依赖,真依赖仅 S2→#7 场景、D2 措辞→标签文案):
157
+
158
+ 1. `extensions/multi-viewers/shared.ts`:S1 三 spawn 样板合一(原语 ≤~15 行、三调用点语义
159
+ 逐一保留:stderr 合并与否 / rc vs ok / env 差异;**不顺手给 sayer 注入 sid**——现语义照抄)
160
+ + `MV_SH` 常量 + 漏 B 导出(+头注释契约行同步)
161
+ 2. `mv_cli.py`:S2 定案 **(b)**(`!dir` 并入 guard,删三元;dir 契约项保留)+ B1 去方括号
162
+ 3. `extensions/multi-viewers/index.ts`:D2 中性文案 + B2 用 `MV_SH` + S3 删三处
163
+ `getArgumentCompletions: () => null`(`argumentHint` 保留——上游扩展命令映射未转发,
164
+ 留着将来生效)
165
+ 4. `tests/test_mv_cli.py`:S2 断言微调(如需)
166
+ 5. `tests/extension_harness.ts`:7 类场景 + sid 装置/断言 + `fileURLToPath` + 断言名统一
167
+ (含现有断言)+ 漏 B 用例(导出直调、只断 throw)
168
+ 6. `tests/run_tests.sh`:漏 A **②′**(真跑前清指纹 + `rc==0` 才写回)
169
+
170
+ **文档两行**:D1(`AGENTS.md:236`)、D3(决策 22 引用化)。**可裁顺序**:A 防静默 → B 用户
171
+ 可见 → C 文档 → D 简化(漏 B 用例可裁,但要砍则记账留与下次同批)。
172
+
173
+ **批次约束**:零新装置、不触碰唤醒路径;验证 = `run_tests.sh --force`(~250s)+ `/reload`
174
+ 零 LLM 走"假主题到门禁取消"(数十秒,顺带见到 D2/B2 新文案)+ ②′ 红绿(~10s)。
175
+
176
+ ---
177
+
178
+ ## 4. 明确不做 / 边界记录
179
+
180
+ - **不专门真跑**本批(改动全在扩展/CLI 人类输出/测试/文档层;不触唤醒路径与协议注入)。
181
+ - `last_ok()` 本批**不动**(对修复前遗留指纹有保护面;其精简留待下批评估)。
182
+ - **stalled+取消** 场景低价值可裁(两表均未列)。
183
+ - D3 的 `--all` vs `HEAD` 判据分叉:记为边界观察,**不进本批**;将来统一偏 loud。
184
+ - S2 的 (a)(删 `[start] dir=`)**留作将来因其它原因动契约时的清理候选**(本批不划算:
185
+ 7–8 处连锁 + doc×3 漂移面;简单/0008、效率/0008 均持此记录)。
186
+ - `argumentHint` 上游未转发:保留,随上游补丁生效(不属本仓缺口)。
187
+
188
+ ---
189
+
190
+ ## 5. 收敛过程(消息索引)
191
+
192
+ - **效率**(0001–0012):墙钟四层账(唤醒是大头、extension 层非瓶颈)+ 省时账(P1 最大、
193
+ harness 性价比最高)+ 反对常驻/框架/并行/再弹 dialog;发现漏 A 并给出两方案 → 认账转 ②;
194
+ 对漏 B"导出 vs 子进程"更正为内联;提供批内编辑顺序与"不专门真跑"验证分级;0002 提出
195
+ "补场景必须与修 `last_ok` 同批"的联动点。
196
+ - **简单**(0001–0012):S1/S2/S3 三处简化主张(spawn 样板合一、dir 半必填二选一、
197
+ 删 no-op 字段)+ 反对过度抽象三条 + harness 覆盖表 + 主张"断言名带标识替代清单表";
198
+ 读码定案 D3(`--all`/`HEAD` 分叉,比两边原表述更细);B2 提出 `MV_SH` 常量;
199
+ 在 S2 (b)/(a) 与漏 B 口径上随证据两度更新立场。
200
+ - **铁律**(0001–0007):首轮以三条铁律逐项检视(P1–P6 到位 + D1/D2/D3 + B1/B2 + harness
201
+ 六类 + 两项装置缺口);裁决漏 B(导出+只断 throw)、漏 A(②)、S2 语义、S1 验收三条;
202
+ 认账并裁定 S2 定案 **(b)**;**发现并复现 ② 的残余洞,给出完整修法 ②′**(本场最重要产出);
203
+ 复核 D2/D3 并记录 loud 方向;round-robin pass 收口。
204
+
205
+ ---
206
+
207
+ ## 6. 一句话结论
208
+
209
+ **0.8.0 的合并与 P1–P6 修复复验通过(harness 28/28、零行为反例);需在下一批补齐
210
+ ②′(漏 A 完整修法)、7 类覆盖漏与 sid/路径两装置缺口、以及 5 处文案/文档偏差(D1/D2/D3、
211
+ B1/B2)——全部为 1–6 行级、零新装置、不触唤醒路径,一次收口 + 约 5 分钟验证即可。**
@@ -11,6 +11,9 @@
11
11
  真实缺陷(见下)。
12
12
  2. **决策溯源**:修复批次(commit)常引用报告中的编号(如"P0/P1/P2"),
13
13
  保留原文才能核对当时的事实与推理。
14
+
15
+ 归档动作的**机械部分**由 `scripts/archive-result.sh` 做(命名、正文逐字复制+字节校验、
16
+ 存档头骨架、索引行、删仓库根松散副本)——判断部分(主题/问题/commit)仍由人写。
14
17
  3. **方法论的样本**:报告本身演示了"观察句 vs 机制句"的证据强度要求
15
18
  (`docs/test-methodology.md` 方法 16)——其中也有几处机制句在讨论中被
16
19
  交叉复核推翻,是活教材。
@@ -33,6 +36,7 @@
33
36
  | `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 删死代码)|
34
37
  | `2026-09-14-e2e25-doc-drift-review.md` | 文档 vs 代码一致性(文档漂移)+ ctx_search 的**自然使用**观察 | **三处文档仍写"兜底 mv-* 并警告"而代码是无兜底、未匹配报错**(行为语义相反);README 缺 `--extension-policy`;报告字段列表过期;4 条缺失项;**根因 = 对实现的复述**(治本:引事实源不复制);自然使用观察:`ctx_search` **0 次**、historian 0 次 | `ba204ef` |
35
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
+ | `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` 第三交付出口) |
36
40
 
37
41
  ## 环境口径(读报告时的背景)
38
42
 
@@ -16,14 +16,17 @@
16
16
  *
17
17
  * 与 CLI 的契约(标记行/退出码)与全部原语见 ./shared.ts。
18
18
  *
19
- * 观看命令交付 = `ctx.ui.setEditorText` 预填输入框(按 Enter 即执行)**并**
20
- * 在 notify 里带一份(预填会被后续输入覆盖——只留预填这一个出口,用户就
21
- * 再也找不到它;2026-09-25 首次真实使用暴露)。
19
+ * 观看命令交付 = **三个出口**(缺一不可,都是实测暴露的):
20
+ * ① `ctx.ui.setEditorText` 预填输入框(按 Enter 即执行;**仅 TUI**,pi-web 忽略)
21
+ * ② `notify` 带一份(即时可见;但 pi-web 上关掉弹窗即消失)
22
+ * ③ `pi.sendMessage` 写一条 custom_message 进消息流(**持久可回滚复制**;
23
+ * 代价 = 参与 LLM 上下文的一行)
22
24
  */
23
25
 
24
26
  import {
25
27
  findCurrentDir,
26
28
  grab,
29
+ MV_SH,
27
30
  runCli,
28
31
  runSayer,
29
32
  specListing,
@@ -35,7 +38,6 @@ export default function register(pi: any) {
35
38
  pi.registerCommand("multi-viewers", {
36
39
  description: "多视角协同分析:生成 spec → 你审阅 → 启动(零 LLM 流程)",
37
40
  argumentHint: "<主题>",
38
- getArgumentCompletions: () => null,
39
41
  handler: async (args: string, ctx: any) => {
40
42
  const topic = stripQuotes(args.trim());
41
43
  if (!topic) {
@@ -64,12 +66,12 @@ export default function register(pi: any) {
64
66
  "启动多视角分析?(现在暂停中,可在其它窗口修改 spec)",
65
67
  `spec:${specDir}\n文件:${specListing(specDir)}\n\n` +
66
68
  "需要修改就去改这个目录,改完点「确认」继续;\n" +
67
- "点「取消」则不启动(spec 保留,可稍后 mv.sh --start)。",
69
+ "点「取消」则不启动(spec 保留,可稍后 " + MV_SH + " --start)。",
68
70
  );
69
71
  if (!go) {
70
72
  ctx.ui.notify(
71
73
  `已取消,spec 保留在:${specDir}\n` +
72
- `之后可在**当前 pi session 内**用:mv.sh --start ${specDir}\n` +
74
+ `之后可在**当前 pi session 内**用:${MV_SH} --start ${specDir}\n` +
73
75
  "(外部终端执行时目录名不带 session id,插话/收尾命令定位不到它)",
74
76
  "info",
75
77
  );
@@ -80,20 +82,29 @@ export default function register(pi: any) {
80
82
  const start = await runCli(["--start", specDir], cwd, sid);
81
83
  const watch = grab(start.output, "[start] watch=");
82
84
  const dir = grab(start.output, "[start] dir=");
83
- if (start.rc !== 0 || !watch) {
85
+ if (start.rc !== 0 || !watch || !dir) {
84
86
  ctx.ui.notify(
85
87
  `启动失败:\n${start.output || "(无输出)"}\n` +
86
- "可用 mv.sh --status 查看环境状态。",
88
+ "可用 `" + MV_SH + " --status` 查看环境状态。",
87
89
  "error",
88
90
  );
89
91
  return;
90
92
  }
91
93
 
92
- // ④ 观看命令:预填进输入框 + notify 里留一份副本(见文件头)
94
+ // ④ 观看命令:三个出口(见文件头)——预填(仅 TUI)+ notify(即时)+ 消息流(持久)
93
95
  ctx.ui.setEditorText(watch);
96
+ // 消息流里留一条持久记录:用户实测 pi-web 的 notify 会随弹窗关闭而消失,
97
+ // 关了窗口就再也找不到这行命令。custom_message 进会话(**参与 LLM 上下文**,
98
+ // 一行开销),在用户空闲时追加 → 立即显示、可回滚复制(agent-session.ts:
99
+ // 非 streaming + 无 triggerTurn → _appendCustomMessage,不触发回合)。
100
+ pi.sendMessage({
101
+ customType: "multi-viewers",
102
+ content: `多视角分析已启动:${dir}\n观看命令(复制执行,不进 LLM):\n${watch}`,
103
+ display: true,
104
+ });
94
105
  ctx.ui.notify(
95
- `分析已启动${dir ? `:${dir}` : ""}\n` +
96
- "观看(已预填进输入框,按 Enter 执行;也可复制这行):\n" +
106
+ `分析已启动:${dir}\n` +
107
+ "观看(复制执行;TUI 下已预填进输入框):\n" +
97
108
  `${watch}\n` +
98
109
  "插话:/multi-viewers-say <文本> 收尾:/multi-viewers-finish",
99
110
  "success",
@@ -104,7 +115,6 @@ export default function register(pi: any) {
104
115
  // ---------------------------------------------------------------- 收尾
105
116
  pi.registerCommand("multi-viewers-finish", {
106
117
  description: "收尾:查状态 → 确认 → 清理分析目录(结果留存;摘要走对话)",
107
- getArgumentCompletions: () => null,
108
118
  handler: async (_args: string, ctx: any) => {
109
119
  const sid = ctx.sessionManager.getSessionId();
110
120
  const st = await runCli(["--status"], ctx.cwd, sid);
@@ -126,7 +136,7 @@ export default function register(pi: any) {
126
136
  if (state === "stopped") {
127
137
  ctx.ui.notify(
128
138
  "分析已结束但未生成结果(状态 stopped)。" +
129
- "要清理请自行运行 mv.sh --cleanup。",
139
+ "要清理请自行运行 `" + MV_SH + " --cleanup`。",
130
140
  "warning",
131
141
  );
132
142
  return;
@@ -179,7 +189,6 @@ export default function register(pi: any) {
179
189
  pi.registerCommand("multi-viewers-say", {
180
190
  description: "向正在进行的多视角分析插话(human 消息,各视角可见可回应)",
181
191
  argumentHint: "<插话内容>",
182
- getArgumentCompletions: () => null,
183
192
  handler: async (args: string, ctx: any) => {
184
193
  const text = args.trim();
185
194
  if (!text) {
@@ -195,7 +204,7 @@ export default function register(pi: any) {
195
204
  ctx.ui.notify(
196
205
  "本 session 没有正在进行的多视角分析(cwd 下无 " +
197
206
  `mv-${sid}-* 分析环境)。先用 /multi-viewers 启动,` +
198
- "或改用 mv.sh --say <目录> \"<文本>\" 显式指定。",
207
+ "或改用 `" + MV_SH + " --say <目录> \"<文本>\"` 显式指定。",
199
208
  "error",
200
209
  );
201
210
  return;
@@ -25,8 +25,11 @@ const PKG_NAME = "pi-multi-viewers";
25
25
  * **找不到就响亮失败**(评审 P4):此前回退到 `/root/pi-multi-viewers` 是单机
26
26
  * 死分支——触发条件(包被拆散/复制)恰恰发生在开发机以外,回退只会把"包根
27
27
  * 解析失败"变成更晚、更难诊断的失败。这里在**加载期** throw,错误信息给出行动。
28
+ *
29
+ * 导出仅供测试直调(断言 throw;不锁散文措辞)——加载期接线本身无覆盖,
30
+ * 残差明账:坏掉时症状 = 三个命令消失(响亮)。
28
31
  */
29
- function findPackageRoot(start: string, pkgName: string): string {
32
+ export function findPackageRoot(start: string, pkgName: string): string {
30
33
  let dir = start;
31
34
  for (let i = 0; i < 8; i++) {
32
35
  try {
@@ -52,26 +55,48 @@ const CLI = path.join(PACKAGE_ROOT, "mv_cli.py");
52
55
  const SAYER = path.join(PACKAGE_ROOT, "human_sayer.py");
53
56
  const OBSERVABILITY = path.join(PACKAGE_ROOT, "observability.py");
54
57
 
55
- /** 运行 mv_cli 一条命令;返回 { rc, output }(stdout+stderr 合并)。 */
56
- export function runCli(
58
+ /** 稳定的 CLI 入口(给用户的出路提示用)——裸 `mv.sh` 不在 PATH 上,
59
+ * 按此常量给绝对路径(评审 B2:出路必须可执行)。 */
60
+ export const MV_SH = `bash ${PACKAGE_ROOT}/scripts/mv.sh`;
61
+
62
+ /** 跑一次子进程并收输出。三处调用点的差异全部显式化(合并前是三份样板):
63
+ * `sid` 决定是否注入 PI_SESSION_ID(目录名与 find-dir 的唯一钥匙);
64
+ * `mergeStderr` 决定 stderr 是否并进同一缓冲(不并时必须 resume 掉,否则管道满阻塞)。 */
65
+ function capture(
57
66
  args: string[],
58
- cwd: string,
59
- sid: string,
60
- ): Promise<{ rc: number; output: string }> {
67
+ opts: { cwd?: string; sid?: string; mergeStderr?: boolean } = {},
68
+ ): Promise<{ code: number; output: string }> {
61
69
  return new Promise((resolve) => {
62
- const proc = spawn("python3", [CLI, ...args], {
63
- cwd,
64
- env: { ...process.env, PI_SESSION_ID: sid },
70
+ const env = opts.sid
71
+ ? { ...process.env, PI_SESSION_ID: opts.sid }
72
+ : process.env;
73
+ const proc = spawn("python3", args, {
74
+ cwd: opts.cwd,
75
+ env,
65
76
  stdio: ["ignore", "pipe", "pipe"],
66
77
  });
67
- let out = "";
68
- proc.stdout.on("data", (d) => (out += d.toString()));
69
- proc.stderr.on("data", (d) => (out += d.toString()));
70
- proc.on("close", (code) => resolve({ rc: code ?? 1, output: out.trim() }));
71
- proc.on("error", (e) => resolve({ rc: 1, output: String(e) }));
78
+ let output = "";
79
+ proc.stdout.on("data", (d) => (output += d.toString()));
80
+ if (opts.mergeStderr) {
81
+ proc.stderr.on("data", (d) => (output += d.toString()));
82
+ } else {
83
+ proc.stderr.resume(); // 丢弃但必须消费
84
+ }
85
+ proc.on("close", (code) => resolve({ code: code ?? 1, output }));
86
+ proc.on("error", (e) => resolve({ code: 1, output: String(e) }));
72
87
  });
73
88
  }
74
89
 
90
+ /** 运行 mv_cli 一条命令;返回 { rc, output }(stdout+stderr 合并)。 */
91
+ export async function runCli(
92
+ args: string[],
93
+ cwd: string,
94
+ sid: string,
95
+ ): Promise<{ rc: number; output: string }> {
96
+ const r = await capture([CLI, ...args], { cwd, sid, mergeStderr: true });
97
+ return { rc: r.code, output: r.output.trim() };
98
+ }
99
+
75
100
  /** 取机器可读标记行的值(`[label] value`);没有 → null。 */
76
101
  export function grab(output: string, label: string): string | null {
77
102
  for (const line of output.split("\n")) {
@@ -116,37 +141,21 @@ export function specListing(specDir: string): string {
116
141
  * 判据 = **退出码**(不是 stderr 文案——中文提示一改就静默失配,e2e16 F2):
117
142
  * rc 0 → stdout 是绝对路径;rc 1 → 未找到(无同 sid 分析)。无降级通道
118
143
  * (不会回退到"项目下最新"——那会插错分析,e2e16 F1)。 */
119
- export function findCurrentDir(cwd: string, sid: string): Promise<string | null> {
120
- return new Promise((resolve) => {
121
- const proc = spawn("python3", [OBSERVABILITY, "--find-dir"], {
122
- cwd,
123
- env: { ...process.env, PI_SESSION_ID: sid },
124
- stdio: ["ignore", "pipe", "pipe"],
125
- });
126
- let out = "";
127
- proc.stdout.on("data", (d) => (out += d.toString()));
128
- proc.stderr.on("data", () => {}); // 原因只在 rc=1 时通知用户(见 handler)
129
- proc.on("close", (code) => {
130
- const dir = out.trim();
131
- resolve(code === 0 && dir ? dir : null);
132
- });
133
- proc.on("error", () => resolve(null));
134
- });
144
+ export async function findCurrentDir(
145
+ cwd: string,
146
+ sid: string,
147
+ ): Promise<string | null> {
148
+ const r = await capture([OBSERVABILITY, "--find-dir"], { cwd, sid });
149
+ const dir = r.output.trim();
150
+ return r.code === 0 && dir ? dir : null;
135
151
  }
136
152
 
137
- /** 执行 human_sayer.py 一次插话。返回 { ok, output }。 */
138
- export function runSayer(
153
+ /** 执行 human_sayer.py 一次插话。返回 { ok, output }。
154
+ * 现语义照抄(不注入 sid——sayer 用显式目录参数)。 */
155
+ export async function runSayer(
139
156
  dir: string,
140
157
  text: string,
141
158
  ): Promise<{ ok: boolean; output: string }> {
142
- return new Promise((resolve) => {
143
- const proc = spawn("python3", [SAYER, dir, text], {
144
- stdio: ["ignore", "pipe", "pipe"],
145
- });
146
- let out = "";
147
- proc.stdout.on("data", (d) => (out += d.toString()));
148
- proc.stderr.on("data", (d) => (out += d.toString()));
149
- proc.on("close", (code) => resolve({ ok: code === 0, output: out.trim() }));
150
- proc.on("error", (e) => resolve({ ok: false, output: String(e) }));
151
- });
159
+ const r = await capture([SAYER, dir, text], { mergeStderr: true });
160
+ return { ok: r.code === 0, output: r.output.trim() };
152
161
  }
package/meeting_fs.py CHANGED
@@ -47,6 +47,17 @@ def result_path(base):
47
47
  """
48
48
  return f"{base}-{RESULT_MD}"
49
49
 
50
+
51
+ def report_path(base):
52
+ """分析报告的固定落盘位(`<分析目录>-report.txt`,与 result.md 同级)。
53
+
54
+ 报告原本只在 `--cleanup` 时往终端打一次,目录一删就再也拿不到——复盘
55
+ (时长口径、配额、provider 失败这类)时无据可查(用户 2026-09-25 复盘
56
+ 回算不出实际时长,正是此缺口)。观测数字应当可复查,故随清理落盘一份。
57
+ `--report` 命令本身仍不持久化(冷路径视图,三个出口共享同一实现)。
58
+ """
59
+ return f"{base}-report.txt"
60
+
50
61
  # 协议参数默认值(gen_protocol 固化进 protocol.json)——**唯一声明点**:
51
62
  # CLI default、engine 签名默认、observability 的兜底读取都引用这里
52
63
  # (此前 10/7 在三处各写一遍,改一处不改另一处就会漂移)。
package/mv_cli.py CHANGED
@@ -54,6 +54,7 @@ USAGE = f"""用法:
54
54
  {PROG} --view [dir] [--since <ref>]
55
55
  {PROG} --say [dir] "<文本>"
56
56
  {PROG} --viewers # 列出并校验当前项目的 viewers/(只读;建视角时用)
57
+ {PROG} --set-viewer <名字> # 新建一个视角文件(正文从 stdin 读;只新建不覆盖)
57
58
 
58
59
  消费命令的 <dir> 可省略(自动发现本 session 当前分析——按 cwd 下
59
60
  mv-<PI_SESSION_ID>-* 最新;无匹配则报错要求显式传目录)
@@ -153,9 +154,6 @@ def cmd_viewers(args):
153
154
  目录**(消费命令的目录自动发现对此不适用——它找的是 mv-<sid>-*)。判据全部
154
155
  复用 `spec_gen` 的单一实现(列举 `list_agent_md` / 名字 `check_agent_name` /
155
156
  集合 `viewer_set_error`),这一层不另写一套规则。
156
-
157
- 数量不足(<2)在这里是**提示**不是错误:建 1 个是合法的中间状态,只有启动
158
- 一次分析时才要求 ≥2(那条判据仍由 `viewer_set_error` 独占)。
159
157
  """
160
158
  if args:
161
159
  fail(f"未知参数: {' '.join(args)}(--viewers 不接受参数——只检查项目 cwd 的 viewers/)")
@@ -163,6 +161,17 @@ def cmd_viewers(args):
163
161
  if not os.path.isdir(vdir):
164
162
  fail(f"未找到 {vdir}——视角文件放在项目 cwd 的 viewers/<视角名>.md"
165
163
  f"(文件名即视角名;可跑 /multi-viewers-setup 交互式建立)")
164
+ return _validate_and_print_viewers(vdir)
165
+
166
+
167
+ def _validate_and_print_viewers(vdir):
168
+ """列出 + 校验 + 打印(`--viewers` 与 `--set-viewer` 的**同一实现**)。
169
+
170
+ 判据全部复用 `spec_gen` 的单一实现(列举 `list_agent_md` / 名字
171
+ `check_agent_name` / 集合 `viewer_set_error`)——这一层不另写规则。
172
+ 数量不足(<2)是**提示**不是错误:建 1 个是合法中间状态(只有启动一次
173
+ 分析才要求 ≥2,那条判据由 `viewer_set_error` 独占)。
174
+ """
166
175
  names, _briefs, empty = spec_gen._discover_viewers(vdir)
167
176
  if not names:
168
177
  fail(f"{vdir} 下没有 *.md——文件名即视角名(如 viewers/效率.md)")
@@ -190,6 +199,38 @@ def cmd_viewers(args):
190
199
  return 0
191
200
 
192
201
 
202
+ def cmd_set_viewer(args):
203
+ """新建一个视角文件:`--set-viewer <名字>`,正文**从 stdin 读**。
204
+
205
+ 为什么是命令而不是"让 LLM 直接写文件":文件名即视角名,于是**命名规则 /
206
+ 不覆盖已有 / 空正文拒绝 / 写完校验并回显**这四条本来只能写在 prompt 里当
207
+ 纪律(LLM 会漏),现在由机制保证(判据复用 `spec_gen` 单一实现)。LLM/人
208
+ 只负责**内容**——那是它该做的部分。不提供 `--force`:本命令语义 = 只新建,
209
+ 改已有视角请直接编辑文件。
210
+ """
211
+ if len(args) != 1:
212
+ fail("用法: --set-viewer <名字>(正文从 stdin 读;如 "
213
+ "`mv.sh --set-viewer 效率 <<'EOF' … EOF`)")
214
+ name = args[0]
215
+ err = spec_gen.check_agent_name(name)
216
+ if err:
217
+ fail(f"非法视角名({err}):{name}")
218
+ vdir = os.path.join(os.getcwd(), "viewers")
219
+ target = os.path.join(vdir, f"{name}.md")
220
+ if os.path.exists(target):
221
+ fail(f"视角已存在,不覆盖: {target}(改名,或直接编辑该文件)")
222
+ body = sys.stdin.read().strip()
223
+ if not body:
224
+ fail(f"视角内容为空({name})——正文从 stdin 传入;空视角没有 lenses,"
225
+ f"分析会退化成同名随机视角")
226
+ os.makedirs(vdir, exist_ok=True)
227
+ with open(target, "w", encoding="utf-8") as f:
228
+ f.write(body + "\n")
229
+ print(f"[set-viewer] 已写入 {target}\n")
230
+ print(body + "\n")
231
+ return _validate_and_print_viewers(vdir)
232
+
233
+
193
234
  def cmd_status(args):
194
235
  d = _dir_only("--status", args)
195
236
  return _call([PYTHON, START_DISCUSSION, "--dir", d, "--status"])
@@ -365,10 +406,10 @@ def cmd_start(spec_dir, extra):
365
406
  # 条件 = 观测到一次 launch 失败,或将来引入 resume/retry 通道。
366
407
  if fnmatch.fnmatch(os.path.basename(os.path.normpath(spec_dir)),
367
408
  "mv-spec-*"):
368
- print(f"[start] spec 已消费,删除(本工具生成形态): {spec_dir}")
409
+ print(f"spec 已消费,删除(本工具生成形态): {spec_dir}")
369
410
  shutil.rmtree(spec_dir, ignore_errors=True)
370
411
  else:
371
- print(f"[start] spec 已消费(保留未删——非 mv-spec-* 形态): {spec_dir}")
412
+ print(f"spec 已消费(保留未删——非 mv-spec-* 形态): {spec_dir}")
372
413
 
373
414
  # 第 2 步:启动已有环境
374
415
  if _call([PYTHON, START_DISCUSSION, "--dir", dir_path,
@@ -418,6 +459,8 @@ def main(argv=None):
418
459
  return cmd_start(rest[0], rest[1:])
419
460
  if cmd == "--viewers":
420
461
  return cmd_viewers(rest)
462
+ if cmd == "--set-viewer":
463
+ return cmd_set_viewer(rest)
421
464
  if cmd == "--status":
422
465
  return cmd_status(rest)
423
466
  if cmd == "--report":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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,
@@ -15,8 +15,8 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
15
15
  1. <名字> —— <一行镜头>(与其它候选怎样互补/对立)
16
16
 
17
17
  - 这一步**只提议**:不建文件、不改任何东西
18
- - 名字:中文、短(≤32 字符)、不含空白与路径分隔符、**不要用 `human`**(保留名)
19
18
  - 建几个**由用户决定**(只建 1 个也行)
19
+ - 名字规则由第 3 步的命令强制(顺带一提:中文、短、不含空白与路径分隔符)
20
20
 
21
21
  **然后结束本回合,等用户输入。**
22
22
 
@@ -25,10 +25,9 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
25
25
  用户会告诉你建哪几个(可能只有一个、也可能点名不在候选里的)。**以用户输入为准**;
26
26
  若某个视角该用什么镜头没说清 → **问一句**,不要替他定。
27
27
 
28
- ## 第 3 步:按用户输入建文件(只新建)
28
+ ## 第 3 步:用命令建文件(内容经 stdin 传给命令,不要直接写文件)
29
29
 
30
- 写之前**先读 `README.md` 的「视角文件写什么」节**(唯一事实源:三个要点 + 正误对照 +
31
- 命名规则)。要点:
30
+ 先读 `README.md` 的「视角文件写什么」节(唯一事实源:三个要点 + 正误对照)。要点:
32
31
 
33
32
  - 纯内容、无 frontmatter、无标题——**文件名就是全部元数据**
34
33
  - 必含三条:① **单一 lenses**(所有观点必须从该视角出发)② **不越界**(其它视角由
@@ -37,19 +36,23 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
37
36
  - **只写视角本身**:身份("你是 X")、参与者名单、消息格式、独立参与者纪律都由脚本
38
37
  生成,**不要写进文件**
39
38
 
40
- 写完跑一次只读检查(会列出视角并用代码判据校验名字与空正文):
39
+ 每个视角调用一次(正文经 stdin;**命名合法性、不覆盖已有、空正文拒绝、写完校验并
40
+ 回显**都由命令保证,不需要你另外检查):
41
41
 
42
42
  ```bash
43
- ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --viewers
43
+ ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --set-viewer <视角名> <<'EOF'
44
+ <视角正文>
45
+ EOF
44
46
  ```
45
47
 
46
- **已有同名文件 → 不覆盖**:告诉用户"该视角已存在",停下等他决定(改名,或他明确
47
- 要求改动)。
48
+ 命令会回显**写入路径 + 正文全文 + 校验结果**——那就是第 4 步要展示的东西。
49
+ 若命令报"视角已存在":告诉用户,停下等他决定(改名,或他明确要求改动)。
48
50
 
49
51
  ## 第 4 步:展示,等审阅
50
52
 
51
- 把每个新建文件的**路径 + 全文**展示给用户,请他提修改意见或确认:
53
+ 把命令的回显(路径 + 全文 + 校验)给用户看,请他提修改意见或确认:
52
54
 
53
- - 有意见 → 改 → **再展示**(反复直到他确认)
55
+ - 有意见 → **直接编辑该文件**(`viewers/<名字>.md`)→ 跑一次
56
+ `~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --viewers` 复核 → 再展示(反复直到他确认)
54
57
  - 确认后一行交接:视角已建好(启动一次分析至少需要 2 个视角),接下来
55
58
  `/multi-viewers "<主题>"` 即可
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+ # archive-result.sh —— 把一场分析的 result.md 归档进 docs/reviews/
3
+ #
4
+ # 为什么是脚本:每场分析后都要做同一串机械动作,靠人/LLM 记(实测会漏——
5
+ # 最容易漏的是索引行)。这里把**机械部分**固化成命令:
6
+ # · 命名(<日期>-<slug>.md)
7
+ # · 正文**逐字复制**并做字节校验(存档不改原文)
8
+ # · 存档头骨架(TODO 标记留在文件里,判断内容仍由人写)
9
+ # · 索引表加一行(插在最后一行存档之后)
10
+ # · 删掉仓库根的松散副本
11
+ # **判断部分不代劳**:写什么主题/抓到什么问题/落地哪个 commit,是人的判断。
12
+ #
13
+ # 用法:
14
+ # scripts/archive-result.sh <result.md> --slug <slug> [--date YYYY-MM-DD] [--dry-run]
15
+ # 例:
16
+ # scripts/archive-result.sh mv-mv-main-20260925-105614-result.md \
17
+ # --slug multi-viewers-postfix-review
18
+ #
19
+ # 退出码:0 成功(或 dry-run);1 参数/环境错;2 目标已存在(不覆盖)
20
+
21
+ set -u
22
+ HERE="$(cd "$(dirname "$0")/.." && pwd)"
23
+ REVIEWS="$HERE/docs/reviews"
24
+ INDEX="$REVIEWS/README.md"
25
+
26
+ SRC=""; SLUG=""; DATE="$(date +%F)"; DRY=0
27
+ while [ $# -gt 0 ]; do
28
+ case "$1" in
29
+ --slug) SLUG="${2:-}"; shift 2 ;;
30
+ --date) DATE="${2:-}"; shift 2 ;;
31
+ --dry-run) DRY=1; shift ;;
32
+ -h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
33
+ -*) echo "错误: 未知参数 $1" >&2; exit 1 ;;
34
+ *) SRC="$1"; shift ;;
35
+ esac
36
+ done
37
+
38
+ [ -n "$SRC" ] || { echo "错误: 缺少 result.md 路径(--help 看用法)" >&2; exit 1; }
39
+ [ -n "$SLUG" ] || { echo "错误: 缺少 --slug <slug>" >&2; exit 1; }
40
+ [ -f "$SRC" ] || { echo "错误: 文件不存在: $SRC" >&2; exit 1; }
41
+ case "$SLUG" in *[!a-zA-Z0-9._-]*) echo "错误: slug 只允许字母/数字/._-(当前: $SLUG)" >&2; exit 1 ;; esac
42
+ [ -f "$INDEX" ] || { echo "错误: 找不到索引 $INDEX" >&2; exit 1; }
43
+
44
+ DEST="$REVIEWS/$DATE-$SLUG.md"
45
+ if [ -e "$DEST" ]; then
46
+ echo "错误: 目标已存在(不覆盖): $DEST" >&2; exit 2
47
+ fi
48
+
49
+ if [ "$DRY" -eq 1 ]; then
50
+ echo "[dry-run] 将归档: $SRC → $DEST"
51
+ echo "[dry-run] 正文 $(wc -c < "$SRC") 字节,逐字复制"
52
+ echo "[dry-run] 将写存档头骨架(含 TODO)+ 在 $INDEX 加一行 + 删 $SRC"
53
+ exit 0
54
+ fi
55
+
56
+ # 1) 存档头骨架 + 正文(逐字)
57
+ {
58
+ cat <<EOF
59
+ <!-- 存档:docs/reviews/$DATE-$SLUG.md
60
+ 来源:一次真实多视角分析的 result.md 原文(未删改,仅加本头与下方说明)。
61
+ 分析场次目录已随 cleanup 删除;文中消息编号不可再核验,仅作溯源线索
62
+ (与代码注释引用约定一致:行为以自描述为准)。 -->
63
+
64
+ # 存档说明
65
+
66
+ - **主题**:TODO(一句话)
67
+ - **场次**:\`TODO(分析目录名)\`(视角 / 档位 / 收敛方式)
68
+ - **判定/发现**:TODO
69
+ - **落地**:TODO(commit 或"未落地")
70
+ - **备注**:TODO(可不填则删掉本行)
71
+
72
+ EOF
73
+ cat "$SRC"
74
+ } > "$DEST"
75
+
76
+ # 2) 逐字校验(正文必须与源**逐字相同**——存档不改原文)
77
+ if ! cmp -s <(tail -c "$(wc -c < "$SRC")" "$DEST") "$SRC"; then
78
+ echo "错误: 正文校验失败(存档与源不一致)——已保留 $DEST 供排查" >&2
79
+ exit 1
80
+ fi
81
+ echo "[archive] 正文逐字校验通过($(wc -c < "$SRC") 字节)"
82
+
83
+ # 3) 索引加一行(紧跟最后一行存档;TODO 留给写索引的人)
84
+ python3 - "$INDEX" "$DATE-$SLUG.md" <<'PY'
85
+ import sys
86
+ idx_path, fname = sys.argv[1], sys.argv[2]
87
+ lines = open(idx_path, encoding="utf-8").read().splitlines(True)
88
+ hits = [i for i, l in enumerate(lines) if l.startswith("| `2026-")]
89
+ if not hits:
90
+ print("错误: 索引里找不到存档行(| `2026-…)", file=sys.stderr)
91
+ sys.exit(1)
92
+ row = f"| `{fname}` | TODO 主题 | TODO 抓到的问题 | TODO 落地 commit |\n"
93
+ lines.insert(hits[-1] + 1, row)
94
+ open(idx_path, "w", encoding="utf-8").write("".join(lines))
95
+ print(f"[archive] 索引已加一行({idx_path};TODO 待补)")
96
+ PY
97
+ [ $? -eq 0 ] || exit 1
98
+
99
+ # 4) 删仓库根松散副本(**只删源文件**,且必须在写成功后)
100
+ rm -f "$SRC"
101
+ echo "[archive] 已写 $DEST;源副本已删;下一步:补 $DEST 与索引行里的 TODO"
@@ -450,11 +450,24 @@ def cleanup_discussion(base):
450
450
  # 报告是附加信息、清理是主职责:报告生成失败**不阻断**清理
451
451
  # (fail-open 只在这一层兜底——build_report 内部各段已各自 fail-open)。
452
452
  print("[cleanup] —— 本次分析报告(删除目录前最后一次可读)——")
453
+ lines = None
453
454
  try:
454
- for line in build_report(base):
455
+ lines = list(build_report(base))
456
+ for line in lines:
455
457
  print(line)
456
458
  except Exception as e: # noqa: BLE001(兜底不吞:打印)
457
459
  print(f"[cleanup] 报告生成失败(不影响清理): {e!r}")
460
+ if lines is not None:
461
+ # 落盘一份(与 <base>-result.md 同级):报告本来只在终端出现一次,
462
+ # 目录删掉后 --report 也不可用 → 观测数字不可复查(复盘时长口径时
463
+ # 踩过)。与 result.md 同样的 fail-open:写不动不阻断清理。
464
+ rp = meeting_fs.report_path(base)
465
+ try:
466
+ with open(rp, "w", encoding="utf-8") as f:
467
+ f.write("\n".join(lines) + "\n")
468
+ print(f"[cleanup] 报告已保存 → {rp}")
469
+ except OSError as e:
470
+ print(f"[cleanup] 报告保存失败(不影响清理): {e!r}")
458
471
  shutil.rmtree(base)
459
472
  print(f"[cleanup] 已删除目录 {base}(含 pi-sessions)")
460
473