pi-multi-viewers 0.2.0 → 0.2.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
@@ -176,7 +176,7 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
176
176
  extension × 1(multi-viewers-say 插话:零 LLM,直接 spawn human_sayer.py;
177
177
  目录发现 = `<cwd>/mv-<sessionId>-*` 最新,兜底 `mv-*`(排除
178
178
  `mv-spec-*`)并警告)
179
- + wrapper。**npm 已发布 0.2.0(2026-09-11)**。
179
+ + wrapper。**npm 已发布 0.2.2(2026-09-11)**。
180
180
  - **开发机安装(两步,缺一不可;2026-09-10 实测)**:
181
181
  ① `pi install /root/pi-multi-viewers`——**注册包**(写
182
182
  `~/.pi/agent/settings.json` 的 `packages` 数组);pi 不是"扫 node_modules
package/README.md CHANGED
@@ -74,6 +74,12 @@ ls ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh
74
74
  两个 pi 命令入口(视角/主题走 prompt,插话走 extension——插话是"本地命令
75
75
  执行",不需要经过 LLM)。
76
76
 
77
+ **目录可以省略**:`--view`/`--say`/`--status`/`--report`/`--wait`/`--cleanup`
78
+ 不带目录时自动定位"本 session 当前分析"(`mv-<sessionId>-*` 最新;找不到
79
+ 则取最新 `mv-*` 并警告;判据 = 含 `repo.git`)。传目录仍支持(显式优先)。
80
+ 这样路径不需要经过任何 LLM 记忆——此前命令都要求绝对路径,等于让主 pi
81
+ 把长路径记在上下文里复用。
82
+
77
83
  或命令行:
78
84
 
79
85
  ```bash
@@ -88,15 +94,16 @@ scripts/mv.sh --start <spec目录> # 启动(自动挂载主 sessi
88
94
  # 高级:--agents "a,b" 起一次性视角(不建 viewers/ 时用;prompt 入口不传它)
89
95
 
90
96
  # 观看:--start 会输出可直接执行的 !! 流式观看命令(复制执行)
91
- scripts/mv.sh --view <dir> # 或一次性增量查看(主 pi 记录 HEAD 作下轮 --since)
97
+ scripts/mv.sh --view # 一次性增量查看(主 pi 记录 HEAD 作下轮 --since)
92
98
  # --follow 会打印【状态】(meeting/all-freezing/round-robin/concluded)
93
99
  # 与【进度】(meeting 消耗/上限 | freezing 集合 | rr → 下一位)
100
+ # 结束时自动附【分析报告】(消息/墙钟/配额/进程跨度/LLM 用量)
94
101
 
95
- # 插话 / 状态 / 收尾
96
- scripts/mv.sh --say <dir> "<文本>" # 插话(命令行形态;pi 内用 /multi-viewers-say)
97
- scripts/mv.sh --status <dir> # running / done / stalled / stopped
98
- scripts/mv.sh --report <dir> # 只读报告(流程/配额/进程/LLM;冷路径,不持久化)
99
- scripts/mv.sh --cleanup <dir> # 收尾(result.md 自动留存到 <dir>-result.md)
102
+ # 插话 / 状态 / 收尾(目录可省略——自动定位本 session 当前分析)
103
+ scripts/mv.sh --say "<文本>" # 插话(命令行形态;pi 内用 /multi-viewers-say)
104
+ scripts/mv.sh --status # running / done / stalled / stopped(done 时附 [result] 路径)
105
+ scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM;冷路径,不持久化)
106
+ scripts/mv.sh --cleanup # 收尾(result.md 自动留存到 <dir>-result.md)
100
107
  ```
101
108
 
102
109
  ## 视角文件写什么(`viewers/<视角名>.md`)
package/docs/design.md CHANGED
@@ -100,7 +100,18 @@ compaction 的 `firstKeptEntryId` 起 + 其后的条目"——窗口内含 compa
100
100
  | `status-<agent>.json` | loop | `{"sessionID": ...}` | 流程(崩溃恢复) | 是(恢复用) | O(1) |
101
101
  | `pi-sessions/fork-src-*.jsonl` | pi | 文档化 session schema | fork 构建 + `--report` | 否(报告用) | O(MB) 全量 → **禁轮询** |
102
102
  | `result.md`(固定位) | resultWriter loop | 结论文档 | 人 | 是(收尾判据) | — |
103
- | `--report`(视图) | start_discussion | 文本行 | 人/主 pi | **否**(不得升级为验收 gate) | 冷路径一次性 |
103
+ | `--report`(视图) | observability | 文本行 | 人(**三个出口**,见下) | **否**(不得升级为验收 gate) | 冷路径一次性 —— **O(session 大小)**:每 agent 读整个 fork-src jsonl(实测 3 × 789KB ≈ 2.4MB/次、50–150ms/次,×3 出口 <0.3s/次分析),**不得进入任何轮询路径**(e2e16 评审量化) |
104
+
105
+ **报告的三个出口**(同一 `build_report`,同一份内容):
106
+ 1. **`--follow` 结束**——viewer 在 done 分支自动附报告(`human_viewer._print_report`)。
107
+ 这是**用户通道自带**:用户执行 `!!` 命令就在结束时直接看到,**零 LLM 参与**
108
+ (此前只靠 prompt 要求主 pi"记得转述"——那是 LLM 依赖,会漏;机制化后
109
+ 用户必然看到)。
110
+ 2. **`--cleanup`**——删目录前最后一次可读(结果与 1 重复出现是刻意的:
111
+ 不同时点各看一次,且清理后现场已不存在)。
112
+ 3. **`--report`**——独立入口(中途查看 / 脚本消费)。
113
+ `--view --since`(主 pi 增量轮询通道)**不附报告**——它面向 LLM,
114
+ 输出进 context,报告对模型无用且占 token。
104
115
 
105
116
  ### 本轮边界(`mv.analysis-start`)
106
117
 
@@ -265,7 +276,39 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
265
276
  `start_discussion.check_status` 与定义处同址,拆后 mock re-export
266
277
  不生效——本轮 4 处测试因此假绿/失败,已改到 `spec_gen` /
267
278
  `observability`)。
268
- 14. **git 守卫范围 = 从讨论 workdir 发起的操作**(`GIT_CEILING_DIRECTORIES`
279
+ 14. **报告附在 `--follow` 输出末尾(机制化,不依赖 LLM)**:`--follow`
280
+ 用户直接执行的通道(`!!`),done 时自动打印报告——用户零操作看到运行
281
+ 事实。**为什么不能只靠 prompt**:让主 pi"记得跑 `--report` 并转述"是
282
+ 流程依赖 LLM(会漏、不可验收),正是本项目一贯要消除的形态;报告既然
283
+ 是给用户的,就该长在用户直接看的通道上。`--view --since` 不附(主 pi
284
+ 通道,进 context 且对模型无用)。
285
+ 15. **消费命令的目录可省略(自动发现,仅精确 sid 匹配)**:`--view/--say/
286
+ --status/--report/--wait/--cleanup` 不带目录 → 按 cwd + `PI_SESSION_ID`
287
+ 发现当前分析(`mv-<sid>-*` 最新;判据 = 含 `repo.git`,同 engine
288
+ "bare 是分析存在的唯一标志";`mv-spec-*` 不在前缀内)。
289
+ **无降级兜底**(e2e16 评审 2:0:1 裁定):曾有"无 sid 匹配 → 取项目下
290
+ 最新 `mv-*`"的兜底,三宗罪——①破坏性操作(`--cleanup`/`--say`)会作用
291
+ 于**猜测目录**;②降级只能靠 stderr 中文文案识别(extension 曾用
292
+ `includes("警告")` 还原布尔,文案一改静默失效,与"判据用退出码"自相
293
+ 矛盾);③"最新"按整名排序,跨 sid 时**系统性取旧**。核查确认**不存在
294
+ "必须无目录且必然无 sid"的设计内场景**(pi 两条通道都有 sid,终端主路径
295
+ 本就显式带目录)→ 未匹配 = rc 1 报错请显式传目录。
296
+ **动机**:唯一知道路径的是 `--start` 的输出,此前每个消费命令都要求
297
+ 传它 → 主 pi 必须把长绝对路径记在 LLM 上下文里复用(改错/截断/相对
298
+ 路径都出过)。发现逻辑下沉后**路径不经过 LLM**。
299
+ 实现单点 = `observability.find_current_dir`(wrapper 的 `resolve_dir`
300
+ 与 extension 同走 `observability.py --find-dir`——此前只有 extension
301
+ 里一份 TS 实现,wrapper 侧完全没有,两份口径会漂移)。
302
+ `--say` 两形态**按参数个数区分**(1 个 = 文本+自动发现;2 个 = 目录+
303
+ 文本)——不是按值猜语义。
304
+ 配套:`--status` 在 done 时打印 `[result] <路径>`(目录可省略后调用方
305
+ 无法自己拼 `<目录>-result.md`,路径必须由机制给出)。
306
+ 16. **prompt 里的失败判据用退出码,不用报错文本匹配**:原 prompt 要求
307
+ "若报错含'未找到 viewers/ 目录'…" —— 匹配 stderr 文案,wrapper 文案
308
+ 一改就静默失配(LLM 会以为没报错而继续)。改为"命令非零退出 → 停下来
309
+ 读报错原文问用户":判据降为退出码(wrapper 的 `fail()` 保证 `exit 1`),
310
+ 原因解释交还给输出原文。
311
+ 17. **git 守卫范围 = 从讨论 workdir 发起的操作**(`GIT_CEILING_DIRECTORIES`
269
312
  注入于 spawn);主项目仓库不在守卫范围(agent 的 cwd 就是主项目,其
270
313
  约束归指令层 + 主项目 `.gitignore`)。要拦主仓库需换机制类(沙箱/钩子),
271
314
  经评估收益不支撑扩面。
@@ -0,0 +1,177 @@
1
+ > **存档说明**:2026-09-11 真实多视角分析(性能 / 简单 / 铁律)的 result.md
2
+ > 原文,主题为「审阅本项目最近两项机制化改动(消费命令目录可省略 + 报告
3
+ > 机制化)的实现质量与遗漏」——即对 `be958ec` 与 `d20fc46` 的自审。
4
+ > **抓到 1 个真回归(F5:`--view` 显式目录被静默丢弃)与 1 个高危设计缺陷
5
+ > (降级兜底:破坏性操作可作用于猜测目录)**,另有 5 条中低问题与 7 项
6
+ > "确认符合设计"。修复见后续 commit。
7
+ > 报告编号(F1–F8 等)为**当次讨论内部编号**,不可跨文档核验;引用的行号与
8
+ > 实测数字均对应当时工作区状态。
9
+
10
+ # 多视角分析结论:两项机制化改动的审阅
11
+
12
+ **主题**:审阅 `be958ec`(消费命令目录可省略)与 `d20fc46`(报告机制化)的实现质量与遗漏
13
+ **分析目录**:`mv-mv-main-20260911-225616`
14
+ **参与者**:性能 / 简单 / 铁律(+ human 插话 1 条)
15
+ **收敛**:全员 pass,本文件为收敛结论的唯一权威版本
16
+ **方法**:只审代码与 git 历史,不修改、不运行测试;产出意见清单并对清单达成一致
17
+
18
+ ---
19
+
20
+ ## 一、结论摘要
21
+
22
+ **总体判断**:两项改动的主体符合设计意图,方向正确;但 **目录可省略在 wrapper 层
23
+ 引入了一个真回归(高危)**,且其"降级兜底"分支是四类症状的共同根因——**三方推荐
24
+ 整个删除**。报告机制化通过(唯一要求:约束成文)。
25
+
26
+ **问题清单 8 条**:高 2 / 中 1 / 低 3 / 观察 2。
27
+
28
+ | # | 严重度 | 问题 | 处置 |
29
+ |---|---|---|---|
30
+ | 1 | **高** | `--view` 显式目录被静默丢弃(回归) | **修**(参数规则统一 + 补测试) |
31
+ | 2 | **高** | 降级发现可作用于破坏性操作(`--cleanup` 删猜测目录) | **删降级兜底**(推荐,2:0:1) |
32
+ | 3 | 中 | 降级信号经 stderr 中文文案解析(`err.includes("警告")`) | 随 #2 一并消失 |
33
+ | 4 | 中 | 降级"最新"按全名排序(跨 sid 取旧目录,与 docstring 不符) | 随 #2 一并消失 |
34
+ | 5 | 中 | 报告 = O(session) 冷路径 × 3 出口,护栏仅存于隐含前提 | **约束成文**(禁入轮询) |
35
+ | 6 | 低 | `result_path` 越界在 viewer(拼路径+组合层为拼串 import) | 移 `meeting_fs` |
36
+ | 7 | 低 | 未找到目录两条错误文案(python + wrapper 各一条) | 留 python 一处 |
37
+ | 8 | 低 | 流程段口径未说明(消息数含流程信号 vs 配额只计 meeting) | 口径行补半句 |
38
+ | — | 观察 | viewer ↔ observability 环(本次新增反向边) | 不 callback 化,不再加边 |
39
+
40
+ ---
41
+
42
+ ## 二、逐条结论与依据
43
+
44
+ ### F5【高】`--view` 显式目录被静默丢弃
45
+
46
+ - **位置**:`scripts/mv.sh:113-119`。shift 吃掉首参后**总是**走
47
+ `resolve_dir ""`(自动发现),显式目录被丢弃。
48
+ - **性质**:相对改动前的**行为回退**(改前 `normalize_dir "$1"` 显式生效);
49
+ 且是**静默**的——多 `mv-*` 并存或跨目录调用时会看错对象且无提示。
50
+ - **测试缺口(根因)**:`tests/test_wrapper.py:195` 仅有无参形态 `["--view"]`,
51
+ 显式目录零覆盖——测试全绿(395)没挡住的直接原因。
52
+ - **修复**(三方一致):
53
+ ```bash
54
+ # 首参非空且非 --since → 显式目录;否则自动发现
55
+ if [ -n "${1:-}" ] && [ "${1:-}" != "--since" ]; then
56
+ dir="$(resolve_dir "$1")"; shift
57
+ else
58
+ dir="$(resolve_dir "")"
59
+ fi
60
+ ```
61
+ 与 `status/report/wait/cleanup` 的 `resolve_dir "${1:-}"` 同一判据;
62
+ `--say` 保持"按参数个数"(文本可含空格/以 `--` 开头,**不能**按值判形)。
63
+ - **性能补证**(性能视角):显式目录**反而省一次 find-dir spawn**——修 F5
64
+ 是恢复更快的路径,不是"加回功能付性能代价"。
65
+ - **验收**:补显式目录形态测试;这是"全绿没挡住"的教训,不只是修一行。
66
+
67
+ ### F1/F2/F7 + 简单#2【高】降级兜底 = 一个概念,推荐整体删除
68
+
69
+ 四类症状同一根因("无 sid 匹配 → 取项目下最新 `mv-*`"的降级兜底):
70
+
71
+ | 症状 | 后果 |
72
+ |---|---|
73
+ | **F1** 降级无机器判据(退出码仍 0,仅 stderr 文案) | `mv.sh --cleanup`(无参)**可删除不是本次分析的目录**;`--say` 可写错分析 |
74
+ | **F2** extension 靠 `err.includes("警告")` 还原布尔 | 文案一改,降级判断**静默**失效(同仓库内与"判据改退出码"的最新修正相矛盾) |
75
+ | **F7** `sorted(os.listdir)[-1]` 在降级路径比较整名(含 sid) | `mv-zzz-2025…` 排在 `mv-aaa-2026…` 后 → **系统性选旧目录**(与 docstring"取最新"不符) |
76
+ | 简单#2 | 同一概念的第四面 |
77
+
78
+ - **收敛(2:0:1)**:简单 + 铁律支持**删除**;性能中性("不构成支持或反对理由")。
79
+ - **删除依据(关键事实,经三方核查)**:**不存在"必须无目录且必然无 sid"的设计内
80
+ 场景**——pi 内两条通道都有 sid(bash 注入 / extension `ctx.sessionManager`);
81
+ 终端主路径是 `--start` 输出的观看命令(本就显式带目录);代码里兜底的原始动机是
82
+ "PI 环境变量未注入时 wrapper 拿不到 sid",属 pi 流程自身降级,不是给人用的便利。
83
+ → 兜底不对任何设计流程构成负载。且**破坏性操作不应由工具猜目标**:
84
+ 最坏失败从"静默删错目录"变为"响亮报错要求显式目录"。
85
+ - **删除方案**:仅精确匹配 `mv-<sid>-*`(同 sid 时间戳可比 → F7 消失);
86
+ 未匹配 → rc 1 报错,请调用方显式传目录 → F1/F2/简单#2 同时消失。
87
+ - **执行收尾三条**(简单视角提出,三方确认):
88
+ 1. **`degraded` 概念删净**:`find_current_dir` 返回值收为一元(`path | None`);
89
+ `--find-dir` 退出码只留 0/1(**不留 rc 2 语义**);extension 的
90
+ `includes("警告")` 与 notify 警告分支一并删——不留无人消费的死代码。
91
+ 2. **错误文案留一处**:留 python 的(工具层贴近原因);wrapper `resolve_dir`
92
+ 在 spawn 失败/rc=1 时透传 python stderr 后 `exit`,不另打一条。
93
+ 3. **测试变更**:删降级用例;加"无 sid 匹配 → rc 1";**保留并新增显式目录用例**
94
+ (F5 验收)。
95
+ - **备选(若最终选择保留)**:**必须三件套齐做**——rc 契约(0 精确/2 降级/1 未找到)
96
+ + 写操作(`--cleanup`/`--say`)降级时 fail 且**列出候选目录** + F7 时间戳排序。
97
+ **半保留不可接受**(只做 rc 不修 F7 = 警告之后仍系统性读错对象)。
98
+ 删/留方案应记入本文件为**分歧项与备选**,非一致意见。
99
+
100
+ ### F-报告【中】报告 = O(session) 冷路径 × 3 出口 → 约束成文
101
+
102
+ - **实测(性能视角)**:每 agent 读整个 fork-src jsonl,本次 3 × 789KB = **2.4MB/次**;
103
+ `iter_after_boundary` 逐行扫到边界(本次边界在 777 行中第 772 行 → **99.4% 读取
104
+ 用于跳过前缀**);量化 **50–150ms/次**,×2 出口 ≈ **<0.3s/次分析**。冷路径可接受。
105
+ - **处置**:把"报告是 O(session 大小) 的冷路径、**不得进入任何轮询路径**"
106
+ 写入既有观测面契约(design.md 契约表"冷路径一次性"行后补半句,**不新开节**)。
107
+ - **不做**尾部 seek 优化(为省 <50ms 引入行/字节边界复杂性——复杂度不匹配)。
108
+ - 出口应为 3 个(`--report` / `--cleanup` / `--follow` 结束);`--view --since`
109
+ 不附报告(进 LLM context,无用且占 token)——符合职责归属。
110
+
111
+ ### F3【低】`result_path` 不在常量家
112
+
113
+ - `human_viewer.py:49-56` 定义 `f"{base}-result.md"`;S1 已把 "文件叫什么/多大算
114
+ 有效" 收进 `meeting_fs`(`RESULT_MD` / `RESULT_MD_MIN_BYTES`),但拼路径留在
115
+ viewer → 组合层(`start_discussion.py:610`)为拼字符串 import 展示模块。
116
+ - 处置:移 `meeting_fs`(与 `RESULT_MD` 同家),viewer re-export 兼容。
117
+
118
+ ### F4【观察】viewer ↔ observability 环
119
+
120
+ - `observability → human_viewer` 是既有边(`--wait` 用 incremental/is_finished);
121
+ `human_viewer → observability`(`_print_report` 延迟 import)是**本次新增的反向边**。
122
+ - 处置:**不 callback 化**(2 行延迟 import 换接口变化 + 两处调用方改动 = 净增);
123
+ 保持现状 + **不再往环上加第三条边**。`observability.py:156` 函数内重复
124
+ `import human_viewer`(顶层已有)为纯清理项,可延后。
125
+
126
+ ### F8【低】流程段口径行
127
+
128
+ - "消息 N(含流程信号)" = 消息文件数(含 freezing/pass/concluded + human),
129
+ 与配额段(仅 `mode=meeting` 且 `type=message`)并列时同一 agent 两个数字。
130
+ - 处置:末尾口径行补半句"消息数含流程信号;配额只计 meeting 发言"。
131
+
132
+ ---
133
+
134
+ ## 三、经检查确认符合设计(不构成问题)
135
+
136
+ 1. **流程段弃用 commit subject 正则、改消息文件计数**(`observability.py:241`):
137
+ 去掉自由文本耦合(格式一改就静默归零),并与配额/冻结/RR 共用同一次
138
+ `each_agent_messages` 读取——一次读取派生四段,本轮最大的简化净收益。
139
+ 2. **extension 删除 TS 版重复发现实现**,改调 python 同一入口——单一实现方向正确
140
+ (问题只在信号通道,不在归并本身)。
141
+ 3. **`_print_report` 延迟 import + fail-open 宽捕获**:两行成本消化模块环,
142
+ 保住"报告异常不阻断观看退出"契约;延迟 import 的用法正确(冷路径一次)。
143
+ 4. **`--status` 在 done 时打印 `[result]` 路径**:目录可省略后调用方无法自己拼路径,
144
+ 由机制给出的机器通道——符合单一来源收口方向。
145
+ 5. **`--say` 按参数个数区分双形态**(不按值猜语义)——与既有约定一致,保持。
146
+ 6. **`find_current_dir` 同 sid 匹配**:目录名尾缀 `YYYYMMDD-HHMMSS` 可比;
147
+ `repo.git` 判据与 engine 一致("bare 是讨论存在的唯一标志");发现逻辑下沉
148
+ python、wrapper/extension 不各写一份——符合单一事实源。
149
+ 7. **报告附 `--follow`(用户通道)而非 `--view --since`(主 pi 通道)**:职责归属正确。
150
+
151
+ ---
152
+
153
+ ## 四、分歧与备选项(如实记录)
154
+
155
+ | 议题 | 立场 | 结论 |
156
+ |---|---|---|
157
+ | 降级兜底:删除 vs 保留 | 简单:删除;铁律:**更正后支持删除**(0005);性能:中性 | **推荐删除**(净删四症状);保留 + 三件套齐做为备选 |
158
+ | 铁律 0002–0004 曾主张保留 | 理由:"PI_SESSION_ID 缺失在 pi 外是常态" | 经简单视角反问"给不出必须无 sid 的场景"后,铁律核查确认场景不存在,**0005 更正**——留档说明立场演化 |
159
+ | 删兜底后无 sid 形态 | 简单建议的"选项 2" | 不是分歧:degraded 概念消失后,无 sid = 报错要求显式目录 |
160
+
161
+ ---
162
+
163
+ ## 五、过程与统计
164
+
165
+ - **讨论消息**:23 条(性能 5 / 简单 9 / 铁律 8 / human 1)——其中含协议信号
166
+ all-freezing ×3、pass ×3,其余为实质发言
167
+ - **human 插话 1 条**:要求简要叙述(后续发言均从简)
168
+ - **消息统计口径注**:本文件的"消息 N"均指消息文件数(含流程信号);
169
+ 配额口径只计 `mode=meeting` 且 `type=message` 的发言
170
+
171
+ ## 六、建议执行顺序(供实施参考)
172
+
173
+ 1. **F5**(回归,含显式目录测试)——纯实现 bug,任何多分析场景都会静默看错对象
174
+ 2. **降级兜底删除**(F1/F2/F7 一次消除)——含执行收尾三条与文档同步
175
+ (design.md 的 `find_current_dir` 决策记录改为"仅精确 sid 匹配、无兜底")
176
+ 3. **报告约束成文**(零代码,仅契约补半句)
177
+ 4. F3 / F8 / F4 清理项(低风险,可顺手)
@@ -21,6 +21,7 @@
21
21
  |---|---|---|---|
22
22
  | `2026-09-10-e2e10-fork-source-modes-review.md` | fork 源模式描述与实现一致性(`docs/design.md` / README / AGENTS.md vs 代码) | 非法 `forkMode` 无 fail-fast(静默走"边界后全量"混合分支 → 930k tokens 超窗);wrapper 静默丢弃 `--fork-mode`;文档数字混源无锚;三处"完整上下文"与有损默认矛盾 | `732fdff`(P0/P1/P2)、`fe1955d`(方法论 11–14) |
23
23
  | `2026-09-10-e2e11-forkmode-guards-review.md` | forkMode 四层守卫与默认值单一源(含文档口径与方法论条目) | compaction 模式产物含**重复** compaction 条目;budget 窗口含 compaction 时 pi replay **静默丢弃前缀(含 preface)**;台账 `dropped` **双重计数**;`FORK_MODES` 与分派**非结构耦合** | `9412e32`(P1–P3 + I1–I5 不变量 + 台账) |
24
+ | `2026-09-11-e2e16-mechanization-review.md` | 机制化改动自审(消费命令目录可省略 + 报告机制化) | **`--view` 显式目录被静默丢弃(回归,测试只覆盖无目录形态)**;降级兜底可作用于破坏性操作(`--cleanup` 删猜测目录)+ extension 靠中文文案还原布尔 + 排序取最新在跨 sid 时系统性取旧;报告 O(session) 冷路径需约束成文 | 「机制化 1+2 修复」提交 |
24
25
  | `2026-09-11-e2e14-observability-review.md` | 本项目可观测性自审(日志系统是否合理 + 如何监控讨论有效性) | 失败/重试不可见(provider error 零记录,实测占墙钟 11%);耗时不可度量;token 无出口;**`--wait` 用 `glob(loop-*.log)` 兼任判定输入**;viewer 与 check_status 的 done 判据分叉;两份逐字相同的 `log()`;观测面契约在文档零命中 | 见「e2e14 修复」提交(第一批 §3.1–§3.5) |
25
26
  | `2026-09-11-e2e13-code-review.md` | 全项目代码合理性评审(第二轮,含时间流实测) | `--agents` 非法名在 spec-gen 路径炸出 traceback + 半成品;viewers/agents 校验与列举多实现(隐藏文件静默入场);wrapper 越界检查第三方扩展配置;`--start` 静默删 spec;session 文件两套查找规则;切换叙事拼 agent 定义正文 | `fe3aa09`(R1–R8) |
26
27
  | `2026-09-10-e2e12-code-review.md` | 全项目代码合理性评审(性能 / 简单 / 铁律) | `_lock_git` 守卫 **fail-open**(锁态下 git 上溯到主项目仓库);`pgrep -f` 自匹配;`protocol.json` 读取散落 6 处 / 3 种失败语义(含 `result_writer` 默认值求值缺陷);`check_status` 用 `git grep` 全文误报 done;engine 直做 fs I/O;两处 git 入口加固不一致 | 见「e2e12 修复」提交(批 A/B/C) |
@@ -256,3 +256,64 @@ python 时被"顺手改写"为含斜杠则不拼——**移植不是重写**:
256
256
  3. 删除函数区间用"下一个函数名"作终点前,先 `grep` 确认区间内没有别的定义;
257
257
  4. 提交前跑**真实调用路径**:`python3 -c "import <mod>"`(抓未定义/未导入名)
258
258
  + 相关测试。两次失误都是真实路径抓到的——静态阅读与"看起来对"都不够。
259
+
260
+ ### 19. 验证分层:测试挂在"代码改动"上,不挂在"发版"上(2026-09-11 发版冗余)
261
+
262
+ **背景**:发版时顺手跑全量测试(382 测试 / 5 分钟)看起来"更稳妥",实为冗余
263
+ ——发版动作本身只改 `package.json` 的版本号与文档文案,**零代码改动**;而区间
264
+ 内的代码改动在其**实现提交时**已经跑过全量测试。
265
+
266
+ **判据**:「这段区间内有没有**未被验证**的代码改动?」——有则跑,没有则不跑。
267
+ 不是「是否在发版」「是否要推送」「是否重要」。
268
+
269
+ **分层**:
270
+
271
+ | 时点 | 验证内容 |
272
+ |---|---|
273
+ | 实现提交 | **全量测试**(代码改了;这是唯一的测试时机) |
274
+ | 发版 | **打包验证**:`npm pack --dry-run` 核清单 + 真实安装后跑 CLI 冒烟 |
275
+ | 推送 | 无需测试(提交时已验)——只核 `git status` 干净 + 本地/远程一致 |
276
+
277
+ **例外**(唯一合理场景):发版区间累积了多个提交、且距上次全量测试较久
278
+ (如跨天),此时补跑一次合理——因为"区间内改动已被验证"这个前提可能不成立。
279
+
280
+ **成本视角**:冗余测试不只是耗时——它稀释了"全绿"的信号价值(每次发版都全绿
281
+ 时,人不再区分"这次真的验了代码"与"这次只 bump 了版本号")。
282
+
283
+ ### 20. LLM 测试的判据 = **时长**,不是"是否用 LLM"(2026-09-11 用户澄清)
284
+
285
+ **规则**(用户原话):
286
+
287
+ > 我要求 llm 测试必须经过我同意的原因是 llm 测试会消耗大量时间,会影响我
288
+ > 后继的操作。因此如果能确保短时间结束的测试是没有关系的,比如仅仅测试
289
+ > 一下启动是没有问题的。
290
+
291
+ **判据**:
292
+
293
+ | 情形 | 是否需要事先同意 |
294
+ |---|---|
295
+ | 完整 e2e 讨论、多轮真实唤醒、分钟级以上占用 | **需要**(且用户在场) |
296
+ | 几秒内结束的链路验证(只验证启动、单次冒烟、构造即停) | 不需要 |
297
+ | spawn 了 pi/loop 但立即终止的测试 | 不需要,但要**主动报告**(说明起过、已停、有无真实请求发出) |
298
+
299
+ **配套纪律**:`mv.sh --start` 会 spawn loop → spawn pi(真实请求)——短测试
300
+ 也要**想好怎么停**(构造完立即 cleanup + `check-residue` 复核)。
301
+
302
+ ### 21. `mv.sh --start` 会真实启动分析(spawn loop + pi)——冒烟别用它
303
+
304
+ **背景**:`cmd_start` 做两件事:创建环境 + **启动 loop**。后者会立刻 spawn
305
+ `meeting_loop.py`,而 loop 首轮就 spawn `pi --mode json --print`——**真实 LLM
306
+ 请求**。在 pi session 内跑(`PI_SESSION_ID`/`PI_SESSION_FILE` 都在)时没有任何
307
+ 保护会阻止它。
308
+
309
+ **实测代价**(2026-09-11 两次,同一坑):
310
+ - ① 验证"目录名构造"时跑 `mv.sh --start` → 真起了 2 个 loop + 2 个 pi 进程;
311
+ - ② 验证"目录可省略"时同样跑 `--start` → 同样起来(20 秒后 cleanup 删目录,
312
+ loop 因 repo.git 消失自退出,但 pi 进程已 spawn → 请求可能已发出)。
313
+
314
+ **方法**:
315
+ 1. **只创建不启动** → 用 python 侧 `start_discussion.py --dir <d> --spec <s>`
316
+ (不带 `--start`);wrapper 无此形态(`--start` 永远启动)。
317
+ 2. 需要走 wrapper 链路时,验证完**立即**:停 loop(`pkill` 按目录精确匹配)
318
+ → `--cleanup` → `check-residue.sh` 复核。
319
+ 3. 判据:"这次验证会不会 spawn pi/loop?"——会,就必须先想好怎么停。
@@ -6,14 +6,13 @@
6
6
  * 不经过 LLM——命令 handler 直接 spawn human_sayer.py(一次调用一次返回),
7
7
  * 结果用 ctx.ui.notify 反馈。
8
8
  *
9
- * 讨论目录发现(零状态文件):
10
- * wrapper --start 的目录名 = mv-<PI_SESSION_ID>-<时间戳>
11
- * (aft 不再替换 bash 后 PI_SESSION_ID 注入可用);
12
- * handler 用 ctx.sessionManager.getSessionId() 取本 session id,
13
- * glob ctx.cwd/mv-<sid>-* 取最新目录——session 隔离(同目录多
14
- * session 并发分析也互不干扰),无状态文件、无 cleanup 比对。
15
- * 兜底:无 sid 目录(PI 环境变量未注入时 wrapper 拿不到 sid)→ 项目下
16
- * 最新的 mv-*,并警告降级(宁可提示也不要静默插错分析)。
9
+ * 讨论目录发现(零状态文件):目录名 = mv-<sid>-<时间戳>;
10
+ * handler ctx.sessionManager.getSessionId() + ctx.cwd,**调用
11
+ * observability.py --find-dir**(python 单一实现——wrapper 的消费命令
12
+ * 走同一入口;本扩展不再自持一份发现逻辑,两边口径不会漂移)。
13
+ * **只精确匹配同 sid 的目录,无降级兜底**(e2e16 F1/F2:降级时插话可能
14
+ * 写错分析,且判据曾是"stderr 是否含中文'警告'"——文案一改静默失效);
15
+ * 未找到 报错提示。
17
16
  *
18
17
  * 前缀 mv- 与 pi-agents-helper 的 discuss-* 命名空间隔离(两个系统的
19
18
  * 插话命令都按"同 sid 最新目录"发现目标,共用前缀会互相插错)。
@@ -55,43 +54,36 @@ const PACKAGE_ROOT = findPackageRoot(__dirname, PKG_NAME);
55
54
  const SAYER = PACKAGE_ROOT
56
55
  ? path.join(PACKAGE_ROOT, "human_sayer.py")
57
56
  : "/root/pi-multi-viewers/human_sayer.py"; // 复制安装退化(开发机)
57
+ const OBSERVABILITY = PACKAGE_ROOT
58
+ ? path.join(PACKAGE_ROOT, "observability.py")
59
+ : "/root/pi-multi-viewers/observability.py"; // 复制安装退化(开发机)
58
60
 
59
- /** (cwd, sessionId) 推导当前分析目录:优先 mv-<sid>-<stamp>
60
- * (session 隔离),找不到回退 mv-<stamp>(无 sid 目录——PI 环境
61
- * 变量缺失时 wrapper 拿不到 sid,降级为项目下最新分析,警告提示)。 */
62
- function findCurrentDir(
63
- cwd: string,
64
- sid: string,
65
- ): { dir: string | null; degraded: boolean } {
66
- try {
67
- const names = fs.readdirSync(cwd, { withFileTypes: true });
68
- const bySid = names
69
- .filter((e) => e.isDirectory() && e.name.startsWith(`mv-${sid}-`))
70
- .map((e) => e.name)
71
- .sort();
72
- if (bySid.length > 0) {
73
- const dir = path.join(cwd, bySid[bySid.length - 1]);
74
- return { dir: fs.existsSync(dir) ? dir : null, degraded: false };
75
- }
76
- const any = names
77
- .filter(
78
- (e) =>
79
- e.isDirectory() &&
80
- e.name.startsWith("mv-") &&
81
- // 排除 mv-spec-*:那是尚未被 --start 消费的 spec 目录,不是分析
82
- // 目录(否则会选中它并报出误导性的"分析不存在")
83
- !e.name.startsWith("mv-spec-"),
84
- )
85
- .map((e) => e.name)
86
- .sort();
87
- if (any.length > 0) {
88
- const dir = path.join(cwd, any[any.length - 1]);
89
- return { dir: fs.existsSync(dir) ? dir : null, degraded: true };
61
+ /** 定位当前分析目录:调用 observability.py --find-dir(**python 单一实现**
62
+ * ——wrapper 消费命令同一入口)。
63
+ *
64
+ * 判据 = **退出码**(不是 stderr 文案——中文提示一改就静默失配,
65
+ * e2e16 F2):rc 0 → stdout 是绝对路径;rc 1 → 未找到(无同 sid 分析)。
66
+ * 无降级通道(不会回退到"项目下最新"——那会插错分析,e2e16 F1)。 */
67
+ function findCurrentDir(cwd: string, sid: string): Promise<string | null> {
68
+ return new Promise((resolve) => {
69
+ if (!OBSERVABILITY) {
70
+ resolve(null);
71
+ return;
90
72
  }
91
- return { dir: null, degraded: false };
92
- } catch {
93
- return { dir: null, degraded: false };
94
- }
73
+ const proc = spawn("python3", [OBSERVABILITY, "--find-dir"], {
74
+ cwd,
75
+ env: { ...process.env, PI_SESSION_ID: sid },
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ });
78
+ let out = "";
79
+ proc.stdout.on("data", (d) => (out += d.toString()));
80
+ proc.stderr.on("data", () => {}); // 原因只在 rc=1 时通知用户(见 handler)
81
+ proc.on("close", (code) => {
82
+ const dir = out.trim();
83
+ resolve(code === 0 && dir ? dir : null);
84
+ });
85
+ proc.on("error", () => resolve(null));
86
+ });
95
87
  }
96
88
 
97
89
  /** 执行 human_sayer.py 一次插话。返回 { ok, output }。 */
@@ -126,23 +118,16 @@ export default function register(pi: any) {
126
118
  return;
127
119
  }
128
120
  const sid = ctx.sessionManager.getSessionId();
129
- const found = findCurrentDir(ctx.cwd, sid);
130
- const dir = found.dir;
121
+ const dir = await findCurrentDir(ctx.cwd, sid);
131
122
  if (!dir) {
132
123
  ctx.ui.notify(
133
- "没有正在进行的多视角分析(cwd 下无 mv-* 目录)。" +
134
- "先用 /multi-viewers 启动分析。",
124
+ "本 session 没有正在进行的多视角分析(cwd 下无 " +
125
+ `mv-${sid}-* 分析环境)。先用 /multi-viewers 启动,` +
126
+ "或改用 mv.sh --say <目录> \"<文本>\" 显式指定。",
135
127
  "error",
136
128
  );
137
129
  return;
138
130
  }
139
- if (found.degraded) {
140
- ctx.ui.notify(
141
- `未找到本 session 的分析目录(目录名不含 session id——` +
142
- `PI 环境变量可能未注入)——插话指向项目下最新分析: ${dir}`,
143
- "warning",
144
- );
145
- }
146
131
  const { ok, output } = await runSayer(dir, text);
147
132
  if (ok && output) {
148
133
  ctx.ui.notify(output, "success");
package/human_viewer.py CHANGED
@@ -46,14 +46,9 @@ def participants_from_bare(bare):
46
46
  return meeting_fs.read_protocol(bare).get("participants") or None
47
47
 
48
48
 
49
- def result_path(base):
50
- """result.md 的固定位(`<分析目录>-result.md`,与 --wait / prompt 一致)。
51
-
52
- resultWriter 的 loop 退出(concluded)时保存到该位置,cleanup 兜底再存
53
- 一次;权威单一事实源是 bare 的 `HEAD:result.md`。调用方**无需**推
54
- resultWriter 是谁、也不必进 work 子目录——分析目录删除后该文件仍在。
55
- """
56
- return f"{base}-result.md"
49
+ # result_path 的实现已迁 meeting_fs(与 RESULT_MD 同家,e2e16 F3);
50
+ # 本模块保留同名引用,既有的 `human_viewer.result_path` 调用方零改动。
51
+ result_path = meeting_fs.result_path
57
52
 
58
53
 
59
54
  def new_messages(bare, since):
@@ -231,10 +226,37 @@ def follow(base, bare, agents, max_meeting=None,
231
226
  if done:
232
227
  print(f"【分析已结束】result.md: {result_path(base)}",
233
228
  flush=True)
229
+ _print_report(base)
234
230
  return
235
231
  time.sleep(poll_interval)
236
232
 
237
233
 
234
+ def _print_report(base):
235
+ """分析结束时打印观测报告(**用户通道自带**,不依赖任何 LLM 动作)。
236
+
237
+ 为什么在这里:`--follow` 是用户直接执行的通道(`!!` 命令),结束时
238
+ 自动附报告 = 用户零操作看到运行事实(提交/墙钟/配额/进程/LLM 用量),
239
+ 而不是指望主 pi 记得去跑 `--report` 再转述(LLM 依赖,可能漏)。
240
+ `--report` 独立入口与 `--cleanup` 的打印保持不变(不同场景各看一次)。
241
+
242
+ **延迟 import observability**:该模块顶层 import 本模块
243
+ (wait_for_completion 用 incremental),顶层反向 import 会成环。本函数
244
+ 只在 done 分支执行一次(冷路径),函数内 import 是标准解法。
245
+
246
+ fail-open:报告是附加信息,生成失败绝不阻断观看退出(契约同
247
+ observability.build_report——任何一段读不出显示 n/a)。
248
+ """
249
+ try:
250
+ from observability import build_report
251
+ print("【分析报告】", flush=True)
252
+ for line in build_report(base):
253
+ print(line, flush=True)
254
+ except Exception as e: # noqa: BLE001
255
+ # 宽捕获是刻意的:报告在观看主循环的退出路径上,任何异常
256
+ # (含未预期)都不该让用户失去"分析已结束"这个关键信息
257
+ print(f"【分析报告】生成失败(不影响观看):{e}", flush=True)
258
+
259
+
238
260
  def main():
239
261
  parser = argparse.ArgumentParser(description="human 分析展示(只读)")
240
262
  parser.add_argument("base", help="分析目录(含 repo.git)")
package/meeting_fs.py CHANGED
@@ -31,6 +31,21 @@ RESULT_MD = "result.md"
31
31
  # 写空文件/仅 frontmatter——只查存在性会退化为空提交,审核 A2)。
32
32
  RESULT_MD_MIN_BYTES = 50
33
33
 
34
+
35
+ def result_path(base):
36
+ """result.md 的固定位(`<分析目录>-result.md`)。
37
+
38
+ **与 RESULT_MD 同家**(e2e16 F3):S1 把"文件叫什么/多大算有效"收进本
39
+ 模块,但拼路径留在 viewer —— 组合层(start_discussion 的 `--status`
40
+ 打印 `[result]`)为拼字符串 import 展示模块,职责颠倒。路径规则是
41
+ 产物契约的一部分,归 fs。
42
+
43
+ resultWriter 的 loop 退出(concluded)时保存到该位置,cleanup 兜底再存
44
+ 一次;权威单一事实源是 bare 的 `HEAD:result.md`。调用方**无需**推
45
+ resultWriter 是谁、也不必进 work 子目录——分析目录删除后该文件仍在。
46
+ """
47
+ return f"{base}-{RESULT_MD}"
48
+
34
49
  # 无进展超时兜底(秒)——协议参数的默认值(gen_protocol 固化进
35
50
  # protocol.json;engine/fake_agent 的签名默认与 CLI default 同源于此)。
36
51
  DEFAULT_STALL_TIMEOUT = 600
package/observability.py CHANGED
@@ -55,6 +55,50 @@ def _loops_alive(base):
55
55
  return bool(_loop_pids(base))
56
56
 
57
57
 
58
+ def find_current_dir(cwd=None, sid=None):
59
+ """发现"本 session 当前的分析目录"——**单一实现**(wrapper 与 extension
60
+ 共用;此前只有 extension 里一份 TS 实现,wrapper 侧完全没有)。
61
+
62
+ 为什么需要:消费命令(--status/--cleanup/--view/--say…)此前都要求调用方
63
+ 传绝对路径,而唯一知道路径的是 `--start` 的输出——主 pi 得把它记在
64
+ LLM 上下文里再原样复用(改错/截断/相对路径都出过)。发现逻辑下沉后,
65
+ 命令可以不带目录,**路径不需要经过任何 LLM 记忆**。
66
+
67
+ **只做精确匹配**(`mv-<sid>-*` 中时间戳最新者):
68
+
69
+ 为什么**没有**"取项目下最新 `mv-*`"的兜底(e2e16 评审 2:0:1 裁定删除):
70
+ 1. **破坏性操作不应由工具猜目标**——`--cleanup`/`--say` 作用于发现结果,
71
+ 兜底意味着"猜一个目录然后删它/写它";最坏失败应是响亮报错而非静默
72
+ 操作错对象;
73
+ 2. 兜底需要机器可判的"这是降级"信号,而 rc 0 + stderr 中文文案不是可靠
74
+ 信号(extension 曾用 `err.includes("警告")` 还原——文案一改就静默失效);
75
+ 3. 兜底的"最新"按整名排序(含 sid 段)→ 跨 sid 时**系统性取旧**(与
76
+ "取最新"的语义相反);
77
+ 4. **不存在"必须无目录且必然无 sid"的设计内场景**:pi 内两条通道都有 sid
78
+ (bash 注入 `PI_SESSION_ID` / extension `ctx.sessionManager`),终端主
79
+ 路径本就用 `--start` 输出的显式目录。无 sid 时应当报错请调用方显式传目录。
80
+
81
+ 判别 = 目录含 `repo.git`(同 engine:"bare 是讨论存在的唯一标志"——光看
82
+ 名字会命中残留/无关目录);`mv-spec-*` 不在匹配前缀内(那是尚未被
83
+ `--start` 消费的 spec 目录)。
84
+
85
+ 返回绝对路径 | None。
86
+ """
87
+ cwd = cwd or os.getcwd()
88
+ sid = sid if sid is not None else os.environ.get("PI_SESSION_ID", "")
89
+ if not sid:
90
+ return None
91
+ try:
92
+ names = sorted(os.listdir(cwd))
93
+ except OSError:
94
+ return None
95
+ # 同 sid 的目录名尾缀 `YYYYMMDD-HHMMSS` 定长可比 → 排序即时间序
96
+ cands = [n for n in names
97
+ if n.startswith(f"mv-{sid}-")
98
+ and os.path.isdir(os.path.join(cwd, n, "repo.git"))]
99
+ return os.path.join(cwd, cands[-1]) if cands else None
100
+
101
+
58
102
  def check_status(base):
59
103
  """讨论状态(单值;状态全集显式于此,T3/#7 修复 e2e7 评审):
60
104
 
@@ -110,7 +154,7 @@ def wait_for_completion(base):
110
154
  # (原内联 65 行自行 git log 全量 + 手工解析 frontmatter——与
111
155
  # viewer 两套输出格式、非增量、概念丢失)。incremental 走
112
156
  # since..HEAD 增量 + 统一 format_message。
113
- import human_viewer
157
+ # (函数内重复 import human_viewer 已删——顶层已有;e2e16 F4 清理项)
114
158
  sys.stdout.reconfigure(line_buffering=True)
115
159
  print(f"[wait] 等待讨论完成: {base}")
116
160
  bare = meeting_fs.bare_of_base(base)
@@ -194,7 +238,20 @@ def build_report(base):
194
238
  return out
195
239
  proto = meeting_fs.read_protocol(bare)
196
240
 
197
- # ---- 流程时间线(bare = 判定域,现场派生) ----
241
+ # ---- 一次读取(消息文件 = 权威口径),供流程/配额/冻结/RR 四段共用 ----
242
+ msgs = meeting_engine.each_agent_messages(bare, agents)
243
+ per_agent = {a: len(msgs.get(a, [])) for a in agents}
244
+ # human 消息不在 participants 里(视而不见原则)——单独数 human/ 目录的
245
+ # 消息文件。**不用 commit subject 统计**:那是自由文本(`discuss: X/NNNN`),
246
+ # 格式一改/手写就静默归零(2026-09-11 实测:构造环境 subject 不同 → "提交 0"
247
+ # 而实际有 3 条消息);消息文件是判定域的事实,格式由本仓控制。
248
+ r_h = meeting_fs.run_git(bare, "ls-tree", "-r", "-z", "--name-only",
249
+ "HEAD", check=False)
250
+ human_n = sum(1 for f in r_h.stdout.rstrip("\0").split("\0")
251
+ if f and f.startswith("human/")
252
+ and meeting_fs.is_message_file(f))
253
+
254
+ # ---- 流程时间线(时间戳来自 commit——消息文件不带墙钟) ----
198
255
  r = meeting_fs.run_git(bare, "log", "--reverse", "--format=%ct%x09%s",
199
256
  "HEAD", check=False)
200
257
  rows = []
@@ -203,23 +260,15 @@ def build_report(base):
203
260
  continue
204
261
  ts, subj = line.split("\t", 1)
205
262
  rows.append((int(ts), subj))
206
- per_agent = {a: 0 for a in agents}
207
- human_n = 0
208
- for _, subj in rows:
209
- m = re.match(r"discuss:\s*(.+?)/(\d+)$", subj)
210
- if not m:
211
- continue
212
- who = m.group(1)
213
- if who == "human" or who not in per_agent:
214
- human_n += 1
215
- else:
216
- per_agent[who] += 1
217
263
  if rows:
218
264
  span = rows[-1][0] - rows[0][0]
219
- out.append(f"流程:{len(agents)} agents | 提交 "
220
- f"{sum(per_agent.values())}(含流程信号;"
221
- + " / ".join(f"{a} {n}" for a, n in per_agent.items())
222
- + f")| 墙钟跨度 {_dur(span)}(首末 commit 差)")
265
+ detail = " / ".join(f"{a} {n}" for a, n in per_agent.items())
266
+ if human_n: # human 单列明细(它不是参与者),但计入合计
267
+ detail += f" / human {human_n}"
268
+ out.append(f"流程:{len(agents)} agents | 消息 "
269
+ f"{sum(per_agent.values()) + human_n}"
270
+ f"(含流程信号;{detail})| 墙钟跨度 {_dur(span)}"
271
+ f"(首末 commit 差)")
223
272
  # 最长无进展间隔(相邻 commit 间隔的最大值)
224
273
  gaps = [(rows[i + 1][0] - rows[i][0], rows[i][0], rows[i + 1][0])
225
274
  for i in range(len(rows) - 1)]
@@ -233,7 +282,7 @@ def build_report(base):
233
282
  # 不是"该 agent 的消息总数"——上限约束的是 meeting 发言轮次,而一个
234
283
  # agent 的消息里还有 freezing/all-freezing/pass/concluded 等流程信号。
235
284
  # 两者混算会出现"meeting 6/2"这种超限假象(口径错误,2026-09-11 实测)。
236
- msgs = meeting_engine.each_agent_messages(bare, agents)
285
+ # msgs 由上方流程段一次读取提供(同一读取派生四段)。
237
286
  lasts = {a: (msgs[a][-1] if msgs[a] else None) for a in agents}
238
287
  types = {a: (lasts[a].get("type") if lasts[a] else None) for a in agents}
239
288
  quota_meeting = proto.get("maxMeetingRounds", 10)
@@ -290,9 +339,11 @@ def build_report(base):
290
339
  f"{u['responses']} 次 | error {u['errors']} 次")
291
340
  if not any_usage:
292
341
  out.append(" n/a(session 缺失,或无本轮数据——边界条目自 2026-09-11 "
293
- "起写入,此前的老分析不适用)」")
342
+ "起写入,此前的老分析不适用)")
294
343
  out.append("(口径:进程跨度=pi 进程生命周期;输出=prompt 分段合计;"
295
- "墙钟=commit 时间差——三者不可互替)")
344
+ "墙钟=commit 时间差——三者不可互替;"
345
+ "消息数含流程信号(freezing/pass/concluded)与 human,"
346
+ "配额只计 meeting 发言)")
296
347
  return out
297
348
 
298
349
 
@@ -390,3 +441,36 @@ def _num(n):
390
441
  if n >= 1_000:
391
442
  return f"{n / 1_000:.1f}k"
392
443
  return str(n)
444
+
445
+
446
+ def _main(argv=None):
447
+ """观测层 CLI——目前只有一个子命令:`--find-dir`(供 wrapper 与
448
+ extension 在"不带目录"时定位当前分析;逻辑单点,两个调用方不各写一份)。
449
+
450
+ 输出契约(二元,无中间态——降级语义已删除,e2e16 F1/F2/F7):
451
+ stdout = 绝对路径(找到时);stderr = 原因(未找到);
452
+ 退出码 0 = 找到,1 = 未找到(调用方应据此报错请用户显式传目录)。
453
+ """
454
+ import argparse
455
+ ap = argparse.ArgumentParser(description="多视角分析:观测层工具")
456
+ ap.add_argument("--find-dir", action="store_true",
457
+ help="定位当前分析目录(按 cwd + PI_SESSION_ID)")
458
+ ap.add_argument("--cwd", default=None, help="项目目录(默认 $PWD)")
459
+ ap.add_argument("--sid", default=None,
460
+ help="session id(默认 $PI_SESSION_ID)")
461
+ args = ap.parse_args(argv)
462
+ if not args.find_dir:
463
+ ap.print_help()
464
+ return 2
465
+ d = find_current_dir(args.cwd, args.sid)
466
+ if not d:
467
+ print("错误: 未找到本 session 的分析目录(cwd 下无 "
468
+ "mv-<PI_SESSION_ID>-* 环境)——请显式传目录参数",
469
+ file=sys.stderr)
470
+ return 1
471
+ print(d)
472
+ return 0
473
+
474
+
475
+ if __name__ == "__main__":
476
+ sys.exit(_main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.2.0",
3
+ "version": "0.2.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,
@@ -26,9 +26,10 @@ argument-hint: '"<主题>"'
26
26
  ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --prepare "<主题>"
27
27
  ```
28
28
 
29
- **若报错含"未找到 viewers/ 目录"、"仅发现 1 个视角"或"视角任务书不能为空"**:
30
- 说明项目缺可用视角。此时**停下来问用户**,不要自己选——建议帮他按
31
- 下面的"视角设计原则"写 `viewers/<视角名>.md`(建好后再重跑本步骤)。
29
+ **这条命令非零退出 说明 spec 生成失败**(通常是项目缺可用视角:未建
30
+ `viewers/`、视角少于 2 个、或视角文件为空)。此时**停下来问用户**,不要
31
+ 自己选——读报错原文搞清原因,建议他按下面的"视角设计原则"
32
+ `viewers/<视角名>.md`(建好后再重跑本步骤)。
32
33
 
33
34
  ### 2. 编辑并请用户审核 spec
34
35
 
@@ -68,13 +69,16 @@ argument-hint: '"<主题>"'
68
69
  ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --start <spec目录绝对路径>
69
70
  ```
70
71
 
71
- 记录 wrapper 输出的**分析目录**,并**原样完整展示**观看命令。
72
+ **原样完整展示** wrapper 输出的观看命令(它是可复制执行的完整命令行——
73
+ 不要改写、截断或转述)。
72
74
 
73
75
  ### 4. 结束回合
74
76
 
75
77
  告知用户:
76
- - 观看:复制上一步的 `!!` 命令执行(实时流式,结束时自动退出)
77
- - 插话:随时 `/multi-viewers-say <文本>`(自动定位当前分析;也可用 wrapper `--say <目录> "<文本>"`)
78
+ - 观看:复制上一步的 `!!` 命令执行(实时流式,结束时自动退出并**附本次
79
+ 分析报告**——消息数/墙钟/配额/冻结/进程跨度/LLM 用量;用户无需任何
80
+ 额外操作即可看到)
81
+ - 插话:随时 `/multi-viewers-say <文本>`(自动定位当前分析)
78
82
  - 完成时告诉主 pi,主 pi 会收尾
79
83
 
80
84
  **然后结束当前回合。**
@@ -82,13 +86,12 @@ argument-hint: '"<主题>"'
82
86
  ## 收尾(用户驱动)
83
87
 
84
88
  ```bash
85
- mv.sh --status <分析目录绝对路径> # done/stopped/running
89
+ mv.sh --status # done/stopped/running(目录可省略——自动定位当前分析)
86
90
  ```
87
91
 
88
- - `done`:读 `<分析目录>-result.md`(与目录同级的固定位)→ 向用户给
89
- **摘要** → `mv.sh --cleanup <分析目录绝对路径>`
90
- - cleanup 会打印**本次分析报告**(提交数 / 墙钟 / 进程跨度 / 配额 / LLM
91
- 用量 / rc≠0)——这是删目录前最后一次可读,**把其中的关键数字一并
92
- 转述给用户**(运行成本与健康度的一手信息)
92
+ - `done`:读上一步打印的 `[result]` 路径(产物固定位;**路径由命令给出,
93
+ 不要自己拼**)→ 向用户给**摘要** → `mv.sh --cleanup`(同样可省略目录)
94
+ - cleanup 会再打印一次**分析报告**(删目录前最后一次可读)。报告在
95
+ 观看输出末尾已自动出现过,**不必重复转述**——用户问起数字时按需引用
93
96
  - `stopped`:报告"分析已结束但未生成结果",不要继续等待
94
97
  - `running`:告知还在进行,继续等用户通知
package/scripts/mv.sh CHANGED
@@ -17,6 +17,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
17
  ROOT_DIR="$(dirname "$SCRIPT_DIR")"
18
18
  PYTHON="${PYTHON:-python3}"
19
19
  START_DISCUSSION="$ROOT_DIR/start_discussion.py"
20
+ OBSERVABILITY="$ROOT_DIR/observability.py"
20
21
  HUMAN_VIEWER="$ROOT_DIR/human_viewer.py"
21
22
  HUMAN_SAYER="$ROOT_DIR/human_sayer.py"
22
23
 
@@ -26,12 +27,15 @@ usage() {
26
27
  用法:
27
28
  $0 --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
28
29
  $0 --start <spec目录> [--fork-mode compaction|budget|full]
29
- $0 --status <dir>
30
- $0 --report <dir>
31
- $0 --wait <dir>
32
- $0 --cleanup <dir>
33
- $0 --view <dir> [--since <ref>]
34
- $0 --say <dir> "<文本>"
30
+ $0 --status [dir]
31
+ $0 --report [dir]
32
+ $0 --wait [dir]
33
+ $0 --cleanup [dir]
34
+ $0 --view [dir] [--since <ref>]
35
+ $0 --say [dir] "<文本>"
36
+
37
+ 消费命令的 <dir> 可省略(自动发现本 session 当前分析——按 cwd 下
38
+ mv-<PI_SESSION_ID>-* 最新;找不到则取最新 mv-* 并警告)
35
39
 
36
40
  默认参数:
37
41
  agents=a,b,c max-meeting=10 max-rr=7 # 配额默认值的权威在 python argparse(wrapper 不传)
@@ -58,6 +62,24 @@ require_dir() {
58
62
  [ -d "$dir" ] || fail "目录不存在: $dir"
59
63
  }
60
64
 
65
+ # 目录解析:显式参数优先;**省略则自动发现**(python observability
66
+ # --find-dir——单一实现,extension 同一入口)。为什么允许省略:唯一知道
67
+ # 路径的是 --start 的输出,此前消费命令都要求把它传给每个命令,等于让
68
+ # 调用方(主 pi)把长绝对路径记在 LLM 上下文里再复用——改错/截断/相对
69
+ # 路径都出过(参数形态标准化就是为此)。发现逻辑下沉后路径不经 LLM 记忆。
70
+ # **调用方必须 `|| exit $?`**:本函数的 exit 发生在命令替换的子 shell 里,
71
+ # 不检查返回值会让空结果继续往下走(表现为双重错误消息:python 的原因 +
72
+ # require_dir 的"缺少目录参数"——实测踩到)。
73
+ resolve_dir() {
74
+ local dir="${1:-}"
75
+ if [ -z "$dir" ]; then
76
+ # 省略 = 自动发现;**失败原因由 python 给出**(错误文案留一处——
77
+ # e2e16 F7:两条文案没有信息增益,只用工具层那条更贴近原因)。
78
+ dir="$("$PYTHON" "$OBSERVABILITY" --find-dir)" || exit 1
79
+ fi
80
+ normalize_dir "$dir"
81
+ }
82
+
61
83
  # 目录参数规范化:裸名(无路径符)会被 start_discussion 加 discussion-
62
84
  # 前缀导致找错目录(实测 2026-09-03)——所有消费命令入口统一转绝对路径
63
85
  normalize_dir() {
@@ -66,36 +88,45 @@ normalize_dir() {
66
88
 
67
89
  cmd_status() {
68
90
  local dir
69
- dir="$(normalize_dir "$1")"
91
+ dir="$(resolve_dir "${1:-}")" || exit $?
70
92
  require_dir "$dir"
71
93
  "$PYTHON" "$START_DISCUSSION" --dir "$dir" --status
72
94
  }
73
95
 
74
96
  cmd_report() {
75
97
  local dir
76
- dir="$(normalize_dir "$1")"
98
+ dir="$(resolve_dir "${1:-}")" || exit $?
77
99
  require_dir "$dir"
78
100
  "$PYTHON" "$START_DISCUSSION" --dir "$dir" --report
79
101
  }
80
102
 
81
103
  cmd_wait() {
82
104
  local dir
83
- dir="$(normalize_dir "$1")"
105
+ dir="$(resolve_dir "${1:-}")" || exit $?
84
106
  require_dir "$dir"
85
107
  "$PYTHON" "$START_DISCUSSION" --dir "$dir" --wait
86
108
  }
87
109
 
88
110
  cmd_cleanup() {
89
111
  local dir
90
- dir="$(normalize_dir "$1")"
112
+ dir="$(resolve_dir "${1:-}")" || exit $?
91
113
  require_dir "$dir"
92
114
  "$PYTHON" "$START_DISCUSSION" --dir "$dir" --cleanup
93
115
  }
94
116
 
95
117
  cmd_view() {
96
118
  local dir since=""
97
- dir="$(normalize_dir "$1")"
98
- shift
119
+ # 首参:非空且非 --since → 显式目录(**F5 回归修复**:此前无条件 shift
120
+ # 后总走自动发现,显式目录被静默丢弃——多分析并存/跨目录调用会看错对象)。
121
+ # 判据与 status/report/wait/cleanup 的 `resolve_dir "${1:-}"` 一致。
122
+ # 注意与 --say 的区别:--say 按**参数个数**区分(文本可含空格/以 -- 开头,
123
+ # 不能按值判形);--view 的 --since 是本命令自己的选项名,可以判形。
124
+ if [ -n "${1:-}" ] && [ "${1:-}" != "--since" ]; then
125
+ dir="$(resolve_dir "$1")" || exit $?
126
+ shift
127
+ else
128
+ dir="$(resolve_dir "")" || exit $?
129
+ fi
99
130
  while [ "$#" -gt 0 ]; do
100
131
  case "$1" in
101
132
  --since)
@@ -121,8 +152,17 @@ cmd_view() {
121
152
  }
122
153
 
123
154
  cmd_say() {
124
- local dir text="${2:-}"
125
- dir="$(normalize_dir "$1")"
155
+ # 两种形态(**按参数个数区分,不是按值猜语义**):
156
+ # --say <dir> "<文本>" 显式目录(兼容原有调用)
157
+ # --say "<文本>" 目录自动发现(本 session 当前分析)
158
+ local dir text
159
+ if [ "$#" -ge 2 ]; then
160
+ dir="$(resolve_dir "$1")" || exit $?
161
+ text="$2"
162
+ else
163
+ dir="$(resolve_dir "")" || exit $?
164
+ text="${1:-}"
165
+ fi
126
166
  require_dir "$dir"
127
167
  [ -d "$dir/work-human" ] || fail "分析缺少 work-human: $dir"
128
168
  [ -n "$text" ] || fail "插话文本不能为空"
@@ -284,34 +324,33 @@ if [ "$#" -ge 1 ]; then
284
324
  exit $?
285
325
  ;;
286
326
  --status)
287
- [ "$#" -ge 2 ] || fail "--status 需要分析目录参数"
288
- cmd_status "$2"
327
+ shift
328
+ cmd_status "${1:-}"
289
329
  exit $?
290
330
  ;;
291
331
  --report)
292
- [ "$#" -ge 2 ] || fail "--report 需要分析目录参数"
293
- cmd_report "$2"
332
+ shift
333
+ cmd_report "${1:-}"
294
334
  exit $?
295
335
  ;;
296
336
  --wait)
297
- [ "$#" -ge 2 ] || fail "--wait 需要分析目录参数"
298
- cmd_wait "$2"
337
+ shift
338
+ cmd_wait "${1:-}"
299
339
  exit $?
300
340
  ;;
301
341
  --cleanup)
302
- [ "$#" -ge 2 ] || fail "--cleanup 需要分析目录参数"
303
- cmd_cleanup "$2"
342
+ shift
343
+ cmd_cleanup "${1:-}"
304
344
  exit $?
305
345
  ;;
306
346
  --view)
307
- [ "$#" -ge 2 ] || fail "--view 需要分析目录参数"
308
347
  shift
309
348
  cmd_view "$@"
310
349
  exit $?
311
350
  ;;
312
351
  --say)
313
- [ "$#" -ge 3 ] || fail "--say 需要分析目录和文本参数"
314
- cmd_say "$2" "$3"
352
+ shift
353
+ cmd_say "$@"
315
354
  exit $?
316
355
  ;;
317
356
  -h|--help)
@@ -33,6 +33,7 @@ import subprocess
33
33
  import sys
34
34
  import time
35
35
 
36
+ import human_viewer
36
37
  import meeting_fs
37
38
  import observability
38
39
  import spec_gen
@@ -601,7 +602,12 @@ def main():
601
602
  cleanup_discussion(base)
602
603
  return
603
604
  if args.status:
604
- print(f"[status] {check_status(base)}")
605
+ st = check_status(base)
606
+ print(f"[status] {st}")
607
+ if st == "done":
608
+ # 产物路径一并给出:目录可省略后(自动发现),调用方**无法**
609
+ # 自己拼出 `<目录>-result.md`——路径由机制提供,不经 LLM 记忆
610
+ print(f"[result] {meeting_fs.result_path(base)}")
605
611
  return
606
612
  if args.report:
607
613
  for line in build_report(base):