pi-multi-viewers 0.5.1 → 0.7.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
@@ -28,8 +28,9 @@ human_sayer.py 【human 通道】插话命令(单次/stdin/交互 -i)
28
28
  scripts/mv.sh 稳定入口 shim(exec mv_cli.py;路径被 prompt/README 引用)
29
29
  scripts/pi-probe.sh LLM 探针(跑 pi + 登记新 session → 残留检查器可追溯)
30
30
  scripts/check-residue.sh 残留检查(session/进程/目录三类;增删 scripts/ 时同步本节)
31
- mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say)
32
- prompts/multi-viewers.md /multi-viewers 入口(视角设计三原则 + 审核闸门)
31
+ mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers)
32
+ prompts/multi-viewers.md /multi-viewers 分析入口(审核闸门;视角原则引 README,不复述)
33
+ prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→落盘→给审)
33
34
  extensions/multi-viewers-say/ /multi-viewers-say 插话(registerCommand,零 LLM)
34
35
  docs/design.md 设计文档(fork 源模式与规模口径 + 决策记录)
35
36
  package.json npm 包 pi-multi-viewers(pi.prompts 注册;**版本号唯一事实源**)
@@ -229,7 +230,7 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
229
230
 
230
231
  ## 安装/发版状态(2026-09-11)
231
232
 
232
- - **当前形态**:prompt × 1(multi-viewers,开发机已注册可用)+
233
+ - **当前形态**:prompt × 2(multi-viewers 分析 / multi-viewers-setup 建视角)+
233
234
  extension × 1(multi-viewers-say 插话:零 LLM,直接 spawn human_sayer.py;
234
235
  目录发现 = `<cwd>/mv-<sessionId>-*` 最新——**无兜底**:未匹配即报错
235
236
  rc 1,需显式传目录)
package/README.md CHANGED
@@ -68,11 +68,12 @@ ls ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh
68
68
  ## 用法
69
69
 
70
70
  ```
71
- /multi-viewers "<主题>" # prompt 入口(推荐;视角来自 viewers/)
71
+ /multi-viewers "<主题>" # 分析入口(推荐;视角来自 viewers/)
72
+ /multi-viewers-setup # 建视角入口(交互式:先建议 → 你定 → 落盘 → 给你审)
72
73
  /multi-viewers-say "<文本>" # 插话(extension:零 LLM 直接写入 human 消息)
73
74
  ```
74
75
 
75
- 两个 pi 命令入口(视角/主题走 prompt,插话走 extension——插话是"本地命令
76
+ 三个 pi 命令入口(分析/建视角走 prompt,插话走 extension——插话是"本地命令
76
77
  执行",不需要经过 LLM)。
77
78
 
78
79
  **目录可以省略**:`--view`/`--say`/`--status`/`--report`/`--wait`/`--cleanup`
@@ -93,6 +94,8 @@ ls viewers/
93
94
  scripts/mv.sh --prepare "<主题>" # spec = question.md(+background.md)
94
95
  scripts/mv.sh --start <spec目录> # 启动(自动挂载主 session;默认 budget 模式)
95
96
  # 可选:--fork-mode compaction|budget|full(见上表;一般不调)
97
+ # 可选:--max-meeting 15 --max-rr 7 --stall-timeout 600
98
+ # (配额:建环境时固化进 protocol.json,之后不可改;meeting 配额是"每 agent")
96
99
  # 可选:--extension-policy mc-tools|none|all(默认 mc-tools = agents 带 MC 的只读检索工具
97
100
  # ctx_search;none = 零扩展、零依赖;all = 走 pi 默认发现。缺 MC 时 mc-tools 可见降级)
98
101
  # 高级:--agents "a,b" 起一次性视角(不建 viewers/ 时用;prompt 入口不传它)
@@ -108,10 +111,15 @@ scripts/mv.sh --say "<文本>" # 插话(命令行形态;pi
108
111
  scripts/mv.sh --status # 状态 + 路径(取值与含义以该命令输出为准)
109
112
  scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM/档位对照;冷路径,不持久化)
110
113
  scripts/mv.sh --cleanup # 收尾(result.md 自动留存到 <dir>-result.md)
114
+ scripts/mv.sh --viewers # 列出+校验当前项目 viewers/(只读;建视角时用)
111
115
  ```
112
116
 
113
117
  ## 视角文件写什么(`viewers/<视角名>.md`)
114
118
 
119
+ 建视角**推荐**走 `/multi-viewers-setup`(交互式:先给候选建议 → 你定建哪几个 →
120
+ 落盘 → 展示给你审);也可以手写。写完用 `scripts/mv.sh --viewers` 自查(列出并
121
+ 用代码判据校验名字与空正文)。
122
+
115
123
  一个视角文件 = **一份视角说明**,纯内容、无格式要求(无 frontmatter、
116
124
  无需标题,**文件名就是全部元数据**)。三个要点(措辞经实验验证):
117
125
 
package/meeting_engine.py CHANGED
@@ -36,6 +36,7 @@ from meeting_fs import (
36
36
  read_protocol, cat_batch, remove_message, write_text, file_size,
37
37
  bare_of_base, bare_of_workdir, log,
38
38
  RESULT_MD, RESULT_MD_MIN_BYTES, DEFAULT_STALL_TIMEOUT,
39
+ DEFAULT_MAX_MEETING, DEFAULT_MAX_RR,
39
40
  )
40
41
  from meeting_core import (
41
42
  meeting_speak_count as core_meeting_speak_count,
@@ -503,7 +504,9 @@ def _commit_result_md(workdir, agent, subject):
503
504
  log(agent, "result.md 无改动——跳过 commit(幂等)")
504
505
 
505
506
 
506
- def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
507
+ def agent_loop(workdir, agent, responder,
508
+ max_meeting=DEFAULT_MAX_MEETING,
509
+ max_rr=DEFAULT_MAX_RR,
507
510
  poll_interval=POLL_INTERVAL,
508
511
  stall_timeout=DEFAULT_STALL_TIMEOUT):
509
512
  """主状态机(v2)。responder 注入:响应一轮并返回是否产出。
package/meeting_fs.py CHANGED
@@ -47,8 +47,12 @@ def result_path(base):
47
47
  """
48
48
  return f"{base}-{RESULT_MD}"
49
49
 
50
- # 无进展超时兜底(秒)——协议参数的默认值(gen_protocol 固化进
51
- # protocol.json;engine/fake_agent 的签名默认与 CLI default 同源于此)。
50
+ # 协议参数默认值(gen_protocol 固化进 protocol.json)——**唯一声明点**:
51
+ # CLI default、engine 签名默认、observability 的兜底读取都引用这里
52
+ # (此前 10/7 在三处各写一遍,改一处不改另一处就会漂移)。
53
+ DEFAULT_MAX_MEETING = 15 # meeting 阶段**每 agent** 发言配额
54
+ DEFAULT_MAX_RR = 7 # RR 阶段轮次配额
55
+ # 无进展超时兜底(秒)——同上(engine/fake_agent 的签名默认与 CLI default 同源)。
52
56
  DEFAULT_STALL_TIMEOUT = 600
53
57
 
54
58
  # thinking 档位缺省值——**唯一声明点**:models.md 的 variant 槽(缺省)
package/mv_cli.py CHANGED
@@ -28,6 +28,8 @@ import subprocess
28
28
  import sys
29
29
  from datetime import datetime
30
30
 
31
+ import spec_gen # --viewers 复用其单一判据(列举/名字/集合校验)
32
+
31
33
  HERE = os.path.dirname(os.path.abspath(__file__))
32
34
  PYTHON = os.environ.get("PYTHON") or "python3"
33
35
  START_DISCUSSION = os.path.join(HERE, "start_discussion.py")
@@ -43,18 +45,22 @@ USAGE = f"""用法:
43
45
  {PROG} --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
44
46
  {PROG} --start <spec目录> [--fork-mode compaction|budget|full]
45
47
  {PROG} --start <spec目录> [--extension-policy none|mc-tools|all]
48
+ {PROG} --start <spec目录> [--max-meeting N] [--max-rr N] [--stall-timeout S]
49
+ # 配额:建环境时固化进 protocol.json(默认 15 / 7 / 600)
46
50
  {PROG} --status [dir]
47
51
  {PROG} --report [dir]
48
52
  {PROG} --wait [dir]
49
53
  {PROG} --cleanup [dir]
50
54
  {PROG} --view [dir] [--since <ref>]
51
55
  {PROG} --say [dir] "<文本>"
56
+ {PROG} --viewers # 列出并校验当前项目的 viewers/(只读;建视角时用)
52
57
 
53
58
  消费命令的 <dir> 可省略(自动发现本 session 当前分析——按 cwd 下
54
59
  mv-<PI_SESSION_ID>-* 最新;无匹配则报错要求显式传目录)
55
60
 
56
61
  默认参数:
57
- agents=a,b,c max-meeting=10 max-rr=7 # 配额默认值的权威在 python argparse(本层不传)
62
+ agents=a,b,c max-meeting=15 max-rr=7 # 默认值的权威在 python argparse;
63
+ # 配额可在 --start 时传(建环境时固化,运行中不可改)
58
64
 
59
65
  --agents: 逗号分隔名称列表(如 "x,y")或纯数字(如 4 → 生成 a..d);
60
66
  human 是保留名,不能作为参与者
@@ -67,10 +73,23 @@ human 通道:
67
73
 
68
74
  def fail(msg):
69
75
  """错误出口(沿用 bash 约定:`错误: ` 前缀 + stderr + rc 1)。"""
76
+ sys.stdout.flush() # 已打印的正常输出先落地(stderr 无缓冲,否则会插到前面)
70
77
  print(f"错误: {msg}", file=sys.stderr)
71
78
  raise SystemExit(1)
72
79
 
73
80
 
81
+ def fail_verbatim(msg):
82
+ """错误文本**自带 `错误: ` 前缀**(来自 spec_gen 的单一判据)→ 原样输出。
83
+
84
+ 与 fail() 的差别只是前缀归属:判据的实现方(spec_gen)负责文案与前缀
85
+ (`start_discussion` 同样 `print(err)` 原样输出);再包一层会变成
86
+ "错误: 错误: …"(实测踩过)。
87
+ """
88
+ sys.stdout.flush()
89
+ print(msg, file=sys.stderr)
90
+ raise SystemExit(1)
91
+
92
+
74
93
  # ---------------------------------------------------------------
75
94
  # 目录参数:解析(显式优先 / 省略则自动发现)与校验
76
95
  # ---------------------------------------------------------------
@@ -127,6 +146,50 @@ def _call(cmd):
127
146
  # 消费命令
128
147
  # ---------------------------------------------------------------
129
148
 
149
+ def cmd_viewers(args):
150
+ """列出并校验**当前项目**的 `viewers/`(只读)。
151
+
152
+ 为什么需要它:视角是**长期资产**,创建/检查它的时刻通常**还没有任何分析
153
+ 目录**(消费命令的目录自动发现对此不适用——它找的是 mv-<sid>-*)。判据全部
154
+ 复用 `spec_gen` 的单一实现(列举 `list_agent_md` / 名字 `check_agent_name` /
155
+ 集合 `viewer_set_error`),这一层不另写一套规则。
156
+
157
+ 数量不足(<2)在这里是**提示**不是错误:建 1 个是合法的中间状态,只有启动
158
+ 一次分析时才要求 ≥2(那条判据仍由 `viewer_set_error` 独占)。
159
+ """
160
+ if args:
161
+ fail(f"未知参数: {' '.join(args)}(--viewers 不接受参数——只检查项目 cwd 的 viewers/)")
162
+ vdir = os.path.join(os.getcwd(), "viewers")
163
+ if not os.path.isdir(vdir):
164
+ fail(f"未找到 {vdir}——视角文件放在项目 cwd 的 viewers/<视角名>.md"
165
+ f"(文件名即视角名;可跑 /multi-viewers-setup 交互式建立)")
166
+ names, _briefs, empty = spec_gen._discover_viewers(vdir)
167
+ if not names:
168
+ fail(f"{vdir} 下没有 *.md——文件名即视角名(如 viewers/效率.md)")
169
+ empty_names = {n for n, _why in empty}
170
+ print(f"[viewers] {vdir}", flush=True) # 与 stderr 的错误行保序(管道下也如此)
171
+ for n in names:
172
+ notes = []
173
+ name_err = spec_gen.check_agent_name(n)
174
+ if name_err:
175
+ notes.append(f"文件名非法:{name_err}")
176
+ if n in empty_names:
177
+ notes.append("空:没有视角内容")
178
+ suffix = f"({';'.join(notes)})" if notes else ""
179
+ print(f" {n}.md{suffix}")
180
+ err = spec_gen.validate_participants(names)
181
+ if err:
182
+ fail_verbatim(err)
183
+ if empty:
184
+ fail_verbatim(spec_gen.viewer_set_error(names, empty))
185
+ gap = spec_gen.viewers_count_gap(names)
186
+ if gap:
187
+ print(f" 校验:{gap}(建 1 个是合法的中间状态)")
188
+ return 0
189
+ print(f" 校验:通过({len(names)} 个视角,名字与内容均合法)")
190
+ return 0
191
+
192
+
130
193
  def cmd_status(args):
131
194
  d = _dir_only("--status", args)
132
195
  return _call([PYTHON, START_DISCUSSION, "--dir", d, "--status"])
@@ -341,6 +404,8 @@ def main(argv=None):
341
404
  if not rest:
342
405
  fail("--start 需要 spec 目录参数")
343
406
  return cmd_start(rest[0], rest[1:])
407
+ if cmd == "--viewers":
408
+ return cmd_viewers(rest)
344
409
  if cmd == "--status":
345
410
  return cmd_status(rest)
346
411
  if cmd == "--report":
package/observability.py CHANGED
@@ -284,7 +284,7 @@ def build_report(base):
284
284
  # msgs 由上方流程段一次读取提供(同一读取派生四段)。
285
285
  lasts = {a: (msgs[a][-1] if msgs[a] else None) for a in agents}
286
286
  types = {a: (lasts[a].get("type") if lasts[a] else None) for a in agents}
287
- quota_meeting = proto.get("maxMeetingRounds", 10)
287
+ quota_meeting = proto.get("maxMeetingRounds", meeting_fs.DEFAULT_MAX_MEETING)
288
288
  quota_rr = proto.get("maxRRRounds", 7)
289
289
  out.append("配额:meeting " + "、".join(
290
290
  f"{a} {meeting_core.meeting_speak_count(msgs, a)}/{quota_meeting}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.5.1",
3
+ "version": "0.7.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,
@@ -0,0 +1,55 @@
1
+ ---
2
+ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建议 → 你定 → 落盘 → 给你审)
3
+ ---
4
+
5
+ # Multi-Viewers Setup(建立视角文件)
6
+
7
+ 这是**建立视角资产**的流程命令,不是分析内容。`viewers/<视角名>.md` 是**长期资产**
8
+ (写好长期复用)。本命令**只新建、不修改已有视角**。
9
+
10
+ ## 第 1 步:先看项目,再提**建议**(不落盘)
11
+
12
+ 机械地读项目(`README.md` / `AGENTS.md` / 代码结构)——据此给出 **3–5 个候选视角**,
13
+ 每个一行:
14
+
15
+ 1. <名字> —— <一行镜头>(与其它候选怎样互补/对立)
16
+
17
+ - 这一步**只提议**:不建文件、不改任何东西
18
+ - 名字:中文、短(≤32 字符)、不含空白与路径分隔符、**不要用 `human`**(保留名)
19
+ - 建几个**由用户决定**(只建 1 个也行)
20
+
21
+ **然后结束本回合,等用户输入。**
22
+
23
+ ## 第 2 步:等用户决定
24
+
25
+ 用户会告诉你建哪几个(可能只有一个、也可能点名不在候选里的)。**以用户输入为准**;
26
+ 若某个视角该用什么镜头没说清 → **问一句**,不要替他定。
27
+
28
+ ## 第 3 步:按用户输入建文件(只新建)
29
+
30
+ 写之前**先读 `README.md` 的「视角文件写什么」节**(唯一事实源:三个要点 + 正误对照 +
31
+ 命名规则)。要点:
32
+
33
+ - 纯内容、无 frontmatter、无标题——**文件名就是全部元数据**
34
+ - 必含三条:① **单一 lenses**(所有观点必须从该视角出发)② **不越界**(其它视角由
35
+ 别的参与者负责;**不要**列举是哪几个——参与者会变,列举会过期)③ **交锋义务**
36
+ (对其它视角的观点可认同或反驳,但要用本视角的论据)
37
+ - **只写视角本身**:身份("你是 X")、参与者名单、消息格式、独立参与者纪律都由脚本
38
+ 生成,**不要写进文件**
39
+
40
+ 写完跑一次只读检查(会列出视角并用代码判据校验名字与空正文):
41
+
42
+ ```bash
43
+ ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --viewers
44
+ ```
45
+
46
+ **已有同名文件 → 不覆盖**:告诉用户"该视角已存在",停下等他决定(改名,或他明确
47
+ 要求改动)。
48
+
49
+ ## 第 4 步:展示,等审阅
50
+
51
+ 把每个新建文件的**路径 + 全文**展示给用户,请他提修改意见或确认:
52
+
53
+ - 有意见 → 改 → **再展示**(反复直到他确认)
54
+ - 确认后一行交接:视角已建好(启动一次分析至少需要 2 个视角),接下来
55
+ `/multi-viewers "<主题>"` 即可
@@ -28,8 +28,8 @@ argument-hint: '"<主题>"'
28
28
 
29
29
  **这条命令非零退出 → 说明 spec 生成失败**(通常是项目缺可用视角:未建
30
30
  `viewers/`、视角少于 2 个、或视角文件为空)。此时**停下来问用户**,不要
31
- 自己选——读报错原文搞清原因,建议他按下面的"视角设计原则"补
32
- `viewers/<视角名>.md`(建好后再重跑本步骤)。
31
+ 自己选——读报错原文搞清原因,让他跑 `/multi-viewers-setup`(交互式建视角),
32
+ 建好后再重跑本步骤。
33
33
 
34
34
  ### 2. 编辑并请用户审核 spec
35
35
 
@@ -48,20 +48,11 @@ argument-hint: '"<主题>"'
48
48
 
49
49
  然后**展示 spec 路径,明确请用户查看/编辑**——用户确认"继续"才执行第 3 步。
50
50
 
51
- ## 视角设计原则(建 viewers/*.md 或临时 agents/X.md 时必读)
51
+ ## 视角设计原则
52
52
 
53
- 视角之间**互补或对立都可以**——对立产生的分歧正是多视角分析的价值。
54
- 每个视角任务书必须包含:
55
-
56
- 1. **单一 lenses**:明确该 agent 用什么角度看(效率/简单化/安全/……),
57
- 要求"所有观点必须从该视角出发"
58
- 2. **不越界**:"其它视角由别的参与者负责,你不要越界展开"(**不要**列举
59
- 具体是哪几个视角——参与者会变,列举会过期)
60
- 3. **交锋义务**:"对其它视角的观点可以认同或反驳,但要用本视角的论据"
61
-
62
- **只写视角本身**:身份("你是 X")、参与者名单、消息格式、独立参与者
63
- 纪律都由脚本生成(agent 名 = 文件名,单一来源)——任务书里**不要写**,
64
- 否则重复且易漂移。
53
+ 要建或按场微调 `viewers/*.md`、`agents/X.md` 时,**先读 `README.md` 的
54
+ 「视角文件写什么」节**(三个要点 + 正误对照 + 命名规则)——那里是唯一事实源,
55
+ 本节不再复述以免漂移。
65
56
 
66
57
  ### 3. 启动分析
67
58
 
package/spec_gen.py CHANGED
@@ -447,6 +447,19 @@ def validate_participants(participants):
447
447
  return None
448
448
 
449
449
 
450
+ def viewers_count_gap(names, where="viewers/"):
451
+ """视角数量不足的**事实句**(不带"错误:"前缀)→ 或 None。
452
+
453
+ 与 viewer_set_error 共用同一句话(那里把它包成错误):差别只在**场景语义**——
454
+ prepare/start 时数量不足 = 不能启动(错误);建视角时 1 个是**合法中间状态**
455
+ (只读检查 `mv.sh --viewers` 把它当提示,不判错)。
456
+ """
457
+ if len(names) < 2:
458
+ return (f"{where} 下仅发现 {len(names)} 个视角"
459
+ f"({', '.join(names)})——多视角分析至少需要 2 个")
460
+ return None
461
+
462
+
450
463
  def viewer_set_error(names, empty, where="viewers/"):
451
464
  """viewers 集合级校验(**唯一实现**):空正文视角 + 至少 2 个。
452
465
 
@@ -461,10 +474,8 @@ def viewer_set_error(names, empty, where="viewers/"):
461
474
  detail = "、".join(f"{where}{n}.md({why})" for n, why in empty)
462
475
  return (f"错误: {detail}——视角任务书不能为空"
463
476
  f"(写清该视角用什么 lenses 看分析对象)")
464
- if len(names) < 2:
465
- return (f"错误: {where} 下仅发现 {len(names)} 个视角"
466
- f"({', '.join(names)})——多视角分析至少需要 2 个")
467
- return None
477
+ gap = viewers_count_gap(names, where)
478
+ return f"错误: {gap}" if gap else None
468
479
 
469
480
 
470
481
  def _discover_viewers(viewers_dir):
@@ -5,7 +5,7 @@
5
5
  python3 start_discussion.py --dir mymeet --topic "主题" --agents a,b \
6
6
  [--stances '{"a": "立场1", "b": "立场2"}'] [--start] \
7
7
  [--extension-policy none|mc-tools|all] \
8
- [--models '{"a": "provider/model"}'] [--max-meeting 10] [--max-rr 7]
8
+ [--models '{"a": "provider/model"}'] [--max-meeting 15] [--max-rr 7]
9
9
 
10
10
  复杂内容用 spec 规格目录(设计 16,与 CLI 内容参数互斥):
11
11
  1. 生成骨架: python3 start_discussion.py --spec-gen myspec --agents a,b,c
@@ -532,7 +532,8 @@ def main():
532
532
  parser.add_argument("--questions", default=None, help="待回答问题(|分隔,对齐 RR)")
533
533
  parser.add_argument("--models", default=None, help='JSON: {"a": "provider/model"}')
534
534
  parser.add_argument("--result-writer", default=None, help="resultWriter(默认最后一位参与者)")
535
- parser.add_argument("--max-meeting", type=int, default=10, help="meeting 阶段发言配额(每 agent)")
535
+ parser.add_argument("--max-meeting", type=int, default=meeting_fs.DEFAULT_MAX_MEETING,
536
+ help="meeting 阶段发言配额(每 agent)")
536
537
  parser.add_argument("--max-rr", type=int, default=7, help="RR 阶段轮次配额(starter)")
537
538
  parser.add_argument("--stall-timeout", type=int,
538
539
  default=meeting_fs.DEFAULT_STALL_TIMEOUT,