pi-multi-viewers 0.1.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 +192 -0
- package/README.md +153 -0
- package/docs/design.md +288 -0
- package/docs/examples/first-experiment/README.md +42 -0
- package/docs/examples/first-experiment/work-a/AGENTS.md +10 -0
- package/docs/examples/first-experiment/work-a/a/0001.md +65 -0
- package/docs/examples/first-experiment/work-a/a/0002.md +70 -0
- package/docs/examples/first-experiment/work-a/perspective.md +5 -0
- package/docs/examples/first-experiment/work-b/AGENTS.md +10 -0
- package/docs/examples/first-experiment/work-b/b/0001.md +100 -0
- package/docs/examples/first-experiment/work-b/perspective.md +6 -0
- package/docs/reviews/2026-09-10-e2e10-fork-source-modes-review.md +366 -0
- package/docs/reviews/2026-09-10-e2e11-forkmode-guards-review.md +229 -0
- package/docs/reviews/2026-09-10-e2e12-code-review.md +346 -0
- package/docs/reviews/2026-09-11-e2e13-code-review.md +284 -0
- package/docs/reviews/2026-09-11-e2e14-observability-review.md +216 -0
- package/docs/reviews/README.md +36 -0
- package/docs/test-methodology.md +258 -0
- package/extensions/multi-viewers-say/index.ts +156 -0
- package/fake_agent.py +120 -0
- package/human_sayer.py +144 -0
- package/human_viewer.py +215 -0
- package/meeting_core.py +255 -0
- package/meeting_engine.py +733 -0
- package/meeting_fs.py +1066 -0
- package/meeting_loop.py +606 -0
- package/package.json +41 -0
- package/prompts/multi-viewers.md +94 -0
- package/scripts/check-residue.sh +190 -0
- package/scripts/mv.sh +325 -0
- package/start_discussion.py +1485 -0
- package/templates/AGENTS.md.tpl +100 -0
- package/templates/agent.md.tpl +9 -0
- package/templates/gitignore.tpl +7 -0
- package/templates/spec-readme.md.tpl +87 -0
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
**若报错含"未找到 viewers/ 目录"、"仅发现 1 个视角"或"视角任务书不能为空"**:
|
|
30
|
+
说明项目缺可用视角。此时**停下来问用户**,不要自己选——建议帮他按
|
|
31
|
+
下面的"视角设计原则"写 `viewers/<视角名>.md`(建好后再重跑本步骤)。
|
|
32
|
+
|
|
33
|
+
### 2. 编辑并请用户审核 spec
|
|
34
|
+
|
|
35
|
+
编辑 spec 目录:
|
|
36
|
+
|
|
37
|
+
- `question.md`(第二行起):**任务类型与产出形态**(审阅→提意见;
|
|
38
|
+
改进建议→说建议;修改→说明可改/需改范围)、分析对象、是否需要达成一致。
|
|
39
|
+
写得越明确,agent 越不会跑偏去做上下文里的其它事(它们的上下文里有
|
|
40
|
+
发起分析的完整对话历史)
|
|
41
|
+
- `agents/X.md`(第二行起):**默认已是 viewers 的快照**——只在用户要求
|
|
42
|
+
按场微调时才改(如"这一场某视角更关注安全")
|
|
43
|
+
- `background.md`(可选):只写**显式约定的边界**(如"不讨论 API 设计")。
|
|
44
|
+
**不要**复述对话内容——fork 已让每个 agent 携带主会话上下文
|
|
45
|
+
(默认 budget 模式:按预算裁剪+折叠)
|
|
46
|
+
- `models.md`:一般不动(默认继承本机配置)
|
|
47
|
+
|
|
48
|
+
然后**展示 spec 路径,明确请用户查看/编辑**——用户确认"继续"才执行第 3 步。
|
|
49
|
+
|
|
50
|
+
## 视角设计原则(建 viewers/*.md 或临时 agents/X.md 时必读)
|
|
51
|
+
|
|
52
|
+
视角之间**互补或对立都可以**——对立产生的分歧正是多视角分析的价值。
|
|
53
|
+
每个视角任务书必须包含:
|
|
54
|
+
|
|
55
|
+
1. **单一 lenses**:明确该 agent 用什么角度看(性能/简单化/安全/……),
|
|
56
|
+
要求"所有观点必须从该视角出发"
|
|
57
|
+
2. **不越界**:"其它视角由别的参与者负责,你不要越界展开"(**不要**列举
|
|
58
|
+
具体是哪几个视角——参与者会变,列举会过期)
|
|
59
|
+
3. **交锋义务**:"对其它视角的观点可以认同或反驳,但要用本视角的论据"
|
|
60
|
+
|
|
61
|
+
**只写视角本身**:身份("你是 X")、参与者名单、消息格式、独立参与者
|
|
62
|
+
纪律都由脚本生成(agent 名 = 文件名,单一来源)——任务书里**不要写**,
|
|
63
|
+
否则重复且易漂移。
|
|
64
|
+
|
|
65
|
+
### 3. 启动分析
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --start <spec目录绝对路径>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
记录 wrapper 输出的**分析目录**,并**原样完整展示**观看命令。
|
|
72
|
+
|
|
73
|
+
### 4. 结束回合
|
|
74
|
+
|
|
75
|
+
告知用户:
|
|
76
|
+
- 观看:复制上一步的 `!!` 命令执行(实时流式,结束时自动退出)
|
|
77
|
+
- 插话:随时 `/multi-viewers-say <文本>`(自动定位当前分析;也可用 wrapper `--say <目录> "<文本>"`)
|
|
78
|
+
- 完成时告诉主 pi,主 pi 会收尾
|
|
79
|
+
|
|
80
|
+
**然后结束当前回合。**
|
|
81
|
+
|
|
82
|
+
## 收尾(用户驱动)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
mv.sh --status <分析目录绝对路径> # done/stopped/running
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `done`:读 `<分析目录>-result.md`(与目录同级的固定位)→ 向用户给
|
|
89
|
+
**摘要** → `mv.sh --cleanup <分析目录绝对路径>`
|
|
90
|
+
- cleanup 会打印**本次分析报告**(提交数 / 墙钟 / 进程跨度 / 配额 / LLM
|
|
91
|
+
用量 / rc≠0)——这是删目录前最后一次可读,**把其中的关键数字一并
|
|
92
|
+
转述给用户**(运行成本与健康度的一手信息)
|
|
93
|
+
- `stopped`:报告"分析已结束但未生成结果",不要继续等待
|
|
94
|
+
- `running`:告知还在进行,继续等用户通知
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 残留检查器——测试/验证结束后的清理验收(一条命令代替人工逐项记忆)
|
|
3
|
+
#
|
|
4
|
+
# 用法: ./scripts/check-residue.sh [--verbose]
|
|
5
|
+
# 退出码: 0 = 干净;1 = 有残留(列出清单)
|
|
6
|
+
#
|
|
7
|
+
# 检查三类测试残留形态(2026-09-09 建立——此前靠人工记忆逐项检查,
|
|
8
|
+
# 反复漏检:afk-e2e 引导 session 漏删即反例):
|
|
9
|
+
# 1. 主 pi 侧 session:~/.pi/agent/sessions/<编码目录>/*.jsonl 中
|
|
10
|
+
# 近 24h 创建且 cwd 不在白名单(真实项目)的——测试引导 session
|
|
11
|
+
# 散落形态(--tmp-xxx-- 等)
|
|
12
|
+
# 2. 讨论进程:meeting_loop(argv 判据)/ pi(comm + PPID 判据)
|
|
13
|
+
# 3. 分析环境目录:$PWD 下 mv-* 残留(另有 gitignore 兜底不入库)
|
|
14
|
+
# + 结构识别(2026-09-10 补):含 repo.git/ 与 pi-sessions/ 的目录
|
|
15
|
+
# ——测试脚手架常用 /tmp/mv-*/disc 等非 mv-<sid>-* 命名,纯命名匹配
|
|
16
|
+
# 会漏检(e2e8 残留即此盲区),改为按环境结构识别
|
|
17
|
+
#
|
|
18
|
+
# 白名单:cwd 为真实项目目录的 session 不算残留(mv-main 等长期会话)。
|
|
19
|
+
WHITELIST=(
|
|
20
|
+
"/root/pi-multi-viewers"
|
|
21
|
+
"/root/pi-agents-helper"
|
|
22
|
+
"/root/pi-agents-meeting-discuss"
|
|
23
|
+
"/root/research"
|
|
24
|
+
"/root/book"
|
|
25
|
+
"/root/.dsh"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# 测试引导 session 登记日志(meeting_fs.build_bootstrap 每次创建追加):
|
|
29
|
+
# 报告对照此日志标注归属——已登记 = 本流程测试产物(可删);未登记 =
|
|
30
|
+
# 非本流程创建(真实会话,勿删)。不靠人猜(2026-09-09 用户:建立时
|
|
31
|
+
# 记录 log 才方便回查,人工确认是推卸责任)。
|
|
32
|
+
REGISTRY="$HOME/.pi/pi-multi-viewers-test-sessions.log"
|
|
33
|
+
is_registered() {
|
|
34
|
+
# $1 = session 文件路径;按 header 的 session_id 对照登记日志
|
|
35
|
+
# (build_bootstrap 登记 id;文件路径会因 --session 续写/复制变化,
|
|
36
|
+
# id 才是稳定标识)。登记过 = 本流程测试产物。
|
|
37
|
+
local sid
|
|
38
|
+
sid=$(head -1 "$1" | python3 -c "import json,sys; print(json.loads(sys.stdin.readline()).get('id',''))" 2>/dev/null)
|
|
39
|
+
[ -n "$sid" ] && grep -q "\"session_id\": \"$sid\"" "$REGISTRY" 2>/dev/null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
RESIDUE=0
|
|
43
|
+
|
|
44
|
+
say() { [ "$1" = "--verbose" ] || [ -n "$VERBOSE" ] && echo "$2"; }
|
|
45
|
+
|
|
46
|
+
# --- 1. 主 pi 侧近期 session(非白名单 cwd) ---
|
|
47
|
+
SESS_DIR="$HOME/.pi/agent/sessions"
|
|
48
|
+
NOW=$(date +%s)
|
|
49
|
+
for d in "$SESS_DIR"/*/; do
|
|
50
|
+
[ -d "$d" ] || continue
|
|
51
|
+
# 编码目录名还原 cwd 近似判断(--tmp-xxx-- / --root-xxx-- 形态)
|
|
52
|
+
enc=$(basename "$d")
|
|
53
|
+
for f in "$d"*.jsonl; do
|
|
54
|
+
[ -f "$f" ] || continue
|
|
55
|
+
age=$((NOW - $(stat -c %Y "$f")))
|
|
56
|
+
if [ "$age" -gt 86400 ]; then
|
|
57
|
+
continue # 超过 24h,不是本次测试产物
|
|
58
|
+
fi
|
|
59
|
+
# 解析 header cwd(第一行 JSON 的 cwd 字段)
|
|
60
|
+
cwd=$(head -1 "$f" | python3 -c "import json,sys; print(json.loads(sys.stdin.readline()).get('cwd',''))" 2>/dev/null)
|
|
61
|
+
whitelisted=0
|
|
62
|
+
for w in "${WHITELIST[@]}"; do
|
|
63
|
+
case "$cwd" in
|
|
64
|
+
"$w"*|"$w") whitelisted=1 ;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
if [ "$whitelisted" = "0" ]; then
|
|
68
|
+
# 归属判定(2026-09-09):登记日志回查——已登记=测试产物
|
|
69
|
+
# (可删),未登记=非本流程创建(勿删,提示加白名单)。
|
|
70
|
+
# 首条消息摘要仅作补充信息(不再作为删除判断依据)。
|
|
71
|
+
if is_registered "$f"; then
|
|
72
|
+
owner="[已登记-测试产物,可删]"
|
|
73
|
+
else
|
|
74
|
+
owner="[未登记-非本流程创建,勿删]"
|
|
75
|
+
RESIDUE=1
|
|
76
|
+
fi
|
|
77
|
+
first_user=$(python3 -c "
|
|
78
|
+
import json, sys
|
|
79
|
+
try:
|
|
80
|
+
for l in open('$f'):
|
|
81
|
+
e = json.loads(l)
|
|
82
|
+
if e.get('type') == 'message' and e.get('message', {}).get('role') == 'user':
|
|
83
|
+
c = e['message'].get('content')
|
|
84
|
+
t = c if isinstance(c, str) else ''.join(x.get('text','') for x in (c or []) if isinstance(x, dict) and x.get('type') == 'text')
|
|
85
|
+
print((t or '')[:60].replace('\\n', ' '))
|
|
86
|
+
break
|
|
87
|
+
except Exception:
|
|
88
|
+
print('')
|
|
89
|
+
" 2>/dev/null)
|
|
90
|
+
echo "[残留-1] session(24h 内 cwd=$cwd,创建 $(stat -c %y "$f" | cut -d. -f1))$owner"
|
|
91
|
+
echo " 首条消息: ${first_user:-(无 user 消息)}"
|
|
92
|
+
echo " 路径: $f"
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
done
|
|
96
|
+
|
|
97
|
+
# --- 2. 讨论/pi 进程 ---
|
|
98
|
+
# 两种进程用**两种判据**(它们的可观测形态不同):
|
|
99
|
+
#
|
|
100
|
+
# (a) meeting_loop:**argv 逐项精确匹配**(读 /proc/<pid>/cmdline)。文本匹配
|
|
101
|
+
# 会把"命令行里恰好提到 meeting_loop.py"的调用者自身算进来(bash -c / grep
|
|
102
|
+
# 都在此列),再靠 grep -v 白名单过滤又会漏检(任何命令行含 "bash -c" 的
|
|
103
|
+
# 真实进程)。argv 精确匹配无此二难:`python3 .../meeting_loop.py <work>
|
|
104
|
+
# <agent>` 的 argv[1] 恰是脚本路径;调用者的 -c 脚本只是**一个** argv 元素。
|
|
105
|
+
# 与 start_discussion._loop_pids 同一技术(判据范围不同:这里查任意讨论的
|
|
106
|
+
# loop——残留检查语义;python 侧查指定 base——存活语义)。
|
|
107
|
+
#
|
|
108
|
+
# (b) pi:**名称 + PPID 双判据**——pi 启动后**重写进程标题**(`/proc/<pid>/cmdline`
|
|
109
|
+
# 变成单个 "pi" + NUL 填充,2026-09-11 实测:2255 字节里只有开头 "pi"),
|
|
110
|
+
# 所以按 argv 找 pi(含旧的 `grep "pi --mode json"`)**从来找不到**。
|
|
111
|
+
# 可用信号是 /proc/<pid>/comm(node 改写的进程名 = "pi")与其 PPID(父进程
|
|
112
|
+
# 必是 meeting_loop——由 loop 直接 spawn)。名称单独用会误认用户自己开的
|
|
113
|
+
# pi TUI;PPID 单独用会在 loop 已死(pi 被 init 收养)时漏检——两者并用
|
|
114
|
+
# 并对"未关联"单独标注,既不误报也不漏报。
|
|
115
|
+
_proc_argv_has() { # $1=pid 目录,$2=精确参数
|
|
116
|
+
local d="$1" arg
|
|
117
|
+
while IFS= read -r -d '' arg; do
|
|
118
|
+
[ "$arg" = "$2" ] && return 0
|
|
119
|
+
done < "$d/cmdline" 2>/dev/null
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
LOOP_PIDS="" # 第一遍收集,供第二遍做 PPID 关联
|
|
123
|
+
for d in /proc/[0-9]*; do
|
|
124
|
+
[ -r "$d/cmdline" ] || continue
|
|
125
|
+
pid="${d#/proc/}"
|
|
126
|
+
while IFS= read -r -d '' arg; do
|
|
127
|
+
case "$arg" in
|
|
128
|
+
*/meeting_loop.py)
|
|
129
|
+
echo "[残留-2] meeting_loop 进程 PID=$pid: $(ps -p "$pid" -o cmd= 2>/dev/null)"
|
|
130
|
+
RESIDUE=1
|
|
131
|
+
LOOP_PIDS="$LOOP_PIDS $pid"
|
|
132
|
+
break
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
done < "$d/cmdline" 2>/dev/null
|
|
136
|
+
done
|
|
137
|
+
for d in /proc/[0-9]*; do
|
|
138
|
+
[ -r "$d/comm" ] || continue
|
|
139
|
+
pid="${d#/proc/}"
|
|
140
|
+
[ "$(cat "$d/comm" 2>/dev/null)" = "pi" ] || continue
|
|
141
|
+
ppid="$(awk '{print $4}' "$d/stat" 2>/dev/null)"
|
|
142
|
+
if [ "$ppid" = "1" ]; then
|
|
143
|
+
# loop 已退出、pi 被 init 收养(PPID=1)——孤儿残留
|
|
144
|
+
echo "[残留-2] pi 进程 PID=$pid(孤儿:PPID=1,其 loop 已退出)"
|
|
145
|
+
RESIDUE=1
|
|
146
|
+
continue
|
|
147
|
+
fi
|
|
148
|
+
case " $LOOP_PIDS " in
|
|
149
|
+
*" $ppid "*)
|
|
150
|
+
echo "[残留-2] pi 进程 PID=$pid(父 meeting_loop PID=$ppid)"
|
|
151
|
+
RESIDUE=1
|
|
152
|
+
;;
|
|
153
|
+
# 其余 comm=pi 的进程不报:用户自己正在用的 pi 会话(PPID 是
|
|
154
|
+
# pi-web/shell 等)不是残留——"名称 + PPID"两个条件同时成立才判定
|
|
155
|
+
esac
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
# --- 3. 分析环境目录残留 ---
|
|
159
|
+
# 3a. 命名形态:$PWD 下 mv-*(兜底:半创建、尚无 repo.git 的环境)。
|
|
160
|
+
# 排除 mv-spec-*——那是用户尚未消费的 spec 目录(正常存在,非残留)
|
|
161
|
+
for d in mv-*/; do
|
|
162
|
+
[ -d "$d" ] || continue
|
|
163
|
+
case "$(basename "$d")" in
|
|
164
|
+
mv-spec-*) continue ;;
|
|
165
|
+
esac
|
|
166
|
+
echo "[残留-3] 分析目录: $PWD/$d"
|
|
167
|
+
RESIDUE=1
|
|
168
|
+
done
|
|
169
|
+
|
|
170
|
+
# 3b. 结构形态:任何含 repo.git/ + pi-sessions/ 的目录 = 讨论环境
|
|
171
|
+
#(扫描 $PWD 与 /tmp,深度 ≤3;与 3a 去重)
|
|
172
|
+
# 去重用空格包裹的字符串匹配(不用 declare -A:避免 bash 4+ 依赖,
|
|
173
|
+
# 路径不含空格——本仓库路径约定)
|
|
174
|
+
SEEN_ENVS=""
|
|
175
|
+
for base in "$PWD" "${TMPDIR:-/tmp}"; do
|
|
176
|
+
[ -d "$base" ] || continue
|
|
177
|
+
while IFS= read -r g; do
|
|
178
|
+
d=$(dirname "$g")
|
|
179
|
+
[ -d "$d/pi-sessions" ] || continue
|
|
180
|
+
case " $SEEN_ENVS " in *" $d "*) continue ;; esac
|
|
181
|
+
SEEN_ENVS="$SEEN_ENVS $d"
|
|
182
|
+
echo "[残留-3] 讨论环境: $d($(du -sh "$d" 2>/dev/null | cut -f1))"
|
|
183
|
+
RESIDUE=1
|
|
184
|
+
done < <(find "$base" -maxdepth 3 -type d -name repo.git 2>/dev/null)
|
|
185
|
+
done
|
|
186
|
+
|
|
187
|
+
if [ "$RESIDUE" = "0" ]; then
|
|
188
|
+
echo "干净:无测试残留"
|
|
189
|
+
fi
|
|
190
|
+
exit "$RESIDUE"
|
package/scripts/mv.sh
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# pi-multi-viewers wrapper——多视角协同分析(fork 主 session + meeting 协议)
|
|
4
|
+
#
|
|
5
|
+
# 用法:
|
|
6
|
+
# ./scripts/mv.sh --prepare "<主题>" [--agents "a,b,c"|4]
|
|
7
|
+
# ./scripts/mv.sh --start <spec目录> # 可选 --fork-mode compaction|budget|full
|
|
8
|
+
# ./scripts/mv.sh --status <dir>
|
|
9
|
+
# ./scripts/mv.sh --report <dir>
|
|
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 封装
|
|
14
|
+
set -u
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
18
|
+
PYTHON="${PYTHON:-python3}"
|
|
19
|
+
START_DISCUSSION="$ROOT_DIR/start_discussion.py"
|
|
20
|
+
HUMAN_VIEWER="$ROOT_DIR/human_viewer.py"
|
|
21
|
+
HUMAN_SAYER="$ROOT_DIR/human_sayer.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
usage() {
|
|
25
|
+
cat <<'USAGE_EOF'
|
|
26
|
+
用法:
|
|
27
|
+
$0 --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
|
|
28
|
+
$0 --start <spec目录> [--fork-mode compaction|budget|full]
|
|
29
|
+
$0 --status <dir>
|
|
30
|
+
$0 --report <dir>
|
|
31
|
+
$0 --wait <dir>
|
|
32
|
+
$0 --cleanup <dir>
|
|
33
|
+
$0 --view <dir> [--since <ref>]
|
|
34
|
+
$0 --say <dir> "<文本>"
|
|
35
|
+
|
|
36
|
+
默认参数:
|
|
37
|
+
agents=a,b,c max-meeting=10 max-rr=7 # 配额默认值的权威在 python argparse(wrapper 不传)
|
|
38
|
+
|
|
39
|
+
--agents: 逗号分隔名称列表(如 "x,y")或纯数字(如 4 → 生成 a..d);
|
|
40
|
+
human 是保留名,不能作为参与者
|
|
41
|
+
|
|
42
|
+
human 通道:
|
|
43
|
+
--view: 增量查看分析进展(--since 之后的新消息+状态;末尾输出 HEAD=<hash>,主 pi 记录作下轮 --since)
|
|
44
|
+
--say: 插话(写一条 human 消息,agents 可见并可回应)
|
|
45
|
+
USAGE_EOF
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fail() {
|
|
49
|
+
echo "错误: $*" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
require_dir() {
|
|
56
|
+
local dir="$1"
|
|
57
|
+
[ -n "$dir" ] || fail "缺少目录参数"
|
|
58
|
+
[ -d "$dir" ] || fail "目录不存在: $dir"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# 目录参数规范化:裸名(无路径符)会被 start_discussion 加 discussion-
|
|
62
|
+
# 前缀导致找错目录(实测 2026-09-03)——所有消费命令入口统一转绝对路径
|
|
63
|
+
normalize_dir() {
|
|
64
|
+
readlink -f "$1"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
cmd_status() {
|
|
68
|
+
local dir
|
|
69
|
+
dir="$(normalize_dir "$1")"
|
|
70
|
+
require_dir "$dir"
|
|
71
|
+
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --status
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
cmd_report() {
|
|
75
|
+
local dir
|
|
76
|
+
dir="$(normalize_dir "$1")"
|
|
77
|
+
require_dir "$dir"
|
|
78
|
+
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --report
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cmd_wait() {
|
|
82
|
+
local dir
|
|
83
|
+
dir="$(normalize_dir "$1")"
|
|
84
|
+
require_dir "$dir"
|
|
85
|
+
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --wait
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cmd_cleanup() {
|
|
89
|
+
local dir
|
|
90
|
+
dir="$(normalize_dir "$1")"
|
|
91
|
+
require_dir "$dir"
|
|
92
|
+
"$PYTHON" "$START_DISCUSSION" --dir "$dir" --cleanup
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
cmd_view() {
|
|
96
|
+
local dir since=""
|
|
97
|
+
dir="$(normalize_dir "$1")"
|
|
98
|
+
shift
|
|
99
|
+
while [ "$#" -gt 0 ]; do
|
|
100
|
+
case "$1" in
|
|
101
|
+
--since)
|
|
102
|
+
[ "$#" -ge 2 ] || fail "--since 需要一个值"
|
|
103
|
+
since="$2"
|
|
104
|
+
shift 2
|
|
105
|
+
;;
|
|
106
|
+
*)
|
|
107
|
+
fail "未知参数: $1(--view 只接受 --since)"
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
done
|
|
111
|
+
require_dir "$dir"
|
|
112
|
+
[ -d "$dir/repo.git" ] || fail "分析不存在: $dir(无 repo.git)"
|
|
113
|
+
if [ -n "$since" ]; then
|
|
114
|
+
"$PYTHON" "$HUMAN_VIEWER" "$dir" --since "$since"
|
|
115
|
+
else
|
|
116
|
+
"$PYTHON" "$HUMAN_VIEWER" "$dir"
|
|
117
|
+
fi
|
|
118
|
+
# HEAD 游标(主 pi 记录作下轮 --since;无状态,不写游标文件——
|
|
119
|
+
# 与 --follow 的 .viewer-cursor 互不干扰)
|
|
120
|
+
echo "HEAD=$(git -C "$dir/repo.git" rev-parse HEAD)"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cmd_say() {
|
|
124
|
+
local dir text="${2:-}"
|
|
125
|
+
dir="$(normalize_dir "$1")"
|
|
126
|
+
require_dir "$dir"
|
|
127
|
+
[ -d "$dir/work-human" ] || fail "分析缺少 work-human: $dir"
|
|
128
|
+
[ -n "$text" ] || fail "插话文本不能为空"
|
|
129
|
+
"$PYTHON" "$HUMAN_SAYER" "$dir" "$text"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# 定位主 pi 的 session 文件:优先 PI_SESSION_FILE,否则用当前 cwd 编码路径查找
|
|
133
|
+
# 读取主 pi 的 model/thinking(用户 2026-08-31:aft 不再替换 bash 后
|
|
134
|
+
# 环境变量可用且是当前生效值——优先环境变量,session 文件解析仅为兜底)
|
|
135
|
+
cmd_prepare() {
|
|
136
|
+
local topic="" background="" agents_list=""
|
|
137
|
+
if [ "$#" -lt 1 ]; then
|
|
138
|
+
usage >&2
|
|
139
|
+
exit 2
|
|
140
|
+
fi
|
|
141
|
+
topic="$1"
|
|
142
|
+
shift
|
|
143
|
+
while [ "$#" -gt 0 ]; do
|
|
144
|
+
case "$1" in
|
|
145
|
+
--background)
|
|
146
|
+
[ "$#" -ge 2 ] || fail "--background 需要一个值"
|
|
147
|
+
background="$2"
|
|
148
|
+
shift 2
|
|
149
|
+
;;
|
|
150
|
+
--agents)
|
|
151
|
+
[ "$#" -ge 2 ] || fail "--agents 需要一个值(逗号分隔名称列表或数字)"
|
|
152
|
+
agents_list="$2"
|
|
153
|
+
shift 2
|
|
154
|
+
;;
|
|
155
|
+
-h|--help)
|
|
156
|
+
usage
|
|
157
|
+
exit 0
|
|
158
|
+
;;
|
|
159
|
+
*)
|
|
160
|
+
fail "未知参数: $1(--prepare 只接受 <问题>、--background、--agents)"
|
|
161
|
+
;;
|
|
162
|
+
esac
|
|
163
|
+
done
|
|
164
|
+
[ -n "$topic" ] || fail "问题不能为空"
|
|
165
|
+
|
|
166
|
+
# agents 解析(数字展开/human/名字校验)全部收归 python
|
|
167
|
+
# _parse_agents(2026-09-09 结构收敛:bash = 调度与展示)
|
|
168
|
+
# viewers 模式(无 --agents):校验 + 快照已全部移入 python
|
|
169
|
+
# _snapshot_viewers(铁律 1 职责边界;用户 2026-09-09:不合规根本
|
|
170
|
+
# 不应该开始 spec-gen,校验在骨架生成之前失败)
|
|
171
|
+
local snapshot_mode=0
|
|
172
|
+
[ -z "$agents_list" ] && snapshot_mode=1
|
|
173
|
+
|
|
174
|
+
local stamp
|
|
175
|
+
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
176
|
+
local spec_dir="$PWD/mv-spec-${stamp}"
|
|
177
|
+
# (目录不预创建——python 校验失败 = 零产物:预创建会让"不合规
|
|
178
|
+
# 零产物"失效,实测 2026-09-09;成功时 gen_spec_skeleton 自建)
|
|
179
|
+
|
|
180
|
+
# 骨架生成 = start_discussion --spec-gen(唯一实现):agents/ 三态
|
|
181
|
+
# (显式占位骨架 / viewers 校验+快照 / 无)全部在 python 侧,bash
|
|
182
|
+
# 与 CLI 直用产出一致;viewers 校验失败时 python 退出非 0(不合规
|
|
183
|
+
# 根本不应该开始 spec-gen)
|
|
184
|
+
local skeleton_args=(--spec-gen "$spec_dir" --topic "$topic")
|
|
185
|
+
[ -n "$background" ] && skeleton_args+=(--background "$background")
|
|
186
|
+
if [ "$snapshot_mode" -eq 0 ]; then
|
|
187
|
+
skeleton_args+=(--agents "$agents_list")
|
|
188
|
+
fi
|
|
189
|
+
"$PYTHON" "$START_DISCUSSION" "${skeleton_args[@]}" || {
|
|
190
|
+
fail "spec 骨架生成失败(start_discussion --spec-gen,见上方错误)"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
cat <<OUTPUT_EOF
|
|
194
|
+
已生成分析 spec:
|
|
195
|
+
$spec_dir
|
|
196
|
+
|
|
197
|
+
请查看/编辑该目录,补充背景、各 agent 视角等。
|
|
198
|
+
编辑完成后,告诉我"继续",我会自动启动分析。
|
|
199
|
+
OUTPUT_EOF
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
cmd_start() {
|
|
203
|
+
local spec_dir="$1"
|
|
204
|
+
shift # 余参 = 透传给 python 的选项(如 --fork-mode X)
|
|
205
|
+
require_dir "$spec_dir"
|
|
206
|
+
[ -f "$spec_dir/question.md" ] || fail "spec 缺少 question.md: $spec_dir"
|
|
207
|
+
|
|
208
|
+
local session_id="${PI_SESSION_ID:-}"
|
|
209
|
+
local stamp
|
|
210
|
+
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
211
|
+
# 目录前缀 mv-:与 pi-agents-helper 的 discuss-<sid>-* **命名空间隔离**
|
|
212
|
+
# ——两个系统的 extension 都按"同 sid 最新目录"发现目标,共用前缀会
|
|
213
|
+
# 在同 session 并发两套时互相插错(且 human 消息格式兼容 → 静默写错)。
|
|
214
|
+
# 前缀也是产品语言的统一(本项目用"分析",不再沿用"discuss")。
|
|
215
|
+
local dir_name="mv"
|
|
216
|
+
if [ -n "$session_id" ]; then
|
|
217
|
+
dir_name="${dir_name}-${session_id}"
|
|
218
|
+
fi
|
|
219
|
+
dir_name="${dir_name}-${stamp}"
|
|
220
|
+
local dir_path="$PWD/$dir_name"
|
|
221
|
+
|
|
222
|
+
# fork 源解析已收归 python(resolve_fork_source:env + sessions 编码
|
|
223
|
+
# glob,2026-09-09 结构收敛;解析失败 python 内部报错退出)
|
|
224
|
+
# 第 1 步:创建分析环境(不启动;fork 源自动解析)
|
|
225
|
+
# 配额(max-meeting/max-rr)是环境属性:唯一默认在 python argparse
|
|
226
|
+
# (10/7),创建时固化 protocol.json——wrapper 不传(W1 双源分叉修复:
|
|
227
|
+
# 此前 wrapper 5 与 python 7 不一致,两条入口 RR 上限差 40%)
|
|
228
|
+
# 余参哑转发(P0,e2e10 评审:wrapper 不得静默丢弃参数——值识别/默认值
|
|
229
|
+
# 的唯一家在 python;本层只转发)
|
|
230
|
+
if ! "$PYTHON" "$START_DISCUSSION" --dir "$dir_path" --spec "$spec_dir" "$@"; then
|
|
231
|
+
fail "环境创建失败,请查看上方输出"
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# spec 已被消费:只删**本工具生成的形态**(mv-spec-*)——用户自建的
|
|
235
|
+
# spec 目录不动(可能是有价值的视角快照)。删除前提示一行(此前静默
|
|
236
|
+
# 删除:spec 是视角任务书/背景/models 在用户侧的唯一副本,删了不可恢复)
|
|
237
|
+
case "$(basename "$spec_dir")" in
|
|
238
|
+
mv-spec-*)
|
|
239
|
+
echo "[start] spec 已消费,删除(本工具生成形态): $spec_dir"
|
|
240
|
+
rm -rf "$spec_dir"
|
|
241
|
+
;;
|
|
242
|
+
*)
|
|
243
|
+
echo "[start] spec 已消费(保留未删——非 mv-spec-* 形态): $spec_dir"
|
|
244
|
+
;;
|
|
245
|
+
esac
|
|
246
|
+
|
|
247
|
+
# 第 3 步:启动已有环境
|
|
248
|
+
if ! "$PYTHON" "$START_DISCUSSION" --dir "$dir_path" --skip-setup --start; then
|
|
249
|
+
fail "启动失败,请查看上方输出"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
cat <<OUTPUT_EOF
|
|
253
|
+
多视角分析已启动
|
|
254
|
+
目录: $dir_path
|
|
255
|
+
|
|
256
|
+
观看分析(复制执行,不进 LLM;Ctrl-C 中断后可插话再续看):
|
|
257
|
+
!!python3 "$ROOT_DIR/human_viewer.py" $dir_path --follow
|
|
258
|
+
|
|
259
|
+
查看进展: $0 --view $dir_path
|
|
260
|
+
插话: $0 --say $dir_path "<文本>"
|
|
261
|
+
查看状态: $0 --status $dir_path
|
|
262
|
+
等待完成: $0 --wait $dir_path
|
|
263
|
+
清理: $0 --cleanup $dir_path
|
|
264
|
+
|
|
265
|
+
说明:
|
|
266
|
+
- human 通道:--view 增量查看(主 pi 记录末尾 HEAD 作下轮 --since);
|
|
267
|
+
--say 插话(agents 可见并可回应;已冻结 agent 不响应)
|
|
268
|
+
- 完成后 result.md 自动保存到固定位置:$dir_path-result.md(与分析目录同级——resultWriter loop 退出时保存;cleanup 也会保存)
|
|
269
|
+
- 读取 result.md 摘要后请执行 --cleanup 清理分析目录
|
|
270
|
+
OUTPUT_EOF
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if [ "$#" -ge 1 ]; then
|
|
274
|
+
case "$1" in
|
|
275
|
+
--prepare)
|
|
276
|
+
shift
|
|
277
|
+
cmd_prepare "$@"
|
|
278
|
+
exit $?
|
|
279
|
+
;;
|
|
280
|
+
--start)
|
|
281
|
+
[ "$#" -ge 2 ] || fail "--start 需要 spec 目录参数"
|
|
282
|
+
shift
|
|
283
|
+
cmd_start "$@"
|
|
284
|
+
exit $?
|
|
285
|
+
;;
|
|
286
|
+
--status)
|
|
287
|
+
[ "$#" -ge 2 ] || fail "--status 需要分析目录参数"
|
|
288
|
+
cmd_status "$2"
|
|
289
|
+
exit $?
|
|
290
|
+
;;
|
|
291
|
+
--report)
|
|
292
|
+
[ "$#" -ge 2 ] || fail "--report 需要分析目录参数"
|
|
293
|
+
cmd_report "$2"
|
|
294
|
+
exit $?
|
|
295
|
+
;;
|
|
296
|
+
--wait)
|
|
297
|
+
[ "$#" -ge 2 ] || fail "--wait 需要分析目录参数"
|
|
298
|
+
cmd_wait "$2"
|
|
299
|
+
exit $?
|
|
300
|
+
;;
|
|
301
|
+
--cleanup)
|
|
302
|
+
[ "$#" -ge 2 ] || fail "--cleanup 需要分析目录参数"
|
|
303
|
+
cmd_cleanup "$2"
|
|
304
|
+
exit $?
|
|
305
|
+
;;
|
|
306
|
+
--view)
|
|
307
|
+
[ "$#" -ge 2 ] || fail "--view 需要分析目录参数"
|
|
308
|
+
shift
|
|
309
|
+
cmd_view "$@"
|
|
310
|
+
exit $?
|
|
311
|
+
;;
|
|
312
|
+
--say)
|
|
313
|
+
[ "$#" -ge 3 ] || fail "--say 需要分析目录和文本参数"
|
|
314
|
+
cmd_say "$2" "$3"
|
|
315
|
+
exit $?
|
|
316
|
+
;;
|
|
317
|
+
-h|--help)
|
|
318
|
+
usage
|
|
319
|
+
exit 0
|
|
320
|
+
;;
|
|
321
|
+
esac
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
usage >&2
|
|
325
|
+
exit 2
|