pi-multi-viewers 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -29,9 +29,12 @@ scripts/mv.sh 稳定入口 shim(exec mv_cli.py;路径被 prompt/READM
29
29
  scripts/pi-probe.sh LLM 探针(跑 pi + 登记新 session → 残留检查器可追溯)
30
30
  scripts/check-residue.sh 残留检查(session/进程/目录三类;增删 scripts/ 时同步本节)
31
31
  mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers)
32
- prompts/multi-viewers.md /multi-viewers 分析入口(审核闸门;视角原则引 README,不复述)
32
+ extensions/multi-viewers/ 【单一扩展单元】index.ts = 三命令 + shared.ts = 助手
33
+ /multi-viewers 分析入口(prepare→暂停点弹窗→start→预填+打印观看命令)
34
+ /multi-viewers-finish 收尾(status→确认→cleanup)
35
+ /multi-viewers-say 插话(零 LLM,直接 spawn human_sayer.py)
36
+ ⚠ extensions/ 平级禁放 .ts 助手(加载器会把平级文件当独立扩展)
33
37
  prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→落盘→给审)
34
- extensions/multi-viewers-say/ /multi-viewers-say 插话(registerCommand,零 LLM)
35
38
  docs/design.md 设计文档(fork 源模式与规模口径 + 决策记录)
36
39
  package.json npm 包 pi-multi-viewers(pi.prompts 注册;**版本号唯一事实源**)
37
40
  templates/ AGENTS.md.tpl / agent.md.tpl / gitignore.tpl / spec-readme.md.tpl
@@ -230,11 +233,14 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
230
233
 
231
234
  ## 安装/发版状态(2026-09-11)
232
235
 
233
- - **当前形态**:prompt × 2(multi-viewers 分析 / multi-viewers-setup 建视角)+
234
- extension × 1(multi-viewers-say 插话:零 LLM,直接 spawn human_sayer.py;
235
- 目录发现 = `<cwd>/mv-<sessionId>-*` 最新——**无兜底**:未匹配即报错
236
- rc 1,需显式传目录)
237
- + wrapper。**npm 已发布**(版本以 `package.json` / registry 为准)。
236
+ - **当前形态**:prompt × 1(multi-viewers-setup 建视角)+ extension × 2
237
+ ——一个扩展单元内注册三命令(`multi-viewers` 分析 / `multi-viewers-finish` 收尾 /
238
+ `multi-viewers-say` 插话;前两个靠 CLI 机器标记行取值 `[prepare] spec=` /
239
+ `[start] dir=` / `[start] watch=` / `[status]`,后者零 LLM 直接 spawn human_sayer.py;
240
+ 目录发现 = `<cwd>/mv-<sessionId>-*` 最新——**无兜底**:未匹配即报错 rc 1)+ wrapper。
241
+ 扩展层测试:`tests/extension_harness.ts`(真扩展代码 + 假 python3 装置,零 LLM;
242
+ `run_tests.sh` 自动带上——缺 bun 可见跳过、`MV_REQUIRE_BUN=1` 严格)。
243
+ **npm 已发布**(版本以 `package.json` / registry 为准)。
238
244
  - **开发机安装(两步,缺一不可;2026-09-10 实测)**:
239
245
  ① `pi install /root/pi-multi-viewers`——**注册包**(写
240
246
  `~/.pi/agent/settings.json` 的 `packages` 数组);pi 不是"扫 node_modules
@@ -247,6 +253,9 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
247
253
  - **发版时**参照 pi-agents-helper 成熟路径:`pi install npm:pi-multi-viewers`
248
254
  用户级安装(package.json `pi.prompts` 声明);prompt 路径用固定安装路径
249
255
  (只支持用户级,项目级 `.pi/npm/` 下不可达);改动 prompt 后 reload 生效。
256
+ - **版本号口径**(用户 2026-09-15 定):第二位只在「结构性变更或大功能」时升
257
+ (如 CLI 从 bash 收敛为 `mv_cli`、新增命令);其它改动(配置/默认值/小功能/
258
+ 文档/修复)只升第三位。版本号唯一事实源 = `package.json`。
250
259
  - **核验法**(照上游约定,不用命令行长度判断):
251
260
  `readlink -f ~/.pi/agent/npm/node_modules/pi-multi-viewers` 指向仓库根,
252
261
  且该路径下 `scripts/mv.sh` 存在。
package/README.md CHANGED
@@ -68,13 +68,15 @@ ls ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh
68
68
  ## 用法
69
69
 
70
70
  ```
71
- /multi-viewers "<主题>" # 分析入口(推荐;视角来自 viewers/)
72
- /multi-viewers-setup # 建视角入口(交互式:先建议 → 你定 → 落盘 → 给你审)
73
- /multi-viewers-say "<文本>" # 插话(extension:零 LLM 直接写入 human 消息)
71
+ /multi-viewers-setup # ① 建视角(首次使用先跑这个;prompt:先建议 → 你定 → 落盘 → 给你审)
72
+ /multi-viewers "<主题>" # ② 分析(extension:生成 spec → 弹窗门禁 → 启动 → 预填观看命令)
73
+ /multi-viewers-say "<文本>" # ③ 插话(分析进行中;extension:零 LLM 直接写入 human 消息)
74
+ /multi-viewers-finish # ④ 收尾(extension:查状态 → 确认 → 清理,报告随清理打印)
74
75
  ```
75
76
 
76
- 三个 pi 命令入口(分析/建视角走 prompt,插话走 extension——插话是"本地命令
77
- 执行",不需要经过 LLM)。
77
+ 四个 pi 命令入口,按使用顺序排列。**②③④ 是 extension**(流程完全由代码执行、
78
+ 零 LLM:跑命令、门禁弹窗、观看命令预填、状态判据都走退出码/机器标记行,
79
+ 不靠 LLM 转述);**① 是 prompt**——写视角是内容工作,本就需要 LLM 参与。
78
80
 
79
81
  **目录可以省略**:`--view`/`--say`/`--status`/`--report`/`--wait`/`--cleanup`
80
82
  不带目录时自动定位"本 session 当前分析"(只匹配 `mv-<sessionId>-*` 最新;**未匹配
package/docs/design.md CHANGED
@@ -17,6 +17,9 @@
17
17
  循环**生成 fork 源文件**(`meeting_fs.build_fork_source`),再用
18
18
  `pi --session <fork 源> --name <分析名>-<视角名>` 打开。
19
19
 
20
+ > 本机制建立在 pi 的 **session jsonl 文件**上——那是一处**外部契约**,
21
+ > 依赖清单与迁移触发条件见 **§六**。
22
+
20
23
  > **下列产物数字的锚**(口径要求见 §二):产物侧,2026-09-10,本仓库
21
24
  > 主 session(≈6.8k 条 / 15MB)。**条数/MB 随主会话增长漂移**(每次跑值
22
25
  > 不同),故本节数字只作量级示意;消费侧数字(tokens)一律带唤醒序号。
@@ -551,6 +554,35 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
551
554
  约束归指令层 + 主项目 `.gitignore`)。要拦主仓库需换机制类(沙箱/钩子),
552
555
  经评估收益不支撑扩面。
553
556
 
557
+ 22. **分析流程 = extension(零 LLM 骨架)**(2026-09-24 定):`/multi-viewers` 与
558
+ `/multi-viewers-finish` 由 prompt 改为 extension 命令——prepare → **门禁 = 暂停点**
559
+ (`ui.confirm`:标题点明「暂停中,可在其它窗口修改 spec」,正文带 spec 路径 +
560
+ 文件清单;用户在**其它窗口**编辑该目录,改完点「确认」继续,取消则保留 spec)
561
+ → start → **观看命令预填输入框**(`ctx.ui.setEditorText` + notify 各一份,用户按 Enter 即执行);收尾 status →
562
+ 确认 → cleanup。**为什么**:prompt 靠 LLM 逐步执行,每步都可能漏(实测:观看
563
+ 命令漏传 2 次、失败判据曾靠读中文报错文本、目录路径曾靠 LLM 记忆);代码执行
564
+ 则天然不遗漏。**与 CLI 的契约 = 机器标记行**(`[prepare] spec=` / `[start] dir=` /
565
+ `[start] watch=` / `[status]` / `[result]`),扩展不解析人类文案——文案可变,
566
+ 标记不可消失(`tests/test_mv_cli.py::TestMachineMarkers` 锁住)。**边界**:spec
567
+ 内容起草仍是真 LLM 工作(任务类型/产出形态),故为**两段式**——骨架零 LLM,
568
+ 起草/摘要走普通对话;门禁取消则保留 spec,用户编辑后自行 `mv.sh --start`。
569
+ **收尾状态语义(2026-09-25 评审)**:`running` 等待;`done | stalled` 并流进
570
+ "notify → confirm → cleanup"(`stalled` = 无存活 loop 的静止态,照 running 处理
571
+ 会让用户等一个永不到来的收尾);`stopped` 只提示手动清理。`stalled` 用独立文案
572
+ (其 result 由清理时保存,不复用 done 的"已留存"路径文案);扩展是唯一守卫
573
+ (CLI 的 cleanup 无状态防护)。**本轮明确不做**(防翻案):`--json` 输出模式、
574
+ resume 新命令面、env 回退配置、`runCli` 超时、启动路径继续优化(已在 1–2s 地板)、
575
+ 给 stalled 加第三种动作。**契约例外**:扩展作为包内第一方消费者**直连**
576
+ `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`。
583
+ 被否决:A(handler 里 `sendUserMessage` 触发 LLM 回合改 spec——时序不可控)、
584
+ D(拆两条命令——把门禁成本转嫁用户;B1 变体/第三种即现形态)。
585
+
554
586
  ### 被否决方案(含重估触发条件)
555
587
 
556
588
  | 方案 | 否因 | 重估触发 | 所在位置 |
@@ -612,3 +644,40 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
612
644
  若重启该实验,须**先登记判据**(可机械核查:覆盖 question.md 评审项数、
613
645
  给出 `file:line` 次数、是否达配额)与比较单位(per-agent / per-wake)。
614
646
  - 图片块与非字符串叶子在预算估算中的计入未覆盖(当前余量充足)。
647
+
648
+ ---
649
+
650
+ ## 六、外部契约:我们对 pi session 格式的依赖(迁移清单)
651
+
652
+ fork 机制建立在 pi 的 **session jsonl 文件**上——这是**外部契约**,不是我们能单方面
653
+ 稳定的内部设计。依赖逐条列出,供上游演进时**逐条验证**(快照:2026-09-22,
654
+ 上游源码副本 `/root/research/pi`)。
655
+
656
+ | 依赖 | 内容 |
657
+ |---|---|
658
+ | CLI | `--session <path>`(须接受**任意路径文件**——我们的 fork 源在分析目录里)、`--session-id`、`--session-dir`、`--name`、`--model`/`--thinking`/`--append-system-prompt`/`--print`/`--approve` |
659
+ | 文件布局 | `~/.pi/agent/sessions/--<cwd 编码>--/<ts>_<sid>.jsonl`;编码 = 去首尾 `/`、内部 `/`→`-`(`spec_gen.pi_sessions_dir`;`PI_SESSION_FILE` 是更稳的入口) |
660
+ | 条目 schema | 每行一个 JSON:`type`/`id`/`parentId`/`timestamp`;消息体在 `message.{role,content}`;**未知类型一律原样透传**(`_fold_entry` 默认分支) |
661
+ | 语义(**只复刻这两处**) | ① replay 起点 = 路径上最后一个 `compaction` 的 `firstKeptEntryId`;② 可见集合 = 该锚点之后的条目(`_normalize_entries` 据此移除窗口内 compaction 并桥接 `parentId`,不变量 I4) |
662
+ | 条目类型 | `compaction`(读/移除)、`thinking_level_change`(剔除继承值,否则 pi 不写本场生效值)、`session_info.name`、`custom_message`(我们的边界条目) |
663
+
664
+ **触碰面**(适配范围;口径 = 函数体行数,2026-09-22 摸底):`meeting_fs` 456 行
665
+ (真正格式耦合 ≈300:`build_fork_source` + `_normalize_entries`)· `meeting_loop` 141 ·
666
+ `spec_gen` 66 · `observability` 33。
667
+
668
+ **触发条件**(任一出现即进入适配):① `packages/coding-agent/docs/session-format.md`
669
+ 改写,或 CLI 默认会话落到 sqlite/repo 抽象(含 `--session-backend` 类开关);
670
+ ② `--session <file>` 不再接受任意路径文件,或首唤报「打不开 fork 源 / 上下文为空」;
671
+ ③ CHANGELOG 出现 "migrate sessions" / "sqlite default" 类条目。
672
+
673
+ **核对方式**(两条命令,读上游源码副本)
674
+ ```bash
675
+ head -3 /root/research/pi/packages/coding-agent/docs/session-format.md # 是否仍声明 stored as JSONL
676
+ grep -n '"--session"' /root/research/pi/packages/coding-agent/src/cli/args.ts
677
+ ```
678
+
679
+ **上游现状与结论**:`pi-agent-core` 已有 `Session`/`SessionStorage`/`SessionRepo` 抽象 +
680
+ `jsonl`/`memory` 实现,SQLite 是独立包(`@earendil-works/pi-session-backend-sqlite-node`,
681
+ 活跃开发);**但 CLI 主路径仍是 jsonl**(`core/session-manager.ts`)、`session-format.md`
682
+ 仍如此定义、Unreleased 无迁移条目。**现在不改**——对着尚未被 CLI 使用的接口写代码是投机。
683
+ (扩展侧另有稳定只读入口 `ctx.sessionManager`,进程外 loop 用不到,与"流程 extension 化"相关。)
@@ -0,0 +1,207 @@
1
+ <!-- 存档:docs/reviews/2026-09-25-multi-viewers-extension-review.md
2
+ 来源:一次真实多视角分析的 result.md 原文(未删改,仅加本头与下方说明)。
3
+ 分析场次目录已随 cleanup 删除;文中消息编号(如 效率/0003)不可再核验,
4
+ 仅作溯源线索(与代码注释引用约定一致:行为以自描述为准)。 -->
5
+
6
+ # 存档说明
7
+
8
+ - **主题**:审阅把 `/multi-viewers` 改成 extension 这次的实现(extension 代码 /
9
+ CLI 机器标记行契约 / 文档同步)
10
+ - **场次**:`mv-mv-main-20260925-094526`(3 视角:效率 / 简单 / 铁律;真实 pi 讨论,
11
+ 默认档 `mc-tools`、`forkMode=budget`、`maxMeeting=15`;约 49 分钟,三方共识收敛)
12
+ - **本场同时是两件事的首次验证**:
13
+ - **extension 流程的首次真实使用**(prepare → 暂停点弹窗 → start → 预填观看命令;
14
+ 本场暴露 2 项静态审阅看不到的问题:① 主题连引号进入 topic/question.md
15
+ ② 观看命令只有"预填"一个出口——被覆盖后用户找不到它)
16
+ - **A 形态(spec 不再经 LLM 扩写)的首场**:本场未跑偏(守住了"只提意见不改代码",
17
+ 每项带 `文件:行`)——A 的判定窗口按报告 §六.1 为 1+2~3 场
18
+ - **落地**(本场 6 类问题 + 2 项首用发现,一次收口):
19
+ P1 `stalled` 并流进 done 路径(不再让用户等一个永不到来的收尾)·
20
+ P2 `stalled` 用独立文案(不复用 done 的 result 路径文案)·
21
+ P3 删掉与暂停点自相矛盾的过期头注释 ·
22
+ P4 删掉单机回退路径,改**加载期响亮失败** ·
23
+ P5 两个扩展**合并为一个单元**(三命令 + `shared.ts`,消 ≈35 行已漂移的重复)·
24
+ P6 取消提示补"在当前 pi session 内执行" + **harness 进仓库**(`tests/extension_harness.ts`,
25
+ `run_tests.sh` 缺 bun 可见跳过 / `MV_REQUIRE_BUN=1` 严格)·
26
+ 首用两项:主题剥引号 · notify 里带一份观看命令
27
+ - **明确不做**(随决策 22 记录):`--json` · resume 命令面 · env 回退配置 ·
28
+ `runCli` 超时 · 启动路径继续优化(已在 1–2s 地板)
29
+
30
+ # /multi-viewers extension 改造审阅 · 三方共识结果
31
+
32
+ **主题**:审阅把 `/multi-viewers` 改成 extension 这次的实现(代码 / 标记行契约 / 文档),
33
+ 只提意见不改代码。
34
+ **参与者**:效率 / 简单 / 铁律(3 视角;meeting 自由讨论 + round-robin 全体 pass)。
35
+ **方法**:只读核对,每项带 `文件:行`;语义类问题**先推演确认再定论**(铁律「发现偏差先
36
+ 推演」);**本场未修改任何文件、未运行真实分析**。
37
+ **场次**:`mv-mv-main-20260925-094526`(fork 模式,mc-tools 档)。
38
+
39
+ ---
40
+
41
+ ## 0. 结论摘要
42
+
43
+ 1. **方向成立、净收益为正**:流程骨架零 LLM(代码执行 prepare → 门禁暂停点 → start →
44
+ 预填观看命令);机读判据从"读中文文案"改为**退出码 + 机器标记行**;内容起草仍留普通
45
+ 对话。省下的是 3–4 个主 session LLM 回合与 4 类已观测失误的返工,**收益不在启动路径**
46
+ ——启动机器成本已到 1–2s 地板(实测:env 创建 0.91s、fork 655ms/84MB/agent、
47
+ 单次 CLI 58–66ms)。
48
+ 2. **共发现 6 类问题**:其中 **stalled 分支是唯一行为错误**(把"无 loop 存活的静止态"
49
+ 指引成"等待一个永不到来的收尾");其余为契约缺口(`[result]` 对 stalled)、过期注释、
50
+ 开发机回退补丁、跨扩展重复 + 稳定入口约定、取消提示可发现性 + 测试半边缺口。
51
+ 3. **讨论收敛出 10 项冻结实现清单**(§三)与 **7 项「本轮明确不做」**(§四),并裁定
52
+ 5 处设计语义:stalled 并流、`[result]` 取 (ii)、回退删除改加载期响亮失败、
53
+ mv.sh 记"第一方直连"例外、launch 失败窗口只记注释不改行为。
54
+ 4. **净复杂度账为负(变简)**:合并消 ≈35 行逐字重复(且副本已实际漂移);删回退消一条
55
+ 单机死分支;stalled 并流 ≈0 净增;`[result]` 取 (ii) 零契约增量。
56
+ 5. **执行建议**:**合并先行** → 其余编辑落在同一扩展单元 → 文档 + harness 一次收口 →
57
+ **一次 reload、一次真跑复验**(不按项分次真跑)。
58
+
59
+ ---
60
+
61
+ ## 一、总体判定
62
+
63
+ | 维度 | 判定 | 依据 |
64
+ |---|---|---|
65
+ | 职责边界 | 成立 | 扩展只做"流程骨架 + UI"(spawn CLI、解析标记、弹窗、预填);引擎/IO/判定留在 CLI/loop;LLM 内容起草未进 handler。唯一越界点是扩展**重述**观测层状态语义并抄错(stalled,P1) |
66
+ | 复杂度匹配 | 成立但有一类债 | 无新概念被引入(标记行沿用既有 `[status]`/`[result]` 约定);债在跨扩展复制与开发机回退(P4/P5);合并后净减 |
67
+ | 设计符合度 | 大体符合 | 与设计决策 22 一致(零 LLM 骨架、两段式、标记契约);偏差集中在过期注释(P3)、`[result]` 缺口(P2)与文档措辞 |
68
+
69
+ 参考基线:`extensions/multi-viewers/index.ts`(~230 行,主入口,2 天改 2 次)、
70
+ `extensions/multi-viewers-say/index.ts`、`mv_cli.py`、`observability.py`、
71
+ `start_discussion.py`、`docs/design.md` 决策 22、`AGENTS.md`、`README.md`。
72
+
73
+ ---
74
+
75
+ ## 二、确认问题清单
76
+
77
+ ### P1(行为错误,唯一)stalled 被并入 running → 指引用户"继续等"
78
+
79
+ - 状态单源:`stalled = 有 result.md 无 concluded 且 **loop 均不存活**`(`observability.py:109`);
80
+ `--wait` 对它的既有出路:「**停止等待**;可读 result.md 或 --cleanup」(`observability.py:165-168`)。
81
+ - 扩展现状:`state === "running" || state === "stalled"` → 「分析仍在进行——完成后再说」
82
+ (`extensions/multi-viewers/index.ts:191-196`)→ 用户会等一个**永远不会到来**的收尾。
83
+ - 安全前提已核:`cleanup_discussion` 先保存 result.md → 打印报告 → 再删目录,且**无状态
84
+ 守卫**(`start_discussion.py:435-459`),所以 stalled 下走 cleanup 完全可用。
85
+
86
+ ### P2(契约缺口)`[result]` 标记只在 done 打印 → 并流后必然踩空
87
+
88
+ - `--status` 仅 `st == "done"` 时打印 `[result]`(`start_discussion.py:624-629`),且有
89
+ negative 测试锁 running 不打印(`tests/test_main_paths.py:674-680`)。
90
+ - 扩展 fallback `result ?? "(未找到 result 路径)"` 与 confirm 正文的 `?? "?"`
91
+ (`index.ts:210,216`)在 stalled 下必然命中。
92
+ - 更深一层:stalled 下 `<base>-result.md` **尚未生成**——保存在收尾(`meeting_loop.py:668-670`,
93
+ rw 正常退出后)或清理(`start_discussion.py:448-449`)触发,而 stalled 的定义正是
94
+ rw 崩溃在保存之前(`meeting_fs.py:1224-1245` 为保存实现)。
95
+ - 裁定:**取 (ii)**——扩展侧 stalled 用独立文案("result.md 已提交;清理会先打印报告并
96
+ 保存结果"),CLI 判据、决策 22 标记清单、negative 测试**零变更**。反对 (i)(CLI 对
97
+ done|stalled 都打印):会把"已存在"与"将存在"两种语义压进同一标记(状态双义)。
98
+
99
+ ### P3(文档/注释偏差)文件头注释与门禁行为自相矛盾
100
+
101
+ - `index.ts:24` 仍写「要改 spec 就先取消,编辑后自行 --start」,而 handler 已是
102
+ confirm **暂停点**(`:137-144`,用户 2026-09-24 拍板:弹窗保持打开、可在其它窗口编辑、
103
+ 改完点确认继续)。修法:**删掉那句**,头部只留最短准确陈述(复述必漂移的反例)。
104
+
105
+ ### P4(补丁)回退 `/root/pi-multi-viewers` 是单机死分支
106
+
107
+ - `FALLBACK_ROOT = "/root/pi-multi-viewers"`(`index.ts:57`);say 扩展为逐文件三目回退
108
+ (`multi-viewers-say/index.ts:53-59`)——两种写法本身即漂移。
109
+ - 触发条件(找不到包根 = 包被拆散/复制)恰是"开发机以外"的场景 → 在非开发机 100% 指向
110
+ 不存在的路径,把"包根解析失败"变成更晚、更难诊断的失败。
111
+ - 裁定:**删除,加载期 throw + 行动性错误信息**("找不到 pi-multi-viewers 包根——请用
112
+ pi install / npm 安装");**不加 env 覆盖**(新配置面=新概念,无真实场景)。
113
+
114
+ ### P5(重复 + 约定)跨扩展复制 ≈35 行(已漂移)+ 稳定入口未闭合
115
+
116
+ - `findPackageRoot` 两处**逐字相同**(`index.ts:39` 与 `multi-viewers-say/index.ts:35`),
117
+ 连同导入样板重复 ≈35 行;副本已实际漂移(P4 两种回退写法即证据)。
118
+ - 已核实合并无平台障碍:上游扩展**支持多文件**(`docs/extensions.md:237-243`);加载器
119
+ **一层扫描、子目录只认 `index.ts`/manifest**(`loader.ts:665-698,704-708`);一个扩展
120
+ 注册多命令可行(`commands` 为 Map,`registerCommand` 多次调用各自成键,`loader.ts:287-294`)。
121
+ - **约束(防静默复杂度)**:助手模块必须放**入口子目录内**;`extensions/` 平级**禁放**
122
+ `.ts` 助手(规则 1 会把它当独立扩展加载)。
123
+ - 稳定入口:`scripts/mv.sh` 是文档化"稳定入口",但扩展为包内第一方消费者、直连
124
+ `mv_cli.py`(say 已有直连先例)。**性能上零差异**(实测 shim 61ms vs 直连 58–66ms)
125
+ ——裁定按**契约一致性**记一行"第一方直连"例外,不以性能论据选边。
126
+
127
+ ### P6(可发现性 + 测试)取消提示的 sid 条件 + 契约测试只锁生产者半边
128
+
129
+ - `mv_cli.py:348-353` 目录名 `mv{-(sid)}-<stamp>`;`--find-dir` 按 `mv-<PI_SESSION_ID>-*`
130
+ 匹配且**无兜底**(`observability.py:819-834`)。pi 的 bash 工具默认注入 `PI_SESSION_ID`
131
+ (上游 `bash.ts:234` 默认 true → `183-185`),所以 pi 内 `!`-执行没问题;**外部终端**
132
+ 执行则目录无 sid,之后 `/multi-viewers-say`、`/multi-viewers-finish` 都定位不到。
133
+ 修法:取消提示加一句「(请在当前 pi session 内执行)」;**反对**新增 resume 命令面。
134
+ - `tests/test_mv_cli.py::TestMachineMarkers` 锁住 CLI 侧三条标记(好),但**扩展消费端
135
+ 零仓库内测试**(上次 16 断言的装置是临时且已删)。这是唯一值得追加的投入。
136
+
137
+ ---
138
+
139
+ ## 三、冻结实现清单(三方共识,实现者对照用)
140
+
141
+ 1. **stalled 并流**:`running` → 等待;`done|stalled` → notify → confirm → cleanup
142
+ (文案按状态参数化;**两处 fallback 不得复用于 stalled**,`index.ts:210,216`);
143
+ `stopped` → 建议手动清理;守卫保留(CLI 侧无防,扩展是唯一防线)。
144
+ 2. **头部注释删句**(`index.ts:24`)。
145
+ 3. **取消提示加句**:「(请在当前 pi session 内执行)」。
146
+ 4. **删首条 notify**(`index.ts:130`;弹窗与取消分支已各给一次路径)。
147
+ 5. **删回退** → 加载期响亮失败(无 env、无 `??` 残留),错误信息给行动。
148
+ 6. **合并**:一个扩展单元、三命令;助手模块放入口子目录内;AGENTS 记"平级禁放 `.ts` 助手"。
149
+ 7. **harness**:单套覆盖合并后单元;`tests/` + `run_tests.sh` 检测 bun、无则**可见跳过**、
150
+ `MV_REQUIRE_BUN=1` 严格开关;断言按**新语义**(stalled → 不含两种 fallback 文本、
151
+ confirm 前不 cleanup、confirm 后 cleanup 一次且输出含保存路径;running/stopped 不清理
152
+ 的防线性保留)。
153
+ 8. **文档同步**(按最终结构取一,勿并存):AGENTS 结构清单/「当前形态」、**mv.sh 例外一行**、
154
+ design 决策 22(并流 + (ii) + 下述"不做"附注)、`design.md:561` 双空格;
155
+ `TestMachineMarkers` 与 `test_main_paths` 不动((ii) 零 CLI 变更)。
156
+ 9. **`cmd_start` why 注释一行**(`mv_cli.py:363-368` 旁):删除与创建绑定 = 一次性消费;
157
+ launch 失败则 spec 已消费、须 cleanup;重估触发 = 观测到 launch 失败或引入 resume/retry。
158
+ 10. **一次 reload + 一次真跑复验**(批次执行,不按项分次)。
159
+
160
+ 执行顺序:**合并先行**(后续编辑落在同一单元)→ 文档 + harness 一次收口。
161
+
162
+ ---
163
+
164
+ ## 四、「本轮明确不做」(随决策 22 记录,防翻案)
165
+
166
+ `--json`(新输出模式)· resume 新命令面 · env 回退配置 · `runCli` 超时(现结论"可改可
167
+ 不改";若将来加,只对只读调用且走共享一处)· 启动路径继续优化(已在 1–2s 地板)·
168
+ 删除点顺序重排(改注释记录,不改行为)· 给 stalled 加第三种收尾动作或把"读 result"
169
+ 搬进扩展(读走 `--report`/直接开文件,cleanup 自打印报告)。
170
+
171
+ ---
172
+
173
+ ## 五、讨论中的语义裁定与记录校正
174
+
175
+ | # | 事项 | 收敛过程 |
176
+ |---|---|---|
177
+ | 1 | **stalled 处置** | 简单初判"四分支保留"(未核状态语义)→ 铁律补核:stalled = 无存活 loop、`--wait` 出路是停止等待 → 双方同形**并流**(`done|stalled` 共用路径,零复制、分支数不变) |
178
+ | 2 | **`[result]` 契约** | 铁律 0003 曾偏 (i),0005 改主张 (ii)(语义纯度论);简单 0004 明确选 (ii);效率倾向 (ii) → **三边 (ii)** |
179
+ | 3 | **回退路径** | 简单最初主张"统一两种写法",0003 撤回改**删除**("统一=两条死路归一,删除=零条死路");效率、铁律同 → 三边闭合 |
180
+ | 4 | **记录校正** | 效率原写"start 失败不删 spec";铁律按 `mv_cli.py:359-373` 校正为「**创建失败不删 spec;launch 失败时 spec 已删**」→ 效率撤回并接受(错误不变量会被用来设计恢复路径,必须更正记录) |
181
+ | 5 | **mv.sh 取舍** | 效率实测排除性能论据(61ms vs 58–66ms)→ 按契约一致性记例外 |
182
+
183
+ ---
184
+
185
+ ## 六、遗留与观察项(非本轮改动)
186
+
187
+ 1. **A 方案判定未完成**:`question.md` 现在只有主题原文(零 LLM 扩写)。判定窗口 = 本场
188
+ 及随后 2–3 场:首轮是否跑偏、是否守"只提意见不改代码"、轮次数 vs 基线。触发条件 =
189
+ **≥1 次明显跑偏 → 启用"起草 prompt"**;实现必须是**独立对话/命令**(在 `/multi-viewers`
190
+ 之前完成),**不得**进 extension handler(决策 22 已否决的 A 变体)。
191
+ 2. **唤醒结束日志**:归 loop(唤醒时序的拥有者),独立于本次收口——它是每唤延迟
192
+ (48–222s,墙钟大头)的唯一度量入口,属另一议题。
193
+ 3. **launch 失败**:发生率无记录;出现一次即触发"删除点/恢复路径"重估(见清单 9)。
194
+ 4. **效率的流程观察**:同一份证据被三个视角各读一遍(本场为取证共约 10+ 次本地调用)
195
+ ——取证值得,但机制上值得考虑复用以降感知成本(非实现问题,留档)。
196
+ 5. **数据口径提醒**:效率的启动/唤醒数字为 n=1 实场观测(首唤 147–222s vs 稳态基线
197
+ ~48s/唤醒),不构成承诺。
198
+
199
+ ---
200
+
201
+ ## 七、验证与局限
202
+
203
+ - 本场结论全部来自**只读核对 + 源码推演**:所有 `文件:行` 为 2026-09-25 工作区现场;
204
+ 上游加载器结论引自 `/root/research/pi`(v0.87 线)。
205
+ - **未做**:修改任何文件、运行真实 LLM、跑 harness(harness 尚不存在)。
206
+ - 落地正确性由实现批的 harness(新语义断言)+ **一次真跑复验**保证;若实现中出现
207
+ 新分支/新命令面/新输出模式,按本轮既定判据(能否删一个分支、复杂度增量应 ≤0)再议。
@@ -32,6 +32,7 @@
32
32
  | `2026-09-13-e2e23-time-breakdown-analysis.md` | 一次分析的**时间构成**(哪些必要、哪些可省) | AFT / MC historian 两笔分钟级开销都不在报告里;反对"不等退出就推进"与"新增常驻机制" | `98786d6` + `c46d996` / `9deefac` |
33
33
  | `2026-09-14-e2e24-extension-policy-review.md` | 扩展策略三档(默认 mc-tools)的实现与证据链 + `ctx_search` 价值评估 | **S1:缺 MC 的降级路径提前 return → 命令被截断(缺 model/print/注入、cwd 错)**;S1a 测试判别力不足;E2 报告缺"声明 vs 生效";E1 成本口径超出精度;T1–T3 文本矛盾;F5/F6/F7/S2/F9 解析链缺陷;**ctx_search 9 次调用全为问卷诱导、0 次决定性帮助、命中 1 条过期记忆** | `91a1171`(Batch 1+3)、`b9329fb`(Batch 2 删死代码)|
34
34
  | `2026-09-14-e2e25-doc-drift-review.md` | 文档 vs 代码一致性(文档漂移)+ ctx_search 的**自然使用**观察 | **三处文档仍写"兜底 mv-* 并警告"而代码是无兜底、未匹配报错**(行为语义相反);README 缺 `--extension-policy`;报告字段列表过期;4 条缺失项;**根因 = 对实现的复述**(治本:引事实源不复制);自然使用观察:`ctx_search` **0 次**、historian 0 次 | `ba204ef` |
35
+ | `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 进仓库) |
35
36
 
36
37
  ## 环境口径(读报告时的背景)
37
38
 
@@ -0,0 +1,213 @@
1
+ /**
2
+ * multi-viewers —— 多视角分析的三个命令(零 LLM 参与;一个扩展单元)
3
+ *
4
+ * /multi-viewers "<主题>" prepare → **暂停点弹窗** → start → 预填观看命令
5
+ * /multi-viewers-finish status → 确认 → cleanup(摘要留给普通对话)
6
+ * /multi-viewers-say "<文本>" 插话(human 消息,各视角可见可回应)
7
+ *
8
+ * 为什么不是 prompt:prompt 靠 LLM 逐步执行(跑命令、转述路径、判断失败),
9
+ * 每一步都可能漏(实测:观看命令漏过 2 次、失败判据曾靠读中文报错文本)。
10
+ * 代码执行同一流程则天然不遗漏;**spec 内容起草仍在普通对话里**(那是真
11
+ * LLM 工作,见 docs/design.md 决策 22——两段式)。
12
+ *
13
+ * 门禁 = `ui.confirm` **暂停点**(用户 2026-09-24 拍板):流程在弹窗处停住,
14
+ * 用户在**其它窗口**编辑 spec 目录,改完点「确认」继续(`--start` 在确认之后
15
+ * 才跑,所以改的内容一定生效);取消则不启动并保留 spec。
16
+ *
17
+ * 与 CLI 的契约(标记行/退出码)与全部原语见 ./shared.ts。
18
+ *
19
+ * 观看命令交付 = `ctx.ui.setEditorText` 预填输入框(按 Enter 即执行)**并**
20
+ * 在 notify 里带一份(预填会被后续输入覆盖——只留预填这一个出口,用户就
21
+ * 再也找不到它;2026-09-25 首次真实使用暴露)。
22
+ */
23
+
24
+ import {
25
+ findCurrentDir,
26
+ grab,
27
+ runCli,
28
+ runSayer,
29
+ specListing,
30
+ stripQuotes,
31
+ } from "./shared.ts";
32
+
33
+ export default function register(pi: any) {
34
+ // ---------------------------------------------------------------- 分析入口
35
+ pi.registerCommand("multi-viewers", {
36
+ description: "多视角协同分析:生成 spec → 你审阅 → 启动(零 LLM 流程)",
37
+ argumentHint: "<主题>",
38
+ getArgumentCompletions: () => null,
39
+ handler: async (args: string, ctx: any) => {
40
+ const topic = stripQuotes(args.trim());
41
+ if (!topic) {
42
+ ctx.ui.notify(
43
+ '主题为空——用法: /multi-viewers "<主题>"(视角来自项目 viewers/)',
44
+ "warning",
45
+ );
46
+ return;
47
+ }
48
+ const sid = ctx.sessionManager.getSessionId();
49
+ const cwd = ctx.cwd;
50
+
51
+ // ① 生成 spec(零 LLM:question.md 首行就是主题原文)
52
+ const prep = await runCli(["--prepare", topic], cwd, sid);
53
+ const specDir = grab(prep.output, "[prepare] spec=");
54
+ if (prep.rc !== 0 || !specDir) {
55
+ ctx.ui.notify(
56
+ prep.output || "spec 生成失败(没有可解析的 [prepare] spec= 标记行)",
57
+ "error",
58
+ );
59
+ return;
60
+ }
61
+
62
+ // ② 门禁 = 暂停点(路径与清单写在弹窗正文里,不再另发一条 notify)
63
+ const go = await ctx.ui.confirm(
64
+ "启动多视角分析?(现在暂停中,可在其它窗口修改 spec)",
65
+ `spec:${specDir}\n文件:${specListing(specDir)}\n\n` +
66
+ "需要修改就去改这个目录,改完点「确认」继续;\n" +
67
+ "点「取消」则不启动(spec 保留,可稍后 mv.sh --start)。",
68
+ );
69
+ if (!go) {
70
+ ctx.ui.notify(
71
+ `已取消,spec 保留在:${specDir}\n` +
72
+ `之后可在**当前 pi session 内**用:mv.sh --start ${specDir}\n` +
73
+ "(外部终端执行时目录名不带 session id,插话/收尾命令定位不到它)",
74
+ "info",
75
+ );
76
+ return;
77
+ }
78
+
79
+ // ③ 启动(环境创建 + 拉起 loop;spec 会被消费删除——CLI 的既定行为)
80
+ const start = await runCli(["--start", specDir], cwd, sid);
81
+ const watch = grab(start.output, "[start] watch=");
82
+ const dir = grab(start.output, "[start] dir=");
83
+ if (start.rc !== 0 || !watch) {
84
+ ctx.ui.notify(
85
+ `启动失败:\n${start.output || "(无输出)"}\n` +
86
+ "可用 mv.sh --status 查看环境状态。",
87
+ "error",
88
+ );
89
+ return;
90
+ }
91
+
92
+ // ④ 观看命令:预填进输入框 + notify 里留一份副本(见文件头)
93
+ ctx.ui.setEditorText(watch);
94
+ ctx.ui.notify(
95
+ `分析已启动${dir ? `:${dir}` : ""}\n` +
96
+ "观看(已预填进输入框,按 Enter 执行;也可复制这行):\n" +
97
+ `${watch}\n` +
98
+ "插话:/multi-viewers-say <文本> 收尾:/multi-viewers-finish",
99
+ "success",
100
+ );
101
+ },
102
+ });
103
+
104
+ // ---------------------------------------------------------------- 收尾
105
+ pi.registerCommand("multi-viewers-finish", {
106
+ description: "收尾:查状态 → 确认 → 清理分析目录(结果留存;摘要走对话)",
107
+ getArgumentCompletions: () => null,
108
+ handler: async (_args: string, ctx: any) => {
109
+ const sid = ctx.sessionManager.getSessionId();
110
+ const st = await runCli(["--status"], ctx.cwd, sid);
111
+ const state = grab(st.output, "[status] ");
112
+ if (st.rc !== 0 || !state) {
113
+ ctx.ui.notify(
114
+ st.output || "查状态失败(没有可解析的 [status] 标记行)",
115
+ "error",
116
+ );
117
+ return;
118
+ }
119
+ if (state === "running") {
120
+ ctx.ui.notify(
121
+ "分析仍在进行(状态 running)——完成后再说 /multi-viewers-finish",
122
+ "info",
123
+ );
124
+ return;
125
+ }
126
+ if (state === "stopped") {
127
+ ctx.ui.notify(
128
+ "分析已结束但未生成结果(状态 stopped)。" +
129
+ "要清理请自行运行 mv.sh --cleanup。",
130
+ "warning",
131
+ );
132
+ return;
133
+ }
134
+ if (state !== "done" && state !== "stalled") {
135
+ ctx.ui.notify(`状态 ${state}——没有可收尾的分析。`, "warning");
136
+ return;
137
+ }
138
+
139
+ // done 与 stalled 并流(评审 P1):stalled = 无存活 loop 的静止态,
140
+ // 此时若照 running 处理,用户会等一个**永远不会到来**的收尾。
141
+ // 两者差别只在文案:done 的结果已落盘;stalled 的结果**尚未**生成
142
+ // (保存在收尾/清理时触发),故不复用 done 的路径文案(评审 P2)。
143
+ if (state === "done") {
144
+ const result = grab(st.output, "[result] ");
145
+ ctx.ui.notify(
146
+ `分析已完成,结果:${result ?? "(--status 未给出 result 路径)"}`,
147
+ "success",
148
+ );
149
+ } else {
150
+ ctx.ui.notify(
151
+ "分析停在未收尾状态(状态 stalled:没有存活的 loop,也没有 concluded)。" +
152
+ "可以清理——清理会先保存结果、打印分析报告,再删目录。",
153
+ "warning",
154
+ );
155
+ }
156
+
157
+ const ok = await ctx.ui.confirm(
158
+ "确认收尾?",
159
+ "将清理分析目录;结果会保存到 `<分析目录>-result.md`," +
160
+ "清理时还会打印一次分析报告。",
161
+ );
162
+ if (!ok) {
163
+ ctx.ui.notify("已取消收尾(分析目录保留)。", "info");
164
+ return;
165
+ }
166
+ const clean = await runCli(["--cleanup"], ctx.cwd, sid);
167
+ if (clean.rc !== 0) {
168
+ ctx.ui.notify(`收尾失败:\n${clean.output}`, "error");
169
+ return;
170
+ }
171
+ ctx.ui.notify(
172
+ `${clean.output}\n\n要摘要就在对话里说一声(主 pi 读该 result.md 即可)。`,
173
+ "success",
174
+ );
175
+ },
176
+ });
177
+
178
+ // ---------------------------------------------------------------- 插话
179
+ pi.registerCommand("multi-viewers-say", {
180
+ description: "向正在进行的多视角分析插话(human 消息,各视角可见可回应)",
181
+ argumentHint: "<插话内容>",
182
+ getArgumentCompletions: () => null,
183
+ handler: async (args: string, ctx: any) => {
184
+ const text = args.trim();
185
+ if (!text) {
186
+ ctx.ui.notify(
187
+ "插话内容为空——用法: /multi-viewers-say <文本>",
188
+ "warning",
189
+ );
190
+ return;
191
+ }
192
+ const sid = ctx.sessionManager.getSessionId();
193
+ const dir = await findCurrentDir(ctx.cwd, sid);
194
+ if (!dir) {
195
+ ctx.ui.notify(
196
+ "本 session 没有正在进行的多视角分析(cwd 下无 " +
197
+ `mv-${sid}-* 分析环境)。先用 /multi-viewers 启动,` +
198
+ "或改用 mv.sh --say <目录> \"<文本>\" 显式指定。",
199
+ "error",
200
+ );
201
+ return;
202
+ }
203
+ const { ok, output } = await runSayer(dir, text);
204
+ if (ok && output) {
205
+ ctx.ui.notify(output, "success");
206
+ } else if (ok) {
207
+ ctx.ui.notify("插话已发送", "success");
208
+ } else {
209
+ ctx.ui.notify(`插话失败: ${output || "未知错误"}`, "error");
210
+ }
211
+ },
212
+ });
213
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * 多视角分析的扩展助手(**不是**独立扩展——放在入口子目录内,加载器不会单独加载它)。
3
+ *
4
+ * 一个扩展单元、三个命令(见 index.ts)共用这些原语:包根定位、CLI 调用、
5
+ * 机器标记行解析、分析目录发现、human 插话。合并前这些在两个扩展里逐字重复
6
+ * ≈35 行,且副本已经漂移(两种回退写法)——评审 P5 裁定合并。
7
+ *
8
+ * 与 CLI 的契约 = **机器可读标记行**(不解析人类文案——文案会变,标记不变):
9
+ * --prepare → `[prepare] spec=<绝对路径>`
10
+ * --start → `[start] dir=<分析目录>` / `[start] watch=<!!观看命令>`
11
+ * --status → `[status] <状态>`(done 时另有 `[result] <路径>`)
12
+ * 判据一律用**退出码**(e2e16 F2:stderr 中文文案一改就静默失配)。
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const PKG_NAME = "pi-multi-viewers";
22
+
23
+ /** 包根 = 从本文件向上找声明了包名的 package.json。
24
+ *
25
+ * **找不到就响亮失败**(评审 P4):此前回退到 `/root/pi-multi-viewers` 是单机
26
+ * 死分支——触发条件(包被拆散/复制)恰恰发生在开发机以外,回退只会把"包根
27
+ * 解析失败"变成更晚、更难诊断的失败。这里在**加载期** throw,错误信息给出行动。
28
+ */
29
+ function findPackageRoot(start: string, pkgName: string): string {
30
+ let dir = start;
31
+ for (let i = 0; i < 8; i++) {
32
+ try {
33
+ const pkg = JSON.parse(
34
+ fs.readFileSync(path.join(dir, "package.json"), "utf8"),
35
+ );
36
+ if (pkg.name === pkgName) return dir;
37
+ } catch {
38
+ // 继续向上
39
+ }
40
+ const parent = path.dirname(dir);
41
+ if (parent === dir) break;
42
+ dir = parent;
43
+ }
44
+ throw new Error(
45
+ `找不到 ${pkgName} 包根(从 ${start} 向上 8 层未见同名 package.json)。` +
46
+ `请用 pi install / npm install 安装该包,而不是手工复制扩展文件。`,
47
+ );
48
+ }
49
+
50
+ const PACKAGE_ROOT = findPackageRoot(__dirname, PKG_NAME);
51
+ const CLI = path.join(PACKAGE_ROOT, "mv_cli.py");
52
+ const SAYER = path.join(PACKAGE_ROOT, "human_sayer.py");
53
+ const OBSERVABILITY = path.join(PACKAGE_ROOT, "observability.py");
54
+
55
+ /** 运行 mv_cli 一条命令;返回 { rc, output }(stdout+stderr 合并)。 */
56
+ export function runCli(
57
+ args: string[],
58
+ cwd: string,
59
+ sid: string,
60
+ ): Promise<{ rc: number; output: string }> {
61
+ return new Promise((resolve) => {
62
+ const proc = spawn("python3", [CLI, ...args], {
63
+ cwd,
64
+ env: { ...process.env, PI_SESSION_ID: sid },
65
+ stdio: ["ignore", "pipe", "pipe"],
66
+ });
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) }));
72
+ });
73
+ }
74
+
75
+ /** 取机器可读标记行的值(`[label] value`);没有 → null。 */
76
+ export function grab(output: string, label: string): string | null {
77
+ for (const line of output.split("\n")) {
78
+ const t = line.trim();
79
+ if (t.startsWith(label)) return t.slice(label.length).trim();
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /** 剥掉一层配对引号(pi 的 extension 参数是**原样**传入的,不像 shell 会剥——
85
+ * 用户照文档写成 `"/multi-viewers \"<主题>\""` 时,引号会进主题与 question.md)。 */
86
+ export function stripQuotes(s: string): string {
87
+ const pairs: [string, string][] = [
88
+ ['"', '"'],
89
+ ["'", "'"],
90
+ ["\u201c", "\u201d"],
91
+ ];
92
+ for (const [open, close] of pairs) {
93
+ if (s.length >= 2 && s.startsWith(open) && s.endsWith(close)) {
94
+ return s.slice(1, -1).trim();
95
+ }
96
+ }
97
+ return s;
98
+ }
99
+
100
+ /** spec 目录清单(人类可读一行,用于门禁弹窗)。 */
101
+ export function specListing(specDir: string): string {
102
+ try {
103
+ return fs
104
+ .readdirSync(specDir, { withFileTypes: true })
105
+ .sort((a, b) => a.name.localeCompare(b.name))
106
+ .map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
107
+ .join(" ");
108
+ } catch {
109
+ return "(目录读取失败)";
110
+ }
111
+ }
112
+
113
+ /** 定位当前分析目录:调用 observability.py --find-dir(**python 单一实现**
114
+ * ——wrapper 消费命令同一入口)。
115
+ *
116
+ * 判据 = **退出码**(不是 stderr 文案——中文提示一改就静默失配,e2e16 F2):
117
+ * rc 0 → stdout 是绝对路径;rc 1 → 未找到(无同 sid 分析)。无降级通道
118
+ * (不会回退到"项目下最新"——那会插错分析,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
+ });
135
+ }
136
+
137
+ /** 执行 human_sayer.py 一次插话。返回 { ok, output }。 */
138
+ export function runSayer(
139
+ dir: string,
140
+ text: string,
141
+ ): 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
+ });
152
+ }
package/mv_cli.py CHANGED
@@ -322,11 +322,15 @@ def cmd_prepare(args):
322
322
  cmd += ["--agents", agents_list]
323
323
  if _call(cmd):
324
324
  fail("spec 骨架生成失败(start_discussion --spec-gen,见上方错误)")
325
+ # 机器可读标记(供 pi extension 解析;人类文字照常保留):扩展靠它拿
326
+ # spec 路径,不解析人类文案(文案会变,标记不变)。
327
+ print(f"[prepare] spec={spec_dir}")
325
328
  print(f"""已生成分析 spec:
326
329
  {spec_dir}
327
330
 
328
- 请查看/编辑该目录,补充背景、各 agent 视角等。
329
- 编辑完成后,告诉我"继续",我会自动启动分析。""")
331
+ 请查看/编辑该目录(question.md 任务书 / background.md 边界 / agents/*.md 视角快照 /
332
+ models.md 模型),确认后启动:
333
+ {PROG} --start {spec_dir}""")
330
334
  return 0
331
335
 
332
336
 
@@ -356,6 +360,9 @@ def cmd_start(spec_dir, extra):
356
360
  "--spec", spec_dir] + extra):
357
361
  fail("环境创建失败,请查看上方输出")
358
362
 
363
+ # 删除与创建**绑定**在此步 = 一次性消费(spec 是视角任务书/背景/models 在
364
+ # 用户侧的唯一副本)。已知边界:launch(第 2 步)失败时 spec 已删——重估触发
365
+ # 条件 = 观测到一次 launch 失败,或将来引入 resume/retry 通道。
359
366
  if fnmatch.fnmatch(os.path.basename(os.path.normpath(spec_dir)),
360
367
  "mv-spec-*"):
361
368
  print(f"[start] spec 已消费,删除(本工具生成形态): {spec_dir}")
@@ -368,11 +375,16 @@ def cmd_start(spec_dir, extra):
368
375
  "--skip-setup", "--start"]):
369
376
  fail("启动失败,请查看上方输出")
370
377
 
378
+ # 机器可读标记(供 pi extension 解析;人类文字照常保留):watch= 供扩展
379
+ # 把观看命令**原样**预填进输入框(不做改写/转述)。
380
+ watch_cmd = f'!!python3 "{HUMAN_VIEWER}" {dir_path} --follow'
381
+ print(f"[start] dir={dir_path}")
382
+ print(f"[start] watch={watch_cmd}")
371
383
  print(f"""多视角分析已启动
372
384
  目录: {dir_path}
373
385
 
374
386
  观看分析(复制执行,不进 LLM;Ctrl-C 中断后可插话再续看):
375
- !!python3 "{HUMAN_VIEWER}" {dir_path} --follow
387
+ {watch_cmd}
376
388
 
377
389
  查看进展: {PROG} --view {dir_path}
378
390
  插话: {PROG} --say {dir_path} "<文本>"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Multi-perspective analysis for Pi: fork the main session into N perspective agents over the meeting protocol.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,141 +0,0 @@
1
- /**
2
- * multi-viewers-say —— 多视角分析插话命令(零 LLM 参与)
3
- *
4
- * 用户输入 `/multi-viewers-say <文本>` 时立即执行 human_sayer:
5
- * 把文本作为 human 消息注入当前分析(各视角 agent 可见、可回应)。
6
- * 不经过 LLM——命令 handler 直接 spawn human_sayer.py(一次调用一次返回),
7
- * 结果用 ctx.ui.notify 反馈。
8
- *
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
- * 未找到 → 报错提示。
16
- *
17
- * 前缀 mv- 与 pi-agents-helper 的 discuss-* 命名空间隔离(两个系统的
18
- * 插话命令都按"同 sid 最新目录"发现目标,共用前缀会互相插错)。
19
- *
20
- * 观看分析仍用 `!!` bash 流式(human_viewer --follow)——命令 API 无原生
21
- * 流式通道(handler 返回 Promise<void>),且 bash 流式是平台原生能力。
22
- */
23
-
24
- import { spawn } from "node:child_process";
25
- import * as fs from "node:fs";
26
- import * as path from "node:path";
27
- import { fileURLToPath } from "node:url";
28
-
29
- // ---- 自定位:import.meta.url 向上找包根(package.json name = 包名)----
30
- // 扩展从包内加载时(pi install / npm 安装 + symlink)零硬编码——
31
- // 包移到哪都能工作;复制安装(拆散包结构)时找不到包根 → 回退开发机路径。
32
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
- const PKG_NAME = "pi-multi-viewers";
34
-
35
- function findPackageRoot(start: string, pkgName: string): string | null {
36
- let dir = start;
37
- for (let i = 0; i < 8; i++) {
38
- try {
39
- const pkg = JSON.parse(
40
- fs.readFileSync(path.join(dir, "package.json"), "utf8"),
41
- );
42
- if (pkg.name === pkgName) return dir;
43
- } catch {
44
- // 继续向上
45
- }
46
- const parent = path.dirname(dir);
47
- if (parent === dir) break;
48
- dir = parent;
49
- }
50
- return null;
51
- }
52
-
53
- const PACKAGE_ROOT = findPackageRoot(__dirname, PKG_NAME);
54
- const SAYER = PACKAGE_ROOT
55
- ? path.join(PACKAGE_ROOT, "human_sayer.py")
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"; // 复制安装退化(开发机)
60
-
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;
72
- }
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
- });
87
- }
88
-
89
- /** 执行 human_sayer.py 一次插话。返回 { ok, output }。 */
90
- function runSayer(
91
- dir: string,
92
- text: string,
93
- ): Promise<{ ok: boolean; output: string }> {
94
- return new Promise((resolve) => {
95
- const proc = spawn("python3", [SAYER, dir, text], {
96
- stdio: ["ignore", "pipe", "pipe"],
97
- });
98
- let out = "";
99
- proc.stdout.on("data", (d) => (out += d.toString()));
100
- proc.stderr.on("data", (d) => (out += d.toString()));
101
- proc.on("close", (code) => resolve({ ok: code === 0, output: out.trim() }));
102
- proc.on("error", (e) => resolve({ ok: false, output: String(e) }));
103
- });
104
- }
105
-
106
- export default function register(pi: any) {
107
- pi.registerCommand("multi-viewers-say", {
108
- description: "向正在进行的多视角分析插话(human 消息,各视角可见可回应)",
109
- argumentHint: "<插话内容>",
110
- getArgumentCompletions: () => null,
111
- handler: async (args: string, ctx: any) => {
112
- const text = args.trim();
113
- if (!text) {
114
- ctx.ui.notify(
115
- "插话内容为空——用法: /multi-viewers-say <文本>",
116
- "warning",
117
- );
118
- return;
119
- }
120
- const sid = ctx.sessionManager.getSessionId();
121
- const dir = await findCurrentDir(ctx.cwd, sid);
122
- if (!dir) {
123
- ctx.ui.notify(
124
- "本 session 没有正在进行的多视角分析(cwd 下无 " +
125
- `mv-${sid}-* 分析环境)。先用 /multi-viewers 启动,` +
126
- "或改用 mv.sh --say <目录> \"<文本>\" 显式指定。",
127
- "error",
128
- );
129
- return;
130
- }
131
- const { ok, output } = await runSayer(dir, text);
132
- if (ok && output) {
133
- ctx.ui.notify(output, "success");
134
- } else if (ok) {
135
- ctx.ui.notify("插话已发送", "success");
136
- } else {
137
- ctx.ui.notify(`插话失败: ${output || "未知错误"}`, "error");
138
- }
139
- },
140
- });
141
- }
@@ -1,88 +0,0 @@
1
- ---
2
- description: 多视角协同分析——N 个视角 agent 携带主会话上下文(按预算裁剪+折叠)评审主题
3
- argument-hint: '"<主题>"'
4
- ---
5
-
6
- # Multi-Viewers(多视角协同分析)
7
-
8
- **分析主题:$1**(视角来自项目 `viewers/`,不在命令行指定)
9
-
10
- 这是用户手动触发的流程命令,不是分析内容。**严格按以下步骤执行**,
11
- 完成第 3 步后**结束当前回合**(不等待、不轮询、不转述全文)。
12
-
13
- ## 视角从哪来
14
-
15
- **唯一来源:项目 cwd 下的 `viewers/*.md`**(稳定视角资产——文件名即视角名,
16
- 写好长期复用;wrapper 会把它们快照进 spec,可按场微调)。这类资产属于
17
- 用户的项目,你**不要**擅自新建或改写它们。
18
-
19
- `viewers/` 还没建 → 见步骤 1 的失败分支。**不要**在命令行指定视角。
20
-
21
- ## 步骤
22
-
23
- ### 1. 生成 spec
24
-
25
- ```bash
26
- ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --prepare "<主题>"
27
- ```
28
-
29
- **这条命令非零退出 → 说明 spec 生成失败**(通常是项目缺可用视角:未建
30
- `viewers/`、视角少于 2 个、或视角文件为空)。此时**停下来问用户**,不要
31
- 自己选——读报错原文搞清原因,让他跑 `/multi-viewers-setup`(交互式建视角),
32
- 建好后再重跑本步骤。
33
-
34
- ### 2. 编辑并请用户审核 spec
35
-
36
- 编辑 spec 目录:
37
-
38
- - `question.md`(第二行起):**任务类型与产出形态**(审阅→提意见;
39
- 改进建议→说建议;修改→说明可改/需改范围)、分析对象、是否需要达成一致。
40
- 写得越明确,agent 越不会跑偏去做上下文里的其它事(它们的上下文里有
41
- 发起分析的完整对话历史)
42
- - `agents/X.md`(第二行起):**默认已是 viewers 的快照**——只在用户要求
43
- 按场微调时才改(如"这一场某视角更关注安全")
44
- - `background.md`(可选):只写**显式约定的边界**(如"不讨论 API 设计")。
45
- **不要**复述对话内容——fork 已让每个 agent 携带主会话上下文
46
- (默认 budget 模式:按预算裁剪+折叠)
47
- - `models.md`:一般不动(默认继承本机配置)
48
-
49
- 然后**展示 spec 路径,明确请用户查看/编辑**——用户确认"继续"才执行第 3 步。
50
-
51
- ## 视角设计原则
52
-
53
- 要建或按场微调 `viewers/*.md`、`agents/X.md` 时,**先读 `README.md` 的
54
- 「视角文件写什么」节**(三个要点 + 正误对照 + 命名规则)——那里是唯一事实源,
55
- 本节不再复述以免漂移。
56
-
57
- ### 3. 启动分析
58
-
59
- ```bash
60
- ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --start <spec目录绝对路径>
61
- ```
62
-
63
- **原样完整展示** wrapper 输出的观看命令(它是可复制执行的完整命令行——
64
- 不要改写、截断或转述)。
65
-
66
- ### 4. 结束回合
67
-
68
- 告知用户:
69
- - 观看:复制上一步的 `!!` 命令执行(实时流式,结束时自动退出并**附本次
70
- 分析报告**——消息数/墙钟/配额/冻结/进程跨度/LLM 用量;用户无需任何
71
- 额外操作即可看到)
72
- - 插话:随时 `/multi-viewers-say <文本>`(自动定位当前分析)
73
- - 完成时告诉主 pi,主 pi 会收尾
74
-
75
- **然后结束当前回合。**
76
-
77
- ## 收尾(用户驱动)
78
-
79
- ```bash
80
- mv.sh --status # 状态 + [result] 路径(目录可省略——自动定位当前分析)
81
- ```
82
-
83
- - `done`:读上一步打印的 `[result]` 路径(产物固定位;**路径由命令给出,
84
- 不要自己拼**)→ 向用户给**摘要** → `mv.sh --cleanup`(同样可省略目录)
85
- - cleanup 会再打印一次**分析报告**(删目录前最后一次可读)。报告在
86
- 观看输出末尾已自动出现过,**不必重复转述**——用户问起数字时按需引用
87
- - `stopped`:报告"分析已结束但未生成结果",不要继续等待
88
- - `running`:告知还在进行,继续等用户通知