pi-multi-viewers 0.2.2 → 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/docs/design.md +15 -1
- package/docs/test-methodology.md +48 -0
- package/fake_agent.py +11 -3
- package/mv_cli.py +363 -0
- package/package.json +1 -1
- package/scripts/mv.sh +10 -357
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,8 @@ spec_gen.py spec 生成层(question/骨架/viewers 校验与快照/ag
|
|
|
23
23
|
observability.py 观测层(check_status / --report / --wait / loop 存活检测)
|
|
24
24
|
human_viewer.py 【human 通道】只读展示(增量/--follow/游标)
|
|
25
25
|
human_sayer.py 【human 通道】插话命令(单次/stdin/交互 -i)
|
|
26
|
-
scripts/mv.sh
|
|
26
|
+
scripts/mv.sh 稳定入口 shim(exec mv_cli.py;路径被 prompt/README 引用)
|
|
27
|
+
mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say)
|
|
27
28
|
prompts/multi-viewers.md /multi-viewers 入口(视角设计三原则 + 审核闸门)
|
|
28
29
|
extensions/multi-viewers-say/ /multi-viewers-say 插话(registerCommand,零 LLM)
|
|
29
30
|
docs/design.md 设计文档(fork 源模式与规模口径 + 决策记录)
|
|
@@ -176,7 +177,7 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
|
|
|
176
177
|
extension × 1(multi-viewers-say 插话:零 LLM,直接 spawn human_sayer.py;
|
|
177
178
|
目录发现 = `<cwd>/mv-<sessionId>-*` 最新,兜底 `mv-*`(排除
|
|
178
179
|
`mv-spec-*`)并警告)
|
|
179
|
-
+ wrapper。**npm 已发布 0.
|
|
180
|
+
+ wrapper。**npm 已发布 0.3.0(2026-09-12)**。
|
|
180
181
|
- **开发机安装(两步,缺一不可;2026-09-10 实测)**:
|
|
181
182
|
① `pi install /root/pi-multi-viewers`——**注册包**(写
|
|
182
183
|
`~/.pi/agent/settings.json` 的 `packages` 数组);pi 不是"扫 node_modules
|
package/docs/design.md
CHANGED
|
@@ -308,7 +308,21 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
|
|
|
308
308
|
一改就静默失配(LLM 会以为没报错而继续)。改为"命令非零退出 → 停下来
|
|
309
309
|
读报错原文问用户":判据降为退出码(wrapper 的 `fail()` 保证 `exit 1`),
|
|
310
310
|
原因解释交还给输出原文。
|
|
311
|
-
17. **
|
|
311
|
+
17. **CLI 层 = Python(bash 只留 shim)**:`scripts/mv.sh` 是 5 行 shim
|
|
312
|
+
(找到仓库根 → `exec python3 mv_cli.py "$@"`),解析/决议/调用/展示全在
|
|
313
|
+
`mv_cli.py`。**路径 `scripts/mv.sh` 不变**(prompt/README/用户习惯引用
|
|
314
|
+
它)。收敛依据不是"更整洁",而是这层产出的真实 bug **全部出自 bash
|
|
315
|
+
陷阱**(`shift` 吃掉显式目录 = F5 回归、`local` 重复声明清空变量、命令
|
|
316
|
+
替换里的 `exit` 不进父 shell 致双重错误消息、参数静默丢弃 = P0),且
|
|
317
|
+
bash 在本项目工具链里**零行覆盖**(Python 覆盖率看不到它)——缺口只能靠
|
|
318
|
+
真实 e2e 或评审暴露。收敛后逻辑进入覆盖率与进程内单测
|
|
319
|
+
(tests/test_mv_cli.py),解析统一出口。子进程边界不变:start_discussion
|
|
320
|
+
/ human_viewer / human_sayer 仍以子进程调用(各自是 CLI 入口)。
|
|
321
|
+
顺带三处"无静默"加固(bash 版是静默的):status/report/wait/cleanup 的
|
|
322
|
+
多余参数响亮失败、`--say` 多于 2 个参数响亮失败(未加引号文本会被当成
|
|
323
|
+
目录)、`--view` 的 viewer 失败透传 rc 且**不打印 HEAD 游标**(给失败的
|
|
324
|
+
一轮发游标会让下一次 `--since` 静默跳过消息)。
|
|
325
|
+
18. **git 守卫范围 = 从讨论 workdir 发起的操作**(`GIT_CEILING_DIRECTORIES`
|
|
312
326
|
注入于 spawn);主项目仓库不在守卫范围(agent 的 cwd 就是主项目,其
|
|
313
327
|
约束归指令层 + 主项目 `.gitignore`)。要拦主仓库需换机制类(沙箱/钩子),
|
|
314
328
|
经评估收益不支撑扩面。
|
package/docs/test-methodology.md
CHANGED
|
@@ -317,3 +317,51 @@ python 时被"顺手改写"为含斜杠则不拼——**移植不是重写**:
|
|
|
317
317
|
2. 需要走 wrapper 链路时,验证完**立即**:停 loop(`pkill` 按目录精确匹配)
|
|
318
318
|
→ `--cleanup` → `check-residue.sh` 复核。
|
|
319
319
|
3. 判据:"这次验证会不会 spawn pi/loop?"——会,就必须先想好怎么停。
|
|
320
|
+
|
|
321
|
+
### 22. 契约写成矩阵表:命令 × 形态 × 判别力(2026-09-11 e2e16 F5)
|
|
322
|
+
|
|
323
|
+
**背景(F5 回归)**:改 `cmd_view` 时只测"新形态"(目录可省略),
|
|
324
|
+
`--view <dir>` 的显式形态**改动前后都没有测试**(旧测试测的是"无参 → 报错"
|
|
325
|
+
的错误分支);395 个测试全绿没挡住——真实多视角评审(e2e16)才抓出。
|
|
326
|
+
|
|
327
|
+
**诊断(不是"漏写一条用例",是结构性缺口)**:
|
|
328
|
+
1. wrapper 测试按功能增量堆叠,没有"命令 × 形态"的系统性盘点;
|
|
329
|
+
2. **bash 层无行覆盖工具**——缺口不出现在覆盖率里(实测 Python 层 96%,
|
|
330
|
+
wrapper 测不到一行);`--wait` 零测试、`--cleanup` 省略形态零测试;
|
|
331
|
+
3. 旧测试用**弱断言**(仅 `assertNotEqual(rc, 0)`)——契约废除后静默变
|
|
332
|
+
空洞:`test_view_requires_dir` 的"目录必传"契约已不存在,它只因测试
|
|
333
|
+
cwd 里没有分析环境才继续绿,看起来在覆盖 `--view` 实则什么都没测。
|
|
334
|
+
|
|
335
|
+
**方法**:
|
|
336
|
+
1. **契约 = 一张表**(`tests/test_wrapper.py::TestCommandFormMatrix`):
|
|
337
|
+
每行一个形态单元格 = `(args, 前置条件, 期望 rc, 输出须含, 副作用)`。
|
|
338
|
+
新增命令/形态必须补行——**结构性强制,不靠记忆**。
|
|
339
|
+
2. **每格三件事**:退出码 + **输出内容** + **副作用/文件效果**;
|
|
340
|
+
禁止仅断言退出码(弱断言是僵尸测试的温床)。
|
|
341
|
+
3. **判别力优先于覆盖率**:显式参数格子必须能区分"用了显式值"与"恰好
|
|
342
|
+
自动发现同一个"。做法:加**诱饵**(更晚时间戳 + 状态不同 `done` vs
|
|
343
|
+
`stopped` + 独有标记)——两个同构环境的 HEAD/输出完全相同,无诱饵时
|
|
344
|
+
F5 类 bug 照样绿(本次实测:加诱饵后重引入 F5,矩阵立即失败并报
|
|
345
|
+
"走了自动发现(选到诱饵环境)")。
|
|
346
|
+
4. **改写旧测试 ≠ 补充**:契约变更时先问"它测的是哪条既有契约、那条契约
|
|
347
|
+
还有效吗"——有效则必须补等价用例;无效则删除并在表里补新形态格。
|
|
348
|
+
|
|
349
|
+
### 23. 概率性断言 = 测试设计缺陷(2026-09-12 test_n_agents 随机红)
|
|
350
|
+
|
|
351
|
+
**背景**:`test_four_agent_cascade` 断言"N=4 应有 message 发言"。FakeAgent 在
|
|
352
|
+
meeting 阶段有 35% 概率写 freezing,而"全员首轮 freezing"概率 = 0.35⁴ =
|
|
353
|
+
**1.50%**——此时测试必失败(N=5 时 0.53%)。实测约每 10 次全量跑红一次。
|
|
354
|
+
|
|
355
|
+
**定性**:**测试装置问题,不是产品问题**——全员首轮 freezing 是合法协议路径
|
|
356
|
+
(引擎照常走完 af → RR → pass → concluded)。但断言把"典型路径"当成不变式了。
|
|
357
|
+
|
|
358
|
+
**方法**:
|
|
359
|
+
1. **测试可以断言不变式,不可以断言分布尾部**:写断言前先问"这个结论在
|
|
360
|
+
装置的随机性下是否恒成立"。不成立 → 断言错,或装置要改。
|
|
361
|
+
2. **修装置优于挑种子**:本次修法是让 FakeAgent **首轮必写 message**——
|
|
362
|
+
理由是**装置对齐生产**(真实 wake prompt 对首位发言者的指令就是"直接产出
|
|
363
|
+
你的第一条视角分析"),而不是"把概率调到能过"。挑 seed 只治一次,改对齐
|
|
364
|
+
治一类。
|
|
365
|
+
3. **确定性优先于概率**:修复后该断言成为**不变式**(P = 0 不可能失败),
|
|
366
|
+
不再依赖运气——这比"多跑几次都绿"才是证据(10 次全绿对 1.5% 概率的
|
|
367
|
+
事件的置信度很低)。
|
package/fake_agent.py
CHANGED
|
@@ -88,9 +88,17 @@ def make_responder(min_sleep, max_sleep, crash_rate):
|
|
|
88
88
|
# RR 阶段:单向流,只写 pass(无异议回退,留待以后)
|
|
89
89
|
decision = meeting_core.T_PASS
|
|
90
90
|
else:
|
|
91
|
-
# meeting 阶段:有内容 → message;无话可说 → freezing
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
# meeting 阶段:有内容 → message;无话可说 → freezing。
|
|
92
|
+
# **首轮例外 = 必写 message**(装置对齐生产,2026-09-12):真实
|
|
93
|
+
# wake prompt 对首位发言者的指令是"你是第一位发言者——直接产出
|
|
94
|
+
# 你的第一条视角分析",fake 首轮却可能 freezing——语义不符,且
|
|
95
|
+
# 使"全员首轮 freezing"有 0.35^N 的概率(N=4 时 1.5%)撞上
|
|
96
|
+
# cascade 测试的 "应有 message" 断言 → 随机红(实测 ~2%/全量跑)。
|
|
97
|
+
if is_first:
|
|
98
|
+
decision = meeting_core.T_MESSAGE
|
|
99
|
+
else:
|
|
100
|
+
decision = meeting_core.T_MESSAGE if random.random() < 0.65 \
|
|
101
|
+
else meeting_core.T_FREEZING
|
|
94
102
|
|
|
95
103
|
time.sleep(random.uniform(min_sleep, max_sleep))
|
|
96
104
|
if random.random() < crash_rate:
|
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/package.json
CHANGED
package/scripts/mv.sh
CHANGED
|
@@ -1,364 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# mv.sh —— 多视角协同分析的**稳定入口**(shim)。
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# ./scripts/mv.sh --wait <dir>
|
|
11
|
-
# ./scripts/mv.sh --cleanup <dir>
|
|
12
|
-
# ./scripts/mv.sh --view <dir> [--since <ref>] # human-viewer 封装
|
|
13
|
-
# ./scripts/mv.sh --say <dir> "<文本>" # human-sayer 封装
|
|
5
|
+
# 逻辑在 mv_cli.py(见该文件首注释:为什么这层从 364 行 bash 收敛为 Python——
|
|
6
|
+
# bash 陷阱产出的真实 bug 与"零行覆盖"的盲区)。本文件只做一件事:找到仓库
|
|
7
|
+
# 根、把展示名($0 语义)与参数交给 mv_cli.py。
|
|
8
|
+
#
|
|
9
|
+
# **路径不能变**:prompt / README / 用户习惯都引用 scripts/mv.sh。
|
|
14
10
|
set -u
|
|
15
|
-
|
|
16
11
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
12
|
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
18
|
-
PYTHON="${PYTHON:-python3}"
|
|
19
|
-
START_DISCUSSION="$ROOT_DIR/start_discussion.py"
|
|
20
|
-
OBSERVABILITY="$ROOT_DIR/observability.py"
|
|
21
|
-
HUMAN_VIEWER="$ROOT_DIR/human_viewer.py"
|
|
22
|
-
HUMAN_SAYER="$ROOT_DIR/human_sayer.py"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
usage() {
|
|
26
|
-
cat <<'USAGE_EOF'
|
|
27
|
-
用法:
|
|
28
|
-
$0 --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
|
|
29
|
-
$0 --start <spec目录> [--fork-mode compaction|budget|full]
|
|
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-* 并警告)
|
|
39
|
-
|
|
40
|
-
默认参数:
|
|
41
|
-
agents=a,b,c max-meeting=10 max-rr=7 # 配额默认值的权威在 python argparse(wrapper 不传)
|
|
42
|
-
|
|
43
|
-
--agents: 逗号分隔名称列表(如 "x,y")或纯数字(如 4 → 生成 a..d);
|
|
44
|
-
human 是保留名,不能作为参与者
|
|
45
|
-
|
|
46
|
-
human 通道:
|
|
47
|
-
--view: 增量查看分析进展(--since 之后的新消息+状态;末尾输出 HEAD=<hash>,主 pi 记录作下轮 --since)
|
|
48
|
-
--say: 插话(写一条 human 消息,agents 可见并可回应)
|
|
49
|
-
USAGE_EOF
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
fail() {
|
|
53
|
-
echo "错误: $*" >&2
|
|
54
|
-
exit 1
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
require_dir() {
|
|
60
|
-
local dir="$1"
|
|
61
|
-
[ -n "$dir" ] || fail "缺少目录参数"
|
|
62
|
-
[ -d "$dir" ] || fail "目录不存在: $dir"
|
|
63
|
-
}
|
|
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
|
-
|
|
83
|
-
# 目录参数规范化:裸名(无路径符)会被 start_discussion 加 discussion-
|
|
84
|
-
# 前缀导致找错目录(实测 2026-09-03)——所有消费命令入口统一转绝对路径
|
|
85
|
-
normalize_dir() {
|
|
86
|
-
readlink -f "$1"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
cmd_status() {
|
|
90
|
-
local dir
|
|
91
|
-
dir="$(resolve_dir "${1:-}")" || exit $?
|
|
92
|
-
require_dir "$dir"
|
|
93
|
-
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --status
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
cmd_report() {
|
|
97
|
-
local dir
|
|
98
|
-
dir="$(resolve_dir "${1:-}")" || exit $?
|
|
99
|
-
require_dir "$dir"
|
|
100
|
-
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --report
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
cmd_wait() {
|
|
104
|
-
local dir
|
|
105
|
-
dir="$(resolve_dir "${1:-}")" || exit $?
|
|
106
|
-
require_dir "$dir"
|
|
107
|
-
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --wait
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
cmd_cleanup() {
|
|
111
|
-
local dir
|
|
112
|
-
dir="$(resolve_dir "${1:-}")" || exit $?
|
|
113
|
-
require_dir "$dir"
|
|
114
|
-
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --cleanup
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
cmd_view() {
|
|
118
|
-
local dir since=""
|
|
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
|
|
130
|
-
while [ "$#" -gt 0 ]; do
|
|
131
|
-
case "$1" in
|
|
132
|
-
--since)
|
|
133
|
-
[ "$#" -ge 2 ] || fail "--since 需要一个值"
|
|
134
|
-
since="$2"
|
|
135
|
-
shift 2
|
|
136
|
-
;;
|
|
137
|
-
*)
|
|
138
|
-
fail "未知参数: $1(--view 只接受 --since)"
|
|
139
|
-
;;
|
|
140
|
-
esac
|
|
141
|
-
done
|
|
142
|
-
require_dir "$dir"
|
|
143
|
-
[ -d "$dir/repo.git" ] || fail "分析不存在: $dir(无 repo.git)"
|
|
144
|
-
if [ -n "$since" ]; then
|
|
145
|
-
"$PYTHON" "$HUMAN_VIEWER" "$dir" --since "$since"
|
|
146
|
-
else
|
|
147
|
-
"$PYTHON" "$HUMAN_VIEWER" "$dir"
|
|
148
|
-
fi
|
|
149
|
-
# HEAD 游标(主 pi 记录作下轮 --since;无状态,不写游标文件——
|
|
150
|
-
# 与 --follow 的 .viewer-cursor 互不干扰)
|
|
151
|
-
echo "HEAD=$(git -C "$dir/repo.git" rev-parse HEAD)"
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
cmd_say() {
|
|
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
|
|
166
|
-
require_dir "$dir"
|
|
167
|
-
[ -d "$dir/work-human" ] || fail "分析缺少 work-human: $dir"
|
|
168
|
-
[ -n "$text" ] || fail "插话文本不能为空"
|
|
169
|
-
"$PYTHON" "$HUMAN_SAYER" "$dir" "$text"
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# 定位主 pi 的 session 文件:优先 PI_SESSION_FILE,否则用当前 cwd 编码路径查找
|
|
173
|
-
# 读取主 pi 的 model/thinking(用户 2026-08-31:aft 不再替换 bash 后
|
|
174
|
-
# 环境变量可用且是当前生效值——优先环境变量,session 文件解析仅为兜底)
|
|
175
|
-
cmd_prepare() {
|
|
176
|
-
local topic="" background="" agents_list=""
|
|
177
|
-
if [ "$#" -lt 1 ]; then
|
|
178
|
-
usage >&2
|
|
179
|
-
exit 2
|
|
180
|
-
fi
|
|
181
|
-
topic="$1"
|
|
182
|
-
shift
|
|
183
|
-
while [ "$#" -gt 0 ]; do
|
|
184
|
-
case "$1" in
|
|
185
|
-
--background)
|
|
186
|
-
[ "$#" -ge 2 ] || fail "--background 需要一个值"
|
|
187
|
-
background="$2"
|
|
188
|
-
shift 2
|
|
189
|
-
;;
|
|
190
|
-
--agents)
|
|
191
|
-
[ "$#" -ge 2 ] || fail "--agents 需要一个值(逗号分隔名称列表或数字)"
|
|
192
|
-
agents_list="$2"
|
|
193
|
-
shift 2
|
|
194
|
-
;;
|
|
195
|
-
-h|--help)
|
|
196
|
-
usage
|
|
197
|
-
exit 0
|
|
198
|
-
;;
|
|
199
|
-
*)
|
|
200
|
-
fail "未知参数: $1(--prepare 只接受 <问题>、--background、--agents)"
|
|
201
|
-
;;
|
|
202
|
-
esac
|
|
203
|
-
done
|
|
204
|
-
[ -n "$topic" ] || fail "问题不能为空"
|
|
205
|
-
|
|
206
|
-
# agents 解析(数字展开/human/名字校验)全部收归 python
|
|
207
|
-
# _parse_agents(2026-09-09 结构收敛:bash = 调度与展示)
|
|
208
|
-
# viewers 模式(无 --agents):校验 + 快照已全部移入 python
|
|
209
|
-
# _snapshot_viewers(铁律 1 职责边界;用户 2026-09-09:不合规根本
|
|
210
|
-
# 不应该开始 spec-gen,校验在骨架生成之前失败)
|
|
211
|
-
local snapshot_mode=0
|
|
212
|
-
[ -z "$agents_list" ] && snapshot_mode=1
|
|
213
|
-
|
|
214
|
-
local stamp
|
|
215
|
-
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
216
|
-
local spec_dir="$PWD/mv-spec-${stamp}"
|
|
217
|
-
# (目录不预创建——python 校验失败 = 零产物:预创建会让"不合规
|
|
218
|
-
# 零产物"失效,实测 2026-09-09;成功时 gen_spec_skeleton 自建)
|
|
219
|
-
|
|
220
|
-
# 骨架生成 = start_discussion --spec-gen(唯一实现):agents/ 三态
|
|
221
|
-
# (显式占位骨架 / viewers 校验+快照 / 无)全部在 python 侧,bash
|
|
222
|
-
# 与 CLI 直用产出一致;viewers 校验失败时 python 退出非 0(不合规
|
|
223
|
-
# 根本不应该开始 spec-gen)
|
|
224
|
-
local skeleton_args=(--spec-gen "$spec_dir" --topic "$topic")
|
|
225
|
-
[ -n "$background" ] && skeleton_args+=(--background "$background")
|
|
226
|
-
if [ "$snapshot_mode" -eq 0 ]; then
|
|
227
|
-
skeleton_args+=(--agents "$agents_list")
|
|
228
|
-
fi
|
|
229
|
-
"$PYTHON" "$START_DISCUSSION" "${skeleton_args[@]}" || {
|
|
230
|
-
fail "spec 骨架生成失败(start_discussion --spec-gen,见上方错误)"
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
cat <<OUTPUT_EOF
|
|
234
|
-
已生成分析 spec:
|
|
235
|
-
$spec_dir
|
|
236
|
-
|
|
237
|
-
请查看/编辑该目录,补充背景、各 agent 视角等。
|
|
238
|
-
编辑完成后,告诉我"继续",我会自动启动分析。
|
|
239
|
-
OUTPUT_EOF
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
cmd_start() {
|
|
243
|
-
local spec_dir="$1"
|
|
244
|
-
shift # 余参 = 透传给 python 的选项(如 --fork-mode X)
|
|
245
|
-
require_dir "$spec_dir"
|
|
246
|
-
[ -f "$spec_dir/question.md" ] || fail "spec 缺少 question.md: $spec_dir"
|
|
247
|
-
|
|
248
|
-
local session_id="${PI_SESSION_ID:-}"
|
|
249
|
-
local stamp
|
|
250
|
-
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
251
|
-
# 目录前缀 mv-:与 pi-agents-helper 的 discuss-<sid>-* **命名空间隔离**
|
|
252
|
-
# ——两个系统的 extension 都按"同 sid 最新目录"发现目标,共用前缀会
|
|
253
|
-
# 在同 session 并发两套时互相插错(且 human 消息格式兼容 → 静默写错)。
|
|
254
|
-
# 前缀也是产品语言的统一(本项目用"分析",不再沿用"discuss")。
|
|
255
|
-
local dir_name="mv"
|
|
256
|
-
if [ -n "$session_id" ]; then
|
|
257
|
-
dir_name="${dir_name}-${session_id}"
|
|
258
|
-
fi
|
|
259
|
-
dir_name="${dir_name}-${stamp}"
|
|
260
|
-
local dir_path="$PWD/$dir_name"
|
|
261
|
-
|
|
262
|
-
# fork 源解析已收归 python(resolve_fork_source:env + sessions 编码
|
|
263
|
-
# glob,2026-09-09 结构收敛;解析失败 python 内部报错退出)
|
|
264
|
-
# 第 1 步:创建分析环境(不启动;fork 源自动解析)
|
|
265
|
-
# 配额(max-meeting/max-rr)是环境属性:唯一默认在 python argparse
|
|
266
|
-
# (10/7),创建时固化 protocol.json——wrapper 不传(W1 双源分叉修复:
|
|
267
|
-
# 此前 wrapper 5 与 python 7 不一致,两条入口 RR 上限差 40%)
|
|
268
|
-
# 余参哑转发(P0,e2e10 评审:wrapper 不得静默丢弃参数——值识别/默认值
|
|
269
|
-
# 的唯一家在 python;本层只转发)
|
|
270
|
-
if ! "$PYTHON" "$START_DISCUSSION" --dir "$dir_path" --spec "$spec_dir" "$@"; then
|
|
271
|
-
fail "环境创建失败,请查看上方输出"
|
|
272
|
-
fi
|
|
273
|
-
|
|
274
|
-
# spec 已被消费:只删**本工具生成的形态**(mv-spec-*)——用户自建的
|
|
275
|
-
# spec 目录不动(可能是有价值的视角快照)。删除前提示一行(此前静默
|
|
276
|
-
# 删除:spec 是视角任务书/背景/models 在用户侧的唯一副本,删了不可恢复)
|
|
277
|
-
case "$(basename "$spec_dir")" in
|
|
278
|
-
mv-spec-*)
|
|
279
|
-
echo "[start] spec 已消费,删除(本工具生成形态): $spec_dir"
|
|
280
|
-
rm -rf "$spec_dir"
|
|
281
|
-
;;
|
|
282
|
-
*)
|
|
283
|
-
echo "[start] spec 已消费(保留未删——非 mv-spec-* 形态): $spec_dir"
|
|
284
|
-
;;
|
|
285
|
-
esac
|
|
286
|
-
|
|
287
|
-
# 第 3 步:启动已有环境
|
|
288
|
-
if ! "$PYTHON" "$START_DISCUSSION" --dir "$dir_path" --skip-setup --start; then
|
|
289
|
-
fail "启动失败,请查看上方输出"
|
|
290
|
-
fi
|
|
291
|
-
|
|
292
|
-
cat <<OUTPUT_EOF
|
|
293
|
-
多视角分析已启动
|
|
294
|
-
目录: $dir_path
|
|
295
|
-
|
|
296
|
-
观看分析(复制执行,不进 LLM;Ctrl-C 中断后可插话再续看):
|
|
297
|
-
!!python3 "$ROOT_DIR/human_viewer.py" $dir_path --follow
|
|
298
|
-
|
|
299
|
-
查看进展: $0 --view $dir_path
|
|
300
|
-
插话: $0 --say $dir_path "<文本>"
|
|
301
|
-
查看状态: $0 --status $dir_path
|
|
302
|
-
等待完成: $0 --wait $dir_path
|
|
303
|
-
清理: $0 --cleanup $dir_path
|
|
304
|
-
|
|
305
|
-
说明:
|
|
306
|
-
- human 通道:--view 增量查看(主 pi 记录末尾 HEAD 作下轮 --since);
|
|
307
|
-
--say 插话(agents 可见并可回应;已冻结 agent 不响应)
|
|
308
|
-
- 完成后 result.md 自动保存到固定位置:$dir_path-result.md(与分析目录同级——resultWriter loop 退出时保存;cleanup 也会保存)
|
|
309
|
-
- 读取 result.md 摘要后请执行 --cleanup 清理分析目录
|
|
310
|
-
OUTPUT_EOF
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if [ "$#" -ge 1 ]; then
|
|
314
|
-
case "$1" in
|
|
315
|
-
--prepare)
|
|
316
|
-
shift
|
|
317
|
-
cmd_prepare "$@"
|
|
318
|
-
exit $?
|
|
319
|
-
;;
|
|
320
|
-
--start)
|
|
321
|
-
[ "$#" -ge 2 ] || fail "--start 需要 spec 目录参数"
|
|
322
|
-
shift
|
|
323
|
-
cmd_start "$@"
|
|
324
|
-
exit $?
|
|
325
|
-
;;
|
|
326
|
-
--status)
|
|
327
|
-
shift
|
|
328
|
-
cmd_status "${1:-}"
|
|
329
|
-
exit $?
|
|
330
|
-
;;
|
|
331
|
-
--report)
|
|
332
|
-
shift
|
|
333
|
-
cmd_report "${1:-}"
|
|
334
|
-
exit $?
|
|
335
|
-
;;
|
|
336
|
-
--wait)
|
|
337
|
-
shift
|
|
338
|
-
cmd_wait "${1:-}"
|
|
339
|
-
exit $?
|
|
340
|
-
;;
|
|
341
|
-
--cleanup)
|
|
342
|
-
shift
|
|
343
|
-
cmd_cleanup "${1:-}"
|
|
344
|
-
exit $?
|
|
345
|
-
;;
|
|
346
|
-
--view)
|
|
347
|
-
shift
|
|
348
|
-
cmd_view "$@"
|
|
349
|
-
exit $?
|
|
350
|
-
;;
|
|
351
|
-
--say)
|
|
352
|
-
shift
|
|
353
|
-
cmd_say "$@"
|
|
354
|
-
exit $?
|
|
355
|
-
;;
|
|
356
|
-
-h|--help)
|
|
357
|
-
usage
|
|
358
|
-
exit 0
|
|
359
|
-
;;
|
|
360
|
-
esac
|
|
361
|
-
fi
|
|
362
13
|
|
|
363
|
-
usage
|
|
364
|
-
|
|
14
|
+
# MV_INVOKED_AS:usage 与启动块里展示用户实际敲的那条命令(沿用 bash 的
|
|
15
|
+
# $0 语义;直接跑 mv_cli.py 时它会退回默认名)
|
|
16
|
+
MV_INVOKED_AS="${MV_INVOKED_AS:-$0}" \
|
|
17
|
+
exec "${PYTHON:-python3}" "$ROOT_DIR/mv_cli.py" "$@"
|