pi-multi-viewers 0.3.0 → 0.5.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 +37 -1
- package/README.md +7 -5
- package/docs/design.md +207 -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/2026-09-14-e2e24-extension-policy-review.md +146 -0
- package/docs/reviews/README.md +8 -2
- package/docs/test-methodology.md +85 -0
- package/meeting_fs.py +202 -5
- package/meeting_loop.py +90 -26
- package/mv_cli.py +1 -0
- package/observability.py +394 -29
- package/package.json +1 -1
- package/prompts/multi-viewers.md +1 -1
- package/scripts/check-residue.sh +39 -17
- package/scripts/pi-probe.sh +134 -0
- package/spec_gen.py +28 -21
- package/start_discussion.py +44 -25
- package/templates/spec-readme.md.tpl +17 -7
package/meeting_loop.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
复用 meeting_engine 的唯一状态机,只注入"唤醒 pi"的 responder。
|
|
5
5
|
协议逻辑(锁/配额/级联/信号)全在引擎,此处只做 LLM 交互。
|
|
6
6
|
|
|
7
|
-
用法:python3 meeting_loop.py <workdir> <agent> [--
|
|
7
|
+
用法:python3 meeting_loop.py <workdir> <agent> [--extension-policy V]
|
|
8
8
|
(配额/超时从 protocol.json 读——单一事实源;无 CLI 覆盖)
|
|
9
9
|
"""
|
|
10
10
|
|
|
@@ -289,7 +289,7 @@ def _prepare_fork_session(workdir, agent, sid, fork_source, fork_cwd,
|
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
|
|
292
|
-
session_dir, first_wake,
|
|
292
|
+
session_dir, first_wake, extension_policy, prompt,
|
|
293
293
|
fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
|
|
294
294
|
"""组装唤醒命令(#3 拆分,e2e7 评审):返回 (cmd, spawn_cwd)。
|
|
295
295
|
|
|
@@ -309,11 +309,50 @@ def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
|
|
|
309
309
|
else:
|
|
310
310
|
cmd = ["pi", "--mode", "json", "--session-id", sid,
|
|
311
311
|
"--session-dir", session_dir]
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
312
|
+
# ---- agent 进程的扩展策略(design.md 决策 20;三档见 meeting_fs)----
|
|
313
|
+
# 三档共用**同一段零扩展前缀**;mc-tools 只决定"能否再追加 -e <MC 入口>"。
|
|
314
|
+
# 为什么这样写:此前 mc-tools 的降级分支自己拼前缀后**提前 return**,把后面的
|
|
315
|
+
# model/thinking/system-prompt/--print 与 spawn cwd 全截断——那条"降级"路径在
|
|
316
|
+
# 无 MC 的机器上产出残缺命令(e2e24 评审 S1;根因 = 前缀抄三遍 + 短路返回)。
|
|
317
|
+
# 现在**只有一个出口**(本函数末尾),降级只是"少追加一个 -e"。
|
|
318
|
+
#
|
|
319
|
+
# 三档语义:mc-tools(默认)给 agents `ctx_search`(MC 的只读检索工具;
|
|
320
|
+
# entry 只注册工具、不装 hook → 不带 historian/压缩);none 零扩展(零依赖);
|
|
321
|
+
# all 走 pi 默认发现(A/B 与显式 opt-in,须有净收益账)。
|
|
322
|
+
no_ext = ["--no-extensions", "--no-skills", "--no-prompt-templates",
|
|
323
|
+
"--no-themes"]
|
|
324
|
+
effective_policy = extension_policy
|
|
325
|
+
downgrade_reason = ""
|
|
326
|
+
if extension_policy not in meeting_fs.EXTENSION_POLICIES:
|
|
327
|
+
# 值域守卫在 __main__ 已拦(配置错误不进 engine);直调也要响
|
|
328
|
+
raise RuntimeError(f"未知 extensionPolicy: {extension_policy!r}"
|
|
329
|
+
f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})")
|
|
330
|
+
if extension_policy == "none":
|
|
331
|
+
cmd += no_ext
|
|
332
|
+
elif extension_policy == "mc-tools":
|
|
333
|
+
entry, err = meeting_fs.resolve_mc_tools_entry()
|
|
334
|
+
if entry:
|
|
335
|
+
cmd += no_ext + ["-e", entry]
|
|
336
|
+
elif meeting_fs.mc_tools_strict():
|
|
337
|
+
# 严格模式(测试/探针保真):缺 MC 即响,不降级——否则测试可能在
|
|
338
|
+
# "没装 MC"的环境里通过,而 ctx_search 从未生效
|
|
339
|
+
log(agent, f"[fatal] mc-tools 档入口解析失败(严格模式):{err}")
|
|
340
|
+
raise RuntimeError(f"mc-tools 档不可用: {err}")
|
|
341
|
+
else:
|
|
342
|
+
# mc-tools **允许**而非要求 MC:缺 MC → 降级零扩展,但**可见**
|
|
343
|
+
effective_policy = "none"
|
|
344
|
+
downgrade_reason = err
|
|
345
|
+
cmd += no_ext
|
|
346
|
+
# "all":不加任何 --no-*(走 pi 默认发现)
|
|
347
|
+
if first_wake:
|
|
348
|
+
# 登记行(观测面的稳定字段;报告据此给"声明 vs 生效")。只在首唤打:
|
|
349
|
+
# 策略在一次运行内不变,变了也是配置错误(重跑即可)。
|
|
350
|
+
log(agent, f"扩展策略: 声明={extension_policy} 生效={effective_policy}"
|
|
351
|
+
f" strict={int(meeting_fs.mc_tools_strict())}"
|
|
352
|
+
+ (f" 降级原因={downgrade_reason}" if downgrade_reason else ""))
|
|
353
|
+
if downgrade_reason:
|
|
354
|
+
log(agent, f"mc-tools 档未生效({downgrade_reason})——本次按零扩展"
|
|
355
|
+
f"运行:ctx_search 不可用")
|
|
317
356
|
model = cfg.get("model") or ""
|
|
318
357
|
if model:
|
|
319
358
|
cmd += ["--model", model]
|
|
@@ -333,6 +372,22 @@ def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
|
|
|
333
372
|
return cmd, (fork_cwd or workdir)
|
|
334
373
|
|
|
335
374
|
|
|
375
|
+
def _spawn_env(workdir):
|
|
376
|
+
"""agent 进程的环境(Popen 的 env 是**整体替换** → 必须合并 os.environ,
|
|
377
|
+
否则丢 PATH)。
|
|
378
|
+
|
|
379
|
+
只注入 `GIT_CEILING_DIRECTORIES=<讨论目录>`:**git 上溯防护**(实现 A1)。
|
|
380
|
+
为什么需要:`_lock_git` 把 work-<agent>/.git 改名后,git 的默认行为是
|
|
381
|
+
**向上继续找仓库**——fork 模式下 cwd = 主项目,实测锁态下
|
|
382
|
+
`git rev-parse --git-dir` 从 workdir 发起会命中主项目 .git(rc=0),守卫
|
|
383
|
+
形同虚设(见 `_lock_git` docstring 的范围说明)。本进程的 argv 就是 agent
|
|
384
|
+
会话里 bash 工具所继承的环境来源。
|
|
385
|
+
(此前还注入 XDG_CONFIG_HOME 做作用域配置——随"屏蔽 AFT"整块退役。)
|
|
386
|
+
"""
|
|
387
|
+
env = {**os.environ, "GIT_CEILING_DIRECTORIES": os.path.dirname(workdir)}
|
|
388
|
+
return env
|
|
389
|
+
|
|
390
|
+
|
|
336
391
|
def _run_wake_proc(cmd, spawn_cwd, workdir, agent):
|
|
337
392
|
"""spawn + 分片等待(#3 拆分):返回 CompletedProcess。
|
|
338
393
|
|
|
@@ -348,17 +403,12 @@ def _run_wake_proc(cmd, spawn_cwd, workdir, agent):
|
|
|
348
403
|
# 成功)。性能实测 ≈150-200 KB/唤醒、峰值亚 MB(不构成风险);
|
|
349
404
|
# 若改为流式读取,必须让"谁读 session 头"同样显式可见(可读性保留票)。
|
|
350
405
|
#
|
|
351
|
-
# GIT_CEILING_DIRECTORIES
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
# 为什么需要:_lock_git 把 work-<agent>/.git 改名后,git 的默认行为是
|
|
355
|
-
# **向上继续找仓库**——fork 模式下 cwd=主项目,实测锁态下
|
|
356
|
-
# `git rev-parse --git-dir` 从 workdir 发起会命中主项目 .git(rc=0),
|
|
357
|
-
# 守卫形同虚设(见 _lock_git docstring 的范围说明)。
|
|
358
|
-
# 注:Popen 的 env 是**整体替换**,必须合并 os.environ(否则丢 PATH)。
|
|
406
|
+
# 环境构造统一在 _spawn_env(GIT_CEILING_DIRECTORIES 的 git 上溯防护
|
|
407
|
+
# 理由见该函数;brief:本进程的 argv 就是 agent 会话里 bash 工具所继承的
|
|
408
|
+
# 环境来源,而 _lock_git 只锁 work-<agent>/.git、git 默认会向上找仓库)。
|
|
359
409
|
proc = subprocess.Popen(cmd, cwd=spawn_cwd, stdout=subprocess.PIPE,
|
|
360
410
|
stderr=subprocess.PIPE, text=True,
|
|
361
|
-
env=
|
|
411
|
+
env=_spawn_env(workdir))
|
|
362
412
|
_current_proc = proc
|
|
363
413
|
try:
|
|
364
414
|
# 分片等待:每片检查讨论目录是否被清理(cleanup 删目录)——
|
|
@@ -387,7 +437,9 @@ def _run_wake_proc(cmd, spawn_cwd, workdir, agent):
|
|
|
387
437
|
err or "")
|
|
388
438
|
|
|
389
439
|
|
|
390
|
-
def wake_llm(workdir, agent, prompt,
|
|
440
|
+
def wake_llm(workdir, agent, prompt,
|
|
441
|
+
extension_policy=meeting_fs.DEFAULT_EXTENSION_POLICY,
|
|
442
|
+
fork_source=None, fork_cwd=None,
|
|
391
443
|
fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
|
|
392
444
|
"""唤醒 pi(fork-only:首唤由本地生成 fork 源 + `--session` 打开,
|
|
393
445
|
后续 `--session-id` 续接)。返回 (sessionID, returncode)。
|
|
@@ -412,7 +464,7 @@ def wake_llm(workdir, agent, prompt, pure=False, fork_source=None, fork_cwd=None
|
|
|
412
464
|
sid = str(uuid.uuid4())
|
|
413
465
|
cmd, spawn_cwd = _build_wake_cmd(workdir, agent, sid, cfg, fork_source,
|
|
414
466
|
fork_cwd, session_dir, first_wake,
|
|
415
|
-
|
|
467
|
+
extension_policy, prompt, fork_mode, topic)
|
|
416
468
|
|
|
417
469
|
log_dir = os.path.join(base, "wake-logs")
|
|
418
470
|
os.makedirs(log_dir, exist_ok=True)
|
|
@@ -489,7 +541,7 @@ def _read_perspective_brief(workdir, agent):
|
|
|
489
541
|
return brief or None
|
|
490
542
|
|
|
491
543
|
|
|
492
|
-
def make_responder(
|
|
544
|
+
def make_responder(extension_policy, fork_source=None, fork_cwd=None,
|
|
493
545
|
fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
|
|
494
546
|
"""构造真实 LLM responder:唤醒 pi,LLM 写内容文件。
|
|
495
547
|
|
|
@@ -514,7 +566,7 @@ def make_responder(pure, fork_source=None, fork_cwd=None,
|
|
|
514
566
|
if mem_available_mb() < MIN_MEM_MB:
|
|
515
567
|
log(agent, "内存不足——抛可恢复异常(不代写 freezing,下轮重试)")
|
|
516
568
|
raise RecoverableWakeError("内存不足")
|
|
517
|
-
wake_llm(workdir, agent, prompt,
|
|
569
|
+
wake_llm(workdir, agent, prompt, extension_policy,
|
|
518
570
|
fork_source=fork_source, fork_cwd=fork_cwd)
|
|
519
571
|
return True
|
|
520
572
|
if rr_turn:
|
|
@@ -534,7 +586,7 @@ def make_responder(pure, fork_source=None, fork_cwd=None,
|
|
|
534
586
|
if mem_available_mb() < MIN_MEM_MB:
|
|
535
587
|
log(agent, "内存不足——抛可恢复异常(不代写 freezing,下轮重试)")
|
|
536
588
|
raise RecoverableWakeError("内存不足")
|
|
537
|
-
wake_llm(workdir, agent, prompt,
|
|
589
|
+
wake_llm(workdir, agent, prompt, extension_policy,
|
|
538
590
|
fork_source=fork_source, fork_cwd=fork_cwd,
|
|
539
591
|
fork_mode=fork_mode, topic=topic)
|
|
540
592
|
return True
|
|
@@ -553,11 +605,17 @@ def _preserve_result_md(workdir):
|
|
|
553
605
|
if __name__ == "__main__":
|
|
554
606
|
if len(sys.argv) < 3:
|
|
555
607
|
print("用法: python3 meeting_loop.py <workdir> <agent> "
|
|
556
|
-
"[--max-meeting N] [--max-rr N] [--stall-timeout S]
|
|
608
|
+
"[--max-meeting N] [--max-rr N] [--stall-timeout S]"
|
|
609
|
+
" [--extension-policy none|mc-tools|all]")
|
|
557
610
|
sys.exit(1)
|
|
558
611
|
workdir, agent = sys.argv[1], sys.argv[2]
|
|
559
612
|
recover_git_lock(workdir, agent)
|
|
560
|
-
|
|
613
|
+
# 扩展策略:CLI 先填,协议字段(若有)覆盖——与 forkMode 同款
|
|
614
|
+
extension_policy = meeting_fs.DEFAULT_EXTENSION_POLICY
|
|
615
|
+
if "--extension-policy" in sys.argv:
|
|
616
|
+
i = sys.argv.index("--extension-policy")
|
|
617
|
+
if i + 1 < len(sys.argv):
|
|
618
|
+
extension_policy = sys.argv[i + 1]
|
|
561
619
|
mm, mr, st = 10, 7, 600
|
|
562
620
|
# 协议从 **bare HEAD** 读(单一来源 = 共享事实;本地副本 LLM 可改)——
|
|
563
621
|
# 与 engine participants()/check_status 同一原语
|
|
@@ -567,8 +625,14 @@ if __name__ == "__main__":
|
|
|
567
625
|
print(f"[fatal] protocol.json 读取失败(bare HEAD 无有效内容): {bare}",
|
|
568
626
|
flush=True)
|
|
569
627
|
sys.exit(1)
|
|
570
|
-
if proto.get("
|
|
571
|
-
|
|
628
|
+
if proto.get("extensionPolicy"):
|
|
629
|
+
extension_policy = proto["extensionPolicy"]
|
|
630
|
+
if extension_policy not in meeting_fs.EXTENSION_POLICIES:
|
|
631
|
+
# 值域守卫(与 forkMode 同款四层守卫之一:loop 门)——非法值不进 engine
|
|
632
|
+
# 重试路径,直接 fatal 退出(配置错误就该在启动时响)
|
|
633
|
+
print(f"[fatal] 非法 extensionPolicy: {extension_policy!r}"
|
|
634
|
+
f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})", flush=True)
|
|
635
|
+
sys.exit(1)
|
|
572
636
|
if proto.get("maxMeetingRounds"):
|
|
573
637
|
mm = proto["maxMeetingRounds"]
|
|
574
638
|
if proto.get("maxRRRounds"):
|
|
@@ -593,7 +657,7 @@ if __name__ == "__main__":
|
|
|
593
657
|
sys.exit(1)
|
|
594
658
|
try:
|
|
595
659
|
agent_loop(workdir, agent,
|
|
596
|
-
make_responder(
|
|
660
|
+
make_responder(extension_policy,
|
|
597
661
|
fork_source=fork_source,
|
|
598
662
|
fork_cwd=proto.get("forkCwd") or "",
|
|
599
663
|
fork_mode=fork_mode_cfg,
|
package/mv_cli.py
CHANGED
|
@@ -42,6 +42,7 @@ PROG = os.environ.get("MV_INVOKED_AS") or "scripts/mv.sh"
|
|
|
42
42
|
USAGE = f"""用法:
|
|
43
43
|
{PROG} --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
|
|
44
44
|
{PROG} --start <spec目录> [--fork-mode compaction|budget|full]
|
|
45
|
+
{PROG} --start <spec目录> [--extension-policy none|mc-tools|all]
|
|
45
46
|
{PROG} --status [dir]
|
|
46
47
|
{PROG} --report [dir]
|
|
47
48
|
{PROG} --wait [dir]
|