pi-multi-viewers 0.2.1 → 0.3.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 +3 -2
- package/README.md +13 -6
- package/docs/design.md +42 -2
- package/docs/reviews/2026-09-11-e2e16-mechanization-review.md +177 -0
- package/docs/reviews/README.md +1 -0
- package/docs/test-methodology.md +86 -0
- package/extensions/multi-viewers-say/index.ts +39 -54
- package/fake_agent.py +11 -3
- package/human_viewer.py +3 -8
- package/meeting_fs.py +15 -0
- package/mv_cli.py +363 -0
- package/observability.py +81 -2
- package/package.json +1 -1
- package/prompts/multi-viewers.md +10 -8
- package/scripts/mv.sh +10 -318
- package/start_discussion.py +7 -1
package/mv_cli.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""mv_cli.py —— 多视角分析命令行(wrapper 的实际实现)。
|
|
3
|
+
|
|
4
|
+
**为什么这层是 Python**(原来是 364 行 bash,2026-09-12 收敛):
|
|
5
|
+
它曾住着参数解析(--prepare/--view/--say 三处)、目录名构造、spec 删除策略、
|
|
6
|
+
路径规范化与约 60 行用户文案。这层产出的真实 bug **全部出自 bash 陷阱**——
|
|
7
|
+
`shift` 吃掉显式目录(F5 回归)、`local` 重复声明清空变量、命令替换里的
|
|
8
|
+
`exit` 不进父 shell(双重错误消息)、参数静默丢弃(P0);而 bash 在本项目
|
|
9
|
+
工具链里**零行覆盖**(Python 覆盖率看不到它),缺口只能靠真实 e2e 或评审
|
|
10
|
+
暴露。收敛后:逻辑进入覆盖率与单测,解析用统一出口,不再有
|
|
11
|
+
shift/subshell/local 类陷阱。
|
|
12
|
+
|
|
13
|
+
`scripts/mv.sh` 保留为稳定入口 shim(prompt/README 引用该路径,不能变)。
|
|
14
|
+
|
|
15
|
+
**子进程边界**:环境创建/启动(start_discussion.py)、观看(human_viewer.py)、
|
|
16
|
+
插话(human_sayer.py)仍以子进程调用——它们是各自的 CLI 入口(argparse +
|
|
17
|
+
main),行为与 bash 版逐字一致;本模块只做「解析 → 决议 → 调用 → 展示」。
|
|
18
|
+
|
|
19
|
+
**退出码约定**(沿用 bash 版):无参数/参数不足 → usage + rc 2;
|
|
20
|
+
--help → usage + rc 0;其余错误 → `错误: ...` 到 stderr + rc 1;
|
|
21
|
+
子命令的业务退出码原样透传。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import fnmatch
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
|
|
31
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
PYTHON = os.environ.get("PYTHON") or "python3"
|
|
33
|
+
START_DISCUSSION = os.path.join(HERE, "start_discussion.py")
|
|
34
|
+
OBSERVABILITY = os.path.join(HERE, "observability.py")
|
|
35
|
+
HUMAN_VIEWER = os.path.join(HERE, "human_viewer.py")
|
|
36
|
+
HUMAN_SAYER = os.path.join(HERE, "human_sayer.py")
|
|
37
|
+
|
|
38
|
+
# 展示名:shim 传入调用路径(沿用 bash 的 `$0` 语义——usage 与启动块里显示
|
|
39
|
+
# 用户实际敲的那条命令);直接跑 mv_cli.py 时退回默认名。
|
|
40
|
+
PROG = os.environ.get("MV_INVOKED_AS") or "scripts/mv.sh"
|
|
41
|
+
|
|
42
|
+
USAGE = f"""用法:
|
|
43
|
+
{PROG} --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
|
|
44
|
+
{PROG} --start <spec目录> [--fork-mode compaction|budget|full]
|
|
45
|
+
{PROG} --status [dir]
|
|
46
|
+
{PROG} --report [dir]
|
|
47
|
+
{PROG} --wait [dir]
|
|
48
|
+
{PROG} --cleanup [dir]
|
|
49
|
+
{PROG} --view [dir] [--since <ref>]
|
|
50
|
+
{PROG} --say [dir] "<文本>"
|
|
51
|
+
|
|
52
|
+
消费命令的 <dir> 可省略(自动发现本 session 当前分析——按 cwd 下
|
|
53
|
+
mv-<PI_SESSION_ID>-* 最新;无匹配则报错要求显式传目录)
|
|
54
|
+
|
|
55
|
+
默认参数:
|
|
56
|
+
agents=a,b,c max-meeting=10 max-rr=7 # 配额默认值的权威在 python argparse(本层不传)
|
|
57
|
+
|
|
58
|
+
--agents: 逗号分隔名称列表(如 "x,y")或纯数字(如 4 → 生成 a..d);
|
|
59
|
+
human 是保留名,不能作为参与者
|
|
60
|
+
|
|
61
|
+
human 通道:
|
|
62
|
+
--view: 增量查看分析进展(--since 之后的新消息+状态;末尾输出 HEAD=<hash>,主 pi 记录作下轮 --since)
|
|
63
|
+
--say: 插话(写一条 human 消息,agents 可见并可回应)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fail(msg):
|
|
68
|
+
"""错误出口(沿用 bash 约定:`错误: ` 前缀 + stderr + rc 1)。"""
|
|
69
|
+
print(f"错误: {msg}", file=sys.stderr)
|
|
70
|
+
raise SystemExit(1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------
|
|
74
|
+
# 目录参数:解析(显式优先 / 省略则自动发现)与校验
|
|
75
|
+
# ---------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def normalize_dir(path):
|
|
78
|
+
"""目录参数规范化:裸名(无路径符)会被 start_discussion 当成相对路径
|
|
79
|
+
找错目录(实测 2026-09-03)——所有消费命令入口统一转绝对路径。"""
|
|
80
|
+
return os.path.realpath(path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_dir(arg):
|
|
84
|
+
"""显式参数优先;**省略则自动发现**(observability `--find-dir` 单一实现,
|
|
85
|
+
extension 同入口)。失败原因由那个 python 进程给出(错误文案留一处,
|
|
86
|
+
e2e16 F7):stderr 直通、退出码原样透传。
|
|
87
|
+
|
|
88
|
+
为什么允许省略:唯一知道路径的是 `--start` 的输出,此前消费命令都要求
|
|
89
|
+
把它传给每个命令,等于让调用方(主 pi)把长绝对路径记在 LLM 上下文里
|
|
90
|
+
再复用——改错/截断/相对路径都出过。
|
|
91
|
+
"""
|
|
92
|
+
if arg:
|
|
93
|
+
return normalize_dir(arg)
|
|
94
|
+
r = subprocess.run([PYTHON, OBSERVABILITY, "--find-dir"],
|
|
95
|
+
stdout=subprocess.PIPE, text=True)
|
|
96
|
+
if r.returncode != 0:
|
|
97
|
+
raise SystemExit(r.returncode)
|
|
98
|
+
return normalize_dir(r.stdout.strip())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def require_dir(d):
|
|
102
|
+
if not d:
|
|
103
|
+
fail("缺少目录参数")
|
|
104
|
+
if not os.path.isdir(d):
|
|
105
|
+
fail(f"目录不存在: {d}")
|
|
106
|
+
return d
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _dir_only(command, args):
|
|
110
|
+
"""status/report/wait/cleanup 的公共入口:只接受 `[dir]`。
|
|
111
|
+
|
|
112
|
+
多余参数**响亮失败**而非静默忽略(无静默铁律 / P0:wrapper 不得静默
|
|
113
|
+
丢弃参数)。
|
|
114
|
+
"""
|
|
115
|
+
if len(args) > 1:
|
|
116
|
+
fail(f"未知参数: {' '.join(args[1:])}({command} 只接受 [dir])")
|
|
117
|
+
return require_dir(resolve_dir(args[0] if args else ""))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _call(cmd):
|
|
121
|
+
"""子进程透传(stdout/stderr 直通,退出码原样返回)。"""
|
|
122
|
+
return subprocess.run(cmd).returncode
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------
|
|
126
|
+
# 消费命令
|
|
127
|
+
# ---------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def cmd_status(args):
|
|
130
|
+
d = _dir_only("--status", args)
|
|
131
|
+
return _call([PYTHON, START_DISCUSSION, "--dir", d, "--status"])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_report(args):
|
|
135
|
+
d = _dir_only("--report", args)
|
|
136
|
+
return _call([PYTHON, START_DISCUSSION, "--dir", d, "--report"])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_wait(args):
|
|
140
|
+
d = _dir_only("--wait", args)
|
|
141
|
+
return _call([PYTHON, START_DISCUSSION, "--dir", d, "--wait"])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def cmd_cleanup(args):
|
|
145
|
+
d = _dir_only("--cleanup", args)
|
|
146
|
+
return _call([PYTHON, START_DISCUSSION, "--dir", d, "--cleanup"])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_view(args):
|
|
150
|
+
"""增量查看 + 输出 HEAD 游标。
|
|
151
|
+
|
|
152
|
+
首参判形:非空且非 `--since` → 显式目录(与 status/report/wait/cleanup
|
|
153
|
+
的 `resolve_dir(arg)` 同一判据;F5 回归教训:无条件 shift 会让显式目录
|
|
154
|
+
被静默丢弃)。与 `--say` 的区别:`--say` 按**参数个数**区分(文本可含
|
|
155
|
+
空格或以 `--` 开头,不能按值判形);`--view` 的 `--since` 是本命令自己
|
|
156
|
+
的选项名,可以按值判形。
|
|
157
|
+
"""
|
|
158
|
+
since = None
|
|
159
|
+
if args and args[0] != "--since":
|
|
160
|
+
d = require_dir(resolve_dir(args[0]))
|
|
161
|
+
args = args[1:]
|
|
162
|
+
else:
|
|
163
|
+
d = require_dir(resolve_dir(""))
|
|
164
|
+
i = 0
|
|
165
|
+
while i < len(args):
|
|
166
|
+
if args[i] == "--since":
|
|
167
|
+
if i + 1 >= len(args):
|
|
168
|
+
fail("--since 需要一个值")
|
|
169
|
+
since = args[i + 1]
|
|
170
|
+
i += 2
|
|
171
|
+
else:
|
|
172
|
+
fail(f"未知参数: {args[i]}(--view 只接受 --since)")
|
|
173
|
+
if not os.path.isdir(os.path.join(d, "repo.git")):
|
|
174
|
+
fail(f"分析不存在: {d}(无 repo.git)")
|
|
175
|
+
cmd = [PYTHON, HUMAN_VIEWER, d] + (["--since", since] if since else [])
|
|
176
|
+
rc = _call(cmd)
|
|
177
|
+
if rc:
|
|
178
|
+
# viewer 失败(坏 ref / 仓库异常)→ **不打印游标**:失败时给出 HEAD
|
|
179
|
+
# 会让下一次 --since 静默跳过消息(bash 版把 rc 吞成 0 且照打游标,
|
|
180
|
+
# 属"静默失败"一类;此处改为响亮失败)
|
|
181
|
+
return rc
|
|
182
|
+
head = subprocess.run(["git", "-C", os.path.join(d, "repo.git"),
|
|
183
|
+
"rev-parse", "HEAD"],
|
|
184
|
+
stdout=subprocess.PIPE, text=True,
|
|
185
|
+
stderr=subprocess.DEVNULL).stdout.strip()
|
|
186
|
+
print(f"HEAD={head}")
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_say(args):
|
|
191
|
+
"""插话。两种形态**按参数个数**区分(不按值猜语义):
|
|
192
|
+
|
|
193
|
+
`--say <dir> "<文本>"` 显式目录
|
|
194
|
+
`--say "<文本>"` 目录自动发现
|
|
195
|
+
|
|
196
|
+
多于 2 个参数**响亮失败**(bash 版静默忽略多余参数——未加引号的文本
|
|
197
|
+
会被当成目录,行为不可预期;此处直接报错并提示加引号)。
|
|
198
|
+
"""
|
|
199
|
+
if len(args) > 2:
|
|
200
|
+
fail("--say 接受 1 个参数(文本)或 2 个(目录 文本)——"
|
|
201
|
+
"文本含空格请加引号")
|
|
202
|
+
if len(args) == 2:
|
|
203
|
+
d = require_dir(resolve_dir(args[0]))
|
|
204
|
+
text = args[1]
|
|
205
|
+
else:
|
|
206
|
+
d = require_dir(resolve_dir(""))
|
|
207
|
+
text = args[0] if args else ""
|
|
208
|
+
if not os.path.isdir(os.path.join(d, "work-human")):
|
|
209
|
+
fail(f"分析缺少 work-human: {d}")
|
|
210
|
+
if not text:
|
|
211
|
+
fail("插话文本不能为空")
|
|
212
|
+
return _call([PYTHON, HUMAN_SAYER, d, text])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------
|
|
216
|
+
# 创建类命令
|
|
217
|
+
# ---------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def cmd_prepare(args):
|
|
220
|
+
"""生成 spec 骨架(`--spec-gen` 是唯一实现,本层只解析参数 + 展示)。"""
|
|
221
|
+
if not args:
|
|
222
|
+
print(USAGE, file=sys.stderr)
|
|
223
|
+
return 2
|
|
224
|
+
topic = args[0]
|
|
225
|
+
background = None
|
|
226
|
+
agents_list = None
|
|
227
|
+
i = 1
|
|
228
|
+
while i < len(args):
|
|
229
|
+
a = args[i]
|
|
230
|
+
if a == "--background":
|
|
231
|
+
if i + 1 >= len(args):
|
|
232
|
+
fail("--background 需要一个值")
|
|
233
|
+
background = args[i + 1]
|
|
234
|
+
i += 2
|
|
235
|
+
elif a == "--agents":
|
|
236
|
+
if i + 1 >= len(args):
|
|
237
|
+
fail("--agents 需要一个值(逗号分隔名称列表或数字)")
|
|
238
|
+
agents_list = args[i + 1]
|
|
239
|
+
i += 2
|
|
240
|
+
elif a in ("-h", "--help"):
|
|
241
|
+
print(USAGE)
|
|
242
|
+
return 0
|
|
243
|
+
else:
|
|
244
|
+
fail(f"未知参数: {a}(--prepare 只接受 <问题>、--background、--agents)")
|
|
245
|
+
if not topic:
|
|
246
|
+
fail("问题不能为空")
|
|
247
|
+
|
|
248
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
249
|
+
spec_dir = os.path.join(os.getcwd(), f"mv-spec-{stamp}")
|
|
250
|
+
# 目录不预创建——python 侧校验失败 = 零产物(预创建会让"不合规零产物"
|
|
251
|
+
# 失效,实测 2026-09-09;成功时 gen_spec_skeleton 自建)。
|
|
252
|
+
# agents 缺省 = viewers 快照模式:校验与快照都在 python 侧
|
|
253
|
+
# (_snapshot_viewers;不合规根本不应该开始 spec-gen)。
|
|
254
|
+
cmd = [PYTHON, START_DISCUSSION, "--spec-gen", spec_dir, "--topic", topic]
|
|
255
|
+
if background:
|
|
256
|
+
cmd += ["--background", background]
|
|
257
|
+
if agents_list:
|
|
258
|
+
cmd += ["--agents", agents_list]
|
|
259
|
+
if _call(cmd):
|
|
260
|
+
fail("spec 骨架生成失败(start_discussion --spec-gen,见上方错误)")
|
|
261
|
+
print(f"""已生成分析 spec:
|
|
262
|
+
{spec_dir}
|
|
263
|
+
|
|
264
|
+
请查看/编辑该目录,补充背景、各 agent 视角等。
|
|
265
|
+
编辑完成后,告诉我"继续",我会自动启动分析。""")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cmd_start(spec_dir, extra):
|
|
270
|
+
"""创建分析环境 + 启动:两段调用 start_discussion(创建 → --start)。
|
|
271
|
+
|
|
272
|
+
中间夹一步 spec 处理:**只删本工具生成的形态**(`mv-spec-*`),用户自建
|
|
273
|
+
的 spec 目录不动(可能是有价值的视角快照)。删除前提示一行——spec 是
|
|
274
|
+
视角任务书/背景/models 在用户侧的唯一副本,删了不可恢复。
|
|
275
|
+
"""
|
|
276
|
+
require_dir(spec_dir)
|
|
277
|
+
if not os.path.isfile(os.path.join(spec_dir, "question.md")):
|
|
278
|
+
fail(f"spec 缺少 question.md: {spec_dir}")
|
|
279
|
+
|
|
280
|
+
session_id = os.environ.get("PI_SESSION_ID", "")
|
|
281
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
282
|
+
# 目录前缀 mv-:与 pi-agents-helper 的 discuss-<sid>-* **命名空间隔离**
|
|
283
|
+
# ——两个系统的 extension 都按"同 sid 最新目录"发现目标,共用前缀会在
|
|
284
|
+
# 同 session 并发两套时互相插错(且 human 消息格式兼容 → 静默写错)。
|
|
285
|
+
dir_name = "mv" + (f"-{session_id}" if session_id else "") + f"-{stamp}"
|
|
286
|
+
dir_path = os.path.join(os.getcwd(), dir_name)
|
|
287
|
+
|
|
288
|
+
# 第 1 步:创建分析环境(不启动;fork 源由 python 侧 resolve_fork_source
|
|
289
|
+
# 自动解析)。余参原样转发(--fork-mode 等):值识别与默认值的唯一家在
|
|
290
|
+
# python argparse,本层不解析、不丢弃。
|
|
291
|
+
if _call([PYTHON, START_DISCUSSION, "--dir", dir_path,
|
|
292
|
+
"--spec", spec_dir] + extra):
|
|
293
|
+
fail("环境创建失败,请查看上方输出")
|
|
294
|
+
|
|
295
|
+
if fnmatch.fnmatch(os.path.basename(os.path.normpath(spec_dir)),
|
|
296
|
+
"mv-spec-*"):
|
|
297
|
+
print(f"[start] spec 已消费,删除(本工具生成形态): {spec_dir}")
|
|
298
|
+
shutil.rmtree(spec_dir, ignore_errors=True)
|
|
299
|
+
else:
|
|
300
|
+
print(f"[start] spec 已消费(保留未删——非 mv-spec-* 形态): {spec_dir}")
|
|
301
|
+
|
|
302
|
+
# 第 2 步:启动已有环境
|
|
303
|
+
if _call([PYTHON, START_DISCUSSION, "--dir", dir_path,
|
|
304
|
+
"--skip-setup", "--start"]):
|
|
305
|
+
fail("启动失败,请查看上方输出")
|
|
306
|
+
|
|
307
|
+
print(f"""多视角分析已启动
|
|
308
|
+
目录: {dir_path}
|
|
309
|
+
|
|
310
|
+
观看分析(复制执行,不进 LLM;Ctrl-C 中断后可插话再续看):
|
|
311
|
+
!!python3 "{HUMAN_VIEWER}" {dir_path} --follow
|
|
312
|
+
|
|
313
|
+
查看进展: {PROG} --view {dir_path}
|
|
314
|
+
插话: {PROG} --say {dir_path} "<文本>"
|
|
315
|
+
查看状态: {PROG} --status {dir_path}
|
|
316
|
+
等待完成: {PROG} --wait {dir_path}
|
|
317
|
+
清理: {PROG} --cleanup {dir_path}
|
|
318
|
+
|
|
319
|
+
说明:
|
|
320
|
+
- human 通道:--view 增量查看(主 pi 记录末尾 HEAD 作下轮 --since);
|
|
321
|
+
--say 插话(agents 可见并可回应;已冻结 agent 不响应)
|
|
322
|
+
- 完成后 result.md 自动保存到固定位置:{dir_path}-result.md(与分析目录同级——resultWriter loop 退出时保存;cleanup 也会保存)
|
|
323
|
+
- 读取 result.md 摘要后请执行 --cleanup 清理分析目录""")
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------
|
|
328
|
+
# 分发
|
|
329
|
+
# ---------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def main(argv=None):
|
|
332
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
333
|
+
if not argv:
|
|
334
|
+
print(USAGE, file=sys.stderr)
|
|
335
|
+
return 2
|
|
336
|
+
cmd, rest = argv[0], argv[1:]
|
|
337
|
+
if cmd == "--prepare":
|
|
338
|
+
return cmd_prepare(rest)
|
|
339
|
+
if cmd == "--start":
|
|
340
|
+
if not rest:
|
|
341
|
+
fail("--start 需要 spec 目录参数")
|
|
342
|
+
return cmd_start(rest[0], rest[1:])
|
|
343
|
+
if cmd == "--status":
|
|
344
|
+
return cmd_status(rest)
|
|
345
|
+
if cmd == "--report":
|
|
346
|
+
return cmd_report(rest)
|
|
347
|
+
if cmd == "--wait":
|
|
348
|
+
return cmd_wait(rest)
|
|
349
|
+
if cmd == "--cleanup":
|
|
350
|
+
return cmd_cleanup(rest)
|
|
351
|
+
if cmd == "--view":
|
|
352
|
+
return cmd_view(rest)
|
|
353
|
+
if cmd == "--say":
|
|
354
|
+
return cmd_say(rest)
|
|
355
|
+
if cmd in ("-h", "--help"):
|
|
356
|
+
print(USAGE)
|
|
357
|
+
return 0
|
|
358
|
+
print(USAGE, file=sys.stderr)
|
|
359
|
+
return 2
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
sys.exit(main())
|
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)
|
|
@@ -297,7 +341,9 @@ def build_report(base):
|
|
|
297
341
|
out.append(" n/a(session 缺失,或无本轮数据——边界条目自 2026-09-11 "
|
|
298
342
|
"起写入,此前的老分析不适用)")
|
|
299
343
|
out.append("(口径:进程跨度=pi 进程生命周期;输出=prompt 分段合计;"
|
|
300
|
-
"墙钟=commit
|
|
344
|
+
"墙钟=commit 时间差——三者不可互替;"
|
|
345
|
+
"消息数含流程信号(freezing/pass/concluded)与 human,"
|
|
346
|
+
"配额只计 meeting 发言)")
|
|
301
347
|
return out
|
|
302
348
|
|
|
303
349
|
|
|
@@ -395,3 +441,36 @@ def _num(n):
|
|
|
395
441
|
if n >= 1_000:
|
|
396
442
|
return f"{n / 1_000:.1f}k"
|
|
397
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
package/prompts/multi-viewers.md
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
**这条命令非零退出 → 说明 spec 生成失败**(通常是项目缺可用视角:未建
|
|
30
|
+
`viewers/`、视角少于 2 个、或视角文件为空)。此时**停下来问用户**,不要
|
|
31
|
+
自己选——读报错原文搞清原因,建议他按下面的"视角设计原则"补
|
|
32
|
+
`viewers/<视角名>.md`(建好后再重跑本步骤)。
|
|
32
33
|
|
|
33
34
|
### 2. 编辑并请用户审核 spec
|
|
34
35
|
|
|
@@ -68,7 +69,8 @@ argument-hint: '"<主题>"'
|
|
|
68
69
|
~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --start <spec目录绝对路径>
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
**原样完整展示** wrapper 输出的观看命令(它是可复制执行的完整命令行——
|
|
73
|
+
不要改写、截断或转述)。
|
|
72
74
|
|
|
73
75
|
### 4. 结束回合
|
|
74
76
|
|
|
@@ -76,7 +78,7 @@ argument-hint: '"<主题>"'
|
|
|
76
78
|
- 观看:复制上一步的 `!!` 命令执行(实时流式,结束时自动退出并**附本次
|
|
77
79
|
分析报告**——消息数/墙钟/配额/冻结/进程跨度/LLM 用量;用户无需任何
|
|
78
80
|
额外操作即可看到)
|
|
79
|
-
- 插话:随时 `/multi-viewers-say
|
|
81
|
+
- 插话:随时 `/multi-viewers-say <文本>`(自动定位当前分析)
|
|
80
82
|
- 完成时告诉主 pi,主 pi 会收尾
|
|
81
83
|
|
|
82
84
|
**然后结束当前回合。**
|
|
@@ -84,11 +86,11 @@ argument-hint: '"<主题>"'
|
|
|
84
86
|
## 收尾(用户驱动)
|
|
85
87
|
|
|
86
88
|
```bash
|
|
87
|
-
mv.sh --status
|
|
89
|
+
mv.sh --status # done/stopped/running(目录可省略——自动定位当前分析)
|
|
88
90
|
```
|
|
89
91
|
|
|
90
|
-
- `done
|
|
91
|
-
|
|
92
|
+
- `done`:读上一步打印的 `[result]` 路径(产物固定位;**路径由命令给出,
|
|
93
|
+
不要自己拼**)→ 向用户给**摘要** → `mv.sh --cleanup`(同样可省略目录)
|
|
92
94
|
- cleanup 会再打印一次**分析报告**(删目录前最后一次可读)。报告在
|
|
93
95
|
观看输出末尾已自动出现过,**不必重复转述**——用户问起数字时按需引用
|
|
94
96
|
- `stopped`:报告"分析已结束但未生成结果",不要继续等待
|