pi-multi-viewers 0.8.2 → 0.8.3
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 +4 -0
- package/docs/design.md +20 -10
- package/docs/reviews/2026-09-25-extension-mechanisms-review.md +182 -0
- package/docs/reviews/README.md +1 -0
- package/extensions/multi-viewers/index.ts +20 -7
- package/mv_cli.py +13 -4
- package/observability.py +5 -3
- package/package.json +1 -1
- package/prompts/multi-viewers-setup.md +1 -9
- package/spec_gen.py +15 -0
- package/start_discussion.py +59 -26
package/AGENTS.md
CHANGED
|
@@ -261,3 +261,7 @@ 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'])"`
|
package/docs/design.md
CHANGED
|
@@ -201,7 +201,7 @@ fail-open(报告失败不阻断清理,且**打印**失败原因不静默)
|
|
|
201
201
|
轮询路径消费 MB 面、不做运行期 LLM 评分(有效性判断留给人 + result.md)、
|
|
202
202
|
不做目录内 retention(cleanup 是唯一清理点)、message frontmatter 不加
|
|
203
203
|
时间戳(第二事实源 + 该字段由 LLM 写,不可信;权威时间 = commit 时间)、
|
|
204
|
-
日志不 JSON
|
|
204
|
+
日志不 JSON 化(主消费者是人)、报告不自动落固定位(视图不占“家”;**例外**:cleanup 删目录前打印并落盘一份快照 `<base>-report.txt`,见观测面契约)。
|
|
205
205
|
|
|
206
206
|
## 数字的归宿(一个数字只留一个"家")
|
|
207
207
|
|
|
@@ -579,17 +579,27 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
|
|
|
579
579
|
`mv.sh --set-viewer <名字>`(正文从 stdin 读)——**命名规则 / 不覆盖已有 / 空正文拒绝 /
|
|
580
580
|
写完校验并回显**四条由命令保证(此前是 prompt 里给 LLM 的纪律,会漏);prompt 只负责
|
|
581
581
|
**看项目给候选 + 内容撰写 + 与用户来回**(那才是 LLM 该做的)。
|
|
582
|
+
**有意的 LLM 义务残留**:prompt 第 4 步要求「改完再跑 `--viewers` 复核」——它不新增
|
|
583
|
+
命令面,且有硬 gate 兜底(prepare/start 的集合校验不过就拒绝启动),故保留。
|
|
582
584
|
**UI 通道按 mode 分级(2026-09-25 实测)**:dialog(`select`/`confirm`/`input`/`editor`)
|
|
583
585
|
全模式可用(RPC/web 走请求-响应子协议、阻塞等用户;不带 `timeout` 即不倒计时,
|
|
584
|
-
暂停点成立);`notify` 全模式可用(TUI = showStatus 行;pi-web
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
586
|
+
暂停点成立);`notify` 全模式可用(TUI = showStatus 行;pi-web 会关闭即消失);
|
|
587
|
+
**`setEditorText` 仅 TUI**——pi-web 忽略(`pi-web/static/app.js` 注释
|
|
588
|
+
「set_editor_text … ignored」+ SDK `ui-context.ts` 里是空实现)。⇒ 交付观看命令
|
|
589
|
+
不能只靠一个通道,**四个通道各司其职**:
|
|
590
|
+
| 通道 | 作用域 | 上下文成本 | 角色 |
|
|
591
|
+
|---|---|---|---|
|
|
592
|
+
| `setEditorText` 预填 | 仅 TUI | 0 | TUI 便利(能直接回车跑) |
|
|
593
|
+
| `notify` | 全模式 | 0 | 即时反馈(pi-web 关掉弹窗即消失) |
|
|
594
|
+
| `pi.sendMessage`(custom_message) | 全模式 | ~百 token(主 session)+ 随 fork 进每场分析 | 持久留痕(pi-web 渲染为折叠块) |
|
|
595
|
+
| `ctx.ui.setWidget` | 全模式 | 0(纯 UI) | 运行期常驻可见(一眼看到、不需点击) |
|
|
596
|
+
三条注记:① `sendMessage` 的 custom_message **会随 fork 进每场分析各视角的上下文**
|
|
597
|
+
(fork 源在首唤由主 session 条目构建,不做类型过滤)——~2 行/场,有界;不为它加
|
|
598
|
+
过滤(那会让构造层获得扩展类型知识,跨层耦合换几行噪音,不配)。② widget 是
|
|
599
|
+
**运行期**状态(fire-and-forget UI,非会话条目;reload/重启后不恢复——持久记录靠
|
|
600
|
+
custom_message)。③ 零上下文留痕档确实存在(`pi.appendEntry` + `registerEntryRenderer`,
|
|
601
|
+
明确不进 LLM 上下文),但其渲染器是 TUI 组件、pi-web 无渲染路径 ⇒
|
|
602
|
+
**可见 ∩ 零上下文 = 空集**,跨模式成本不可归零,接受现值。需要分级时用 `ctx.mode`。
|
|
593
603
|
|
|
594
604
|
### 被否决方案(含重估触发条件)
|
|
595
605
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<!-- 存档:docs/reviews/2026-09-25-extension-mechanisms-review.md
|
|
2
|
+
来源:一次真实多视角分析的 result.md 原文(未删改,仅加本头与下方说明)。
|
|
3
|
+
分析场次目录已随 cleanup 删除;文中消息编号不可再核验,仅作溯源线索
|
|
4
|
+
(与代码注释引用约定一致:行为以自描述为准)。 -->
|
|
5
|
+
|
|
6
|
+
# 存档说明
|
|
7
|
+
|
|
8
|
+
- **主题**:复验 0.8.2 新上线的三处机制(① `--cleanup` 落盘报告 ② `mv.sh --set-viewer`
|
|
9
|
+
③ 观看命令交付通道)+ 这三处与扩展 harness 的测试覆盖、断言强度
|
|
10
|
+
- **场次**:`mv-mv-main-20260925-153113`(3 视角:效率 / 简单 / 铁律;真实 pi 讨论;
|
|
11
|
+
`forkMode=budget`、扩展策略 `mc-tools`(声明=生效)、`maxMeeting=15`;共识收敛,
|
|
12
|
+
墙钟 28m42s / 46 次唤醒 / provider error 38 次)
|
|
13
|
+
- **判定**:三处机制**可以收下**(效率账:① <0.3s/场换掉 20–40 分钟重跑取数;
|
|
14
|
+
② 一次命令调用省掉 LLM 的校验/重试回合;③ ~百 token/场换掉分钟级往返)
|
|
15
|
+
- **本场抓到(多数在最新那批代码里,均属防静默失效)**:
|
|
16
|
+
- **①(c) 显示层失败会跳过清理**(最重要):`cleanup_discussion` 的 print 在管道关闭时抛
|
|
17
|
+
`BrokenPipeError` → 逃逸 → **rmtree 被跳过**、目录残留、rc≠0。三处逃逸点:
|
|
18
|
+
banner print 在 try 外 / except 处理器自身再 print / 写盘失败处理器又 print。
|
|
19
|
+
修法 = `_print_best_effort`(全部 stdout 走它,函数内无裸 `print(`)+ `rmtree` 进
|
|
20
|
+
`finally` ⇒ **rmtree 必达**;唯一例外 = 产物留存真失败(result.md 权威位在待删目录内)
|
|
21
|
+
修后由 `51a4535` 落地,本仓复现脚本前后对照(逃逸+残留 → 无逃逸+已删+报告仍落盘)
|
|
22
|
+
- **② `--set-viewer` 半成功**:集合级校验在**写之后** → viewers/ 里预置坏文件时
|
|
23
|
+
"rc≠0 但新文件已写入"。修法 **B′**(校验前移到读 stdin/写之前,单一组合点
|
|
24
|
+
`spec_gen._viewer_set_errors`);另 `exists` → `lexists`
|
|
25
|
+
- **③ 通道模型修正**:从"三个出口(缺一不可)"收敛为**四通道角色模型**;并指出
|
|
26
|
+
`custom_message` **随 fork 进入每场分析上下文**(~2 行/场,有界)——此前只说"主 session"
|
|
27
|
+
- 文档描述簇(三处"不持久化"复述只同步了一处)、断言偏弱("落盘≡打印"未锁)、
|
|
28
|
+
prompt 里 README 三要点的第二处复述、fail-open 宽窄不对称(Exception vs OSError)
|
|
29
|
+
- **落地**:`51a4535`(475 python 测试 + 48 harness 断言全绿;净增 ≈11 行生产代码、
|
|
30
|
+
零运行时行为变化)。报告另记录"明确不做"(fork 过滤 / `ctx.mode` 分支 / 符号链接测试矩阵)
|
|
31
|
+
与已知限(widget 仅运行期常驻)
|
|
32
|
+
|
|
33
|
+
# 0.8.2 复验 · 三方共识结果
|
|
34
|
+
|
|
35
|
+
参与者:效率 / 简单 / 铁律(3 视角,真实 pi 讨论;`forkMode=budget`、`mc-tools`、`maxMeeting=15`)。
|
|
36
|
+
主题:复验 0.8.2 新上线的三处机制(① `--cleanup` 落盘报告 ② `mv.sh --set-viewer` ③ 观看命令交付通道)+ 测试覆盖与断言强度。**只提意见,不改代码、不跑测试**。
|
|
37
|
+
|
|
38
|
+
## 0. 结论摘要
|
|
39
|
+
|
|
40
|
+
1. **三处机制方向正确、净收益为正**(效率账:① 用 <0.3s/场换掉 20–40 分钟重跑取数;② 用一次命令调用省掉 LLM 的校验/重试回合;③ 用 ~百 token/场换掉分钟级往返)。
|
|
41
|
+
2. ① ② 的实现基本符合设计,但有**一处文档描述缺口簇、一处测试断言弱、一处职责边界含糊**;③ 在讨论**进行中**因真实观察新增了第四个交付通道(`setWidget`,HEAD `0b28256`),审查基准随之推进,结论按版本分段。
|
|
42
|
+
3. **本批必须落地**(三方一致,零运行时变化):
|
|
43
|
+
- ① **显示层失败可跳过清理**的修补(`_print_best_effort` + `rmtree` 必达;唯一例外=产物留存真失败)——本轮最重要行为修复;
|
|
44
|
+
- ① 落盘内容 ≡ 打印内容 的等值断言 + 写失败 fail-open 分支用例 + 三处"不持久化"复述改引用 + 两处新能力文案;
|
|
45
|
+
- ② **B′**:集合校验前移到写之前(单点 `_viewer_set_errors` + `if names:` 守卫),消掉"写成功但 rc≠0"的半成功;
|
|
46
|
+
- ② prompt 删去 README 三要点复述与命名括注;
|
|
47
|
+
- ③ `index.ts:19`/`:96` 的"三个出口"改四通道角色表述;决策 22 收敛为角色表 + 三注记(fork 复制事实、widget「(运行期)」、why-not-appendEntry);
|
|
48
|
+
- harness **单一日志**(sendMessage 入 `calls`、删 `sent`、失败路径精确 kinds)+ ① 三断言用例——**净删代码、覆盖变强**。
|
|
49
|
+
4. **一句话**:三处机制可以收下;本批做的是"让清理必定发生、让校验先于副作用、让文档按实际写"——全部是防静默失效类的修复,生产代码净增 ≈11 行(显示层 ~6 + B′ ~5),其余为断言与文档。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 1. 审查基准与版本口径(一个动态事实,先钉死)
|
|
54
|
+
|
|
55
|
+
| 项 | 事实 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| 0.8.2 | tag `v0.8.2` = `81c8820`(内容:`e06bceb` 报告落盘 + `--set-viewer` + 归档脚本;`81c8820` 发版) |
|
|
58
|
+
| ③ 的历史 | `sendMessage` 第三出口 ∈ **0.8.1**(`e92eacc`,见 `v0.8.0..v0.8.1`);④ `setWidget` ∈ **`0b28256`**(讨论进行中落地,未发版) |
|
|
59
|
+
| 本场 HEAD | `0b28256`("观看命令加常驻面板出口")——③ 按**四通道**审,① ② 与 v0.8.2 一致 |
|
|
60
|
+
| 引用口径 | 复验结论**按版本分段**:①② 对 v0.8.2;③ 对 0.8.1(sendMessage)/ `0b28256`(widget)——避免被读成"0.8.2 已复验"而重复检查 |
|
|
61
|
+
| 数字校准 | harness **48** 条断言(0b28256 后);口径见 `docs/design.md:106`(`build_report` 实测 50–150ms/次、×3 出口 <0.3s/场,单点数字非承诺) |
|
|
62
|
+
| 行号校准 | setup prompt:命名括注 `:19`、三要点复述 `:33-34`、复核义务 `:55-56`;README 三要点 `:129-133` |
|
|
63
|
+
|
|
64
|
+
讨论中审查对象发生变化(`0b28256` 在 15:34 落地),**结论绑定 revision**——执行时以当时 HEAD 为准。
|
|
65
|
+
|
|
66
|
+
## 2. ① `--cleanup` 落盘报告(`meeting_fs.report_path` + fail-open)
|
|
67
|
+
|
|
68
|
+
### 2.1 复验判定
|
|
69
|
+
|
|
70
|
+
- **实现正确**:`cleanup_discussion` 只调一次 `build_report`,同一份 `lines` 既打印又落盘(无二次遍历);落盘在 `shutil.rmtree` 之前;路径声明 `meeting_fs.report_path` 与 `result_path` 同家(各一行,未过度参数化)。`<base>-report.txt` 已在 `.gitignore:8`(`check-ignore` 实测命中)。
|
|
71
|
+
- **职责边界**:落盘点选在 cleanup("cleanup 是唯一清理点",design.md:202)正确;观测数字从"只在终端出现一次"变为"可复查"。
|
|
72
|
+
|
|
73
|
+
### 2.2 发现(按类别)
|
|
74
|
+
|
|
75
|
+
**(a) 文档描述簇(两个方向)**:
|
|
76
|
+
- 旧话未删:`observability.py:5`(模块头)与 `:217`(`build_report` docstring)仍写"**不持久化**……视图不占'数字的家'",`docs/design.md:204` 仍写"报告不自动落固定位(视图不占'家')"——同一提交只更新了 design.md:167-171(观测面契约),留下三处相反复述。
|
|
77
|
+
- 新话未加:`start_discussion.py:436`(cleanup docstring)与扩展收尾弹窗 `index.ts:180-181`("清理时还会打印一次分析报告")都未提报告已**落盘**——用户事后只找 result,不知道有 report.txt,① 的"可复查"收益在**发现性**上打折。
|
|
78
|
+
- 改法(按仓库纪律"删复述引权威 > 最短准确陈述"):`build_report` 处改为"函数自身不写文件;唯一落盘点在 `cleanup_discussion`";design.md:204 补"(cleanup 删除前例外)";弹窗/docstring 加一句"报告落盘到 `<分析目录>-report.txt`"。
|
|
79
|
+
|
|
80
|
+
**(b) fail-open 宽窄不对称**:生成段 `except Exception`(:454-459)与写盘段 `except OSError`(:465-470)——统一为两段同捕 `Exception`(对称、免推理;`join` 抛非 `OSError` 会阻断 rmtree 的自述矛盾一并消失)。
|
|
81
|
+
|
|
82
|
+
**(c) 显示层失败可跳过清理(本场最重要的行为发现,3/3 决定本批修)**:
|
|
83
|
+
- 现状三处逃逸点:banner print 在 `try` 之外(:452);`try` 内 handler 自身 `print`(:459)→ 再次 flush 时重抛;写盘段把 BrokenPipe 当 `OSError` 捕获后**也再 print**(:469-470)。此外 `_preserve_result_md` 的 print(:432)在报告段之前,也在保护之外。
|
|
84
|
+
- 结果**随缓冲而定**:整份输出留在缓冲区(管道 ≈8KB)→ 异常只在解释器退出时出现、清理已完成;报告超过缓冲或未缓冲 → 循环中途炸、**rmtree 被跳过**(目录残留 + rc≠0)。
|
|
85
|
+
- **修补形态(冻结)**:
|
|
86
|
+
- 新增 `_print_best_effort`;契约 = **`cleanup_discussion` 的全部 stdout 输出都走它**(含早退 `:446` 与 `_preserve_result_md` 的 `:432`)——按"出口"定义、可 `grep` 校验(函数内无裸 `print(`);
|
|
87
|
+
- 不变量:**`rmtree` 必达,唯一例外 = 产物留存真失败(I/O)**;显示层失败永不算留存失败;
|
|
88
|
+
- `_preserve_result_md(base)` 调用**留在 `try` 之外**——它真失败(`meeting_fs.py:1255` 的 `open(dest,"w")` 不吞)时冒泡、**不删目录**(result.md 权威位置 `<base>/repo.git` 在待删目录内,删了就永久丢失);
|
|
89
|
+
- `try/finally`(`rmtree` + 末行提示)只从**报告段**起;rmtree 自身失败仍冒泡;清理成功 **rc 0**。
|
|
90
|
+
|
|
91
|
+
**(d) 测试缺口**:
|
|
92
|
+
- 落盘内容 ≡ 打印内容**未断言**(现只 `assertIn("配额:meeting", report)`,tests/test_main_paths.py:319-323;落盘只写前 3 行也能过)——提交信息所称"内容与打印一致"未被锁;
|
|
93
|
+
- **写失败 fail-open 分支零覆盖**(`[cleanup] 报告保存失败(不影响清理)`,tests/ 无命中)。
|
|
94
|
+
|
|
95
|
+
### 2.3 冻结清单(①)
|
|
96
|
+
|
|
97
|
+
- [ ] `start_discussion.py`:`_print_best_effort` 助手 + 全部 stdout 走它 + `cleanup_discussion` 重排(`try/finally` 只包报告段、`rmtree` 在 `finally`;`_preserve_result_md` 在 try 外;统一捕 `Exception`;清理成功 rc 0)
|
|
98
|
+
- [ ] `observability.py:5`/`:217`、`design.md:204`:改为引用落盘点,不复述"不持久化"
|
|
99
|
+
- [ ] `start_discussion.py:436` docstring + `extensions/multi-viewers/index.ts:180-181` 弹窗:补"报告落盘 `<分析目录>-report.txt`"
|
|
100
|
+
- [ ] 测试:`assertEqual(落盘, 打印)`(print 已 mock,可直接重建)+ 写失败分支用例 + **三断言新用例**(目录已删 + rc 0 + `<base>-report.txt` 存在)
|
|
101
|
+
|
|
102
|
+
## 3. ② `mv.sh --set-viewer`
|
|
103
|
+
|
|
104
|
+
### 3.1 复验判定
|
|
105
|
+
|
|
106
|
+
- **四条保证确由机制保证**(逐条核对,判据复用 `spec_gen` 单一实现):命名合法(`check_agent_name`,写前);不覆盖(`os.path.exists` 写前拒绝 + 测试断言原文件未动);空正文拒绝(且不建目录);写完校验并回显(`_validate_and_print_viewers`)。
|
|
107
|
+
- **prompt 义务已下移**:第 3 步明确"四条由命令保证,不需要你另外检查";内容经 stdin 传命令(不直接写文件)。LLM 只负责内容——职责分工正确。
|
|
108
|
+
- 复杂度:`--viewers` 与 `--set-viewer` 共用同一校验实现(净简化,无第二套规则);守卫扁平、无 `--force`。
|
|
109
|
+
|
|
110
|
+
### 3.2 发现与裁定
|
|
111
|
+
|
|
112
|
+
- **半成功(职责边界含糊)**:`cmd_set_viewer` 先写文件、后调集合级校验;若 viewers/ 里**预先**有坏文件(空文件/非法名),命令 rc≠0 **但新文件已写入**。可复现:`viewers/` 放一个空 `甲.md` → `--set-viewer 乙` → `乙.md` 已写、rc=1。
|
|
113
|
+
**裁定(3/3):采纳 B′**(最小形态):
|
|
114
|
+
- 抽 `_viewer_set_errors(names, empty) = validate_participants(names) or (viewer_set_error(names, empty) if empty else None)`——**唯一组合点**(`if empty` 保护:否则建第 2 个视角会被 ≥2 判据误判为错误);
|
|
115
|
+
- `_validate_and_print_viewers` 改调它(判据组合不再有第二份);
|
|
116
|
+
- 前置检查放 `exists` 检查后、**读 stdin 前**(错误路径不消费输入),**只留 `if names:` 守卫**(去掉冗余 `os.path.isdir`——`_discover_viewers` 对目录缺失/空/无 md 一律返回 `(None,None,[])`;不守卫则 `validate_participants(None)` 抛 TypeError,**首次建视角即命中**);
|
|
117
|
+
- 组合用例:预置坏视角 → 断言 **rc≠0 且新文件不存在**;「已写入 `<path>`」仅出现在成功路径。
|
|
118
|
+
- 回退 A(零改动):保留半成功,但契约行必须写**双义**("rc≠0 = 未写入,或已写入但目录整体不合规——以「已写入」行区分"),且用一条用例钉住;A/B′ 的断言不可共用。
|
|
119
|
+
- **prompt 残留(3/3 同意删)**:
|
|
120
|
+
- `prompts/multi-viewers-setup.md:33-34`:三要点是 README:129-133 的**第二处完整复述**(该 prompt `:30` 已声明 README 为唯一事实源)→ 删复述、留指针(删后零额外读取);
|
|
121
|
+
- `:19` 命名括注是 `check_agent_name` 的部分抄写(漏 ≤32/human)→ 删;
|
|
122
|
+
- `:55-56` "编辑后跑 `--viewers` 复核"是 LLM 义务(有 prepare/start 硬 gate 兜底)→ 在决策 22 记为**有 backstop 的有意残留**,不新增编辑命令。
|
|
123
|
+
- `os.path.exists` → `os.path.lexists`(一字收紧"绝不覆盖",悬空符号链接是目前唯一破口);**不**铺符号链接测试矩阵。
|
|
124
|
+
|
|
125
|
+
## 4. ③ 观看命令交付通道(0.8.1 的 sendMessage + `0b28256` 的 widget)
|
|
126
|
+
|
|
127
|
+
### 4.1 事实核验
|
|
128
|
+
|
|
129
|
+
- 投递已实证:`sendMessage` 写入会话成功;**本场三个 fork 源各含恰好 1 条** `customType=multi-viewers` 的 `custom_message`(逐文件解析:481/550/555 条目中各 1)——**说明它进入每场分析各视角的上下文**(fork 源在**首唤**构建,`meeting_loop._prepare_fork_session`),不是"只在主 session"。
|
|
130
|
+
- 显示端:pi-web 把 `custom_message` 渲染为**折叠块**(需点击/重载)→ 用户实测"看不见";`notify` 弹窗关闭即消失;`setWidget` 面板为**一眼可见、不需点击**(pi-web 注释 "Persistent widget panel … Not a popup")。
|
|
131
|
+
|
|
132
|
+
### 4.2 角色模型(替代"缺一不可/三个出口"的说法)
|
|
133
|
+
|
|
134
|
+
| 通道 | 作用域 | 上下文成本 | 必需性/角色 |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| `setEditorText` 预填 | 仅 TUI | 0 | TUI 便利(能直接跑) |
|
|
137
|
+
| `notify` | 全模式 | 0 | 即时反馈(会消失) |
|
|
138
|
+
| `pi.sendMessage` | 全模式 | ~100–200 token/回合(主 session)+ 每场 fork 3×~100–200(一次性、有界) | **持久留痕**(折叠;跨重启仍在) |
|
|
139
|
+
| `ctx.ui.setWidget` | 全模式(pi-web 已验证) | 0(纯 UI) | **运行期常驻可见** |
|
|
140
|
+
|
|
141
|
+
### 4.3 冻结清单(③)
|
|
142
|
+
|
|
143
|
+
- [ ] `extensions/multi-viewers/index.ts:19` 与 **`:96`**(第二处,枚举漏 widget)→ 改四通道角色表述(与决策 22 一致);
|
|
144
|
+
- [ ] 决策 22 的 UI 通道段**收敛为 4 行角色表 + 三注记**:① fork 复制事实(`design.md:589` 现只写"参与 LLM 上下文的一行",须补"随 fork 进每场分析");② widget「**(运行期)**」(fire-and-forget UI 状态、非会话条目,跨重启不恢复);③ why-not-appendEntry(零上下文留痕档存在但渲染 TUI-only,pi-web 不可见);
|
|
145
|
+
- [ ] 文档可如实写"显示层失败也不阻断清理"(①(c) 修完后);
|
|
146
|
+
- **不做**:fork 过滤(`build_fork_source` 不按 customType 过滤;新增黑名单=构造层获得扩展类型知识,跨层耦合换 ~2 行噪音,不配);`ctx.mode` 分支(把客户端差异搬进扩展);"瘦身" sendMessage(收益 <50 token/回合,不配 churn)。
|
|
147
|
+
|
|
148
|
+
### 4.4 display-only 口径(写准,防未来翻案)
|
|
149
|
+
|
|
150
|
+
`sendMessage` **无** excludeFromContext 选项(options 仅 `{triggerTurn, deliverAs}`;`excludeFromContext` 属 bashExecution);**零上下文留痕档存在**——`pi.appendEntry` + `registerEntryRenderer`("do NOT participate in LLM context"),但渲染器是 TUI 组件、pi-web 无渲染路径 ⇒ **可见 ∩ 零上下文 = 空集**,跨模式成本不可归零、接受现值。widget 的零上下文已覆盖"面板可见"需求。
|
|
151
|
+
|
|
152
|
+
## 5. 测试与断言(冻结清单)
|
|
153
|
+
|
|
154
|
+
- **harness 单一日志(第一优先简化)**:mock 的 `sendMessage` 也 push 进 `calls`(kind="sendMessage");删 `sent` 数组与两段特判;成功后精确 `eq(kinds(calls), [...])` 一次覆盖四通道;取消/失败路径精确 `eq(kinds(calls), ["notify:error"])`(现失败路径只 `includes("notify:error") && !includes("setEditorText")`,四通道下只挡一个);content 用**语义包含**(`includes(dir)`/`includes(watch)`),不锁排版;D2 负断言改**正向**("含 TUI 限定词")。
|
|
155
|
+
- ①:等值断言 + 写失败分支 + 显示层三断言用例(目录已删 + rc 0 + report.txt 存在)。
|
|
156
|
+
- ②:组合用例(rc≠0 且文件不存在);两条 None 路径已由现有首建用例覆盖(`test_creates_and_validates` / `test_creates_viewers_dir_when_missing`),无需新装置。
|
|
157
|
+
- 数字:harness 现 **48** 条;本批预计 +4~6 条断言、删 `sent` 相关代码——**净减代码**。
|
|
158
|
+
|
|
159
|
+
## 6. 文档同步清单
|
|
160
|
+
|
|
161
|
+
1. **报告出口描述**:`design.md:139` 第 2 项(`--cleanup`)补"打印 + **落盘快照**",**计数保持三**(比加第四出口少一个概念;`design.md:106`、`meeting_fs.py:57` 不动)。
|
|
162
|
+
2. **术语拆分**:观看命令 = 「**四个交付通道**」;报告保留「出口」(全仓"三个出口"5 处属两个机制,改 `:19`/`:96` 时勿误伤报告三处)。
|
|
163
|
+
3. 决策 22:角色表 + 三注记(见 4.3);观测面契约处(:167-171)已更新,保持。
|
|
164
|
+
4. `AGENTS.md`:如无引用冲突无需改(版本号唯一事实源 = package.json;本批若发版按"修复+文档"升第三位)。
|
|
165
|
+
5. 版本引用按 §1 分段。
|
|
166
|
+
|
|
167
|
+
## 7. 明确不做 / 已知限 / 回退口径
|
|
168
|
+
|
|
169
|
+
- **不做**:fork 过滤、`ctx.mode` 分支、符号链接测试矩阵、② 的 C 方案(改为"创建成功即 rc0、集合问题仅提示"——属产品行为变更)、为 prompt 的"复核义务"新增编辑命令。
|
|
170
|
+
- **回退口径**:② 若取 A,契约行写双义(§3.2);①(c) 若本批不修(不推荐,已 3/3 支持修),则须双落点——`cleanup_discussion` 代码注释(主防线)+ 结论一行,且**不得**声称"fail-open 覆盖显示层"。
|
|
171
|
+
- **已知限**(记录在案):widget 仅运行期常驻(pi 重启/reload 后不恢复,持久记录靠折叠的 custom_message);手动 `mv.sh --cleanup` 或外部删除后,面板会短暂指向已删目录(下次运行同 key 覆盖);`sendMessage` 内容参与每场 fork 上下文(~2 行/场,预算窗口内,有界)。
|
|
172
|
+
|
|
173
|
+
## 8. 收敛过程(消息索引)
|
|
174
|
+
|
|
175
|
+
- **开题(效率/0001、简单/0001、铁律/0001)**:效率给运行开销账(三处净收益为正)+ 两问(report.txt 是否 ignore、display-only 档);简单给简洁性复验(① 哨兵可省/两文案未同步、② 最干净、③ 头注释三出口矛盾)+ harness 失败路径断言弱于取消路径;铁律按三铁律逐条核(① 三处"不持久化"只同步一处、断言强度、fail-open 宽窄;② 校验在写之后 + prompt 复述;③"缺一不可"强于设计 + fork 下游效应 + 版本归属修正)。
|
|
176
|
+
- **交错与澄清**:挂账两项由简单/铁律独立结清(`.gitignore:8` 命中;零上下文档存在但 TUI-only);效率两次撤回("viewer 看不到"被 fork 源证据证伪;"零成本前置"被组合复制论证撤回);简单撤回哨兵建议(BrokenPipe 反例成立);铁律更正自身(fork 在首唤构建,本场即已进入;0009 的"受保护块从留存开始"按字面会丢产物,由简单/0007 修正、铁律/0015 确认)。
|
|
177
|
+
- **关键分歧裁定**:① 显示层项——效率先降级 → 铁律/0007 代码验证(三逃逸点、缓冲而定)→ 效率/0008 收回、支持本批修 → 简单/0006 站修 → **3/3 修**;② 半成功——效率 A→B′、简单 A→B′、铁律 B′,**最终三方一致 B′**(A 回退写双义);③ 通道——从"三个出口(缺一不可)"收敛为四通道角色模型(保留、不加过滤)。
|
|
178
|
+
- **RR 表态**:效率/0015、简单/0011、铁律/0015 均 `pass`,无异议——共识闭合。
|
|
179
|
+
|
|
180
|
+
## 9. 一句话结论
|
|
181
|
+
|
|
182
|
+
**三处机制可以收下;本批修复的全部是"防静默失效"类问题——清理必达(唯一例外=产物留存真失败)、校验先于副作用、文档按实际写;生产代码净增 ≈11 行、断言净增 ≈4~6 条(并删 `sent` 相关代码),运行时零变化。**
|
package/docs/reviews/README.md
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
| `2026-09-14-e2e25-doc-drift-review.md` | 文档 vs 代码一致性(文档漂移)+ ctx_search 的**自然使用**观察 | **三处文档仍写"兜底 mv-* 并警告"而代码是无兜底、未匹配报错**(行为语义相反);README 缺 `--extension-policy`;报告字段列表过期;4 条缺失项;**根因 = 对实现的复述**(治本:引事实源不复制);自然使用观察:`ctx_search` **0 次**、historian 0 次 | `ba204ef` |
|
|
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
|
+
| `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` |
|
|
40
41
|
|
|
41
42
|
## 环境口径(读报告时的背景)
|
|
42
43
|
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 与 CLI 的契约(标记行/退出码)与全部原语见 ./shared.ts。
|
|
18
18
|
*
|
|
19
|
-
* 观看命令交付 =
|
|
20
|
-
* ① `ctx.ui.setEditorText`
|
|
21
|
-
* ② `notify
|
|
22
|
-
* ③ `pi.sendMessage
|
|
23
|
-
*
|
|
19
|
+
* 观看命令交付 = **四个通道,各司其职**(每一个都是实测逼出来的,见 docs/design.md 决策 22):
|
|
20
|
+
* ① `ctx.ui.setEditorText` 预填输入框——TUI 便利(能直接回车跑);pi-web 忽略
|
|
21
|
+
* ② `notify`——即时反馈;pi-web 上关掉弹窗即消失
|
|
22
|
+
* ③ `pi.sendMessage`(custom_message)——**持久留痕**,跨重启仍在;但 pi-web 渲染为
|
|
23
|
+
* **折叠的** `multi-viewers (click to expand)`,且随 fork 进入每场分析上下文
|
|
24
|
+
* ④ `ctx.ui.setWidget`——**运行期常驻可见**(一眼看到、不需点击;MC 待办用的同一通道)
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
import {
|
|
@@ -91,7 +92,8 @@ export default function register(pi: any) {
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// ④
|
|
95
|
+
// ④ 观看命令:四个通道各司其职(见文件头与决策 22)——预填(TUI)/ notify(即时)/
|
|
96
|
+
// custom_message(留痕)/ widget(常驻可见)
|
|
95
97
|
ctx.ui.setEditorText(watch);
|
|
96
98
|
// 消息流里留一条持久记录:用户实测 pi-web 的 notify 会随弹窗关闭而消失,
|
|
97
99
|
// 关了窗口就再也找不到这行命令。custom_message 进会话(**参与 LLM 上下文**,
|
|
@@ -102,6 +104,15 @@ export default function register(pi: any) {
|
|
|
102
104
|
content: `多视角分析已启动:${dir}\n观看命令(复制执行,不进 LLM):\n${watch}`,
|
|
103
105
|
display: true,
|
|
104
106
|
});
|
|
107
|
+
// 常驻面板:pi-web 实测把 custom_message 渲染成折叠的 `multi-viewers (click to expand)`,
|
|
108
|
+
// 且历史条目未必实时刷新 → 观看命令还需要一条**一眼可见、不需点击**的常驻出口。
|
|
109
|
+
// setWidget 正是这个语义(pi-web 注释:"Persistent widget panel … Not a popup";
|
|
110
|
+
// 主 pi 的 magic-context 待办面板用的就是它)。
|
|
111
|
+
ctx.ui.setWidget("multi-viewers", [
|
|
112
|
+
`多视角分析进行中:${dir}`,
|
|
113
|
+
`观看(复制执行,不进 LLM):${watch}`,
|
|
114
|
+
`插话 /multi-viewers-say <文本> 收尾 /multi-viewers-finish`,
|
|
115
|
+
]);
|
|
105
116
|
ctx.ui.notify(
|
|
106
117
|
`分析已启动:${dir}\n` +
|
|
107
118
|
"观看(复制执行;TUI 下已预填进输入框):\n" +
|
|
@@ -167,7 +178,7 @@ export default function register(pi: any) {
|
|
|
167
178
|
const ok = await ctx.ui.confirm(
|
|
168
179
|
"确认收尾?",
|
|
169
180
|
"将清理分析目录;结果会保存到 `<分析目录>-result.md`," +
|
|
170
|
-
"
|
|
181
|
+
"清理时还会打印并落盘一份报告(<分析目录>-report.txt)。",
|
|
171
182
|
);
|
|
172
183
|
if (!ok) {
|
|
173
184
|
ctx.ui.notify("已取消收尾(分析目录保留)。", "info");
|
|
@@ -178,6 +189,8 @@ export default function register(pi: any) {
|
|
|
178
189
|
ctx.ui.notify(`收尾失败:\n${clean.output}`, "error");
|
|
179
190
|
return;
|
|
180
191
|
}
|
|
192
|
+
// 收尾成功 → 清掉常驻面板(否则留下一行指向已删除目录的观看命令)。
|
|
193
|
+
ctx.ui.setWidget("multi-viewers", undefined);
|
|
181
194
|
ctx.ui.notify(
|
|
182
195
|
`${clean.output}\n\n要摘要就在对话里说一声(主 pi 读该 result.md 即可)。`,
|
|
183
196
|
"success",
|
package/mv_cli.py
CHANGED
|
@@ -186,11 +186,9 @@ def _validate_and_print_viewers(vdir):
|
|
|
186
186
|
notes.append("空:没有视角内容")
|
|
187
187
|
suffix = f"({';'.join(notes)})" if notes else ""
|
|
188
188
|
print(f" {n}.md{suffix}")
|
|
189
|
-
err = spec_gen.
|
|
189
|
+
err = spec_gen._viewer_set_errors(names, empty)
|
|
190
190
|
if err:
|
|
191
191
|
fail_verbatim(err)
|
|
192
|
-
if empty:
|
|
193
|
-
fail_verbatim(spec_gen.viewer_set_error(names, empty))
|
|
194
192
|
gap = spec_gen.viewers_count_gap(names)
|
|
195
193
|
if gap:
|
|
196
194
|
print(f" 校验:{gap}(建 1 个是合法的中间状态)")
|
|
@@ -217,8 +215,19 @@ def cmd_set_viewer(args):
|
|
|
217
215
|
fail(f"非法视角名({err}):{name}")
|
|
218
216
|
vdir = os.path.join(os.getcwd(), "viewers")
|
|
219
217
|
target = os.path.join(vdir, f"{name}.md")
|
|
220
|
-
|
|
218
|
+
# lexists(不是 exists):悬空符号链接在 exists 下为假 → 会被"覆盖"写入,
|
|
219
|
+
# 违背"绝不覆盖"的承诺(悬空链接是这条承诺目前的唯一破口)。
|
|
220
|
+
if os.path.lexists(target):
|
|
221
221
|
fail(f"视角已存在,不覆盖: {target}(改名,或直接编辑该文件)")
|
|
222
|
+
# **写前**做集合级校验(评审 ② B′):否则 viewers/ 里已有坏文件时,本命令
|
|
223
|
+
# 会"先写成功、再以 rc≠0 退出"——副作用已发生却报失败(半成功)。
|
|
224
|
+
# 用 if names 守卫:names 为 None 表示目录还不存在/还没有视角,那是合法起点
|
|
225
|
+
# (不守卫会让 validate_participants(None) 抛 TypeError——首次建视角即命中)。
|
|
226
|
+
names, _briefs, empty = spec_gen._discover_viewers(vdir)
|
|
227
|
+
if names:
|
|
228
|
+
err = spec_gen._viewer_set_errors(names, empty)
|
|
229
|
+
if err:
|
|
230
|
+
fail_verbatim(f"{err}\n(修正 viewers/ 后再建新视角——本次未写入任何文件)")
|
|
222
231
|
body = sys.stdin.read().strip()
|
|
223
232
|
if not body:
|
|
224
233
|
fail(f"视角内容为空({name})——正文从 stdin 传入;空视角没有 lenses,"
|
package/observability.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""观测层——状态判定 / --report / --wait(从 start_discussion 拆出,S2)。
|
|
2
2
|
|
|
3
3
|
职责:**运行期只读观测**——check_status 状态机、--report 观测面聚合、
|
|
4
|
-
--wait 阻塞观察、loop
|
|
5
|
-
|
|
4
|
+
--wait 阻塞观察、loop 进程存活检测。本层**不写任何文件**:报告的唯一落点
|
|
5
|
+
在 `start_discussion.cleanup_discussion`(删目录前打印并落盘一份
|
|
6
|
+
`<base>-report.txt`,观测数字可复查)——本模块只生成行、不负责落地。
|
|
6
7
|
|
|
7
8
|
依赖方向:只准 import meeting_core / meeting_fs / meeting_engine /
|
|
8
9
|
human_viewer(observability 是"读"侧,human_viewer.incremental 是它
|
|
@@ -214,8 +215,9 @@ def build_report(base):
|
|
|
214
215
|
|
|
215
216
|
契约(design.md 观测面契约节):
|
|
216
217
|
- **冷路径一次性**:不常驻、不被轮询;调用方(人/主 pi)按需触发。
|
|
217
|
-
-
|
|
218
|
+
- **本函数不落盘**:视图不占"数字的家"——数字的家是 bare(判定域)、
|
|
218
219
|
loop log 的登记字段、pi session 的文档化字段;报告只是它们的一次投影。
|
|
220
|
+
唯一的落盘点是 `cleanup_discussion`(删目录前写 `<base>-report.txt`)。
|
|
219
221
|
- **fail-open**:任何一段读不出(缺目录/缺文件/格式变)→ 该段显示 n/a,
|
|
220
222
|
不报错、不改判定、不阻塞。
|
|
221
223
|
- **跨度分标**:进程跨度(elapsed_ms)≠ per-response 跨度(session
|
package/package.json
CHANGED
|
@@ -16,7 +16,6 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
|
|
|
16
16
|
|
|
17
17
|
- 这一步**只提议**:不建文件、不改任何东西
|
|
18
18
|
- 建几个**由用户决定**(只建 1 个也行)
|
|
19
|
-
- 名字规则由第 3 步的命令强制(顺带一提:中文、短、不含空白与路径分隔符)
|
|
20
19
|
|
|
21
20
|
**然后结束本回合,等用户输入。**
|
|
22
21
|
|
|
@@ -27,14 +26,7 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
|
|
|
27
26
|
|
|
28
27
|
## 第 3 步:用命令建文件(内容经 stdin 传给命令,不要直接写文件)
|
|
29
28
|
|
|
30
|
-
先读 `README.md`
|
|
31
|
-
|
|
32
|
-
- 纯内容、无 frontmatter、无标题——**文件名就是全部元数据**
|
|
33
|
-
- 必含三条:① **单一 lenses**(所有观点必须从该视角出发)② **不越界**(其它视角由
|
|
34
|
-
别的参与者负责;**不要**列举是哪几个——参与者会变,列举会过期)③ **交锋义务**
|
|
35
|
-
(对其它视角的观点可认同或反驳,但要用本视角的论据)
|
|
36
|
-
- **只写视角本身**:身份("你是 X")、参与者名单、消息格式、独立参与者纪律都由脚本
|
|
37
|
-
生成,**不要写进文件**
|
|
29
|
+
先读 `README.md` 的「视角文件写什么」节——那是唯一事实源(三个要点、正误对照、命名规范)。
|
|
38
30
|
|
|
39
31
|
每个视角调用一次(正文经 stdin;**命名合法性、不覆盖已有、空正文拒绝、写完校验并
|
|
40
32
|
回显**都由命令保证,不需要你另外检查):
|
package/spec_gen.py
CHANGED
|
@@ -478,6 +478,21 @@ def viewer_set_error(names, empty, where="viewers/"):
|
|
|
478
478
|
return f"错误: {gap}" if gap else None
|
|
479
479
|
|
|
480
480
|
|
|
481
|
+
def _viewer_set_errors(names, empty, where="viewers/"):
|
|
482
|
+
"""集合级校验的**唯一组合点**:整组名字 + 空正文 + 数量 ≥2。
|
|
483
|
+
|
|
484
|
+
为什么单独存在:`--viewers`(只读检查)与 `--set-viewer`(写前校验)必须
|
|
485
|
+
用**同一套**判据——同一套规则曾在两处漂移过(文案与检查项不一致)。
|
|
486
|
+
`if empty` 是必要保护:否则 viewers/ 里只有一个**合法**视角时,
|
|
487
|
+
`viewer_set_error` 会因为数量不足而报错,把"建第 2 个视角"判成非法。
|
|
488
|
+
`names` 为空(目录缺失/无 .md)返回 None——那是"还没有视角",不是错误。
|
|
489
|
+
"""
|
|
490
|
+
if not names:
|
|
491
|
+
return None
|
|
492
|
+
return (validate_participants(names)
|
|
493
|
+
or (viewer_set_error(names, empty, where) if empty else None))
|
|
494
|
+
|
|
495
|
+
|
|
481
496
|
def _discover_viewers(viewers_dir):
|
|
482
497
|
"""发现 viewers 目录(多视角产品约定):*.md 文件名即 agent 名。
|
|
483
498
|
|
package/start_discussion.py
CHANGED
|
@@ -424,52 +424,85 @@ def setup_environment(args, participants, base, spec_dir=None,
|
|
|
424
424
|
f"extensionPolicy={args.extension_policy}")
|
|
425
425
|
|
|
426
426
|
|
|
427
|
+
def _print_best_effort(*args, **kwargs):
|
|
428
|
+
"""打印,但**绝不让显示层失败影响主职责**(清理 / 产物留存)。
|
|
429
|
+
|
|
430
|
+
为什么需要:`cleanup_discussion` 的输出可能在管道关闭时抛
|
|
431
|
+
`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` 里冒泡,见其注释)。
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
print(*args, **kwargs)
|
|
441
|
+
except Exception: # noqa: BLE001(显示层失败永不上抛)
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
|
|
427
445
|
def _preserve_result_md(base):
|
|
428
446
|
"""清理前保存 result.md(薄包装 → meeting_fs.preserve_result_md,
|
|
429
|
-
T2 合并:与 loop 退出路径共享同一实现)。
|
|
447
|
+
T2 合并:与 loop 退出路径共享同一实现)。
|
|
448
|
+
|
|
449
|
+
这里的失败**必须冒泡**(不吞):result.md 的权威位置在 `<base>/repo.git`,
|
|
450
|
+
即**待删目录之内**——留存失败还继续删 = 永久丢失产物。所以它不包在
|
|
451
|
+
报告段的 try/finally 里(评审批 ①(c):唯一允许阻断清理的失败)。
|
|
452
|
+
"""
|
|
430
453
|
dest = meeting_fs.preserve_result_md(base)
|
|
431
454
|
if dest:
|
|
432
|
-
|
|
455
|
+
_print_best_effort(f"[cleanup] 已保存 result.md → {dest}")
|
|
433
456
|
|
|
434
457
|
|
|
435
458
|
def cleanup_discussion(base):
|
|
436
|
-
"""清理一次讨论:保存 result.md(若存在)→ 删目录。
|
|
459
|
+
"""清理一次讨论:保存 result.md(若存在)→ 打印并落盘报告 → 删目录。
|
|
437
460
|
|
|
438
461
|
result.md 是讨论唯一产物(审核报告等)——清理前先从 bare git 历史
|
|
439
462
|
复制到父级目录(<base名>-result.md),避免清理丢产物(用户建议)。
|
|
463
|
+
报告在本步**打印并落盘**到 `<base>-report.txt`(原先只在终端出现一次,
|
|
464
|
+
目录删掉后无法复查——复盘时长口径时踩到):观测数字应当可复查。
|
|
440
465
|
Pi 的 session 文件存放在 <base>/pi-sessions,随目录一起删除,无需
|
|
441
466
|
额外清理全局 DB。
|
|
442
467
|
不负责终止 loop 进程(职责边界,用户 2026-08-31 定)——loop 每轮
|
|
443
468
|
检测到 repo.git 消失即自行退出(meeting_engine.agent_loop)。
|
|
469
|
+
|
|
470
|
+
失败语义(评审批 ①(c) 冻结):**rmtree 必达**;报告生成 / 打印 / 落盘
|
|
471
|
+
失败都不阻断清理(fail-open,捕 `Exception` 而非只捕 `OSError`——两段
|
|
472
|
+
对称,不依赖"这些代码只可能抛 OSError"的脆弱推理);唯一例外是
|
|
473
|
+
`_preserve_result_md` 真失败(I/O)→ 冒泡且**不删目录**。
|
|
474
|
+
清理成功返回 0(显示层失败不算失败)。
|
|
444
475
|
"""
|
|
445
476
|
if not os.path.isdir(base):
|
|
446
|
-
|
|
477
|
+
_print_best_effort(f"[cleanup] 目录不存在: {base}")
|
|
447
478
|
return
|
|
448
479
|
_preserve_result_md(base)
|
|
449
|
-
# 报告(**删目录前最后一次可读**——目录删后 --report 不可用)。
|
|
450
|
-
# 报告是附加信息、清理是主职责:报告生成失败**不阻断**清理
|
|
451
|
-
# (fail-open 只在这一层兜底——build_report 内部各段已各自 fail-open)。
|
|
452
|
-
print("[cleanup] —— 本次分析报告(删除目录前最后一次可读)——")
|
|
453
|
-
lines = None
|
|
454
480
|
try:
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if lines is not None:
|
|
461
|
-
# 落盘一份(与 <base>-result.md 同级):报告本来只在终端出现一次,
|
|
462
|
-
# 目录删掉后 --report 也不可用 → 观测数字不可复查(复盘时长口径时
|
|
463
|
-
# 踩过)。与 result.md 同样的 fail-open:写不动不阻断清理。
|
|
464
|
-
rp = meeting_fs.report_path(base)
|
|
481
|
+
# 报告(**删目录前最后一次可读**——目录删后 --report 不可用)。
|
|
482
|
+
# 报告是附加信息、清理是主职责:报告生成失败**不阻断**清理
|
|
483
|
+
# (fail-open 只在这一层兜底——build_report 内部各段已各自 fail-open)。
|
|
484
|
+
_print_best_effort("[cleanup] —— 本次分析报告(删除目录前最后一次可读)——")
|
|
485
|
+
lines = None
|
|
465
486
|
try:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
except
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
487
|
+
lines = list(build_report(base))
|
|
488
|
+
for line in lines:
|
|
489
|
+
_print_best_effort(line)
|
|
490
|
+
except Exception as e: # noqa: BLE001(兜底不吞:打印)
|
|
491
|
+
_print_best_effort(f"[cleanup] 报告生成失败(不影响清理): {e!r}")
|
|
492
|
+
if lines is not None:
|
|
493
|
+
# 落盘一份(与 <base>-result.md 同级):报告本来只在终端出现一次,
|
|
494
|
+
# 目录删掉后 --report 也不可用 → 观测数字不可复查(复盘时长口径时
|
|
495
|
+
# 踩过)。与 result.md 同样的 fail-open:写不动不阻断清理。
|
|
496
|
+
rp = meeting_fs.report_path(base)
|
|
497
|
+
try:
|
|
498
|
+
with open(rp, "w", encoding="utf-8") as f:
|
|
499
|
+
f.write("\n".join(lines) + "\n")
|
|
500
|
+
_print_best_effort(f"[cleanup] 报告已保存 → {rp}")
|
|
501
|
+
except Exception as e: # noqa: BLE001(同生成段:对称)
|
|
502
|
+
_print_best_effort(f"[cleanup] 报告保存失败(不影响清理): {e!r}")
|
|
503
|
+
finally:
|
|
504
|
+
shutil.rmtree(base) # 必达(唯一例外见 docstring)
|
|
505
|
+
_print_best_effort(f"[cleanup] 已删除目录 {base}(含 pi-sessions)")
|
|
473
506
|
|
|
474
507
|
|
|
475
508
|
|