pi-multi-viewers 0.2.2 → 0.4.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 +26 -3
- package/README.md +7 -5
- package/docs/design.md +182 -2
- package/docs/reviews/2026-09-12-e2e17-thinking-level-analysis.md +374 -0
- package/docs/reviews/2026-09-13-e2e19-scoped-config-review.md +126 -0
- package/docs/reviews/2026-09-13-e2e20-pure-run-review.md +159 -0
- package/docs/reviews/2026-09-13-e2e21-postfix-review.md +257 -0
- package/docs/reviews/2026-09-13-e2e23-time-breakdown-analysis.md +175 -0
- package/docs/reviews/README.md +7 -2
- package/docs/test-methodology.md +100 -0
- package/fake_agent.py +11 -3
- package/meeting_fs.py +49 -5
- package/meeting_loop.py +46 -24
- package/mv_cli.py +363 -0
- package/observability.py +351 -29
- package/package.json +1 -1
- package/prompts/multi-viewers.md +1 -1
- package/scripts/check-residue.sh +39 -17
- package/scripts/mv.sh +10 -357
- package/scripts/pi-probe.sh +134 -0
- package/spec_gen.py +20 -17
- package/start_discussion.py +37 -24
- package/templates/spec-readme.md.tpl +17 -7
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" "$@"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# pi-probe.sh —— LLM 探针(把临时 pi 运行变成**可追溯、经同意的测试产物**)
|
|
4
|
+
#
|
|
5
|
+
# 它管三件事(都是吃过的教训):
|
|
6
|
+
#
|
|
7
|
+
# 1. **闸门:没有当次同意就不跑**(2026-09-13 教训)
|
|
8
|
+
# 我在未征得用户同意的情况下连跑 6 个真实 pi 进程(合计 ~9.5 分钟,其中一个
|
|
9
|
+
# 458s),理由是"在后台跑、不占用户终端"。判据本应是**预估时长**(用户规则:
|
|
10
|
+
# 秒级冒烟免批;分钟级/多轮必须当次确认),且**不因后台运行而豁免**。
|
|
11
|
+
# 现在:不带 `--approved "<凭据>"` 直接**拒绝执行**并提示先去问。
|
|
12
|
+
#
|
|
13
|
+
# 2. **登记:跑过的 LLM 运行留下账本**
|
|
14
|
+
# `~/.pi/pi-multi-viewers-llm-runs.log` 每行一次运行:起止时间、时长、退出码、
|
|
15
|
+
# 凭据、命令摘要。汇报时给账本,不靠记忆。
|
|
16
|
+
#
|
|
17
|
+
# 3. **收尾提醒:跑完先汇报,再决定下一步**
|
|
18
|
+
# 结束时打印一行显式提醒——2026-09-13 的第二个错误正是"跑完没汇报就接着跑
|
|
19
|
+
# 第二批实验"。
|
|
20
|
+
#
|
|
21
|
+
# 另外它把新建的 session 登记进 `~/.pi/pi-multi-viewers-test-sessions.log`
|
|
22
|
+
# (与 build_bootstrap 同一份),使 check-residue 能判定归属(否则白名单 cwd
|
|
23
|
+
# 下的探针 session 完全不可见)。
|
|
24
|
+
#
|
|
25
|
+
# 用法:
|
|
26
|
+
# ./scripts/pi-probe.sh --approved "用户 2026-09-13 15:20 同意跑 AFT 隔离实验" \
|
|
27
|
+
# --print "ok"
|
|
28
|
+
# ./scripts/pi-probe.sh --approved "..." --no-extensions --print "ok"
|
|
29
|
+
#
|
|
30
|
+
# 退出码 = pi 的退出码(闸门拒绝时为 2)。
|
|
31
|
+
set -u
|
|
32
|
+
|
|
33
|
+
SESS_DIR="$HOME/.pi/agent/sessions"
|
|
34
|
+
REGISTRY="$HOME/.pi/pi-multi-viewers-test-sessions.log"
|
|
35
|
+
LEDGER="$HOME/.pi/pi-multi-viewers-llm-runs.log"
|
|
36
|
+
|
|
37
|
+
usage() {
|
|
38
|
+
cat >&2 <<'EOS'
|
|
39
|
+
用法: pi-probe.sh --approved "<凭据>" [pi 参数...]
|
|
40
|
+
|
|
41
|
+
--approved "<凭据>" 必填:用户**当次**同意的凭据(原话/时间/授权范围)。
|
|
42
|
+
没有它就说明还没问——先去问,不要绕过这里直接调 pi。
|
|
43
|
+
|
|
44
|
+
判据(用户规则):预估**时长**——秒级冒烟免批;预计 ≥30s、多轮唤醒、
|
|
45
|
+
整场分析一律**当次**确认,且不因"后台跑、不占用户终端"豁免。
|
|
46
|
+
EOS
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
APPROVED=""
|
|
50
|
+
if [ "$#" -gt 0 ] && [ "$1" = "--approved" ]; then
|
|
51
|
+
APPROVED="${2:-}"
|
|
52
|
+
shift 2 2>/dev/null || true
|
|
53
|
+
fi
|
|
54
|
+
if [ -z "$APPROVED" ]; then
|
|
55
|
+
echo "拒绝执行:没有 --approved(= 未取得用户当次同意)。" >&2
|
|
56
|
+
usage
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
if [ "$#" -eq 0 ]; then
|
|
60
|
+
echo "拒绝执行:缺少 pi 参数。" >&2
|
|
61
|
+
usage
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
cmd_brief="pi $*"
|
|
66
|
+
|
|
67
|
+
BEFORE="$(mktemp)"
|
|
68
|
+
AFTER="$(mktemp)"
|
|
69
|
+
trap 'rm -f "$BEFORE" "$AFTER"' EXIT
|
|
70
|
+
find "$SESS_DIR" -name '*.jsonl' -print 2>/dev/null | sort > "$BEFORE"
|
|
71
|
+
|
|
72
|
+
T0=$(date +%s)
|
|
73
|
+
pi "$@"
|
|
74
|
+
rc=$?
|
|
75
|
+
T1=$(date +%s)
|
|
76
|
+
|
|
77
|
+
# 账本(运行即登记:时长 / 退出码 / 凭据 / 命令摘要)
|
|
78
|
+
python3 - "$LEDGER" "$T0" "$T1" "$rc" "$APPROVED" "$cmd_brief" <<'PYEOF'
|
|
79
|
+
import json, sys
|
|
80
|
+
from datetime import datetime, timezone
|
|
81
|
+
ledger, t0, t1, rc, approved, cmd = sys.argv[1:7]
|
|
82
|
+
rec = {
|
|
83
|
+
"started": datetime.fromtimestamp(int(t0), timezone.utc).isoformat(),
|
|
84
|
+
"seconds": int(t1) - int(t0),
|
|
85
|
+
"rc": int(rc),
|
|
86
|
+
"approved": approved,
|
|
87
|
+
"cmd": cmd[:400],
|
|
88
|
+
}
|
|
89
|
+
with open(ledger, "a", encoding="utf-8") as f:
|
|
90
|
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
91
|
+
print(f"[pi-probe] 账本已记:{rec['seconds']}s rc={rec['rc']} | 同意凭据:{approved[:60]}")
|
|
92
|
+
PYEOF
|
|
93
|
+
|
|
94
|
+
# session 登记(归属判定用)
|
|
95
|
+
find "$SESS_DIR" -name '*.jsonl' -print 2>/dev/null | sort > "$AFTER"
|
|
96
|
+
NEW=$(comm -13 "$BEFORE" "$AFTER")
|
|
97
|
+
if [ -n "$NEW" ]; then
|
|
98
|
+
echo "$NEW" | while IFS= read -r f; do
|
|
99
|
+
[ -n "$f" ] || continue
|
|
100
|
+
python3 - "$f" "$REGISTRY" <<'PYEOF'
|
|
101
|
+
import json, os, sys
|
|
102
|
+
from datetime import datetime, timezone
|
|
103
|
+
path, registry = sys.argv[1], sys.argv[2]
|
|
104
|
+
sid = cwd = ""
|
|
105
|
+
with open(path, encoding="utf-8") as f:
|
|
106
|
+
for line in f:
|
|
107
|
+
try:
|
|
108
|
+
ev = json.loads(line)
|
|
109
|
+
except ValueError:
|
|
110
|
+
continue
|
|
111
|
+
if ev.get("type") == "session":
|
|
112
|
+
sid = ev.get("id") or ""
|
|
113
|
+
cwd = ev.get("cwd") or ""
|
|
114
|
+
break
|
|
115
|
+
rec = {
|
|
116
|
+
"created": datetime.now(timezone.utc).isoformat(),
|
|
117
|
+
"session_id": sid,
|
|
118
|
+
"path": os.path.abspath(path),
|
|
119
|
+
"cwd": cwd,
|
|
120
|
+
"origin": "pi-probe",
|
|
121
|
+
}
|
|
122
|
+
with open(registry, "a", encoding="utf-8") as f:
|
|
123
|
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
124
|
+
print(f"[pi-probe] session 已登记: {path}")
|
|
125
|
+
print(f"[pi-probe] session_id={sid} cwd={cwd}(用完删掉该文件)")
|
|
126
|
+
PYEOF
|
|
127
|
+
done
|
|
128
|
+
else
|
|
129
|
+
echo "[pi-probe] 未新建 session(用了 --session/--session-id 续接既有会话?)"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# 收尾提醒(2026-09-13 教训:跑完先汇报,别接着跑下一批)
|
|
133
|
+
echo "[pi-probe] ⚠ 先把本次结果汇报给用户并停下等他指示——不要连续追加第二批实验。"
|
|
134
|
+
exit "$rc"
|
package/spec_gen.py
CHANGED
|
@@ -22,7 +22,7 @@ PI_AGENT_DIR = os.environ.get("PI_CODING_AGENT_DIR",
|
|
|
22
22
|
MAX_AGENT_NAME_LEN = 32
|
|
23
23
|
|
|
24
24
|
import meeting_fs
|
|
25
|
-
from meeting_fs import run_git, DEFAULT_STALL_TIMEOUT
|
|
25
|
+
from meeting_fs import run_git, DEFAULT_STALL_TIMEOUT, DEFAULT_THINKING
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def _join_model_ref(provider, model_id):
|
|
@@ -276,24 +276,27 @@ def gen_spec_skeleton(spec_dir, participants, topic=None, background=None,
|
|
|
276
276
|
"本行是说明行,不会注入。\n\n")
|
|
277
277
|
if background:
|
|
278
278
|
f.write(background + "\n")
|
|
279
|
-
# models.md(用户 8024/9204/9271:预列各 agent,每行 agent名: model
|
|
280
|
-
# variant
|
|
281
|
-
#
|
|
279
|
+
# models.md(用户 8024/9204/9271:预列各 agent,每行 agent名: model,
|
|
280
|
+
# variant;model/thinking 预填主 pi 当前值,用户少改一个文件)
|
|
281
|
+
# **两个槽都永远显式写出**(含探测失败的路径):variant 槽留空会静默落到
|
|
282
|
+
# DEFAULT_THINKING,而 spec 文件表面完全正常(甚至更"干净")——意图与
|
|
283
|
+
# 生效值之间没有留痕(e2e17 评审 §7.3)。探测失败另打一行可见提示。
|
|
282
284
|
pm, pt = _detect_pi_model_thinking()
|
|
285
|
+
variant = pt or DEFAULT_THINKING
|
|
283
286
|
with open(os.path.join(spec_dir, "models.md"), "w") as f:
|
|
284
|
-
lines = ["# models.md——模型配置(可选)。每行:agent名: model
|
|
285
|
-
"model
|
|
287
|
+
lines = ["# models.md——模型配置(可选)。每行:agent名: model, variant。"
|
|
288
|
+
"model 写 default = 继承本机默认;variant 不写 = "
|
|
289
|
+
f"{DEFAULT_THINKING}(**两个槽都显式写出**,一眼可见)。"
|
|
286
290
|
"本行是说明行,不会注入。"]
|
|
287
291
|
for p in participants:
|
|
288
|
-
|
|
289
|
-
lines.append(f"{p}: {pm}, {pt}")
|
|
290
|
-
elif pm:
|
|
291
|
-
lines.append(f"{p}: {pm}")
|
|
292
|
-
elif pt:
|
|
293
|
-
lines.append(f"{p}: default, {pt}")
|
|
294
|
-
else:
|
|
295
|
-
lines.append(f"{p}: default")
|
|
292
|
+
lines.append(f"{p}: {pm or 'default'}, {variant}")
|
|
296
293
|
f.write("\n".join(lines) + "\n")
|
|
294
|
+
if not pt:
|
|
295
|
+
# 探测失败**可见**(不是静默取档):打印在使用者能看到的地方,
|
|
296
|
+
# 不阻断(终端直用时 PI_* 环境变量本就不存在,报错会断掉合法路径)。
|
|
297
|
+
print(f"[spec-gen] 未探测到主 pi 的 thinking 档位——models.md 用默认 "
|
|
298
|
+
f"{DEFAULT_THINKING}(如需其它档位请直接编辑该文件)",
|
|
299
|
+
file=sys.stderr)
|
|
297
300
|
# agents/X.md 占位 + .order(仅显式 --agents 时;快照路径的 .order
|
|
298
301
|
# 由 _snapshot_viewers 写入,此处不得重写)
|
|
299
302
|
if participants and not viewers_dir:
|
|
@@ -367,7 +370,7 @@ def gen_question(topic, stances, background, questions):
|
|
|
367
370
|
return "\n".join(lines)
|
|
368
371
|
|
|
369
372
|
|
|
370
|
-
def gen_protocol(topic, participants, max_meeting, max_rr,
|
|
373
|
+
def gen_protocol(topic, participants, max_meeting, max_rr, extensions=False,
|
|
371
374
|
result_writer=None,
|
|
372
375
|
stall_timeout=DEFAULT_STALL_TIMEOUT,
|
|
373
376
|
fork_source=None, fork_cwd=None,
|
|
@@ -385,8 +388,8 @@ def gen_protocol(topic, participants, max_meeting, max_rr, pure=False,
|
|
|
385
388
|
"stallTimeoutSeconds": stall_timeout,
|
|
386
389
|
"commitPolicy": "one-message-per-commit",
|
|
387
390
|
}
|
|
388
|
-
|
|
389
|
-
|
|
391
|
+
# 显式写出扩展策略(默认零扩展 = agents 不加载任何外部扩展)
|
|
392
|
+
proto["extensions"] = bool(extensions)
|
|
390
393
|
if fork_source:
|
|
391
394
|
# fork 模式(多视角):首唤挂载主 session + cwd=主项目
|
|
392
395
|
# forkMode 取值域与默认值的定义在 meeting_fs(FORK_MODES /
|