pi-multi-viewers 0.2.0 → 0.2.1
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 +1 -1
- package/docs/design.md +19 -2
- package/docs/test-methodology.md +23 -0
- package/human_viewer.py +27 -0
- package/observability.py +23 -18
- package/package.json +1 -1
- package/prompts/multi-viewers.md +5 -4
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.
|
|
179
|
+
+ wrapper。**npm 已发布 0.2.1(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/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`(视图) |
|
|
103
|
+
| `--report`(视图) | observability | 文本行 | 人(**三个出口**,见下) | **否**(不得升级为验收 gate) | 冷路径一次性 |
|
|
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,13 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
|
|
|
265
276
|
`start_discussion.check_status` 与定义处同址,拆后 mock re-export
|
|
266
277
|
不生效——本轮 4 处测试因此假绿/失败,已改到 `spec_gen` /
|
|
267
278
|
`observability`)。
|
|
268
|
-
14.
|
|
279
|
+
14. **报告附在 `--follow` 输出末尾(机制化,不依赖 LLM)**:`--follow` 是
|
|
280
|
+
用户直接执行的通道(`!!`),done 时自动打印报告——用户零操作看到运行
|
|
281
|
+
事实。**为什么不能只靠 prompt**:让主 pi"记得跑 `--report` 并转述"是
|
|
282
|
+
流程依赖 LLM(会漏、不可验收),正是本项目一贯要消除的形态;报告既然
|
|
283
|
+
是给用户的,就该长在用户直接看的通道上。`--view --since` 不附(主 pi
|
|
284
|
+
通道,进 context 且对模型无用)。
|
|
285
|
+
15. **git 守卫范围 = 从讨论 workdir 发起的操作**(`GIT_CEILING_DIRECTORIES`
|
|
269
286
|
注入于 spawn);主项目仓库不在守卫范围(agent 的 cwd 就是主项目,其
|
|
270
287
|
约束归指令层 + 主项目 `.gitignore`)。要拦主仓库需换机制类(沙箱/钩子),
|
|
271
288
|
经评估收益不支撑扩面。
|
package/docs/test-methodology.md
CHANGED
|
@@ -256,3 +256,26 @@ 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 了版本号")。
|
package/human_viewer.py
CHANGED
|
@@ -231,10 +231,37 @@ def follow(base, bare, agents, max_meeting=None,
|
|
|
231
231
|
if done:
|
|
232
232
|
print(f"【分析已结束】result.md: {result_path(base)}",
|
|
233
233
|
flush=True)
|
|
234
|
+
_print_report(base)
|
|
234
235
|
return
|
|
235
236
|
time.sleep(poll_interval)
|
|
236
237
|
|
|
237
238
|
|
|
239
|
+
def _print_report(base):
|
|
240
|
+
"""分析结束时打印观测报告(**用户通道自带**,不依赖任何 LLM 动作)。
|
|
241
|
+
|
|
242
|
+
为什么在这里:`--follow` 是用户直接执行的通道(`!!` 命令),结束时
|
|
243
|
+
自动附报告 = 用户零操作看到运行事实(提交/墙钟/配额/进程/LLM 用量),
|
|
244
|
+
而不是指望主 pi 记得去跑 `--report` 再转述(LLM 依赖,可能漏)。
|
|
245
|
+
`--report` 独立入口与 `--cleanup` 的打印保持不变(不同场景各看一次)。
|
|
246
|
+
|
|
247
|
+
**延迟 import observability**:该模块顶层 import 本模块
|
|
248
|
+
(wait_for_completion 用 incremental),顶层反向 import 会成环。本函数
|
|
249
|
+
只在 done 分支执行一次(冷路径),函数内 import 是标准解法。
|
|
250
|
+
|
|
251
|
+
fail-open:报告是附加信息,生成失败绝不阻断观看退出(契约同
|
|
252
|
+
observability.build_report——任何一段读不出显示 n/a)。
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
from observability import build_report
|
|
256
|
+
print("【分析报告】", flush=True)
|
|
257
|
+
for line in build_report(base):
|
|
258
|
+
print(line, flush=True)
|
|
259
|
+
except Exception as e: # noqa: BLE001
|
|
260
|
+
# 宽捕获是刻意的:报告在观看主循环的退出路径上,任何异常
|
|
261
|
+
# (含未预期)都不该让用户失去"分析已结束"这个关键信息
|
|
262
|
+
print(f"【分析报告】生成失败(不影响观看):{e}", flush=True)
|
|
263
|
+
|
|
264
|
+
|
|
238
265
|
def main():
|
|
239
266
|
parser = argparse.ArgumentParser(description="human 分析展示(只读)")
|
|
240
267
|
parser.add_argument("base", help="分析目录(含 repo.git)")
|
package/observability.py
CHANGED
|
@@ -194,7 +194,20 @@ def build_report(base):
|
|
|
194
194
|
return out
|
|
195
195
|
proto = meeting_fs.read_protocol(bare)
|
|
196
196
|
|
|
197
|
-
# ----
|
|
197
|
+
# ---- 一次读取(消息文件 = 权威口径),供流程/配额/冻结/RR 四段共用 ----
|
|
198
|
+
msgs = meeting_engine.each_agent_messages(bare, agents)
|
|
199
|
+
per_agent = {a: len(msgs.get(a, [])) for a in agents}
|
|
200
|
+
# human 消息不在 participants 里(视而不见原则)——单独数 human/ 目录的
|
|
201
|
+
# 消息文件。**不用 commit subject 统计**:那是自由文本(`discuss: X/NNNN`),
|
|
202
|
+
# 格式一改/手写就静默归零(2026-09-11 实测:构造环境 subject 不同 → "提交 0"
|
|
203
|
+
# 而实际有 3 条消息);消息文件是判定域的事实,格式由本仓控制。
|
|
204
|
+
r_h = meeting_fs.run_git(bare, "ls-tree", "-r", "-z", "--name-only",
|
|
205
|
+
"HEAD", check=False)
|
|
206
|
+
human_n = sum(1 for f in r_h.stdout.rstrip("\0").split("\0")
|
|
207
|
+
if f and f.startswith("human/")
|
|
208
|
+
and meeting_fs.is_message_file(f))
|
|
209
|
+
|
|
210
|
+
# ---- 流程时间线(时间戳来自 commit——消息文件不带墙钟) ----
|
|
198
211
|
r = meeting_fs.run_git(bare, "log", "--reverse", "--format=%ct%x09%s",
|
|
199
212
|
"HEAD", check=False)
|
|
200
213
|
rows = []
|
|
@@ -203,23 +216,15 @@ def build_report(base):
|
|
|
203
216
|
continue
|
|
204
217
|
ts, subj = line.split("\t", 1)
|
|
205
218
|
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
219
|
if rows:
|
|
218
220
|
span = rows[-1][0] - rows[0][0]
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
detail = " / ".join(f"{a} {n}" for a, n in per_agent.items())
|
|
222
|
+
if human_n: # human 单列明细(它不是参与者),但计入合计
|
|
223
|
+
detail += f" / human {human_n}"
|
|
224
|
+
out.append(f"流程:{len(agents)} agents | 消息 "
|
|
225
|
+
f"{sum(per_agent.values()) + human_n}"
|
|
226
|
+
f"(含流程信号;{detail})| 墙钟跨度 {_dur(span)}"
|
|
227
|
+
f"(首末 commit 差)")
|
|
223
228
|
# 最长无进展间隔(相邻 commit 间隔的最大值)
|
|
224
229
|
gaps = [(rows[i + 1][0] - rows[i][0], rows[i][0], rows[i + 1][0])
|
|
225
230
|
for i in range(len(rows) - 1)]
|
|
@@ -233,7 +238,7 @@ def build_report(base):
|
|
|
233
238
|
# 不是"该 agent 的消息总数"——上限约束的是 meeting 发言轮次,而一个
|
|
234
239
|
# agent 的消息里还有 freezing/all-freezing/pass/concluded 等流程信号。
|
|
235
240
|
# 两者混算会出现"meeting 6/2"这种超限假象(口径错误,2026-09-11 实测)。
|
|
236
|
-
msgs
|
|
241
|
+
# msgs 由上方流程段一次读取提供(同一读取派生四段)。
|
|
237
242
|
lasts = {a: (msgs[a][-1] if msgs[a] else None) for a in agents}
|
|
238
243
|
types = {a: (lasts[a].get("type") if lasts[a] else None) for a in agents}
|
|
239
244
|
quota_meeting = proto.get("maxMeetingRounds", 10)
|
|
@@ -290,7 +295,7 @@ def build_report(base):
|
|
|
290
295
|
f"{u['responses']} 次 | error {u['errors']} 次")
|
|
291
296
|
if not any_usage:
|
|
292
297
|
out.append(" n/a(session 缺失,或无本轮数据——边界条目自 2026-09-11 "
|
|
293
|
-
"
|
|
298
|
+
"起写入,此前的老分析不适用)")
|
|
294
299
|
out.append("(口径:进程跨度=pi 进程生命周期;输出=prompt 分段合计;"
|
|
295
300
|
"墙钟=commit 时间差——三者不可互替)")
|
|
296
301
|
return out
|
package/package.json
CHANGED
package/prompts/multi-viewers.md
CHANGED
|
@@ -73,7 +73,9 @@ argument-hint: '"<主题>"'
|
|
|
73
73
|
### 4. 结束回合
|
|
74
74
|
|
|
75
75
|
告知用户:
|
|
76
|
-
- 观看:复制上一步的 `!!`
|
|
76
|
+
- 观看:复制上一步的 `!!` 命令执行(实时流式,结束时自动退出并**附本次
|
|
77
|
+
分析报告**——消息数/墙钟/配额/冻结/进程跨度/LLM 用量;用户无需任何
|
|
78
|
+
额外操作即可看到)
|
|
77
79
|
- 插话:随时 `/multi-viewers-say <文本>`(自动定位当前分析;也可用 wrapper `--say <目录> "<文本>"`)
|
|
78
80
|
- 完成时告诉主 pi,主 pi 会收尾
|
|
79
81
|
|
|
@@ -87,8 +89,7 @@ mv.sh --status <分析目录绝对路径> # done/stopped/running
|
|
|
87
89
|
|
|
88
90
|
- `done`:读 `<分析目录>-result.md`(与目录同级的固定位)→ 向用户给
|
|
89
91
|
**摘要** → `mv.sh --cleanup <分析目录绝对路径>`
|
|
90
|
-
- cleanup
|
|
91
|
-
|
|
92
|
-
转述给用户**(运行成本与健康度的一手信息)
|
|
92
|
+
- cleanup 会再打印一次**分析报告**(删目录前最后一次可读)。报告在
|
|
93
|
+
观看输出末尾已自动出现过,**不必重复转述**——用户问起数字时按需引用
|
|
93
94
|
- `stopped`:报告"分析已结束但未生成结果",不要继续等待
|
|
94
95
|
- `running`:告知还在进行,继续等用户通知
|