pi-multi-viewers 0.3.0 → 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.
@@ -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 默认 max 隐式——只有非 max 才写 `, variant`,日常更简洁;
281
- # model/thinking 预填主 pi 当前值,用户少改一个文件)
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[, variant]。"
285
- "model 默认 default,variant 默认 max(只有不用 max 才写 variant)。"
287
+ lines = ["# models.md——模型配置(可选)。每行:agent名: model, variant。"
288
+ "model default = 继承本机默认;variant 不写 = "
289
+ f"{DEFAULT_THINKING}(**两个槽都显式写出**,一眼可见)。"
286
290
  "本行是说明行,不会注入。"]
287
291
  for p in participants:
288
- if pm and pt:
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, pure=False,
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
- if pure:
389
- proto["pure"] = True
391
+ # 显式写出扩展策略(默认零扩展 = agents 不加载任何外部扩展)
392
+ proto["extensions"] = bool(extensions)
390
393
  if fork_source:
391
394
  # fork 模式(多视角):首唤挂载主 session + cwd=主项目
392
395
  # forkMode 取值域与默认值的定义在 meeting_fs(FORK_MODES /
@@ -3,7 +3,7 @@
3
3
 
4
4
  用法:
5
5
  python3 start_discussion.py --dir mymeet --topic "主题" --agents a,b \
6
- [--stances '{"a": "立场1", "b": "立场2"}'] [--start] [--pure] \
6
+ [--stances '{"a": "立场1", "b": "立场2"}'] [--start] [--extensions] \
7
7
  [--models '{"a": "provider/model"}'] [--max-meeting 10] [--max-rr 7]
8
8
 
9
9
  复杂内容用 spec 规格目录(设计 16,与 CLI 内容参数互斥):
@@ -70,8 +70,11 @@ def _spec_models(spec_dir, participants):
70
70
  """解析 models.md(容错,用户 8024/9204 定):返回 {agent: (model, variant)}。
71
71
 
72
72
  每行格式:`agent名: model, variant`(model 与 variant 逗号分隔)。
73
- - model 缺省/'default' → None(创建时填本机默认模型)
74
- - variant 缺省/'default'/'max' → 'max'(默认档,专业用户才改)
73
+ - model 缺省/'default' → None(创建时填本机默认模型)——`default`
74
+ 这个别名**只在 model 槽有意义**
75
+ - variant 缺省 → DEFAULT_THINKING;**没有 `default` 别名**(同词在相邻
76
+ 两行反义会让读者必错;且空已是缺省,别名不增加表达力)——写了 `default`
77
+ 就按原值透传给 pi(可见失败,而非静默重解释)
75
78
  容错:空行/无 ':'/agent 不在 participants → 跳过;单字段行只有 model。
76
79
  规则:第一行说明跳过(_spec_read)。
77
80
  """
@@ -93,8 +96,13 @@ def _spec_models(spec_dir, participants):
93
96
  parts = [p.strip() for p in rest.split(",")]
94
97
  model = parts[0] if parts and parts[0] else ""
95
98
  variant = parts[1] if len(parts) > 1 else ""
99
+ # `default` **只在 model 槽有意义**(= 继承本机)。variant 槽不做
100
+ # 同名别名(e2e17 评审 §7.2:相邻两行同词反义——model 的 default
101
+ # = 继承,variant 的 default = max,按上一行直觉读下一行必错);
102
+ # 且别名并不增加表达力:空 = max 已是缺省。写 `default` 不再是
103
+ # 别名 → 原值传给 pi(可见的失败,而不是静默重解释成 max)。
96
104
  m = None if (not model or model == "default") else model
97
- v = "max" if (not variant or variant == "default") else variant
105
+ v = variant or meeting_fs.DEFAULT_THINKING
98
106
  out[agent] = (m, v)
99
107
  return out
100
108
 
@@ -303,21 +311,23 @@ def setup_environment(args, participants, base, spec_dir=None,
303
311
  for p in participants}
304
312
  agent_extra = {k: v for k, v in agent_extra.items() if v}
305
313
  # models:spec 模式从 models.md 读(自包含,{agent: (model, variant)}),
306
- # CLI --models 已互斥({agent: model} 旧格式——variant 用默认 max)
314
+ # CLI --models 已互斥({agent: model} 旧格式——variant 用默认档)
307
315
  if spec_dir:
308
316
  models = _spec_models(spec_dir, participants)
309
317
  else:
310
- models = {p: (m, "max") for p, m in (args.models or {}).items()}
311
- # default 模型 创建时实时获取 Pi 默认模型填入:
312
- # pi-agent.json model 后,meeting_loop 才会传 --model;
313
- # 骨架期 models.md 仍写 default(--spec-gen 不获取),创建时(--spec)
314
- # 才解析。运行期固化不变(环境自包含)。
315
- if spec_dir:
316
- dm = _default_model()
317
- if dm:
318
- for p in participants:
319
- if p not in models or models[p][0] is None:
320
- models[p] = (dm, models.get(p, (None, "max"))[1])
318
+ models = {p: (m, meeting_fs.DEFAULT_THINKING)
319
+ for p, m in (args.models or {}).items()}
320
+ # 归一:每个 participant 都有 (model, variant) 条目,**此后只读**——
321
+ # 此前 :320 补默认模型与 :384 写 pi-agent.json 各自再写一遍
322
+ # `models.get(p, (None, "max"))`(同层同义重复,值两处声明,e2e17
323
+ # 评审 §7.10)。
324
+ # default 模型 → 创建时实时获取 Pi 默认模型填入:pi-agent.json 带
325
+ # model 后,meeting_loop 才会传 --model;骨架期 models.md 仍写 default
326
+ # (--spec-gen 不获取),创建时(--spec)才解析。运行期固化不变。
327
+ dm = _default_model() if spec_dir else None
328
+ for p in participants:
329
+ m, v = models.get(p, (None, meeting_fs.DEFAULT_THINKING))
330
+ models[p] = (m or dm, v)
321
331
  # stance_ref(agent 定义"立场见 question.md"提示):spec 模式一律保留
322
332
  # (设计 16.6:无法程序判断 question.md 有无立场节 → 一律提示;
323
333
  # 互斥下 CLI stances 必为 None,传占位 dict 触发生成)
@@ -340,7 +350,7 @@ def setup_environment(args, participants, base, spec_dir=None,
340
350
  # 共享配置(work-a 提交,setup commit 进 bare)
341
351
  with open(os.path.join(wa, "protocol.json"), "w") as f:
342
352
  json.dump(gen_protocol(spec_topic or args.topic, participants, args.max_meeting,
343
- args.max_rr, args.pure, args.result_writer,
353
+ args.max_rr, args.extensions, args.result_writer,
344
354
  args.stall_timeout,
345
355
  fork_source=getattr(args, "fork_source", None),
346
356
  fork_cwd=os.getcwd(),
@@ -381,14 +391,14 @@ def setup_environment(args, participants, base, spec_dir=None,
381
391
  with open(os.path.join(workdir, "AGENTS.md"), "w") as f:
382
392
  f.write(gen_agents_md(args, p, participants, spec_background,
383
393
  main_pi_cwd=os.getcwd()))
384
- mv = models.get(p, (None, "max"))
394
+ mv = models[p] # 归一后必有条目(见上方归一循环)
385
395
  with open(os.path.join(workdir, ".pi/agent", f"{p}.md"), "w") as f:
386
396
  f.write(gen_agent_def(p, participants, {p: mv[0]} if mv[0] else None,
387
397
  stances_arg, agent_extra.get(p)))
388
398
  with open(os.path.join(workdir, "pi-agent.json"), "w") as f:
389
399
  json.dump({
390
400
  "model": mv[0] or "",
391
- "thinking": mv[1] if mv[1] else "max",
401
+ "thinking": mv[1] or meeting_fs.DEFAULT_THINKING,
392
402
  "prompt_file": f".pi/agent/{p}.md",
393
403
  }, f, indent=2, ensure_ascii=False)
394
404
 
@@ -409,7 +419,8 @@ def setup_environment(args, participants, base, spec_dir=None,
409
419
  rw = args.result_writer or participants[-1]
410
420
  print(f"[setup] 环境就绪: {base}({len(participants)} agents: {', '.join(participants)})")
411
421
  print(f"[setup] resultWriter={rw}, maxMeeting={args.max_meeting}, maxRR={args.max_rr}, "
412
- f"立场={'有' if (args.stances or spec_dir) else '无'}, pure={args.pure}")
422
+ f"立场={'有' if (args.stances or spec_dir) else '无'}, "
423
+ f"extensions={args.extensions}")
413
424
 
414
425
 
415
426
  def _preserve_result_md(base):
@@ -539,7 +550,9 @@ def main():
539
550
  "protocol.json,各 agent 首唤由本地生成 fork "
540
551
  "源挂载主上下文;不传 = 从主 pi 环境自动解析"
541
552
  "(PI_SESSION_ID;解析失败明确报错)")
542
- parser.add_argument("--pure", action="store_true", help="--pure 模式(禁外部插件)")
553
+ parser.add_argument("--extensions", action="store_true",
554
+ help="让 agents 加载外部扩展(默认零扩展:不加载任何"
555
+ "外部扩展/技能/prompt-template/主题)")
543
556
  parser.add_argument("--start", action="store_true", help="创建后启动讨论")
544
557
  parser.add_argument("--skip-setup", action="store_true",
545
558
  help="跳过环境生成,只启动已有环境(需 --dir)")
@@ -691,12 +704,12 @@ def main():
691
704
  workdir = os.path.join(base, f"work-{p}")
692
705
  cmd = [sys.executable, os.path.join(base, "meeting_loop.py"),
693
706
  workdir, p]
694
- if args.pure:
695
- cmd.append("--pure")
707
+ if args.extensions:
708
+ cmd.append("--extensions")
696
709
  # 配额(max-meeting/max-rr/stall-timeout)是环境属性:创建时
697
710
  # 固化在 protocol.json,启动继承(loop 读 protocol 优先)。
698
711
  # 不传 CLI —— 避免无条件覆盖 protocol.json 的固化值
699
- # (审核 C1:配额单一事实源;与 pure 处理一致)
712
+ # (审核 C1:配额单一事实源;与 extensions 处理一致)
700
713
  with open(os.path.join(base, f"loop-{p}.log"), "w") as f:
701
714
  procs.append(subprocess.Popen(cmd, stdout=f,
702
715
  stderr=subprocess.STDOUT,
@@ -42,14 +42,19 @@ fork 机制已让每个 agent 携带发起分析时的对话上下文(默认 b
42
42
 
43
43
  - 分析只聚焦某些方面("不讨论 API 设计")
44
44
  - 已知的硬约束("必须保持向后兼容")
45
- - 用户指定的优先级("性能问题优先级最高")
45
+ - 用户指定的优先级("效率问题优先级最高")
46
46
 
47
47
  没有就留空。
48
48
 
49
49
  ## models.md —— 模型配置(可选)
50
50
 
51
- 每行:`agent名: model[, variant]`。model 默认 default(继承本机默认),
52
- variant 默认 max。只有不用默认/-max 时才需要改。
51
+ 每行:`agent名: model, variant`(**两个槽都显式写出**,一眼可见本场跑在
52
+ 什么模型/档位上)。model 写 `default` = 继承本机默认;variant 不写 =
53
+ `max`。想换档位(如 `high`)改这一行即可——**逐 agent 一行**,可以只改
54
+ 某一个视角。
55
+
56
+ > 这里写的就是**声明值**;实际生效值由 pi 解析(它自己的优先级链)。
57
+ > `--report` 会把声明值与 session 里的生效值对照列出。
53
58
 
54
59
  ## agents/X.md —— 视角任务书(每个 agent 一个)
55
60
 
@@ -64,7 +69,7 @@ variant 默认 max。只有不用默认/-max 时才需要改。
64
69
 
65
70
  写好视角任务书的要点(实测有效的措辞模式):
66
71
 
67
- 1. **单一视角**:明确写出这个 agent 用什么 lenses 看(性能/简单化/
72
+ 1. **单一视角**:明确写出这个 agent 用什么 lenses 看(效率/简单化/
68
73
  安全/成本/用户体验/……),并要求"所有观点必须从该视角出发"
69
74
  2. **不越界**:写明"其它视角由别的参与者负责,你不要越界展开"——
70
75
  实验证明这句能避免视角串味(不要列举具体是哪几个视角——
@@ -72,10 +77,15 @@ variant 默认 max。只有不用默认/-max 时才需要改。
72
77
  3. **交锋义务**:写明"对其它视角的观点可以认同或反驳,但要用本视角
73
78
  的论据"——防止附和式讨论
74
79
 
75
- 范例(agents/性能.md 全文——身份那句由脚本加,不在正文里):
80
+ 范例(agents/效率.md 全文——身份那句由脚本加,不在正文里;本例内容较长,
81
+ 要点是"把该视角盯住的东西逐项写明"):
82
+
83
+ 你的所有观点必须从运行效率角度出发:时间效率(一次分析从开始到收尾要
84
+ 多久、哪些环节在吃时间、哪些等待可消除)与运行效率(不必要的计算与
85
+ 等待、可并行而串行的部分、随规模增长的代价)。
86
+ 能用已有数据说出"省多少、占多少比例"就说;拿不到数据就写明需要什么
87
+ 数据,不要凭感觉下结论。
76
88
 
77
- 你的所有观点必须从性能角度出发:复杂度、热点、不必要的计算、扩展性。
78
- 如果其它视角的优化建议会显著损害性能,你应该明确反对并说明理由。
79
89
  其它视角由别的参与者负责,你不要越界展开。
80
90
  对其它视角的观点可以认同或反驳,但要用本视角的论据。
81
91