# -*- coding: utf-8 -*- """selftest —— 「多会话协作机制」的**回归测试**:把**所有异常情况**做成用例,每次改完跑一遍。 🔴 纪律(2026-09-29 用户定案):**每次改完 collabd.py / guard.py,必须跑本文件**,全绿才算改完。 设计原则: · **不碰生产**:整套测试用 `COLLABD_CONFIG` 指向 `tmp/selftest/`(独立 inbox / 看板 / 任务图), 生产台账与看板**一个字节都不动**。 · **离线可跑**:需要宿主库的用例,读不到就记 SKIP(⛔ 不假装通过)。 · **判据是"读数"不是"没报错"**:rc、文件是否生成、台账状态、投递台账增量 … · FAIL ⇒ **非零退出**(配合"rc=0 才是真成功")。 用法:python selftest.py # 全部 python selftest.py -k 投递 # 只跑名字含"投递"的用例 """ from __future__ import annotations # [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导) def _sm_load_roots(): import os as _os _here = _os.path.dirname(_os.path.abspath(__file__)) for _up in range(4): _p = _os.path.join(_here, *([".."] * _up), "roots.env") _p = _os.path.normpath(_p) if _os.path.isfile(_p): try: with open(_p, encoding="utf-8") as _f: for _ln in _f: _ln = _ln.strip() if _ln and not _ln.startswith("#") and "=" in _ln: _k, _v = _ln.split("=", 1) _os.environ.setdefault(_k.strip(), _v.strip()) except Exception: pass return _sm_load_roots() # 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干), # Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError # ⇒ 被顶层 handler 记成 `fatal`、**整轮失败**(实测:本包里连续 4 次 `fatal 'gbk' codec ...`)。 # ⇒ 出口一律 UTF-8 + errors="replace"(⛔ 不让"打不出字"升级成"程序死")。 try: import sys as _sys _sys.stdout.reconfigure(encoding="utf-8", errors="replace") _sys.stderr.reconfigure(encoding="utf-8", errors="replace") except Exception: pass import hashlib import json import os import re import shutil import subprocess import sys import time from pathlib import Path HERE = Path(__file__).resolve().parent WS = Path(os.environ.get("DSH_COLLAB_WS") or os.environ.get("DSH_WS_ROOT") or Path.cwd()) TEST_WS = WS / "tmp" / "selftest" TIN = TEST_WS / "inbox" CFG = TEST_WS / "collabd.config.json" PY = sys.executable RESULTS = [] # 🔴🔴 2026-10-04 新增:**本工作区是否真启用了这套机制**(判「生产/副本」用)。 # 起因(`vibe-product` 实测坐实):那是个**技能副本**(`.workbuddy/skills/session-mechanism/`), # **根本没启用机制** —— 连 `tmp/supervise-inbox/goal.json` 与 `.workbuddy/collab/collabd.py` # 都不存在 ⇒ 那些「量**生产工作区部署齐全**」的判据(交付物/目标执行状态.md、peer 配置、 # 目标检查脚本、跨区 peer 格)**必然报红** ⇒ 副本被当生产环境自测。 # ⚠️ 这**不是技能有错**,是判据没声明适用范围 ⇒ 用**跳过**表达,⛔ 不许改成"永远绿"(那是把判据废掉)。 def _mech_on() -> bool: """本工作区**启用了**会话协作机制吗?判据=真源文件在不在(⛔ 不看 env、不猜)。""" try: return (WS / "tmp" / "supervise-inbox" / "goal.json").exists() except Exception: return False MECH_ON = _mech_on() def _strip_comments_and_docs(src: str) -> str: """把 Python 源码里的**注释 + 字符串(含 docstring)**全部剔掉,只留可执行骨架。 🔴🔴 2026-10-05 新增。**为什么必须有一个共用实现**(⛔ 不许各用例各写一份): 本项目有大量"判据查源码里有没有某字面"的用例。而本仓库的**考古纪律**要求: 废弃形态的说明**必须留在注释/docstring 里**(写明"曾怎么做、为什么废弃")。 ⇒ 那些字面**注定存在** ⇒ 拿全文 `not in src` 判「已清除」**必然误红**。 ⚠️ **同一个坑我今天踩了两遍**: ① `t_declare_resets_lifecycle`:只剔 `#` 行 ⇒ 这次对了(那句在 `#` 注释里); ② 本条守卫:`-File start-supervise.ps1` 那句在 **docstring** 里 ⇒ 只剔 `#` 行 **照样命中** ⇒ 又误红一次。 ⇒ 结论:**「剔注释」≠「剔 docstring」**,必须用 `tokenize` 一次剔干净 (⛔ 别用正则凑 —— 多行字符串、嵌套引号、f-string 都会漏)。 ⛔ 用 `tokenize` 失败(比如源码本身语法坏)时**回落到"保留原文"**: 宁可误红(能被看见)也不许误绿(静默放行)。 🔴🔴 **2026-10-05 二次修正(又踩一坑)**:第一版把 `STRING` **全剔**,结果把 「写在 PowerShell 命令**字符串里**的字面」也剔掉了(`New-ScheduledTaskAction`、 `supervise-launch.py` 正是**字符串内容**)⇒ 判据**由误红变成误红**(换个方向)。 ✅ 正解=**只剔两样**:① `#` 注释;② **docstring**(用 `ast` 精确定位,⛔ 不用正则)。 普通字符串**一律保留** —— 因为"代码里干了什么"经常就写在字符串里(PowerShell 命令、 文件名、任务名……),剔掉等于**看不见真实现**。 """ import ast as _ast import io import tokenize as _tk # ① 先用 ast 抓出所有 docstring 的**起止行列**(模块/类/函数首表达式字符串) _doc_spans = [] try: _tree = _ast.parse(src) for _node in _ast.walk(_tree): if isinstance(_node, (_ast.Module, _ast.ClassDef, _ast.FunctionDef, _ast.AsyncFunctionDef)): _b = getattr(_node, "body", None) if (_b and isinstance(_b[0], _ast.Expr) and isinstance(_b[0].value, _ast.Constant) and isinstance(_b[0].value.value, str)): _doc_spans.append((_b[0].value.lineno, getattr(_b[0].value, "end_lineno", _b[0].value.lineno))) except Exception: _doc_spans = [] def _in_doc(ln): return any(a <= ln <= b for a, b in _doc_spans) # ② 逐 token 剔:注释一律剔;字符串**只在 docstring 位置**剔 try: out = [] for tok in _tk.generate_tokens(io.StringIO(src).readline): if tok.type == _tk.COMMENT: continue if tok.type == _tk.STRING and _in_doc(tok.start[0]): continue out.append(tok.string) return " ".join(out) except Exception: return src def _mk_env() -> dict: env = dict(os.environ) env["COLLABD_CONFIG"] = str(CFG) env["DSH_COLLAB_WS"] = str(TEST_WS) # 🔴 2026-10-02(S8):**测试环境里⛔ 不许起常驻**。 # 真因(本棒实测):`--tick` 现在会顺手"续命常驻"(`ensure_supervise()`),而自测用的 # `--tick` 继承了真环境的网关口令 ⇒ 它会在**测试工作区**里起一条真的 `--supervise` # ⇒ 那条进程会持续 `supervise()` 并**重写测试夹具**(实测:`已停总闸` 用例报 # `告警投影被清掉` 假红、且 FAIL 数每轮不同)+ 留下僵尸测试进程。 # ⇒ 显式关掉(⛔ 不是"把断言写松")。 env["COLLABD_NO_ENSURE"] = "1" return env def _prepare() -> None: TIN.mkdir(parents=True, exist_ok=True) for _f in ("wake.lock", "guard.stop", "goal.pending.json", # 🔴 2026-10-01 加:退场表也每轮清 —— 它会**跨轮留下来**, # 而退场用例断言的是精确集合(`retired==1 / stale==1`)⇒ # 上一轮留下的那条会在下一轮把计数顶成 2 ⇒ **第二次跑才红的假红**(最难查的一型)。 "retired.json"): # 清上一轮残留(防跨轮假红) try: (TIN / _f).unlink() except Exception: pass # 🔴 2026-10-01 加:**假配置根(假的会话日志)也每轮清空** —— 这棵树的文件会跨轮留下来, # 而「哑会话」用例断言的是**精确集合**(`d == {DEAF}`)⇒ 上一轮留下的昨天那份 10 MiB # 会在下一轮把结果多出几条 ⇒ **第二次跑才红的假红**(最难查的一型)。⛔ 只删测试区内的。 try: shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True) except Exception: pass (TEST_WS / "live.md").write_text("(selftest)", encoding="utf-8") (TEST_WS / "tg.json").write_text(json.dumps({"nodes": [ {"id": "T1", "title": "测试件-1", "status": "todo", "line": "line-a"}, {"id": "T2", "title": "测试件-2", "status": "done", "line": "line-b"}, ]}, ensure_ascii=False), encoding="utf-8") CFG.write_text(json.dumps({ "_说明": "selftest 专用(⛔ 不碰生产)", "workspace": str(TEST_WS).replace("\\", "/"), "inbox": "inbox", "live": "live.md", "taskgraph": "tg.json", "lines": {"line-a": "A 线", "line-b": "B 线"}, "goal_docs": [], "targets": {}, "host_db": "", "singleton_port": 20098, "interval": 5, "idle_min": 12, "stuck_min": 30, "vacuum_min": 5, "health_every": 300, "wake_enable": True, "wake_min_gap": 300, "wake_max_ports": 4, "wake_text": "(selftest)", }, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def case(name): def deco(fn): fn._case = name return fn return deco def report(name): """**报告型**用例:照常显示、照常报红/报绿,但 ⛔ **不计入 `--verify` 的成败**。 🔴 2026-10-04 新增(口径判据落地时撞到):有些检查**天生依赖现网存量** —— 例「产物是否都落在目标文件夹」在**历史散落的文件清理掉之前**必然红。 ⇒ 若让它计入 verify ⇒ **`install.py --verify` 在新机器上永远失败**, 而那个失败与"安装对不对"**毫无关系** ⇒ 判据被当成安装故障 ⇒ ⛔ 误报。 ✅ 判据纪律:**"装得对不对"与"存量干不干净"必须分开判**, 后者只**如实报**,不牵动前者(否则一条提示会永久堵住安装)。 """ def deco(fn): fn._case = name fn._report_only = True return fn return deco def run_cli(args, timeout=60): p = subprocess.run([PY, "-u", str(HERE / "collabd.py")] + args, cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=_mk_env(), timeout=timeout, errors="replace") return p.returncode, p.stdout or "" def tasks() -> dict: try: return json.loads((TIN / "tasks.json").read_text(encoding="utf-8")) or {} except Exception: return {} def wakeups_n() -> int: try: return len((TIN / "wakeups.jsonl").read_text(encoding="utf-8").strip().splitlines()) except Exception: return 0 def state() -> dict: try: return json.loads((TIN / "collabd-state.json").read_text(encoding="utf-8")) or {} except Exception: return {} def put_tasks(t: dict) -> None: (TIN / "tasks.json").write_text(json.dumps(t, ensure_ascii=False, indent=2), encoding="utf-8") def run_cli_env(args, env: dict, timeout=60): """同 `run_cli`,但**自定义环境**(用例要故意抽掉网关口令等)。""" p = subprocess.run([PY, "-u", str(HERE / "collabd.py")] + args, cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env, timeout=timeout, errors="replace") return p.returncode, p.stdout or "" _PROD_SNAP = {} def _snap_prod(): """记录**生产 inbox** 关键产物的 (mtime,size) —— 元用例据此断言「测试不碰生产」。""" global _PROD_SNAP _PROD_SNAP = {} for f in ("tasks.json", "wakeups.jsonl", "digest.md", "TO-MAIN.md", "wake.lock"): p = WS / "tmp" / "supervise-inbox" / f try: st = p.stat() _PROD_SNAP[f] = (st.st_mtime, st.st_size) except Exception: _PROD_SNAP[f] = None def imp(): # 🔴 先设环境,**再加载模块** —— 否则模块会去读**生产配置**(本用例曾因此"碰生产"+结果飘) os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("cb_selftest", HERE / "collabd.py") m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) return m # ── 用例(每条都对应一个真实踩过的坑) ───────────────────────────── @case("命名:两级前缀解析(合规 / 缺级 / 接续会话 / 主控前缀)") def t_name(): m = imp() a = m.parse_session_name("[协作]-[手机接入]-N9 复测") b = m.parse_session_name("[主]-[手机接入]-机制线") c = m.parse_session_name("[协作]N9-2156") # 只一级 ⇒ 不合规 d = m.parse_session_name("接续 · 机制线(旧式)") # 🆕 2026-10-01:**接续会话**(会话自己建出来的"下一棒",标题由建它的那条会话写) e = m.parse_session_name("[唤醒机制] 接续 · 日志事前叫停钩子落地(第 2 棒)") f = m.parse_session_name("接续棒:日志增长治理(任务 0 → 任务 A)") # 🆕 **主控前缀**(实测现役主会话就这么写:中点分隔、无方括号) g = m.parse_session_name("主控 · 协作机制 · 阈值交接收尾") return [("合规二级=worker/手机接入", (a["role"], a["topic"], a["ok"]) == ("worker", "手机接入", True)), ("合规二级=main", (b["role"], b["topic"], b["ok"]) == ("main", "手机接入", True)), ("只一级 ⇒ 判不合规", c["ok"] is False), ("🔴 只一级 ⇒ 角色仍判 worker(旧写法剥错方括号 ⇒ 靠 `role_of` 兜底才没出事)", c["role"] == "worker"), ("旧式接续 ⇒ 判不合规", d["ok"] is False), ("🆕 `[<类别>] 接续 · …` ⇒ **worker**,类别取方括号里那一个(⛔ 方括号里不是角色)", (e["role"], e["topic"], e["form"]) == ("worker", "唤醒机制", "continuation")), ("🆕 无方括号 `接续棒:…` ⇒ **worker**(⛔ 不是主会话)", f["role"] == "worker"), ("🆕 `主控 · …`(无方括号)⇒ **main**(否则真主会话被判「角色未知」)", (g["role"], g["form"]) == ("main", "main-prefix")), ("🆕 `form` 标出形态;老调用方只看 `ok` ⇒ 兼容(`ok=False` 语义未变)", a["form"] == "prefix")] @case("命名:看板与常驻程序**同一套角色判据**(⛔ 防两处漂移 —— 漂了就没人能发现换主会话)") def t_role_drift(): """角色判据在 `collabd.py::parse_session_name()` 与 `board.py::_role_of_title()` **两处** (2026-09-30 起就是"改一处必须改两处")⇒ 这里**真的把两边拉出来逐样本对账**,⛔ 不靠人盯。""" m = imp() import importlib.util spec = importlib.util.spec_from_file_location("bd_selftest", HERE / "board.py") bd = importlib.util.module_from_spec(spec) spec.loader.exec_module(bd) samples = ["[协作]-[唤醒机制]-日志事前叫停棒", "[唤醒]-[唤醒机制]-脉冲", "[主]-[唤醒机制]-机制线", "[跟进]-[会话协作自检]-队列上报跟进棒", "[唤醒机制] 接续 · 第 2 棒", "接续棒:日志增长治理(任务 0 → 任务 A)", "主控 · 协作机制 · 收尾", "接续 · 机制线(旧式)", "[协作]N9-2156", # 🔴 2026-10-01 补(**第三处漂移**,扫线上标题时发现的,不是推理出来的): # `[接续] …` 这种写法**方括号里是「接续」**(⛔ 不是类别)—— # `collabd.is_continuation()` 判 `"接续" in name`(整串)⇒ worker; # 而 `board._role_of_title()` 原来只查**方括号后面那段** ⇒ 返回**空**。 # 后果:看板的「主会话」位上坐着一条**接续棒**(线上实测 `3aa35bae`,355 分钟没动)。 "[接续] 会话机制合并技能包(第 1 棒)", "下载 open design", ""] bad = [t for t in samples if m.parse_session_name(t)["role"] != bd._role_of_title(t)] sa = (HERE / "collabd.py").read_text(encoding="utf-8") sb = (HERE / "board.py").read_text(encoding="utf-8") return [ ("两边角色判据逐样本一致(%d 条;不一致:%s)" % (len(samples), bad or "无"), not bad), ("🔴🔴 2026-10-02 两类改造:`[唤醒]`/`[跟进]` 两边**都判空**(已退役)—— ⛔ 判成活角色 ⇒ " "那些历史标题会被当**主会话候选**(带 `[<类别>]` 时更会变成「该类别的主会话」)⇒ 自指死结", m.parse_session_name("[唤醒]-[X]-脉冲").get("role") == "" and bd._role_of_title("[唤醒]-[X]-脉冲") == "" and m.parse_session_name("[跟进]-[X]-队列上报").get("role") == "" and bd._role_of_title("[跟进]-[X]-队列上报") == ""), ("🔴🔴 `[接续] …`(**方括号里是「接续」**)两边都判 `worker` —— ⛔ 判空 ⇒ 接续棒" "会被当**主会话候选** ⇒ 看板「主会话」位上坐着一条干活的棒(线上实测 `3aa35bae`)", m.parse_session_name("[接续] 会话机制合并技能包(第 1 棒)").get("role") == "worker" and bd._role_of_title("[接续] 会话机制合并技能包(第 1 棒)") == "worker"), ("🔴 collabd 的主会话候选判据=**只排 `worker`**(2026-10-02 两类改造,`waker`/`follow` 已退役)" "+ **退役角色闸**(2026-10-04,见下条)", '_role not in ("worker",)' in sa and 'is_retired_role_title' in sa), ("🔴 board 同款", '_role_of_title(t) not in ("worker",)' in sb and '_is_retired_role_title' in sb), # 🔴🔴 2026-10-04 21:5x 新增(**用户拍板「候选一」**):退役角色**不许当主会话候选**。 # 病根:`[跟进]`/`[唤醒]` 在 2026-10-02 从映射表摘掉后 ⇒ `role=""` ⇒ # 而排除元组是 `not in ("worker",)` ⇒ **`""` 恰好放行** ⇒ 这两类**已退役的干活的棒** # 被收进主会话候选 ⇒ 看板「主会话」位上坐着它。**实测现网**: # `main_by_topic = {"机制排查与修复": "6ab1463e", "会话协作自检": "57f58ecf"}` # —— 两个任务类别**全都指向 `[跟进]` 退役会话**。 # ⚠️ 修法⛔ 不是"排除空串"(空串里还有真主会话 `a80f300d`「复盘失败并避免重犯」)。 # ⚠️ 也⛔ 不是"恢复 `[跟进]`/`[唤醒]` 映射"(那等于让退役类别复活,与 10-02 口径相反)。 ("🔴🔴 2026-10-04 **退役角色不许当主会话候选**(用户拍板「候选一」)—— 判据=" "「一级方括号是退役角色词」(⛔ 不是『角色解析为空』,空串里还有真主会话)", m.is_retired_role_title("[跟进]-机制排查与修复-队列跟进") and m.is_retired_role_title("[唤醒]-会话协作自检-脉冲") and not m.is_retired_role_title("复盘失败并避免重犯") and not m.is_retired_role_title("主控 · 协作机制 · 收尾")), ("🔴 退役角色闸**只认一级方括号里的整词** —— ⛔ 不许误杀「类别名里恰好含『唤醒』」的" "在役会话(`[协作]-[唤醒机制]-…` 一级是「协作」⇒ 是 worker)", not m.is_retired_role_title("[协作]-[唤醒机制]-日志事前叫停棒") and not m.is_retired_role_title("[主]-[唤醒机制]-机制线")), ("🔴 两侧**共用同一实现**:`board.py` 从 `collabd.py` 取该判据(⛔ 不各写一份字面量 —— " "这两侧已漂过三次)", # ⚠️ ⛔ 不能用 `bd._is_retired_role_title is m.is_retired_role_title` 来判: # 本用例把两个文件**各自按路径 exec 一遍**(各自 `importlib` 一次 collabd) # ⇒ 拿到的是**两个模块对象** ⇒ 函数身份比较**恒 False**(实测踩到:判据没错、是写法不成立)。 # ✅ 正解①**行为逐样本一致**(真正要守的东西);②`board.py` 里是**取**不是**重定义** # (`__module__` 指向 board 加载的那份 collabd ⇒ 证明"从模块取"这条路走通了)。 all(bd._is_retired_role_title(s) == m.is_retired_role_title(s) for s in ("[跟进]-X-队列上报", "[唤醒]-X-脉冲", "[协作]-[唤醒机制]-棒", "复盘失败并避免重犯", "主控 · A · B", "")) and bd._is_retired_role_title.__module__ == "_cb_shared"), ] @case("目标:无目标 ⇒ 拒绝启动 rc=3") def t_goal_missing(): g = TIN / "goal.json" bak = g.read_text(encoding="utf-8") if g.exists() else None if g.exists(): g.unlink() p = subprocess.run([PY, "-u", str(HERE / "guard.py")], cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=_mk_env(), timeout=60, errors="replace") if bak is not None: g.write_text(bak, encoding="utf-8") return [("rc==3", p.returncode == 3), ("提示含『没有任务目标』", "没有任务目标" in (p.stdout or ""))] @case("目标:有待确认 ⇒ 拒绝启动 rc=4;--propose/--confirm 能走通") def t_goal_pending(): g, gp = TIN / "goal.json", TIN / "goal.pending.json" bak = g.read_text(encoding="utf-8") if g.exists() else None if g.exists(): g.unlink() subprocess.run([PY, "-u", str(HERE / "guard.py"), "--propose", "自测目标"], cwd=str(HERE), env=_mk_env(), timeout=60, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace", creationflags=0x08000000) p = subprocess.run([PY, "-u", str(HERE / "guard.py")], cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=_mk_env(), timeout=60, errors="replace") rc_pending = p.returncode subprocess.run([PY, "-u", str(HERE / "guard.py"), "--confirm"], cwd=str(HERE), env=_mk_env(), timeout=60, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace", creationflags=0x08000000) ok_confirmed = g.exists() and not gp.exists() if bak is not None: g.write_text(bak, encoding="utf-8") return [("有 pending ⇒ rc==4", rc_pending == 4), ("--confirm 后 goal.json 生成且 pending 清除", ok_confirmed)] @case("🔴 `goalctl` 的**配置落点**必须跟工作区走(⛔ 不许写死技能目录)") def t_goalctl_cfg_location(): """🔴🔴 2026-10-06 立(vibe-product 实测踩到)。 **病根**:`goalctl.py` 原为 `CFG = HERE / "collabd.config.json"`(`HERE` = **技能包目录**)。 而定则「**技能就是技能、程序就是程序,谁用产生的文件放在他自己那里**」⇒ **技能目录里⛔ 不放生产配置** ⇒ 该文件**恒不存在** ⇒ `_load(CFG,{})` 恒 `{}`。 **症状(不报错、不崩溃,只是那一路永远读不到)**: · `taskgraph` 取默认 `INBOX/taskgraph.json` ⇒ **改了配置不生效** (实测:vibe-product 配置里已改指 proto-board,`goals_open()` 仍报 `bad=['任务图读不到(taskgraph.json)']` ⇒ 目标**判不出来**); · `wake_enable`/`targets`/写回配置等**全部作用在一个不存在的文件上**。 ✅ **判据**:`goalctl.CFG` 必须命中 ①`COLLABD_CONFIG`(若有)或 ②`<工作区>/.workbuddy/collab/collabd.config.json`, ⛔ **不得**落在技能包目录下。 """ import importlib.util as _ilu rows = [] # ① 显式 env(钩子/启动器路径) os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) _s1 = _ilu.spec_from_file_location("gc_cfg1", HERE / "goalctl.py") _m1 = _ilu.module_from_spec(_s1) _s1.loader.exec_module(_m1) rows.append(("给 `COLLABD_CONFIG` ⇒ 用它(实得 %s)" % _m1.CFG, _m1.CFG == CFG)) # ② 不给 env ⇒ 必须是**工作区标准落点**(⛔ 不是技能目录) # ⚠️ 先**真造**一份工作区标准落点的配置 —— 否则该路径不存在 ⇒ 按设计回落到技能目录 # (那是"三路都不中"的告警分支,⛔ 不是本用例要测的形状)。 _hold = os.environ.pop("COLLABD_CONFIG", None) _std = TEST_WS / ".workbuddy" / "collab" / "collabd.config.json" _std_bak = _std.read_text(encoding="utf-8") if _std.exists() else None _std.parent.mkdir(parents=True, exist_ok=True) try: if not _std.exists(): _std.write_text(json.dumps({"workspace": str(TEST_WS)}, ensure_ascii=False), encoding="utf-8") _s2 = _ilu.spec_from_file_location("gc_cfg2", HERE / "goalctl.py") _m2 = _ilu.module_from_spec(_s2) _s2.loader.exec_module(_m2) rows.append(("不给 env ⇒ 工作区标准落点(实得 %s)" % _m2.CFG, _m2.CFG == _std)) rows.append(("🔬 反例:⛔ 不许落在技能包目录下(`HERE/collabd.config.json`)", _m2.CFG != (HERE / "collabd.config.json"))) finally: if _std_bak is None: try: _std.unlink() except Exception: pass else: _std.write_text(_std_bak, encoding="utf-8") if _hold is not None: os.environ["COLLABD_CONFIG"] = _hold return rows @case("🔴 `goalctl.goals_open()` 也必须认中文验收存值(⛔ 别只有 collabd/board 认)") def t_goalctl_acc_chinese(): """🔴🔴 2026-10-06 立(vibe-product 实测踩到,属"判据分叉"复发)。 **病根**:`board.py::acc_is_pass` 自称「**全库唯一实现**」,注释里点明三处 (`board.py` / `collabd.py::_acc_is_pass` / `board.html::accIsPass`)必须同款。 🔴 **实际是四处** —— `goalctl.py::goals_open()` 里那句 `str(acc[k]) != "pass"` **被漏掉了**。而存值**真源就是中文** ⇒ 已通过的验收被计入 why ⇒ **恒判「未完成」**。 **症状**:`goalctl.py status` 报「未完成(验收 V1=过;V2=过;…)」—— **字面自相矛盾**, 而那正是判据与存值对不上的直接体现(不报错、不崩溃,只少说一句真话)。 ✅ **本用例的判据能"改前报红"**:喂中文 `过`/`已过|…`/`达(…)`, 旧写法 `!= "pass"` ⇒ `goals_open()` 必为 `True`(红);修后为 `False`(绿)。 ⛔ 不喂英文 `pass`(那种写法旧代码也能过 ⇒ 恒绿,等于没测)。 """ m = imp() # ⚠️ `imp()` 加载的是 **collabd**(它的 `goals_open()` 返回 **bool**)。 # 本用例要测的是 **goalctl** 的同名函数(返回 **dict**,含 `why`)⇒ 必须**单独加载**。 # (实测踩到:直接用 `m.goals_open()` ⇒ `TypeError: 'bool' object is not subscriptable`) import importlib.util as _ilu _spec = _ilu.spec_from_file_location("gc_selftest_acc", HERE / "goalctl.py") _gc = _ilu.module_from_spec(_spec) _spec.loader.exec_module(_gc) # 🔴🔴 **图落点必须跟着 `CFG` 走,⛔ 不许猜**: # · 测试配置(`imp()` 设的 `COLLABD_CONFIG`)里写的是 `"taskgraph": "tg.json"` # ⇒ 相对落点 = `TEST_WS/tg.json`; # · 而 `goal.json` 走 `goalctl.INBOX`(= `WS/tmp/supervise-inbox`,**硬编码**)。 # ⚠️ 两者**不是同一个目录**,实测各踩过一次: # 写 `TEST_WS/inbox/` ⇒ `bad=['任务图读不到','无有效验收判据']`; # 写 `TEST_WS/tg.json` 而旧版 `CFG` 读不到 ⇒ 回落到 `INBOX/taskgraph.json` 才"恰好"过。 # ⇒ 用**配置里那个值**算,⛔ 不硬编码任一侧。 _tg_rel = str((json.loads(CFG.read_text(encoding="utf-8")) if CFG.is_file() else {}) .get("taskgraph") or "") tg = (TEST_WS / _tg_rel) if _tg_rel else (_gc.INBOX / "taskgraph.json") tg.parent.mkdir(parents=True, exist_ok=True) gj = _gc.INBOX / "goal.json" gj.parent.mkdir(parents=True, exist_ok=True) bak_g = gj.read_text(encoding="utf-8") if gj.exists() else None tg_bak = tg.read_text(encoding="utf-8") if tg.exists() else None tk_bak = tasks() try: # 前两路(台账/任务图)清成全 done ⇒ **只剩"验收那一路"能说话** put_tasks({}) tg.write_text(json.dumps({"nodes": [ {"id": "T1", "title": "测试件-1", "status": "done", "line": "line-a"}]}, ensure_ascii=False), encoding="utf-8") def _o(acc): gj.write_text(json.dumps({"acceptance_state": acc}, ensure_ascii=False), encoding="utf-8") return _gc.goals_open() # ── 真源的中文写法(全部应判"过" ⇒ goals_open 的 why 为空) _cn = [("裸「过」", {"V1": "过"}), ("「过|…」带竖线后缀", {"V1": "过|pid 8024 活(13:30:55)"}), ("🆕 副词:「已过|…」", {"V1": "已过|四条全在"}), ("🆕 副词:「均已通过」", {"V1": "均已通过"}), ("🆕 「达(…)」", {"V1": "达(1440 与 390 两视口都有)"}), ("四条中文全过", {"V1": "过", "V2": "已过|x", "V3": "达(y)", "V4": "均完成"})] # ── 必须仍判"不过"的(fail-closed 不破;⛔ 这是防"改成恒 True"的变异对照) # 🔴 **emoji 前缀是刻意的 fail-closed**(2026-10-06 核实):本项目约定 # `🔴`/`⛔` = **表有问题**的标记,所以真源里**否定值才带 emoji** # (例 `🔴 不过|…`),肯定值一律写 `过|…`/`pass`。 # ⇒ `🔴 已过` 判 False 是**当前三处一致的正确行为**,⛔ 不是缺陷; # 本用例把它钉成 fail-closed 期望,防有人"顺手"改成 lstrip 掉任意非字母。 _no = [("「未过」", {"V1": "未过"}), ("「不过」", {"V1": "\U0001f534 不过|没做"}), ("「待重验」", {"V1": "待重验(等复跑)"}), ("「未达」", {"V1": "未达(缺两视口)"}), ("空值", {"V1": ""}), ("英文 fail", {"V1": "fail"}), ("🔴 emoji+肯定词 ⇒ ⛔ 仍判非过(约定:emoji=表有问题)", {"V1": "\U0001f534 已过|四条全在"})] rows, o_cn, o_no = [], [], [] for lab, acc in _cn: o = _o(acc) o_cn.append(o["open"]) rows.append(("中文「%s」⇒ ⛔ 不判未完成(why=%s)" % (lab, o["why"][:2]), o["open"] is False)) for lab, acc in _no: o = _o(acc) o_no.append(o["open"]) rows.append(("「%s」⇒ 仍判未完成(fail-closed)" % lab, o["open"] is True)) finally: if tg_bak is not None: tg.write_text(tg_bak, encoding="utf-8") else: try: tg.unlink() except Exception: pass put_tasks(tk_bak) if bak_g is not None: gj.write_text(bak_g, encoding="utf-8") else: try: gj.unlink() except Exception: pass # ── 变异对照:两边**必须同时出现**(全 True 或全 False ⇒ 判据是恒的,没测出东西) rows.append(("🔬 变异对照:中文过 ⇒ 全 False,非过 ⇒ 全 True(⛔ 防恒绿/恒红)", (not any(o_cn)) and all(o_no))) return rows @case("目标三态:『没有判据』⛔ 不等于『全过』(防『读不到被当成已完成』)") def t_goal_undeclared(): """🔴 2026-09-30 实测踩到(**跑整体验证时发现**):`acceptance_state` 里**只有 `_说明`/`_更新` 两个说明行**时,旧写法 `if not acc`(判**字典空不空**)**永不触发** ⇒ `goalctl status` 显示 「三路全过」。形状同族:**不崩溃,只是少说一句话**。 ⇒ 判据改成「**有没有有效项**」(键不以 `_` 开头),并把状态分成**三态**: `open`(有未过项)/`pass`(有判据且全过)/`undeclared`(一条有效判据都没有 ⇒ ⛔ 不算过)。 """ m = imp() gj, tg = TIN / "goal.json", TEST_WS / "tg.json" bak_g = gj.read_text(encoding="utf-8") if gj.exists() else None tg_bak, tk_bak = tg.read_text(encoding="utf-8"), tasks() try: # 把台账清空 + 任务图全 done ⇒ **只剩"验收那一路"能说话**(否则前两路恒为 open,测不出区别) put_tasks({}) tg.write_text(json.dumps({"nodes": [ {"id": "T1", "title": "测试件-1", "status": "done", "line": "line-a"}, {"id": "T2", "title": "测试件-2", "status": "done", "line": "line-b"}]}, ensure_ascii=False), encoding="utf-8") gj.write_text(json.dumps({"acceptance_state": {"_说明": "x", "_更新": "y"}}, ensure_ascii=False), encoding="utf-8") s1, o1, r1 = m.goal_state(), m.goals_open(), m._acc_short()[0] gj.write_text(json.dumps({"acceptance_state": {"V1": "pass"}}, ensure_ascii=False), encoding="utf-8") s2, o2 = m.goal_state(), m.goals_open() gj.write_text(json.dumps({"acceptance_state": {"V1": "fail"}}, ensure_ascii=False), encoding="utf-8") s3, o3 = m.goal_state(), m.goals_open() finally: tg.write_text(tg_bak, encoding="utf-8") put_tasks(tk_bak) if bak_g is not None: gj.write_text(bak_g, encoding="utf-8") else: try: gj.unlink() except Exception: pass return [("只有说明行 ⇒ `undeclared`(实得 %s)" % s1, s1 == "undeclared"), ("…且 `goals_open() is True`(⛔ 不判完成)", o1 is True), ("…且 `_acc_short` 明说『没有有效判据』(%s)" % r1[:38], "有效判据" in r1), ("有判据且全过 ⇒ `pass`(实得 %s)" % s2, s2 == "pass"), ("…且 `goals_open() is False`", o2 is False), ("有判据且未过 ⇒ `open`(实得 %s)" % s3, s3 == "open"), ("…且 `goals_open() is True`", o3 is True)] @case("验收判据认**真源的中文写法**(⛔ 别只喂英文 `pass` —— 那等于判据恒绿)") def t_acc_is_pass_chinese(): """🔴🔴 2026-10-03 实测坐实(同族第三发:`pitfalls.md` P0-13/P0-20)。 **上一条用例(`t_goal_undeclared`)只喂英文 `{"V1": "pass"}`** ⇒ 判据**恒命中** ⇒ **永远绿**。 而**真源写的全是中文**:`goal.json::acceptance_state` 的值形如 `过|pid 8024 活(…)` / `🔴 不过|…` / `**复核:过(形态已治本…)**`。 ⇒ `board.py` 10-02 已修(认中文)但 **`collabd.py` 的 `goal_state()`/`_acc_short()` 没跟修** ⇒ 现算坐实 **7 条有效判据里 5 条两判据相反** ⇒ `goal_state()` 恒判 `open` ⇒ `--once` 的 `if not goals_open()` 走 `paused_round(why="目标三路全过")` ⇒ **即使真完成也会被说成"已完成"**。 ✅ **本用例的判据是"能改前报红"的那种**:先验**英文**(旧写法也能过 ⇒ 证明不是恒真), 再验**中文三种真源写法**(旧写法会红 ⇒ 证明真能抓)。 """ m = imp() rows = [] # ── 层 1:`_acc_is_pass()` 本身逐写法(⛔ 判据主体,⛔ 不依赖任何文件状态) for label, val, want in [ ("英文 pass", "pass", True), ("英文 PASS", "PASS", True), ("中文「过」", "过", True), ("中文「过|…」带竖线后缀", "过|pid 8024 活(13:30:55)", True), ("🔴 2026-10-03 实测补:竖线右边**带 json 串自带冒号**(V4 真实值)", '过|board.py --serve 8788 起得来,body={"ok":true,"err":""};探针结束后端口已释放', True), ("中文「通过」", "通过", True), ("🆕 2026-10-04 实测补:中文「达」(vibe-product 真源 14 条主用写法!)", "达(1440 与 390 两视口都有)", True), ("🆕 2026-10-04:「达」裸写法", "达", True), ("🆕 2026-10-04:「达标」", "达标(零残留)", True), ("🆕 2026-10-04:「合格」", "合格(复核通过)", True), ("🆕 2026-10-04:「完成」", "完成(已实测)", True), # 🔴🔴 2026-10-05 **第 5 次复发**:判定词前带**表完成的副词**(`已`/`均`/`都`)。 # 实测(vibe-product 页签):9 条真值**全写 `已过|…`**,看板显示 **0 / 9 通过** # —— 因为 `已过` 第一个字是 `已`、⛔ 不是 `过`。用户原话: # 「完成情况 0 / 9 通过,但是我看到都显示已过呀」。 # 前四次都在**补词** ⇒ 这次改成**剥副词再比**(`ACC_ADV_PREFIX`)。 ("🆕 2026-10-05:「已过」⇒ 必须判过(本轮报障真值)", "已过|clipper 扩展已装入浏览器并可连 daemon", True), ("🆕 2026-10-05:「已过(本类别)|…」", "已过(本类别)|会话协作自检:唤醒 9", True), ("🆕 2026-10-05:「已通过」", "已通过|x", True), ("🆕 2026-10-05:「已完成」", "已完成|y", True), ("🆕 2026-10-05:「均已通过」⇒ **两级副词**都要剥", "均已通过(实测)", True), ("🆕 2026-10-05:「都已达标」⇒ 两级副词", "都已达标|z", True), ("🆕 2026-10-05:「已`*`达」⇒ 剥副词后再去星号", "已 * 达(两视口)", True), ("🔴 2026-10-05:`未过` ⇒ ⛔ 剥不动、必须判非过(fail-closed 不许破)", "未过|还没测", False), ("🔴 2026-10-05:`已未过` ⇒ ⛔ 剥掉 `已` 还剩 `未过` ⇒ 仍非过", "已未过|x", False), ("🔴 2026-10-05:`已不过` ⇒ ⛔ 剥掉 `已` 还剩 `不过` ⇒ 仍非过", "已不过|x", False), ("🔴 2026-10-05:`非pass` ⇒ ⛔ 非过", "非pass|x", False), ("🆕 2026-10-04:白名单词前带 `*` 强调符", "* 达(两视口)", True), ("🆕 2026-10-04:「未达」⇒ ⛔ 不许因含『达』判过", "未达(还差 2 条)", False), ("🆕 2026-10-04:「待达标」⇒ ⛔ 不许判过", "待达标", False), ("「复核:过」判定词不在开头", "**复核:过(形态已治本)**", True), ("中文「不过|…」⇒ 未过", "🔴 不过|零条活着", False), ("「待重验」⇒ 未过", "待重验(判据随口径变更作废)", False), ("英文 fail", "fail", False), ("空值 ⇒ 判不出来 ⇒ 未过", "", False), ]: got = m._acc_is_pass(val) rows.append(("_acc_is_pass(%s) = %s(应 %s)" % (label, got, want), got is want)) # ── 层 2:端到端(喂中文进真源 ⇒ `goal_state()` 必须是 `pass`,⛔ 不是 `open`) gj, tg = TIN / "goal.json", TEST_WS / "tg.json" bak_g = gj.read_text(encoding="utf-8") if gj.exists() else None tg_bak, tk_bak = tg.read_text(encoding="utf-8"), tasks() try: put_tasks({}) # 前两路清空 ⇒ 只剩验收那一路能说话 tg.write_text(json.dumps({"nodes": [ {"id": "T1", "title": "测试件-1", "status": "done", "line": "line-a"}]}, ensure_ascii=False), encoding="utf-8") gj.write_text(json.dumps({"acceptance_state": { "V1-中文写法": "过|pid 1234 活,心跳 8 秒前", "V2-带括注": "过(复核:已实测)", "_说明": "这行不参与判定"}}, ensure_ascii=False), encoding="utf-8") s_cn, short_cn = m.goal_state(), m._acc_short() gj.write_text(json.dumps({"acceptance_state": { "V1-真不过": "🔴 不过|零条活着"}}, ensure_ascii=False), encoding="utf-8") s_bad = m.goal_state() finally: tg.write_text(tg_bak, encoding="utf-8") put_tasks(tk_bak) if bak_g is not None: gj.write_text(bak_g, encoding="utf-8") else: try: gj.unlink() except Exception: pass rows.append(("真源全中文且全过 ⇒ `goal_state()` = pass(实得 %s)" % s_cn, s_cn == "pass")) rows.append(("…且 `_acc_short` 报『全部 pass』(%s)" % short_cn[0][:34], "全部 pass" in short_cn[0])) rows.append(("真源有一条中文「不过」⇒ `open`(实得 %s)" % s_bad, s_bad == "open")) # ── 层 3:🔴🔴 **三处实现必须同款**(2026-10-05 加 —— 这是防"漂"的真判据) # 技能文档明令:`board.py` / `collabd.py` / `board.html` 三处判据必须逐字同款 # (**已漂过 4 次**,每次都只修一处 ⇒ 前四次修完照样再栽)。 # ⚠️ 判据要**能改前报红**:撤掉任一处的新逻辑 ⇒ 本条必须红。 # ✅ 做法=**抽三处的"判定词表 + 副词表",逐字比对**(⛔ 不靠"我记得三处一样")。 _W_WANT = ("pass", "过", "通过", "达", "达标", "合格", "完成") _A_WANT = ("已", "已经", "均", "都", "经复核", "复核后") # ① board.py —— 🔴🔴 **唯一真身**(`collabd.py::_acc_is_pass` 是转发,⛔ 不再是第二份实现)。 # ⚠️ 2026-10-05 修:此处原写 `m.ACC_PASS_WORDS`,而 `m` = **collabd 模块** # (`imp()` 把 collabd.py 装成 `cb_selftest`)⇒ `AttributeError` ⇒ **本用例整条红**。 # 真因=**拿错了"真身"**:判定词表住在 `board.py`,collabd 只是 import 它。 # ⇒ 判据必须**去 board.py 里读**(这才是"三处同款"要守的那个源)。 _rows_ok = [] _bp = HERE / "board.py" if not _bp.is_file(): _bp = HERE / "scripts" / "board.py" try: _bs = _bp.read_text(encoding="utf-8") _bw = re.search(r"^ACC_PASS_WORDS\s*=\s*\(([^)]*)\)", _bs, re.M) _ba = re.search(r"^ACC_ADV_PREFIX\s*=\s*\(([^)]*)\)", _bs, re.M) _tw = tuple(re.findall(r'"([^"]*)"', _bw.group(1))) if _bw else () _ta = tuple(re.findall(r'"([^"]*)"', _ba.group(1))) if _ba else () _rows_ok.append(("① `board.py::ACC_PASS_WORDS` 逐字同款", _tw == _W_WANT)) _rows_ok.append(("① `board.py::ACC_ADV_PREFIX` 逐字同款", _ta == _A_WANT)) # 🔴 `board.py` 也必须真含"剥副词再比"那段(⛔ 只对表不够) _rows_ok.append(("① `board.py::acc_is_pass` 真含「剥副词再比」逻辑", "ACC_ADV_PREFIX" in _bs and "len(_cut)" in _bs)) except Exception as _e: _rows_ok.append(("① `board.py` 读取失败 %r" % (_e,), False)) # ② board.html 的 ACC_PASS_WORDS / ACC_ADV_PREFIX(抽源码) # ⚠️ 2026-10-05 修:`HERE` = **本脚本所在目录(scripts/)**, # 而 `board.html` 住在**技能根**下的 `assets/`(= `HERE.parent/assets`) # ⇒ 原写 `HERE/"assets"` 必 `FileNotFoundError`(本轮实测就是这么红的)。 _html = HERE.parent / "assets" / "board.html" if not _html.is_file(): _html = HERE / "assets" / "board.html" try: _hs = _html.read_text(encoding="utf-8") _mw = re.search(r"var\s+ACC_PASS_WORDS\s*=\s*(\[[^\]]*\])", _hs) _ma = re.search(r"var\s+ACC_ADV_PREFIX\s*=\s*(\[[^\]]*\])", _hs) # ⚠️ JS 数组用**单引号**(`['pass','过']`)⇒ ⛔ `json.loads` 会 `JSONDecodeError` # (本轮实测就是这么红的)⇒ 统一**按引号抽词**(双引号/单引号都认)。 _jw = tuple(re.findall(r"""['"]([^'"]*)['"]""", _mw.group(1))) if _mw else () _ja = tuple(re.findall(r"""['"]([^'"]*)['"]""", _ma.group(1))) if _ma else () _rows_ok.append(("② `board.html::ACC_PASS_WORDS` 逐字同款", _jw == _W_WANT)) _rows_ok.append(("② `board.html::ACC_ADV_PREFIX` 逐字同款", _ja == _A_WANT)) # 🔴 前端 HTML **必须**出现"剥副词"那段逻辑(⛔ 只对表不够:改了表但没改算法也白搭) _rows_ok.append(("② `board.html::accIsPass` 真含「剥副词再比」逻辑", "ACC_ADV_PREFIX" in _hs and "slice(cut.length)" in _hs)) except Exception as _e: _rows_ok.append(("② `board.html` 读取失败 %r" % (_e,), False)) # ③ collabd.py 的**兜底白名单**(转发路径自动跟 board.py;兜底必须同款) _cb = HERE / "scripts" / "collabd.py" if not _cb.is_file(): _cb = HERE / "collabd.py" try: _cs = _cb.read_text(encoding="utf-8") _i = _cs.index("def _acc_is_pass") _j = _cs.index("\n# 🔴🔴 2026-10-04 加", _i) _body = _cs[_i:_j] _rows_ok.append(("③ `collabd.py::_acc_is_pass` 转发 `board.acc_is_pass`", "board" in _body and "acc_is_pass(" in _body)) _rows_ok.append(("③ `collabd.py` 兜底含「剥副词再比」逻辑", '"已"' in _body and '"已", "已经"' in _body)) except Exception as _e: _rows_ok.append(("③ `collabd.py` 解析失败 %r" % (_e,), False)) rows.extend(_rows_ok) return rows @case("两类检查会话是**两件事**:prompt 必须不同 + 闸④「本项目无待执行排期」") def t_two_kinds_of_check_agent(): """🔴🔴 2026-10-03 08:0x 用户明令:「**这里面是两件事 提示词应该不一样**」。 用户原话两条: 1、会话全结束(**还要加上 定时任务中没有本项目待执行的任务**)且**队列不为空** ⇒ **结果检查会话**去跟进任务会话执行结果,判断是否创建对应任务会话继续完成目标; 2、会话全结束(同上)且**队列为空 >3 分钟** ⇒ **目标检查会话**去核对目标完成状态, 判断是建任务会话还是**修改目标状态**,并执行。 ⇒ 改前**两套 prompt 完全共用一个模板** ⇒ 结果检查会话拿到了"改目标状态"的出口(越权), 目标检查会话拿到的是"看队列 head"的开场(串味)。本用例钉死"两套必须不同"。 ✅ 判据**能改前报红**:把 `_check_prompt` 改成无视 `reason` 恒返回同一份 ⇒ 本条必红。 """ import re as _re m = imp() rows = [] pr = m._check_prompt(1, "自检目标", "sessions-ended") pg = m._check_prompt(1, "自检目标", "queue-empty") rows.append(("两套 prompt 渲染都不炸(各 %d/%d 字符)" % (len(pr), len(pg)), True)) rows.append(("⛔ 两套 prompt **不是同一份**(实得 %s)" % (pr == pg), pr != pg)) rows.append(("结果检查:标题是「结果检查会话」(%s)", "【结果检查会话" in pr)) rows.append(("目标检查:标题是「目标检查会话」(%s)", "【目标检查会话" in pg)) # 🔴🔴 2026-10-09 **改口径**(用户令逐字:「**其他类型 阻碍 遇到阻碍修改目标状态 暂停机制 让用户决策**」): # 旧断言是「结果检查 ⛔ 一个改状态的出口都不许有」—— 那正是空转病根的一半: # **看得见阻碍的只有结果检查**(它靠"队列非空"触发),而有权改状态的目标检查要 # **队列为空**才触发 ⇒ 队列里躺着 `blocked` 时它永不触发 ⇒ 两条腿互相锁死。 # ✅ 现在两腿**各有一个**出口:结果检查=「阻碍」档、目标检查=「已完成」档。 # ⚠️ 两腿"允许写哪个状态"的**旗标级校验只保留一处**(`t_check_prompt_cmds_exist`)—— # 本轮实测教训:同一判据写两处 ⇒ 改一处漏一处,下面这两行就是这么报红的。 rows.append(("结果检查:写进用户口径「遇阻碍 ⇒ 暂停机制 ⇒ 让用户决策」", "机制暂停" in pr and "让用户决策" in pr)) # 目标检查必须有改状态的出口 rows.append(("目标检查含 `--set-life 已完成` 出口", "--set-life 已完成" in pg)) rows.append(("目标检查讲了三路完成度(含 `acceptance_state`)", "acceptance_state" in pg)) rows.append(("目标检查有「不确定就不改」的 fail-safe", "不确定就不改" in pg)) # 排期名分类 rows.append(("排期名分类:结果检查 → [%s]" % m.CHECK_KINDS["sessions-ended"][1], m.CHECK_KINDS["sessions-ended"][1] == "结果检查")) rows.append(("排期名分类:目标检查 → [%s]" % m.CHECK_KINDS["queue-empty"][1], m.CHECK_KINDS["queue-empty"][1] == "目标检查")) # 闸④ rows.append(("闸④ 函数存在:`ws_pending_schedules()`", callable(m.ws_pending_schedules))) ps = m.ws_pending_schedules(exclude="__不存在的排期名__") rows.append(("闸④ 现网能读出本工作区待执行排期(%d 条,测试工作区应为 0)" % len(ps), isinstance(ps, list))) # ⚠️ 判据必须读**函数体**而不是 docstring —— docstring 里写"fail-safe"只是作者的说明, # ⛔ 拿它当判据=判"作者记得写这四个字"(同 P0-36 的教训:别拿字面量当事实)。 # 真判据=**读不到库时返回非空** ⇒ 只能靠"把 RO 连接打坏"来验,见下面那条。 rows.append(("闸④ 判据注释里写明 fail-safe 方向=不建", "fail-safe" in (m.ws_pending_schedules.__doc__ or ""))) # 真判据:临时把只读连接打坏 ⇒ 必须返回**非空**(当"有排期"⇒ 闸拦 ⇒ 不建) _orig = m._ro_conn try: def _boom(): raise RuntimeError("自测:故意让读库失败") m._ro_conn = _boom broken = m.ws_pending_schedules() finally: m._ro_conn = _orig rows.append(("🔴 闸④ 读库失败时返回**非空**(fail-safe=当有排期⇒不建),实得 %r" % (broken[:1],), bool(broken))) rows.append(("🔴 闸④ 读库失败时**不**返回空列表(否则闸会放行)", broken != [])) # 非法 reason 必须被拒 rows.append(("非法 reason 不在 CHECK_KINDS(%s)", "瞎写的" not in m.CHECK_KINDS)) # 旧名必须已废(⛔ 静默变成"什么都给"更坏) try: m.CHECK_PROMPT(1, "x") rows.append(("旧名 `CHECK_PROMPT` 已废(会抛)", False)) except RuntimeError: rows.append(("旧名 `CHECK_PROMPT` 已废(会抛)", True)) return rows @case("🔴🔴 闸⑥ 同类检查冷却期:两种情况 × 每类最多一条 ⇒ 不堆积(用户「不该有任何堆积」)") def t_check_cooldown_no_pileup(): """🔴 2026-10-05 立(用户口径逐字:「只有两种情况会创建 定时任务创建会话,不应该又任何堆积」)。 ⛔ **病根**(实测,见 `references/pitfalls.md` P0-69):排期名带递增棒次(`第%d棒`) ⇒ 闸⑤「同名去抖」**永不命中** ⇒ 同一种情况可被反复建 ⇒ 自测区 2 小时堆 11 条。 🔴 **判据必须能「改前报红」**(P0-13 同族): · 删掉闸⑥ 那段 ⇒ ②③④ 全红; · 把 `_stamp.update(...)` 改回整体覆盖 ⇒ ③ 红(另一类的冷却记忆被抹); · 把 `CHECK_COOLDOWN_S` 改成 0 ⇒ ① 红。 """ import inspect as _ins m = imp() src = _ins.getsource(m.maybe_spawn_check_agent) cd = getattr(m, "CHECK_COOLDOWN_S", None) # ① 冷却常量在位且是「真窗口」(⛔ 0 / None 等于没冷却) ok_const = isinstance(cd, (int, float)) and cd >= 600 # ② 闸⑥ 在:按 reason 记冷却键(⛔ 只认带引号的字符串拼装,别认注释里的裸串) ok_key = ('"last_" + reason' in src) or ("'last_' + reason" in src) # ③ 合并写:⛔ 不许整体覆盖 stamp(覆盖 ⇒ 另一类的 last_* 被抹 ⇒ 闸⑥ 对另一情况失效) ok_merge = "_stamp.update(" in src # ④ 顺序:闸⑥ 必须在闸⑤(同名去抖)**之后**(⛔ 顺序颠倒 ⇒ 语义变成"先拦粗的") p5 = src.find("active_schedule_exists(name)") p6 = src.find('"last_" + reason') if p6 < 0: p6 = src.find("'last_' + reason") ok_order = p5 >= 0 and p6 >= 0 and p5 < p6 return [("冷却窗口 CHECK_COOLDOWN_S 在位且 ≥10 分钟(实测=%s)" % cd, ok_const), ("闸⑥ 按 reason 记冷却键(同名去抖失效时的兜底)", ok_key), ("stamp 合并写(⛔ 整体覆盖会抹掉另一类的冷却记忆)", ok_merge), ("闸⑥ 排在闸⑤ 之后(先精确后兜底)", ok_order)] @case("🔴🔴 闸⑥ 同类检查冷却期:两种情况 × 每类最多一条 ⇒ 不堆积(用户「不该有任何堆积」)") def t_check_cooldown_no_pileup(): """🔴 2026-10-05 立(用户口径逐字:「只有两种情况会创建 定时任务创建会话,不应该又任何堆积」)。 ⛔ **病根**(实测,见 `references/pitfalls.md` P0-69):排期名带递增棒次(`第%d棒`) ⇒ 闸⑤「同名去抖」**永不命中** ⇒ 同一种情况可被反复建 ⇒ 自测区 2 小时堆 11 条。 🔴 **判据必须能「改前报红」**(P0-13 同族): · 删掉闸⑥ 那段 ⇒ ②③④ 全红; · 把 `_stamp.update(...)` 改回整体覆盖 ⇒ ③ 红(另一类的冷却记忆被抹); · 把 `CHECK_COOLDOWN_S` 改成 0 ⇒ ① 红。 """ import inspect as _ins m = imp() src = _ins.getsource(m.maybe_spawn_check_agent) cd = getattr(m, "CHECK_COOLDOWN_S", None) # ① 冷却常量在位且是「真窗口」(⛔ 0 / None 等于没冷却) ok_const = isinstance(cd, (int, float)) and cd >= 600 # ② 闸⑥ 在:按 reason 记冷却键(⛔ 只认带引号的字符串拼装,别认注释里的裸串) ok_key = ('"last_" + reason' in src) or ("'last_' + reason" in src) # ③ 合并写:⛔ 不许整体覆盖 stamp(覆盖 ⇒ 另一类的 last_* 被抹 ⇒ 闸⑥ 对另一情况失效) ok_merge = "_stamp.update(" in src # ④ 顺序:闸⑥ 必须在闸⑤(同名去抖)**之后**(⛔ 顺序颠倒 ⇒ 语义变成"先拦粗的") p5 = src.find("active_schedule_exists(name)") p6 = src.find('"last_" + reason') if p6 < 0: p6 = src.find("'last_' + reason") ok_order = p5 >= 0 and p6 >= 0 and p5 < p6 return [("冷却窗口 CHECK_COOLDOWN_S 在位且 ≥10 分钟(实测=%s)" % cd, ok_const), ("闸⑥ 按 reason 记冷却键(同名去抖失效时的兜底)", ok_key), ("stamp 合并写(⛔ 整体覆盖会抹掉另一类的冷却记忆)", ok_merge), ("闸⑥ 排在闸⑤ 之后(先精确后兜底)", ok_order)] @case("检查会话的 prompt:**引用的命令必须真存在**(⛔ 引用不存在的命令 ⇒ 静默当成功)") def t_check_prompt_cmds_exist(): """🔴🔴 2026-10-03 实测坐实:`CHECK_PROMPT` 让检查会话用 `goalctl.py set-life已完成` 改生命周期 —— ⛔ **该命令在 `goalctl.py` 里根本不存在**。 实跑后果:只打印用法、**rc=0 静默放行** ⇒ 检查会话**以为改成功了**,实际一个字没改。 ✅ 真源=`collabd.py --set-life`(`L4200` 的 CLI 分支 ⇒ `goal_life_set()`)。 ✅ 本用例判据**能改前报红**:占位符个数对不上会当场 `TypeError`; prompt 里仍写 `goalctl.py set-life` 则本条**必红**。 """ import re as _re m = imp() rows = [] # 🔴 2026-10-03 08:0x:`_check_prompt` 已拆两套(`reason` 必传)⇒ **两套都要验**, # ⛔ 只验默认那套=漏掉"结果检查里不该有 --set-life"这类越权。 for reason, want_life in (("queue-empty", True), ("sessions-ended", False)): tag = "目标检查" if want_life else "结果检查" try: p = m._check_prompt(1, "自检目标", reason) ok = True except TypeError as e: p, ok = "", False rows.append(("%s:`_check_prompt()` 渲染不炸(实得 %s)" % (tag, e.__class__.__name__), False)) if not ok: continue rows.append(("%s:渲染成功,无残留占位符(实得 %s)" % (tag, _re.findall(r"%[sd]", p)), not _re.findall(r"%[sd]", p))) rows.append(("%s:坏引用 `用...goalctl.py set-life` 不在正文里" % tag, not _re.search(r"用`[^`]*goalctl\.py set-life", p))) if want_life: rows.append(("目标检查:指向 `collabd.py --set-life`(⛔ 不是不存在的命令)", "--set-life" in p and "collabd.py" in p)) rows.append(("目标检查:`--set-life` 与值之间有空格", "--set-life 已完成" in p)) else: # 🔴🔴 2026-10-09 改口径(用户令:「遇到阻碍修改目标状态 暂停机制 让用户决策」): # 旧断言是「结果检查 ⛔ 一个 `--set-life` 都不许有」—— 那正是病根的一半: # 结果检查是**唯一能看见 `blocked`** 的那条腿(它靠"队列非空"触发), # 而有权改状态的「目标检查」要**队列为空**才触发 ⇒ 队列里躺着 blocked 时它永不触发 # ⇒ **看得见的没权、有权的看不见** ⇒ 只剩每 30 分钟空转一棒(实测连开 11 棒)。 # ✅ 现口径:给结果检查**一个**出口(只许「阻碍」),⛔ 「已完成」仍归目标检查。 rows.append(("结果检查:**有一个**状态出口 —— 只许 `--set-life 阻碍`", "--set-life 阻碍" in p)) rows.append(("结果检查:⛔ 不许 `--set-life 已完成`(越权)", "--set-life 已完成" not in p)) rows.append(("结果检查:`--set-life` 的「为什么」旗标写对(`--check-why`,⛔ 不是 `--why`)", "--check-why" in p and "--set-life 阻碍 --by" in p)) rows.append(("结果检查:写明写完**必须回读** `goal.json` 确认", "必须回读" in p)) rows.append(("结果检查:写明**谁把它改回「进行中」**(⛔ 不留给用户空等)", "--set-life 进行中" in p)) return rows @case("台账四态:running → blocked(带原因) → 解除后原因清掉") def t_states(): run_cli(["--report", "C1", "--state", "running", "--by", "t", "--line", "line-a"]) ok1 = (tasks().get("C1") or {}).get("state") == "running" run_cli(["--report", "C1", "--state", "blocked", "--by", "t", "--reason", "缺一份对接单"]) v = tasks().get("C1") or {} ok2 = v.get("state") == "blocked" and "对接单" in str(v.get("block_reason")) run_cli(["--report", "C1", "--state", "pending", "--by", "t"]) v = tasks().get("C1") or {} ok3 = v.get("state") == "pending" and not v.get("block_reason") t = tasks(); t.pop("C1", None); (TIN / "tasks.json").write_text(json.dumps(t, ensure_ascii=False, indent=2), encoding="utf-8") return [("上报 running 生效", ok1), ("blocked 带原因", ok2), ("解除后原因被清掉", ok3)] @case("已停总闸:goal.run=paused ⇒ --once 只写一行、⛔ 不产告警(防『人为停掉被读成机制坏了』)") def t_goal_paused(): """🔴 2026-10-01 体检修实测:**暂停中的需求被当成"机制故障"** 报给用户。 真因:`goal.json.run=paused` 时 `collabd.py` **零处**引用 `run` ⇒ `--once` 照跑、照写 `NEXT.md`/`STALL.md`/`NEED-USER.md`/`digest.md`,而 `digest.md` 经钩子**注入每个会话每一轮**。 ⇒ 本用例钉死两件事:① 已停时**只留一行"已停"** 并把告警投影清掉 ② 已停时**不产任何唤醒/排期**。 🔴🔴 2026-10-05 `--tick` **已删**(用户:「检查程序 常驻 自己不会判断吗,非要什么 tick once 去触发?」)⇒ 原先「已停时 `--tick` ⛔ 不投递」这条断言**失去对象**(分支不存在了)。 ⛔ 替换判据=**已停时 `--once` 不产唤醒**(这才是那条红线现在仅存的载体), +**钉死 `--tick` 确已删除**(⛔ 防止有人把它加回来却不补这条闸)。 """ gp = TIN / "goal.json" _had = gp.exists() _old = gp.read_text(encoding="utf-8") if _had else "" try: # 埋脏投影:已停时它们**必须被清掉**(⛔ 留着就被钩子继续注入) for _n in ("NEXT.md", "STALL.md", "NEED-USER.md", "queue.md"): (TIN / _n).write_text("(selftest 脏投影)", encoding="utf-8") gp.write_text(json.dumps({"run": "paused", "acceptance_state": {}}, ensure_ascii=False), encoding="utf-8") w0 = wakeups_n() rc1, out1 = run_cli(["--once"]) _txt = "" try: _txt = (TIN / "digest.md").read_text(encoding="utf-8") except Exception: pass ok_line = ("已停" in _txt) and len([_l for _l in _txt.splitlines() if _l.strip()]) <= 6 ok_clear = all(not (TIN / _n).exists() for _n in ("NEXT.md", "STALL.md", "NEED-USER.md", "queue.md")) # 🔴🔴 2026-10-05:`--tick` 已删 ⇒ 改成两件事 —— # ① 已停时 `--once` 不产唤醒(红线载体只剩它) ② `--tick` 确已从参数表与代码里消失 rc2, out2 = run_cli(["--once"]) ok_no_wake = wakeups_n() == w0 _src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace") _seg = _src.split("_KNOWN = {")[1][:400] if "_KNOWN = {" in _src else "" ok_tick = ('if "--tick" in sys.argv' not in _src) and ('"--tick"' not in _seg) # 恢复 active ⇒ 必须**退出已停态**(`paused` 标记被清),⛔ 不许一次暂停就永久停摆 gp.write_text(json.dumps({"run": "active", "acceptance_state": {}}, ensure_ascii=False), encoding="utf-8") rc3, _ = run_cli(["--once"]) ok_resume = (not state().get("paused")) and rc3 == 0 return [("已停时 --once rc==0", rc1 == 0), ("digest 只有一行『已停』(⛔ 不产告警)", ok_line), ("告警投影被清掉(NEXT/STALL/NEED-USER/queue)", ok_clear), ("已停时 ⛔ 不产唤醒(%d→%d)" % (w0, wakeups_n()), ok_no_wake and rc2 == 0), ("🔴 `--tick` 确已删除(参数表与分支都不在位)", ok_tick), ("恢复 active ⇒ 退出已停态", ok_resume)] finally: if _had: gp.write_text(_old, encoding="utf-8") else: try: gp.unlink() except Exception: pass @case("投递:常驻程序(--once / --report)**零投递**") def t_no_deliver(): w0 = wakeups_n() run_cli(["--once"]) run_cli(["--report", "C2", "--state", "running", "--by", "t"]) w1 = wakeups_n() t = tasks(); t.pop("C2", None); (TIN / "tasks.json").write_text(json.dumps(t, ensure_ascii=False, indent=2), encoding="utf-8") return [("--once 与 --report 合计零投递(%d→%d)" % (w0, w1), w1 == w0)] @case("投递:目标会话(**跟进会话**)在跑 ⇒ 必须延后(target-busy)") def t_target_busy(): """🔴🔴 2026-10-02 23:4x **本用例已随「跟进会话整套退役」失效**(⛔ 保留代码,⛔ 删了=没有用例=假绿)。 🔴 **为什么整组退役**(不是改断言、不是修代码):用户逐字「按照新的逻辑整体修改」/ 「按照之前的讨论 唤醒会话 跟进会话 和 上报程序 都去掉才对」/「上报机制也不需要了」。 ⇒ `follow_for_topic()` 已**短路**返回 `why="follow-retired"` ⇒ 这条路径**根本走不到**: `target-busy`/`target-deaf`/「真投出」/「台账增条」这四组断言**永久拿不到**那个状态。 ⚠️ ⛔ **不许把这四条改成"预期 follow-retired"** —— 那等于**把断言改恒真**(P0-13 同族): 改完之后任何实现都能过,等于没有守卫。 ✅ **正确做法**:整组标记退役,⛔ 不动断言(将来口径变回时可直接复活)。 ⚠️ 退役判据(机读,⛔ 不看代码注释):`follow_for_topic()` **真跑一次**返回 `why == "follow-retired"`。 —— 本用例**后半段(真调用)已改判这条**;前半段的旧断言**刻意留着不改**。 """ m = imp() # ✅ 退役判据(真调用,⛔ 不是 grep 源码) r = m.follow_for_topic({}, "任意类别", []) if r.get("why") != "follow-retired" or str(r.get("sid") or ""): return [("🔴 两类改造:`follow_for_topic()` 必须短路成 `follow-retired` 且 `sid` 为空" "(现 = %s / sid=%r)" % (r.get("why"), r.get("sid")), False)] return [("✅ 两类改造:`follow_for_topic()` 短路成 `follow-retired`(唤醒/跟进整套退役)", True), ("⏸ 下面 2 条为**已退役**的旧断言(target-busy 的 ① 恒真 / ② 才有分量)," "⛔ 刻意不改 —— 路径已死,改成新预期=断言恒真。判据见下方源码注释", True)] # ═════════════ 以下=旧实现,⛔ 刻意保留勿删(口径若变回,直接复活这段即可) ═════════════ FAKE = "f0117000-0000-4000-8000-00000000000f" _old_scan, _old_st, _old_busy = m._scan_follows, m._session_status, m._session_busy try: m._scan_follows = lambda topics=None: {"by_topic": {}, "sids": [FAKE], "cand": [], "topics": list(topics or []), "err": ""} m._session_status = lambda sid: ("working" if str(sid) == FAKE else _old_st(sid)) m._session_busy = lambda sid: ("busy" if str(sid) == FAKE else _old_busy(sid)) r1 = m._deliver_str("自测:不该被投出", "selftest-busy", {"roles": {}}) m._session_busy = lambda sid: ("idle" if str(sid) == FAKE else _old_busy(sid)) r2 = m._deliver_str("自测:不该被投出", "selftest-busy", {"roles": {}}) finally: m._scan_follows, m._session_status, m._session_busy = _old_scan, _old_st, _old_busy return [("① 目标 `busy` ⇒ skipped==target-busy(现 = %s)" % (r1.get("skipped") or "空"), r1.get("skipped") == "target-busy"), ("② 目标 `idle` ⇒ ⛔ 不延后,往下走到 live 判(现 = %s)" % (r2.get("skipped") or "空"), r2.get("skipped") != "target-busy")] @case("投递:错名修复 —— `why` 透传(有候选但不活 ⇒ follow-not-live,⛔ 不再记 no-follow-session)") def t_deliver_why_passthrough(): """🔴 2026-10-02 修(投递链路真因):`_deliver_str` 旧实现只按 `_want==""` 分档 ⇒ 把 **(a) 真·一条候选都没有** 与 **(b) 有候选、但都不在活会话里** **合并记成同一个 `no-follow-session`** —— 字面意思与事实**相反**(现场实况:名册里 **6 条 `[跟进]` 都在、一条都不活**), ⇒ **把两天的排查引向"再多建一条"**,而多建**不管用**。 ⇒ 本用例钉住:`skipped` 必须**透传 `follow_for_topic()` 已算准的 `why`**,两档分得开。 ⚠️ 用**假的 `follow_for_topic`** 造前提(真实路径要求"网关探到活会话、且候选不在其中", 本机不可控 ⇒ 会变成天天 SKIP 的摆设用例);只替 `follow_for_topic` 与 `need_user`, 并临时补上网关口令过掉 `no-token` 闸,`finally` 里**逐项还原**。 """ m = imp() FAKE = "f0f0f0f0-0000-4000-8000-00000000000e" _old_rft, _old_nu = m.follow_for_topic, m.need_user _old_tok = os.environ.get(m.GW_ENV) _seen = [] os.environ[m.GW_ENV] = "selftest-token" # ⛔ 不设 ⇒ 早退成 `no-token`,走不到本分支 try: m.need_user = lambda t: _seen.append(t) m.follow_for_topic = lambda st, topic="", live_sids=None: { "sid": "", "source": "", "topic": topic, "why": "follow-not-live", "cand": [FAKE], "all": [FAKE]} r1 = m._deliver_str("自测:有候选但不活", "selftest-why-live", {"roles": {}}) m.follow_for_topic = lambda st, topic="", live_sids=None: { "sid": "", "source": "", "topic": topic, "why": "no-follow-session", "cand": [], "all": []} r2 = m._deliver_str("自测:一条候选都没有", "selftest-why-none", {"roles": {}}) finally: m.follow_for_topic, m.need_user = _old_rft, _old_nu if _old_tok is None: os.environ.pop(m.GW_ENV, None) else: os.environ[m.GW_ENV] = _old_tok return [ ("① 有候选·都不活 ⇒ skipped==follow-not-live(现 = %s)" % (r1.get("skipped") or "空"), r1.get("skipped") == "follow-not-live"), ("② 真·无候选 ⇒ skipped==no-follow-session(现 = %s)" % (r2.get("skipped") or "空"), r2.get("skipped") == "no-follow-session"), ("③ 两档确实分开(⛔ 不再合并成一个名字)", (r1.get("skipped") or "") != (r2.get("skipped") or "")), ("④ 两档各喊一次用户(⛔ 不静默丢件)", len(_seen) == 2), ] @case("投递:粗判第一条候选已哑、同类别另有活候选 ⇒ ⛔ 不许终局判 target-deaf(修『走不到精确重算』)") def t_coarse_deaf_not_terminal(): # 🔴🔴 2026-10-02 23:4x **本用例随「跟进会话整套退役」失效**(⛔ 保留代码,⛔ 删了=没有用例=假绿)。 # `follow_for_topic()` 已短路成 `follow-retired` ⇒ "粗判 vs 精确"那条判据链**永久走不到**。 # ⚠️ ⛔ 不许把断言改成"预期 follow-retired"(=断言恒真,P0-13 同族);判据见 `t_target_busy`。 return [("⏸ 已退役(2026-10-02 两类改造):`follow_for_topic()` 短路 ⇒ 粗判/精确重算那条链走不到", True), ("✅ 退役判据:真调用返回 `follow-retired`", imp().follow_for_topic({}, "X", []).get("why") == "follow-retired")] # ═════════════ 以下=旧实现,⛔ 刻意保留勿删 ═════════════ """🔴 2026-10-02(S5 · 验收 V2)钉死**两条判据打架**:粗判(`live_sids=[]`)与精确 (拿锁后用活网关列表重算)对**同一条**会话给出相反结论。 真因两处,**同族**、必须同批修(本用例同时钉住): ① **终局短路错位**:旧实现拿**粗判的 `_tgt`** 去 `_deaf_sids()` 判定并**直接返回** `target-deaf`(还在取锁之前)⇒ 只要"最近活动的那条候选"恰好哑了,**每轮都短路**, 永远走不到拿锁后的精确重算 ⇒ "候选里还有活的"这个事实**一辈子没机会被发现**。 ② **候选收窄过早**:`follow_for_topic()` 旧实现先把候选收窄成**单条**(`by_topic` 首见 =最近活动),再交给 `_pick_live()` ⇒ "最近活动那条死了" ≡ "这个类别投不出去", 同类别里活着的候选**轮不到**。 ⚠️ 造前提的手法(⛔ 不依赖现场恰好有活会话 —— 那会让用例天天 SKIP=没测): 伪造 `_scan_follows`(两条同类候选:`DEAF` 最近活动、`LIVE` 活着)+假日志根把 `DEAF` 判哑 +伪造 `discover_gateways()`/`_gw_http()`。**真实 `follow_for_topic()` / `_pick_live()` / `_deliver_str()` 全程在位**——被替掉的只有"外部世界",判据链一条不换。 🎯 **回退反证成立**:把 `follow_for_topic()` 的 `_lst` 换回 `[_lst[0]]`(或把粗判恢复成 `_tgt in _deaf_sids()`)⇒ 下面①~⑤ 立即变红。 """ m = imp() TP = "机制排查与修复" DEAF = "deadbeef-0000-4000-8000-0000000000d1" # 最近活动 ⇒ 粗判的 `c[0]` LIVE = "deadbeef-0000-4000-8000-0000000000d2" # 同类别、**活着** ⇒ 本该投它 # ⚠️ 假日志根**用完必须清**(`_fake_cfg` 是**追加**写入同一棵树):本用例的 DEAF 那份 10 MiB # 会被后面的 `t_deaf_detect`(断言"结果恰好只有那一条哑的")读到 ⇒ **假红**(本轮实测踩到: # 47 PASS / 1 FAIL,红的正是 `t_deaf_detect`)。与 `t_deaf_latest_day_only` 同一手法。 shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True) cfg = _fake_cfg({DEAF: DEAF_MIB + 100_000}) # 只把 DEAF 判哑(LIVE 无日志 ⇒ 不哑) restore = _with_fake_cfg(cfg) _old = (m._scan_follows, m.discover_gateways, m._gw_http, m.need_user) _old_tok = os.environ.get(m.GW_ENV) _seen, _st_in = [], {"roles": {}} try: # 清掉 wake:⛔ 否则 `same-item`/`too-soon` 会先短路,走不到精确阶段(假红) (TIN / "collabd-state.json").write_text( json.dumps({"wake": {"hash": "", "ts": 0}}), encoding="utf-8") (TIN / "wake.lock").write_text("free", encoding="utf-8") (TIN / "wakeups.jsonl").unlink(missing_ok=True) os.environ[m.GW_ENV] = "selftest-token" # ⛔ 不给 ⇒ 早退成 `no-token`,走不到投递 m._scan_follows = lambda topics=None: { "by_topic": {TP: DEAF}, "by_topic_all": {TP: [DEAF, LIVE]}, "sids": [DEAF, LIVE], "cand": [], "topics": [TP], "err": ""} m.discover_gateways = lambda *a, **k: [{"port": 1, "sessionId": LIVE}] m._gw_http = lambda *a, **k: (200, "{}") m.need_user = lambda t: _seen.append(t) r = m._deliver_str("自测:粗判第一条哑", "selftest-coarse-deaf", _st_in, topic=TP) _w = (r.get("delivered") or {}) _sid_in_wake = str((_st_in.get("wake") or {}).get("sessionId") or "") _n = wakeups_n() finally: m._scan_follows, m.discover_gateways, m._gw_http, m.need_user = _old if _old_tok is None: os.environ.pop(m.GW_ENV, None) else: os.environ[m.GW_ENV] = _old_tok restore() shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True) # ⛔ 不留残渣给后面用例 return [ ("① ⛔ 不许短路成 target-deaf(现 = %s)" % (r.get("skipped") or "空(↓已投出)"), r.get("skipped") != "target-deaf"), ("② 走到精确阶段并**真投出**(http=%r)" % (_w.get("http"),), _w.get("http") == 200), ("③ 投给**活着的那条** `%s…`(⛔ 不是粗判的第一条 `%s…`)" % (LIVE[:8], DEAF[:8]), _sid_in_wake == LIVE), ("④ 投递台账**真增 1 条**(现 %d)" % _n, _n == 1), ("⑤ ⛔ 没对用户喊话(粗判假警报已消除;现 %d 次)" % len(_seen), len(_seen) == 0), ] @case("投递:跨进程互斥锁存在且会被释放") def t_lock_released(): m = imp() m._deliver_str("自测", "selftest", {"roles": {}}) _p = TIN / "wake.lock" try: _txt = _p.read_text(encoding="utf-8", errors="replace").strip() except Exception: _txt = "" try: import inspect as _ins _src = _ins.getsource(m._deliver_str) except Exception: _src = "" return [ # 🔴 2026-10-01 **契约变更**:旧契约="释放就是把文件删掉";新契约= # "**文件永久存在,释放 = 原地把内容写成 `free`**"。为什么必须变: # `_deliver_str` 每轮取锁后都在 `finally` 里 `unlink(wake.lock)`,而宿主有 # **SafeDelete 批量删除护栏**(按"本轮删除次数"计数,达 50 即要求确认并**终止进程**) # ⇒ 常驻 ≈8 分钟(10 s/轮 × 50)就被杀(现场 `_supervise-bg.out.log` 原文 # `{"count":50,"threshold":50,"targetCount":1,"targets":[…\wake.lock]}`)。 # ⚠️ 断言随之改判**语义**而非存在性:`不存在`(从未取锁)**或** `内容=free` 都算"已释放"。 ("锁已释放(不存在 或 内容=free;实得 %r)" % _txt, (not _p.exists()) or _txt == "free"), # 🔴 反向那条**非有不可**:只判"已释放"的话,"每轮把文件 unlink 掉"这种旧写法**照样绿** —— # 而它正是护栏杀进程的来源。⚠️ 且必须要求**源码真读到**(`_src` 为空=没验,⛔ 不许恒真)。 ("锁释放 ⛔ 不用 unlink(源码读到 %d 字符,%s `_lk.unlink`)" % (len(_src), "无" if "_lk.unlink" not in _src else "仍有"), bool(_src) and "_lk.unlink" not in _src), ] @case("投递:锁被**别的进程**持有时 ⇒ ⛔ 不投(`locked`)") def t_lock_held(): # 🔴 为什么非有此例:上面那条只验"**能释放**",⛔ 验不到"**真能互斥**"。 # 而"跨进程互斥"是本机制**避免同一内容成对重复投递**的唯一屏障(实测事故:22:24:04/30 等 # 四组成对投递把主会话压成 `parkInQueue`「只进不出」⇒ 用户看到"卡死")。 # ⚠️ 判据必须是**真的把锁占住**再调 —— 伪造"别的进程正持有"(内容=假 pid + mtime=现在 ⇒ 不陈旧)。 m = imp() _p = TIN / "wake.lock" _p.parent.mkdir(parents=True, exist_ok=True) try: m._deliver_str("自测", "selftest", {"roles": {}}) # 先走一遍:把锁建起来、并释放成 free _p.write_text("999999", encoding="utf-8") # 伪造"别的进程正持有"(⛔ 不陈旧 ⇒ 不许抢占) r = m._deliver_str("自测-锁被别人拿着", "selftest", {"roles": {}}) finally: try: _p.write_text("free", encoding="utf-8") # 收尾:还原成未持有(⛔ 不 unlink) except Exception: pass return [("skipped==locked(现 = %s)" % (r.get("skipped") or "空"), r.get("skipped") == "locked")] # ── 🔴 2026-10-01 加:**会话哑掉(诊断日志撞 ~10 MiB 上限)** 的回归面 ────────────── # 真因(本机实测):宿主**单会话诊断日志有 10 MiB 硬上限**,撞上即**拒写**(EPERM) # ⇒ 该会话**界面不再刷新**(=哑);而投递只认 `http=200` ⇒ 向已哑会话投递**仍记 # `ok:true` =假绿**,通知全落空机制却毫无察觉(实测主会话 f8a792ab 停在 10,485,715 B, # 其后累计 45,422 行被丢)。 # ⇒ 修法:`collabd.py` 加 `_deaf_sids()`(≥9.5 MiB 判哑)+ 投递**两道闸** # (粗判按登记目标 · **出手前再按网关实际目标**)⇒ 记 `skipped=target-deaf` + 喊用户。 # ⚠️ 阈值分工(⛔ 别混):**9.5 MiB** =「已哑、不许再投」(`_deaf_sids` 的拦截线); # **5 MiB** =「该交接了」(会话主动换,见 CODEBUDDY.md §1.5 G)。 DEAF_MIB = 9_500_000 # ⚠️ 与 `collabd._deaf_sids()` 的阈值**逐字同源**(那边改了这里必须跟) def _make_sparse(f) -> bool: """把已打开的**文件对象**标成 NTFS 稀疏文件(Windows);非 Windows 直接返回 True。 🔴🔴 2026-10-05 修一处**实测坐实了的假认知**(pitfalls P0-84): 本函数的老注释写着「用 `truncate` 建**稀疏文件**……⛔ 不真写 10 MiB」。 **在 Windows 上这是错的** —— `truncate()` 走的是 `SetEndOfFile`, ⛔ 不会自动把文件标成稀疏 ⇒ **逻辑大小与实占 1:1**。 实测:`open(p,"wb").truncate(9_600_000)` 建 10 个档, 逻辑 91.6 MB/实占 91.6 MB(不是"看起来大、实际很小")。 ⇒ 单次 `selftest` 真占 ~60 MB;而 `install.py --verify` 每跑一次就跑一次 selftest, **再叠加宿主的 safe-delete shim**(删工作区文件一律改道回收站)⇒ 那些字节**不会消失,只是搬进回收站**,实测攒到 13.8 GB(`tmp/selftest`)。 ⛔ 不许再靠"truncate 不真写"这个假设省事。 正解:写数据**之前**先 `FSCTL_SET_SPARSE`,之后再 truncate 才真稀疏。 ⚠️ 顺序不能反 —— 先 truncate 再加标志,已分配的簇不会自动归还。 """ if os.name != "nt": return True try: import ctypes import msvcrt from ctypes import wintypes FSCTL_SET_SPARSE = 0x000900C4 GENERIC_WRITE = 0x40000000 FILE_SHARE_RW = 0x00000001 | 0x00000002 k32 = ctypes.WinDLL("kernel32", use_last_error=True) k32.CreateFileW.restype = wintypes.HANDLE k32.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE] k32.DeviceIoControl.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD, ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID] OPEN_EXISTING = 3 h = k32.CreateFileW(str(f.name), GENERIC_WRITE, FILE_SHARE_RW, None, OPEN_EXISTING, 0x00000080, None) # FILE_ATTRIBUTE_NORMAL if h == wintypes.HANDLE(-1).value or not h: return False try: br = wintypes.DWORD(0) ok = k32.DeviceIoControl(h, FSCTL_SET_SPARSE, None, 0, None, 0, ctypes.byref(br), None) return bool(ok) finally: k32.CloseHandle(h) except Exception: return False def _fake_cfg(sizes: dict, day_offset: int = 0) -> Path: """在**测试区内**造一份假 `logs/<日期>/sdk/conversations/`(⛔ 生产日志目录一个字节不动)。 `_deaf_sids()` 只读 `CODEBUDDY_CONFIG_DIR/logs/...` ⇒ 换掉配置根即可**完全离线**复现本坑。 `day_offset`:0=今天(默认)、1=昨天 —— 🔴 **按天分目录**正是"永久判哑"那个坑的土壤。 🔴 体积用「**先标稀疏 → 再 `truncate`**」造(见 `_make_sparse`): 目标是秒级拿到 10 MiB 的**体积读数**(`getsize`),而**实占尽量小**。 ⚠️ 老实现只 `truncate`、⛔ 没标稀疏 ⇒ 在 Windows 上**真写**(P0-84 实测 1:1)。 ⚠️ 这里**不追求零占用**:只要"不真写满"即可 —— 某些文件系统不支持稀疏时 `_make_sparse` 返回 False,此时行为与老实现相同(真写), **但会打一条 stderr 提示**,⛔ 不许静默退化。 """ _d = time.strftime("%Y-%m-%d", time.localtime(time.time() - 86400 * int(day_offset))) root = TEST_WS / "fake-cfg" / "logs" / _d / "sdk" / "conversations" root.mkdir(parents=True, exist_ok=True) for _sid, _n in (sizes or {}).items(): _p = root / (_sid + ".log") with open(_p, "wb") as f: if not _make_sparse(f): print(f"⚠ _fake_cfg: {_p.name} 未能标成稀疏文件 ⇒ 本次将真占用 {_n} 字节", file=sys.stderr) f.truncate(int(_n)) return TEST_WS / "fake-cfg" def _with_fake_cfg(cfg: Path): """把 `CODEBUDDY_CONFIG_DIR` 临时指到假配置根,返回**还原函数**(⛔ 必须放在 finally 里调)。 为什么非还原不可:`_db()` 也读同一个变量 —— 不还原会把**后面所有用例**(主会话解析等) 一起带偏(那是"改一个用例弄红一片"的经典假红)。 """ old = os.environ.get("CODEBUDDY_CONFIG_DIR") def _restore(): if old is None: os.environ.pop("CODEBUDDY_CONFIG_DIR", None) else: os.environ["CODEBUDDY_CONFIG_DIR"] = old os.environ["CODEBUDDY_CONFIG_DIR"] = str(cfg) return _restore @case("哑会话:诊断日志 ≥ 9.5 MiB ⇒ _deaf_sids() 判哑(只读 · 判据=读数、含边界)") def t_deaf_detect(): """⚠️ 本用例走 **Python 直调**(不起子进程):`_deaf_sids()` 每次调用**现读**环境变量 ⇒ 换 `CODEBUDDY_CONFIG_DIR` 即生效,无需重启进程。""" DEAF = "deadbeef-0000-4000-8000-000000000001" NEAR = "deadbeef-0000-4000-8000-000000000002" # 差 1 字节 ⇒ 验**边界不含** OKAY = "cafe0000-1111-4222-8333-444444444444" cfg = _fake_cfg({DEAF: DEAF_MIB + 100_000, NEAR: DEAF_MIB - 1, OKAY: 4096}) restore = _with_fake_cfg(cfg) try: d = imp()._deaf_sids() finally: restore() return [("≥9.5 MiB ⇒ 判哑(%s…)" % DEAF[:8], DEAF in d), ("9,499,999 B(差 1 字节)⇒ ⛔ 不判哑(边界不含)", NEAR not in d), ("KB 级日志 ⇒ ⛔ 不判哑", OKAY not in d), ("⛔ 不误伤:结果里恰好只有那一条哑的", d == {DEAF})] @case("哑会话:投递命中已哑目标 ⇒ skipped=target-deaf(⛔ 不投 + 喊用户;⛔ 不记假绿)") def t_deaf_skip(): # 🔴🔴 2026-10-02 23:4x **本用例随「跟进会话整套退役」失效**(⛔ 保留代码,⛔ 删了=没有用例=假绿)。 # 投给「已哑跟进会话」这条路已不存在(`follow-retired`)⇒ `skipped==target-deaf` 拿不到。 # ⚠️ ⛔ 不许改成"预期 follow-retired"(=断言恒真);判据见 `t_target_busy`。 # ✅ 但**「不记假绿」这条原则仍活着**,改由退役判据承担:真调用拿 `sid=""` ⇒ 投不出去 ⇒ 台账不增。 m0 = imp() _r = m0.follow_for_topic({}, "X", []) return [("⏸ 已退役(2026-10-02 两类改造):投给已哑跟进会话这条路不存在了", True), ("✅ 退役判据:`sid` 为空 ⇒ **投不出去**(⛔ 记 `ok:true` 就是假绿,退役后天然满足)", (not str(_r.get("sid") or "")) and _r.get("why") == "follow-retired")] # ═════════════ 以下=旧实现,⛔ 刻意保留勿删 ═════════════ """🔴 这条钉住的正是**假绿**:旧实现向已哑会话投递**仍记 `ok:true · http:200`**。 判据三件:① 判读是 `target-deaf` ② 投递台账**零增量** ③ 已把"该点哪一下"落到 `NEED-USER.md`。""" DEAF = "deadbeef-0000-4000-8000-000000000001" cfg = _fake_cfg({DEAF: DEAF_MIB + 100_000}) try: (TIN / "NEED-USER.md").unlink() # ⚠️ 先清:否则上一轮留下的那份会让"喊过用户"**恒真** except Exception: pass w0 = wakeups_n() restore = _with_fake_cfg(cfg) try: m = imp() # 🔴 2026-10-01 改:登记成 `main` 已经没用(投递**只看跟进会话**,用户口径见 `_deliver_str`) # ⇒ 改成**伪造一条跟进会话**,让这道闸真的被走到(⛔ 否则只会落 `no-follow-session`)。 _old_scan = m._scan_follows m._scan_follows = lambda topics=None: {"by_topic": {}, "sids": [DEAF], "cand": [], "topics": list(topics or []), "err": ""} try: r = m._deliver_str("自测:哑会话不该被投", "selftest-deaf", {"roles": {}}) finally: m._scan_follows = _old_scan _sk = str(r.get("skipped") or "") _nu = (TIN / "NEED-USER.md").exists() _w1 = wakeups_n() finally: restore() return [("skipped==target-deaf(现 = %s)" % (_sk or "空"), _sk == "target-deaf"), ("⛔ 零投递(wakeups %d→%d)" % (w0, _w1), _w1 == w0), ("已喊用户(NEED-USER.md 落盘)", _nu)] @case("哑会话:**只认最新一天** —— 昨天撞过上限、今天已正常轮转 ⇒ ⛔ 不判哑(修『永久判哑』)") def t_deaf_latest_day_only(): """🔴 本用例钉住的正是**方向相反的新假绿**:旧实现扫「今天+昨天」**任一**命中即判哑, 而宿主会话日志**按天分目录**、昨天那份**永不再增长** ⇒ **昨天撞过 10 MiB、今天已轮转**的 会话被**永久判哑** ⇒ 本该投出去的唤醒被静默拦下。 实证 `a202550c`:昨天 10.0 MB、**今天 4.2 MB 且宿主仍在写它**。 ⇒ 判据改为「**每个会话取它最新一天那份日志**」。⛔ 今天没有日志 ⇒ 仍退到昨天(保守兜底,不许丢)。 """ ROTATED = "a202550c-0000-4000-8000-00000000000a" # 昨 10 MiB / 今 4.2 MB ⇒ 已轮转 ⇒ ⛔ 不判哑 OLDONLY = "0badf00d-0000-4000-8000-00000000000b" # 只有昨天、且撞上限 ⇒ 仍判哑(⛔ 兜底不许丢) NEWDEAF = "feedface-0000-4000-8000-00000000000c" # 今天才撞上限 ⇒ 判哑 HEALTHY = "5ca1ab1e-0000-4000-8000-00000000000d" # 今 4 KB ⇒ ⛔ 不判哑 shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True) # ⛔ 先清空:前面两条用例也在同一棵树下造过文件 _fake_cfg({ROTATED: DEAF_MIB + 500_000, OLDONLY: DEAF_MIB + 500_000}, day_offset=1) # 昨天那份 cfg = _fake_cfg({ROTATED: 4_200_000, NEWDEAF: DEAF_MIB + 500_000, HEALTHY: 4096}) # 今天那份 restore = _with_fake_cfg(cfg) try: d = imp()._deaf_sids() finally: restore() shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True) return [("昨 10 MB / 今 4.2 MB(已轮转)⇒ ⛔ 不判哑", ROTATED not in d), ("今天才撞上限 ⇒ 判哑", NEWDEAF in d), ("只有昨天那份、且撞上限 ⇒ 仍判哑(兜底不丢)", OLDONLY in d), ("今天 KB 级 ⇒ ⛔ 不判哑", HEALTHY not in d), ("⛔ 不误伤:结果恰好={今天哑的, 只有昨天的}(现 %d 条)" % len(d), d == {NEWDEAF, OLDONLY})] # ── 🔴 2026-09-30 加:**投递=宿主钩子唤起(`--tick`)** 的回归面 ──────────────── # 背景(用户口径「又给我整到自动任务去了」):投递改由**宿主钩子**唤起 `collabd.py --tick` # (钩子是宿主子进程 ⇒ 自带网关口令)。 # ⚠️ **2026-09-30 同日用户改口**(取代上面那句里的"⛔ 不靠自动化排期"): # 「自动任务(符合条件时)是通过 协作程序去唤醒 主会话,这样流程统一」 # ⇒ **允许自动任务**当"闹钟"(`goalctl.py wake` ⇒ `--tick`);⛔ 但判断与投递**仍只在本程序一处**。 # 🔴 下面两条约束**与"谁拨这一下"无关**,**仍然有效**(改触发源⛔ 不免除它们): # ① `--once`(常驻程序/投影轮)**⛔ 不许消费队列** —— 它在 UserPromptSubmit 上先跑; # ② `--tick` **投不出去就不许消费队列** —— 旧实现无条件 `notified[kid]=stt` ⇒ **静默丢件**。 @case("投递(新):纯投影 --once ⛔ 不消费队列") def t_projection_no_consume(): put_tasks({"P1": {"state": "running", "by": "t", "line": "line-a"}}) (TIN / "collabd-state.json").write_text(json.dumps( {"notified": {}, "notify_pending": ["P1=running"]}, ensure_ascii=False), encoding="utf-8") rc, _ = run_cli(["--once"]) s = state() kept = "P1=running" in list(s.get("notify_pending") or []) no_aw = not s.get("notify_awaiting") put_tasks({}) return [("rc==0", rc == 0), ("--once 后 notify_pending 原样保留(现 = %s)" % (s.get("notify_pending"),), kept), ("--once 未设 notify_awaiting", no_aw)] @case("投递(新):投不出去(no-token)⇒ ⛔ 不消费队列(防静默丢件)") def t_no_consume_when_skipped(): put_tasks({"Q1": {"state": "running", "by": "t", "line": "line-a"}}) (TIN / "collabd-state.json").write_text(json.dumps( {"notified": {}, "notify_pending": ["Q1=running"]}, ensure_ascii=False), encoding="utf-8") env = _mk_env() env.pop("CODEBUDDY_GATEWAY_PASSWORD", None) # 抽掉口令 ⇒ 投递必被挡(no-token) # 🔴🔴 2026-10-05 `--tick` 已删 ⇒ 载体换成 `--supervise` 的**判定本体** `supervise()`: # 本用例的红线是「**投不出去就不许消费队列**」(防静默丢件),⛔ 与 tick 这个入口无关 # ⇒ 直接真调 `supervise(st, deliver=True, mutate=True)` 并抽掉口令(⛔ 不换个入口就丢掉红线)。 m = imp() try: _st = json.loads((TIN / "collabd-state.json").read_text(encoding="utf-8")) except Exception: _st = {} _old = os.environ.pop("CODEBUDDY_GATEWAY_PASSWORD", None) try: m.supervise(_st, deliver=True, mutate=True) except Exception: pass finally: if _old is not None: os.environ["CODEBUDDY_GATEWAY_PASSWORD"] = _old rc, out = 0, "" s = state() pend = list(s.get("notify_pending") or []) notified = dict(s.get("notified") or {}) put_tasks({}) return [("rc==0", rc == 0), ("队列项仍在(现 = %s)" % (pend,), "Q1=running" in pend), ("⛔ 未被标记 notified", notified.get("Q1") != "running"), ("⛔ 未设 notify_awaiting", not s.get("notify_awaiting"))] @case("宿主卡住探针:只读尾部、测试环境零命中、⛔ 不误报") def t_park_probe(): # 2026-09-30 立(用户给的窗口 22:55–23:20 实测定型):指纹 = parkInQueue + hasWaiter=false。 # ⚠️ 测试工作区名是 `selftest` ⇒ 候选日志目录里没有它的日志 ⇒ 必须**零命中且不报错**(⛔ 不碰生产日志)。 m = imp() r = m.probe_host_park() real = ('[2026/9/29 22:53:00.208] [Info] [pid=32356] [AcpView] [AcpView][PromptIterator] ' 'received prompt session=fe146dd9-… route=parkInQueue runState=idle queueLen=0 hasWaiter=false') echo = ('[2026/9/30 00:36:15.002] [Info] [pid=50716] [SandboxShell] ProcessOutput | processId=pipe-18 | ' 'content=Info] [pid=32356] [AcpView] route=parkInQueue queueLen=1 hasWaiter=false | stderr(0)=""') ok_line = ('[2026/9/29 23:12:16.901] [Info] [pid=55508] [AcpView] [AcpView][PromptIterator] ' 'received prompt session=fe146dd9-… route=resolveWaiter runState=idle queueLen=0 hasWaiter=true') return [("返回 dict 且含 n/last/file", all(k in r for k in ("n", "last", "file"))), ("测试环境零命中(不读生产日志)", r["n"] == 0), ("只读尾部(常量 ≤ 512KB)", 0 < m.PARK_TAIL <= 512 * 1024), ("⛔ 不命中「取证命令的回声行」(防假阳性)", m._is_park_line(echo) is False), ("✅ 命中真正的 parkInQueue 记录行", m._is_park_line(real) is True), ("✅ 不误判正常行(resolveWaiter/hasWaiter=true)", m._is_park_line(ok_line) is False)] @case("投递消费回查:投出去 ≠ 跑起来了 ⇒ 未消费必须升级为 NEED-USER") def t_consume_check(): m = imp() nu = TIN / "NEED-USER.md" if nu.exists(): nu.unlink() # ① 刚投出去(宽限期内)⇒ waiting,⛔ 不报警 st1 = {"wake": {"expect": {"sid": "0" * 36, "at": time.time()}}} r1 = m.check_delivery_consumed(st1) quiet = not nu.exists() # ⚠️ 必须在跑 ② 之前取快照(② 会写这个文件 ⇒ 否则本项必假红) # ② 投出很久且目标会话零活动 ⇒ unconsumed,且必须落 NEED-USER.md(喊人) st2 = {"wake": {"expect": {"sid": "0" * 36, "at": time.time() - 9999}}} r2 = m.check_delivery_consumed(st2) shouted = nu.exists() and "切走再切回" in nu.read_text(encoding="utf-8", errors="replace") return [("宽限期内 = waiting 且不报警", r1.get("waiting") is True and quiet), ("超期未动 = unconsumed", r2.get("unconsumed") is True), ("落 NEED-USER 且写明处置动作(切走再切回)", shouted), ("回查后清掉 expect(⛔ 不重复报)", "expect" not in (st2.get("wake") or {}))] @case("need_user:同句节流(⛔ 不每轮刷时间戳)+ 人写的「## 解除条件」两路都必须原样保留") def t_need_user_throttle_keep(): """🔴 2026-10-01 立(治两处现场事故,均已在生产实测): · **噪音**:常驻每 ~10 s 用同一句话重写 NEED-USER.md ⇒ 时间戳一直变、人看不出「这条是新的还是旧的」 (现场:22:06–22:12 之间被刷了 20+ 次)。 · **抹掉人的回信口**:人手工写了「## 解除条件」,下一轮被机器**静默抹掉** ⇒ 人就再也不写了 (现场:21:03 手写的解除条件当场被覆盖)。 ⚠️ 判据必须能「改前报红」(P0-13):把节流去掉 ⇒ ③ 报红;把保留 `_keep` 去掉 ⇒ ④ 报红。""" import inspect as _ins m = imp() nu = TIN / "NEED-USER.md" human = "\n## 解除条件\n- 用户手写:这是人唯一的回信口,⛔ 不许抹\n" try: if nu.exists(): nu.unlink() # ① 第一次喊 ⇒ 必须落文件(⛔ 不许"喊了但没落") m.need_user("自测原因-A") got1 = nu.exists() and ("自测原因-A" in nu.read_text(encoding="utf-8", errors="replace")) # ② 模拟人回信:追加「## 解除条件」 nu.write_text(nu.read_text(encoding="utf-8", errors="replace").rstrip() + human, encoding="utf-8") mt2 = nu.stat().st_mtime # ③ 同一句话、节流窗口内再喊 ⇒ ⛔ 不重写(mtime 不变)+ 人的话必须还在 m.need_user("自测原因-A") t3 = nu.read_text(encoding="utf-8", errors="replace") quiet = (nu.stat().st_mtime == mt2) and ("用户手写" in t3) # ④ 换一个原因 ⇒ 允许重写,但**人的话仍要原样保留** m.need_user("自测原因-B") t4 = nu.read_text(encoding="utf-8", errors="replace") keep = ("自测原因-B" in t4) and ("## 解除条件" in t4) and ("用户手写" in t4) # ⑤ 源码级:need_user 段内零 unlink(删文件 = 撞宿主 SafeDelete 批量删除护栏,P0-6) try: _src = _ins.getsource(m.need_user) except Exception: _src = "" no_unlink = bool(_src) and (".unlink" not in _src) return [("① 首次喊 ⇒ NEED-USER.md 落盘且含原因", got1), ("③ 同原因在窗口内 ⇒ ⛔ 不重写(mtime 未变)且人的话还在", quiet), ("④ 换原因才重写,且人的「## 解除条件」原样保留", keep), ("⑤ need_user 段内零 unlink", no_unlink)] finally: if nu.exists(): nu.unlink() @case("投递忙判据:读宿主状态机日志的 busy=(⛔ 不再用 `status=='working'` 那档恒真的判据)") def t_srsm_busy(): """🔴 2026-10-01 立(治「状态窗口死结」,现场已在生产实测)。 病根:`sessions.status` 取值域只有 `working/completed/error/archived` —— **没有"空闲"这一档**, 活着的会话跑完一轮、停在等下一轮时 status **仍是 `working`** ⇒ 拿它判"忙"**恒真** ⇒ 投递永远等不到空闲(实测 `e2ccdea3` 连打 6 分钟 `延后投递…等它空闲`+`投递未成(target-busy)`)。 正解:宿主自己的 `[SessionRunStateMachine]` 日志(⛔ 不进数据库,查库查不到)。 ⚠️ 判据必须能「改前报红」(P0-13):把"新鲜度"那半去掉 ⇒ ③ 报红;把「先判终结态」去掉 ⇒ ⑤ 报红。 """ m = imp() tmpd = TEST_WS / "logs" tmpd.mkdir(parents=True, exist_ok=True) f = tmpd / "fakews__abcd.log" SID = "11111111-2222-3333-4444-555555555555" OTHER = "99999999-2222-3333-4444-555555555555" def line(ts, sid, busy): return ("[%s] [Info] [pid=1] [SessionRunStateMachine] transition | sessionId=%s | event=X | " "from=a | to=b | lifecycle=running | busy=%s | queueBusy=true" % (ts, sid, busy)) now = time.strftime("%Y/%m/%d %H:%M:%S") old = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(time.time() - 7200)) orig_dirs, orig_status = m._log_dirs, m._session_status try: m._log_dirs = lambda: [f] # ① 末条 `busy=true` 且新鲜 ⇒ busy(正对照) f.write_text(line(now, SID, "true") + "\n", encoding="utf-8") c1 = m._session_busy(SID) == "busy" # ② 末条 `busy=false` ⇒ idle(⛔ 不能被前一条 busy=true 带偏 ⇒ 必须取**最后一条**) f.write_text(line(now, SID, "true") + "\n" + line(now, SID, "false") + "\n", encoding="utf-8") c2 = m._session_busy(SID) == "idle" # ③ 末条 `busy=true` 但已是 2 小时前 ⇒ idle —— 这正是"会话早停了、只是没写 busy=false"那一类 f.write_text(line(old, SID, "true") + "\n", encoding="utf-8") c3 = m._session_busy(SID) == "idle" # ④ 日志里**没有**该 sid ⇒ ''(⛔ 不猜、⛔ 不回落成 busy 或 idle) f.write_text(line(now, OTHER, "true") + "\n", encoding="utf-8") c4 = m._session_busy(SID) == "" # ⑤ 库里已**终结** ⇒ 即便日志末条还是 `busy=true` 也**不判忙** # (实测:自动化拉起的会话跑完**不写 `busy=false`**,直接在库里变 completed) f.write_text(line(now, SID, "true") + "\n", encoding="utf-8") m._session_status = lambda s: "completed" c5 = (m._target_busy(SID) is False) finally: m._log_dirs, m._session_status = orig_dirs, orig_status try: f.unlink() except Exception: pass # ⑥ 源码级**反回归**:正则必须用**命名组**。 # 本轮真踩过:写成 `(?:\.(\d+))?` 时内层 `(\d+)` **仍是捕获组** ⇒ 后面 `m.group(8)` 全错位一位 # ⇒ `busy` 恒读成 False ⇒ **恒判"空闲"**(方向相反:会往正在跑的会话里插话)。 # ⛔ 这条断言不是"看代码像不像" —— 它是**唯一**能在这类错位再发生时立刻报红的东西。 # ⚠️ 判据范围必须**从正则定义处起**(`getsource(_session_busy)` 只给函数体 ⇒ 看不见正则本身); # 末端取 `def _target_busy` ⇒ 恰好覆盖「正则 + `_session_busy`」这一段。 _all = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace") _i = _all.find("_SRSM_RE = re.compile(") _j = _all.find("def _target_busy", _i if _i >= 0 else 0) seg = _all[_i:_j] if (_i >= 0 and _j > _i) else "" named = bool(seg) and ("?P" in seg) and not re.search(r"group\(\s*[\"']?[1-9]", seg) # ===== 🔴🔴 2026-10-04 第 2 源(`vibe-product` 实测坐实):会话级日志兜底 ===== # 🔴 病根:工作区日志 `__.log` 里 `SessionRunStateMachine` 有 615 条, # 但**那个 sid 一条都没有** ⇒ 宿主**不为它写**工作区级状态机日志 # ⇒ `_session_busy()` 恒 `''` ⇒ 老逻辑**必然误判「没在跑」⇒ 往正在跑的会话插话** # (该会话当时 `working` 358 次 / `idle` 仅 3 次 / 用户只发言 4 次,跨 2h46m)。 # ✅ 正解=**双源**:工作区读不到时回落到**会话级** `/<日期>/sdk/conversations/.log`。 # ⚠️ 那个文件**字段完全不同**:`state-machine:transition {"to":"working"}`,⛔ 没有 `busy=`。 # ⚠️ 文件名**带连字符**,而调用方常拿库里 32 位无连字符 id ⇒ 两种形态都得试。 conv_dir = TEST_WS / "logs" / time.strftime("%Y-%m-%d") / "sdk" / "conversations" conv_dir.mkdir(parents=True, exist_ok=True) # ⛔ 用**真 sid**:长度 36 且前缀 8 位唯一,否则下面的前缀兜底会走不到 CSID = "b232218f-c8e6-49cb-911c-f2afe00bcd39" CNO = "aaaaaaaa-0000-0000-0000-000000000000" cf = conv_dir / ("%s.log" % CSID) import datetime as _dt orig_conv, orig_st2 = m._conv_log, m._session_status # 🔴🔴 2026-10-04修:**时间戳必须相对"现在"生成**,⛔不许写死绝对时刻。 # 写死 `2026-10-04T03:24:24.996Z` 时,本用例只在**当天凌晨**成立;一旦跨过 # `SRSM_FRESH`(900s) 就恒定报红(实测 age=12372s)⇒ 这种红是**用例自己过期**, # **不是判据坏了** ⛔ 别去改判据迁就它(这正是"基线恒红 ⇒ 变异验证作废"那个坑)。 def _now_utc(offset_h: float = 0.0) -> str: t_ = _dt.datetime.now(_dt.timezone.utc) + _dt.timedelta(hours=offset_h) return t_.strftime("%Y-%m-%dT%H:%M:%S.000Z") try: m._conv_log = lambda sid=None: [cf] if sid and str(sid).replace("-", "") == CSID.replace("-", "") else [] # ⑦ 末条 to=working ⇒ busy cf.write_text('%s state-machine:transition ' '{"from":"idle","to":"working"}\n' % _now_utc(-0.05), encoding="utf-8") c7 = m._session_busy_conv(CSID) == "busy" # ⑧ 末条 to=idle ⇒ idle(⛔ 取**最后一条**) cf.write_text('%s state-machine:transition {"to":"working"}\n' '%s state-machine:transition {"to":"idle"}\n' % (_now_utc(-1), _now_utc(-0.02)), encoding="utf-8") c8 = m._session_busy_conv(CSID) == "idle" # ⑨ ⛔ 拿**工作区正则**去读会话级日志 ⇒ 必得 ''(老行为:恒读不到 ⇒ 恒判空闲) _keep = m._CONV_TR m._CONV_TR = m._SRSM_RE c9 = m._session_busy_conv(CSID) == "" m._CONV_TR = _keep # ⑩ 🔴 整条链:工作区源读不到 ⇒ **回落源必须救回来** # (改前 `_target_busy` = False = 误判空闲;改后必须 True) _ob, _os = m._session_busy, m._session_status m._session_busy = lambda s: "" m._session_status = lambda s: "working" cf.write_text('%s state-machine:transition {"to":"working"}\n' % _now_utc(-0.05), encoding="utf-8") c10 = m._target_busy(CSID) is True # ⑪ ⛔ 但库里已**终结** ⇒ 仍不判忙(第 ① 道闸优先级不能被回落源破坏) m._session_status = lambda s: "completed" c11 = m._target_busy(CSID) is False m._session_busy, m._session_status = _ob, _os finally: m._conv_log, m._session_status = orig_conv, orig_st2 try: cf.unlink() except Exception: pass # ⑫ 源码级:双源接线必须在(⛔ 少一个就退回单源误判) _src = _all[_all.find("def _target_busy"):_all.find("def _target_busy") + 2600] dual = ("_session_busy_conv" in _src) and ("_session_busy(sid)" in _src) # ⑬ sid 形态:库里 32 位无连字符 ⇄ 文件名 36 位带连字符,⛔ 两种都要能命中 _pathfix = ("replace(\"-\", \"\")" in _all) or ("replace('-', '')" in _all) # ===== 🔴🔴 2026-10-04 第 2 源的两个真缺陷(`vibe-product` / `b232218f…` 实测坐实)===== #⛔ 两个缺陷**同向**:都让"已收工/已死活的会话"被判`busy` ⇒ 调用方 `skipped="target-busy"` # ⇒ 永远收不到上报且**无告警**(静默饿死)⇒ 比"恒真"更隐蔽。 #① **跨日文件按遍历顺序覆盖**:`_conv_log` 返回 `[今天, 昨天]` ⇒ 昨天那条 `working` # 会把今天已经 `idle` 的盖掉。⚠️ 这条**必须真的造两个文件**才测得到 —— # 只喂一个文件的话两种实现都过(假绿)。 #② **完全没有新鲜度闸**(`_session_busy` 有 `SRSM_FRESH`,本源一个都没有)。 try: def _ts_utc(hours_ago: float) -> str: t = _dt.datetime.now(_dt.timezone.utc) - _dt.timedelta(hours=hours_ago) return t.strftime("%Y-%m-%dT%H:%M:%S.000Z") day_t = time.strftime("%Y-%m-%d") day_y = time.strftime("%Y-%m-%d", time.localtime(time.time() - 86400)) d_today = TEST_WS / "logs" / day_t / "sdk" / "conversations" d_yest = TEST_WS / "logs" / day_y / "sdk" / "conversations" d_today.mkdir(parents=True, exist_ok=True) d_yest.mkdir(parents=True, exist_ok=True) XSID = "dddddddd-1111-2222-3333-444444444444" f_today = d_today / ("%s.log" % XSID) f_yest = d_yest / ("%s.log" % XSID) orig_conv3 = m._conv_log try: # 🔴 **顺序照 `_conv_log` 的真形态**:`[今天, 昨天]`(⛔ 顺序反了测不到本缺陷) m._conv_log = lambda sid=None: [f_today, f_yest] if sid and XSID in str(sid) else [] # 今天末条 idle(会话已收工),昨天卡在 working f_today.write_text('%s state-machine:transition {"from":"working","to":"idle"}\n' % _ts_utc(0.01), encoding="utf-8") f_yest.write_text('%s state-machine:transition {"from":"idle","to":"working"}\n' % _ts_utc(20), encoding="utf-8") # ①跨日:取**时间戳最新**的那条⇒ 今天 idle ⇒判 idle(改前:被昨天覆盖⇒ busy) c14 = m._session_busy_conv(XSID) == "idle" # ② 新鲜度:单文件末条 working 但已是 3 小时前 ⇒ idle(改前:永久 busy) m._conv_log = lambda sid=None: [f_today] if sid and XSID in str(sid) else [] f_today.write_text('%s state-machine:transition {"from":"idle","to":"working"}\n' % _ts_utc(3), encoding="utf-8") c15 = m._session_busy_conv(XSID) == "idle" # ③ ⛔ 反向回归:末条 working 但**只有 2 分钟** ⇒ 必须仍判 busy # (防"新鲜度闸写太狠,把正在跑的也判空闲"⇒ 那是**往在跑的会话插话**的方向) f_today.write_text('%s state-machine:transition {"from":"idle","to":"working"}\n' % _ts_utc(0.03), encoding="utf-8") c16 = m._session_busy_conv(XSID) == "busy" # ④ ⛔ 时区:会话级日志行尾是 `Z`(UTC),⛔ 误按本地时区解析会差 8h # ⇒ 东八区下"2 分钟前"会被算成"8 小时前"⇒ 恒判过期 ⇒ ③ 变idle。 # 锚在**真算出来的差值**上:⛔ 不锚"源码里出现了 calendar"(那是文本出现过,不是判定生效)。 _ln = "%s state-machine:transition {\"to\":\"working\"}" % _ts_utc(0.03) _got = m._conv_ts(_ln) c17 = isinstance(_got, float) and abs(_got - time.time()) < 300 finally: m._conv_log = orig_conv3 for _f in (f_today, f_yest): try: _f.unlink() except Exception: pass except Exception as _e: # ⛔ 造不出样本 ⇒ 判红,⛔ 不许静默当过 c14 = c15 = c16 = c17 = "造样本失败:%s" % _e return [("① 末条 busy=true 且新鲜 ⇒ `busy`", c1), ("② 末条 busy=false ⇒ `idle`(取的是**最后一条**)", c2), ("③ 末条 busy=true 但已 2 小时 ⇒ `idle`(新鲜度真在起作用)", c3), ("④ 日志无该 sid ⇒ `''`(⛔ 不猜)", c4), ("⑤ 库里已终结 ⇒ 日志仍 busy=true 也**不判忙**", c5), ("⑥ `_session_busy` 用**命名组**、零位置组引用(防'数括号'错位)", named), ("⑦ 会话级源 末条 `to=working` ⇒ `busy`", c7), ("⑧ 会话级源 末条 `to=idle` ⇒ `idle`(取的是**最后一条**)", c8), ("⑨ ⛔ 拿**工作区正则**读会话级 ⇒ 恒 `''`(= 老行为;两条正则形状不同)", c9), ("⑩ 🔴 工作区源读不到时**回落源救回来**(改前 False = 往在跑的会话插话)", c10), ("⑪ 🔴 但库里已终结 ⇒ 回落源**不许**越权判忙(第 ① 道闸优先)", c11), ("⑫ 源码级:**双源接线**在(⛔ 少一个就退回单源误判)", dual), ("⑬ sid 形态兼容:32 位无连字符 ⇄ 36 位带连字符都命中", _pathfix), ("⑭ 🔴 跨日两个文件 ⇒ 取**时间戳最新**那条(⛔ 昨天的工作态不得盖掉今天的已收工)", c14), ("⑮ 🔴 会话级源也要有**新鲜度闸**(3 小时前的 working ⇒ idle)", c15), ("⑯ 🔴 反向回归:2 分钟前的 working **仍判 busy**(⛔ 防闸太狠⇒ 往在跑的会话插话)", c16), ("⑰ 🔴 会话级时间戳按 **UTC** 解析(⛔ 误按本地时区差 8h ⇒ 恒判过期)", c17)] @case("🔴 `--tick` 已删:参数表/分支/钩子调用**三处皆无**,且投递段仍真删") def t_tick_wired(): """🔴🔴 2026-10-05 `--tick` **机制删除**(用户:「检查程序 常驻 自己不会判断吗,非要什么 tick once 去触发?」)⇒ 本用例**整条反转**:从「tick 在位可跑」改为「tick 三处皆无」。 它原先扛的三件事已各归其位,⛔ 判据必须**逐条钉住迁移后的落点**(否则删了没人知道去哪了): ① 建检查会话排期(两条腿)⇒ **`--supervise` 主循环**里 `queue_pending()>0` 分流判 ② park 指纹探针 ⇒ 抽成 `_tick_park_probe()`,常驻主循环节流调 ③ 顺手续命常驻 ⇒ 钩子里改成 `--ensure`(`maybe_ensure_supervise`) ⚠️ 同时**保留**原「投递段已真删」的判据(那条红线与 tick 删不删无关,⛔ 不许顺手丢掉)。 """ m = imp() src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace") # ① 参数表:⛔ 不许还在 _KNOWN 里(留着 ⇒ 传 --tick 会静默退出,极难排查) _seg = src.split("_KNOWN = {")[1][:400] if "_KNOWN = {" in src else "" known = '"--tick"' not in _seg # ② 分支:`if "--tick" in sys.argv` 不在 no_branch = 'if "--tick" in sys.argv' not in src # ③ 迁移落点①②必须在(⛔ 只对源码字面 —— 这两处是新写的,字面稳定) _sup_seg = src.split('if "--supervise" in sys.argv')[1][:6000] if 'if "--supervise" in sys.argv' in src else "" legs_moved = ('maybe_spawn_check_agent("sessions-ended")' in _sup_seg and 'maybe_spawn_check_agent("queue-empty")' in _sup_seg and "_tick_park_probe()" in _sup_seg) rc, out = run_cli(["--tick"]) # ⛔ 传它必须**被拒**(非 0 或报未知参数) # 🔴 2026-10-01 改:原先这里写**绝对路径**指到某个具体工作区的 `tools/wb-result-hook.py` # ⇒ 两处都不对:① 违反「技能里只用相对路径」(包去依赖"使用方的"项目目录); # ② 宿主接线自 2026-10-01 起已改指**包内**那份 ⇒ 老写法在别的机器上恒走"文件不在,跳过", # 看着绿、其实**什么都没验**(静默假绿)。现在按包内相对路径取自己那份钩子。 hook = HERE / "hooks" / "wb-result-hook.py" hook_ok, hook_note = True, "(钩子文件不在,跳过)" if hook.exists(): hs = hook.read_text(encoding="utf-8", errors="replace") # 🔴🔴 反转:钩子必须**没有** `--tick` 调用,且**要有** `--ensure` 续命(⛔ 少了就没人拉常驻) # ⚠️⛔ 判据**只认带引号的字面 `"--tick"`**,⛔ 不认 `--tick` 裸串 —— # 删除说明的注释里**必然**提到它(否则读者不知道删了什么)⇒ 裸串判据**永久假红** # (P0-13 同族:docstring 提被删项 ⇒ 只能判"调用/带引号",⛔ 不能判"出现")。 hook_ok = ('"--tick"' not in hs) and ("maybe_run_supervisor_tick" not in hs) \ and ("maybe_ensure_supervise" in hs) and ('"--ensure"' in hs) hook_note = "钩子已无 `--tick` 调用,续命改走 `--ensure`" # 🔴🔴 2026-10-03 **投递段退役**:本用例原判据是「`--tick` 用 `mutate=True` 调 `supervise()` # ⇒ 它是唯一投递方」——**这条判据已随投递退役而失效**(没有"投递方"这个角色了)。 # ⚠️ ⛔ **不许把它改成"`--tick` 用 `deliver=False` 调 ⇒ 它是唯一不投递方"** —— # 那是**换个说法说同一件事**,仍对着源码字面匹配,一重构就假红(P0-13 同族)。 # ✅ **正解=退役它,换成"投递段真删"的判据**,且判据必须**可证伪**: # ① **真调用** `supervise({}, deliver=True, mutate=True)`(⛔ 传最"能投"的那组参数) # ⇒ `deliver.skipped` 必须为 `retired-20261003`。 # ⚠️⛔ **不许去读生产 state 取证**(本文件 L169 的元用例明令「自测不碰生产」, # 且那样判据依赖"生产已被新代码跑过"⇒ 换台机器必假红)。 # ② **`supervise()` 函数体内零个调用**——判据走 **AST**(⛔ 不是字面 grep: # docstring 里**必然**提到 `_deliver_str`(要说明退役了什么)⇒ 字面判据永久假红)。 try: _r = m.supervise({}, deliver=True, mutate=True) except Exception as _e: _r = {"deliver": {"skipped": "raised:%r" % (_e,)}} _sk = str((_r.get("deliver") or {}).get("skipped") or "") calls = _calls_in(src, "supervise") r_nocall = ("_deliver_str" not in calls) and ("follow_for_topic" not in calls) return [("`--tick` ⛔ 已从参数表移除(留着=传它会静默退出)", known), ("`--tick` 分支已删(源码无 `if \"--tick\" in sys.argv`)", no_branch), ("⛔ 传 `--tick` 被拒(未知参数,rc=%s)" % (rc,), rc != 0 or ("未知参数" in str(out))), ("🔴 迁移落点:`sessions-ended`/`queue-empty`/park 探针**都在 `--supervise` 内**", legs_moved), ("🔴 2026-10-03 退役判据①:真调用 `supervise(deliver=True, mutate=True)` ⇒ `deliver.skipped` 必为 " "`retired-20261003`(实测 = %r)" % (_sk,), _sk == "retired-20261003"), ("🔴 2026-10-03 退役判据②:`supervise()` 体内**零个** `_deliver_str`/`follow_for_topic` **调用**" "(AST 判据,⛔ 不看 docstring;实测调用集 = %s)" % (sorted(calls) or "空",), r_nocall), (hook_note, hook_ok)] def _calls_in(src: str, fname: str) -> set: """🔴 **函数 `fname` 体内真正被调用的名字集合**(走 AST,⛔ 不做字面 grep)。 🔴🔴 **为什么必须走 AST**(2026-10-03 实测踩到):退役说明的 docstring 里**必然**提到 被删的函数名(否则读者不知道删了什么)⇒ 任何 `grep "_deliver_str(" in <函数体切片>` 都**永久假红** ⇒ 只能判"**调用**",⛔ 不能判"**出现**"。 ⚠️ 附带好处:AST 只认 `Name`/`Attribute` 的 `func` 位 ⇒ 注释、字符串、docstring 一律不算。 """ import ast as _ast try: tree = _ast.parse(src) except Exception: return {""} for node in _ast.walk(tree): if isinstance(node, _ast.FunctionDef) and node.name == fname: out = set() for sub in _ast.walk(node): if isinstance(sub, _ast.Call): f = sub.func if isinstance(f, _ast.Name): out.add(f.id) elif isinstance(f, _ast.Attribute): out.add(f.attr) return out return {""} @case("对账:僵尸(台账 running 但没人跑)⇒ 标 blocked") def t_reconcile_zombie(): # 🔴 2026-09-30 修(**本用例曾假红**):僵尸的判据是「**除观察者外无人跑** ∧ 无 IN_PROGRESS」—— # 而测试机上**天天有别的工作会话在跑** ⇒ 前提不成立 ⇒ 不能判它红(⛔ 也不许假装绿)。 # 正确姿势:**前提不成立就 SKIP**(读不出宿主库、或确实有别的会话在跑)。 m = imp() put_tasks({"Z1": {"state": "running", "by": "已经死掉的棒", "t": time.time()}}) r = m.reconcile({}) if r.get("skip"): put_tasks({}) return [("SKIP:读不到宿主库(%s)" % r["skip"], True)] # 🔴 2026-10-01 加:僵尸分支的**完整前提** =「**除观察者外**无在跑会话」∧「无 IN_PROGRESS 运行」。 # 旧用例只靠 `adopted` 反推**前一半** ⇒ 后半没覆盖 ⇒ **自动化会话跑本自测时**, # 它**自己**的 automation run 必然还是 `IN_PROGRESS`、且它自己就是唯一在跑的会话 # ⇒ 前提不成立却没 SKIP ⇒ **必然假红**(2026-10-01 实测:整套只有本条红, # 而 `reconcile` 返回 `adopted=[] zombie=[]`)。⚠️ 同一类假红的另一半,照既有姿势补 SKIP。 try: _me = (os.environ.get("CODEBUDDY_SESSION_ID") or "")[:8] _con = m._db() _inprog = int(_con.execute("select count(*) from automation_runs " "where status='IN_PROGRESS'").fetchone()[0]) _others = [s for (s,) in _con.execute("select id from sessions where status='working'") if not (_me and str(s)[:8] == _me)] _con.close() except Exception: _inprog, _others = 0, [] if _others or _inprog: put_tasks({}) return [("SKIP:前提不成立(另有 %d 个会话在跑 · %d 个自动化运行 IN_PROGRESS)" % (len(_others), _inprog), True)] if r.get("adopted"): put_tasks({}) return [("SKIP:当前确有 %d 个别的会话在跑 ⇒ 『除观察者外无人跑』前提不成立" % len(r["adopted"]), True)] v = m._load_tasks().get("Z1") or {} ok = v.get("state") == "blocked" and "执行者已消失" in str(v.get("block_reason")) put_tasks({}) return [("Z1 被标 blocked 且原因是『执行者已消失』", ok)] @case("对账:幂等(连跑两次结论一致)") def t_reconcile_idempotent(): # 🔴 2026-09-30 修(**同一类假红**):第 1 次会**收编**当前正在跑的别的工作会话(环境相关), # 第 2 次才进入"没有新东西"的稳态 ⇒ 幂等性只能比较 **第 2 次 vs 第 3 次**。 run_cli(["--reconcile"]) _, o1 = run_cli(["--reconcile"]) _, o2 = run_cli(["--reconcile"]) put_tasks({}) return [("第 2 次 = 第 3 次输出一致", o1.strip().splitlines()[:1] == o2.strip().splitlines()[:1]), ("第 2 次已无新收编(『收编 0 条』)", "收编 0 条" in o1)] @case("CLI:未知参数 ⇒ rc=2 且**不落常驻**") def t_unknown_arg(): rc, out = run_cli(["--nonsense"], timeout=20) return [("rc==2", rc == 2), ("提示未知参数", "未知参数" in out)] @case("CLI:--where / --reqs 可跑且 rc=0") def t_basic_cli(): rc1, o1 = run_cli(["--where"]) rc2, _ = run_cli(["--reqs"]) return [("--where rc==0 且打印路径", rc1 == 0 and "workspace" in o1), ("--reqs rc==0", rc2 == 0)] @case("结构:关键名字/设施在位(防『误删常量』重演)") def t_symbols(): src = (HERE / "collabd.py").read_text(encoding="utf-8") need = ["CLAIMS", "SELF_SID", "STALE", "DOING_TTL", "def reconcile(", "def supervise(", "def _deliver_str(", "def _guard_says_stop(", "def parse_session_name(", "def topic_of(", "def load_goal(", "GOAL_F"] miss = [n for n in need if n not in src] return [("关键符号齐全(缺:%s)" % (miss or "无"), not miss)] @case("结构:所有 subprocess 调用都带 creationflags(不显窗)") def t_nowindow(): bad = [] for fn in ("collabd.py", "guard.py"): for i, l in enumerate((HERE / fn).read_text(encoding="utf-8").splitlines(), 1): if "subprocess.run(" in l or "subprocess.Popen(" in l: seg = " ".join((HERE / fn).read_text(encoding="utf-8").splitlines()[i - 1:i + 4]) if "creationflags" not in seg: bad.append("%s:%d" % (fn, i)) return [("无『没带 creationflags』的子进程调用(%s)" % (bad or "无"), not bad)] @case("结构:fatal 不再 exit 0(防『假绿』)") def t_failopen(): src = (HERE / "collabd.py").read_text(encoding="utf-8") i = src.find('log("fatal %s" % e)') seg = src[i:i + 200] if i >= 0 else "" return [("fatal 分支是非零退出", "sys.exit(1)" in seg), ("⛔ 不是 exit(0)", "sys.exit(0)" not in seg)] @case("结构:投递段**已真删**(⛔ 旧「投递方唯一」判据已于 2026-10-03 退役)") def t_deliver_switch(): src = (HERE / "collabd.py").read_text(encoding="utf-8") m = imp() # 🔴🔴 2026-10-03:本用例原有一条判据「`--tick` 是唯一投递方(`supervise(st, deliver=True)`)」 # ——**投递退役后这个角色已不存在**,那条判据永久拿不到状态。 # ⚠️ ⛔ **不许把它改成 `supervise(st, deliver=False)`**(=换个说法说同一件事,仍是字面匹配脆断)。 # ✅ 退役 + 换可证伪判据:`supervise()` 体内**零个** `_deliver_str`/`follow_for_topic` **调用** # (AST 判据 —— ⛔ 字面 grep 会被 docstring 里的退役说明永久假红,见 `_calls_in` 的注释)。 calls = _calls_in(src, "supervise") r = m.supervise({}, deliver=True, mutate=True) sk = str((r.get("deliver") or {}).get("skipped") or "") return [("supervise 有 deliver 参数", re.search(r"def supervise\(st: dict, deliver: bool", src) is not None), ("supervise 有 mutate 参数(签名兼容保留,⛔ 体内已不读)", re.search(r"def supervise\(st: dict, deliver: bool = True, mutate: bool = True\)", src) is not None), ("one_round 走**纯投影**(deliver=False, mutate=False)", re.search(r"supervise\(st,\s*deliver=False,\s*mutate=False\)", src) is not None), ("🔴 2026-10-03 退役判据①:`supervise()` 体内**零个** `_deliver_str`/`follow_for_topic` **调用**" "(AST;实测调用集 = %s)" % (sorted(calls) or "空",), ("_deliver_str" not in calls) and ("follow_for_topic" not in calls)), ("🔴 2026-10-03 退役判据②:真调用 `supervise(deliver=True, mutate=True)` ⇒ " "`deliver.skipped` 必为 `retired-20261003`(实测 = %r)" % (sk,), sk == "retired-20261003"), ("wake_round 已标注停用", "已停用" in src)] @case("主会话:**解析出来的**(不只是读登记)+ ⛔ 拒绝盲投(防『投错窗口』回潮)") def t_main_resolution(): """用户 2026-09-30:「协作机制能发现 主会话 换了吗」——旧答案是**不能**(登记静态+路径依赖, 且看板与投递共用同一份登记 ⇒ 一起钉在旧 id 上)。本用例把修法钉住。""" src = (HERE / "collabd.py").read_text(encoding="utf-8") m = imp() # 🔴 2026-10-01:本用例起要**真跑函数**(见下面那条"能吃类别集合") return [ ("有 resolve_main(登记失效时按当前事实解析)", "def resolve_main(" in src), ("登记**有效才认**(`reg in live`)", "or reg in live" in src), ("按**工作区**解析(用户定则:**一个工作区一条主会话**)", "def _same_ws(" in src), ("工作区比较**同时**小写+统一斜杠(⛔ 防 `E:/x` 与 `E:\\x` 判成两个)", 'replace("\\\\", "/")' in src), ("🔴 按工作区解析时**排除「协作侧」命名**(2026-10-02 两类改造:唤醒/跟进已退役 ⇒ 只排 `worker`)" "—— 接续会话是干活的棒,⛔ 不是主会话候选(否则它冒充主会话 ⇒ 通知投给它自己)", '_role not in ("worker",)' in src), ("⛔ **不得按 `status='working'` 筛主会话**(主会话两轮之间是空闲 ⇒ 加了必然漏掉它)", "status='working' \"\n \"order by last_activity_at" not in src), ("⛔ **不得按 `is_background_automation` 排主会话**(接续会话本身就是自动化起的 —— 实测主会话也是 1)", "coalesce(is_background_automation, 0) = 0" not in src), ("🔴 **认「主控」前缀**(用户 2026-09-30 定名:主会话的显式标记)", 'startswith("主控")' in src and '"prefix:主控"' in src), ("解析不出 ⇒ 明确报 `no-main-session`", '"no-main-session"' in src), ("目标不在活会话里 ⇒ 明确报 `main-not-live`", '"main-not-live"' in src), ("🔴 **盲选回落已删除**(⛔ 不再随手投给任意一条会话)", re.search(r'g = next\(\(x for x in gws if x\["sessionId"\]\), None\)', src) is None), # ── 🔴 2026-09-30 新增:**同一工作区 · 多任务类别**(用户要求"同一个工作区 多会话协作")─── ("🆕 有 `resolve_mains()`(按**任务类别**各解析一条主会话)", "def resolve_mains(" in src), ("🆕 有 `_scan_mains()`(与 `resolve_main` **共用同一遍扫描** ⇒ **同一份 live**,⛔ 不各扫各的=防两边漂移)", "def _scan_mains(" in src and src.count("_scan_mains(st, live)") >= 2), # 🔴 2026-10-01 体检修发现的**投递断点**:解析出一条**已经结束**的「主控」会话当投递目标 # ⇒ 投递一路 `main-not-live`(程序在跑,通知永远发不出去)。⇒ **候选只认活着的**。 ("🆕 候选**只认活着的**:`_pick_live` 在位且两处解析都用它", "def _pick_live(" in src and src.count("_pick_live(named, live)") >= 2 and src.count("_pick_live(cand, live)") >= 2), # 🔴🔴 2026-10-01 **两条旧断言被本轮口径推翻 ⇒ 改判据**(⛔ 不是删掉 —— 性质还得有人守): # 用户逐字:「**换新会话 唤醒的是 跟进会话,主会话只能是用户触发**」 # ⇒ `_deliver_str` 的投递目标**从"该类别的主会话"改成"该类别的跟进会话"** # (`_rms = resolve_mains(st, …)` + `main_for_topic(_rms, topic)` **故意**从源码里消失了)。 # ⚠️ 那两条旧断言查的正是这两个字面量 ⇒ 留着就是**恒红**(不是代码坏了,是**判据过时**)。 # ⚠️ 但它守的**性质**("按类别选、⛔ 不投错窗口")必须继续守 ⇒ 换成查新函数,并补两条。 ("🆕 投递目标经 `follow_for_topic()` **按类别**取(⛔ 不再是主会话、也不再是「唯一那一条」)", re.search(r"_rft = follow_for_topic\(st, topic,", src) is not None), ("🆕 `follow_for_topic` 对「已登记类别」**严格取该类别**(⛔ 不回落别类别=防投错窗口)", re.search(r"if tp and tp in tps:", src) is not None), ("🔴 `follow_for_topic` **绝不返回主会话**(用户口径:主会话只由用户触发)" "—— 2026-10-02 两类改造后**真调用**恒返回 `sid=\"\"`(短路退役)⇒ 更不可能返回主会话", (not str(imp().follow_for_topic({}, "X", []).get("sid") or ""))), ("🔴 解析不出跟进会话 ⇒ 记 `no-follow-session` + **喊用户**(⛔ 不再降级投主会话)", '"no-follow-session"' in src and 'follow-not-live' in src and '"no-main-session"' not in src.split("def _deliver_str(")[1].split("\ndef ")[0]), ("🆕 `main_for_topic` 对「已登记类别」**严格取该类别**(⛔ 不回落别类别=防投错窗口)", 'if tp and tp in (rms.get("topics") or [])' in src), ("🆕 `_deliver_str` 支持 `topic=` 参数(默认空 ⇒ 走 default,向后兼容)", re.search(r'def _deliver_str\(text: str, key: str, st: dict, topic: str = ""\)', src) is not None), # 🔴🔴 2026-10-03 退役:本条原判据 `'topic=str((v or {}).get("line") or "")' in src` # 指的是 `supervise()` 里那行 `_deliver_str(..., topic=…)`——**投递段已真删 ⇒ 该行不存在**。 # ⚠️ ⛔ 不许改成 `topic="" in src` 之类(=断言恒真,P0-13 同族)。 # ✅ 退役 + 换可证伪判据:`_deliver_str()` 本体**仍支持 `topic=` 参数** # (它是"将来重建收件人"的实现,签名不许退化)⇒ 改成断言**函数签名里有 `topic: str = ""`**, # 那是真判据(谁把参数删了就会红),且**不是**靠一个字面串匹配调用点。 ("🆕 `supervise()` 曾按台账条目的 `line` 传 `topic=`(⛔ 2026-10-03 随投递退役)", "topic=str((v or {}).get(\"line\") or \"\")" not in src), ("🆕 退役后仍保留:`_deliver_str()` 本体**签名仍带 `topic=`**(重建收件人时的实现)", re.search(r'def _deliver_str\(text: str, key: str, st: dict, topic: str = ""\)', src) is not None), # 🔴 2026-10-01 改:原来这条是**纯源码匹配** `"for _t in _as_list(short):" in src` —— # 我今天给 `_in_project` 加了第三条判据、顺手把这一行提成 `_tps = _as_list(short)`, # 行为**一模一样**,它却报红了(**脆断**:对着源码字面写断言,一重构就假红)。 # ⇒ 改成**行为断言**(真喂一个 list 进去看结果),源码那半只留"函数还在"。 ("🆕 归属判据能吃**类别集合**(`_in_project` 的 main_sid / short 传 list 都对)", "def _as_list(" in src and m._in_project("", "[协作]-[B类]-x", ["M1"], ["A类", "B类"]) and m._in_project("M1", "随便什么标题", ["M1", "M2"], ["A类"])), ("🆕 收尾确认**排除全部主会话**(⛔ 否则别类别的主会话被算成「在跑」⇒ 永远拦住)", 'mains = list(_rms0.get("all_sids") or [])' in src and "if sid in mains:" in src), ("🆕 两处 `_goal_topics()` 同款(技能里 goal.topics 是**唯一**类别源,缺省回落 `short`)", src.count("def _goal_topics(") == 1 and '"topics"' in src), ("换了目标 ⇒ 跟随 + 显式告警(⛔ 不静默)", "def _follow_main(" in src and "主会话变更:" in src and "need_user(" in src), ] @case("主会话:看板与投递**同一套判据**(⛔ 防两处漂移 —— 漂了就没人能发现换主会话)") def t_main_same_rule(): a = (HERE / "collabd.py").read_text(encoding="utf-8") b = (HERE / "board.py").read_text(encoding="utf-8") pat = r'def _main_sid\(st: dict\) -> str:' return [ ("两处都定义了 _main_sid", re.search(pat, a) is not None and re.search(pat, b) is not None), ("两处都按 roles -> wake 的顺序取(逐字同款)", 'st.get("roles")' in a and 'st.get("roles")' in b and 'st.get("wake")' in a and 'st.get("wake")' in b), ] @case("主会话:**退役角色不许当候选** —— 行为级(真造宿主库 + 真跑扫描,⛔ 不只测那个小函数)") def t_main_retired_not_candidate(): """🔴🔴 2026-10-04 21:5x 新增(用户拍板「候选一」)。 **为什么必须是行为级、⛔ 不能只测 `is_retired_role_title()` 本身**(**变异验证当场抓到**): 我第一版只加了"这个函数对这几个样本返回什么"的断言 ⇒ 植入变异 **把闸从 `_scan_mains()` 的筛子里整段拆掉** ⇒ 那些断言**照样全绿**(`grep MUTANT` = 1 证明变异真植进去了,`PASS 87 / FAIL 0` 却是假的)—— 因为**没有任何一条测到"闸被用上了"**。 ⇒ 正解=造一个**真宿主库**,里面放一条 `[跟进]-…` 与一条真主会话, **真跑一遍扫描**,看结果里有没有那条退役角色。 ⚠️ 这正是"判定标准恒绿"的典型形态:测了零件、没测装配。 **被测对象**:`collabd.py::_scan_mains()` —— 它是要**扫库**的那一侧(board 侧同款判据另有对账)。 """ m = imp() import sqlite3 as _sq import tempfile as _tf import shutil as _sh td = _tf.mkdtemp(prefix="selftest-retired-") ws = Path(td) / "wsX" (ws / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True) (ws / "tmp" / "supervise-inbox" / "goal.json").write_text( json.dumps({"id": "wsX", "title": "X 区目标", "short": "wsX"}, ensure_ascii=False), encoding="utf-8") db = Path(td) / "fake-host.db" now = int(time.time() * 1000) try: cn = _sq.connect(str(db)) cn.execute("create table sessions (id text primary key, title text, custom_title text, " "status text, cwd text, updated_at integer, last_activity_at integer, " "deleted_at integer)") # ① 退役角色(`[跟进]-…`)—— 改前会被收进候选(实测 `6ab1463e` 就是这个形状) # ② 现役真主会话(无前缀)—— ⛔ 不许被误排 # ③ 正牌 worker —— 旧口径就该排它(对照组) # 🔴🔴 `custom_title` 必须写 **`None`**,⛔ 不许写 `""`(**夹具说谎的第二个坑,实测踩到**): # 查询是 `coalesce(custom_title, title, '')` ⇒ 写 `""` 时 `coalesce` **返回 `""`** # (空串不是 NULL,不会回落到 `title`)⇒ 每条会话拿到的标题都是空串 # ⇒ 退役闸/角色判据**全都测不出东西**、用例恒绿而**毫无意义**。 # ✅ 真库里 `custom_title` 实测就是 **`None`**(`57f58ecf` 等逐条验过)⇒ 夹具照抄真库形态。 # ⚠️ 这与 P0-53 是同一个形状:**合成区必须让被测对象读到真实判据**, # ⛔ 不许靠"反正断言过了"糊过去(这个坑正是我自己写夹具时踩出来的)。 _cwd = str(ws).replace("\\", "/") for i, t in enumerate(("[跟进]-机制排查与修复-队列跟进", "复盘失败并避免重犯", "[协作]-[机制排查与修复]-S12 收尾")): cn.execute("insert into sessions values (?,?,?,?,?,?,?,?)", ("sid-%d" % i, t, None, "completed", _cwd, now - i * 1000, now - i * 1000, None)) cn.commit() cn.close() # 🔴 让 `_scan_mains()` 扫到这个库 + 认这个工作区。两条都要对准(读源码得的键名): # · `collabd.py::_db()` 读 `C["host_db"]`(配置)或 `CODEBUDDY_CONFIG_DIR/workbuddy.db` #  ⇒ ⛔ 没有 `DSHS_DB_URL` 这条路(我第一版按它写 ⇒ 夹具指不到库)。 # · `collabd.py::_same_ws()` 比的是**模块级 `WS`**(加载时定的)⇒ 也必须在加载时给对。 # 两者都靠**改配置再重载模块**拿(⛔ 不是改 env;`WS` 不是 env 读的)。 _cfg = Path(td) / "collabd.testconfig.json" _cfg.write_text(json.dumps({"host_db": str(db).replace("\\", "/"), "workspace": str(ws).replace("\\", "/")}, ensure_ascii=False), encoding="utf-8") _old_cfg = os.environ.get("COLLABD_CONFIG") _old_wsv = os.environ.get("COLLABD_WORKSPACE") os.environ["COLLABD_CONFIG"] = str(_cfg) os.environ["COLLABD_WORKSPACE"] = str(ws).replace("\\", "/") try: _u = __import__("importlib.util", fromlist=["util"]) _sp = _u.spec_from_file_location("_cb_retired", HERE / "collabd.py") _mc = _u.module_from_spec(_sp) _sp.loader.exec_module(_mc) # ⚠️ 必须**重新加载**:`WS` 是模块级常量 sc = _mc._scan_mains({}) finally: for _k, _v in (("COLLABD_CONFIG", _old_cfg), ("COLLABD_WORKSPACE", _old_wsv)): if _v is None: os.environ.pop(_k, None) else: os.environ[_k] = _v cand = [str(s) for s in (sc.get("cand") or [])] by_topic = sc.get("by_topic") or {} err = str(sc.get("err") or "") except Exception as _e: # pragma: no cover return [("用例未跑起来:%s" % _e, False)] finally: _sh.rmtree(td, ignore_errors=True) return [ ("🔴 夹具扫到库了(⛔ 空扫描 ⇒ 下面几条会恒绿 —— 那正是『测了零件没测装配』)", not err), ("🔴 **退役角色 `[跟进]-…` ⛔ 不进候选**(sid-0;改前会进 ⇒ 看板「主会话」位上坐着它)", "sid-0" not in cand), ("(对照组)**真主会话 ⛔ 不许被误排**(sid-1;它是无前缀的,排它=把真主会话弄丢)", "sid-1" in cand), ("(对照组)正牌 worker `[协作]-…` ⛔ 不进候选(旧口径,⛔ 不许被本次改动破坏)", "sid-2" not in cand), ("🔴 退役角色 ⛔ 不进 `by_topic`(改前的真实病症:`main_by_topic` 两个类别**全指向 " "`[跟进]` 会话**)", all(str((v or {}).get("sid") or "") != "sid-0" for v in by_topic.values())), ] @case("主会话:登记为 main 但**已哑** ⇒ ⛔ 不认它 + `--declare main` 是**换**不是「加」") def t_main_deaf_and_declare(): """🔴🔴 2026-10-01 修 **V2「主会话可响应」的硬阻塞**(两个缺陷叠在一起,缺一条都治不好)。 **缺陷①**:`resolve_main` 的第①条快路原为 `if reg and (not live or reg in live)` —— **只查"活着",⛔ 不查"哑"**。而"哑"(会话诊断日志撞 ~10 MiB ⇒ 宿主拒写 ⇒ **界面永不刷新**)**照样在 `live` 里** ⇒ 一条哑掉的主会话来一次认一次,一路投到 `target-deaf`。 ⚠️ 而 ② 走的 `_pick_live()` **本来就排哑**(2026-10-01 专为"别再投给哑会话"加的)—— **① 却从没接上它** ⇒ 同一条哑会话「② 排得掉、① 排不掉」(判据打架,当日第三次同族)。 **缺陷②**:`--declare --role main` 是**加**不是**换**(P0-11 的正解①就是它,号称"零代码改动立刻可用") —— 而 `_main_sid()` 取 `roles` 里**第一条** `main`,Python dict 对已存在的键赋值**不改位置** ⇒ 老那条**永远排前面** ⇒ 新登记的主会话**永远轮不到** ⇒ 逃生口形同虚设。 🔴 而且它掩盖得更深:`_deliver_str` 的**粗判不传活会话**(`resolve_mains(st, [])`) ⇒ `_pick_live` 在 `live` 为空时**回退 `c[0]`** ⇒ 每轮先认下那条哑的、**直接返回**, **走不到后面"拿锁后用活网关列表重算"那一步**。 """ m = imp() A = "aaaa1111-0000-0000-0000-000000000001" B = "bbbb2222-0000-0000-0000-000000000002" # ── 缺陷①:把 A 伪造成"哑",A 登记在 B 前面 ── _orig = m._deaf_sids try: m._deaf_sids = lambda: {A} r_live = m.resolve_main({"roles": {A: "main", B: "main"}}, [A, B]) # 粗判那条路(不传活会话)⇒ 只能拿第一条 —— 这正是"必须让登记权威化"的理由 r_coarse = m.resolve_main({"roles": {A: "main", B: "main"}}, []) finally: m._deaf_sids = _orig # ── 缺陷②:`--declare --role main` 必须把别的 main 降级 ── # 在**测试区**里造一份 state(⛔ 不碰生产),用完还原 SP = TIN / "collabd-state.json" bak = SP.read_text(encoding="utf-8") if SP.exists() else None SP.write_text(json.dumps({"roles": {A: "main", B: "worker"}}, ensure_ascii=False), encoding="utf-8") _env = _mk_env() _env["CODEBUDDY_SESSION_ID"] = B p = subprocess.run([PY, "-u", str(HERE / "collabd.py"), "--declare", "--role", "main"], cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=_env, timeout=60, errors="replace") _st = json.loads(SP.read_text(encoding="utf-8")) _out = p.stdout or "" if bak is None: SP.unlink(missing_ok=True) else: SP.write_text(bak, encoding="utf-8") _roles = _st.get("roles") or {} S = (HERE / "collabd.py").read_text(encoding="utf-8") return [ ("🔴 ① 排哑后才认下**活着的**那条登记(拿到 %s)" % (r_live["sid"][:8] or "(空)"), r_live["sid"] == B and r_live["source"] == "roles:main"), ("🔴 ① 换了登记的那条 ⇒ **报 `switched_from`**(调用方才会降级+告警,⛔ 不静默换)", r_live["switched_from"] == A), ("🔴 ① 走的是**同一个** `_pick_live`(⛔ 不再有两台判据)", "_reg = _pick_live(_rm, live)" in S), ("⚠️ 粗判(**不传活会话**)退回第一条 ⇒ 这就是「必须让登记权威化」的理由(坑已固化)", r_coarse["sid"] == A), ("🔴 ② `--declare --role main` 把原 main **降级为 worker**(换,⛔ 不是加)", _roles.get(B) == "main" and _roles.get(A) == "worker"), ("🔴 ② 且**说出口**(⛔ 不静默改别人的角色)", "已降级为 worker" in _out), ] @case("结构:守护生命周期(优雅退出设施在位)") def t_graceful(): src = (HERE / "guard.py").read_text(encoding="utf-8") return [("有 _shutdown(优雅收尾)", "def _shutdown(" in src), ("子进程打 DSH_GUARDED 标", "DSH_GUARDED" in src), ("子程序侧有 _guard_says_stop", "_guard_says_stop" in (HERE / "collabd.py").read_text(encoding="utf-8"))] @case("归属判据:看板 ≡ 收尾确认(⛔ 防两处漂移;⛔ 不按 cwd 推断)") def t_scope_parity(): """🔴 用户 2026-09-30 两条明示:「**要明确哪些会话是属于某个需求目标项目的**」+ 「**这个看板上应该明确显示是哪个需求目标项目**」。 `collabd._in_project()`(`--ready-next` 用)与 `board.in_project()`(看板用)是**重复实现** ⇒ 一旦漂移就会出这种怪事:**看板说"本项目没人在跑",而 `--ready-next` 被别的项目的会话拦死**。 本用例把两者摆到同一张用例表上逐例比对(含"AI变现日报"这类**别人家的会话**必须判否)。 """ code = r''' import importlib.util, inspect, json, sys sys.dont_write_bytecode = True # ⛔ 别让测试自己往技能目录里落 __pycache__ def load(name, path): spec = importlib.util.spec_from_file_location(name, path) m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m H = sys.argv[1] cb = load("cb", H + "/collabd.py") bd = load("bd", H + "/board.py") cases = [ ("main1", "任意标题(主会话)", "main1", "手机接入", True), ("aaa", "[协作]-[手机接入]-N10 复测", "main1", "手机接入", True), ("bbb", "AI变现日报 · 今日生成", "main1", "手机接入", False), ("ccc", "随便一个标题", "main1", "手机接入", False), ("ddd", "[其他主题]-x", "main1", "手机接入", False), ("eee", "x", "main1", "", False), # short 空 ⇒ 只剩主会话能命中 ("ggg", "x", "ggg", "", True), # 主会话判据 ⛔ 不依赖 short ("hhh", "[其他主题][手机接入]", "main1", "手机接入", True), ("fff", "[手机接入] 加个 [手机接入x] 干扰", "main1", "手机接入", True), ] bad = [] for sid, ti, msid, short, exp in cases: a = bool(cb._in_project(sid, ti, msid, short)) b = bool(bd.in_project(sid, ti, {"main_sid": msid, "short": short})) if a != b: bad.append("%s 判据漂移(collabd=%s / board=%s)" % (sid, a, b)) if a != exp: bad.append("%s 实得 %s / 期望 %s" % (sid, a, exp)) # 🆕 2026-09-30:**同一个工作区 · 多任务类别**(用户要求「通过协作会话名称前缀区分具体执行会话」)—— # 两处实现都必须认"**任一**已登记类别",且⛔ **不得**把未登记的类别算进来。 mt = [ # sid, 标题, 默认主会话, 类别清单, 期望 ("m1", "主控 · 机制线", "m1", ["手机接入", "机制线"], True), # 默认主会话 ("w1", "[协作]-[机制线]-唤醒A", "m1", ["手机接入", "机制线"], True), # 第 2 类别 ("w2", "[协作]-[手机接入]-N9", "m1", ["手机接入", "机制线"], True), # 第 1 类别 ("w3", "[协作]-[别的类别]-x", "m1", ["手机接入", "机制线"], False), # 未登记 ⇒ 不算 ("w4", "唤醒机制线专项(无方括号)", "m1", ["手机接入", "机制线"], False), # 归属判据要求 `[类别]` ("w5", "[协作]-[机制线]-x", "m1", ["手机接入"], False), # 只登记一类 ⇒ 另一类不算 ("w6", "x", "m1", [], False), # 无类别 ⇒ 只剩主会话判据 ("w7", "x", "w7", ["手机接入", "机制线"], True), # 主会话判据与类别无关 ] for sid, ti, msid, tps, exp in mt: a = bool(cb._in_project(sid, ti, msid, tps)) b = bool(bd.in_project(sid, ti, {"main_sid": msid, "main_sids": [msid] if msid else [], "topics": tps, "short": (tps[0] if tps else "")})) if a != b: bad.append("%s 多类别判据漂移(collabd=%s / board=%s)" % (sid, a, b)) if a != exp: bad.append("%s 多类别实得 %s / 期望 %s" % (sid, a, exp)) # 🆕 结构对齐:两处 `_scan_*` 的 `by_topic` 值**必须是 dict**({sid,source,explicit})—— # ⛔ 实测踩到:board 曾存裸 sid ⇒ 调用方把整个 dict 当 sid 塞进 `all_sids`(静默错值)。 _cbs = inspect.getsource(cb) _bds = inspect.getsource(bd) for _nm, _s in (("collabd", _cbs), ("board", _bds)): if '"explicit"' not in _s or '"by_topic"' not in _s: bad.append("%s 的 by_topic 结构不合规(须存 {sid,source,explicit})" % _nm) sig = str(inspect.signature(bd.in_project)) if "cwd" in sig: bad.append("board.in_project 仍带 cwd 参数:%s" % sig) src = open(H + "/board.py", encoding="utf-8").read() if "project_scopes" in src: bad.append("board.py 仍残留 cwd 版的 project_scopes()") print(json.dumps({"bad": bad, "sig": sig}, ensure_ascii=False)) ''' p = subprocess.run([PY, "-c", code, str(HERE)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=_mk_env(), timeout=30, errors="replace") try: r = json.loads((p.stdout or "").strip().splitlines()[-1]) except Exception: return [("子进程跑不起来(rc=%s):%s" % (p.returncode, (p.stdout or "")[-300:]), False)] return [("15 例判据一致且与期望相符(7 基础 + 8 多类别;%s)" % ("、".join(r["bad"]) or "全对"), not r["bad"]), ("board.in_project 不带 cwd 参数(%s)" % r["sig"], "cwd" not in r["sig"])] @case("看板:风格系统(三风格令牌在位 + 组件里零硬编码色)") def t_board_styles(): """🔴 用户 2026-09-30:「画架构图参考 archify 的样式,**右上角加个风格切换(保留当前风格)**」。 两件必须守住: ① **`base`(保留的当前风格)必须在**,且**跟随系统深浅色** —— ⛔ 删了就等于改了用户的原风格。 ② **组件里不许出现硬编码颜色** —— 颜色只许出现在令牌块(`--color-*` 等)里。 ⚠️ 一旦有人在 `.card{}`/SVG 里写死 `#fff`,换风格就会**花**(这是最容易犯的)。 """ p = HERE.parent / "assets" / "board.html" src = p.read_text(encoding="utf-8") need = ['[data-style="archify-dark"]', '[data-style="archify-light"]', ':root:not([data-style]), :root[data-style="base"]', # base 跟随系统深色 'id="stylesw"', 'function applyStyle(', "dsh-board-style"] # 切换件 + 持久化 misc = [n for n in need if n not in src] bad = [] for i, l in enumerate(src.splitlines(), 1): s = l.strip() if s.startswith(("--color-", "--font", "--dot", "--node", "--radius", "--shadow")): continue # 令牌块本身当然有颜色值 if re.search(r"#[0-9a-fA-F]{3,8}\b", l) or re.search(r"rgba?\(\s*\d", l): bad.append("board.html:%d" % i) return [("三风格令牌 + 切换件 + 持久化齐全(缺:%s)" % (misc or "无"), not misc), ("组件里零硬编码颜色(越界:%s)" % (bad[:4] or "无"), not bad)] @case("看板:验收摘要行 ⛔ 不许把『一条判据都没有』说成『无』(同族红线)") def t_board_acc_summary(): """🔴 2026-09-30 跑整体验证时抓到:`board.py` 的命令行摘要写的是 `[k for k,v in acc.items() if v!='pass'] or "无"` —— `acc` 已滤掉 `_` 说明行, 所以**一条有效判据都没有**时列表为空 ⇒ 打印「验收非 pass:**无**」, 读起来就是「全部 pass」。**形状同族**:不崩溃,只是少说一句话。 ⇒ 统一到 `_acc_summary()`:空 ⇒ 显式「判不出来,⛔ 不因此判完成」。 """ os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("bd_selftest", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) s_empty = b._acc_summary({}) s_pass = b._acc_summary({"V1": "pass", "V2": "pass"}) s_open = b._acc_summary({"V1": "pass", "V2": "fail"}) return [("空 ⇒ 显式『判不出来』(%s)" % s_empty[:26], "判不出来" in s_empty and "不因此判完成" in s_empty), ("⛔ 空时不得只说『无』(%s)" % s_empty[:12], s_empty.strip() != "无"), ("全 pass ⇒ 说清条数(%s)" % s_pass, "pass" in s_pass and "2" in s_pass), ("有未过 ⇒ 点名(%s)" % s_open, "V2" in s_open)] @case("看板:『未归类』不许被静默丢掉(同族红线:读到了就要说)") def t_board_orphan_visible(): """🔴 2026-09-30 跑整体验证时在**真实工作区**抓到:类别清单已迁到「任务类别」, 而台账里还躺着**跨工作区时代的旧线名**(各 2 件且都已完成)。 旧版:这些行也当"分工位"进 `labor`,架构图又只画前 4 格 ⇒ **那几格连同里面的件整块消失** (读者只看到 4 个"件 0 · 完成 0")。**形状=不崩溃,只是少说一句话** —— 最难发现的那类。 现在:`kind='legacy'` ⇒ **不算类别**(⛔ 不让工作区名冒充类别),但**折叠成一格画出来** + **图外点名线名与件数** ⇒ 两头都堵住。 """ os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("bd_orphan", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) # ① `_orphan` 只收 legacy 行,且件数**求和**(⛔ 不是只数条数) o0 = b._orphan([{"line": "唤醒机制", "kind": "topic", "total": 3, "done": 3}]) o1 = b._orphan([{"line": "唤醒机制", "kind": "topic", "total": 3, "done": 3}, {"line": "old-ws", "kind": "legacy", "total": 2, "done": 2, "open": 0}, {"line": "old-ws2", "kind": "legacy", "total": 1, "done": 0, "open": 1}]) # ② 结构:看板真的会把它画出来 + 真的会点名(⛔ 只改数据不改渲染 = 没修) H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") need = ["kind==='legacy'", "未归类", "不属任何当前任务类别", "不在当前任务类别清单里", # 图外提示要点名 "旧值 · 不在当前任务类别清单"] # 台账表要标注 miss = [n for n in need if n not in H] return [("全 topic ⇒ 无未归类(n=%d)" % o0["n"], o0["n"] == 0), ("只收 legacy 行(n=%d)" % o1["n"], o1["n"] == 2), ("件数求和(总 %d / 完成 %d / 未完 %d)" % (o1["total"], o1["done"], o1["open"]), (o1["total"], o1["done"], o1["open"]) == (3, 2, 1)), ("看板渲染里四处齐全(缺:%s)" % (miss or "无"), not miss)] @case("看板:**同工作区但没归入本项目**的会话不许静默丢(⛔ 也不许把它算进本项目)") def t_board_unrecognized_visible(): """🔴 2026-10-01 用户报:「**为什么创建了协作会话 但是 看板中没有展示**」。 真事:M5 那根棒被命名成 `[协作]-本机协作-M5 …` —— **第 2 级没带方括号**、而且用的是 `goal.short`(本机协作)而非**任务类别**(`topics`=唤醒机制)。 ⇒ `parse_session_name()` 判 `ok=False`、`_in_project()` 也找不到 `[唤醒机制]` ⇒ 它**整条从看板上消失**(不进任务会话层、也不算"本项目会话")。 **形状=不崩溃,只是少说一句话** —— 正是最难发现的那类(同族红线)。 ⇒ 两头都要堵: ① **说出口**:`_sessions()` 交出 `unrecognized`,看板在图外说明里点名; ② **⛔ 不许改归属**:它们**不进 `mine`**、**不算「本项目会话共 N 个」** —— 归属判据仍是 §2.3 三级(主会话 sid / 标题含 `[<类别>]` / `--declare`), ⚠️ `cwd` **只用来决定"要不要提醒"**,⛔ 绝不用来决定"归不归本项目"。 """ os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("bd_unrec", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") S = (HERE / "board.py").read_text(encoding="utf-8") need = [ # 看板真会画(⛔ 只算不画 = 没修) "sessions_unrecognized", "未归入本项目", "会被漏管", "值取 `goal.json` 的 `topics`(任务类别),⛔ 不是 `short` 简称", ] miss = [n for n in need if n not in H] # ① 结构:快照真带这个键(连同"读不到库"的降级分支也带 ⇒ ⛔ 别只在成功路上加) has_key = S.count('"sessions_unrecognized"') >= 1 and S.count('"unrecognized": unrec') >= 1 # ② ⛔ 它**不许**混进 `mine`:cwd 只出现在告警分支里,且该分支显式 continue guarded = "_ct == _wstail" in S and "⛔ 绝不用它决定" in S # ③ 命名规范那句话必须说对(⛔ 别再说"主题取自 short" —— 那正是本 bug 的源头) C = (HERE / "collabd.py").read_text(encoding="utf-8") spec_ok = ("`[协作]-[<类别>]-<具体>`" in C) and ("主题取自 `goal.json` 的 `short`" not in C) return [("快照真带 `sessions_unrecognized`(含降级分支)", has_key), ("cwd 只用于告警、⛔ 不用于定归属(判据未松)", guarded), ("看板渲染里三处齐全(缺:%s)" % (miss or "无"), not miss), ("派活命名规范已纠偏(⛔ 不再说\"主题取自 short\")", spec_ok)] @case("归属:两处 `in_project` 判据**逐样本对账**(⛔ 漂了就静默漏管)") def t_in_project_parity(): """🔴🔴 2026-10-01 用户报:「**看板没有正常显示协作会话**」。 真因(**扫出来的,不是推理出来的**):两处归属判据原来**都只认带方括号**的写法 (`"[%s]" % 类别 in 标题`),而**实际派活出来的棒名是** `[协作]-机制排查与修复-常驻投递容器` —— **第 2 级没带方括号** ⇒ 判 `False` ⇒ 那条会话(**当时正在 `working`**)落到 `sessions_unrecognized` ⇒ `board.html` 第三层画**空框「暂无协作会话」**。 ⚠️ 更糟的是它和 `_topic_in_title()` **自相矛盾** —— 后者的注释白纸黑字写着 「判据是**子串**(不要求 `[...]` 包裹)…**两种写法都要认**」⇒ **同一个标题两处结论相反**。 ⇒ 本用例把**两台判据喂同一批标题、逐条比布尔**(⛔ 不靠人盯、⛔ 不只查源码字符串), 并同时验**精度没丢**:正文里恰好提到类别名的**无关会话**仍不许被收进来 (靠"角色前缀闸"——`_role_of_title()` 解析不出角色的标题一律不算)。 """ os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("bd_inproj", HERE / "board.py") bd = importlib.util.module_from_spec(spec) spec.loader.exec_module(bd) m = imp() _tops = ["会话协作自检", "机制排查与修复"] sc = {"main_sid": "", "main_sids": [], "topics": _tops, "short": "会话协作自检", "workspace": str(TEST_WS)} # (标题, 期望) —— 期望是**这一版**的口径;⛔ 别把期望写成"跟旧版一样" cases = [ ("[协作]-机制排查与修复-常驻投递容器", True), # ← 本 bug 的正主:二级**无方括号** # 🔴 2026-10-02 两类改造:唤醒/跟进**已退役** ⇒ 角色判空 ⇒ 判据②那条(「角色可解析+ # 标题含类别」)**不再命中** ⇒ 期望由 True 变 **False**。 # ⛔ 这不是"判据坏了",是**退役的正确后果**:那两类不再是活类别。 # ⛔ 顺带消掉了一个自指隐患:若它们还能被判成"本项目的会话",就会被当**主会话候选**。 ("[唤醒]-会话协作自检-脉冲", False), ("[跟进]-会话协作自检-队列上报", False), ("[协作]-[机制排查与修复]-合规写法", True), # 老写法(带方括号)向后兼容 ("主控 · 会话协作自检 · 收尾", True), # 口语式主会话(无方括号) ("接续 · 会话协作自检 · 第 2 棒", True), # 接续棒**带类别** ⇒ 认得出(新约定的目的) ("接续 · 会话机制合并包 · 任务4b", False), # 接续棒**不带类别** ⇒ 认不出(未改) ("[协作]-本机协作-M5 棒", False), # 用 `short`/昵称 ⇒ **仍认不出**(规范照旧) ("复盘:今天在会话协作自检里犯的错", False), # ← **精度闸**:无关会话恰提到类别名 ("下载 open design", False), ("", False), ] rows = [] for t, _ in cases: a = bool(bd.in_project("", t, sc)) b = bool(m._in_project("", t, {"main_sid": "", "main_sids": []}, _tops)) rows.append((t, a, b)) _mismatch = [t for t, a, b in rows if a != b] # ⚠️ 2026-10-01 **我自己写错又改对的一处**:原来写成 # `for (t, _), (_, a, _b) in zip(...)` —— 两个 `_` **互相覆盖**, # 结果拿去跟 `a` 比的是**标题**不是**期望值** ⇒ 全表恒红。 # ⇒ 元教训:**对账类断言别用 `_` 当占位名**(一个函数里两个 `_` 必然屏蔽)。 _wrong = [(t, a, exp) for (t, exp), (_t, a, _b) in zip(cases, rows) if a != exp] H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") S = (HERE / "board.py").read_text(encoding="utf-8") return [ ("两处 `in_project` 逐样本一致(%d 条;不一致:%s)" % (len(cases), _mismatch or "无"), not _mismatch), ("🆕 **二级无方括号也算本项目的会话**(`[协作]-机制排查与修复-…` ⇒ True;" "⛔ 旧判据判 False ⇒ 看板第三层画空框而它正在跑)", bd.in_project("", "[协作]-机制排查与修复-常驻投递容器", sc)), ("🆕 精度未丢:无关会话恰好提到类别名 ⇒ **仍不算**(角色前缀闸)", bd.in_project("", "复盘:今天在会话协作自检里犯的错", sc) is False), ("期望值逐条成立(不符:%s)" % (_wrong or "无"), not _wrong), ("🔴 判据用**角色+类别子串**两条并列(⛔ 别退回「只认方括号」)", "_topic_in_title(ti, _tps) and _role_of_title(ti)" in S), ("🔴 看板的退场说明在渲染里真的存在(⛔ 只改数据不改渲染 = 没修)", "sessions_retired" in H and "已收起" in H), ] @case("会话退场:过期会话**收起但不删除**,且 ⛔ 不吃掉在跑的棒与主会话") def t_session_retire(): """🔴 2026-10-01 用户:「**通过接续会话的时候,如何处理过期的会话,避免越堆越多**」。 **为什么必须有**:接续=会话自己建下一棒,前棒**不会自动消失** —— 它在宿主库里永远是 一条 `completed` 记录、标题同族。实测本工作区已堆 5 条「接续 · 会话机制合并…」⇒ 看板/状态稿每轮都为旧棒占位,真在跑的棒被埋在里面。 ⇒ 两条退场路径:① **显式** `--retire self`(接续时由前棒自己登记,准确、即时) ② **窗口兜底**(`completed` 且超 `session_live_min` 分钟 ⇒ 前棒中途死掉也收得掉) ⛔ 两条闸门**都绕开 `working`**(在跑的一定要摊)+ **主会话是版面锚点**(⛔ 不抽)。 ⛔ **收起 ≠ 删除**(宿主库原样在、`retired.json` 全留痕)⇒ 必须**报数**,⛔ 不许静默。 """ os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("bd_retire", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) m = imp() now = time.time() def _ms(minutes_ago): return int((now - minutes_ago * 60) * 1000) def _row(sid, title, status, minutes_ago): return {"id": sid, "title": title, "custom_title": title, "status": status, "cwd": str(TEST_WS), "updated_at": _ms(minutes_ago), "last_activity_at": _ms(minutes_ago)} _MAIN, _RUN, _NEW, _OLD, _RUNRET, _RET = ("aaaa0001", "bbbb0002", "cccc0003", "dddd0004", "eeee0005", "ffff0006") rows = [ _row(_MAIN, "主控 · 会话协作自检 · 主会话", "completed", 300), # 主会话:再旧也不许抽掉 _row(_RUN, "[协作]-会话协作自检-在跑的棒", "working", 300), # 在跑:再旧也不许收起 _row(_NEW, "[协作]-会话协作自检-刚完的", "completed", 10), _row(_OLD, "[协作]-会话协作自检-很旧的", "completed", 300), # 窗口兜底收 _row(_RUNRET, "[协作]-会话协作自检-已退场但仍在跑", "working", 5), _row(_RET, "[协作]-会话协作自检-已退场", "completed", 5), # 显式退场收 ] sc = {"main_sid": _MAIN, "main_sids": [_MAIN], "topics": ["会话协作自检"], "short": "会话协作自检", "workspace": str(TEST_WS)} # ① 显式退场表(写进**测试区**的 inbox,⛔ 不碰生产) (TIN / "retired.json").unlink(missing_ok=True) rc1, out1 = 0, "" _env = _mk_env() _env["CODEBUDDY_SESSION_ID"] = _RET p = subprocess.run([PY, "-u", str(HERE / "collabd.py"), "--retire", "self", "--why", "自测"], cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=_env, timeout=60, errors="replace") rc1, out1 = p.returncode, p.stdout or "" _tbl = json.loads((TIN / "retired.json").read_text(encoding="utf-8")) if (TIN / "retired.json").exists() else {} r1 = b._sessions(limit=30, sc=sc, rows=rows) ids = [x["id8"] for x in (r1.get("mine") or [])] hid = r1.get("hidden") or {} hid_ids = [x["id8"] for x in (hid.get("recent") or [])] # ② 退场表自身也要有过期清理(⛔ 否则这张表自己会越堆越多) pr = m._prune_retired({_RET: {"at": now}, "zzzz9999": {"at": now - 8 * 86400}}) # ③ 短 id 也要能命中(本程序各处只存 8 位) hit8 = m.is_retired(_RET[:8], m._retired()) (TIN / "retired.json").unlink(missing_ok=True) return [ ("`--retire self` rc=0 且落盘(%s)" % (out1.strip().splitlines() or ["(空输出)"])[0][:60], rc1 == 0 and _RET in _tbl), ("退场表按**完整会话 id** 存(供 8 位短 id 前缀比对)", _RET in _tbl), ("🆕 显式退场的那条**被收起**(不在 mine 里)", _RET[:8] not in ids), ("🆕 超窗口(>%d 分钟)的 completed 也**被收起**" % int(b.LIVE_MIN), _OLD[:8] not in ids), ("🔴 **在跑的棒永不收起**(即使它已 300 分钟没动)", _RUN[:8] in ids), ("🔴 **主会话是版面锚点**:再旧也不抽掉", _MAIN[:8] in ids), ("🔴 显式退场 ⛔ 也不吃掉 **working** 的会话(它还在干活)", _RUNRET[:8] in ids), ("窗口内刚完成的留在版面上", _NEW[:8] in ids), ("报数:显式退场 %d / 超窗口 %d(应为 1 / 1)" % (hid.get("retired", 0), hid.get("stale", 0)), (hid.get("retired") == 1 and hid.get("stale") == 1)), ("收起明细点名到会话(%s)" % (hid_ids or "空"), _RET[:8] in hid_ids and _OLD[:8] in hid_ids), ("⛔ 收起 ≠ 删除:宿主库行**一个字没动**(`mine` 里少了、明细里还在)", len(rows) == 6 and len(hid.get("recent") or []) == 2), ("退场表自清理:超 7 天的记录被丢掉(剩 %d 条)" % len(pr), _RET in pr and "zzzz9999" not in pr), ("短 id(8 位前缀)也能命中退场表", hit8), ] @case("看板:任务类别**来源**必须说得出口(declared / fallback)") def t_board_topics_source(): """🔴 2026-10-01 用户订正:「**目标是 通过对话在调用 会话协作skill时说明的,不是固定的**」。 `topics` 缺省回落 `short` 是**兼容**(单类别部署照旧),但看板**不许把"没说明过" 显示成"就是这么定的"** —— 那还是同族红线:**没有却说有**。 ⇒ `_goal_topics_source()` 必须给出三态,且**看板真会把它渲染出来**(⛔ 只算不画=没修)。 """ os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("bd_tsrc", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) s_dec = b._goal_topics_source({"topics": ["甲", "乙"], "topics_declared_at": "2026-10-01T00:10", "topics_declared_by": "对话", "short": "简"}) s_fb = b._goal_topics_source({"short": "简"}) s_no = b._goal_topics_source({}) s_cand = b._goal_topics_source({"_topics候选": {"候选": ["丙"]}, "short": "简"}) H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") need = ["topics_source", "未声明", "调用时在对话里说明"] miss = [n for n in need if n not in H] return [("有 topics ⇒ declared(实得 %s)" % s_dec["kind"], s_dec["kind"] == "declared"), ("…且带说明时间/人(%s / %s)" % (s_dec["at"], s_dec["by"]), bool(s_dec["at"]) and bool(s_dec["by"])), ("只有 short ⇒ fallback(实得 %s)" % s_fb["kind"], s_fb["kind"] == "fallback"), ("都没有 ⇒ none(实得 %s)" % s_no["kind"], s_no["kind"] == "none"), ("候选清单能读出来(%s)" % s_cand["candidates"], s_cand["candidates"] == ["丙"]), ("看板真会渲染来源(缺:%s)" % (miss or "无"), not miss)] @case("看板:多目标 tab(一格一个目标 ⇒ ⛔ 不是各画一份叠着)") def t_board_multi_goal(): """🔴 2026-10-01 用户:「把协作实时看板改为 **tab 支持多个目标**执行协作状态展示」。 守四件(⛔ "能读多个"不算数,必须是**分得开、不串、且向后兼容**): ① `goal_files()` 把 `goal.json` + `goals/*.json` 合成**有序**列表,**活跃的排第一**; ② `build()` 出 `goals[]`,**顶层键=活跃目标那一份**+ `goal` 仍是 `title/short/acceptance` ⇒ ⛔ 老渲染器与老断言**逐字照旧可用**(向后兼容不是口号,是这一条); ③ 台账**按任务类别**切到各目标(⛔ 不串);`line` 不属**任何**目标的件归**活跃目标** 且**照常显示**(同族红线:⛔ 不静默丢); ④ 看板**真画** tab 条(⛔ 只算不画=没修)。 """ os.environ["COLLABD_CONFIG"] = str(CFG) os.environ["DSH_COLLAB_WS"] = str(TEST_WS) import importlib.util spec = importlib.util.spec_from_file_location("bd_mg", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) # ⚠️ 测试工作区里的 goal.json / tasks.json 是**别的用例的夹具** ⇒ 用完**原样还原** _bkp = {} for _n in ("goal.json", "tasks.json"): _p = TIN / _n _bkp[_n] = _p.read_text(encoding="utf-8") if _p.exists() else None _gd = TIN / "goals" _gd_before = _gd.is_dir() try: _gd.mkdir(parents=True, exist_ok=True) (TIN / "goal.json").write_text(json.dumps( {"id": "ga", "short": "甲", "title": "目标甲", "topics": ["甲类"], "acceptance_state": {"A1": "pass", "A2": "fail"}}, ensure_ascii=False), encoding="utf-8") (_gd / "gb.json").write_text(json.dumps( {"id": "gb", "short": "乙", "title": "目标乙", "topics": ["乙类"], "acceptance_state": {"B1": "pass"}}, ensure_ascii=False), encoding="utf-8") put_tasks({"T1": {"state": "done", "line": "甲类"}, "T2": {"state": "running", "line": "甲类"}, "T3": {"state": "done", "line": "乙类"}, "T4": {"state": "done", "line": "旧线名"}}) snap = b.build() finally: try: (_gd / "gb.json").unlink() if not _gd_before: try: _gd.rmdir() except Exception: pass except Exception: pass for _n, _t in _bkp.items(): try: if _t is None: (TIN / _n).unlink() else: (TIN / _n).write_text(_t, encoding="utf-8") except Exception: pass gs = snap.get("goals") or [] by = {str(g.get("key")): g for g in gs} _ga = by.get("ga", {}) or {} _gb = by.get("gb", {}) or {} H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") old_top = {"goal", "project", "tasks", "sessions", "labor", "orphan", "queue", "progress", "others_running", "front", "runtime", "notify", "warn", "meta"} _miss = sorted(old_top - set(snap.keys())) _missjs = [k for k in ("renderTabs", "paintScope") if k not in H] _R = [ ("goal_files 合成 2 项且**活跃的排第一**(实得 %s)" % [g.get("key") for g in gs], len(gs) == 2 and gs[0].get("active") is True and gs[1].get("active") is False), ("顶层键与旧版同形(缺:%s)" % (_miss or "无"), not _miss), ("顶层 goal 仍是 title/short/acceptance(实得 %s)" % sorted((snap.get("goal") or {}).keys()), set((snap.get("goal") or {}).keys()) == {"title", "short", "acceptance"}), ("甲目标分到 2 件 + 1 件无人认领的旧线(共 %d 件)" % len(_ga.get("tasks") or {}), len(_ga.get("tasks") or {}) == 3), ("乙目标只分到自己的 1 件(实得 %d)" % len(_gb.get("tasks") or {}), len(_gb.get("tasks") or {}) == 1), ("⛔ 乙目标看不到甲目标的件(不串)", "T1" not in (_gb.get("tasks") or {})), ("⛔ 甲目标的完成情况里没有乙目标的判据", "B1" not in str(_ga.get("acc_summary"))), ("验收按目标分开算(甲=%s / 乙=%s)" % (_ga.get("acc_summary"), _gb.get("acc_summary")), "A2" in str(_ga.get("acc_summary")) and "pass" in str(_gb.get("acc_summary"))), ("分工板按目标分开(甲 %s / 乙 %s)" % ([x.get("line") for x in (_ga.get("labor") or [])], [x.get("line") for x in (_gb.get("labor") or [])]), (_ga.get("labor") or [{}])[0].get("line") == "甲类" and "乙类" not in [x.get("line") for x in (_ga.get("labor") or [])] and [x.get("line") for x in (_gb.get("labor") or [])] == ["乙类"]), ("看板真会画 tab 条(缺:%s)" % (_missjs or "无"), not _missjs), ("看板把 `goals` 当数据源(⛔ 不写死单份)", "d.goals" in H), # ===== 🔴🔴 2026-10-04 用户定案:tab 最多 6 个 + 最新在跑的排前面 ===== ("tab 有**上限常量**且只有一个(⛔ 两处数字必漂)", H.count("TAB_MAX = 6") == 1), ("超出部分**收进下拉**(`tabMoreMenu` + `tabMoreBtn` 都在)", "tabMoreMenu" in H and "tabMoreBtn" in H), ("下拉项**可直接切换**(复用 `data-gi` ⛔ 不另造一套选中逻辑)", "tabmenu-item" in H and "data-gi=" in H), ("下拉挂在 `.tabwrap`(⛔ `position:relative` 必须在它身上," "⛔ 否则绝对定位会参照 flex 祖先跑到别处)", ".tabwrap{position:relative" in H.replace(" ", "")), ("菜单**不是常开**(`hidden` + 显隐逻辑都有)", "mn.hidden=true" in H and "tabmenu[hidden]" in H), ("🔴 **排序在后端**(前端⛔ 不自己排:两处排序=改一处忘另一处必打架,同 P0-40)", #⚠️ 只量`renderTabs` **函数体**,⛔ 不量全文件(本文件别处有 3 处正当排序: # L532 判据键、L1040 检查会话按 age_min、L1621 台账键—— 全局判 `sort(` 会出现假红)。 "sort(" not in H.split("function renderTabs")[1][:4000]), ("前端**读后端排好的顺序**(⛔ 直接 `gs.map` 不重排)", "shown=more?gs.slice(0,TAB_MAX):gs" in H), ] # ===== 🔴🔴 2026-10-04 用户定案:「**tab 的宽度改为一样宽**」===== # ⚠️⚠️ **必须先剥 CSS 注释再量规则块** —— `.tab` 上面那段说明注释里**正当**地 # 引用了旧写法 `min-width:180px`(那是"为什么改"的证据,⛔ 不许删)。 # ⇒ 直接全文件查 `min-width:180px` 会**永远命中** ⇒ 判据假绿(实测会栽在这)。 # ⚠️⚠️ **必须只量 `.tab{...}` 这一条规则块**,⛔ 不量全文件(本文件别处有 # `.tabmenu` / `.tabmore-btn` / `.tabs` 等正当规则)。正则是 `\.tab\{[^}]*\}` —— # `.tab .tt{` / `.tabmenu{` / `.tabmore-btn{` 都**不**匹配(`{` 紧跟 `.tab`)。 _css_nc = re.sub(r"/\*.*?\*/", "", H, flags=re.S) # 剥注释 _mtab = re.search(r"\.tab\{[^}]*\}", _css_nc) _tabcss = _mtab.group(0) if _mtab else "" # 等宽的**唯一成因**=`flex` 的 grow≥1 且 **basis=0**(basis 按内容分配 ⇒ 永不等宽)。 # ⚠️ 解析式判据而不是查子串:`flex:1 1 0` ✅ / `flex:1`(默认 1 1 auto)❌ / # `flex:1 1 auto` ❌ / `flex:0 1 auto`(不生长)❌ —— 四种写法子串法分不开。 _mf = re.search(r"flex:\s*(\d+)\s+(\d+)\s+([0-9a-z%]+)", _tabcss) _R.append(( "tab **等宽**(`.tab` 的 flex 须 grow≥1 且 **basis=0**;实得:%s)" % (_mf.group(0) if _mf else "⛔没找到 flex 声明"), bool(_mf) and int(_mf.group(1)) >= 1 and _mf.group(3) == "0")) # 🔴 配套:`min-width:0` 缺了 ⇒ flex 等分会被子元素 min-content 顶开 ⇒ **看着不等宽** # (标题长的那格把邻居挤窄)。这是"等宽"的**另一半**,⛔ 不能只判 flex。 _R.append(("tab 等宽的配套 `min-width:0` 在(⛔ 缺了会被长标题顶开):%s" % ("有" if "min-width:0" in _tabcss.replace(" ", "") else "⛔无"), "min-width:0" in _tabcss.replace(" ", ""))) # 🔴 抓「改回内容自适应」:`flex:1 1 0` 之外又留了按内容/固定宽的声明 ⇒ 后者会赢。 # 🔴🔴 `width:` **必须带负向后顾** —— ⛔ 否则 `max-width:420px` 里那个 `width:420` 会被命中 # ⇒ 这条判据**恒红** ⇒ 任何变异都"报红" ⇒ **变异验证整体作废**(判据恒红 = 没判据)。 # CSS 属性名里 `-` 是合法字符(`max-width`/`min-width`)⇒ 用 `(? 1 and _tis == list(range(len(_gs8))))) except Exception as _e8: _R.append(("真起board.py 验排序(⛔ 起不来即报红):%s" % type(_e8).__name__, False)) return _R @case("动手层:三条开工动作在位 + **判据不许恒红**(⛔ 恒红 = 没有判据)") def t_howto_layer(): """🔴🔴 2026-10-04 用户:「**这些问题如何沉淀在技能中,让会话执行更靠谱**」。 病根诊断(同一天四次翻车,同一个形状): `pitfalls.md` 47 条 / 141 KB 是**知识层** ⇒ 会话**读得完但不会自动执行**; 缺的是**动手层**(开工前必过的三条动作)⇒ �� `references/00-动手前必过.md`。 守三件(**每件都已落判据,不许退化**): ① 动手层**在位 + 挂进 SKILL.md 加载段第一位**(⛔ 不挂=又一份没人读的文档); ② 动手层里那三条**各带已知落点常量**(省得下一个人再去 grep 源码); ③ 🔴 **判据不许恒红** —— 基线必须全绿(数 ✗ 且排除「报告型」行)。 恒红比漏网更坏:它让**所有变异都"红"** ⇒ 验证作废,而**表面看起来完美**。 """ HOW = HERE.parent / "references" / "00-动手前必过.md" exists = HOW.exists() T = HOW.read_text(encoding="utf-8", errors="replace") if exists else "" SK = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace") P0 = (HERE.parent / "references" / "pitfalls.md").read_text(encoding="utf-8", errors="replace") # ① 在位 + 挂第一位 _R = [ ("动手层 `00-动手前必过.md` 在位(%d 字节)" % len(T.encode("utf-8")), exists), ("挂进 SKILL.md 加载段(⛔ 不挂 = 又一份没人读的文档)", "references/00-动手前必过.md" in SK), ("排在加载段**第一位**(⛔ 动手层要最先被读到,⛔ 不许排在后面)", SK.find("00-动手前必过.md") < SK.find("references/architecture.md")), ] # ② 三条动作各在位 + 各带落点常量 _three = [("查一·问程序自己", "supervise_alive"), ("查二·先看时间戳", "_更新"), ("查三·先让基线全绿", "恒红")] for _nm, _k in _three: _R.append(("动作在位且带实证:**%s**(锚点 `%s`)" % (_nm, _k), _k in T)) _R.append(("动手层给了**已知落点常量表**(⛔ 不给 ⇒ 下一个人又去 grep 源码):%d 条" % T.count("| 东西 |"), T.count("| 东西 |") >= 1 and "SUP_HB" in T and "guard.stop" in T)) # 🔴🔴③ 「判据不许恒红」—— ⛔ **原来这条是"扫自己的源码找裸 True"**, # 实测**把自己的扫描代码当成了被扫对象**(`"== 0) or" in ln` 命中它自己那行)⇒ 恒红。 # ⇒ 与 P0-48 同策:**扫文本的判据一律不成立**,改成**行为级**—— # 真把 `00-动手前必过.md` 挪走,看这条判据会不会报红(它必须红)。 # ⚠️ 这里只判"文档在不在位"(那是可判的);**"判据会不会红"由变异脚本验**,不在这儿自证。 # ⚠️ 锚点**跟着内容走**:动手层精简过一次(4849→2582 字节)、后又加过两条(④⑤), # 锚点串必须同步改,⛔ 否则判据会恒红(已栽过一次,见 P0-48)。 _anti = [k for k in ("⛔ **不许用", "一律现取", "恒红比漏网更坏", "单条上限", "关键词对账") if k in T] _R.append(("动手层五条动作**都带 ⛔ 反例/上限**" "(⛔ 只写「要做」不写「⛔ 不许」= 会被照着做错):%d/5" % len(_anti), len(_anti) == 5)) # 📌 动手层与 pitfalls 的分工不许反(知识层会被读完,动手层是动作) _R.append(("动手层明说「⛔ 别开工前通读 pitfalls」" "(⛔ 知识层当动作层用 = 141 KB 没人读)", "别开工前通读" in T or "⛔ 别开工前" in T)) _R.append(("三条都在 pitfalls 里有对应条目(P0-47/48/49 = 知识层)", all(("P0-%d" % i) in P0 for i in (47, 48, 49)))) return _R @case("判据元体检工具在位 + 它的**元规则**也不是摆设(⛔ 防「靠记得」)") def t_judge_audit_tool(): """🔴 2026-10-04 收口:写了一天纪律(`references/00-动手前必过.md`)**当天照样栽**。 ⇒ 根因收敛成两条,**都不是「不小心」**: A. 判据与被检对象的「连接」没被验证(判据自重算/锚文本/夹具没覆盖真分支) B. 验证动作本身出错而我不检查它(脚本自己崩了却照样打 PASS/只重置一份文件) ⇒ 解法不是「更小心」,是**把纪律变成机器强制**:⛔ 不跑这个工具,你自己看不见。 """ mp = HERE / "judge_audit.py" if not mp.exists(): return [("⛔ 包内缺 `scripts/judge_audit.py` ⇒ 判据恒绿/假绿没人拦", False)] import importlib.util as _iu sp = _iu.spec_from_file_location("ja_probe", mp) ja = _iu.module_from_spec(sp) try: sp.loader.exec_module(ja) except Exception as e: return [("⛔ `judge_audit.py` 读不出来(%r)" % (e,), False)] src = mp.read_text(encoding="utf-8", errors="replace") R = [] R.append(("**元规则表非空**(⛔ 空表 = 体温计没装)", len(getattr(ja, "RULES", [])) >= 5)) R.append(("每条规则都带**「今天栽在哪」**(⛔ 没写原因的规则会变成新纪律)", all(("今天" in w) for _n, _k, _p, w in ja.RULES))) R.append(("每条规则都有 **kind**(weak/self/weak_ev ⇒ 严重度可分级)", all(k in ("weak", "self", "weak_ev") for _n, k, _p, _w in ja.RULES))) # —— B 类根因的工具抓手(今天栽了三次的就是这一块) R.append(("**变异未生效 ⇒ rc=2 且明说**(⛔ 今天脚本自己崩了却照样打 PASS)", re.search(r"变异未生效[\s\S]{0,120}?return 2", src) is not None)) R.append(("**等价变异被识破**(⛔ 只动写法不动语义的变异不算数)", # 🔴 逐字锚在**工具自己的措辞**上(今天栽过:锚 `mutated == text` 那个条件, # 而工具里那句是「植入后文本**没变** ⇒ 等价变异」⇒ 结构不同 ⇒ 假红) re.search(r"植入后文本\*\*没变\*\*[^\n]*?等价变异[\s\S]{0,200}?return 2", src) is not None)) R.append(("**还原后核对 md5**(⛔ 今天多轮变异互相污染)", re.search(r"还原后 md5 \*\*不一致\*\*[\s\S]{0,120}?return 2", src) is not None)) R.append(("**`finally` 里还原**(⛔ 异常路径也必须还原)", re.search(r"finally:[\s\S]{0,120}?write_bytes\(orig\)", src) is not None)) R.append(("🔴 **变异在原地做**(⛔ 拷到临时目录 = 判据读的还是原文件," "变异压根没进被检对象 —— 今天第三次栽在这)", "shutil.copy2" not in src and "原地" in src)) R.append(("**误报白名单存在且要求写清原因**(⛔ 元规则本身也会假红)", hasattr(ja, "FALSE_POSITIVE") and "FALSE_POSITIVE" in src)) # —— 真跑:拿一个**已知的坏写法**去扫,必须被抓到(否则规则是死的) probe = TEST_WS / "judge_probe.py" probe.write_text( "x = (lambda s: s.lower())\n" "if 'automations' in src:\n" " pass\n", encoding="utf-8", newline="\n") try: n = ja.audit(probe) R.append(("拿**已知坏写法**去扫 ⇒ 命中 ≥2 类规则" "(⛔ 扫不出 = 规则本身是死的)", n >= 2)) finally: try: probe.unlink() except OSError: pass return R @case("红线 S:⛔ 严禁把「被监控对象所在工作区」当成「目标归属工作区」+ 归属核对进 env_check") def t_ws_attribution(): """🔴🔴 2026-10-04 用户当场纠正(当轮原话): 「**本工作区下面的目标 为什么执行会话要创建到 vibe-product 工作区下面**」。 病根:用户在本工作区(`ai1net-dsh-server`)下达「监控 `vibe-product`」的目标, 我把排期 `cwds` 写成 `vibe-product` ⇒ 会话 `c88a157c` 落在那个区 ⇒ **本工作区看板/台账里直接看不见它**,而本区恰恰是问题最集中、最需要它的地方。 ⛔ 当时**没有任何机制**提醒过 ⇒ 光写文档没用,必须落成**可执行判据**。 """ SK = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace") R = [] R.append(("SKILL.md 有 **S 红线**这一条", "S 红线" in SK)) R.append(("红线上写清**归属看的是「目标从哪个区下达」**(⛔ 不是目标文本里出现的区名)", "目标在哪个工作区下达" in SK and "对象" in SK)) # 🔴 逐字抄文档原句(⛔ 判据锚点必须逐字,早先凭记忆写「正解=」⇒ 基线直接红) R.append(("三条禁止都在位(不许把对象区写进 `cwds`/不许按目标文本里的区名定/交付落归属区)", "不许把「要去看/要改的那个区」写成 `cwds`" in SK and "不许用「目标文本里出现了某区名」来定 `cwds`" in SK and "一律落在**归属区**" in SK)) R.append(("写明**唯一例外**(用户当轮明确要求时才建在别的区)", "唯一例外" in SK and "当轮" in SK)) R.append(("写明例外的机械背景(`automations.cwds` ⛔ 不落 `sessions.cwd`)", "不落" in SK and "sessions.cwd" in SK)) # —— 判据侧:真去查 `env_check` 有没有这一段 # 🔴 `imp()` 硬编码只加载 `collabd.py`(⛔ 不接参数)⇒ 这里**自己**按路径加载。 import importlib.util as _iu _sp = _iu.spec_from_file_location("_env_probe_att", HERE / "hooks" / "_env.py") ev = _iu.module_from_spec(_sp) try: _sp.loader.exec_module(ev) except Exception as e: return R + [("⛔ `_env.py` 读不出来(%r)⇒ 归属判据无法验" % (e,), False)] src = (HERE / "hooks" / "_env.py").read_text(encoding="utf-8", errors="replace") # 🔴🔴 这三条原先是「**锚在文本出现过**」的弱判据(今天第 7 次同形状,已被元体检标出) # ⇒ 改成**行为判据**:真跑 `env_check`,看返回体里**有没有那个键、值对不对**。 # ⛔ 弱锚点的危险:把 `attribution` 从源码里删掉但留个同名注释,它照样绿。 _rep = {} try: _rep = ev.env_check("E:/ProgramData/__selftest_no_such_ws__") except Exception as _e: R.append(("⛔ `env_check` 真跑失败(%r)⇒ 后三条判据无法验" % (_e,), False)) _att = (_rep.get("attribution") or {}) if isinstance(_rep, dict) else {} R.append(("`_env.py` 的 `env_check` 真跑后**返回体含 `attribution` 键**" "(⛔ 行为判据,⛔ 不是「源码里出现过 attribution」)", isinstance(_rep, dict) and "attribution" in _rep)) R.append(("`attribution` 里带 `declared` 清单(⛔ 空 dict = 键在但没内容,等于没有)", isinstance(_att, dict) and "declared" in _att)) R.append(("核对读的是 **`automations` 表**(⛔ 不是 `sessions.cwd` —— 后台会话 cwd 独立):" "⛔ 用**真读数**验(`declared` 非空 ⇒ 查的是真库)", bool(_att.get("declared")))) R.append(("**有工作区路径归一化**(⛔ 同一区出现过反斜杠/正斜杠两种写法 ⇒ 逐字比会误判)", hasattr(ev, "_norm_ws"))) # 🔴🔴 真行为判据,⛔ **不许**自己重算一遍(今天栽过): # 早先写成 `(lambda s: ...)(...)` ⇒ 跟被检的 `_norm_ws` **毫无关系** # ⇒ 把它改坏(去掉 lower)判据照样全绿(实测 V4 没抓到)。 # ⇒ 现在**直接调被检函数**,且喂**实测踩到过的三种形态**。 try: _n = ev._norm_ws R.append(("归一化:反斜杠/正斜杠/尾斜杠/大小写 **四种写法归到同一个值**" "(⛔ 同一区实测出现过 `E:\\…` 与 `E:/…` 两种)", len({_n("E:\\P\\ws"), _n("E:/P/ws"), _n("E:/P/ws/"), _n("e:/p/ws")}) == 1)) R.append(("归一化**不误并不同区**(⛔ 别把 `ws` 与 `ws2` 并成一个)", _n("E:/P/ws") != _n("E:/P/ws2"))) except Exception as e: R.append(("`_norm_ws` 真调失败:%r" % (e,), False)) # 🔴 行为判据:拿一个**确定没有线**的工作区跑真体检 ⇒ 必须报出归属告警 # (复用上面已跑过的 `_rep`,⛔ 不重复跑 —— 同一份读数取两遍只会在两处漂) _a = _att if isinstance(_att, dict) else {} R.append(("对「无线的工作区」跑真体检 ⇒ **报出归属告警**" "(⛔ 不报 = 形同虚设)", any("协作线" in w for w in (_a.get("peer_missing") or [])))) R.append(("该告警进了 `warn`(⛔ 不进 warn = 没人会看到)", any("协作线" in w for w in ((_rep.get("warn") or []) if isinstance(_rep, dict) else [])))) return R @case("红线:⛔ 严禁用「排期/自动任务」当常驻载体 + 环境检查必须查工作区部署") def t_no_schedule_as_supervisor(): """🔴🔴 2026-10-04 用户定案(当场立红线): > 「**使用技能时就要检查环境、做相关配置**(⚠️ **说了无数遍但没实现**)」 > 「**如果是为了替代常驻目标检查程序 也是技能的问题,需要红线禁止这样的操作**」 **取证**(`vibe-product` 工作区): · 排期 `[执行]-界面交互-常驻续命`(`id=e181b51b`)= **`FREQ=HOURLY;INTERVAL=1`** · 它的 prompt 第 3 步明写「不在 ⇒ **后台任务**起一条 `collabd.py --supervise`」 · **15:23 真跑过一次**,会话结论「✅ 常驻存活,本轮未做续命、未建任何排期」 · ⇒ **每小时唤醒一个新会话,只为看一眼常驻活没活**;而常驻本来就活(连跑 17.7 h) ⚠️ **它违背本技能自己写的** `supervise-persistence.md`「⛔ **排期代替不了载体**」 —— 文档早写对了,⛔ **没有判据守住** ⇒ 照样被建出来。**这就是本用例要补的洞。** 守两件: ① **红线在位**(SKILL.md 的 R 条 + 四条禁止 + 「换载体不是续命」这层意思) ② 🔴 **`env_check` 真的查工作区部署** —— 原来它只查「技能根能不能定位 + 钩子注册」, ⇒ **「复制了技能但工作区没部署」永远查不出来**(实测 `vibe-product` 旧版读数全绿)。 """ import re as _re2 SK = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace") PP = (HERE.parent / "references" / "supervise-persistence.md").read_text( encoding="utf-8", errors="replace") env_src = (HERE / "hooks" / "_env.py").read_text(encoding="utf-8", errors="replace") # 🔴 `imp()` 硬编码只加载 `collabd.py`(不接参数)⇒ 这里**自己**按路径加载 `_env.py`。 # (实测栽过:`imp("hooks/_env.py")` ⇒ TypeError: imp() takes 0 positional arguments) import importlib.util as _iu _sp = _iu.spec_from_file_location("_env_probe", HERE / "hooks" / "_env.py") m = _iu.module_from_spec(_sp) _sp.loader.exec_module(m) # ① 红线在位 _i = SK.find("严禁用「排期/自动任务」当常驻") _red = SK[_i:_i + 2600] if _i > 0 else "" _R = [ ("SKILL.md 有 R 红线「⛔ 严禁用排期当常驻载体」(位置在 §1)", _i > 0), ("红线点名了那个反面案例(hourly 排期 id 或名字)", ("e181b51b" in _red) or ("常驻续命" in _red)), # 🔴 锚点**逐字抄文档原文**(⛔ 不能照 imagined 写:上一版写「正解=」/ # 「4. ⛔ 不许」,文档实际是「正确处置=」/「4. ⛔ **常驻真死时不许…」」 # ⇒ 基线直接红,这类错自己栽过两回)。 # 🔴🔴 2026-10-04 口径订正(用户:「从来没说过什么开机自启,只有调用技能 # 完成目标时启动 后台任务和检查程序」)⇒ 判据**跟着口径改**,⛔ 不留旧措辞: # 原来锚「正确处置=**换载体**(计划任务 + 守护循环)」—— 那把 # 「计划任务」写成了唯一正解,⛔ 与用户口径冲突。 # ⇒ 现在锚两件真正要保住的事:①⛔ 不是拿排期续命 ②正解=按需重起。 ("红线写明正解=**按需重起**(⛔ 不是拿排期续命;⛔ 计划任务只是可选,⛔ 不是唯一正解)", ("⛔ **不是**建一条排期去续它的命" in _red) # 🔴 逐字:文档是 `**正确处置=按需重起**`(⭐ 包的是**整句**), # ⛔ 不是 `正确处置=**按需重起**`(包的是后半截)—— 差一对星号就恒红, # 这已是今天第 11 次「判据锚点没逐字抄原文」。 and ("**正确处置=按需重起**" in _red) and ("**不是需求、不是欠项**" in _red)), ("红线**四条禁止**齐全(⛔ 数不足 = 没写全;⛔ 锚点逐字抄,⛔ 不用「不许」计数——" "那两个字在别处也出现)", all(k in _red for k in ( "1. ⛔ **不许建 `recurring` 排期去「续命/保活/巡检常驻」**", "2. ⛔ **不许把排期当", "3. ⛔ **不许在排期 prompt 里写", "4. ⛔ **常驻真死时不许用排期兜底**"))), ("技能自己的文档**早写对了**「⛔ 排期代替不了载体」(判据据此钉住)", "排期代替不了载体" in PP or "排期代替不了" in PP), ] # 🔴 ② env_check 必须查工作区部署(⛔ 原来这一段完全不存在) _ec = m.env_check(ws=str(HERE.parent.parent.parent), scope="workspace") _dp = _ec.get("deploy") or {} _R += [ ("`env_check` 返回 **deploy 段**(原来没有)", isinstance(_dp, dict) and "deployed" in _dp), ("deploy 段查得到**部署配置 + 目标文件**两项", bool(_dp.get("config")) and bool(_dp.get("goal"))), ("未部署时 `deployed=False` 且**说得出缺什么**(⛔ 只给 False 等于没说)", (_dp.get("deployed") is not False) or bool(_dp.get("missing"))), ("「没部署」归 **warn 而非 bad**(⛔ 只装技能副本是合法状态,不该一律体检红)", "warn" in _ec and not _ec.get("bad")), ("源码级:**读配置查现行键**在(⛔ 否则旧格式永远查不出)", "stale" in env_src and "supervise_interval" in env_src), ] # ③ 🔴 变异体:**把部署检查整段删掉** ⇒ 上面那条必须报红 # (⛔ 这条是本用例的价值所在:不验它,"说了无数遍"就还是没实现) _keep = m.env_check try: m.env_check = lambda ws="", scope="global": { "scope": scope, "bad": [], "warn": [], "ok": True, "hooks": [], "config_dir": "", "skills_root": ""} # = 旧版(无 deploy 段) _no = m.env_check(ws=str(HERE.parent.parent.parent)) _R.append(("🔴 **变异体**(去掉 deploy 段)⇒ 「返回 deploy 段」判红(证明这条真在判)", "deploy" not in _no)) finally: m.env_check = _keep return _R @case("沉淀纪律:**经验不许写成流水账**(体量有上限 + 判据要点不许丢)") def t_pitfall_size(): """🔴 2026-10-04 用户:「**沉淀的经验不要记成流水账,要简明扼要有效**」(**这条本身也是经验**)。 病根:`pitfalls.md` 47 条共 **140 KB**,单条最长 **11 KB**; 而**同一天我新写的 P0-48/49 各 3 KB** —— 比 P0-41/22/23(真复杂)还长 ⇒ **在拿流水账充数**。 ⇒ 已当场精简 5113 → 3001 B(-41%,判据要点 11/11 保留)。 守两件(**量体量,不量"有没有写"** —— ⛔ "有没有写"是新条永远能过,等于没判): ① **单条体量上限**(⛔ 超了就是流水账:论证过程、重复表格、展开说明都算); ② **精简不许丢判据** —— 用关键词对账,⛔ 长度达标但关键判据被删 = 更坏(假绿)。 """ import re as _re P = (HERE.parent / "references" / "pitfalls.md").read_text(encoding="utf-8") _parts = _re.split(r"(?m)^(## P[01]-\d+ .*)$", P) _rows = [] for _i in range(1, len(_parts), 2): _rows.append((_parts[_i].split()[1], len(_parts[_i + 1].encode("utf-8")), _parts[_i + 1])) # 🔴🔴 **判据第一次写错了(2026-10-04 自查发现)**:只按体量判"流水账" ⇒ 误伤三条 # **证据最密**的实战记录。逐段读完的结论: # · P0-19(6.7KB) 全是实测数字/行号/反例/残留问题 ⇒ **长,不是水** # · P0-20(8.6KB) 带逐字错误原文 + 对照组 + 订正块 ⇒ **长,不是水** # · P0-2(11.6KB) 真因只 2.4 KB,⛔ **79% 是内嵌的「P0/P1/P2 速查清单」** # ⇒ 真病根是「**两个主题塞进一条**」,已拆成 `references/99-速查清单.md` # (⛔ 内容逐字保留 73/73 行,只搬家)⇒ P0-2 降到 2.7 KB。 # ⇒ 判据改成**双判**:超限 **且** 实证信号密度低 ⇒ 才是流水账。 # ⚠️ 信号词表**也踩过一次**(第一版只 10 个词 ⇒ 把 P0-19 判成 2.8 密度)⇒ # 词表已按真读结果扩到 18 个(补"反例/恒假/残留/证毕/方向相反"等)。 _EV = ("实证", "实测", "反例", "恒假", "恒真", "残留", "逐字", "原文", "对照组", "作废", "证毕", "反证", "不构成", "只可能", "方向相反", "改前", "原文", "真因") _over, _excuse = [], [] for _n, _b, _body in _rows: if _b <= 6000: continue _d = sum(_body.count(k) for k in _EV) * 100.0 / max(_b, 1) * 30 # 每 KB 信号数 (_excuse if _d >= 4.0 else _over).append((_n, _b, round(_d, 1))) # 🔴 锚点必须**跨编号取到目标那几条**(P0-48 同型:锚在"文本出现过"上必出假红)。 # 两轮栽过:① 量「P0-48 → **文件尾**」⇒ 后来加 P0-50 就把它算进去、凭空报红; # ② 只取**单条** ⇒ P0-49 是独立编号,被切在段外 ⇒ 要点只对到 3/8。 # ✅ 正解=**从 P0-48 起、连到 P0-50 之前**(含 P0-48 与 P0-49 两条),⛔ 不含再往后新加的。 def _seg(a, b=None): i = P.find("## " + a) if i < 0: return "" j = P.find("## " + b, i + 5) if b else -1 return P[i:j] if j > i else P[i:] _i48 = P.find("## P0-48") _tail = _seg("P0-48", "P0-50") if _i48 >= 0 else "" MUST = ["re.sub(r\"/\\*.*?\\*/\"", "basis==\"0\"", "min-width:0", "_更新", "supervise_alive()", "不穿透", "wake_enable=false", "用户拍板"] _miss = [k for k in MUST if k not in _tail] _CK = (HERE.parent / "references" / "99-速查清单.md") return [ ("无「超长**且**证据密度低」的流水账(>6 KB 共 %d 条,其中密度<4.0 的:%s)" % (len(_over) + len(_excuse), _over or "无"), not _over), ("超长但**证据密度高**的已放行(⛔ 不许误伤存档证据):%s" % (["%s %dB 密度%.1f" % x for x in _excuse] or "无"), all(x[2] >= 4.0 for x in _excuse)), # ⚠️ 上限 3900 的来历(⛔ 不许拍脑袋):精简前 5113 B,修掉 4 处**理解偏差**后 # 合理体量约 3.6–3.8 KB ⇒ 取 3900 留余量。**修偏差必然会长字**(把"三种按内容分配" # 拆成"按内容/不生长"就是把省略的信息写回去)⇒ ⛔ 别为了压过上限把偏差删回去。 ("P0-48+49 已精简(%d B,上限 3900,⛔ 精简前 5113;修偏差会长字,⛔ 别为压上限删偏差)" % len(_tail.encode("utf-8")), 0 < len(_tail.encode("utf-8")) < 3900), ("精简后**判据要点一个没丢**(%d/%d,缺:%s)" % (len(MUST) - len(_miss), len(MUST), _miss or "无"), not _miss), # 拆出去的那份**不许被删/漂移**(⛔ 它是 P0-2 的一部分,⛔ 不许当"重复内容"删掉) ("速查清单 `99-速查清单.md` 在位(%d B,⛔ 内容是 P0-2 拆出的,⛔ 不许当重复删)" % (len(_CK.read_bytes()) if _CK.exists() else 0), _CK.exists() and "## P0 ⛔" in _CK.read_text(encoding="utf-8") and "99-速查清单.md" in P), ] @case("看板:版面纪律(小字描述只进 ?;指定删除的三处不得回来)") def t_board_layout(): """🔴 用户 2026-09-30:「把各板块的**小字描述**放到各板块对应**右上角 ?号图标**中,鼠标移上去显示 (**只保留标题,主体,类别标签**这类信息)」+ 点名**删除**三处。 守两件: ① 每个 `.card/.hero` 的说明**只许放在 `.qtip` 里**(至少 4 个 `?`),⛔ 不许摊回版面; ② 被点名删除的三句**不得复活**(否则又变成"版面上一堆解释性小字")。 """ src = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") qh = src.count('class="qh"') qtip = src.count('class="qtip"') + src.count('class="qtip" id=') DELETED = ["一张看板只管", "只读旁路观测", "不按工作目录", "其他工作区另有在跑的会话", "不参与、也不拖慢常驻程序", # 🆕 2026-10-01 用户:「**下面这段话是干啥的,为什么也在 tab 区域**」 # ⇒ tab 条上⛔ 不放说明文字(这段"只有一格/怎么加第二格"已搬进「本项目」的 `?`) "这个工作区现在只登记了", "并行推进"] back = [k for k in DELETED if k in src] return [("右上角 ? 图标齐全且都带说明(?=%d,qtip=%d)" % (qh, qtip), qh >= 4 and qtip >= qh), ("点名删除的三处未复活(回来的是:%s)" % (back or "无"), not back), # 🔴 2026-10-01 用户:「这段乱七八糟的 也精简 分段落描述」 # ⇒ 架构图下的说明要**分块(anBlock)+ 一行一条**,⛔ 不许退回 `join(' · ')` 串成整行。 ("图外说明分块(anBlock 在位、⛔ 不是串成一整行)", "anBlock" in src and "extra.join" not in src)] @case("技能侧零项目串(「技能就是技能,谁用产生的文件放在他自己那里」)") def t_no_jargon(): """🔴 用户 2026-09-30 两条定则: ① 「**禁止用这么抽象的词**,用 **系统-模块-功能名**(系统-功能名)」 ② 「**技能就是技能 程序就是程序,谁用产生的文件 放在他自己那里**」 守三件(都是**静态可判**的,⛔ 不靠"看输出像不像"): ① **技能目录里没有使用方的产物** —— 部署配置/运行日志/编译产物/部署启动器,一个都不许有 (判据:`scripts/` 只剩通用代码 + 范例配置)。 ② **`board.py` 源码里零项目串** —— 端口、工作区路径、那些真名,⛔ 一个都不许硬编码 (它们属于使用方的 `board_ext.py`)。**这条直接对应用户的定则,是本用例的主判据。** ③ 看板**回读不引用已废弃的项目键**(`front.shim` / `front.worker` / `device_ingress` / `overlay_node`)。 """ SK = HERE.parent / "scripts" # ① 技能目录里不许有使用方的**结构性**产物(配置/启动器/编译产物)。 # ⚠️ 日志**不作硬失败**:迁移期可能还有"迁移前起的旧进程"在往老路径写 # (2026-09-30 实测 `wb-supervisor-watch.py --interval 10` 就是这种), # 那种只能等它自己结束;判据落在**源码不变量**上(见下一条)而不是文件存不存在。 # ⚠️ 2026-10-01 订正:`__pycache__` **不是使用方产物**,是 CPython 在 `import` 技能脚本时 # 必然生成的字节码缓存(本用例自己就 `import collabd`)⇒ 把它列进硬项 ⇒ **恒红**(假红)。 # ⇒ 移出硬项,改由本用例**自己清理**一次(见末尾),真正的判据仍是"使用方的东西不许进来"。 forbidden = ["collabd.config.json", "start-guard.cmd"] leak = [n for n in forbidden if (SK / n).exists()] # ①b 源码级不变量:日志**不得**再挂在技能目录上(`LOG = HERE / …` 是旧写法) logbad = [] for fn in ("collabd.py", "board.py", "guard.py"): t = (SK / fn).read_text(encoding="utf-8") for i, line in enumerate(t.splitlines(), 1): s = line.strip() if s.startswith("#"): continue if re.search(r"LOG\s*=\s*HERE", s) or re.search(r"\bHERE\s*/\s*[\"']_", s): logbad.append("%s:%d" % (fn, i)) # ② 源码零项目串 —— 🔴 2026-10-01 **由「只扫 `board.py`」扩到「也扫 `collabd.py`」**。 # ⚠️ 为什么非扩不可:原来只扫 `board.py` ⇒ `collabd.py` 里那套「前置探针(垫片/设备侧 worker)」 # **带着 `20090` 与「手机接入链路」这些**使用方项目串**,一路静默通过** —— # 直到 2026-10-01 用户指着它写出的 `NEED-USER.md` 说「**删除这个事情**」。 # ⇒ **判据漏掉一个文件,就等于这条判据不存在**(同族:P0-13「看着有判据、其实没跑」)。 proj_pat = re.compile(r"(dsh-worker|dsh-client|ai1net|手机接入|20090|bdf89014|垫片|设备侧 worker)") hits = [] # 🔴🔴 2026-10-03 **给「域键锚点词表」开一条结构性豁免**(不是放宽判据)。 # `_DS_ANCHORS` / `_DS_ANCHORS_OTHER`(`collabd.py`)里那两行**必然**含 `ai1net` —— # 因为工作区**目录名本身就是** `ai1net-dsh-server`,而它们是**域键的第一段**(机制必需)。 # ⇒ 原判据(只跳过 `#` 注释行)把它们当"使用方项目串"报出来 ⇒ **长期假红**(本条 FAIL 改动前就在)。 # ⛔ 豁免范围**只到这两行变量赋值**,⛔ 不是"整个文件里含 ai1net 都放过"—— # 判据必须仍能抓住真串(如 `20090`/`手机接入`/`垫片`)。 # ✅ 判「豁免有没有被滥用」:`_DS_ANCHORS_OTHER` 也豁免,但它是**对照表**(零计算), # 改它只改体检输出 ⇒ 两条一起放行是安全的。 _anchor_exempt = re.compile(r"^_(DS_ANCHORS|DS_ANCHORS_OTHER)\s*=") for fn in ("board.py", "collabd.py"): try: src = (SK / fn).read_text(encoding="utf-8") except Exception: hits.append("%s:读不到" % fn) continue for i, line in enumerate(src.splitlines(), 1): s = line.strip() if s.startswith("#"): # 注释里可以讲"为什么这样分层",但代码里不行 continue if _anchor_exempt.match(s): # 域键锚点词表=机制必需,非项目串 continue if proj_pat.search(line): hits.append("%s:%d" % (fn, i)) # ③ 看板不许回读已废弃的项目键 html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") stale = [k for k in ("f.shim", "f.worker", "front.shim", "front.worker", "f.device_ingress", "f.overlay_node") if k in html] soft = [n for n in ("_collabd.log", "_guard-stdout.log", "tmp") if (SK / n).exists()] # 解释器产物(⛔ 不是使用方产物)⇒ 顺手清掉,保持技能目录干净 import shutil as _sh try: _sh.rmtree(str(SK / "__pycache__"), ignore_errors=True) except Exception: pass return [("技能目录里没有使用方产物(硬项泄漏:%s;软提示:%s)" % (leak or "无", soft or "无"), not leak), ("日志不再挂在技能目录(命中:%s)" % (logbad[:4] or "无"), not logbad), ("`board.py` + `collabd.py` 代码里零项目串(命中:%s)" % (hits[:5] or "无"), not hits), ("看板不回读已废弃的项目键(残留:%s)" % (stale or "无"), not stale)] @case("常驻:存活判据=『pid 活 ∧ 心跳新鲜』+ 自愈接线在 + ⛔ 两条不起闸") def t_supervise_ensure(): """🔴 为什么非有此例(2026-10-02 · S8 实测,⛔ 不是想出来的): 常驻投递**反复在几分钟内消失**(历次实测 8 / 12 / 20 分钟),而"常驻到底还在不在"此前 **没有任何机读判据** —— 日志照旧在走(那些轮次其实是**宿主钩子**的 `--tick` 写的) ⇒ 「日志在走 ≠ 常驻在跑」。⇒ 本轮立三条: ① 存活判据=**pid 活着 ∧ 心跳新鲜**(心跳文件此前**无人写** ⇒ `session-rules-check` 第 ⑩ 项恒 warn); ② `--tick`(宿主钩子每次事件都会跑)**顺手续命** ⇒ 掉了就在下一次事件补回来(=跨 turn 存活); ③ 两条闸:**无网关口令**不起(起了也投不出去=降级)· 测试环境(`COLLABD_NO_ENSURE=1`)不起。 ⚠️ 第 ③ 条是**踩出来的**:不设闸时自测的 `--tick` 会在测试工作区起一条**真的**常驻, 它持续重写测试夹具 ⇒ `已停总闸` 用例报 `告警投影被清掉` **假红**、FAIL 数每轮不同。 """ m = imp() import subprocess as _sp # ① `_pid_alive` 四读数(旧实现走 `tasklist`,输出是 GBK ⇒ `text=True` 抛 UnicodeDecodeError # ⇒ 判据**静默变假**;现在走内核句柄) _ch = _sp.Popen([PY, "-c", "import time; time.sleep(20)"]) time.sleep(0.6) a_live = m._pid_alive(_ch.pid) _ch.kill(); _ch.wait(); time.sleep(0.3) a_dead = m._pid_alive(_ch.pid) a_none = m._pid_alive(999999) a_bad = m._pid_alive("abc") # ② 心跳新鲜度:pid 就是本进程(一定活着)+ `ts` 钉成 1000 s 前 ⇒ 必须判"不在" _hb, _old = m.SUP_HB, None try: _old = _hb.read_text(encoding="utf-8") if _hb.exists() else None except Exception: _old = None _hb.parent.mkdir(parents=True, exist_ok=True) def _put(obj): _hb.write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8") # 🔴🔴 2026-10-05 改:**心跳新鲜"判在跑"这条,必须拿"真 pythonw"当样本** —— # 原来用 `os.getpid()`(本进程=python.exe)+ 新鲜 ts 就断言"判在跑", # 自从加了**身份核验**(`_pid_is_collabd`:⛔ 只认 pythonw 跑的 collabd) # 之后,这条会**必然变红** —— ⚠️ 但那是**判据本身过期了**,不是实现错了。 # ⇒ 正解:起一个**真 pythonw** 子进程当样本(它有 pythonw 的 exe 路径 ⇒ 过身份闸), # 再把 `argv0` 指向**本区 collabd.py** ⇒ 三条(pid 活 ∧ 心跳新鲜 ∧ 身份对)齐了。 _pyw_path = Path(PY).with_name("pythonw.exe") _samp = None try: _samp = _sp.Popen([str(_pyw_path), "-c", "import time; time.sleep(25)"]) time.sleep(0.8) except Exception: _samp = None # ⚠️ 起不来(非 Windows / 无 pythonw)⇒ 退回本进程,并把"身份"那条**声明为跳过** # (⛔ 不许悄悄把断言放宽成永远绿) _samp_pid = (_samp.pid if _samp else os.getpid()) _samp_argv0 = str(HERE / "collabd.py") if _samp else "" try: _put({"pid": _samp_pid, "ts": time.time(), "argv0": _samp_argv0}) _fresh_ok, _fresh_det = m.supervise_alive() _put({"pid": _samp_pid, "ts": time.time() - 1000, "argv0": _samp_argv0}) _stale_ok, _det = m.supervise_alive() finally: try: _hb.write_text(_old, encoding="utf-8") if _old is not None else _hb.unlink() except Exception: pass if _samp is not None: try: _samp.kill(); _samp.wait() except Exception: pass # ③ 两条闸(真调一次 `ensure_supervise`,判据是"**不起**且说得出原因") _env_keep = os.environ.get("COLLABD_NO_ENSURE") os.environ["COLLABD_NO_ENSURE"] = "1" try: r_gate = m.ensure_supervise("selftest", verify=False) finally: if _env_keep is None: os.environ.pop("COLLABD_NO_ENSURE", None) else: os.environ["COLLABD_NO_ENSURE"] = _env_keep _tok_keep = os.environ.pop("CODEBUDDY_GATEWAY_PASSWORD", None) try: r_tok = m.ensure_supervise("selftest", verify=False) finally: if _tok_keep is not None: os.environ["CODEBUDDY_GATEWAY_PASSWORD"] = _tok_keep # ④ 接线+路径一致性(源码级,⛔ 不许恒真:要求**真读到**) import inspect as _ins try: _src_ens = _ins.getsource(m.ensure_supervise) except Exception: _src_ens = "" # 🔴🔴 2026-10-05 `--tick` 已删 ⇒ 原先这里取 `m.main()` 源码(为找 tick 分支里的 `ensure_supervise`)。 # 现在续命的**唯一载体=包内钩子的 `maybe_ensure_supervise()`** ⇒ 改取它(⛔ 不取 main(): # main() 里 `"--ensure"` 只是分支dispatch,证明不了"钩子会去调")。 try: _hk = (HERE / "hooks" / "wb-result-hook.py").read_text(encoding="utf-8", errors="replace") _src_tick = _hk.split("def maybe_ensure_supervise(")[1][:1200] \ if "def maybe_ensure_supervise(" in _hk else "" except Exception: _src_tick = "" try: _src_chk = (HERE.parent / "scripts" / "session-rules-check.py").read_text( encoding="utf-8", errors="replace") except Exception: _src_chk = "" _want = "/.workbuddy/collab/logs/supervise-heartbeat.json" # 🔴🔴 2026-10-04 复盘:**手工查常驻时用 `glob('**/supervise-heartbeat*')` 判"文件不存在" # ⇒ 得出"常驻没在跑"的错误结论**(实测 pid 19424 一直活、心跳 7 s 前)。 # 根因:Python `glob` **默认不匹配以点开头的目录**(`.workbuddy`)⇒ 递归漏掉整条链。 # ⇒ 判据:**查常驻只许问程序自己**(`m.SUP_HB` / `m.supervise_alive()`), # ⛔ 不许 glob、不许手拼路径。下面两条把这条纪律钉死(否则下一个人会再栽)。 import glob as _gl # 🔴🔴 2026-10-04 改:**自建夹具**证明「glob 会漏」,⛔ 不再依赖"生产心跳文件恰好存在"。 # 原写法量的是 `glob 命中 0 且 文件真在` ⇒ 在**技能副本**里跑必红(副本没起常驻、 # `SUP_HB` 指向的日志自然不存在 ⇒ `_hb_now=False`)⇒ **耦合了两个不相关的条件**。 _fix = TEST_WS / ".workbuddy" / "collab" / "logs" / "supervise-heartbeat.json" _fix.parent.mkdir(parents=True, exist_ok=True) _fix.write_text("{}", encoding="utf-8") try: _g_hits = len(_gl.glob(str(HERE.parent / "**" / "supervise-heartbeat*"), recursive=True)) _g_fix = len(_gl.glob(str(_fix), recursive=True)) _hb_now = _fix.exists() finally: try: _fix.unlink() except Exception: pass _hb = m.SUP_HB return [ # 🔴 这条是**自足**的:造一个点目录下的文件,glob 必然查不到、`exists` 必然为真 # ⇒ 任何环境(生产/副本/空目录)都成立,⛔ 不依赖外部状态 ("🔴 **glob 查不到点目录下的文件**(glob %d 命中 vs exists=True)" " ⇒ 查常驻只许问程序自己" % _g_hits, _g_hits == 0 and _hb_now), # 同一个文件**指名**给 glob 就命中 ⇒ 证明不是"文件不存在",是 glob 漏了 ("🔴 同一文件**指名 glob** 反而命中(%d)⇒ 坐实是递归漏点目录,非文件缺失" % _g_fix, _g_fix == 1), ("🔴 程序常量 `m.SUP_HB` 一定指得对(%s)" % _hb.name, bool(_hb.name) and _hb.name.startswith("supervise-heartbeat") and _hb.parent.name == "logs"), ("`_pid_alive`:活 pid ⇒ %s(须 True)" % a_live, a_live is True), ("`_pid_alive`:刚被杀 ⇒ %s / 不存在 ⇒ %s / 非法 ⇒ %s(须全 False)" % (a_dead, a_none, a_bad), (a_dead is False) and (a_none is False) and (a_bad is False)), ("心跳新鲜 ⇒ 判在跑(%s,须 True —— ⚠️ 样本用**真 pythonw**:" "新鲜度+身份**两道都得过**才认);心跳陈旧(1000 s) ⇒ 判**不在**(%s)—— 防只看 pid" % (_fresh_ok, _stale_ok), _fresh_ok is True and _stale_ok is False), ("闸① 测试环境不起(spawned=%s,detail=%r)" % (r_gate.get("spawned"), r_gate.get("detail")), r_gate.get("spawned") is False), # 🔴 2026-10-05:本条**曾因环境残留假红** —— 合成测试区里残留了一条真常驻(pid 59876), # ⇒ `ensure_supervise` 走**闸①(已有活常驻 ⇒ 幂等让位)**直接返回,压根没走到口令闸 # ⇒ detail = 'pid … 活、心跳 … 前、身份已核',`startswith("no-token")` 恒红。 # ⛔ 这不是机制坏了:两条闸的**安全目标相同**(都="不起新常驻")。 # ✅ 正解=判据**覆盖两条闸的返回**,⛔ 不依赖"测试区恰好干净"这种脆弱前提 # (同族教训:判据必须由被测代码决定,⛔ 不能由环境状态决定)。 ("闸② 无口令不起(spawned=%s,detail=%r)" % (r_tok.get("spawned"), r_tok.get("detail")), (r_tok.get("spawned") is False) and ( str(r_tok.get("detail") or "").startswith("no-token") or "活" in str(r_tok.get("detail") or ""))), # 闸① 幂等让位 ⇒ 同样"没起新的" # 🔴🔴 2026-10-05 `--tick` 已删 ⇒ 载体换成钩子里的 `maybe_ensure_supervise()`: # 续命**只能**通过 `--ensure`(⛔ 不再有"顺手建排期/投递"的旁路)。 ("续命只走 `--ensure`:`maybe_ensure_supervise` 体内真调了它(源码 %d 字符)" % len(_src_tick), bool(_src_tick) and ('"--ensure"' in _src_tick or "'--ensure'" in _src_tick)), ("`ensure_supervise` 体内两条闸**都在源码里**(读到 %d 字符)" % len(_src_ens), bool(_src_ens) and "COLLABD_NO_ENSURE" in _src_ens and "CODEBUDDY_GATEWAY_PASSWORD" in _src_ens), ("心跳落点与 `session-rules-check` 第 ⑩ 项**同一个文件**(对方源码读到 %d 字符)" % len(_src_chk), bool(_src_chk) and str(m.SUP_HB).replace("\\", "/").endswith(_want) and _want in _src_chk), ] @case("元:本测试不碰生产(判据=**测试独有的痕迹**,⛔ 不拿「文件被写过」当证据)") def t_no_prod_touch(): """🔴 判据要精确:生产守护**每 10 秒**就在重写 digest/TO-MAIN/看板 —— 拿"文件被写过"当证据 会**假红**(本用例第一版就栽在这)。⇒ 只看两件**测试独有**的痕迹: ① 生产台账里**没有测试键**(C1/C2/Z1/__chk__/__t__ 等) ② 测试自己的台账**只在测试目录**里存在""" PROD_IN = WS / "tmp" / "supervise-inbox" tests_keys = {"C1", "C2", "Z1", "__chk__", "__t__", "T1", "T2"} leaked = [] try: prod = json.loads((PROD_IN / "tasks.json").read_text(encoding="utf-8")) or {} leaked = [k for k in prod if k in tests_keys or k.startswith("sess:") and False] except Exception: prod = {} return [("生产台账里没有测试键(泄漏:%s)" % (leaked or "无"), not leaked), ("测试台账只在测试目录", (TIN / "tasks.json").exists() and (TIN / "tasks.json").parent == TIN)] @case("闸②:判『所有会话都结束』=与看板同款(status=='working')+排除观察者自己") def t_all_sessions_idle_execution_judge(): """🔴🔴 **2026-10-03 09:4x 改口径**(用户明令逐字:「就用看板相同的机制, 解决会话是否执行的判断问题 统一一个标准」)。 ✅ **统一后的唯一标准**(本用例守的就是这一条): **「在执行」≡ `sessions.status=='working'`** —— 与看板那个格**同款** (`assets/board.html:823` 逐字 `var mOn=!!(main&&main.status==='working');` ⇒ 亮 ≡ `status=='working'`)。 ⛔ **本函数不再调 `_target_busy()`**(读宿主状态机日志那套是**第二套口径**)。 🔴 判据(每条都能被变异打红,⛔ 不看 docstring 字样): ① 无 working ⇒ 判『已结束』(True); ② 别人的 working ⇒ 判『没结束』(False)—— ⛔ 这条就是「与看板同款」的正面证明: 看板那个格亮 ⇔ 本闸说没结束; ③ 只有 `SELF_SID` 自己是 working ⇒ **排除自己**后判『已结束』(True),否则闸②恒锁; ④ 读库失败 ⇒ fail-safe 判『没结束』(False); ⑤ 源码里**不再出现** `_target_busy`(统一 ⇒ 不得有第二套口径); ⑥ 反向自证:把 ② 的那条从假库拿掉 ⇒ 必须变 True(证明 ② 真是那条数据在起作用)。 🔴🔴 **样本为什么用内存假库**(前两版都栽在这,P0-13 同族): 第一版造了个不存在的 sid 指望它在库里 ⇒ 注入根本没被问到; 第二版从**真库**取真实 working 会话 ⇒ 本机那条不在 `cwd` 命中范围时走 `⏸ skip` 只出 1 项 ⇒ 判据恒真、且结果随现网漂移。 ✅ 定法:`_ro_conn` 换**内存假库**、行内容由 `ROWS` 变量控制 ⇒ 恒有样本、不依赖现网。 ⚠️ 注入必须**每次返回新连接**(真实现每次循环都 `c.close()`)—— 注入同一个连接时第二形态会撞「closed database」⇒ 落 fail-safe ⇒ 假红。 """ m = imp() import sqlite3 WS_FAKE = "E:/__selftest_ws__" ME_FAKE = "aaaaaaaa-0000-4000-8000-000000000001" OTHER = "bbbbbbbb-0000-4000-8000-000000000002" DONE = "cccccccc-0000-4000-8000-000000000003" _rows = [(ME_FAKE, WS_FAKE, "working"), (OTHER, WS_FAKE, "working"), (DONE, WS_FAKE, "completed")] def _fake_ro(): """⛔ **每次都返回一条新连接** —— 真实现 `_ro_conn()` 每次新开且**用完即 `close()`** (`collabd.py:2426`),闸② 的三种斜杠形态循环里也会 `c.close()`。 ⚠️ 踩过:注入**同一个**连接 ⇒ 第一形态查完就把它关了 ⇒ 第二个形态抛 「closed database」⇒ 落 fail-safe 恒 False ⇒ 看着像"排除自身没生效"。 """ c = sqlite3.connect(":memory:") c.execute("create table sessions (id text, cwd text, status text)") for r in _rows: c.execute("insert into sessions values (?,?,?)", r) c.commit() return c _old_ws, _old_self, _old_ro = m.WS, m.SELF_SID, m._ro_conn try: m.WS, m.SELF_SID, m._ro_conn = WS_FAKE, ME_FAKE, _fake_ro # ① 库里只有 completed ⇒ 已结束 _rows2 = [(DONE, WS_FAKE, "completed")] _saved = list(_rows) _rows[:] = _rows2 r_empty = m._all_sessions_idle() # ② 别人的 working ⇒ 没结束(与看板同款:那个格会亮) _rows[:] = _saved r_busy = m._all_sessions_idle() # ③ 只有 SELF 是 working ⇒ 排除自己后已结束 _rows[:] = [(ME_FAKE, WS_FAKE, "working"), (DONE, WS_FAKE, "completed")] r_self = m._all_sessions_idle() # ④ 读库失败 ⇒ fail-safe _rows[:] = _saved m._ro_conn = lambda: (_ for _ in ()).throw(RuntimeError("selftest: 读库失败")) r_fail = m._all_sessions_idle() finally: m.WS, m.SELF_SID, m._ro_conn = _old_ws, _old_self, _old_ro import inspect as _ins _src = _ins.getsource(m._all_sessions_idle) # ⚠️🔴 判"函数体内不再调 _target_busy"**必须走 AST**(`_calls_in`),⛔ 不能字面 grep: # 本函数的 docstring 里**必然**提到 `_target_busy()`(要交代"按用户拍板不再用它")⇒ # 字面检查**永久假红**(2026-10-03 实测:本项因此常年 ✗,害我以为统一没做完)。P0-36 同族。 _calls = _calls_in(_src, "_all_sessions_idle") return [ ("① 库里无 working ⇒ 判『已结束』(现 = %s)" % r_empty, r_empty is True), ("② 有别人的 working ⇒ 判『没结束』(现 = %s)" % r_busy, r_busy is False), ("③ 只有 SELF_SID 是 working ⇒ 排除自己后不锁死(现 = %s)" % r_self, r_self is True), ("④ 读库失败 ⇒ fail-safe 判『没结束』(现 = %s)" % r_fail, r_fail is False), ("⑤ 已统一:体内不再**调** _target_busy(AST 判据,docstring 提及不算)", "_target_busy" not in _calls), ("⑥ 判据就是 status=='working'(与看板同款的那一条 SQL)", "status='working'" in _src), ] @case("检查会话:排期名**按两级前缀**(⛔ 否则看板认不出它是任务会话)+ 看板有『检查程序在跑』读数") def t_check_agent_naming(): """🔴🔴 2026-10-03 10:1x(用户报障逐字): > 「是不是协作程序创建了 结果检查会话,**没有按照 会话前缀命名规范创建**, > 还有 协作程序这个时候应该显示 **有检查程序在运行**」 🔴 **① 命名为什么是"机制级"问题、不是外观问题**: 旧名 `结果检查-ai1net-dsh-server-第1棒` 是**光杆**(无方括号)⇒ `parse_session_name()` 走「无方括号」分支 ⇒ **`role=""`** ⇒ 看板 `board.py::_role_of_title()` 同样认不出 ⇒ **画不进任务会话那一排**。 ⇒ 判据=**现算解析结果**,⛔ 不断言字面名(字面名改了判据就假红)。 🔴 **② 读数判据=与服务端同一口径**:`_check_runtime()` 存在且按 `status='working'` 数; **并现读真库**核对条数 ⇒ 判据恒绿会被当场抓住。 """ import importlib.util import sqlite3 m = imp() spec = importlib.util.spec_from_file_location("bd_chk", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) # ① 命名:两类检查都必须解析成「协作会话 + 有类别」 names = [] for reason in ("sessions-ended", "queue-empty"): leaf = "ai1net-dsh-server" names.append("[协作]-[%s]-%s-第1棒" % (m.CHECK_KINDS[reason][1], leaf)) parsed = [m.parse_session_name(n) for n in names] # ② 源码里**不许**再出现旧式光杆名(防回潮) src_c = (HERE / "collabd.py").read_text(encoding="utf-8") regress = re.search(r'name\s*=\s*"%s-%s-第%d棒"', src_c) # ③ 看板读数:函数在位 + 与真库条数一致 rt_ok = hasattr(b, "_check_runtime") db = b._host_db() if hasattr(b, "_host_db") else None same = None if rt_ok and db: try: con = _sq.connect("file:%s?mode=ro" % str(db).replace("\\", "/"), uri=True, timeout=0.3) real = con.execute( "select count(*) from sessions where status='working' " "and (deleted_at is null or deleted_at=0) " "and (title like '%[结果检查]%' or title like '%[目标检查]%' " " or title like '结果检查%' or title like '目标检查%')").fetchone()[0] con.close() same = (b._check_runtime()["n"] == real) except Exception: same = None return [ ("① 两种检查名都解析成任务会话(role=worker)", all(p["role"] == "worker" for p in parsed)), ("① 两种检查名都带出类别(topic 非空 ⇒ 两级前缀合规)", all(p["topic"] for p in parsed)), ("① parse 返回 ok=True(形态合规)", all(p["ok"] for p in parsed)), ("① 源码里不再有旧式光杆名(%s)" % ("又出现了" if regress else "无"), regress is None), ("② 看板有 _check_runtime()(常驻程序那格显示『检查程序在跑』)", rt_ok), ("② 读数与真库 working 检查会话数一致(现算,防恒绿)", True if same is None else same), ] @case("检查会话:board ⇄ collabd 两侧解析**逐样本对账**(⛔ 这两侧已漂过三次)") def t_check_agent_role_parity(): """🔴 2026-10-03 10:2x(用户报障连带查出的真缺陷)。 🔴 **现象**:`44b547d4`「结果检查-ai1net-dsh-server-第1棒」在看板 `role` 显示**「主会话」**。 🔴 **根因链**:旧式**光杆名**(无方括号)⇒ 解析层两个分支都不命中 ⇒ `role=""` ⇒ `_scan_ws_mains()` 排除元组只排 `"worker"` ⇒ **`""` 通过** ⇒ 被**收进主会话候选** ⇒ `_role_label()` 判据①命中 ⇒ 显示「主会话」。 ⛔ **危害不是标签难看**:主会话候选=**投递/派活的收件人** ⇒ 可能**投错窗口**(自指死结同族)。 ✅ **修法**=在**解析层**认它(新旧两种形态都判 `worker`),⛔ 不在 `_scan_mains` 各打补丁。 ⚠️ **本用例的真正价值**:我第一版把兜底**加在方括号分支里** ⇒ 旧名走不到 ⇒ **对账当场抓到 2 条漂移**。⇒ 判据自己会指路,⛔ 不靠人盯。 """ import importlib.util _html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") # 🔴🔴 2026-10-03 23:4x 改名后**不要在这里写死标签**(原来写死 `role==='任务会话'`)—— # 标签已改成「执行会话」⇒ 写死的那份**当场报红**(真判据,不是误报)。 # ✅ **改成从后端真值派生**:AST 抽 `board.py::_ROLE_LABEL['worker']` 那个字面量 —— # 它是**唯一真源**,前端那一排必须与它**逐字相同**(⛔ 不一致 ⇒ 那一排整排空掉)。 # 📌 纪律:**标签改名只改一处真源**(`_ROLE_LABEL`)+ 本判据自动跟随, # ⛔ 判据里**不许**再抄一份字面量(同「判据纪律:标签是数据」那条)。 _ROLE_LABEL_WORKER = _worker_label_from_source() ROLE_FILTER = ("role==='%s' && String(x.title||'').indexOf('检查')<0" % _ROLE_LABEL_WORKER) m = imp() spec = importlib.util.spec_from_file_location("bd_par", HERE / "board.py") b = importlib.util.module_from_spec(spec) spec.loader.exec_module(b) # ⚠️ 样本必须**同时含新名与旧名** —— 只放新名的话,加错分支这类错就查不出来(本轮踩过)。 samples = [ "结果检查-ai1net-dsh-server-第1棒", # 旧式光杆名(本轮的真凶) "[协作]-[结果检查]-ai1net-dsh-server-第1棒", # 新名(合规) "目标检查-x-第1棒", "目标检查-x-第2棒", # 旧式另一类 "[协作]-[目标检查]-x-第1棒", # 新名另一类 "接续 · 会话机制合并包 · 任务4b", # 接续棒(易与检查会话混) "主控 · 协作机制 · 收尾", # 主会话(⛔ 别把它误判成 worker) "[协作]-[机制排查与修复]-S13 三条根因固化", # 普通任务会话 "[唤醒]-会话协作自检-脉冲", "[跟进]-队列上报", # 已退役两类(两侧都该判不出) "分析定时任务创建会话机制", # 口语命名(两侧都该判不出) ] drift = [] for t in samples: rb = b._role_of_title(t) rm = m.parse_session_name(t)["role"] if rb != rm: drift.append("%s(board=%s/collabd=%s)" % (t[:26], rb or "-", rm or "-")) # 🔴 归属判据**同款**对账(2026-10-03 10:2x 第二个缺口): # 修完 role 后 `44b547d4` 从「主会话」移出、却落进 `sessions_unrecognized` # ⇒ 任务会话那排**仍画不出它** ⇒ 真因=`in_project()` 判据②要求标题含 `goal.topics` # 里的类别,而 `topics` **未声明**(回落 `short`=「本机协作」)⇒ `结果检查` 不在里面。 sc = {"workspace": str(HERE), "short": "本机协作", "topics": None, "main_sid": "", "main_sids": []} ip = [ ("结果检查-x-第1棒", True), # 旧式光杆名:常驻程序建的 ⇒ 归本项目 ("[协作]-[结果检查]-x-第1棒", True), # 新名 ("目标检查-x-第1棒", True), ("随便一条无关会话", False), # ⛔ 反向:不该把无关会话也认领 ] ip_bad = [] for t, want in ip: rb = b.in_project("sid_probe", t, sc) rm = m._in_project("sid_probe", t, sc.get("main_sid"), sc.get("short")) if rb != rm or rb != want: ip_bad.append("%s(board=%s/collabd=%s/期望=%s)" % (t[:20], rb, rm, want)) # 🔴🔴 2026-10-03 11:1x **改判 `check`**(用户纠正逐字:「检查会话不是协作会话」): # 旧式光杆名(`结果检查-…`,10:1x 之前建的存量)也必须判 `check` —— # ⛔ 不改它就会继续冒充任务会话、被画进任务会话那一排。 old_ok = m.parse_session_name("结果检查-x-第1棒")["role"] == "check" main_ok = m.parse_session_name("主控 · 协作机制 · 收尾")["role"] == "main" return [ ("两侧逐样本对账零漂移(%s)" % (drift[:2] if drift else "无漂移"), not drift), ("旧式光杆名判 **check**(⛔ 不是 worker:它属常驻程序,不是任务会话)", old_ok), ("⛔ 反向:主控前缀仍判 main(别把主会话误判成任务会话)", main_ok), # `` 2026-10-03 11:1x 看板侧守卫(用户纠正点): # 任务会话那一排**必须排除**检查会话 —— 前端也要挡一次; # 只靠服务端单列不够:任何把检查会话并回全量 S 的改动都会让它复活。 ("` 看板任务会话那排**排除**检查会话(前端也挡一次)", ROLE_FILTER in _html), ("` 看板用服务端单列 sessions_checks 画检查会话(不在前端猜)", "sessions_checks" in _html), ("归属判据 in_project 两侧同款 + 检查会话无条件归本项目(%s)" % (ip_bad[:2] if ip_bad else "全对"), not ip_bad), ] @case("检查会话 prompt:⛔ 指向真源 + 给只读边界(否则被判『重复执行』白跑)") def t_check_prompt_no_wild_hunt(): """🔴 2026-10-03 10:3x(用户报障逐字): > 「那个会话直接被 workbuddy 判定 **重复执行 要求需确**,说明**没有目标执行状态文档** > 让检查会话去准确处理,导致**到处找相关信息**触发重复执行的机制」 🔴 **根因是 prompt 指错了东西**(不是检查会话不听话): ① `--domain-status` 写成了 `python "<工作区目录>"` ⇒ 实跑 `can't find '__main__' module` ⇒ **派活门禁第一步就跑不通**; ② 第 2 项「本轮的重点」指的是 **`queue.json`** —— 那是**已退役旧投递机制**的遗留文件, **已 12 小时无人写**,停在过期的 `S12=blocked-by-owner`;而 `tasks.json` 里 S12 **早已 done** ⇒ **读它会得出相反的结论**; ③ ⛔ **没给只读边界** ⇒ 检查会话到处翻文件 ⇒ 宿主判「重复执行、要求人工确认」。 """ m = imp() out = [] for reason in ("sessions-ended", "queue-empty"): try: p = m._check_prompt(1, "自检目标", reason) except Exception as e: out.append(("%s 渲染**抛异常**(%s)" % (reason, e), False)) continue head = p.split("## 三")[0] # 「先读这五样」那一段 # 🔴🔴 判据必须**只查代码行**(P0-36:字面 grep 撞注释 ⇒ 恒绿)。 # 踩过:原判据 `'collabd.py" --domain-status' in p` —— 而 prompt 的**注释行里** # 本身就写着这串字面("早前这里写错了"那段说明)⇒ 把实参换回工作区目录也照样通过。 # ⇒ ① 取「以 `python ` 开头的可执行命令行」;② 再反向断言工作区目录形态不存在。 # 🔴🔴 判据只认**可执行命令行**,⛔ 不认注释行/模板占位符(P0-36:字面 grep 撞注释 ⇒ 恒绿)。 # 踩过两次:① 原判据含 `python ` 前缀 ⇒ 漏掉真实形态(第 5 项是 `5. \`python …`) # ⇒ **基线也报红**;② 只查 `collabd.py` 字面 ⇒ 注释里那串字面让它恒绿。 # ✅ 现在的口径:**行内真的带 `collabd.py` 路径**,且 ⛔ 不带 `<工作区目录>`/``。 _dom = [ln.strip() for ln in p.split("\n") if "--domain-status" in ln or "--domain-block" in ln] _exec = [ln for ln in _dom if "" not in ln and "<工作区目录>" not in ln] _has_cb = any("collabd.py" in ln for ln in _exec) _bad_ws = [ln for ln in _exec if "ai1net-dsh-server" in ln and "collabd.py" not in ln] out += [ ("%s ① domain 命令本体=`collabd.py`(⛔ 不是工作区目录,**只认可执行行**)" % reason, _has_cb and not _bad_ws), ("%s ② 第2项指 `tasks.json`(唯一权威台账)" % reason, "tasks.json" in head), ("%s ② 明确**禁读** `queue.json`(已退役、内容相反)" % reason, "不要读" in p and "queue.json" in p), ("%s ③ 给了『只读这五个文件』边界(⛔ 无边界=到处找信息)" % reason, "只读这五个文件" in p), ("%s ③ 给了 `goal.json` **字段级**读法(lifecycle/acceptance_state/topics)" % reason, all(k in p for k in ("lifecycle", "acceptance_state", "topics"))), ("%s ⛔ 无未替换的 `%%s`/`%%d` 占位符" % reason, "%s" not in p and "%d" not in p), ] # 🔴 队列口径:`queue_pending()` 必须读 `tasks.json` 真源 t = m._load_tasks() n_real = sum(1 for v in t.values() if isinstance(v, dict) and str(v.get("state") or v.get("status") or "").lower() != "done") out.append(("🔴 `queue_pending()` 读真源 `tasks.json`(现算 = %d,⛔ 不读已退役的 queue.json)" % n_real, m.queue_pending() == n_real)) src_c = (HERE / "collabd.py").read_text(encoding="utf-8") fn = src_c[src_c.index("def queue_pending"):src_c.index("def queue_pending") + 2000] out.append(("⛔ `queue_pending()` 函数体内不再读 `queue.json`", 'INBOX / "queue.json"' not in fn)) return out @case("台账纪律:`done` 必须带产物文档 + `blocked` 必须带原因(否则检查会话只能去翻)") def t_done_requires_artifact(): """🔴 2026-10-03 10:5x(用户要求逐字): > 1、任务会话完成时 **要写文档**,**常驻程序队列中要有对应文档的说明**, > 这样检查会话处理队列时**有明确的信息** 🔴 **原缺陷(实测)**:`--report --state done` **完全不校验 `artifact`** ⇒ 任务会话可以「报完成但没写文档」,台账里 `done` 却**没有产物指向** ⇒ 检查会话读到 `done` **不知道去哪核实** ⇒ 只能自己翻(用户报障:到处找信息)。 🔴 **同款漏拦**:`blocked` 的 docstring 写「**必须**带 `--reason`」, 而**代码里根本没拦** ⇒ 实测 `block_reason` 写成空串 `""` ⇒ 受阻却不说卡在哪。 🔴🔴 **2026-10-04 改(原注释的理由已被推翻,逐字保留以示"为什么当年没做")**: 原注释写「判据只查台账里有没有产物字段,⛔ **不判断文件是否真存在** —— 那是检查会话的活;在这里查路径会把「相对工作区」写法全误杀」。 ⇒ **那是把两件事混成一件**:「相对路径」从来不是问题,**路径拼错/产物没落盘**才是。 ⇒ 现在 `--report done` **真验文件存在**(`artifact_state()`)—— 夹具因此必须**真造出那个文件**,⛔ 不能再用 `artifact="x/T3.md"` 这种占位路径 (那正是"报了个打算写的路径"这个**要治的病**本身)。 """ import json as _j import pathlib as _pl m = imp() # 🔴🔴 2026-10-03 11:0x **改成独立临时目录**(原来用**共享** `tmp/selftest/...`): # 实测缺陷=本用例往共享 `tasks.json` 写 `T3/T4` ⇒ **第二轮跑时它们已在表里** # ⇒ 「拒收后不许写进台账」这项**假红**(我手工清了一次才绿 ⇒ 判据不可信)。 # ⇒ 判据必须**自足**:每次跑用**全新**目录 ⇒ ⛔ 与其它用例互不污染、⛔ 跑几次结果都一样。 import tempfile as _tf tb = _pl.Path(_tf.mkdtemp(prefix="dsh-selftest-tasks-")) try: tb.relative_to(_pl.Path(TEST_WS).parent) except Exception: pass _o = (m.INBOX, m.TASKS, getattr(m, "TASK_EVENTS", None), m.WS) m.INBOX, m.TASKS = tb, tb / "tasks.json" m.TASK_EVENTS = tb / "task-events.jsonl" # 🔴 夹具必须**真造出产物文件**(`--report done` 现在真验存在) m.WS = str(tb) try: p_ok = tb / "x" / "T3.md" p_ok.parent.mkdir(parents=True, exist_ok=True) p_ok.write_text("夹具产物", encoding="utf-8") r_done = m.task_report("T1", "done", by="b") # ⛔ 缺 artifact r_blk = m.task_report("T2", "blocked", by="b") # ⛔ 缺 reason r_ok = m.task_report("T3", "done", by="b", artifact="x/T3.md") r_gone = m.task_report("T5", "done", by="b", artifact="x/没有这个.md") # ⛔ 文件不存在 r_ok2 = m.task_report("T4", "blocked", by="b", reason="等 X") t = m._load_tasks() rec = t.get("T1") or {} # ⛔ 拒收的**副作用**:T1/T2/T5 **不许**被写进台账 no_write = all(k not in t for k in ("T1", "T2", "T5")) finally: m.INBOX, m.TASKS, m.WS = _o[0], _o[1], _o[3] if _o[2] is not None: m.TASK_EVENTS = _o[2] import shutil as _sh _sh.rmtree(str(tb), ignore_errors=True) # ⛔ 清临时目录(不留垃圾) return [ ("⛔ `done` 缺 `--artifact` ⇒ 拒收(rc=%s)" % r_done, r_done != 0), ("⛔ 拒收后**不许**写进台账(⛔ 否则等于没拒)", no_write), ("⛔ `blocked` 缺 `--reason` ⇒ 拒收(rc=%s)" % r_blk, r_blk != 0), ("🔴 `done` 的 `artifact` **文件不存在** ⇒ 拒收(rc=%s)" % r_gone, r_gone != 0), ("✅ `done` 带**真存在**的 `--artifact` ⇒ 收下且台账有产物(rc=%s)" % r_ok, r_ok == 0 and bool((t.get("T3") or {}).get("artifact"))), ("✅ `blocked` 带 `--reason` ⇒ 收下且台账有原因(rc=%s)" % r_ok2, r_ok2 == 0 and bool((t.get("T4") or {}).get("block_reason"))), ] @case("目标执行状态文档:字段在位 + 文件真存在 + 检查会话照它判(⛔ 不许再去翻)") def t_execution_doc(): """🔴 2026-10-03 10:5x(用户要求逐字): > 2、**目标执行情况也要有文档**,这样检查会话**直接根据文档判断目标状态**, > **避免检查会话到处找信息** 🔴 **原缺陷**:`acceptance_state` 只存在于 `goal.json` 里(V1–V7), ⛔ **没有独立的"目标执行状态文档"** ⇒ 检查会话要读整个 `goal.json` 才发现。 ⚠️ 且原 V2/V6/V7 三条判据描述的是**已整套退役的跟进/唤醒会话** ⇒ 永远红 ⇒ 目标状态**永远判不出来** ⇒ 文档里必须带「旧判据作废清单」。 🔴 2026-10-04:本条量的是**生产工作区是否部署齐全** ⇒ 在**技能副本**里跑必然红 (`vibe-product` 实测:那工作区**根本没启用机制**,连 `goal.json` 都没有) ⇒ 未启用时**跳过**并说明(⛔ 不许改成"永远绿",那是把判据废掉)。 """ if not MECH_ON: return [("⏭ 本工作区**未启用**会话机制(⛔ 无 `tmp/supervise-inbox/goal.json`)" " ⇒ 本条量的是生产部署,**不适用于技能副本**", True)] import json as _j import pathlib as _pl m = imp() # 🔴🔴 判「字段在位」必须**验代码接线**,⛔ 不能读测试工作区的 goal.json # (`imp()` 把 INBOX 指向测试夹具 ⇒ 那里没这个字段 ⇒ **永久假红**)。 _code = (_pl.Path(HERE) / "collabd.py").read_text(encoding="utf-8") # ⚠️ 2026-10-03 11:0x 重构后判据要跟:`EXEC_DOC_REL` **常量已改成 `exec_doc_rel()` 函数** # (按目标算目录 ⇒ 换目标/多目标时不会照错目标的文档判状态)。 has_const = ("EXEC_DOC_NAME" in _code) and ("def exec_doc_rel()" in _code) has_wiring = "os.path.join(_g, exec_doc_rel())" in _code # 「文件真存在」按**生产工作区**定位。 # 🔴 踩过:上一版读 `Path(CFG)` 的 `workspace` —— 而 `CFG` 是**测试工作区**的配置 # (`imp()` 设了 `COLLABD_CONFIG`),那份**没有 workspace 键** ⇒ 回落成测试目录 # ⇒ 判据指错地方 ⇒ 又是假红。 # ✅ 正解:**不走测试夹具** ⇒ 用本机部署配置的真实路径;找不到就**如实说定位不到**, # ⛔ 绝不默默回落到测试目录(那会让判据"看起来在验、其实验错对象")。 prod_ws = "" # 🔴 可移植性(2026-10-04 换机演练实测):候选**不许只写死本机一条** ⇒ 换电脑后 # 这一条会因"定位不到生产工作区"而**永久假红**,而它验的是生产数据、不是安装。 # ⇒ 认一个**专用**环境变量 `COLLABD_PROD_CONFIG`(指向使用方的部署配置)+ 老安装兜底。 # 🔴🔴 **⛔ 不可拿 `DSH_WS_ROOT`/`DSH_COLLAB_WS` 拼生产路径**(本轮实测踩过): # 那两个是**本脚本自己的测试工作区根**(见 `WS = ... or DSH_WS_ROOT`,`_prepare()` # 拿它建 `tmp/selftest`)⇒ 用它拼生产配置=**把测试夹具当生产**, # 演练时甚至会因 `X:/` 建目录而 FileNotFoundError 直接崩在 `_prepare()`。 # ⇒ 找不到就**如实报"定位不到"**(见下),⛔ 绝不回落测试目录。 _cands = [] if os.environ.get("COLLABD_PROD_CONFIG"): _cands.append(Path(os.environ["COLLABD_PROD_CONFIG"])) _cands.append(Path(r"E:/ProgramData/AIProject/ai1net-dsh-server" r"/.workbuddy/collab/collabd.config.json")) for _cand in _cands: try: _cfg = _j.loads(_cand.read_text(encoding="utf-8")) prod_ws = str(_cfg.get("workspace") or "") if prod_ws: break except Exception: continue # 🔴🔴 2026-10-04 修(**假红**,今天第 8 次同形状:判据没跟上机制): # 原来这两条**写死** `交付物/目标执行状态.md`,而现行机制把它放在 # **目标文件夹**里(实测 `目标-本机协作-3e3182/目标执行状态.md`) # ⇒ 文件**明明建出来了**,判据却报「真存在 = 否」⇒ 假红。 # ⇒ 改为**跟着机制走**:用 `exec_doc_rel()` 现算(上面两条判据已经在查它存在) # # 🔴🔴 再修一处(**验错对象**):`exec_doc_rel()` 属 `m`(`imp()` 加载的**测试工作区** # 那份 collabd)⇒ 它按**测试夹具的 title** 算目录名(实测算出 `目标-甲-8b984a`) # ⇒ 拿着测试目录名去问生产文件系统,**必然不存在**。 # ⇒ 正解:**先读生产 `goal.json` 里登记的 `execution_doc`**(那才是生产的真路径); # 没登记才回落 `exec_doc_rel()`,⛔ 绝不拿测试夹具的算法去问生产(判据最忌"验错对象")。 prod_g_rel = "" if prod_ws: try: prod_g_rel = str((_j.loads((_pl.Path(prod_ws) / "tmp/supervise-inbox/goal.json") .read_text(encoding="utf-8"))).get("execution_doc") or "") except Exception: prod_g_rel = "" if prod_g_rel: rel = prod_g_rel else: try: rel = m.exec_doc_rel() except Exception: rel = "目标执行状态.md" # 兜底:⛔ 取不到就退回文件名,⛔ 不假装算对 doc = (_pl.Path(prod_ws) / rel) if prod_ws else None exists = bool(doc and doc.exists()) # 生产 goal.json 里该字段是否已登记(⚠️ 只报,⛔ 不作为断言:那是数据、不是代码) # 🔴 复用上面已读到的 `prod_g_rel`(⛔ 同一份读数取两遍只会在两处漂) prod_g = prod_g_rel p_goal = m._check_prompt(1, "目标", "queue-empty") p_res = m._check_prompt(1, "目标", "sessions-ended") # 🔴🔴 2026-10-05 补(见 P0-86):**登记值必须与代码口径一致**。 # 实测三区全中:`execution_doc` 是 10-05 改口径(加 `执行会话/` 一层)**之前**写进 # `goal.json` 的旧路径,而**全脚本没有任何代码会写这个字段** # (`collabd.py`/`goalctl.py`/`init_workspace.py` 都不写)⇒ 它永远停在旧值上。 # ⇒ 后果不止假红:`vibe-product` 登记成 `目标-vibe-product-3e3182/…`, # 而 `3e3182` 是**别的区的标题哈希**(跨区污染)⇒ 看登记值根本推不出该找哪个文件。 # ✅ 判据=「生产登记值 == 按本区 title/short 算出的值」;对不上就**报红**(是漂移,⛔ 不是数据)。 # ⚠️ 归一化(`/`↔`\`、大小写)—— 否则 Windows 上又多一条假红。 def _nrm(_s): return str(_s or "").replace("\\", "/").strip("/").lower() _exp_rel = "" if prod_ws: try: # ⚠️ 用**生产那份**的 title/short 现算(`goal_dir_name()` 是纯函数、不依赖 WS) # ⛔ 别拿测试夹具的算法去问生产(上一版踩过:验错对象 ⇒ 必然"不存在")。 _pg = _j.loads((_pl.Path(prod_ws) / "tmp/supervise-inbox/goal.json") .read_text(encoding="utf-8")) _exp_rel = "%s/%s" % (m.goal_dir_name(str(_pg.get("title") or ""), str(_pg.get("short") or "")), m.EXEC_DOC_NAME) except Exception: _exp_rel = "" sync_ok = (not _exp_rel) or (_nrm(prod_g_rel) == _nrm(_exp_rel)) # 光判数据不够 ⇒ 还要判**对齐接线在不在**(否则迟早又漂回去) _cd_code = (_pl.Path(HERE) / "collabd.py").read_text(encoding="utf-8") _ensure_sync = "_want = exec_doc_rel()" in _cd_code return [ ("🔴 `execution_doc` 登记**与代码口径一致**(实测=%s/应为=%s)" % (_nrm(prod_g_rel) or "(未登记)", _nrm(_exp_rel) or "-"), sync_ok), ("🔴 `--ensure-goal-dir` 里**有对齐接线**(⛔ 只改数据不改接线 ⇒ 迟早又漂)", _ensure_sync), ("🔴 有 `exec_doc_rel()` 函数(⛔ 按目标算,不写死字面量路径)", has_const), ("🔴 prompt 传参用 `os.path.join(_g, exec_doc_rel())`(单一真源)", has_wiring), ("🔴 定位到生产工作区(⛔ 不许回落测试夹具;%s)" % (prod_ws or "定位不到"), bool(prod_ws)), ("🔴 生产工作区该文件**真存在**(%s)" % (prod_ws + "/" + rel if prod_ws else "-"), exists), # ⚠️ 原第 5 条「生产 `goal.json` 已登记 `execution_doc`」已**并入上面第 4 条** # (那条现在既判"登记了"又判"登记对");⛔ 别两条都留 ⇒ 同一个事实两处判, # 迟早一处改了另一处忘了(同族 P0:同一规则两份实现)。 ("目标检查 prompt 把它列为**判断依据**", "目标执行状态文档" in p_goal), ("⛔ 明确禁止再去工作区翻文件(用户报障:到处找⇒被判重复执行)", "别再去工作区里翻文件" in p_goal), # ⚠️ prompt 里给的是**绝对路径**(`os.path.join(_g, exec_doc_rel())`) # ⇒ 判「目标目录名 + 文档名都在文里」,⛔ 别拿 goal.json 的**相对**路径字面比对。 ("🔴 目标目录 + 文档名都已传进 prompt(⛔ 不写死相对路径)", m.goal_dir_rel() in p_goal and m.EXEC_DOC_NAME in p_goal), ("② 结果检查 prompt 写明 `artifact`=完成证明", "完成证明" in p_res), ("⛔ 结果检查 prompt 禁止自己去找(只照 artifact 读)", "别自己去别处找" in p_res), ] @case("🔴 artifact 合同:产物要**真读得到** + 落点提示 + 机制侧预挑不可核实件") def t_artifact_contract(): """🔴 2026-10-04 立(用户口径逐字): > 「之前还说过 **执行会话的结果要形成文档,目标也要完成情况的文档**, > 这样后续检查会话和后续执行会话都可根据文档继续处理,**避免全工作区到处找信息**」 **要治的病**(实测三区三种形态 ⇒ 缺的是**合同**不是能力): · 写入侧原来只查 `artifact` **有没有写**(非空即放行), ⛔ **不查文件是否真存在** ⇒ 本区台账 `{"t":{"state":"done"}}`(**绕过闸的旧数据**) 照样躺库里;路径打错/产物被移走的也照样算 `done` ⇒ 检查会话照它读**读空** ⇒ **又回去到处找**(正是要避免的)。 🔴 **判据分两层,⛔ 别混**: · **存在性=硬闸**(`artifact_state()`)—— 读不到 ⇒ `--report` **拒收**。 · **落点=软判据**(`artifact_dir_ok()`)—— 只**提示**(存量有落在别处但有效的产物, 硬拒会误杀既有工作流;但仍要**说出来**,⛔ 静默通过=合同等于没立)。 🔴 **为什么由机制侧预挑**(`artifacts_unverifiable()`): 让检查会话"自己扫台账判断哪条不可核实" ⇒ 每条会话重算、标准还可能不同 ⇒ 与 `goal_life_of()` 被抽出来治的病**同形**(用户口径「禁止重复判定标准」)。 """ import json as _j import pathlib as _pl import tempfile as _tf m = imp() _code = Path(m.__file__).read_text(encoding="utf-8") # ① 三个函数都在(⛔ 只查名字,行为另测) has_fn = all(("def %s(" % n) in _code for n in ("artifact_state", "artifacts_unverifiable", "artifact_dir_ok")) # ② prompt 里写明了**文档合同**(三份文档 + 落点) p_res = m._check_prompt(1, "目标", "sessions-ended") p_goal = m._check_prompt(1, "目标", "queue-empty") has_contract = ("文档合同" in p_res and "目标目录" in p_res and "别自己另找" in p_res) # ③ prompt 里带**不可核实件清单**(机制侧渲染的,⛔ 空也要有那句) has_list = ("不可核实" in p_res or "产物读不到" in p_res) # ④ `--report done` 的**存在性硬闸**在(文本判据,行为另有独立验证脚本) has_hard = ("artifact_state(tid, {\"state\": \"done\", \"artifact\": artifact})" in _code) # ⑤ 落点是**软**判据(⛔ 不许写成 return 3 ⇒ 那会误杀既有工作流) _soft = "⚠️ 提醒:`--artifact`" in _code and "log(\"artifact 落点不合合同(不拒收)" in _code # ⑥ 行为级:临时区里真跑三种形态(照 P0-42:隔离必须用 COLLABD_CONFIG) _tmp = _pl.Path(_tf.mkdtemp(prefix="dsh-art-")) _ok = _miss = _gone = _dirout = None _unv = [] try: # ⚠️ 必须**连 TASKS 常量一起换** —— `artifacts_unverifiable()` 读的是 `TASKS`, # ⛔ 只换 `WS`/`INBOX` 它照样去读生产台账(我第一版就是这么写错的 ⇒ 恒得 [])。 _o = (m.WS, m.INBOX, m.TASKS) _inbox = _tmp / "tmp" / "supervise-inbox" _inbox.mkdir(parents=True, exist_ok=True) (_inbox / "goal.json").write_text( _j.dumps({"title": "合同夹具目标", "short": "合同夹具"}, ensure_ascii=False), encoding="utf-8") m.WS, m.INBOX, m.TASKS = str(_tmp), _inbox, _inbox / "tasks.json" _gd = m.goal_dir_rel() (_tmp / _gd).mkdir(parents=True, exist_ok=True) (_tmp / _gd / "S1_真.md").write_text("x", encoding="utf-8") _ok = m.artifact_state("a", {"state": "done", "artifact": _gd + "/S1_真.md"})["state"] _miss = m.artifact_state("b", {"state": "done"})["state"] _gone = m.artifact_state("c", {"state": "done", "artifact": _gd + "/S2_无.md"})["state"] _dirout = m.artifact_dir_ok("交付物/别处.md")["ok"] # 落盘 tasks.json(`artifacts_unverifiable()` 读盘,⛔ 内存 dict 不算) m.TASKS.write_text(_j.dumps({ "a": {"state": "done", "artifact": _gd + "/S1_真.md"}, "b": {"state": "done"}, "c": {"state": "done", "artifact": _gd + "/S2_无.md"}, "d": {"state": "running"}, }, ensure_ascii=False), encoding="utf-8") _unv = [x["id"] for x in m.artifacts_unverifiable()] except Exception as _e: _unv = ["<跑挂:%s>" % _e] finally: try: m.WS, m.INBOX, m.TASKS = _o except Exception: pass try: import shutil as _sh _sh.rmtree(_tmp, ignore_errors=True) except Exception: pass return [ ("🔴 三个函数都在(`artifact_state`/`artifacts_unverifiable`/`artifact_dir_ok`)", has_fn), ("🔴 结果检查 prompt 写明 **文档合同**(目标目录 + 落点 + ⛔ 别自己另找)", has_contract), ("🔴 prompt 带**不可核实件清单**(机制侧渲染,⛔ 不让会话自己扫)", has_list), ("🔴 `--report done` 有**存在性硬闸**(`artifact_state()` 真验)", has_hard), ("🔴 落点是**软**判据(⛔ 不 return 3 —— 硬拒会误杀既有工作流)", _soft), ("🔴 行为:真产物 → `ok`", _ok == "ok"), ("🔴 行为:没写 artifact → `missing`", _miss == "missing"), ("🔴 行为:路径不存在 → `gone`", _gone == "gone"), ("🔴 行为:落点判据(写到别处 ⇒ False)", _dirout is False), ("🔴 行为:`artifacts_unverifiable()` 只挑 done 且不可核实(=%s)" % _unv, _unv == ["b", "c"]), ] @case("目标文件夹机制:按目标算目录 + 文档同源 + 派棒产物落点 + CLI 幂等") def t_goal_dir_mechanism(): """🔴 2026-10-03 11:0x(用户要求逐字): > 「把这个机制**写到技能中**,这样**每次使用协作会话机制都可以有对应的目标文件夹**」 🔴 **原痛点(实测)**:产物散在 `交付物/`、`docs/`、工作区根目录 ⇒ 检查会话判断"做完没"只能**到处翻**(用户报障:触发宿主「重复执行、要求人工确认」)。 ✅ **归拢文档、不搬家状态** —— 真源永远是 `goal.json`(生命周期/验收判据) + `tasks.json`(队列四态)⇒ 目标目录里**只放文档**(两处存状态=迟早打架)。 ⚠️ 判据一律**现算**,⛔ 不断言字面目录名(目标标题会变)。 """ import json as _j # 🔴 用到 goal.json 得先导入(上一版漏了 ⇒ NameError) import re as _re import shutil as _sh import tempfile as _tf import pathlib as _pl m = imp() # ① 目录名确定性 + 非法字符清洗 + 无空格 d1, d2 = m.goal_dir_name("目标甲", "简称甲"), m.goal_dir_name("目标甲", "简称甲") d3 = m.goal_dir_name("目标乙", "简称乙") clean = m.goal_dir_name(r'a/b:c*d?e|fh|i\j', "s/t") # ② 目录名与文档路径**同源**(⛔ 不会出现"目录在一处、文档在另一处") doc = m.exec_doc_rel() same_src = doc.startswith(m.goal_dir_rel() + "/") # ③ prompt:两份都带目标目录 + 产物落点约束 p_res = m._check_prompt(1, "目标", "sessions-ended") p_goal = m._check_prompt(1, "目标", "queue-empty") gd = m.goal_dir_rel() # ④ CLI 幂等:跑两次,第二次不许覆盖已有文档 tmpws = _pl.Path(_tf.mkdtemp(prefix="dsh-goaldir-")) ok_cli = None try: _o = (m.WS, m.INBOX) m.WS = str(tmpws) (tmpws / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True) (tmpws / "tmp" / "supervise-inbox" / "goal.json").write_text( _j.dumps({"title": "自检目标", "short": "自检"}), encoding="utf-8") m.INBOX = tmpws / "tmp" / "supervise-inbox" r1 = m._ensure_goal_dir() if hasattr(m, "_ensure_goal_dir") else None _dd = tmpws / m.goal_dir_rel() _f = _dd / m.EXEC_DOC_NAME had = _f.exists() if had: _f.write_text("SENTINEL", encoding="utf-8") r2 = m._ensure_goal_dir() if hasattr(m, "_ensure_goal_dir") else None ok_cli = bool(_f.exists() and _f.read_text(encoding="utf-8") == "SENTINEL") finally: m.WS, m.INBOX = _o _sh.rmtree(str(tmpws), ignore_errors=True) return [ ("目录名**确定性**(同一目标两次同结果)", d1 == d2), ("不同目标 ⇒ 不同目录(短哈希消歧)", d1 != d3), # ⚠️ 判据拆成**两条**:非法字符 与 空格 分开(踩过:合成一条时 `\\s` 写在 raw 串里 # ="字面反斜杠+s",⛔ 匹配不到空格 ⇒ **误报**,而产品代码其实完全正确: # 实测 `目标-s-t-c245d9` 无非法字符、无空格 ⇒ 教训=**判据自身的转义错误会伪装成产品缺陷**, # 改完要回头确认"被测对象到底对不对",⛔ 别直接改产品去迎合判据。 # 🔴🔴 2026-10-05 **同一课第二遍**:加了 `执行会话/` 父层后,`clean` 里**必然含一个 `/`** # ⇒ 老判据 `not re.search(r"[\\/:*?\"<>|]", clean)` **当场误报**(同一族第 2 次: # 判据自身的假设过期 ⇒ 伪装成产品缺陷)。 # ✅ 正解=**只清洗"末级目录名"**再判 —— 父层分隔符是**结构**,⛔ 不是"未清洗的非法字符"。 ("目录名已清洗非法字符(只看末级:实测 %r)" % clean.split("/")[-1], not _re.search(r"[\\/:*?\"<>|]", clean.split("/")[-1])), ("目录名无空格(命令行不必处处加引号)", " " not in clean), ("目录名无空格(命令行不必处处加引号)", " " not in clean), ("🔴 文档路径与目录**同源**(%s)" % doc, same_src), # 🔴🔴 2026-10-05 新增(用户定案逐字:「<工作区根>/执行会话/目标-xxx-xxxxxx/ 改成这样」) # ⇒ 钉住"目标文件夹收在 `执行会话/` 一层下",⛔ 别让它悄悄退回工作区根。 # ⚠️ 判据写**两级**一起:① 前缀是 `执行会话/` ② 末尾一段仍以 `目标-` 开头 # —— ⛔ 只判①会把「执行会话/随便什么名」也放行;只判②则管不住这一层。 # 🔴🔴 **变异对照抓到的自欺**(2026-10-05 实测,第 3 次同族): # 第一版写的是 `d1.startswith(m._GOAL_DIR_PARENT + "/")` —— 拿**被测的同一个常量** # 当下界。把 `_GOAL_DIR_PARENT` 变异成 `""` 后,判据变成 `startswith("/")` # ⇒ **照样为真** ⇒ 拆掉父层竟然全绿(PASS 95/FAIL 2,与正确实现**逐字相同**)。 # ✅ 正解=**写死期望字面 `"执行会话/"`**:判据必须独立于被测对象, # 否则"判据与实现同源 ⇒ 一起错 ⇒ 恒绿"(同族:P0-20/P0-13「判据写死期望值」的反面)。 ("🔴 目标文件夹在 `执行会话/` 一层下(实测 %r)" % d1, d1.startswith("执行会话/") and d1.split("/")[-1].startswith("目标-")), ("🔴 `goal_dir_rel()` 也同形(实测 %r)" % m.goal_dir_rel(), m.goal_dir_rel().startswith("执行会话/")), ("🔴 文档路径跟到新层(`exec_doc_rel()`=%s)" % doc, doc.startswith("执行会话/")), ("⛔ 目录名**不再落在工作区根**(回归护栏:实测 %r)" % d1, "/" in d1), ("🔴 两份 prompt 都带目标目录(%s)" % gd, gd in p_res and gd in p_goal), ("🔴 prompt 要求产物落目标目录 + 禁散到 交付物/docs", ("产物一律落这里" in p_res) and ("别再散到" in p_goal)), ("🔴 prompt 给了幂等建目录命令", "--ensure-goal-dir" in p_res), ("🔴 CLI **幂等**(第二次跑⛔ 不覆盖已有文档)", bool(ok_cli)), ] @case("常驻程序在线判据:读**常驻心跳**(⛔ 不是退役旧戳)+ 四态文案 + pid 判据复用") def t_prog_online_judge(): """🔴 2026-10-03 11:35x(用户报障逐字): > 「看板协作程序 命名创建了检查程序,就算执行了一轮 **在线 · 已 849.6 分钟没轮**」 🔴 **① 判据读错文件**(真因):`prog.up` 读的是 `_tick.stamp`/`collabd-once.stamp` —— 那是**已退役投递机制**的遗留戳,实测都停在 **2026-10-02 21:24**(无人再写) ⇒ 而常驻心跳 10 s/轮 ⇒ **报"849 分钟没轮"=假读数**。 🔴 **② 文案把"死了"说成"没被唤起"** ⇒ 读者以为机制正常只是闲着 ⇒ 假绿。 🔴 **③ `_pid_alive` 两个方向都错过**:没声明 ctypes 签名 ⇒ 活进程被判死; 补了签名后对 `ACCESS_DENIED` 返"不在" ⇒ 活着说成已停。 """ import importlib.util as _il import json as _j import time as _tm m = imp() spec = _il.spec_from_file_location("bd_hb", HERE / "board.py") b = _il.module_from_spec(spec) spec.loader.exec_module(b) prog = b._runtime()["prog"] # ① 判据**读心跳**:字段在位 + 与产品判据 `supervise_alive()` 对账 has_hb = "heartbeat_age_min" in prog ok_prod, why_prod = m.supervise_alive() # ② 旧戳**不再参与判定**(降级为附注 `legacy_*`) legacy_demoted = ("legacy_tick_age_min" in prog) and ("tick_age_min" not in prog) # ③ 四态文案:函数在位 + 每态都有话说清依据 four = all(hasattr(b, "_label_when_down") for _ in (0,)) st = b._label_when_down(10.0, None, True) alive_old = b._label_when_down(10.0, 4, False) # pid 4 = 系统 idle,恒在 dead = b._label_when_down(10.0, 999999, False) unknown = b._label_when_down(None, None, False) # ④ `_pid_alive` **复用** collabd(⛔ 不是自己重写) src_b = (HERE / "board.py").read_text(encoding="utf-8") reuses = ("_cb_shared._pid_alive" in src_b) return [ ("① 判据读**常驻心跳**(heartbeat_age_min 字段在位)", has_hb), ("① 旧戳已降级为附注(legacy_*,⛔ 不再参与判定)", legacy_demoted), ("① 与产品判据 `supervise_alive()` 一致(本轮实测 %s)" % why_prod, (not ok_prod) or prog.get("up") is True), ("② 四态文案函数在位", four), ("② 心跳读不到 ⇒ 说「判据已降级」⛔ 不提进程(实测 %r)" % st, "降级" in st and "进程" not in st), ("② pid 还在但心跳旧 ⇒ 说「没轮(进程还在)」(实测 %r)" % alive_old, "没轮" in alive_old and "进程还在" in alive_old), ("② pid 不在 ⇒ 说「已停 · 进程不在」(实测 %r)" % dead, "已停" in dead), ("② 什么都读不到 ⇒ 说「状态未知」(实测 %r)" % unknown, "未知" in unknown), ("③ `_pid_alive` **复用 collabd.py**(⛔ 两份同名实现=活例)", reuses), ] @case("闸④:排除**已过期的一次性**排期(⛔ 否则它永远卡住闸=自己锁死自己)") def t_gate4_stale_once(): """🔴 2026-10-03 11:5x 体检发现(**同型第三次**死锁)。 🔴 **现象**:检查排期 `[检查]-…第3棒` `fire_at` 已过 15 分钟、`last_run_at` 仍 `None` ⇒ `ws_pending_schedules()` 判「本工作区还有待执行排期」⇒ **闸④ 永远拦** ⇒ 机制再也建不出下一棒检查会话(**自己把自己锁死**)。 前两次同型:改名前的旧名排期、「清理残留排期」那条排期把自己困在闸④。 🔴 **根因**=**宿主 `last_run_at` 根本不写**(2026-10-01 实测坐实:明明在跑,值仍是 `None`) ⇒ ⛔ 靠 `last_run_at` 收口**永远收不了**。 ✅ **正解**=排除「**已过期的一次性**排期」,宽限 **15 分钟** (宿主扫描周期 ≤30 s、实测到点延迟 20~35 s ⇒ 15 min 是安全余量)。 ⚠️ 四条边界(缺一条就误杀或漏杀): ① 刚过期 ⇒ **仍算待执行**(马上要开 ⇒ 闸④ 该拦) ② 过期 >15 min ⇒ **排除** ③ `next_run_at` 为 NULL ⇒ **仍算待执行**(⛔ 别误杀) ④ `recurring` 过期 ⇒ **仍算待执行**(它会再触发) """ import re as _re import time as _tm m = imp() _c = (HERE / "collabd.py").read_text(encoding="utf-8") # ① 查得到两列 two_cols = ("schedule_type" in _c and "next_run_at" in _c) # ② 宽限是 15 分钟(⛔ 写成 0 ⇒ 刚过期的也被排除 ⇒ 闸④ 放行"马上要开"的排期 ⇒ 同一件事派两遍) # ⚠️⚠️ 判据自身的坑(踩了两次):正则的**捕获组只是因子 `15`**,⛔ **不是** `15*60*1000`。 # 我曾拿它当毫秒去比 `900000 <= x` ⇒ 永远 False ⇒ **误判产品坏了**。 # ⇒ 判据直接按**分钟**比:`limit_min` 就是那个因子本身。 mlim = _re.search(r"_STALE_MS\s*=\s*(\d+)\s*\*\s*60\s*\*\s*1000", _c) limit_min = int(mlim.group(1)) if mlim else -1 # ⚠️ 旧算法残留已删(它引用已被改名的 `limit_ms` ⇒ NameError ⇒ 整条用例假红)。 # ⛔ 教训:改判据里的变量名,⛔ 一定要 grep 一遍**所有**引用点(同 P0-3 的"改一处漏一处")。 # ③ 只排 `once`(⛔ recurring 永远不该被"过期排除") only_once = 'str(_st or "").lower() == "once"' in _c # ④ 🔴 2026-10-03 12:0x **语义已改**:`next_run_at` 为 NULL 的 `once` 排期 # **也要排除** —— 实测宿主在排期**到点触发后会把 `next_run_at` 清成 NULL**, # 而 `status` 仍 `ACTIVE`、`last_run_at` 仍 `None` # ⇒ 我上一版写"NULL ⇒ 仍算待执行(别误杀)"**是错的** ⇒ 那样它照样永��卡闸④。 # (第五种情形:(a) 已过宽限 / (b) 已被消费 ⇒ 两者都排除。) null_excluded = "if str(_st or \"\").lower() == \"once\":" in _c and "_gone = (not _na)" in _c # ⑤ 真库现读:造一条过期 once 也不能把**别人**的排期算进来(⛔ 只验判据形状,不动生产) live = m.ws_pending_schedules() return [ ("① 闸④ 查询**取到** `schedule_type` 与 `next_run_at` 两列", two_cols), ("② 过期宽限 = 15 分钟(实测 %s 分钟;⛔ 写 0 会放行『马上要开』的排期)" % limit_min, 10 <= limit_min <= 30), ("③ 只排 `once`(⛔ `recurring` 会再触发,永不按过期排除)", only_once), ("④ `once` 且 `next_run_at` 为 NULL(已被宿主消费)⇒ **也排除**", null_excluded), ("⑤ 判据现读不抛(ws_pending_schedules() = %s)" % (live if live else "[]"), True), ] @case("看板 tab:**跨工作区**并列查看(⛔ 原本只认一个 INBOX,天生看不见别的目录)") def t_tab_peer_workspace(): """🔴 2026-10-03 15:44x(用户报障逐字): > 「为什么 会话协作看板 tab 选项不能切换看另外两个工作区的目标」 🔴 **根因**:`goal_files()` 扫的是 `INBOX/goal.json` + `INBOX/goals/*.json` —— **两者都在同一个 INBOX 下**,而 `INBOX` 由部署配置的 `workspace` 决定 ⇒ 一个 `board.py --serve` 实例**天生只看见一个工作区**(注释里也明写"同一个工作区")。 ✅ 修法:配置新增 `peer_workspaces`(要并列查看的其它工作区)⇒ 读它们的 `goal.json` 进 tab。 🔴 **严格只读**这条边界必须钉住:⛔ 不写对方文件、⛔ 不起对方进程、⛔ 不改对方状态。 🔴 2026-10-04:本条要**现读生产配置**的 `peer_workspaces` ⇒ 在**技能副本**里跑, 那个工作区**没启用机制**(`vibe-product` 实测)⇒ peer 必为空 ⇒ 0 格 ⇒ 报红。 ⇒ 未启用时**跳过**(⛔ 不许改成"永远绿")。 """ if not MECH_ON: return [("⏭ 本工作区**未启用**会话机制 ⇒ 跨区 peer 配置无从谈起,本条不适用", True)] import inspect as _ins import re as _re m = imp() spec = _ins.getsource # 占位(下面直接读文本) src_b = (HERE / "board.py").read_text(encoding="utf-8") src_h = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") # ① 函数在位 has_fn = "_peer_workspaces" in src_b # ② 排除本工作区(⛔ 否则活跃目标出现两格) excludes_self = 's == str(WS or "").replace(' in src_b # ③ 去重键带工作区前缀(⛔ 否则同名目标互相顶掉) key_prefixed = 'k = "%s/%s" % (ws_name, k)' in src_b # ④ _goal_block 透传 peer(⛔ 上游加了下游不带 ⇒ 前端永远看不到) block_carries = '"peer": peer' in src_b call_passes = 'it.get("peer") or ""' in src_b # ⑤ 形参必须存在(⛔ 少了 ⇒ TypeError ⇒ 快照生不出 ⇒ 看板一直 warming —— 本轮实测踩到) # 🔴 判据=**只扫 `_goal_block()` 的签名**(⛔ 别用裸子串:命中注释/docstring 就变假绿) _sig_m = _re.search(r"def\s+_goal_block\s*\(([^)]*)\)", src_b, _re.S) _sig = " ".join((_sig_m.group(1) if _sig_m else "").split()) sig_ok = ("peer" in _sig and "ws_root" in _sig) sig_txt = "_goal_block(%s)" % (_sig[:60] or "!! 找不到该函数") # ⑥ 前端显示 peer 标记 # ⚠️ 2026-10-03 20:30x 判据跟着改版走:peer 标记 20:2x 从**第一行徽章**挪到了**第二行** # (用户要的两行式:第一行=目标名+目标状态,第二行=工作区名+判据状态)⇒ 字面量从 `g.peer?` 变 `g.peer||`。 # 🔴 **剥掉注释再查**(20:2x 踩过:注释里留着旧写法 ⇒ 判据被注释骗、变异不报红)。 _h_nc = re.sub(r"/\*.*?\*/", "", src_h, flags=re.S) _h_nc = re.sub(r"//[^\n]*", "", _h_nc) front_shows = ("g.peer||" in _h_nc) or ("g.peer?" in _h_nc) # ⑦ 只读取向:源码里 ⛔ 不能有写对方文件的调用 no_write = not _re.search(r"_root\s*/\s*\"tmp\".*\.write_text", src_b) # ⑧~⑩ **现网真读数** —— 🔴 **⛔ 不能沿用 `imp()` 注入的测试环境**: # `imp()` 把 `COLLABD_CONFIG`/`DSH_COLLAB_WS` 指到 `tmp/selftest/` ⇒ board 只看见测试那 1 格 # ⇒ 断言写什么都只是**假象**(2026-10-03 实测:命令行直接加载=3 格,用例内=1 格)。 # ⇒ 正确姿势:**真磁盘上造两个工作区** + **干净环境**加载 board 现读(P0-13:判据要能证伪)。 import tempfile def _load_board(cfg_p, ws_env, tag): """在**指定配置**下加载 `board.py`(⛔ 用完还原 env,防污染后续用例)。""" _old = {k: os.environ.get(k) for k in ("COLLABD_CONFIG", "DSH_COLLAB_WS")} try: os.environ["COLLABD_CONFIG"] = str(cfg_p) if ws_env is None: os.environ.pop("DSH_COLLAB_WS", None) else: os.environ["DSH_COLLAB_WS"] = str(ws_env) _u = __import__("importlib.util", fromlist=["util"]) _sp = _u.spec_from_file_location(tag, HERE / "board.py") _mm = _u.module_from_spec(_sp) _sp.loader.exec_module(_mm) return _mm finally: for _k, _v in _old.items(): if _v is None: os.environ.pop(_k, None) else: os.environ[_k] = _v def _dump(p, obj): Path(p).write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8") def _mk_ws(root, gid, title): """真磁盘上造一个**最小工作区**(只含 `tmp/supervise-inbox/goal.json`)。""" _r = Path(root) / gid (_r / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True) _dump(_r / "tmp" / "supervise-inbox" / "goal.json", {"id": gid, "title": title, "short": gid}) return _r _td = tempfile.mkdtemp(prefix="selftest-peer-") # ⛔ 不用 TEST_WS(会被别的用例清掉) try: _a = _mk_ws(_td, "wsA", "A 区目标") _b = _mk_ws(_td, "wsB", "B 区目标") # 🔴🔴 2026-10-04 21:4x **夹具必须补上「这个 peer 还在会话列表里」这个前提**。 # 病根(我当天加的过滤打红了本条):`board.goal_files()` 现在会**跳过"未删会话为 0"的 peer** # (用户口径:「tab 要把已经不再 会话列表的工作区 目标移除」)。 # 而 `_mk_ws()` 只在真磁盘造 `goal.json`,**宿主库里没有 wsB 的任何会话** # ⇒ `_peer_session_rows()` 返回空 ⇒ wsB **被正确地过滤掉** ⇒ `goal_files()`=1 格 ⇒ 本用例 FAIL。 # ⚠️ **被测代码行为是对的,是夹具在说谎**(= P0-53 同一个形状:合成区必须让被测对象 # 能读到真实判据,⛔ 不许靠改断言期望值把它"修绿")。 # ⇒ 正解两条(都必须): # ① 造一个**最小的宿主库**(只有 `sessions` 表 + 本用例要用的列), # wsB 那条 `cwd` 精确等于 `_b` ⇒ `_peer_session_rows(_b)` 有 1 条 ⇒ 不被过滤。 # ② 配置里用 **`host_db`** 指过去 —— `board.py::_host_db()` 优先读它 # (⛔ 别去改 `CODEBUDDY_CONFIG_DIR`:那会把真库暴露给用例,且污染后续用例)。 # ⚠️ 库要**留在 `_td` 里**(随 `shutil.rmtree` 一起回收),⛔ 不落靠 `tempfile` 的全局清理。 _db = Path(_td) / "fake-host.db" # ⚠️ `mkdtemp()` 返回的是 **str** ⇒ 必须包 `Path()` try: import sqlite3 as _sq _cn = _sq.connect(str(_db)) _cn.execute("create table sessions (id text primary key, title text, custom_title text, " "status text, cwd text, updated_at integer, last_activity_at integer, " "deleted_at integer)") _cn.execute("insert into sessions values (?,?,?,?,?,?,?,?)", ("sid-wsB-1", "B 区主会话", "", "working", str(_b).replace("\\", "/"), int(time.time() * 1000), int(time.time() * 1000), None)) _cn.commit() _cn.close() except Exception as _e: # pragma: no cover pass # 造库失败 ⇒ 走下面 live_ok 的如实报错,⛔ 不假装通过 _c_on = _a / "tmp" / "peer.on.json" _dump(_c_on, {"workspace": str(_a).replace("\\", "/"), "inbox": "tmp/supervise-inbox", "host_db": str(_db).replace("\\", "/"), "peer_workspaces": [str(_b).replace("\\", "/")]}) gs1 = _load_board(_c_on, _a, "bd_peer_on").goal_files() p1 = [g.get("peer") for g in gs1 if g.get("peer")] a1 = [g for g in gs1 if g.get("active")] live_ok = (len(gs1) == 2 and p1 == ["wsB"] and len(a1) == 1) live_txt = "登记 1 个 peer ⇒ goal_files()=%d 格(peer=%s,active %d 个)" % ( len(gs1), p1, len(a1)) # ⑨ **变异对照**(🔴 判据必须可证伪:去掉 `peer_workspaces` ⇒ 必须回落到 1 格) _c_off = _a / "tmp" / "peer.off.json" _dump(_c_off, {"workspace": str(_a).replace("\\", "/"), "inbox": "tmp/supervise-inbox"}) gs0 = _load_board(_c_off, _a, "bd_peer_off").goal_files() p0 = [g.get("peer") for g in gs0 if g.get("peer")] mut_ok = (len(gs0) == 1 and not p0) mut_txt = "去掉 peer_workspaces ⇒ %d 格(peer %d 个)" % (len(gs0), len(p0)) # ⑪ 🔴 **变异对照 ②:把"该 peer 的会话删光" ⇒ 它必须从 tab 掉出去** # (守 2026-10-04 用户口径「tab 要把已经不再 会话列表的工作区 目标移除」) # ⚠️ 这一条是本轮新加的过滤的**证伪项**:⛔ 没有它,那个过滤写错了也没人发现 # (同族:判据必须能证伪 ⇒ 打掉接线必须报红)。 _db2 = Path(_td) / "fake-host2.db" # ⚠️ 同上,必须包 `Path()` try: import sqlite3 as _sq2 _cn2 = _sq2.connect(str(_db2)) _cn2.execute("create table sessions (id text primary key, title text, custom_title text, " "status text, cwd text, updated_at integer, last_activity_at integer, " "deleted_at integer)") # ⚠️ **只有已删会话**(`deleted_at` 非空)⇒ 等价于"用户把会话全删了" _cn2.execute("insert into sessions values (?,?,?,?,?,?,?,?)", ("sid-wsB-del", "B 区(已删)", "", "completed", str(_b).replace("\\", "/"), int(time.time() * 1000), int(time.time() * 1000), int(time.time() * 1000))) _cn2.commit() _cn2.close() except Exception: # pragma: no cover pass _c_del = _a / "tmp" / "peer.del.json" _dump(_c_del, {"workspace": str(_a).replace("\\", "/"), "inbox": "tmp/supervise-inbox", "host_db": str(_db2).replace("\\", "/"), "peer_workspaces": [str(_b).replace("\\", "/")]}) gs2 = _load_board(_c_del, _a, "bd_peer_del").goal_files() p2 = [g.get("peer") for g in gs2 if g.get("peer")] gone_ok = (len(gs2) == 1 and not p2) gone_txt = "peer 会话全删 ⇒ %d 格(peer %s)" % (len(gs2), p2) finally: shutil.rmtree(_td, ignore_errors=True) # ⑩ 生产侧:⛔ **不硬编码路径** —— 指不到真工作区 ⇒ SKIP(⛔ 不假装通过) # 🔴🔴 2026-10-05 修一处**恒红判据**(实测 `PASS 96/FAIL 1` → `95/2`,多出的正是本条的 FAIL): # 老写法 `prod_ok = (_n >= 1 and len(pp) == _n and len(ap) == 1)` 要求 # **登记几个 peer 就必须出几格**。可是 peer 工作区**没建目标时本来就不该出格** —— # 同一组判据的第 ⑪ 条(`gone_ok`)写的正是这个语义("peer 已不在会话列表 ⇒ 不占 tab"), # **两条自相矛盾**:⑩ 要 `len(pp)==_n`,⑪ 又认可少出格。 # ⇒ 实测现场:ai1net 登记了 2 个 peer(`vibe-product` ✅ 有 `goal.json` / # `agent-product` ❌ 还没建目标)⇒ `_n=2` 但 `len(pp)=1` ⇒ **必红**。 # ⚠️ 这是"恒红比漏网更坏"的又一实例:只要有任何 peer 区没活跃目标,基线就永远红 # ⇒ 之后跑任何变异都"看着有红",验证作废。按纪律③「基线先全绿」必须先修它。 # 正解=判**包含关系 + 上限**,⛔ 不是判**数量相等**: # · 出格数 ≤ 登记数(⛔ 出格数超登记 ⇒ 有来源不明的格,真问题) # · 每一格都必须在登记表里(⛔ 否则是把别区数据挂到本区名下) # · 本区活跃目标恰好 1 个(⛔ 多/少都说明看板认错了 active) # ⚠️ 放宽后**仍可证伪**:① 把某格来源改成本区 ⇒ `pp_set ⊆ reg` 失败; # ② 篡改出格数 > 登记数 ⇒ 上限失败;③ active 改 0/2 ⇒ 失败。均有变异可抓。 _pcfg = WS / ".workbuddy" / "collab" / "collabd.config.json" if not _pcfg.is_file(): prod_ok, prod_txt = True, "SKIP:读不到生产配置(%s)" % _pcfg else: try: # ⚠️ 两侧粒度不同,⛔ 不能直接比字符串: # · `collabd.config.json` 的 `peer_workspaces` = **全路径**(`E:/ProgramData/AIProject/vibe-product`) # · `goal_files()` 出的 `peer` 字段 = **短名**(`vibe-product`,看板 tab 就用它做标题) # 第一版直接 `set(pp) <= set(_reg)` ⇒ 恒 False(`vibe-product` ≠ 全路径)⇒ **判据写成了恒红**。 # 正解:把登记的全路径**取 basename** 再比。 _reg_paths = [str(x).replace("\\", "/") for x in ((json.loads(_pcfg.read_text(encoding="utf-8")) or {}).get( "peer_workspaces") or [])] _reg = [p.rstrip("/").rsplit("/", 1)[-1].lower() for p in _reg_paths] _n = len(_reg) gp = _load_board(_pcfg, None, "bd_peer_prod").goal_files() pp = [str(g.get("peer")).rstrip("/").rsplit("/", 1)[-1].lower() for g in gp if g.get("peer")] ap = [g for g in gp if g.get("active")] # 出格数 ≤ 登记数(peer 没建目标 ⇒ 少出格=正常)∧ 每格都在登记表里 ∧ active 恰 1 prod_ok = (_n >= 1 and len(pp) <= _n and set(pp) <= set(_reg) and len(ap) == 1) _miss = [r for r in _reg if r not in set(pp)] prod_txt = ("生产配置登记 %d 个 peer ⇒ %d 格(peer=%s,active %d)" "|未出格(无活跃目标)=%s" % (_n, len(gp), pp, len(ap), _miss)) except Exception as e: # pragma: no cover prod_ok, prod_txt = False, "抛异常:%s" % e return [ ("① 有 `_peer_workspaces()`(配置驱动的并列查看)", has_fn), ("② **排除本工作区**(⛔ 否则活跃目标出现两格)", excludes_self), ("③ 去重键带**工作区前缀**(⛔ 同名目标互相顶掉)", key_prefixed), ("④ `_goal_block()` **透传** peer(⛔ 上游加了下游不带=前端看不到)", block_carries), ("④ 调用点按位置传入 peer", call_passes), ("⑤ `_goal_block` 形参含 peer/ws_root(⛔ 少了 ⇒ TypeError ⇒ 看板 warming):%s" % sig_txt, sig_ok), ("⑥ 前端 tab 显示 peer 标记(⛔ 否则三格标题一样,读者分不清)", front_shows), ("⑦ **严格只读**(⛔ 源码里不得有写对方文件的调用)", no_write), ("⑧ **现网真读数**(真磁盘两工作区+干净环境):%s" % live_txt, live_ok), ("⑨ **配置缺省 ⇒ 回落 1 格**(向后兼容;⛔ 它只守「配置可关」这一侧,抓不住代码坏 ⇒ 证伪看 ⑧):%s" % mut_txt, mut_ok), ("⑩ 生产侧按生产配置现读:%s" % prod_txt, prod_ok), ("⑪ **peer 已不在会话列表 ⇒ 不占 tab**(用户 2026-10-04 口径;⛔ 判据是**未删会话数**,不是心跳):%s" % gone_txt, gone_ok), ] @case("跨工作区 peer 格:⛔ **不得吃本工作区数据**(吃了=把本区执行情况挂到别人名下=说假话)") def t_peer_block_no_self_data(): """🔴 2026-10-03 16:0x 用户报障,逐字:「**选择另一个工作区目标 tab 下面没有显示对应工作区目标和执行情况**」 🔴🔴 **实测真因(比"没显示"更糟 —— 看板在说假话)**:`build()` 里的 `tasks`/`srows`/`st` **只取一次、所有格共用** ⇒ 三格的 `labor`/`sessions`/`progress` **md5 完全相同** ⇒ 切到 peer 那格,看到的其实是**本工作区**的执行情况,只是标题挂着对方工作区的名字。 ✅ 修法:peer 格只喂 `_peer_tasks()`/`_peer_srows()`/`_peer_state()` —— **只读对方目录下的文件**; 读不到 ⇒ **空 + 界面如实说明**,⛔ 绝不拿本工作区的数据补位。 🔴 **判据可证伪**:把 `_peer_tasks(_wr)` 换回本区的 `tasks` ⇒ ⑤ **必然报红**(两侧 tasks 变相同)。 """ import tempfile src = (HERE / "board.py").read_text(encoding="utf-8") # ① peer 分支喂的是对方数据源 feeds_peer = ("_peer_tasks(_wr)" in src and "_peer_state(_wr)" in src and "_peer_srows(srows, _wr)" in src) # ② 补捞(⛔ 没有 ⇒ 全库最近 50 条里捞不到对方会话 ⇒ 显示 0 条=假象) has_backfill = "_peer_session_rows" in src # ③ peer 格的作用域要换到对方工作区 scope_swaps = 'sc["workspace"] = ws_root' in src # ④ 本区切分基准只按本区目标算 own_only = "if not it.get(\"peer\")" in src # ⑤ 真读数(本区有台账 / 对方无 ⇒ peer 格必须为空,且 ⛔ 不得等于本区的) _td = tempfile.mkdtemp(prefix="selftest-peerblk-") live_ok, live_txt = True, "SKIP:未跑" try: A = Path(_td) / "wsA" B = Path(_td) / "wsB" for _r, _gid in ((A, "wsA"), (B, "wsB")): (_r / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True) (_r / "tmp" / "supervise-inbox" / "goal.json").write_text( json.dumps({"id": _gid, "title": _gid + " 目标", "short": _gid, "topics": [_gid]}, ensure_ascii=False), encoding="utf-8") (A / "tmp" / "supervise-inbox" / "tasks.json").write_text( json.dumps({"S1": {"state": "done", "line": "wsA"}}, ensure_ascii=False), encoding="utf-8") cfg = A / "tmp" / "cfg.json" cfg.write_text(json.dumps({"workspace": str(A).replace("\\", "/"), "inbox": "tmp/supervise-inbox", "peer_workspaces": [str(B).replace("\\", "/")]}, ensure_ascii=False), encoding="utf-8") _old = {k: os.environ.get(k) for k in ("COLLABD_CONFIG", "DSH_COLLAB_WS")} try: os.environ["COLLABD_CONFIG"] = str(cfg) os.environ["DSH_COLLAB_WS"] = str(A) _u = __import__("importlib.util", fromlist=["util"]) _sp = _u.spec_from_file_location("bd_peerblk", HERE / "board.py") bd = _u.module_from_spec(_sp) _sp.loader.exec_module(bd) try: gs = (bd.build() or {}).get("goals") or [] except Exception as e: # pragma: no cover gs, live_txt = [], "SKIP:build() 抛异常(%r)" % e if gs: own = [g for g in gs if not g.get("peer")] per = [g for g in gs if g.get("peer")] ta = (own[0].get("tasks") if own else None) or {} tb = (per[0].get("tasks") if per else None) or {} same = (json.dumps(ta, sort_keys=True, ensure_ascii=False) == json.dumps(tb, sort_keys=True, ensure_ascii=False)) live_ok = bool(ta) and (not tb) and (not same) live_txt = "本区 tasks=%d 件 / peer tasks=%d 件 / 两者相同?%s" % ( len(ta), len(tb), same) finally: for _k, _v in _old.items(): if _v is None: os.environ.pop(_k, None) else: os.environ[_k] = _v finally: shutil.rmtree(_td, ignore_errors=True) return [ ("① peer 分支喂的是**对方**数据源 `_peer_tasks/_peer_srows/_peer_state`(⛔ 不是本区 tasks/srows/st)", feeds_peer), ("② 有 `_peer_session_rows()` **补捞**(⛔ 没有 ⇒ 全库最近 50 条捞不到对方会话 ⇒ 显示 0 条=假象)", has_backfill), ("③ peer 格把作用域换到**对方工作区**(⛔ 不换 ⇒ `_sessions()` 的 cwd 硬过滤把对方会话全排除)", scope_swaps), ("④ 本区切分基准**只按本区目标**算(⛔ 混入 peer topics ⇒ 台账件两边都不进 ⇒ 静默丢失)", own_only), ("⑤ **真读数**(本区有台账/对方无 ⇒ peer 格必须为空且 ≠ 本区):%s" % live_txt, live_ok), ] @case("跨工作区 tab:**有差异的字段必须逐个被视图取到**(⛔ 漏一个 ⇒ 静默沿用本区 ⇒ 几个 tab 看起来一样)") def t_view_takes_all_scoped_fields(): """🔴 2026-10-03 20:2x 用户报障(逐字):「**为什么不同目标,协作程序框图里面的内容是一样的**」 🔴🔴 **真因(实测,不是猜)**:服务端**数据是对的**(本区块内 `sessions_checks=4` 条、 两个 peer 块各 0 条),但前端 `scopeView()` 合并视图时**只显式取了 7 个字段** ⇒ 其余字段**原封不动沿用顶层 `d`**(=**本工作区**那份) ⇒ 切 tab 时「协作程序状态/检查状态/检查会话/完成情况」**全都显示同一个工作区的**。 🔴 **这是我今天第三次踩「加字段必须顺着数据流查到消费点」**(前两次:加了没渲染 / 判据写死字面量)⇒ **光靠"记得改"已经不管用了**。 ✅ **本条判据是数据驱动的**:跑一次真 `build()` ⇒ 逐字段比较两块 ⇒ **凡"两块有差异"而"视图没取"的字段一律报红** ⇒ 以后新增字段漏了会被自动抓住。 """ import re as _re2 _root = os.environ.get("DSH_COLLAB_WS") or os.environ.get("DSH_WS_ROOT") or "" _pcfg = (Path(_root) / ".workbuddy" / "collab" / "collabd.config.json") if _root else None if not _pcfg or not _pcfg.is_file(): return [("SKIP:没指到真工作区(跑的时候设 `DSH_COLLAB_WS=<工作区>` 才会真跑)", True)] _old = {k: os.environ.get(k) for k in ("COLLABD_CONFIG", "DSH_COLLAB_WS")} try: os.environ["COLLABD_CONFIG"] = str(_pcfg) os.environ.pop("DSH_COLLAB_WS", None) _u = __import__("importlib.util", fromlist=["util"]) _sp = _u.spec_from_file_location("bd_viewchk", HERE / "board.py") bd = _u.module_from_spec(_sp) _sp.loader.exec_module(bd) gs = (bd.build() or {}).get("goals") or [] if len(gs) < 2: return [("SKIP:本工作区只登记了 1 个目标(⛔ 比不出差异)", True)] a, b2 = gs[0], gs[1] _IDENT = {"key", "peer", "ws_root", "active", "life", "life_at", "life_by", "peer_scope"} # 🔴🔴 2026-10-05 12:0x 归位两个**后端排序键**(实测取证,不是为了让判据变绿而塞进豁免): # `tab_created`=该块的 `declared_at`(排序键)、`tab_i`=排序后重编的 tab 序号 # —— 二者由 `board.py` 在 `blocks.sort(key=…)` 里**当场用掉**, # 前端 `board.html` 对它们的**消费点命中数 = 0**(它们只用于后端排序)⇒ **不是视图字段**, # ⛔ 要求 `scopeView()` 去取,等于要求视图去取一个它根本不渲染的东西。 # ⚠️ 反向自证:判据**仍然有牙** —— 本次真漏的 `ws_name`(渲染点 `renderProject()` L660 读 # `d.ws_name`)**不在**此表内,修它之前判据照红(见变异对照)。 _IDENT |= {"tab_created", "tab_i"} _diff = [k for k in sorted(set(a.keys()) | set(b2.keys())) if k not in _IDENT and json.dumps(a.get(k), sort_keys=True, ensure_ascii=False) != json.dumps(b2.get(k), sort_keys=True, ensure_ascii=False)] # 🔴 提取赋值清单前**先剥掉注释** —— ⛔ 否则"把某行注释掉"这种变异**照样匹配得到** # (本轮实测踩到:注释掉 `v.sessions_checks=` 之后判据仍报绿 ⇒ 判据被注释骗了)。 _html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") _html = _re2.sub(r"/\*.*?\*/", "", _html, flags=_re2.S) _html = _re2.sub(r"//[^\n]*", "", _html) _fn = _re2.search(r"function scopeView\(d,g\)\{.*?\n\}", _html, _re2.S) _taken = set(_re2.findall(r"v\.(\w+)\s*=", _fn.group(0))) if _fn else set() _missing = [k for k in _diff if k not in _taken] return [("两块有差异的字段 %d 个,**视图必须逐个取到**(⛔ 漏了会静默沿用本区):%s" % (len(_diff), ("全部已取 ✔" if not _missing else "⛔ 漏了 %s" % _missing)), not _missing)] except Exception as _e: # pragma: no cover return [("用例异常:%r" % _e, False)] finally: for _k, _v in _old.items(): if _v is None: os.environ.pop(_k, None) else: os.environ[_k] = _v @case("每个 hook 都能真正跑起来(⛔ 不许一导入就崩)") def t_hooks_actually_runnable(): """🔴🔴 2026-10-03 **实测抓到的真缺陷**(用户报障「协作程序相关的连线 有用的保留 没用的 可以去掉」,我逐条去核实"这三条线还活着吗"时撞出来的): ⚠️ **事故链**:用户 10-02 报「后台一闪一闪」⇒ 为根治闪屏,给 `wb-result-hook.py` 加了 `_win_pythonw()`,写成 `PYW = _win_pythonw()`,而**函数体内部引用 `PYW`** ⇒ 调用时 `PYW` 尚未赋值 ⇒ **模块导入即 `NameError`** ⇒ 该 hook 挂的**三个事件(PreToolUse/UserPromptSubmit/SessionEnd)全部静默废掉** ⇒ 图上「钩子 → 协作程序 --once/--tick」两条线**其实早就断了,只是没人发现** (`hook.log` 停在 10-02 21:24、三个 stamp 也停在同一分钟 —— 三处独立证据互相印证)。 🔴 **为什么这条判据必须存在**:⛔ 语法检查(`ast.parse`)**照样通过** —— `NameError` 是**运行期**错误,语法完全合法。⇒ 「能解析」≠「能跑」, 光靠语法自检永远抓不到这类缺陷。而它造成的后果是**整条链路静默失效**, 看板上的连线会一直画着、看着一切正常。 ✅ **本条判据=真的把每个 hook 起一次**(喂最小合法 stdin),断言 `rc=0` 且无 traceback。 ⚠️ 起法要点:⛔ 必须用 `sys.executable`(当前解释器)而**不是** `python`(PATH 可能指向别处); 喂完**必须等它自己跑完**(hook 会 spawn 后台子进程,不能立刻判)。 """ import subprocess as _sp2 import tempfile as _tf2 # 🔴 **只测真 hook**:目录里的 `*.bak-*` 是历史备份,⛔ 不是在跑的钩子 # (它们恰好也都能起来 ⇒ 混进来只是给输出添噪音,且会让人误以为有 12 个钩子)。 hooks = sorted(p for p in (HERE / "hooks").glob("*.py") if ".bak-" not in p.name) out = [] for hk in hooks: payload = json.dumps({"session_id": "selftest-probe", "cwd": str(TEST_WS), "hook_event_name": "UserPromptSubmit", "prompt": "selftest probe"}) try: r = _sp2.run([sys.executable, str(hk)], input=payload.encode("utf-8"), capture_output=True, timeout=60, cwd=str(TEST_WS)) except Exception as exc: out.append(("%s 起不来:%r" % (hk.name, exc), False)) continue err = (r.stderr or b"").decode("utf-8", "replace") # 🔴 两个判据缺一不可:退出码 0 **且** stderr 里没有 traceback # (有的脚本出错仍返回 0,只看 rc 会漏)。 out.append(("%s 起得来(rc=%d%s)" % ( hk.name, r.returncode, ",stderr 有 traceback:%s" % err.strip().splitlines()[-1] if "Traceback" in err else ""), r.returncode == 0 and "Traceback" not in err)) return out or [("⛔ hooks 目录里一个 hook 都没有 ⇒ 判据本身失效", False)] @case("架构图:每条线的**两端都落在真实框上**(⛔ 不许线头悬空)") def t_board_edges_land_on_boxes(): """🔴🔴 2026-10-03 用户红框圈出一条线问「**这跟红框标注的线到底有没有用 有用就连对目标,没用就删除了**」—— 取证发现它**既没用不了、也没连对**: 🔴 **病根=把「与框宽有关的 x」写死**:④ 那条线写死 `x=340`,而它成立的前提是 当时 `PX=280`、宽 580。后来框**收窄到 470** 且改成**跟随画布中心** ⇒ 框变 405..875 ⇒ 340 掉到框外**左侧 65px** ⇒ 箭头悬空。 🔴 **同一个病 09-30 修过一次**(当时原话「WorkBuddy 左边那根线是要连到哪里?」) —— 当时**只改了线的位置、没把它与框宽绑死** ⇒ 框一变就复发。 ⇒ 教训:**⛔ 凡是"与可变量(框宽/框位)有关"的坐标,一律现算,别写死。** ✅ **本条判据=把源码里那几个常量取出来重算一遍**,逐条断言: ① ④ 出口 `RX` **必须落在常驻程序框内**(`PX < RX < PX+PW`)—— 这条就是"连对目标"; ② `RX` **必须小于 `hkX`** —— 否则会**穿过** Hook 框("连过去却穿框"同样是错); ③ `--once`/`--tick` 两个出口**必须落在 Hook 框顶边之内**; ④ 四个出口**两两不重叠**(重叠=两条线并成一条、看不出谁是谁)。 ⚠️ 取常量方式:**从源码正则抽**(⛔ 不另写一份副本)—— 副本会与源码漂移, 那样这条判据就变成了"检查我自己写的数"(同族陷阱:判据必须扎在真源上)。 ⚠️ 已知同源缺陷(本条**抓不到**,⛔ 别以为它是万能的):源码里若有人把 `hkX=430` 又写回 `hkX=400`,而 `RX` 仍由 `PX+24` 现算 ⇒ 两者同时满足本条 ⇒ 但**观感上 ④ 仍穿框**。 ⇒ 那属于"把条件写复杂了",根治办法是**保持 `RX < hkX` 恒成立**(别靠加更多分支)。 """ import re as _re3 src = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") # 🔴🔴 2026-10-03 **又踩一次「朴素扫描被注释骗」**(同族坑,见 P0-36): # ④ 那段注释里**逐字写着** `RXV=PX-40`/`RXH=PX+28` ⇒ 正则先命中注释 # ⇒ 抽出来是一整段散文 ⇒ `eval` 失败 ⇒ 判据整条失效。 # ✅ **正解=先剥注释再抽**(顺序不能反):块注释 → 行注释 → 再找赋值。 _code = _re3.sub(r"/\*.*?\*/", "", src, flags=_re3.S) _code = _re3.sub(r"(?m)^\s*//.*$", "", _code) # 只吃**整行**注释(行尾注释常跟在代码后) _code = _re3.sub(r"//[^\n]*", "", _code) def _n(pat, dflt=None, hay=None): m = _re3.search(pat, hay if hay is not None else _code) return float(m.group(1)) if m else dflt CX = _n(r"var CX=(\d+)") PW = _n(r"var PW=(\d+)") # 🔴 `PX` 源码里是**表达式** `CX-PW/2`(与 PW 同一行声明)⇒ ⛔ 不能按纯数字抽。 PX = (CX - PW / 2) if (CX is not None and PW is not None) else None hkX = _n(r"var hkX=(\d+)") hkW = _n(r", hkW=(\d+)") if None in (CX, PW, PX, hkX, hkW): return [("⛔ 抽不出几何常量(CX/PW/hkX/hkW 实得 %s)⇒ 判据本身失效,⛔ 别当通过用" % (CX, PW, hkX, hkW), False)] # 🔴🔴 2026-10-03 **自查抓到的一个真漏洞(本条第一版栽在这)**: # 第一版 RX 在判据里**自己重算** `PX+24` ⇒ 它检查的是「我算的数」, # **不是「源码里那个数」** ⇒ 把源码改成 `var RX=340;`(原事故)它照样报绿。 # 🔴 这就是判据的经典自欺:**判据与被检对象各算一遍,必然有一份是假的**。 # ✅ **正解=判据只认源码里的真实写法**:抽 `var ...=` 后面到底是**表达式**还是**字面量**, # 若是字面量**直接报红**(那正是"写死与框宽有关的坐标"这个病根本身)。 # 🔴🔴 2026-10-03 **三改=④ 变折线后本条同步**(用户原话:「**稍微向左边折一下再连过去**」 # +「**还把 hook 图框挤到旁边 多难看**」): # ④ 不再是"一条竖直线",而是 `RXV`(框外左侧竖直段)→ `RYY`(折点)→ `RXH`(**框内**终点)。 # ⇒ **竖直段看 `RXV`、终点看 `RXH`**,两个都要查; # ⚠️ 且新增一条**反向判据**:`hkX` 必须是**它自己的版面值**(⛔ 不许被线牵着走)—— # 上一版我为腾位置把它 400→430,用户明确指正 ⇒ 这条写进判据防复发。 res = [] def _expr(name, env=None): # 🔴 `var RXV=…, RXH=…, RYY=…` 三个变量**在同一行逗号声明** ⇒ # ① 只有**第一个**变量带 `var`,后两个没有 ⇒ 正则要写成「`var NAME=`」**或**「裸 `NAME=`」 # (⚠️ 只写 `var %s=` 会漏掉 `RXH`/`RYY`,实测踩到); # ② 取值必须止于**下一个逗号或分号**,⛔ 否则 `var RXV=PX-40, RXH=…` 会把后面一起吞进来。 m = _re3.search(r"\b(?:var\s+)?%s=([^,;]+)" % _re3.escape(name), _code) if not m: res.append(("⛔ 源码里抽不到 `var %s=` ⇒ 判据失效,⛔ 别当通过用" % name, False)) return None, None e = m.group(1).strip() lit = _re3.fullmatch(r"(\d+(?:\.\d+)?)", e) if lit: res.append(("⛔ 源码 `var %s=%s` 是**写死字面量**(病根:框一改它就落到框外)" " ⇒ 必须由 PX 现算" % (name, e), False)) try: # 🔴 源码里 `R4.y` / `R4.h` 是**属性访问**写法 ⇒ eval 环境不能只给 dict, # 否则 `R4.y+(R4.h/2)` 会 `AttributeError`(实测踩到 ⇒ 整条判据失效)。 # ✅ 两种都支持:dict 下标(`R4["y"]`)与属性(`R4.y`)——用一个既支持点又支持下标的类型。 class _Box(dict): __getattr__ = dict.__getitem__ v = float(eval(e, {"__builtins__": {}}, # noqa: S307 {"PX": PX, "PW": PW, "CX": CX, "R4": _Box({"y": R4Y, "h": R4H})})) except Exception: res.append(("⛔ `var %s=%s` 求值不了 ⇒ 判据失效,⛔ 别当通过用" % (name, e), False)) return None, e return v, e R4Y, R4H = _n(r"R4=\{y:(\d+)"), _n(r"h:(\d+)\}, R5") if R4Y is None or R4H is None: return res + [("⛔ 抽不到常驻程序行几何 `R4={y:...,h:...}` ⇒ 判据失效", False)] RXV, _ = _expr("RXV") RXH, _ = _expr("RXH") RYY, _ = _expr("RYY") once_x = hkX + 40 tick_x = PX + PW / 2 + 120 if RXV is None or RXH is None or RYY is None: return res res += [ # 🔴🔴 2026-10-03 **判据跟着改口径**(用户原话:「**线连到左边不行 非的连到下边 # 是咋想的**」):④ 终点现在**落在框的左边框上**(`RXH==PX`), # ⛔ 不再是"框内某点"(旧判据 `PX= 30)) return res @case("目标完成 ⇒ 真的收工(⛔ 不许只改状态不关进程);阻碍 ⇒ 不关") def t_goal_done_stops_supervise(): """🔴🔴 2026-10-03 **接上用户口径第①条**(逐字): 「**目标完成后需要把目标状态改为 完成,关闭后台任务 和 协作程序 (有新目标或之前的目标有调整 在打开)**」; 第②条:「**目标 遇到阻碍** …(**不用关闭后台任务和协作程序**)」。 🔴 **改前取证**:`goal_life_set()` 原来**只写 `goal.json` 四个字段就 `return`**, 全文无 `stop`/`kill` 调用 ⇒ "完成即收工"**在代码里根本没实现**。 ✅ 本条判据查三件事(**源码级**,⛔ 不靠"跑一遍看看"): ① `supervise_stop()` **存在**(有停的手段); ② `goal_life_set()` 在 `life == GOAL_LIFE_DONE` 分支**真的调它**; ③ `ensure_supervise()` **会清 `guard.stop`** —— 🔴 这条最容易漏: 停止标志是常驻的退出条件,不清就**永远起不来**("完成"变成不可逆,与口径相反)。 ⚠️ 判据一律**剥注释后**匹配(注释里写着"关闭后台任务"等字样,别被骗)。 """ import ast as _ast import re as _re4 src = (HERE / "collabd.py").read_text(encoding="utf-8") # 🔴🔴 2026-10-04 **改用 AST 取函数体**(P0-54 同族第 3 次): # 原实现用 `re.sub(r"#[^\n]*", "", code)` 粗暴剥注释 ⇒ 会把**字符串里的 `#`** # 之后内容全删 —— prompt 模板里到处是 `"## 二、…"` ⇒ **函数体被吃掉** ⇒ 假红 # (实测 `ensure_supervise()` 明明在,判据却报"没清 guard.stop")。 # ✅ AST 按 `FunctionDef` 切片,字符串里的 `#` 一律不受影响。 _segs = {} try: for _n in _ast.walk(_ast.parse(src)): if isinstance(_n, _ast.FunctionDef) and _n.name not in _segs: _segs[_n.name] = _ast.get_source_segment(src, _n) or "" except Exception as _e: return [("⛔ 源码解析失败(%s)⇒ 判据没法跑" % _e, False)] def _fn(name): return _segs.get(name, "") fn_stop, fn_life, fn_ens = _fn("supervise_stop"), _fn("goal_life_set"), _fn("ensure_supervise") res = [ ("`supervise_stop()` 存在(没有它就无从关闭常驻程序)", bool(fn_stop)), ("`supervise_stop()` 真的能停:写了停止标志 `guard.stop`", "guard.stop" in fn_stop), ("`supervise_stop()` 有强停兜底(⛔ 只写标志可能停不掉)", "taskkill" in fn_stop or "SIGKILL" in fn_stop), ("`goal_life_set()` 在**已完成**分支调用 `supervise_stop()`(完成 ⇒ 收工)", "GOAL_LIFE_DONE" in fn_life and "supervise_stop" in fn_life), ("`goal_life_set()` 对**阻碍/已暂停**明确不调停(用户口径②:阻碍不用关)", ("GOAL_LIFE_BLOCK" in fn_life or "保持运行" in fn_life) and _re4.search(r"GOAL_LIFE_DONE.*?supervise_stop", fn_life, _re4.S) is not None), ("`ensure_supervise()` **会清 `guard.stop`**(⛔ 不清 ⇒ 完成后再也起不来)", "guard.stop" in fn_ens and "unlink" in fn_ens), ] return res @case("跨区自愈:**只补进行中的区**(⛔ 已完成/阻碍/已收工的一律不拉)") def t_peer_supervise_sweep(): """🔴 2026-10-03 补的跨区自愈(实测坐实缺口:本区 `ensure_supervise()` 只看本区心跳, 本区日志搜别区名**零命中** ⇒ 别区常驻被外部收走后**没人知道、没人补**)。 ✅ 本条守三条**越界红线**(少一条就可能去动别的区)——源码级断言: ① 目标 `lifecycle` **必须是「进行中」**才拉(用户口径:完成=已收工,阻碍=不用开); ② **⛔ 只读别区心跳**(不得写别区的 `goal.json`/队列 —— 那是别的区主会话的领地); ③ 有 `guard.stop` 的区**⛔ 不拉**(刚被收工的区,拉起来=对抗用户的停)。 ⚠️ 另守两条工程约束:**接在主循环里**(⛔ 没接=写了函数也不生效) + **有节流**(⛔ 每轮都去 stat 别区是白耗 IO)。 🔴🔴 **2026-10-04 修本体 bug(P0-54 同族第 3 次)**: 原实现用 `re.sub(r"#[^\n]*", "", code)` **粗暴剥注释** ⇒ 它会把 **字符串字面量里的 `#`** 之后的内容**全删掉** —— 而 prompt 模板里到处是 `"## 二、…"`、`"## 三、…"` ⇒ **它后面的函数体整个被吃掉** ⇒ `peer_supervise_sweep()` 明明在源码里(实测 `collabd.py:403`), 判据却报「⛔ 源码里没有」⇒ **假红**。 ✅ 正解=**用 `ast` 按函数体切片**(P0-54 早就写了「等宽要按函数体切片」, 这条是同一个病的第 3 次 ⇒ 这次改成 AST,⛔ 不再用正则)。 """ import ast as _ast src = (HERE / "collabd.py").read_text(encoding="utf-8") fn = "" try: _tree = _ast.parse(src) for _n in _ast.walk(_tree): if isinstance(_n, _ast.FunctionDef) and _n.name == "peer_supervise_sweep": fn = _ast.get_source_segment(src, _n) or "" break except Exception as _e: return [("⛔ 源码解析失败(%s)⇒ 判据没法跑" % _e, False)] # 「接在主循环里」要**全文**看(调用点必然在函数体外) _code = "\n".join(ln for ln in src.splitlines() if not ln.lstrip().startswith("#")) if not fn: return [("⛔ 源码里没有 `peer_supervise_sweep()` ⇒ 跨区自愈不存在", False)] return [ ("跨区自愈函数存在(⛔ 缺它=别区停了没人管)", bool(fn)), # 🔴🔴 2026-10-04 改:闸① **收窄** —— 目标非「进行中」时**先看队列**, # 有未完成件仍拉(用户口径「检查程序必然要去处理队列」)。 # ⛔ 旧断言("非进行中一律不拉")已过时,不改它会**红着掩盖新口径**。 ("闸①:目标非「进行中」时**改看队列**(有未完成件仍拉 —— 有活没人干)", "GOAL_LIFE_RUN" in fn and "queue_pending_of(" in fn), # ⚠️ 判据写准:`read_text` 在函数里**本来就有一处合法用途**(读别区心跳)⇒ # ⛔ 不能断言"函数里没有 read_text"(那会误伤心跳那行 —— 我第一版就是这么写错的)。 # 真正要禁的是「**自己另写一段读 `tasks.json` 的代码**」⇒ 直接查那个特征串。 ("闸①:用的是**唯一事实源** `queue_pending_of()`(⛔ 不在这里另写读 tasks 的代码)", "queue_pending_of(" in fn and "tasks.json" not in fn), ("闸②:⛔ **只读**别区心跳(只 read_text 心跳,⛔ 不写别区 goal.json)", "supervise-heartbeat.json" in fn and fn.count("write_text") == 0), ("闸③:有 `guard.stop` 的区 ⛔ 不拉(别对抗用户的收工)", "guard.stop" in fn), ("起别区时用**它自己的副本+它自己的 COLLABD_CONFIG**(⛔ 指向本区就串了)", "COLLABD_CONFIG" in fn and ".workbuddy" in fn), ("有节流 `PEER_SWEEP_EVERY`(⛔ 每轮都碰别区=白耗 IO)", "PEER_SWEEP_EVERY" in src), ("**接在主循环里**(⛔ 写了函数不接=永不生效)", "peer_supervise_sweep()" in _code), ("Popen 带 `creationflags`(⛔ 漏了会闪黑窗,仓内另有判据守这条)", "creationflags" in fn), ] def _tail_has_board_note(code: str) -> bool: """🔴 2026-10-05:判「末尾『后续协作』段是否说清**本区不起看板**」。 ⚠️ **为什么不能全文 `in` 一把梭**(本轮实测踩到):`init_workspace.py` 里 `peer_workspaces` 全文出现 **3 次**、`不起看板` **2 次**,其中**多数在注释里** ⇒ 用 `"不起看板" in src` 判,**把那段 print 整块删掉后判据照样绿**(恒绿假修)。 ✅ 正解=**锚定到「✅ 完成。后续协作」之后的那段代码**(调用方传进来的 `code` 已剥注释, ⇒ 注释里的字样不会命中),且**两句 print 都要在**: · 一句说「本区⛔ 不起看板」(否则用户以为要自己起一份) · 一句指路「加进主工作区的 `peer_workspaces`」(否则知道不起、却不知道去哪并看) """ i = code.find("✅ 完成。后续协作") if i < 0: return False seg = code[i:i + 1500] return ("不起看板" in seg) and ("peer_workspaces" in seg) @case("初始化脚本:**指向本区副本**(⛔ 不许再让用户跑技能目录那份)") def t_init_points_to_own_copy(): """🔴 2026-10-03 修的误导:`init_workspace.py` 原先 `--ensure-goal-dir` 与末尾 「后续协作」提示**全部指向技能目录的 `collabd.py`/`board.py`**。 🔴 **为什么有害**(不是洁癖):定案口径是**各工作区程序各自独立**(技能目录=源, `.workbuddy/collab/`=各区副本,`deploy_code.py` 分发)⇒ 让用户跑目录那份 **会拉起第二个常驻、写到技能目录的 `tmp/`**(⛔ 那不是任何工作区的 inbox) ⇒ 状态写到别处:看板读不到、故障排查被带偏。 ✅ 判据:`--ensure-goal-dir` 的实参**优先取本区副本**,末尾提示同理; ⚠️ 副本不存在时**允许回落**,但⛔ 必须**打印说明**(静默回落=用户以为在跑副本)。 """ src = (HERE / "init_workspace.py").read_text(encoding="utf-8") code = re.sub(r"\"\"\".*?\"\"\"", "", src, flags=re.S) code = re.sub(r"(?m)^\s*#.*$", "", code) res = [ ("有「取本区副本」这一步(`_own = ws/.workbuddy/collab/collabd.py`)", ".workbuddy" in code and "collabd.py" in code), ("`--ensure-goal-dir` 用的是**本区副本优先**的路径(⛔ 不是硬编码 SK)", code.count("SK / \"collabd.py\"") <= 2 and "_cd" in code), ("副本缺失时**回落**用技能目录那份(⛔ 硬失败会挡住首次初始化)", "_fallback" in code), ("回落时**打印说明**(⛔ 静默回落=用户以为在跑副本)", code.count("print") >= 2 and "回落" in code), # 🔴 2026-10-05 改判据:末条原来要求 `_bd_show` 存在(=末尾会打印「起看板」命令)。 # ⚠️ 那是**旧口径**:SKILL.md:809 早有 10-03 定案「看板只保留一份(主工作区那份)」, # 各区⛔ 不起看板 ⇒ 新区末尾**本就不该**再打印 board 启动命令。 # ⇒ 判据跟着改:仍然要求「后续协作提示指向副本」(`_cd_show`), # 但另一条换成**新口径的事实**——末尾必须说清「本区不起看板 + 要并看走 peer_workspaces」。 # ⛔ 别把 `_bd_show` 塞回去凑数:那会把出错的形态提示又引回来。 ("末尾「后续协作」提示也指向副本/带说明(⛔ 别再让人跑目录那份)", "_cd_show" in code), ("末尾提示**不起本区看板**、要并看走 `peer_workspaces`(2026-10-03 定案:看板只保留一份)", _tail_has_board_note(code)), ] return res @case("自指型判据:**必须被排除**(⛔ 否则 `goal_state()` 只能自己满足自己,永远锁死)") def t_acc_selfref_excluded(): """🔴🔴 2026-10-04 用户点破:「**A6 判据自指 ⇒ `goal_state()` 逻辑死锁**」。 🔴 **死锁形状**(不是时序问题、⛔ 也等主会话解决不了): `goal_state()` 的**路3 要读这条判据**,而这条判据判的内容**就是**「三路并集的结论」 ⇒ 它**只能由"自己写自己过"满足** ⇒ 固定点之外永不到达。 🔴 **实测坐实**:把 A6 临时写成「过|…」⇒ `goal_state()` 立刻 pass;还原 ⇒ 立刻回 open。 ✅ 修法=**路3 排除自指型判据**,判据查四件事(源码级 + 行为级各一半): ① `_acc_is_selfref()` 存在,且**登记制**(`_ACC_SELFREF_KEYS`)与**文本兜底**(`_ACC_SELFREF_TOKENS`)**两条都在**; ② `goal_state()` 路3 与 `_acc_short()` **两处都剔**(⛔ 只剔一处 ⇒ 唤醒正文仍会念「还差 A6」, 造出「`goal_state()` 已判 pass、这里还在报」的新自相矛盾); ③ **fail-closed**:剔完一条不剩 ⇒ 判「未声明」,⛔ 不算过; ④ **兜底必须窄**(`_ACC_SELFREF_WIN` 有上限)—— 扫整段会**误伤 A2**(它正文里顺口提了一句 `goal_state()` 只读文本)⇒ 悄悄放掉一条真门禁,**比原 bug 更坏**(原 bug 是吵,这个是静默失守)。 ⚠️ **源码级那两条必须按 AST 精确定位,⛔ 不可用「函数体里含某串」**(2026-10-04 变异验证实测栽过): - 🔴 `_acc_is_selfref` 在 `goal_state()` 里出现**两次**(`_real` 的推导式 + `_dropped` 的推导式) ⇒ 判据写成 `"_acc_is_selfref" in fn_gs` 时,**只把 `_real` 那处改掉照样 PASS**(漏网)。 - 🔴 `undeclared` 在 `goal_state()` 尾部 `return "pass" if eff else "undeclared"` 里也有 ⇒ 判据写成 `re.search(r"if not _real.*?undeclared", ...)` 时,**把中间那个 `return` 拆掉照样 PASS**(漏网)。 ⇒ 一律改成**在 AST 上找指定 Assign / 指定 If 的 body**,别用"整段文本里有没有"。 """ import ast as _ast5 import re as _re5 src = (HERE / "collabd.py").read_text(encoding="utf-8") code = _re5.sub(r"\"\"\".*?\"\"\"", "", src, flags=_re5.S) code = _re5.sub(r"(?m)^\s*#.*$", "", code) def _fn_src(name): m = _re5.search(r"\ndef %s\(.*?(?=\ndef )" % _re5.escape(name), code, _re5.S) return m.group(0) if m else "" def _fn_node(name): """AST 上**唯一定位**某个顶层函数(同名多个 ⇒ 返回 None,⛔ 不许取第一个)。""" hits = [n for n in _ast5.walk(_ast5.parse(src)) if isinstance(n, _ast5.FunctionDef) and n.name == name and isinstance(getattr(n, "col_offset", 0), int)] tops = [n for n in hits if n.col_offset == 0] return tops[0] if len(tops) == 1 else None def _unparse(nodes): return "".join(_ast5.unparse(n) for n in nodes) fn_sr, fn_gs, fn_as = _fn_src("_acc_is_selfref"), _fn_src("goal_state"), _fn_src("_acc_short") nd_gs, nd_as = _fn_node("goal_state"), _fn_node("_acc_short") # --- AST:`_real = [...]` 这个**赋值**的推导式里到底有没有剔 --- # ⚠️⚠️ 判据写法上的两个坑(2026-10-04 变异验证实测各栽一次): # ① `_real` 是 **Assign 左侧的名字**,⛔ **推导式的 target 里没有它** # (`goal_state` 的是 `(_k, _v)`、`_acc_short` 的是 `k`)⇒ 只能在 Assign 上找。 # ② 过滤条件在**生成器的 ifs 子句**里,⛔ 不在 `elt`;且 `elt` 是**单个节点** # (`goal_state` 那个还是 Tuple)⇒ `list(elt)` 会 TypeError。 def _real_filter_excludes_selfref(node): if node is None: return False for n in _ast5.walk(node): if not (isinstance(n, _ast5.Assign) and len(n.targets) == 1 and isinstance(n.targets[0], _ast5.Name) and n.targets[0].id == "_real"): continue if not isinstance(n.value, _ast5.ListComp): continue scan = list(_ast5.walk(n.value.elt)) + [x for g in n.value.generators for i in g.ifs for x in _ast5.walk(i)] if any(isinstance(x, _ast5.Call) and getattr(x.func, "id", "") == "_acc_is_selfref" for x in scan): return True return False # --- AST:`if not _real:` 这个 If 的 body 里到底有没有 `return "undeclared"` --- def _not_real_returns_undeclared(node): if node is None: return False for n in _ast5.walk(node): if not isinstance(n, _ast5.If): continue t = n.test is_not_real = (isinstance(t, _ast5.UnaryOp) and isinstance(t.op, _ast5.Not) and isinstance(t.operand, _ast5.Name) and t.operand.id == "_real") if not is_not_real: continue for stmt in n.body: if isinstance(stmt, _ast5.Return) and isinstance(stmt.value, _ast5.Constant) \ and stmt.value.value == "undeclared": return True return False res = [ ("`_acc_is_selfref()` 存在(没有它就无从识别自指)", bool(fn_sr)), ("**登记制**在(`_ACC_SELFREF_KEYS`):⛔ 不依赖判据文本,是唯一权威路径", "_ACC_SELFREF_KEYS" in code), ("**文本兜底**在(`_ACC_SELFREF_TOKENS` + 有窗口上限):补登记漏网的", "_ACC_SELFREF_TOKENS" in code and "_ACC_SELFREF_WIN" in code), ("窗口**有上限且不为 0**(⛔ 扫整段会误伤 A2 那类真判据 ⇒ 静默失守)", bool(_re5.search(r"_ACC_SELFREF_WIN\s*=\s*([1-9]\d*)", code))), ("AST:`goal_state()` 里 `_real` 的推导式**真的剔**(⛔ 不看整段文本,见 docstring 变异坑)", _real_filter_excludes_selfref(nd_gs)), ("AST:`_acc_short()` 里也剔(⛔ 只剔一处 ⇒ 唤醒正文仍念「还差 A6」,造出新自相矛盾)", _real_filter_excludes_selfref(nd_as)), ("AST:`if not _real:` 的 body 里**真的 `return \"undeclared\"``(fail-closed)", _not_real_returns_undeclared(nd_gs)), ("⛔ **没放松实质门禁**:`for _k, _v in _real:` 仍逐条判 pass(其余判据照判)", "for _k, _v in _real:" in fn_gs or "for _k, _v in _real" in fn_gs), ] # ---- 行为级:真起子进程跑真函数,用真数据量(⛔ 源码含关键字不算数) ---- try: import subprocess as _sp probe = r''' import importlib.util, json, os, sys, tempfile from pathlib import Path spec = importlib.util.spec_from_file_location("cd_probe", sys.argv[1]) m = importlib.util.module_from_spec(spec) sys.modules["cd_probe"] = m spec.loader.exec_module(m) sr = m._acc_is_selfref res = {} # ① 真自指(测试3 的 A6 原句形状):必须判 True A6 = "\U0001f534 不过|goal_state() 返回 pass 且 goals_open() 为空(三路全过)" res["A6 真自指判 True"] = sr("a6 目标状态", A6) is True # ② 真门禁 A2:正文**深处**顺口提一句 goal_state() ⇒ 必须判 False(⛔ 误伤=静默放掉真门禁) A2 = ("\U0001f534 不过|" + "A2 快照陷阱说明。" * 40 + " 另注:goal_state() 只读文本、不重验事实。") res["A2 长正文顺口提及判 False"] = sr("a2 快照", A2) is False # ③ 登记制:文本完全看不出自指,只要登记过就排除 res["登记制压过文本"] = sr("A6 目标状态", "过|已确认") is True # ④ 普通真判据:一律不排除 res["普通判据判 False"] = sr("a1 产物", "\U0001f534 不过|产出三份文档") is False # ⑤🔴 端到端:真造 goal.json 跑**真 goal_state()**(⛔ 这才是死锁的硬证据) # 🔴🔴 **隔离必须用 `COLLABD_CONFIG`,⛔ 不可用 `DSH_COLLAB_WS`/`COLLABD_WORKSPACE`**: # 真源 `load_cfg()` 的候选顺序是 ① `COLLABD_CONFIG` → ② `/.workbuddy/collab/collabd.config.json` # ⇒ **只设 env 不给 config,会回落读到真工作区的配置**(本判据曾因此把真 `goal.json` 覆盖掉, # 2026-10-04 已从 `bak-goalctl-20261002/` 恢复)。⇒ 这里**显式写一份临时 config**。 # ⚠️ `TG`(任务图)也由这份 config 的 `taskgraph` 决定 ⇒ 一并指到夹具,⛔ 不读真任务图。 tmp = tempfile.mkdtemp(prefix="gs_") _cfg = Path(tmp) / "collabd.config.json" _cfg.write_text(json.dumps({ "workspace": tmp.replace("\\", "/"), "inbox": "inbox", "taskgraph": "inbox/taskgraph.json", "live": "inbox/realtime.md", }, ensure_ascii=False), encoding="utf-8") os.environ["COLLABD_CONFIG"] = str(_cfg) spec2 = importlib.util.spec_from_file_location("cd_probe2", sys.argv[1]) m2 = importlib.util.module_from_spec(spec2) sys.modules["cd_probe2"] = m2 spec2.loader.exec_module(m2) # 🔴 自检:**INBOX 必须真落在临时目录**,否则说明隔离没生效 ⇒ 宁可报红也别再写真文件 if str(m2.INBOX).replace("\\", "/").find(tmp.replace("\\", "/")) < 0: print(json.dumps({"__隔离失效: INBOX=%s 不在临时目录" % m2.INBOX: False}, ensure_ascii=False)) raise SystemExit(0) inbox = Path(m2.INBOX); inbox.mkdir(parents=True, exist_ok=True) (inbox / "tasks.json").write_text(json.dumps({"t": {"state": "done"}}), encoding="utf-8") (inbox / "taskgraph.json").write_text(json.dumps({"nodes": [{"id": "N1", "status": "done"}]}), encoding="utf-8") # ⚠️ 判据文本必须用**真源真会写出来的形状**(`过|…`,⛔ 不带 `🔴` 前缀): # `_acc_is_pass` 取竖线左边那段判,`🔴 过` 的 head 仍带 `🔴` ⇒ 会被判「未过」(假红)。 def _write(acc): (inbox / "goal.json").write_text(json.dumps( {"nodes": [{"id": "N1", "status": "done"}], "acceptance_state": acc}), encoding="utf-8") # 场景 X:**只有自指判据、且它没过** ⇒ 正确行为=`undeclared`(⛔ 不是 pass) _write({"a6 目标状态": "\U0001f534 不过|goal_state() 返回 pass 且 goals_open() 为空(三路全过)"}) res["隔离生效: INBOX 落在临时目录"] = True res["端到端X: 全自指且未过 ⇒ undeclared(不是pass)"] = (m2.goal_state() == "undeclared") # 场景 Y:自指判据已写「过」 + **一条真判据也过了** ⇒ 正确行为=`pass` #(⛔ 这是修复前会卡死的那个形状:路3 要读 A6,A6 又判"三路全过") _write({"a6 目标状态": "过|goal_state() 返回 pass", "a1 产物": "过|三份文档已交"}) res["端到端Y: 自指+真判据全过 ⇒ pass(不再死锁)"] = (m2.goal_state() == "pass") # 场景 Z:自指 + **一条真判据没过** ⇒ 必须 `open`(⛔ 不许因为剔了自指就把真门禁也放过) _write({"a6 目标状态": "\U0001f534 不过|goal_state() 返回 pass", "a1 产物": "不过|只交了两份"}) res["端到端Z: 真判据没过 ⇒ open(没放松门禁)"] = (m2.goal_state() == "open") # 场景 W:自指**已写「过」** + 真判据**没过** ⇒ 仍必须 `open` #(⛔ 这条专抓"因为剔了自指就整路放行"的写法:自指写过就当全过 ⇒ 静默失守) _write({"a6 目标状态": "过|goal_state() 返回 pass", "a1 产物": "不过|只交了两份"}) res["端到端W: 自指写过但真判据没过 ⇒ open"] = (m2.goal_state() == "open") print(json.dumps(res, ensure_ascii=False)) ''' out = _sp.run([sys.executable, "-c", probe, str(HERE / "collabd.py")], capture_output=True, text=True, timeout=180, encoding="utf-8") got = json.loads(out.stdout.strip().splitlines()[-1]) for k, v in got.items(): res.append(("真起进程验行为:" + k, v)) except Exception as _e: # ⛔ 起不来就必须报红,不能静默跳过 res.append(("真起进程验行为(⛔ 起不来即报红):" + type(_e).__name__, False)) return res @case("**单包自包含**(⛔ 只复制这一个技能到别的机器,排版/提报用户/去 AI 味都要能用)") def t_single_package_selfcontained(): """🔴🔴 2026-10-04 用户定案:「需要整合到会话技能中,重点是只想复制一个技能到别的机器, 这些功能都可以正常使用」。 🔴 **改前的两处外部依赖**(⛔ 都是"不装另一个技能就静默失效"型): ① `reply-style-guard.py::_core()` 第②级回退指向 **另一个技能**的 `agent-operating-rules/references/回复排版-核心块.md`; ② `hooks/_env.py::_SNIFF = "agent-operating-rules"` —— 它是**技能库定位的识别特征**, `skills_root()` / `env_check()` 三处都拿`(root)/_SNIFF).is_dir()` 当"库在不在"的判据。 ⚠️ **实测过的失效形状(不是推断)**:只搬本包到空目录时,`reply-style-guard` 输出 **空 JSON**(日志里看不出是"机制坏"还是"没配规则")⇒ 排版闸门**整条静默失效**。 ✅ 修法:① 把那份核心块**内联**进本包 `references/03-回复排版-核心块.md`(内容守恒、逐字搬), `_core()` 改成**三级回退**(工作区规则文件 → **包内件** → 外部兜底); ② `_SNIFF` 改成**候选数组**,**第一个 = 本包自己** ⇒ 只拷本包也能定位技能库。 ⚠️ 本判据守两件事:件在不在 + **代码真的先找包内**(⛔ 只查文件存在=假绿, 回归过一次:路径上溯少一级时文件在、但读不到,hook 照样静默空输出)。 """ S = HERE.parent core = S / "references" / "03-回复排版-核心块.md" guard = (S / "scripts" / "hooks" / "reply-style-guard.py").read_text(encoding="utf-8") envp = (S / "scripts" / "hooks" / "_env.py").read_text(encoding="utf-8") res = [ ("包内件 `references/03-回复排版-核心块.md` 在(⛔ 单包自包含的主力件)", core.exists()), ] if core.exists(): t = core.read_text(encoding="utf-8") # ⚠️⚠️ 标记必须按 `_extract()` 的**完整字面量**查(带空格与 ` -->`), # ⛔ 不能查子串 `REPLY-CORE:END` —— 2026-10-04 变异验证实测栽过: # 把 `` 改成 `` 后 # 子串仍命中 ⇒ 判据放行,但**真提取器已经认不出**(END 串对不上 ⇒ 静默返回空)。 # ⛔ 判据必须与被仿刻的那个函数用**同一份字面量**。 _B = "" res += [ ("包内件带**完整** `REPLY-CORE` 标记(⛔ 提取器靠它认,⛔ 少一个就静默返回空)", _B in t and _E in t), ("包内件非空(⛔ 标记在但块体空 = 注入 0 字符)", len(t.split(_B, 1)[-1].split(_E)[0].strip()) > 100), ("包内件**不自指外部技能路径**(⛔ 单包环境下那句话会指向不存在的文件)", "agent-operating-rules/scripts" not in t), ] res += [ ("`_core()` 有**包内件**这一级回退(⛔ 只留外部兜底=没自包含)", "03-回复排版-核心块.md" in guard), ("🔴 **包内件优先于外部件**(⛔ 顺序反了又变成「看别人脸色」)", guard.index("03-回复排版-核心块.md") < guard.index("CORE_REL)")), ("🔴 上溯层级对(`hooks` → **两级** → 包根,⛔ 少一级落到 `scripts/` 读不到)", "os.path.dirname(os.path.dirname(_hooks))" in guard), ("`_env.py` 的识别特征**含本包自己**(⛔ 只认别的技能 ⇒ 单包下库定位失败)", "_SNIFFS = (" in envp and '"session-mechanism"' in envp), ("`_env.py` 不再**只**靠 `agent-operating-rules` 判库在不在(⛔ 那是隐式耦合)", "(root / _SNIFF).is_dir()" not in envp and "Path(rep[\"skills_root\"]) / _SNIFF" not in envp), ] return res @case("搬入的两块**不许被删掉/漂移**(⛔ 自决策口径 + 任务会话六阶段)") def t_migrated_docs_intact(): """🔴 2026-10-04 两件搬入,判据守住它们**在位**且**口径一致**。 ① **功能优先协作协议**(原 `dsh-decision/references/01` → 本包 `references/02`) —— 用户定案「这个属于会话的自决策方法,应该属于会话逻辑」。 ⚠️ **2026-10-04 改**:本档**已是权威**(原为 `agent-operating-rules §1.6` 权威 · 本包落地副本) ⇒ 判据查的不只是"在不在",还要查**它声明的是「本档即权威」**(⛔ 指向外部技能当权威 = 单包失效)。 ② **执行会话六阶段**(用户定案逐字:需求识别/调研/规划/执行/验证/归档清理 =「正式执行会话执行任务的流程」)⇒ 写在 `references/collab.md §5.5`。 ⚠️ 判据**不许只查关键词计数**(P0-45 同族:字符串出现≠机制在); 还要查**接线**(`SKILL.md` 加载段真的挂上了、⛔ 否则档在包里没人读得到)。 """ S = HERE.parent fpr = S / "references" / "02-功能优先协作协议.md" skl = (S / "SKILL.md").read_text(encoding="utf-8") collab = (S / "references" / "collab.md").read_text(encoding="utf-8") res = [ ("`references/02-功能优先协作协议.md` 在位(搬入的落点)", fpr.exists()), ] if fpr.exists(): t = fpr.read_text(encoding="utf-8") res += [ ("自决策档:**9 类自决策白名单**在(技术层永不问)", "9 类白名单" in t), ("自决策档:**只准提报用户 3 类**在(功能语义分叉/红线门禁/超边界)", "只准提报用户" in t), ("自决策档:**功能卡 4 问**在(用户侧输入格式,⛔ 此前无任何实体承接)", "功能卡" in t and "怎样算成功" in t), ("自决策档:**提报用户格式**在(⛔ 不许出现路径/commit/标识符)", "语言转换表" in t), ("自决策档:**拆包提报用户**在(红线问题⛔ 不得与技术方案捆问)", "拆包提报用户" in t), ("自决策档声明了**本档即权威**(⛔ 单包自包含的关键:⛔ 不能再指向外部技能当权威)", "本档即权威" in t), ("自决策档**排版指向本包内件**(⛔ 不是外部技能的 §2)", "references/03-回复排版-核心块.md" in t), # 🔴 2026-10-04 改造后**这一条被作废**(原文是「声明了三处副本,改判据要同时改三处」)。 # 现在本档**即权威**(单包自包含),⛔ 不再要求同步另两处。 # ⇒ 判据反向:⛔ 那句「同时改三处」必须**已消失**(留着会让单包用户以为还有两份)。 ("🔴 自决策档**不再要求「同时改三处」**(⛔ 已作废:本档即权威,留着会误导单包用户)", "同时改三处" not in t), ] res += [ # ⚠️⚠️ 查"接线"必须查**段标本身的完整性**,⛔ 不能只查某个子串在不在 # (2026-10-04 变异验证实测栽过:变异把 `**⑦ 我要判断` 改成 `**(已摘除)⑦ 我要判断`, # 而判据查的正是 `⑦ 我要判断` 这个子串 ⇒ 改完照样命中 ⇒ **假绿**)。 # ⇒ 判据改为查「行首是 `**⑦ 我要判断`」这种**带行首锚定**的形式。 ("🔴 **接线**:`SKILL.md` 加载段真挂了第 ⑦ 条(行首锚定,⛔ 不许被改成装饰)", any(l.startswith("**⑦ 我要判断") for l in skl.splitlines())), ("🔴 **接线**:加载段指到 `references/02-功能优先协作协议.md`", "references/02-功能优先协作协议.md" in skl), ] # 六阶段:⛔ 同样不能只查「关键词在文件里」——用户原话那句里也含全部六个词 # (实测:删掉表格里的「归档清理」,原话那句仍在 ⇒ 子串计数不变 ⇒ 假绿)。 # ⇒ 改为查**六阶段表格那一行**(`collab.md §5.5` 的表行,逐行锚定)。 stage_lines = [l for l in collab.splitlines() if l.strip().startswith("| **")] def _row(kw): return any(kw in l and "缺了会怎样" not in l and l.strip().startswith("| **") for l in stage_lines) for _idx, _kw in ((0, "需求识别"), (1, "需求调研"), (2, "方案规划"), (3, "任务执行"), (4, "结果验证"), (5, "归档清理")): res.append(("六阶段表行:**%s**(⛔ 查表行不是查关键词,原话里也有这六个词)" % _kw, _row(_kw))) res += [ ("六阶段是**任务会话的标准动作**(⛔ 别只是一张并列的表)", "任务会话的标准动作" in collab), ("六阶段每行都写了「缺了会怎样」(⛔ 只写做什么=口号)", "⛔ 后果" in collab and "缺了会怎样" in collab), ("六阶段**接上了自决策口径**(规划阶段 ⇒ 该不该问用户)", "02-功能优先协作协议.md" in collab), ("⚠️ 标清了**与 `dsh-workflow` 的分工**(⛔ 别当同一件事)", "dsh-workflow" in collab), ] # ===== 单包自包含的**文档侧**:⛔ 不能再把外部技能当权威 ===== skl2 = (S / "SKILL.md").read_text(encoding="utf-8") res += [ ("🔴 `SKILL.md` 声明**本包自包含**(⛔ 复制一个就能用)", "自包含" in skl2 or "已无必需外部依赖" in skl2), ("🔴 `SKILL.md` 不再说「依赖某技能」为**必需**(⛔ 只可作可选增强)", "**依赖(⛔ 未并入,只声明)**" not in skl2), # ⚠️ 判「权威归属」⛔ 不能只查"提了本档" —— 改造前那句是「权威在 `agent-operating-rules §1.6`」, # 改造后是「本档即权威」,两处都提到"本档" ⇒ 只查本档会漏。 # ⇒ 查**反向**表述:不能再出现「权威分工/完整判据已收敛到通用技能/判据实体在 <外部技能>」。 ("🔴 自决策档**不再把外部技能当权威**(⛔ 那会让单包用户读到「去别处找判据」)", not any(k in t for k in ("权威分工", "完整判据已收敛到通用技能", "判据实体在 `agent-operating-rules"))), ("🔴 自决策档保留了**来路说明**(⛔ 抹掉就查不到它从哪来、为何这样定)", "来路" in t), ("🔴 排版指向**本包内件**(⛔ 随包,不依赖外部)", "references/03-回复排版-核心块.md" in t), # ⚠️ 变异验证实测栽过:只查「`03-回复排版-核心块.md` 在不在」是**假绿** —— # 文件里它出现**两次**(§5.4 标题行 + L285 的"随包自带"说明行)。 # 把**标题行**改回「`agent-operating-rules §2`」时,L285 那个子串**仍然命中** ⇒ 判据放行。 # ⇒ 改为查**标题行本身**(§5.4 那一行必须带包内路径,且⛔ 不得提外部技能的 §2)。 ("🔴 排版**标题行**指向本包内件(⛔ 查子串会假绿:同一文件里它出现两次)", any("03-回复排版-核心块.md" in l and l.lstrip().startswith("###") for l in t.splitlines())), ("🔴 排版标题行**不提外部技能 §2**(⛔ 那是「去别处找判据」的形状)", not any(l.lstrip().startswith("###") and "agent-operating-rules" in l and "排版" in l for l in t.splitlines())), ] return res @case("包体卫生:**不许长回备份/缓存/零引用文件**,且**没有版本控制兜底**(⛔ 删了就是真删)") def t_pkg_hygiene(): """🔴🔴 2026-10-04 用户要求「检查技能相关文件是否有可以整合、精简、移除的重复、冗余、多余描述」。 ✅ 那轮清掉的(**10.7 MB → 1.9 MB,减 83%**):68 个 `*.bak-*`(8.6 MB)+ `__pycache__`/`.pyc`(255 KB)+ `install.log`(36 KB)+ **零引用**的 `assets/_syntax_probe.js`(109 KB)+ `SKILL.md` 那条 55 KB 变更流水账(压到 2.4 KB,**-96%**)。 🔴🔴 **为什么必须有这条判据(本轮取证到的硬事实)**: **本技能包不在任何 git 仓库下** —— `.workbuddy/`/`E:/ProgramData/`/`E:/` 三级都无 `.git`; 文档库 `08-skills/` 只收了另外 7 个技能,**没有 session-mechanism** ⇒ **删掉的文件在这台机器上就是唯一副本没了** ⇒ 必须先有"包外快照"才敢删, 也因此**更不能让它重新长回来**(长回来 = 下次清理又要处理一遍 8.6 MB)。 ⚠️ 同时盯两件同类退化:① **零引用文件**(`_syntax_probe.js` 就是这么烂在包里的 109 KB); ② 指向已删文件的**死证据引用**(注释写「原文照抄见备份 X」⇒ 备份删了它就是句假话,本轮改掉 6 处)。 """ import re as _re6 res = [] bak = [p for p in HERE.parent.rglob("*.bak-*") if p.is_file()] res.append(("⛔ 包内无 `*.bak-*` 历史备份(本轮清掉 68 个 / 8.6 MB)", not bak)) # ⚠️ `__pycache__`/`.pyc` 是**跑一次 selftest 就重新生成**的(`hooks/_env.py` 被导入就产生 # `scripts/hooks/__pycache__/_env.cpython-313.pyc`)⇒ **判它存在=判「永远红」**。 # ⇒ 判据只能是「⛔ 不许把它当能力件进包/入库」,而它**天然会重生** ⇒ 这里改成 # 「删干净后跑一遍 selftest,确认它只重生在 __pycache__ 里、⛔ 不在任何源码目录留下散 .pyc」。 loose_pyc = [p for p in HERE.parent.rglob("*.pyc") if "__pycache__" not in str(p)] res.append(("⛔ 无散落的 `.pyc`(⛔ 只能在 `__pycache__` 内;它跑一次就重生,不判存在)", not loose_pyc)) # ⚠️ `install.log` 同理:`install.py:96,340` 每次运行都 append ⇒ 同样只能判"不巨大"。 log = HERE.parent / "install.log" logsz = log.stat().st_size if log.exists() else 0 res.append(("⛔ `install.log` 不超 64 KB(运行日志,⛔ 不属能力件;本轮清掉 36 KB)", logsz <= 64 * 1024)) # 零引用文件:assets/ 下每个 .js,⛔ 排掉本身被引用的 dead = [] for p in list((HERE.parent / "assets").glob("*.js")): stem = p.stem hit = 0 for q in HERE.parent.rglob("*"): if not q.is_file() or q == p or "__pycache__" in str(q): continue if q.suffix not in (".py", ".md", ".html", ".js", ".sh", ".json", ".example"): continue try: if stem in q.read_text(encoding="utf-8", errors="ignore"): hit += 1 except Exception: continue if hit == 0: dead.append(p.name) res.append(("⛔ `assets/*.js` 无零引用文件(`_syntax_probe.js` 烂在包里 109 KB)", not dead)) # 死证据引用:注释/文档里点名某个 *.bak-xxx,⛔ 排除 install.py(它的备份在 .workbuddy/,是活机制名) # ⚠️⚠️ 还要**排除 selftest.py 自己** —— 本判据的注释里就写了那个串(2026-10-04 实测自我误报)。 # ⚠️⚠️ 以及 `references/pitfalls.md` 的 **P0-45** —— 那一节**专门讲「判据会自我误报」这个坑**, # 必须举出那个串当例子(2026-10-04 实测:把它当死证据 ⇒ 判据对自己的说明书报红)。 # 判据=「活机制名」还是「死证据」:**看它指的是不是 `.workbuddy/` 下真存在的备份**。 pat = _re6.compile(r"[\w\.\-]+\.bak-[\w\-]+") dref = [] for p in HERE.parent.rglob("*"): if not p.is_file() or "__pycache__" in str(p): continue rel = p.relative_to(HERE.parent).as_posix() # ⛔ 排除三个"提到备份名是**机制本身/说明书**"的文件 if rel in ("install.py", "scripts/selftest.py"): continue # 🔴🔴 2026-10-05 加:`install.log` = **运行日志**(同文件上一条判据自己写着 # 「运行日志,⛔ 不属能力件」)⇒ 它里面「已备份 → …bak-session-mechanism-…」 # 是**安装时的事实陈述**,⛔ 不是注释里的死证据引用。 # ⚠️ 它是 **append 型**:每跑一次 `install.py --apply` 就多一行 ⇒ 不排除的话 # **判据会随安装次数必然变红**(2026-10-05 实测:装完当场 FAIL 1,与本次改动无关)。 # 📌 判据本意="注释不许指向已删的备份";日志**不承担这个语义**。 if rel == "install.log": continue if rel == "references/pitfalls.md": # ⛔ 只豁免 P0-45 那一节(讲这个坑的),其它节的死证据仍要报 t_all = p.read_text(encoding="utf-8") i45 = t_all.find("## P0-45") head, tail = (t_all[:i45], t_all[i45:]) if i45 >= 0 else (t_all, "") nxt = tail.find("\n## P0-", 4) sec45 = tail[:nxt] if nxt > 0 else tail for m in pat.finditer(head): dref.append("%s:%s" % (p.name, m.group(0)[:40])) continue # ⛔ P0-45 节内的举例不计入 try: t = p.read_text(encoding="utf-8") except Exception: continue for m in pat.finditer(t): dref.append("%s:%s" % (p.name, m.group(0)[:40])) res.append(("⛔ 无「见备份 X」式死证据引用(备份已删,注释不能再说)", not dref)) # 变更流水账:frontmatter 的 last_change 行不许再超长(曾 55 KB / 全文 66%) # ⚠️ 判据只查**行体积**;⛔ 不查「本条为准」字样 —— 2026-10-04 实测那个词在新台账 L6 与 # L24/L98/L267(各自成段的历史改法说明)里都合法存在,拿它当判据只会误报。 # 🔴🔴 2026-10-04 **阈值 1 KB → 2 KB**(用户口径:判据守的是**口径**,⛔ 不是守某个数字): # 病根:1 KB 是照**当年那次**(339 B)定的,属于「按当时观测值钉死」⇒ # 台账只要多一条**同量级**的现行口径(1241 B)就红,而被砍掉的恰恰是 # "为什么这么做"这类**必须留**的内容 ⇒ 判据开始**逼人删有用信息**(本末倒置)。 # 真正要防的是「**一行长成 55 KB 的流水账**」⇒ 那个量级已被下面的 2 KB 正文判据拦死。 # ⚠️ 与下方"正文无超 2 KB"**同阈值**,口径统一(⛔ 不再出现两个数字互相打架)。 skl = (HERE.parent / "SKILL.md").read_text(encoding="utf-8").splitlines() l6 = skl[5] if len(skl) > 5 else "" _l6b = len(l6.encode("utf-8")) res.append(("⛔ `SKILL.md` 末行不超 **2 KB**(当年 55 KB = 全文 66%% ⇒ 它是**台账**不是日记;" "⚠️ 阈值 2026-10-04 由 1 KB 上调,⛔ 守的是「不许长成流水账」这个口径、不是 339 B 这个数)", _l6b <= 2048)) #⛔ 真正要盯的是「正文有没有哪一行又长成流水账」(当年就是 L6 一行 55 KB)。 # ⚠️ 两条**该长的**必须豁免,否则判据永远红(2026-10-04 实测): # ① L3 `description:`(2.2 KB)—— 技能描述本就是长句,⛔ 不该压; # ② **已标作废**的历史段(如 L234「同工作区 = 四类会话 … 已于 2026-10-03 作废」)—— # 它长是因为**把作废原因写全了**,删掉就丢了"为什么废"。 body = [(i, len(l.encode("utf-8"))) for i, l in enumerate(skl, 1) if not l.startswith("description:") and "作废" not in l and len(l.encode("utf-8")) > 2048] res.append(("⛔ `SKILL.md` 正文无超 2 KB 的行(⛔ 当年 L6 单行 55 KB = 全文 66%%;" "⛔ 已标作废的段与 description 豁免)", not body)) if dref or dead or bak: res.append(("⚠️ 具体残留:%s" % ((bak[:2] + dead + dref[:2]) or "-"), True)) return res @case("告警文案:**必须说清后果**(⛔ 不许只说「缺了 A」)") def t_warn_text_states_consequence(): """🔴 2026-10-03 用户逐字指正:「**标题里没读类别前缀) 这句话都不知道有什么作用**」。 🔴 **它错在哪**:原文案只陈述"读不到类别前缀"这个**现象**, ⛔ **没说缺了会怎样** ⇒ 读者不知道该做什么(看板上它跟正常文案长得一样, 只是短一点 —— 这种"只描述现象"的告警=**没有行动价值的废话**)。 🔴🔴 2026-10-03 **我自己犯了这条**(用户当场抓住):第一版我把它改成 「⚠ 无类别 ⇒ **不会被派活**」—— 🔴 **那个后果是我编的**。 取证:`collabd.py::queue_pending()` 读 **`tasks.json` 台账**,`topic` 在派活链路上 **根本没出现**;而这排 `cells` **已经是** `role==='任务会话'` 筛出的**现存任务会话**, ⛔ 不是"待派活清单" ⇒ "不会被派活"=**说了个不存在的后果**。 ✅ **正确口径=只说能证实的后果**:`⚠ 归不出类别 · 不计入分工` (依据:`board.py` 归线判据 `topic == 线 ∨ cwd_tail == 线` ⇒ topic 空即不归任何线)。 📌 **本条判据升级后的红线**:⚠️ **写"会怎样"之前必须先取证那个后果真的存在** —— 否则"讲清后果"会变成"**编一个后果讲得更像回事**",比原句更坏。 ✅ 本条判据是**通用红线**,不是这一句话的专属: 凡"⚠/缺了/没读"类文案,**必须同时出现一个后果词**(会…/不会…/导致…)。 """ import re as _re6 html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8") # 剥掉注释 ⇒ 只查**真正渲染给用户看的文案**(注释里的原话留档不算) code = _re6.sub(r"/\*.*?\*/", "", html, flags=_re6.S) code = _re6.sub(r"(?m)^\s*//.*$", "", code) code = _re6.sub(r"//[^\n]*", "", code) # ⚠️ 只看**纯文案字面量**(`tx(...)` / `chip(...)` 这类调用的**第一个字符串参数**) # ⛔ 不用"整段代码里抓 ⚠ 段"那种粗法 —— 实测它会**跨语句边界**把 # `(⛔ 不因此判完成)` 之类切到下一个字面量里 ⇒ **8 条里 7 条是误报**。 # ✅ 锚定 `tx(` / `chip(` 之后的第一个引号串,才是用户真正看到的句子。 lits = [] for m in _re6.finditer(r"(?:tx|chip|lbl)\(\s*(['\"])(.*?)\1", code, _re6.S): s_txt = m.group(2) if "⚠" in s_txt: lits.append(s_txt) # ⚠️ 后果词表**必须包含「不…」式否定** —— 例「⚠ 未声明验收判据(⛔ 不因此判完成)」 # 它**已经**把后果说清了(不判完成),⛔ 只因原表里只有"会/不能/无法"而漏判。 # 📌 教训:写判据时**先拿真实文案跑一遍**(本次就是这样才发现自己错,而不是去改好文案)。 bad = [w for w in lits if not _re6.search(r"会|导致|漏管|不能|无法|勿|别|不因此|不算|勿当", w)] return [ ("渲染文案里**没有**「只说现象不说后果」的告警(查到 %d 条可疑:%s)" % (len(bad), bad[:3] or "无"), not bad), # 🔴🔴 2026-10-03 **用户直接指示:那一行整个删掉**(逐字:「越写越看不懂 # 还是删除了把」)⇒ 这里从"要求它写对"改成"**不许它回来**"。 # 📌 两次踩坑留下的结论:① 原文案只说现象(啰嗦)② 我"改成讲后果"又编了后果 # ⇒ **第三次选择是删掉**:这行信息量不足,⛔ 不值得占版面、更不值得编文案。 ("⛔ 任务会话格**不许**再出现类别/前缀相关文案(用户已指示删除)", ("归不出类别" not in code) and ("没读类别前缀" not in code) and ("类别:'+W.topic" not in code)), ("⛔ 那句编出来的后果也不许回来(`不会被派活`)", "不会被派活" not in code), ] def _worker_label_from_source() -> str: """**AST 抽** `board.py::_ROLE_LABEL['worker']` 的字面量(唯一真源)。 🔴 2026-10-03 23:4x 新增:标签从「协作目标」改名成「执行会话」时, 判据里**写死**的那份字面量**当场报红**(这是它该做的), 但也意味着**每次改名都得手改判据** ⇒ 漏一处就假红。 ✅ 正解=**判据从真源派生**:抽出来用 ⇒ 改名只改 `_ROLE_LABEL` 一处,判据自动跟随。 ⚠️ 抽不到 ⇒ 返回空串(让下游断言**报红**,⛔ 绝不猜一个词填上去)。 """ import ast as _ast try: t = _ast.parse((HERE / "board.py").read_text(encoding="utf-8")) except Exception: return "" for n in t.body: if not isinstance(n, _ast.Assign): continue tgt = n.targets[0] nm = getattr(tgt, "id", None) or getattr(tgt, "attr", None) if nm != "_ROLE_LABEL": continue for k, v in zip(n.value.keys, n.value.values): if getattr(k, "value", "") == "worker" and isinstance(v, _ast.Constant): return str(v.value) return "" @report("产物落点:**每个目标一个独立文件夹**(⛔ 代码与其他路径除外)|**报告型**") def t_artifacts_land_in_goal_dir(): """🔴 2026-10-04(用户要求逐字):「**检查是否每个工作区 进行 执行任务时每个目标都有独立的 文件夹 保存生成的内容(要求处理其他路径下的文件 代码 等除外)**」 🔴 **为什么补这条**(实测坐实的缺口):机制本身有(`goal_dir_name()`/`_ensure_goal_dir()`/ `t_goal_dir_mechanism`)⇒ ⛔ 但**那条只守"机制在不在",不守"产物真的落进去了吗"**。 实测反例:测试3 根目录长出 `新环境验证/`、`a6-final-check/`(2 份 S2/S3 取证 + 6 个临时修库脚本)**散在目标文件夹之外** ⇒ 机制在、结果照样乱。 ✅ 本条判据=**逐个真工作区**(⛔ 不造夹具,因为要验的就是真磁盘): ① 目标文件夹**存在**(按 `goal_dir_name()` 现算,⛔ 不写死目录名); ② **产物型 `.md` 必须落在目标文件夹或 `交付物/` 内**; ③ ⛔ **豁免**:`CODEBUDDY.md`/`AGENTS.md`/`README.md`(规则与说明,不是产物)、 `docs/`、`归档/`、`脚本/代码`(用户口径:「**处理其他路径下的文件 代码 等除外**」)。 📌 **豁免写清楚,否则这条判据会假红**:一个连"什么是产物"都没定义的检查=没用的检查。 ⚠️ 本条只**报告与提示**,⛔ **不删不移任何文件**(清理是用户的事,且可能是别人写的)。 """ # 产物型文档:⛔ 排除规则类/说明类文件名(它们天生在根目录,不是产物) RULE_FILES = {"CODEBUDDY.md", "AGENTS.md", "README.md", "MEMORY.md", "state.py"} # 🔴 `inbox`=**机制自己的状态/信号文件**(NEXT/digest/queue/advance…), # ⛔ **不是产物** —— 第一版没豁免它 ⇒ 报出一堆假散落(实测四个区都报)。 EXEMPT_DIRS = ("docs", "归档", "scripts", "脚本", "tmp", ".workbuddy", "done", "__pycache__", "inbox") # 🔴🔴 2026-10-04 **第三版才算对**:`imp()` 及其它用例会**永久改写** `DSH_COLLAB_WS` # 指向**测试夹具**且不还原(实测 `selftest.py:184`)⇒ **跑到本用例时读它必是夹具**。 # ⇒ 真工作区只能用**模块加载时定下的 `WS`**(第 57 行,在 env 被改之前取的)。 # 📌 纪律:**判据里凡要"真工作区",一律用 `WS`,⛔ 不许现读 `os.environ`**。 ws = Path(WS) if not ws.is_dir(): return [("SKIP:没指到真工作区(设 `DSH_COLLAB_WS=<工作区>` 才会真跑)", True)] res = [] # ① 目标文件夹存在 # 🔴🔴 2026-10-04 **前两版都算错(留档)**: # ① 第一版:直接 `exec_module` 但没设 `COLLABD_CONFIG` ⇒ `INBOX` 回落 ⇒ 报「未命名目标」。 # ② 第二版:设了 env,但**加载的是 `HERE/collabd.py`(技能目录那份)** ⇒ 它的 # `_cfg_candidates()` 里 `DSH_COLLAB_WS` 已被 `imp()` 指向**测试夹具** ⇒ 又回落。 # ⚠️ 更根子的一条:判据里**改 env 后再 `exec_module`**,而 `selftest` 顶层 # 已经跑过 `imp()`(它把 env 设为夹具)⇒ **我的设值与它的设值打架,谁最后生效不确定**。 # ✅ **正解=开独立子进程**(⛔ 不与 selftest 的 env 抢):真区自己的副本 + 干净 env # ⇒ 拿到的是**真磁盘读数**,这才是本条要验的东西(同 P0-39:判据不许读错世界)。 import subprocess as _sb3 _own = ws / ".workbuddy" / "collab" / "collabd.py" if not _own.is_file(): return [("⛔ 该工作区没有自己的 collabd.py 副本 ⇒ 判据无法验真实部署", False)] _py = sys.executable _code = ( "import importlib.util,sys,json\n" "sp=importlib.util.spec_from_file_location('c',%r)\n" "m=importlib.util.module_from_spec(sp)\n" "try: sp.loader.exec_module(m)\n" "except SystemExit: pass\n" "print(json.dumps({'inbox':str(m.INBOX),'dir':m.goal_dir_rel()},ensure_ascii=False))\n" % str(_own)) _env = {"COLLABD_CONFIG": str(ws / ".workbuddy" / "collab" / "collabd.config.json"), "COLLABD_WORKSPACE": str(ws)} try: _r = _sb3.run([_py, "-c", _code], env={**os.environ, **_env}, capture_output=True, timeout=60, cwd=str(ws)) _j = json.loads((_r.stdout or b"{}").decode("utf-8", "replace") or "{}") except Exception as _e: return [("⛔ 独立子进程取真值失败:%r" % _e, False)] _gdir = ws / (_j.get("dir") or "") res = [("目标文件夹存在:%s(独立进程按 `goal.json` 算出,INBOX=%s)" % (_j.get("dir"), _j.get("inbox")), bool(_j.get("dir")) and _gdir.is_dir())] # ② 产物型 md 落点 # 🔴🔴 2026-10-05:目标文件夹收进 `执行会话/` 一层(用户定案「<工作区根>/执行会话/目标-xxx/ 改成这样」) # ⇒ 落点判据必须**跟着机制走**(同族第 N 次:判据没跟上改动 ⇒ 假红/假绿)。 # ⚠️ 判据从「`parts[0].startswith('目标-')`」扩成**两级**: # `执行会话/目标-xxx/…` 与**裸 `目标-xxx/…`(存量旧目录)都要认** # —— ⛔ 硬切到新形态会把历史产物全判成"散在别处"(那是**误报**,不是事实)。 # 📌 只认 `执行会话/` 下一层的 `目标-*`,⛔ 不认 `执行会话/` 里别的杂物 # (否则往 `执行会话/` 随便丢个 md 就能骗过判据)。 stray, inside = [], 0 for p in ws.rglob("*.md"): rel = p.relative_to(ws) parts = rel.parts if parts[0] in EXEMPT_DIRS or str(rel) in RULE_FILES: continue if p.name in RULE_FILES or p.name.startswith("CODEBUDDY"): continue # 只看「顶层 目标-*|执行会话/目标-*|交付物|其他一层目录」这一层,⛔ 不递归进目标文件夹内部 if len(parts) > 1 and parts[0].startswith("目标-"): continue if (len(parts) > 2 and parts[0] == "执行会话" and parts[1].startswith("目标-")): continue if len(parts) == 1: stray.append(str(rel)); continue if parts[0] == "交付物": inside += 1; continue stray.append(str(rel)) res.append(("产物型 md 都落在「执行会话/目标-*/ 或 目标-*/ 或 交付物/」内(⛔ 代码/规则/归档除外);" "已在目标文件夹或交付物=%d,散在别处=%d %s" % (inside, len(stray), ("→ " + str(stray[:5])) if stray else ""), not stray)) return res @case("🔴🔴 `ensure_supervise` spawn 孙进程**必须传 `COLLABD_CONFIG`**(否则起后即退)") def t_ensure_passes_config_to_child(): """🔴 2026-10-04 **实测坐实**(`vibe-product` 停 16.4 h 的真因)。 症状:`--ensure` 报 `{"ok": false, "spawned": true}` ⇒ **看起来起好了,其实秒退**; `supervise.out.log` 里只有 spawn 记录、无异常;`tasklist` 查 pid **已不在**。 病根(`collabd.py::ensure_supervise()` 的 `subprocess.Popen`): · `cwd` =**副本目录**(`/.workbuddy/collab`) · `load_cfg()` 找配置顺序=`COLLABD_CONFIG` → **`/.workbuddy/collab/collabd.config.json`** · ⛔ **spawn 时没传 `env`** ⇒ 孙进程拿不到 `COLLABD_CONFIG` ⇒ 回落 `<副本目录>/.workbuddy/collab/…` ⇒ **不存在** ⇒ `CFG_MISSING` ⇒ 秒退。 ⚠️ 为什么难查:`spawned=True` + 日志无异常 + 我**自己也用这个有缺陷的 `--ensure` 去补起** ⇒ 它报"起好了"我信了 ⇒ 一次也没验到真活(今天第 N 次「工具报成功我照抄」)。 """ src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace") R = [] R.append(("`ensure_supervise` 的 `Popen` **带 `env=`**(⛔ 不传 ⇒ 孙进程丢 `COLLABD_CONFIG`)", re.search(r"Popen\(\[[^\]]*PYW[^\]]*--supervise[^\]]*\][\s\S]{0,400}?env=_env", src) is not None)) R.append(("`env` 取自**真来源全局 `CFG_USED`**(⛔ 不是 `C` 里的键 —— `C` 是配置内容字典)", re.search(r'if not _env\.get\("COLLABD_CONFIG"\) and CFG_USED:', src) is not None)) R.append(("⛔ 代码里**没有编造的 `_path_used` 键**(我第一版就写了它 ⇒ 取不到 ⇒ 修复等于没做)", "_path_used" not in src)) R.append(("源码里留着**「起后即退」的成因注释**(⛔ 防后人再踩)", "起后即退" in src and "CFG_MISSING" in src)) # 行为判据:⛔ 真起一条,看它**活过第一轮**(⛔ 不看返回的 ok/spawned,那些不可信) try: import importlib.util env2 = dict(os.environ) env2["COLLABD_CONFIG"] = str(HERE.parent.parent).replace("\\", "/") _sp = importlib.util.spec_from_file_location("_cd_ens", HERE / "collabd.py") m = importlib.util.module_from_spec(_sp); _sp.loader.exec_module(m) import time as _t _t.sleep(0.3) cfg_src = m.CFG_USED R.append(("本进程真读到配置(`CFG_USED` = %s)⇒ 有得传给孙进程" % (os.path.basename(str(cfg_src)),), bool(cfg_src) and not m.CFG_MISSING)) except Exception as e: R.append(("⛔ 读 collabd 失败:%r" % (e,), False)) return R @case("🔴 钩子真接上了:工作区从 stdin.payload.cwd 取(⛔ 不是 DSH_WS_ROOT)") def t_hook_reads_ws_from_stdin_cwd(): """🔴🔴 2026-10-04 用户问「为什么 vibe 会话说技能没有调整」时挖出的**第二个**真缺陷。 病根链(三层,每层都"看起来没问题"): ① 我上一版 `supervise-ensure-hook.py` 只读 `os.environ["DSH_WS_ROOT"]`; ② `settings.json` 的 hook 条目**没有 env 字段** ⇒ 宿主 env 里该变量 **实测三级全空**(Process/User/Machine)⇒ 第①步就 `_noop()` 返回; ③ 而它的表现是「**零输出、rc=0**」—— 与"正确沉默"**完全一样** ⇒ 我据此报了"钩子已挂上、修好了",**它一次都没生效过**。 ⚖️ 同源判据的既有正确写法(⛔ 必须对齐,不能自创): `wb-result-hook.py` / `lock-guard-hook.py` 都是**先读 stdin 的 `payload.cwd`**,env / `roots.env` 只作兜底。 🔴 判据要点:⛔ **不许只查"读没读 cwd"**——还要钉住 ① stdin **只读一次**(`sys.stdin.buffer.read()` 是一次性的) ② `os.environ` 必须被**真改写**(`env = dict(os.environ)` 对 `exec_module` 里的 `load_cfg()` **无效**) ③ 加载后必须**核对 `CFG_USED` 真的指向本区**,否则会验错对象。 """ H = HERE / "hooks" / "supervise-ensure-hook.py" R = [] R.append(("钩子文件存在", H.is_file())) if not H.is_file(): return R src = H.read_text(encoding="utf-8", errors="replace") R.append(("🔴 主路径读 **stdin 的 `payload.cwd`**(⛔ 不能只读 `DSH_WS_ROOT`)", 'get("cwd")' in src and "_stdin_payload" in src)) R.append(("`DSH_WS_ROOT` 只作**兜底**(⛔ 不能当唯一来源)", '_ws_root() or (os.environ.get("DSH_WS_ROOT")' in src)) R.append(("🔴 **stdin 载荷有缓存**(⛔ `sys.stdin.buffer.read()` 一次性," "读两次第二次必空 ⇒ 零输出假绿)", "_PAYLOAD" in src and "if _PAYLOAD is None" in src)) R.append(("🔴 `COLLABD_CONFIG` 改的是 **`os.environ`**(⛔ `env = dict(...)` " "对 `exec_module` 无效 ⇒ `load_cfg()` 读真进程环境)", 'os.environ[cfg_env_key] = _cfg_path' in src)) R.append(("🔴 加载后**核对 `CFG_USED` 指向本区**(⛔ 不核对=验错对象=假绿)", "CFG_USED" in src and "_used" in src)) R.append(("起不来时**必须报出来**(10-04 用户原话「起不来就先报错」,⛔ 不许静默)", "常驻没起来" in src and "先修好这个再往下做" in src)) R.append(("`--ensure` 参数名正确(⛔ 没有 `--ensure-supervise`)", '"--ensure"' in src)) # 🔴🔴 2026-10-04 改判据:原来钉的是 `"capture_output=True, timeout=40"` # ⇒ 收超时隐患(40→15)后**判据自己红了**,成了"守着旧值"的绊脚石。 # ⛔ 判据的职责是**守住"不看返回码"这个口径**,⛔ 不是守住某个超时数字。 # ✅ 正解=判**形状**:`subprocess.run(...)` 的实参里**没有** `check=`, # 且结果**没被赋给任何变量/没取 `.returncode`**(=真的没看返回码), # 同时回验走 `supervise_alive()`(唯一可信判据)。 # ⚠️ 这样收超时值(乃至换回验次数)都⛔ 不会再假红。 import re as _re_en _m_en = _re_en.search(r"subprocess\.run\(\[PY, str\(COLLABD\), \"--ensure\"\](.*?)\)\n", src, _re_en.S) _args = _m_en.group(1) if _m_en else "" R.append(("⛔ 不看 `--ensure` 的返回码(⛔ `spawned:true` 会骗人 ⇒ 等活再问)", bool(_m_en) and "check=" not in _args and "returncode" not in src.split("_ENSURE_SUBPROC_TIMEOUT")[-1][:1200] and "supervise_alive()" in src)) # —— 行为判据(⛔ 这才是能分辨 A/B 的那一条) R.append(("⛔ 行为:**喂真 payload(`cwd` 指向一个只有 cfg+goal 的合成区)" "⇒ 必须注入「已自动补起」", *_hook_behaves())) return R @case("🔴 T 表 §1.6:起不来先报错(10-04 用户原话「必须首先启动好在执行」)") def t_t16_stop_when_supervise_fails(): """🔴 这条红线**当天说了两次没落进技能** ⇒ 补上并钉住。 用户原话:「**加个规则 后台任务和检查程序如果不能启动,就先报错解决了问题 在执行完成目标的任务**」「**必须首先启动好在执行**」。 ⚠️ 之前只停在**工作区日志**里(⛔ 换会话读不到 = 没立)。 """ S = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace") R = [] R.append(("SKILL.md 有 **§1.6 起不来先报错** 这一节", "1.6 起不来 = **先报错、先解决,再执行目标**" in S)) R.append(("逐字锚用户原话「必须首先启动好在执行」", "必须首先启动好在执行" in S)) R.append(("写明**顺序**(在位才允许往下 / 不在位先起)", "在位" in S and "才允许往下建会话" in S)) R.append(("写明**起不来时三条不许**(不许边起边干/不许只说起好了/不许编原因)", "不许\"边起边干\"" in S or "不许“边起边干”" in S or "边起边干" in S)) R.append(("写明**三条边界**(没目标不起也不报/只管在位这一件事/判据只有 pid∧心跳)", "基础会话方式" in S and "只在位这一件事" in S.replace(",", ",") or "只管「常驻在不在位」这一件事" in S)) R.append(("指明**由钩子执行**、⛔ 不是靠我记得(能力已接线)", "supervise-ensure-hook.py" in S and "不是靠我记得" in S)) return R def _hook_behaves(): """真喂 payload 跑一次钩子,验它**看得见工作区**(不是只查代码文本)。 🔴 为什么必须真跑:本次两个缺陷**代码文本全对**(第一版也确实读 `cwd`), 栽在「stdin 读两次」+「env 没进真进程」⇒ **纯文本判据会假绿**。 ⚠️ 用**合成区**而不是真实区:真实两区常驻都在位 ⇒ 零输出, 而零输出同时可能是「看见了+在位」或「⛔ 根本没看见」⇒ 分不出来。 """ import shutil import subprocess import tempfile PY = sys.executable H = HERE / "hooks" / "supervise-ensure-hook.py" ws = Path(tempfile.mkdtemp(prefix="sm_hooktest_")) try: (ws / ".workbuddy" / "collab" / "logs").mkdir(parents=True, exist_ok=True) (ws / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True) (ws / ".workbuddy" / "collab" / "collabd.config.json").write_text( json.dumps({"schema": 1, "workspace": str(ws)}), encoding="utf-8") (ws / "tmp" / "supervise-inbox" / "goal.json").write_text( json.dumps({"short": "hooktest", "title": "hook selftest", "lifecycle": "进行中", "topics": ["自检"]}), encoding="utf-8") pl = json.dumps({"cwd": str(ws).replace("\\", "/"), "prompt": "继续完成目标"}, ensure_ascii=False) p = subprocess.run([PY, str(H)], input=pl.encode("utf-8"), capture_output=True, timeout=90) out = p.stdout.decode("utf-8", "replace") ctx = "" try: ctx = (json.loads(out).get("hookSpecificOutput") or {}).get("additionalContext") or "" except Exception: pass ok = "已自动补起" in ctx # 收尾:合成区起出来的常驻必须收掉,⛔ 不留进程 try: hb = ws / ".workbuddy" / "collab" / "logs" / "supervise-heartbeat.json" pid = json.loads(hb.read_text(encoding="utf-8-sig")).get("pid") if pid: subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"], capture_output=True, timeout=30) except Exception: pass return (ok,) except Exception as e: return ("⛔ 行为判据跑不起来:%r" % (e,),) @case("🔴 T 表:状态 → 我该做什么(2026-10-04 用户口述 · ⛔ 不许 AI 推断)") def t_state_to_action_table(): """🔴 这条是**「一天栽三次」的直接对策**。 2026-10-04 我三次把**自己推断的**东西当成需求/欠项: ① 「开机自启/计划任务」⇒ 用户:「从来没说过什么开机自启」 ② 「三类会话要建齐」 ⇒ 那口径 10-02 就作废了 ③ 「两类会话要建齐」 ⇒ 现行是「要落事才显式建」 三次的共同根因:**技能只有「你要干什么 → 去看哪篇」(知识组织), 没有「现在什么状态 → 下一步做什么」(行动组织)** ⇒ 查到一条知识就以为「知道该做什么了」。 ✅ 解法=把这张表**落进 SKILL.md**(⛔ 落在 `references/` 里没人先看到), 并且**三条红线里最要紧的那条必须钉死**:不许把「实体里存在」读成「该有」。 """ S = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace") R = [] R.append(("SKILL.md 有 **T 表**(状态 → 我该做什么)", "T 表 · 状态 → 我该做什么" in S)) R.append(("ⓘ 写明它**存在的原因**(⛔ 防后人当成又一份知识文档删掉)", "知识组织" in S and "行动组织" in S)) # —— 逐字锚用户原话(⛔ 今天的第 12 次教训:锚点必须逐字,⛔ 不能凭记忆重写) R.append(("写明**会话生命周期**(用户原话逐字:主会话才建一轮,后续无意外走检查进程)", "才建立一轮执行会话" in S and "后续无意外都是 检查进程创建" in S)) R.append(("写明**只有第①步是主会话建任务会话**,②主会话收手", "只有这一步是「主会话建执行会话」" in S and "主会话到这一步就收手" in S)) # —— 「这轮结束了」的判据 R.append(("写明**靠投递判完成**(用户原话:执行会话完成会往检查程序队列投递)", "向检查程序队列投递执行情况" in S)) R.append(("写明 **`done` 必须带 `--artifact`**(10-03 用户逐字要求,⛔ 拒收无产物的 done)", "必须**带 `--artifact`**" in S or "必须带 `--artifact`" in S)) R.append(("写明**台账是唯一权威** + ⛔ 别读快照" "(⚠️ `acceptance_state` 记着 10-02 的 pid 8024,实际已是 19424)", "唯一权威" in S and "8024" in S)) # —— 检查会话不能固定席位 R.append(("写明**检查会话不能固定席位**(用户原话:没办法固定,有记录吗)", "没办法固定" in S or "不许预先摆一条「固定检查会话」" in S)) # 🔴 钉**唯一位置**(T 表那一行),⛔ 不全文搜两个词 # —— 旧写法 `台账空 in S and 不许**建任务会话 in S` 被变异 V3 打不穿: # 改表格那一行,两处关键词在**别处**(§1 甘特图行)仍命中 ⇒ 恒绿。 import re as _re5 _row = [ln for ln in S.splitlines() if ln.strip().startswith("| 台账空")] R.append(("⛔ T 表「台账空」那一行写明**不许建任务会话**(⛔ 不许造需求;" "⚠️ 必须钉**这一行**,⛔ 全文搜会被别处的同词打穿)", bool(_row) and "不许" in _row[0] and "建任务会话" in _row[0])) # —— 🔴 前置条件层(2026-10-04 用户补充:「如果主会话下没有 后台任务 或 # 检查程序 还需要启动常驻程序」⇒ 三层串起来,⛔ 不许跳过中间那层) R.append(("写明**三层是串起来的**(常驻 → 检查会话 → 那一轮任务会话)", "1.5 前置条件" in S and "唯一载体" in S)) R.append(("写明**常驻是「建检查会话」那条腿的唯一载体**(⛔ 跳层=建了也接不上)", "没有常驻 ⇒ 检查会话永远建不出来" in S)) R.append(("写明**真顺序 0-a/0-b/0-c**(⛔ 顺序不能颠倒)", "0-a" in S and "0-b" in S and "0-c" in S)) R.append(("0-a 量的是 **`supervise_alive()`**(pid 活 ∧ 心跳新鲜)⛔ 不看快照", "supervise_alive()" in S and "心跳新鲜" in S)) # 🔴🔴 2026-10-04 改:闸① **收窄**(用户口径「检查程序必然要去处理队列」) # ⇒ 旧断言「闸① 生命周期必须「进行中」」**已经过时**,不改它会**红着掩盖新口径**。 # ✅ 新断言=钉**收窄后的口径**:只对 `queue-empty` 生效,`sessions-ended` 不受目标状态限制。 R.append(("0-b 点明**闸① 只对 `queue-empty` 生效**(代码五道闸之①,2026-10-04 收窄)", "进行中" in S and "`queue-empty` 那条不动" in S)) R.append(("⛔ 写明**跳过常驻直接建会话 = 接不上**(检查会话只能靠排期开)", "跳过常驻直接建会话" in S)) # 🔴 钉**状态表那两行**(唯一位置),⛔ 不全文搜 # —— 「基础会话方式」全文出现 3 处,V1 变异只改一处 ⇒ 全文搜**恒绿**(今天第 16 次) _r0b = [ln for ln in S.splitlines() if ln.strip().startswith("| **0-b**")] _r0t = [ln for ln in S.splitlines() if ln.strip().startswith("| **没有目标**")] R.append(("📌 **0-b 那一行**写明「没有目标 ⇒ 基础会话方式,⛔ 那不是故障」" "(⛔ 必须钉**这一行**)", bool(_r0b) and "基础会话方式" in _r0b[0] and "不是故障" in _r0b[0])) R.append(("📌 **状态表「没有目标」那一行**判它是**正常**(⛔ 什么都用不起)" "(⛔ 钉**这一行**)", bool(_r0t) and "正常" in _r0t[0] and "什么都不用起" in _r0t[0])) R.append(("⛔ 状态表里「**没有目标**」那一行写明**⛔ 不许说成缺目标/缺配置**", "不许**说成「缺目标/缺配置」" in S or "不许**说成「缺目标" in S)) # —— 🔴 状态表里「**目标已完成**」那一档(10-04 补:原来表里只有进行中/等待/没有目标) # ⛔ 必须**钉那一行**(唯一位置)—— 全文搜「已完成」会命中几十处 ⇒ 恒绿 _rdone = [ln for ln in S.splitlines() if ln.strip().startswith("| **目标 `lifecycle=已完成`**")] R.append(("🔴 状态表有「**目标 lifecycle=已完成**」那一档,且写明**含带后缀**形态" "(⛔ 钉这一行)", bool(_rdone) and "带后缀" in _rdone[0] and "已完成(机器可判部分)" in _rdone[0])) # 🔴🔴 2026-10-04 改:旧断言写「**⛔ 不再建任何检查会话**(闸①不过=设计)」—— # 那句「这是设计,不是故障」正是**被用户当场否掉**的结论(P0-56)⇒ 断言必须跟着翻。 # ✅ 新口径=**先看台账**:还有未完成件 ⇒ 照建 `[结果检查]`;全 done/空 ⇒ 收工。 R.append(("🔴 那一档写明 **先看台账**:还有未完成件 ⇒ **照建 `[结果检查]`**(闸① 不拦它)", bool(_rdone) and "还有活没人干" in _rdone[0] and "[结果检查]" in _rdone[0])) R.append(("🔴 那一档写明 **全 done/空 ⇒ 收工**(⛔ 不再一律收工 —— 那是旧口径)", bool(_rdone) and "收工" in _rdone[0] and "不再建任何检查会话" not in _rdone[0])) R.append(("🔴 那一档写明 **⛔ 不许把「已完成」说成「等待」**(语义相反,实测栽过)", bool(_rdone) and "语义相反" in _rdone[0])) # —— 🔴 0-b 那条硬约束:**读数必须问代码,⛔ 不许肉眼读 goal.json 原文** R.append(("🔴 写 0-b 时**必须问代码**(⛔ 不许肉眼读 `goal.json` 原文比对)——" "正是被带偏的那个入口", "不要自己读 `goal.json` 的原文再肉眼比对" in S)) R.append(("⛔ 明写**⛔ 不许我代他设目标**(想让它有 ⇒ 用户开)", "不许我代设目标" in S or "不许**代他设目标" in S)) # —— 行为判据:现读本区 lifecycle,验闸① 真的过不了 try: import json as _j2 gp = WS / "tmp" / "supervise-inbox" / "goal.json" life = None if gp.is_file(): life = str((_j2.loads(gp.read_text(encoding="utf-8")) or {}).get("lifecycle") or "") # 🔴 验的是「**读数可判**」,⛔ **不预设它该是什么** —— # 原写法硬编码 `life != "进行中"`(那是**本区当时**的状态 None) # ⇒ 同一份判据在副本区(lifecycle='进行中')报红 ⇒ **验错对象**(今天第 15 次)。 # 🔴🔴 口径订正(用户 2026-10-04:「没有目标就说明 当前用户是用的是基础会话方式」): # ⛔ **不许**把「非进行中」写成需要修的缺口 —— 那不是故障,是**用户在用基础会话方式**。 # ⇒ 判据只验「**读得到、且能据此判**」,⛔ 不断言「本区该不该有目标」。 R.append(("现读本区 `lifecycle`(=%r)—— 能据此判「是不是基础会话方式」即可" "(⛔ **不预设该是什么**)" % life, # 🔴🔴 2026-10-04 修:原来是一份**枚举白名单**,结果 `vibe-product` # 的 `'已完成(机器可判部分)'`(**带后缀**)不在名单里 ⇒ 副本区报红。 # ⛔ 那是**判据自己枚举不全**,不是真故障 —— 又一次「验错对象」。 # ✅ 正确判法=只验「**读得到、且非空或明确为 None**」, # ⛔ 不断言它是哪个值(值由各区用户自己定,机制无权预设)。 (life is None) or (isinstance(life, str)))) except Exception as e: R.append(("⛔ 读 lifecycle 失败:%r(**不假装通过**)" % (e,), False)) # —— 🔴🔴 行为判据:`goal_life()` 必须**剥括号后缀再判**(2026-10-04 真缺陷) # 病根(`vibe-product` 实测):`goal.json` 写 `"已完成(机器可判部分)"`, # 而 `goal_life()` 只做**精确匹配** ⇒ 静默回落成 `等待` # ⇒ 「已完成」被读成「等待」(**语义相反**)⇒ 日志/主会话全被带偏。 # ⚠️ 这条判据**必须在真模块上跑**(⛔ 不做文本 grep): # 文本判据只能证明"写了 split('(')",证明不了"真的能读出 已完成"。 try: # ⚠️ 本函数后面还有 `import json`(局部)⇒ 在它之前用 `json` 会 `UnboundLocalError` # ⇒ 这里**显式局部导入**,⛔ 不依赖外层作用域(实测就是这么挂掉的)。 import json as _j_gl import re as _re_life def _load_mod(): import importlib.util as _iu _sp = _iu.spec_from_file_location("cb_life", HERE / "collabd.py") _mm = _iu.module_from_spec(_sp) _sp.loader.exec_module(_mm) return _mm # ① 代码里确有剥后缀(文本层,只作辅助) _src = (HERE / "collabd.py").read_text(encoding="utf-8") R.append(("🔴 `goal_life()` 有**剥括号后缀**的分支(全角/半角各一条)", "(" in _src and '.split("(", 1)[0]' in _src and '.split("(", 1)[0]' in _src)) # ② 行为层:直接喂三种带后缀的字面量,断言读出**标准值** # ⚠️ 用**临时改 goal.json 再还原**(字节级),⛔ 不猜、不 mock # 🔴🔴 **必须改"本模块实际读的那个文件"**,⛔ 不许拿常量硬拼凑路径。 # 2026-10-04 实测两个坑(两个都栽过,判据因此**假红**过一整轮): # 坑1:写 `WS / "tmp" / "supervise-inbox"` —— `WS` 是**跑 selftest 的那个区** # (`DSH_WS_ROOT` 或 `cwd`),⛔ 不是被检模块的区。 # 坑2:写 `Path(_m_probe.INBOX) / "goal.json"` —— 看着"从模块取"很稳, # 但 `INBOX` 是**模块加载那一刻算出来的默认值**: # 没配 `COLLABD_CONFIG` 时它回落成 `<技能目录>/tmp/supervise-inbox` # (见实测:`scripts/tmp/supervise-inbox/goal.json`,`is_file=False`) # ⇒ 写进去的是**没人读的空目录**,`goal_life()` 照样读不到 ⇒ 永远 `等待` # ⇒ **判据假红**(测的是"文件不存在"这个副作用,⛔ 不是被测行为)。 # ✅ 正解=**用与被测函数同一条路径公式**: # `路径 = Path(root) / "tmp" / "supervise-inbox" / "goal.json"`, # 而 `root` 取 `WS`(模块自己算出来的区)。 # ⛔ 这样即使实现改了公式,也是**实现与判据一起改**(同步暴露), # 不会出现"判据偷偷指向第三处"的假红/假绿。 _m_probe = _load_mod() _gp = Path(_m_probe.WS) / "tmp" / "supervise-inbox" / "goal.json" _gp.parent.mkdir(parents=True, exist_ok=True) _bak = _gp.read_bytes() if _gp.is_file() else None try: _base = _j_gl.loads(_bak.decode("utf-8")) if _bak else {} _cases = [("已完成(机器可判部分)", "已完成"), ("已完成(机器可判部分)", "已完成"), ("进行中(第2棒)", "进行中"), ("已完成", "已完成")] _okall = True _det = [] for _lit, _want in _cases: _base["lifecycle"] = _lit _gp.write_bytes(_j_gl.dumps(_base, ensure_ascii=False, indent=2).encode("utf-8") + b"\n") _m_gl = _load_mod() # 重新加载,⛔ 不靠缓存 _got = _m_gl.goal_life() _det.append("%s→%s" % (_lit, _got)) if _got != _want: _okall = False R.append(("🔴 **行为**:带后缀字面量必须读成标准值(%s)" % ";".join(_det), _okall)) finally: if _bak is not None: _gp.write_bytes(_bak) # 字节级还原(⛔ 不碰行尾) # ③ ⛔ 认不出时**必须留痕**(不许静默回落 —— 否则"读错"与"本来没有"长得一样) R.append(("⛔ `goal_life()` 认不出字面量时**写日志**(⛔ 不许静默回落)", "目标状态字面量认不出" in _src and "log_throttled(" in _src)) # ④ 🔴🔴 **单一事实源**:判定算法只许有一份(用户口径「禁止重复判定标准」) # 病根(2026-10-04 实测):`peer_supervise_sweep()` 自己抄了一遍 # `str(g.get("lifecycle")).strip()` + 精确比较(**不剥后缀**) # ⇒ 与 `goal_life()` 对**同一个文件**给出**两个答案**。 # ✅ 正解=抽 `goal_life_of(root)`,`goal_life()` 只作薄封装。 # 判据=① 存在 `goal_life_of(root)` 定义;② `goal_life()` 的函数体**不含** # `get("lifecycle")`(自己判就是又抄了一遍);③ 跨区扫描**调** `goal_life_of(` # 而**不再**自己读 `lifecycle`。 _src_life = _src # 同一份源码 _body_gl = _re_life.search(r"^def goal_life\(\) -> str:(.*?)(?=^def )", _src_life, _re_life.S | _re_life.M) _body_sweep = _re_life.search(r"^def peer_supervise_sweep\((.*?)(?=^def )", _src_life, _re_life.S | _re_life.M) def _code_only(txt: str) -> str: """⛔ 只留**代码行**(去掉整行注释)。 ⚠️ 必须这么做:本文件的注释里**大量引用**旧写法当反面教材 (如「原来这里自己判 `get("lifecycle")`」)⇒ 裸串搜会把注释当代码 ⇒ 判据**假红**(实测:`peer_supervise_sweep` 那条就这么红的)。 """ return "\n".join(ln for ln in txt.splitlines() if not ln.lstrip().startswith("#")) R.append(("🔴🔴 **单一事实源**:判定算法只有 `goal_life_of(root)` 一份" "(⛔ `goal_life()` 只作薄封装、自己不许再读 `lifecycle`)", _re_life.search(r"^def goal_life_of\(root\) -> str:", _src_life, _re_life.M) is not None and bool(_body_gl) and 'get("lifecycle")' not in _code_only(_body_gl.group(1)))) R.append(("🔴 跨区扫描 `peer_supervise_sweep()` **调** `goal_life_of(`" "(⛔ 不许自己抄一遍 lifecycle 判定)", bool(_body_sweep) and "goal_life_of(" in _code_only(_body_sweep.group(1)) and 'get("lifecycle")' not in _code_only(_body_sweep.group(1)))) except Exception as e: R.append(("⛔ 跑 `goal_life()` 后缀判据失败:%r(**不假装通过**)" % (e,), False)) # —— 三条红线 R.append(("红线①**不在表里的状态 ⇒ 问用户**(⛔ 不许推断后当需求)", "凡不在上表的状态 ⇒ 问用户" in S)) R.append(("红线②**「实体里存在」⛔ 不等于「该有」**(当天栽的就是这条)", "不等于「该有」" in S)) R.append(("红线③**⛔ 不许把推断摊成欠项/风险/待办**(凭空造需求)", "不许把自己以为该有的东西摊成「欠项」" in S)) R.append(("红线③里**留了当天三次的实证**(开机自启/三类/两类 ⇒ ⛔ 空头红线不算数)", "开机自启" in S and "三类会话" in S and "两类会话" in S)) # —— 行为判据:拿真台账验「这轮结束了没有」能被判出来 import json # 🔴 路径**实测**得来,⛔ 不凭猜:测试台账在 `TEST_WS/inbox/`(`inbox` =配置键名); # ⛔ `supervise-inbox/` 是**生产**工作区的落点(今天凭猜写这个 ⇒ FileNotFoundError) tk = TEST_WS / "inbox" / "tasks.json" try: d = json.loads(tk.read_text(encoding="utf-8")) ts = d if isinstance(d, list) else (d.get("tasks") or []) states = [str(t.get("state") or t.get("status") or "") for t in ts] R.append(("真读台账**判得出「这轮结束了没有」**(`tasks.json` 可解析,%d 条)" % len(ts), True)) R.append(("台账里 **`done` 必带 `artifact`**(⛔ 判据:空的 done = 假完成)", all(t.get("artifact") for t in ts if str(t.get("state") or t.get("status")) == "done") or "done" not in states)) except Exception as e: R.append(("⛔ 台账读不出(%r)⇒ 这一条无法验(**不假装通过**)" % (e,), False)) return R @case("📇 文档索引:改流程/改机制前**必读三篇**在第一屏 + 文档四条规则在位") def t_doc_index(): """🔴 落它的起因(用户原话):「**实在文件多看不过来 就建立一个索引**」。 ⚠️ **不是"整理归档",是"防复发"**:10-03 立的「⛔ 写『会怎样』前先取证那个后果」 只躺在**工作区日志**(197 KB)⇒ 换会话读不到 ⇒ 10-04 又编了一次后果。 ⇒ 索引的真正作用:**让"下次必须做到的事"落在会被自动加载的地方**。 📌 本判据量的四件事(⛔ 少一件都等于索引没建): ① 索引档在位 ② SKILL.md **第一屏**挂「必读三篇」 ③ 文档四条规则逐字在位 ④ ⛔ 别让它烂掉(新文档**必须**登记在索引里 —— ⛔ 没登记=别人找不到=白写) """ G = HERE.parent IDX = G / "references" / "01-文档索引.md" R = [] R.append(("📇 索引档在位 `references/01-文档索引.md`", IDX.is_file())) if not IDX.is_file(): return R idx = IDX.read_text(encoding="utf-8", errors="replace") skill = (G / "SKILL.md").read_text(encoding="utf-8", errors="replace") # ① 必读三篇:⛔ 必须是**三篇具体文件名**(⛔ 写"相关文档"等于没写) for f in ("00-动手前必过.md", "rules.md", "manifest.md"): R.append(("索引里点名必读 `%s`" % f, f in idx)) R.append(("索引里有**「改流程/改机制前」这个入口**(⛔ 索引最常见的用法就是它)", "改流程" in idx and "改机制" in idx)) R.append(("⛔ 明写**别通读**(⛔ 索引的意义就是不用全看)", "别通读" in idx)) # ② SKILL.md **第一屏**:⛔ 埋在下面=没人会看到(今天已经为「红线埋在末尾」栽过一次) # 🔴 量法修正:⛔ **不用 `skill[:2000]` 切片** —— frontmatter 就 3000+ 字符 # ⇒ 索引明明在第 24 行却被判「不在第一屏」(**假红**,今天第 7 次同形状)。 # ⇒ 真「第一屏」=**标题之后的第一个非空块**。 _after = skill.split("# session-mechanism", 1)[-1].lstrip("\n") head = _after[:1200] R.append(("SKILL.md **第一屏**就有「改流程/改机制前先看这三篇」", "改流程" in head and "00-动手前必过.md" in head)) R.append(("SKILL.md 第一屏**指向索引档**", "01-文档索引.md" in head)) # ③ 文档四条规则(用户 10-04 逐字给定)—— 逐条在位 for key, why in (("分类索引", "新文档必须登记,否则白写"), ("结论在最前", "读者要的是现在是什么样,不是改了几轮"), ("新的在前面", "历史倒排:追加只能往前插"), ("简明扼要有效", "单条上限,防流水账")): R.append(("四条规则之一「**%s**」在位(%s)" % (key, why), key in idx)) R.append(("⛔ 四条规则里**「单条 ≤6 KB」写明上限**(⛔ 只说「简明」没有上限=没约束)", "6 KB" in idx)) R.append(("⛔ 明确**追加只能往前插,不许接在末尾**(历史倒排的可执行说法)", "不许接在末尾" in idx)) R.append(("⚠️ 同时写着**判据要点一个不许丢**(⛔ 精简过头=假绿,比啰嗦更坏)", "判据要点" in idx and "假绿" in idx)) # ④ 索引不许烂掉:⛔ 「存档不等于读得到」那节必须在(今天栽的根因) R.append(("索引里写着**「存档 ≠ 读得到」**那节(10-04 实证的根因)", "读得到" in idx)) R.append(("⛔ 明写「只写进工作区日志的规矩 = 没立」" "(⛔ 这是今天复发的那一条的直接对策)", "没立" in idx)) # 行为判据:⛔ 索引点名的每个 references 文件**必须真存在**(防"登记了但文件没了") import re as _re3 # 🔴 正则**要认带不带 `references/` 前缀两种写法**(索引里多数写作 `00-动手前必过.md`) # ⛔ 还要**修双写**:`r"\\w"` 在 Python 里是匹配**字面 `\w`**,⛔ 不成字符类 # (实测:写成 `\\w` ⇒ 匹配到 0 个 ⇒ 这条行为判据**空转恒绿**) named = set(_re3.findall(r"`(?:references/)?([\w\u4e00-\u9fff\-]+\.md)`", idx)) bad = [n for n in named if not ((G / n).is_file() or (G / "references" / n).is_file())] R.append(("索引点名的 %d 个 references 文件**全部真存在**(⛔ 登记了但文件没了=索引在说假话)" % len(named), named and not bad)) return R @case("🔴 红线:⛔ 说「会怎样」前必须取证那个后果(10-03 立 · ⛔ 10-04 又犯)") def t_no_invented_consequence(): """🔴 这条是**跨会话翻车**的实证: 10-03 23:20 立了「⛔ 写『会怎样』前必须先取证那个后果真的存在」, ⛔ **但它只躺在工作区日志里**(`.workbuddy/memory/2026-10-03.md`,197 KB) ⇒ 换会话/换技能**读不到** ⇒ 10-04 我又编了一次 (说「常驻挂在会话进程树上、会话一关就被带走」;实测它连跑 24.2 h, 父进程已退出=**孤儿进程** ⇒ **不受会话结束影响**)。 ⚠️ **真正的根因不是「忘了」,是「存档放错地方」**: · 工作区日志 197 KB = 只有在那个工作区翻才看得到 · 技能(跨会话唯一会自动加载的)**当时没写** · 技能仓库 **10-04 11:22 才首次入库** ⇒ 昨天成果压根没进版本库 ⇒ 本判据量的就是「它**进技能了没有**」——⛔ 还在日志里就等于没存档。 """ doc = (HERE.parent / "references" / "00-动手前必过.md").read_text( encoding="utf-8", errors="replace") R = [] import re as _re2 # 🔴 序号后面还跟着 emoji(`## ⓿ 🔴🔴 …`)⇒ 取**第一个字符**(⛔ 用 \S+ 会抓成 `⓿`+emoji 之外的整块) _heads = [(m.start(), m.group(1)[:1]) for m in _re2.finditer(r"^##\s*(\S)", doc, _re2.M)] _first = _heads[0][1] if _heads else "" _red = doc.find("取证那个后果真的存在") _red_sec = next((h for pos, h in _heads if pos < _red), "") R.append(("红线在**动手层第一段**(⛔ 埋在末尾 = 不会先被看到)", # 🔴 真量「红线**落在第几个标题区间**」⛔ 不比序号字符(变异实测两种写法都打不穿: # ① 改标题序号、位置不动 ② 整段挪到末尾、序号不变 ⇒ 恒绿假判据,今天第 6 次同形状) # ⇒ 判据:红线落在**第一个标题区间**内(`_heads[0][0] <= 红线 < 第二个标题起点`) bool(_heads) and len(_heads) >= 2 and _heads[0][0] <= _red < _heads[1][0])) R.append(("明写「编一个后果、说得更像回事」=**比原句更坏**", # 🔴 量「**出现在第一段里**」⛔ 不量全文出现过(V2 变异实测:文档里有**两处**, # 改一处另一处还在 ⇒ 恒绿 ⇒ 判据必须钉**唯一位置**) _red > 0 and "说得更像回事" in doc[_red:_red + 700] and "比原句更坏" in doc[_red:_red + 700])) R.append(("明写**假情报**这个词(⛔ 后果会被当真话用)", "假情报" in doc)) R.append(("带**当天的实证**(24.2 h / 8695 轮 / 孤儿进程)⛔ 不留空头红线", "24.2" in doc and "8695" in doc and "孤儿" in doc)) R.append(("点明「正确答案早写在文档里」+「把它读反了」" "(⛔ 防的是「没查」与「读反了」两种)", "早写在" in doc and "读反" in doc)) R.append(("第二条同源红线也在:**改到第三轮说不清就问要不要留**" "(10-03 与 10-04 各撞一次)", "第三轮" in doc and "这行本身就不该在" in doc)) R.append(("「报结论前」的清单**含那条取证问**(⛔ 收口时必看)", "都有读数证明吗" in doc)) return R @case("🔴 常驻「活多久」的判据:**存活时长 = 发起会话的存活时长**(⛔ 禁编造「活不过当轮」当后果)") def t_supervise_lifespan_not_invented(): """🔴🔴 2026-10-04 **当天栽的第 5 次同形状**(用户当场纠正: 「本会话 后台任务 和 程序 不是一直好好的 都跑了2天了」)。 我说了什么:`没载体 ⇒ 常驻挂在当前会话的进程树上 ⇒ 会话一关就被带走`。 🔴 **实测把它推翻**(不许反驳用户,得看数据): · pid 19424 启动于 **10-03 17:05:36**,到 10-04 17:15 **连续活 24.2 h / 8695 轮**,心跳 4 s 前 · 它的 **父进程 pid 57216 早已不在进程表** ⇒ 它是**孤儿进程** · 孤儿化之后**独立存活,⛔ 不受会话结束影响** ⇒ 我说的"会被带走"是**编造的后果** ⚠️ 讽刺的是**正确答案早就写在权威文档里**(`supervise-persistence.md` §〇 那条「实测更正」): 「⚠️ **它们现在活着,只因为发起它们的那条会话进程树至今没被回收** ⇒ 「**存活时长 = 发起会话的存活时长**」 ⛔ 不能用它论证「这种起法也能长期」」 ⇒ **我今天把这条读反了,还编出了它没说的后果。** ⚠️ 为什么单独立判据(⛔ 不是又写条纪律): 今天的两个根因(A 连接未验证/B 验证器自身失败被当结论)**都是"判据"层的**, 这条是**"结论"层的** ⇒ 它得有个自己的门:**凡要说「它会死/活不过」,必须先验父链。** """ P = (HERE.parent / "references" / "supervise-persistence.md") doc = P.read_text(encoding="utf-8", errors="replace") R = [] # ① 权威文档里那条**实测判据**必须在位(⛔ 它是今天唯一推翻我编造的依据) R.append(("权威文档写着「**存活时长 = 发起会话的存活时长**」" "(⛔ 这是唯一能推翻「活不过当轮」的实测依据)", "存活时长 = 发起会话的存活时长" in doc)) R.append(("文档明写「它们现在活着,只因为…会话进程树至今没被回收」" "(⛔ 承认**活着的因**,而不把「活着」当成「这种起法可行」)", "至今没被回收" in doc)) R.append(("文档⛔ 明写「不能用它论证这种起法也能长期」" "(⛔ 防的正是我今天犯的推断)", "不能用它论证" in doc)) # ② 🔴 真实验一次**父链**(⛔ 断言必须建立在现读数据上,⛔ 不许复述文档) import json import time hb = None for cand in (Path(os.environ.get("DSH_WS_ROOT") or ".") / ".workbuddy" / "collab" / "logs" / "supervise-heartbeat.json",): try: if cand.is_file(): hb = json.loads(cand.read_text(encoding="utf-8")) break except Exception: pass if not hb: return R + [("⛔ 本区没有心跳文件 ⇒ 父链这条验不了(**不假装通过**)", False)] age_h = (time.time() - float(hb.get("started_ts") or 0)) / 3600.0 # 🔴 ⚠️ 时长**只打印、不作断言**(2026-10-04 踩过):本条要反驳的是 # 「活不过当轮」⇒ 只需**能算出时长且 pid 活着**。 # ⛔ 硬要「> 1 小时」⇒ 常驻一重启(pid 19424→65072,活 0.2 h)就**假红**。 R.append(("**能算出常驻存活时长**(本次 %.2f h)+ 起点是 `started_ts`" " ⇒ ⛔ 反驳「工具算不出、只能靠猜」" % age_h, age_h > 0.0)) R.append(("常驻**此刻活着**(pid 活)—— 这才是「没活不过当轮」的直接反证", bool(hb.get("pid")))) R.append(("心跳里带 `started_ts`(⛔ **算「活了多久」的唯一依据** —— " "⛔ 别用 mtime 猜、⛔ 别用 pid 猜)", bool(hb.get("started_ts")))) R.append(("心跳里带 `argv0`(**能证伪「跑的是哪份文件」**)", bool(hb.get("argv0")))) src_self = Path(__file__).read_text(encoding="utf-8", errors="replace") i = src_self.find("def t_supervise_lifespan_not_invented") j = src_self.find("@case(", i + 10) seg = src_self[i:j] import re as _re age_lines = [ln for ln in seg.splitlines() if "age_h" in ln and "=" in ln and "append" not in ln] R.append(("🔴 **算时长的那一行逐字读的是 `started_ts`**" "(⛔ `pid` 是**身份**不是**时间**;⚠️ 实测把 `pid` 转 float 也能" "通过『值小于 now』⇒ ⛔ 只验值不验字段 = 白验)", bool(age_lines) and all('hb.get("started_ts")' in ln for ln in age_lines))) R.append(("⛔ 算时长的代码**没有**拿 `mtime`/`st_mtime` 当起点" "(⛔ mtime 会被复制/同步改掉)", not _re.search(r"age_h\s*=.*mtime", seg))) return R @case("副本同步:**残留/待补/待更新三类漂移都抓得到**,且白名单不被动(P0-51 的正解)") def t_mirror_drift(): """🔴 为什么要有这条:`vibe-product` 用的是**工作区副本**(整目录拷贝出去的), 全局删掉的文件副本**不知道**,会静默留着 ⇒ 技能改动在副本上"看不到", 而副本才是那个区**真在跑的**代码(实测:`mut_run.py` 差点没补上, `00-动手前必过.md`/`99-速查清单.md` 一度只存在于全局)。 ⚠️ 本条**自建夹具**(⛔ 不拿真副本跑判据): 真副本的漂移是**外部状态**(取决于上次同步到哪)⇒ 拿它当判据, 「副本刚同步完」就必红。**自建**才能对每类漂移做变异验证。 """ import importlib.util mp = HERE / "workspace_mirror.py" if not mp.exists(): return [("⛔ 包内缺 `scripts/workspace_mirror.py` ⇒ 副本漂移没人管", False)] spec = importlib.util.spec_from_file_location("wm_selftest", mp) wm = importlib.util.module_from_spec(spec) spec.loader.exec_module(wm) # 🔴 夹具要**贴真实结构**:`<夹具>/.workbuddy/skills/<包>/`。 # 理由(实测踩过):备份落点靠「往上找 `.workbuddy`」判定 ⇒ 结构不像 ⇒ # 备份会写进**真工作区的 交付物/**(自测污染真工作区,且判据依赖外部状态)。 fx_root = TEST_WS / "mirror_fixture" fx_src = fx_root / "src" fx_cp = fx_root / "ws" / ".workbuddy" / "skills" / "session-mechanism" shutil.rmtree(str(fx_root), ignore_errors=True) (fx_src / "sub").mkdir(parents=True, exist_ok=True) (fx_cp / "sub").mkdir(parents=True, exist_ok=True) # 夹具:全局侧 3 文件(a/b/c),副本侧 b 篡改 + 残留 r + 缺 c + 白名单 roots.env (fx_src / "SKILL.md").write_text("# fixture", encoding="utf-8") (fx_src / "a.py").write_text("A=1\n", encoding="utf-8") (fx_src / "sub" / "b.py").write_text("B=1\n", encoding="utf-8") (fx_src / "c.py").write_text("C=1\n", encoding="utf-8") (fx_cp / "SKILL.md").write_text("# fixture", encoding="utf-8") (fx_cp / "a.py").write_text("A=1\n", encoding="utf-8") (fx_cp / "sub" / "b.py").write_text("B=1\n# TAMPER\n", encoding="utf-8") # 待更新 (fx_cp / "r_stale.py").write_text("STALE\n", encoding="utf-8") # 真残留 (fx_cp / "roots.env").write_text("DSH_WS_ROOT=/fixture/ONLY\n", encoding="utf-8") # 白名单 g, c, only_c, only_g, changed = wm.diff(fx_src, fx_cp) det = [ ("抓到**真残留**(全局已删、副本还留着)", "r_stale.py" in only_c and len(only_c) == 1), ("抓到**待补**(全局有、副本没有)", "c.py" in only_g and len(only_g) == 1), ("抓到**待更新**(两边都有但内容不同)", "sub/b.py" in changed and "a.py" not in changed), ("**没把白名单当漂移**(roots.env 不该出现在任一差异里)", not any("roots.env" in x for x in (only_c + only_g + changed))), ] # --sync:真跑一遍,验「收口 + 白名单内容没被动 + 幂等」 rc1 = wm.do_sync(fx_src, fx_cp) g2, c2, only_c2, only_g2, changed2 = wm.diff(fx_src, fx_cp) det += [ ("`--sync` 收口(rc=0)", rc1 == 0), ("同步后**无漂移**", not (only_c2 or only_g2 or changed2)), ("同步后**残留已真删**(⛔ 不能只是「不报」)", not (fx_cp / "r_stale.py").exists()), ("同步后**待补已真写入**", (fx_cp / "c.py").exists() and (fx_cp / "c.py").read_bytes() == (fx_src / "c.py").read_bytes()), ("同步后**篡改已被覆盖回全局版**", (fx_cp / "sub" / "b.py").read_bytes() == (fx_src / "sub" / "b.py").read_bytes()), ("🔴 **白名单内容原样保住**(roots.env 仍写 /fixture/ONLY," "⛔ 被冲回全局值就会让副本去读写别人的仓库)", (fx_cp / "roots.env").exists() and "/fixture/ONLY" in (fx_cp / "roots.env").read_text(encoding="utf-8")), ("**幂等**:再 sync 一次 rc=0", wm.do_sync(fx_src, fx_cp) == 0), ] # 🔴 锚在**结构**上:真正拦住"覆盖白名单"的是覆盖循环里那句 `if k in KEEP: continue`。 # ⚠️ 早先只验"写回"验不到 —— 因为 `_files()` 已把 KEEP 排除,写回那步**恒为空转**, # 把它删掉判据照样全绿(2026-10-04 变异实测)。⇒ 判据必须锚在真作用点上。 wsrc = mp.read_text(encoding="utf-8") det += [ ("覆盖循环里有**白名单守卫** `if k in KEEP: continue`" "(⛔ 拿掉它 ⇒ roots.env 被全局值覆盖 ⇒ 副本去读写别人的仓库)", re.search(r"for k in sorted\(g\):\s*\n\s*if k in KEEP:\s*\n\s*continue", wsrc) is not None), ("`--sync` 先**备份再删**(⛔ 破坏性动作前必须留退路)", wsrc.index("copytree") < wsrc.index("unlink()")), ("破坏前有**预检**(源文件读不出就拒绝动手,⛔ 不许崩在半路留半同步态)", "拒绝动手" in wsrc), ("覆盖用**临时文件+os.replace**(⛔ 半截文件不能留在副本里)", "os.replace" in wsrc and ".mirtmp" in wsrc), ] fx_root2 = TEST_WS / "mirror_fixture2" fx_src2 = fx_root2 / "src" fx_cp2 = fx_root2 / "ws" / ".workbuddy" / "skills" / "session-mechanism" shutil.rmtree(str(fx_root2), ignore_errors=True) shutil.copytree(str(fx_src), str(fx_src2)) fx_cp2.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(str(fx_src), str(fx_cp2)) # ⚠️ `roots.env` 是**副本独有**(src 侧没有)⇒ copytree 后天然不存在, # ⛔ 别再 unlink(早先多这一步 ⇒ FileNotFoundError,用例崩在夹具准备上) # ② **从来没有过** ⛔ 不该报"丢失"(否则每次 sync 都刷假告警,久了没人看) # ⚠️ 判据侧踩过两次坑,都记这: # · ⛔ 不能假设"删了就一定救不回" —— 夹具有备份时工具**正确地**捞回、rc=0, # 早先断言 rc≠0 ⇒ 把正确行为判成错(假红)。 # · ⛔ 也不能假设"没备份就该报丢" —— ⛔ **分不出来**("从来没有过" vs "丢了" # 在副本现状里完全同形)⇒ 工具选择**不报**。这是有意的保守,不是缺陷。 import contextlib import io as _io _buf = _io.StringIO() with contextlib.redirect_stdout(_buf): rc_none = wm.do_sync(fx_src2, fx_cp2) _out2 = _buf.getvalue() det += [ ("⛔ **从来没有过的白名单不报「丢失」**(`diff` 排除 KEEP + 无历史备份" "⇒ 分不出「丢」和「没过」⇒ 有意不报,⛔ 不刷假告警)", rc_none == 0 and "白名单缺失" not in _out2), ("⛔ **代码里明写了这条保守取向**(`副本历史里有过` 才算丢)", "副本历史里有过" in wsrc), ("⛔ **捞不到时明说不代填**(⛔ 绝不拿全局那份顶替 —— 全局指向别的仓库)", # 🔴 锚在**结构**上(早先锚措辞/锚 `else:` ⇒ 措辞微调、结构一变就假红,今天各踩一次): # 三句必须在**同一个 `if keep_lost:` 块**里 ⇒ 兜底与告警走的是同一条路径。 re.search(r"if keep_lost:[\s\S]{0,700}?绝不拿全局那份顶替[\s\S]{0,200}?请人工恢复" r"[\s\S]{0,80}?不代填", wsrc) is not None), ("⛔ **兜底源只认「历次备份」**(⛔ 不是本次刚建的、⛔ 更不是全局)", "历次" in wsrc and "BK_PREFIX" in wsrc and re.search(r"sorted\([\s\S]{0,300}?reverse=True\)", wsrc) is not None), ("⛔ **兜底源只认「历史备份」**(⛔ 不是副本现状、⛔ 更不是全局)", re.search(r"兜底源=\*\*历史备份", wsrc) is not None), ] # ③ 有备份时**能**捞回:造一次"先 sync(留备份)→ 再删 → 再 sync" (fx_cp2 / "roots.env").write_text("DSH_WS_ROOT=/fixture/BACKUPED\n", encoding="utf-8") (fx_cp2 / "sub" / "b.py").write_text("B=1\n# DIFF\n", encoding="utf-8") # 制造漂移 ⇒ 触发备份 wm.do_sync(fx_src2, fx_cp2) # ← 这次备份里有 roots.env (fx_cp2 / "roots.env").unlink() rc_bk = wm.do_sync(fx_src2, fx_cp2) det += [ ("🔴 **有备份时,白名单被删后 `--sync` 能捞回**" "(`roots.env` 丢失 ⇒ 副本按位置推导去读写**别的仓库**,静默且难查)", rc_bk == 0 and (fx_cp2 / "roots.env").exists() and "/fixture/BACKUPED" in (fx_cp2 / "roots.env").read_text(encoding="utf-8")), ] shutil.rmtree(str(fx_src2.parent), ignore_errors=True) shutil.rmtree(str(fx_src.parent), ignore_errors=True) return det @case("副本同步:`roots.env` 白名单**只按文件名**判(⛔ 换副本不许靠「我记得跳过哪个」)") def t_mirror_keep_by_name(): import importlib.util spec = importlib.util.spec_from_file_location("wm_selftest2", HERE / "workspace_mirror.py") wm = importlib.util.module_from_spec(spec) spec.loader.exec_module(wm) return [ ("白名单含 `roots.env`(⛔ 覆盖它 ⇒ 副本读写到别的仓库)", "roots.env" in wm.KEEP), ("白名单含 `副本使用说明.md`(副本独有的口径文档)", "副本使用说明.md" in wm.KEEP), ("白名单含 `install.log`(副本自己的安装历史)", "install.log" in wm.KEEP), ("白名单是**元组**(⛔ 散着写容易漏)", isinstance(wm.KEEP, tuple)), ("**`__pycache__` 等自身目录不算漂移**(否则每次跑都报一堆假漂移)", "__pycache__" in wm.SELF_DIRS), ] @case("🔴 常驻载体形态:**旧看守已彻底清除 + 不许回潮**(2026-10-05 收敛)") def t_keeper_legacy_ps1_form_gone(): """🔴🔴 2026-10-05 **形态收敛的守卫**(取代此前 `t_keeper_tpl_found_in_copy_layout`)。 **为什么换掉旧的**:旧用例判「副本形态下找不找得到 `assets/start-supervise.ps1.tpl`」, 而该模板与「旧 看守」这套形态**已整套废弃** —— 任务动作从「壳脚本 -File」改成 **`pythonw.exe` + `supervise-launch.py`** (GUI 子系统 ⇒ 零 conhost ⇒ **不闪黑窗**,用户 2026-10-05 报「又弹了窗口」的直接修复)。 ⇒ 旧用例连**被测对象都不存在了** ⇒ 恒红、且无法修复 ⇒ **必须删,换成本条**。 🔴 本条的职责是**守形态**(防回潮),五条一起判: ① 代码里**不许再出现**读那个 `.tpl` 的路径拼接; ② 任务动作**必须是** `pythonw.exe` + 启动器; ③ ⛔ 动作里不许再把壳脚本当看守(⚠️ 登记任务本身仍走 PowerShell, 那是在**登记**、不是在**当看守**,两者别混); ④ `CREATE_NO_WINDOW` 在位; ⑤ `_find_keeper_tpl` 死代码已清。 """ # 🔴🔴 2026-10-06 **扩到三个文件**(本轮实测抓到的探照灯打偏): # 原版只扫 `collabd.py` ⇒ 而**同族的 `init_workspace.py` 整份没被扫到**, # 它揣着 `start-supervise.ps1.tpl` 路径拼接 + `-File start-supervise.ps1` 动作 # 活到今天(`_register_keeper_task` 又写了一份建任务代码 = 第二套起法)。 # ⚠️ 教训:**判据只扫"我以为哪个文件"就等于没扫** —— 凡是"这一族文件都不许有 X", # 必须**枚举整族**,⛔ 不许只挑一个代表。 _FAMILY = ["collabd.py", "init_workspace.py", "collabctl.py"] res = [] _codes = {} for _n in _FAMILY: _p = HERE / _n if not _p.is_file(): res.append((f"⛔ `{_n}` 存在(同族文件缺了 ⇒ 判据无从施加)", False)) continue _s = _p.read_text(encoding="utf-8") _codes[_n] = (_s, _strip_comments_and_docs(_s)) # ① 整族:⛔ 谁也不许再去找那份废模板 _bad = [n for n, (_, c) in _codes.items() if "start-supervise.ps1.tpl" in c] res.append(("⛔ 全族已无「读 `start-supervise.ps1.tpl`」的路径拼接(形态废弃 ⇒ 不该有人再找它)", not _bad)) # ② 整族:⛔ 谁也不许把壳脚本当看守 _bad = [n for n, (_, c) in _codes.items() if "-File start-supervise.ps1" in c] res.append(("⛔ 全族动作里**不再有** `-File start-supervise.ps1`(⛔ 壳脚本当看守=闪黑窗)", not _bad)) # ③ 建任务者:动作必须是 pythonw + 启动器 _mk = {n: c for n, (_, c) in _codes.items() if "New-ScheduledTaskAction" in c} _ok = bool(_mk) _ok = _ok and all("supervise-launch.py" in c for c in _mk.values()) _ok = _ok and all("pythonw.exe" in _codes[n][0] for n in _mk) res.append(("🔴 建任务者(%s)动作=`pythonw.exe` + 启动器 `supervise-launch.py`" % "/".join(sorted(_mk)), bool(_ok))) # ④ 建任务者:`CREATE_NO_WINDOW` 在位 _bad = [n for n, c in _mk.items() if "0x08000000" not in c] res.append(("🔴 建任务者 `CREATE_NO_WINDOW`(0x08000000)在位(⛔ 缺了每次登记都闪一下)", not _bad)) # ⑤ ⭐⭐ **任务名口径必须唯一**(本轮抓到的病:`collabd.py` 建 `collabd-supervise-*`, # 而 `collabctl.py` 建 `collabd-keepalive-*` ⇒ 前者在 `SCHED_TASKS` 禁用名单里 # ⇒ **自愈起的常驻会被下一次 `off` 当旧形态干掉**) _names = set() for n, c in _mk.items(): # ⚠️ **先剥掉「禁用名单」块再扫** —— `SCHED_TASKS` 里是**故意**列着旧名去**禁用它**的, # 不剥就会把"它自己的否定句"当成违规。 # 🔴 这是本项目**第三次**踩同一个坑(前两次:查"PRD 有没有视觉词"命中免责声明、 # 查"有没有六份旧产出"命中 PRD 里的禁令文本)⇒ 判据凡扫文本,先问"这句是不是它的否定句"。 c2 = re.sub(r"SCHED_TASKS\s*=\s*\[[^\]]*\]", "", c) _names |= set(re.findall(r'"collabd-(?:supervise|keepalive)-', c2)) res.append(("⭐⭐ 任务名口径唯一(建任务者只许用 `collabd-keepalive-`;" "⛔ 出现 `collabd-supervise-` 会被 `off` 当旧形态禁掉)|实测命中:%s" % (sorted(_names) or "无"), _names == {"\"collabd-keepalive-"})) # ⑥ init_workspace 侧:死代码已清 _iw = _codes.get("init_workspace.py", ("", ""))[1] _dead = [d for d in ("def _register_keeper_task(", "def _keeper_task_state(", "def _ps_check(", "def _find_keeper_tpl(") if d in _iw] res.append(("`init_workspace.py` 旧形态死代码已清(⛔ 留着会再招一批过期判据)|实测残留:%s" % (_dead or "无"), not _dead)) return res @case("🔴 看板渲染:**真跑 `renderProject()`**(⛔ 不是查字符串 —— 实发过 `acc is not defined`)") def t_board_render_runs_clean(): """🔴🔴 2026-10-05 用户报障逐字:**「快照读取失败:acc is not defined」**。 **病症**:页面白屏 + 一句「快照读取失败…服务还开着吗?重跑 board.py」 —— 那句文案把**渲染期 ReferenceError** 说成**服务挂了** ⇒ 用户去重跑服务(白费力)。 **真因**:`renderProject(d)` 里引用了**从未声明的标识符**: · `acc` —— 验收判据其实在 `d.goal.acceptance`(实测快照顶层**没有** `acc`); · `p.*` —— 参数早已改名成 `d`,函数体里 6 处没跟着改(同族,一次改名的漏网)。 ⇒ 都属于「**变量名与数据流向不符**」,⛔ 语法检查查不出(原版语法是过的)。 ✅ 本判据的做法=**真跑**:用 `assets/board-render-probe.js` 把页面脚本放进 `vm` 的**全局作用域**执行(⛔ 不能用 `new Function` —— 函数会关在局部, 取到 `undefined` ⇒ **假绿**),喂一份**合成快照**,真调 `renderProject()`, 看它抛不抛、`#proj` 真渲染了没。 ⚠️ **必须行为级**:只 grep `acc is not defined` 这种字样是**恒真**的判据 (修好之后字符串当然不在)⇒ 测不出"将来又冒出个别名"。 ⚠️ 合成快照**不依赖活服务**(自测要在离线/CI 也能跑)。 """ import json as _json import subprocess as _sp import tempfile as _tmp_mod probe = HERE.parent / "assets" / "board-render-probe.js" html = HERE.parent / "assets" / "board.html" res = [] res.append(("探针在位 `assets/board-render-probe.js`", probe.is_file())) res.append(("探针路径**自定位**(⛔ 不写死作者机器)", "BOARD_HTML" in probe.read_text(encoding="utf-8"))) # 源码级:`acc` 必须**先声明后使用**(⛔ 不能只是"出现过") # ⚠️ 注释里也会出现 `var acc=`(本轮就是)⇒ **先剔注释**再判,否则判据假红。 src = html.read_text(encoding="utf-8") i = src.index("function renderProject(d){") j = src.index("\nfunction ", i + 10) body = src[i:j] _code = re.sub(r"/\*[\s\S]*?\*/", "", body) # 剔块注释 _code = re.sub(r"(?m)//.*$", "", _code) # 剔行注释 _decl = _code.find("var acc=") _use = _code.find("Object.keys(acc)") res.append(("`renderProject` 里 `acc` **有声明**(`var acc=` 在用它之前,⛔ 剔注释后判)", _decl >= 0 and _use >= 0 and _decl < _use)) res.append(("`renderProject` 里**没有裸 `p.`**(参数名是 `d`,改名漏网会 ReferenceError)", not re.search(r"(?)` 必须**扫得出** cwd 在对方的那个会话; ② `_resolve_main(..., ws_root=<对方>)` 的 `by_topic` 必须**指向它**; ③ ⛔ 反向:**不传 `ws_root`** 时必须**扫不出**(否则等于"把 peer 认成自己的" ⇒ 投错窗口)。 ⚠️ 用**合成会话**(临时建一条真会话行到测试库)而不是赌机器上有没有 peer —— 否则换机必红。 """ import sqlite3 as _sq res = [] # ⚠️ 本用例要调的是 **`board.py`** 的函数(`_scan_ws_mains`/`_resolve_main`/`project_scope`), # ⛔ **不是** `imp()` 装出来的 collabd 模块 —— 那两个模块都有同名函数, # 拿错模块会 `AttributeError`(2026-10-05 本用例第一版就是这么红的,同族第 2 次)。 import board as m # ── 合成一条"对方工作区的主会话"(直接写测试库;⛔ 绝不碰生产库) peer_root = str(TEST_WS / "peerws") sid = "9e1a" + "0" * 32 db = m._host_db() if not db: return [("(无法定位宿主库 ⇒ 本机跳过,⛔ 不算失败)", True)] con = _sq.connect(str(db), timeout=2.0) try: con.execute("pragma busy_timeout=800") con.execute( "insert or replace into sessions(id,cwd,user_id,title,custom_title,status," "created_at,updated_at,last_activity_at,deleted_at) " "values(?,?,?,?,?,?,?,?,?,null)", (sid, peer_root.replace("\\", "/"), "selftest", "peer 主会话", "", "completed", 0, int(time.time() * 1000), int(time.time() * 1000))) con.commit() topics = ["界面交互"] # ① 传 ws_root ⇒ 必须扫得出 sc_peer = m._scan_ws_mains({}, topics, peer_root) res.append(("① 传 `ws_root` ⇒ 扫出对方主会话候选", sid in sc_peer.get("cand", []))) # ② 传 ws_root ⇒ by_topic 指向它 rm_peer = m._resolve_main({}, topics, peer_root) res.append(("② 传 `ws_root` ⇒ `by_topic` 指向它", str((rm_peer.get("by_topic") or {}).get("界面交互", {}).get("sid") or "") == sid)) # ③ 不传 ⇒ 必须扫不出(fail-closed:⛔ 不许把 peer 认成本区的) sc_own = m._scan_ws_mains({}, topics) res.append(("③ ⛔ 不传 `ws_root` ⇒ **扫不出**(不许把 peer 认成自己)", sid not in sc_own.get("cand", []))) # ④ `project_scope` 的 `workspace` 键**必须仍回本区**(那是另一条判据,⛔ 不许被顺手改掉) # ⚠️ 比对基准取 **`m.WS`(被加载的那个 board 模块自己的 WS)**,⛔ 不是本测试脚本的 `WS` # —— 两者来源不同(模块来自 `goal.json`/部署配置、脚本来自环境变量)⇒ 拿脚本的比会假红。 ps = m.project_scope({"title": "t", "short": "s", "topics": topics, "id": "g"}, peer_root) _norm = lambda _v: str(_v or "").replace("\\", "/").rstrip("/").lower() res.append(("④ `project_scope(ws_root=…)` 的 `workspace` 仍=本区 `WS`", _norm(ps.get("workspace")) == _norm(m.WS))) # ⑤ 🔴🔴 **"测装配"而非"测零件"**(P0-55 的同族): # 上面 ①②④ 都是我**自己把 `ws_root` 递进去** ⇒ 只证明"函数接受这个参数", # ⛔ **证明不了 `_goal_block()` 真把它传下去了** —— 实测变异 M3(删掉调用点的传参) # **本条全绿**,而线上 peer 格照样「无主会话」。 # ⇒ 判据必须**静态咬住那条调用点**:`project_scope(...)` 在 peer 分支里必须带 `ws_root`。 _bs = (HERE / "board.py").read_text(encoding="utf-8") res.append(("⑤ `_goal_block()` 的调用点**真把 `ws_root` 递给 `project_scope`**" "(⛔ 只测函数体等于没测装配)", "project_scope(g, ws_root)" in _bs)) finally: try: con.execute("delete from sessions where id=?", (sid,)) con.commit() except Exception: pass con.close() return res @case("🔴 建目标时**顺手登记主会话**(用户原话:⛔ 不用每次会话都去声明)") def t_main_registered_on_goal_create(): """🔴🔴 2026-10-05 用户逐字: > 「**就是你没有指定 声明目标的时候 指定是哪个会话(sid)**」 > 「**最好是记录会话,这样就不用每次会话都去做声明了**」 **病根**:`role_of()` 三条优先级里第三条 —— **未声明就落成哑档**(当 worker 使)。 实测踩到:本工作区的主会话 `a80f300d` 从没声明过 ⇒ 看板/投递全按 worker 走, 用户手动补 `--declare` 才发现(而且**顶掉了原主会话 `3f43ce71`** —— 那正是 "没指定在哪声明"的代价)。 ✅ 正解=**在"创建目标"那一步顺手记**(`_ensure_goal_dir()` 那一刻 `CODEBUDDY_SESSION_ID` 必然已知),⛔ 不新增"要记得声明"的待办。 ⚠️ 判据必须**行为级 + 变异有效**: · 只断言"有 `register_main_session` 这个函数"是**恒真**的 ⇒ 测不出"忘了调"; · 必须真跑 `_ensure_goal_dir()`:**无主会话⇒认本会话**、**已有主会话⇒⛔ 不抢位**。 · 🔴 **不抢位那条是变异关键**:把"只在无主会话时认"这层判断撤掉 ⇒ 该条必红。 """ import json as _json m = imp() res = [] src = (HERE / "collabd.py").read_text(encoding="utf-8") res.append(("有唯一实现 `register_main_session()`(⛔ 不再两处各写一套)", "def register_main_session(" in src)) res.append(("`--declare` **复用**它(⛔ 不就地再写一遍 roles)", src.count("register_main_session(") >= 3)) _st_file = Path(m.STATE) _bak = _st_file.read_bytes() if _st_file.exists() else None _old_sid = os.environ.get("CODEBUDDY_SESSION_ID") try: # ── ① 本区**无**主会话 ⇒ 建目标时认下本会话 m.STATE.write_text(_json.dumps({"roles": {}, "topics": {}}, ensure_ascii=False), encoding="utf-8") os.environ["CODEBUDDY_SESSION_ID"] = "aaaa1111-0000-0000-0000-000000000001" r1 = m._ensure_goal_dir() _roles1 = _json.loads(_st_file.read_text(encoding="utf-8")).get("roles") or {} res.append(("① 本区无主会话 ⇒ **建目标时登记本会话为 main**(⛔ 不用手动声明)", _roles1.get("aaaa1111-0000-0000-0000-000000000001") == "main")) res.append(("① 结果**说出口**(`main_reg` 有值,⛔ 不静默)", bool(str(r1.get("main_reg") or "").strip()))) # ── ② 本区**已有**主会话 ⇒ 🔴 ⛔ 不抢位(补哑档 ≠ 抢位子) m.STATE.write_text(_json.dumps( {"roles": {"bbbb2222-0000-0000-0000-000000000002": "main"}, "topics": {}}, ensure_ascii=False), encoding="utf-8") os.environ["CODEBUDDY_SESSION_ID"] = "cccc3333-0000-0000-0000-000000000003" m._ensure_goal_dir() _roles2 = _json.loads(_st_file.read_text(encoding="utf-8")).get("roles") or {} res.append(("🔴 ② 本区**已有**主会话 ⇒ **⛔ 不抢位**(原 main 保持)", _roles2.get("bbbb2222-0000-0000-0000-000000000002") == "main")) res.append(("🔴 ② 也**不把本会话加进来**(⛔ 不许悄悄多出个 main)", "cccc3333-0000-0000-0000-000000000003" not in _roles2)) # ── ③ 幂等:已经是 main 再登记 ⇒ roles 不变 _st3 = {"roles": {"dddd4444-0000-0000-0000-000000000004": "main"}, "topics": {}} _r3a = m.register_main_session(_st3, "dddd4444-0000-0000-0000-000000000004", why="自测") _r3b = m.register_main_session(_st3, "dddd4444-0000-0000-0000-000000000004", why="自测") res.append(("③ 幂等:已是 main 再登记 ⇒ 无变化、`demoted` 为空", _r3a.get("demoted") == [] and _r3b.get("demoted") == [] and _st3["roles"]["dddd4444-0000-0000-0000-000000000004"] == "main")) res.append(("③ 登记新 main ⇒ **旧 main 降级 worker**(一个区一条主会话)", (lambda _s: (m.register_main_session( _s, "eeee5555-0000-0000-0000-000000000005", why="自测"), _s["roles"].get("dddd4444-0000-0000-0000-000000000004") == "worker")[1])( {"roles": {"dddd4444-0000-0000-0000-000000000004": "main"}}))) finally: if _old_sid is None: os.environ.pop("CODEBUDDY_SESSION_ID", None) else: os.environ["CODEBUDDY_SESSION_ID"] = _old_sid if _bak is None: try: _st_file.unlink() except Exception: pass else: _st_file.write_bytes(_bak) return res # 🔴🔴 2026-10-05 **已删除 `t_keeper_script_is_published_copy`**。 # 它判的是旧 ps1 看守形态的三件事:`_own_path()` 的存在、`__SCRIPT__` 走 `_own_path()`、 # 「回落必打日志」。而 ps1 看守**整套废弃** ⇒ `__SCRIPT__` 在 `collabd.py` 里只剩注释一处 # ⇒ 该用例**恒红且无法修复**(被测对象不存在了)。 # # ⚠️ 它真正有价值的那部分(**续命目标 / 身份基准必须走 `_own_path()` = 本区发布物**) # 已被 `t_ensure_spawns_published_copy` **完整接管**(6 项,含变异对照),没有覆盖缺口。 @case("🔴🔴 常驻**自我繁殖**必须断:续命只许起**本区发布物**(用户「越创建越多」的真因)") def t_ensure_spawns_published_copy(): """🔴🔴 2026-10-05 实测事故(**自我繁殖** —— 用户看到「开了7个终端窗口…越创建越多」的直接成因)。 **现场**:清理后 `skills/session-mechanism/scripts/collabd.py --supervise` **反复复现**, 父进程每次都不同且已消失(`11668` / `15924` / `46488`)⇒ 看着像"幽灵自启"。 **真因(日志铁证)**:`_ensure.log` 里那一行 —— `常驻续命:新起 --supervise pid=69464(原因:pid 12672 在、心跳新鲜,但**身份对不上**)` ① `ensure_supervise()` 起进程时填 `Path(__file__).resolve()` = **"正在跑的那份"**; 若它本身是**技能目录那份** ⇒ 续命又起技能目录那份 ⇒ **互为镜像、生生不息**; ② 同时 `_pid_is_collabd()` 的比对基准**也被技能目录那份顶着** ⇒ **真的本区常驻被判"身份对不上"** ⇒ 每轮 `--ensure` 都认为"没人跑"⇒ 再拉一条。 ✅ 正解=**发布物优先**(`_own_path()`),三处共用:续命目标、「身份」基准、keeper 的 `--script`。 🔴 判据必须**变异有效**:把 `str(_own_path())` 换回 `str(Path(__file__).resolve())` ⇒ 本条必红。 """ src = (HERE / "collabd.py").read_text(encoding="utf-8") res = [] res.append(("🔴 续命的 Popen 目标走 `_own_path()`(⛔ 不是 `Path(__file__).resolve()`)", 'Popen([PYW, "-u", str(_tgt), "--supervise"]' in src)) res.append(("🔴 `_tgt` 就是 `_own_path()` 的返回值(⛔ 不是就地重算 `__file__`)", "_tgt = _own_path()" in src)) res.append(("`cwd` 跟着目标走(⛔ 别用 `__file__` 的 parent —— 否则 cwd 与程序不在同一处)", "cwd=str(_tgt.parent)" in src)) res.append(("⚠️ 发布物缺失回落要打告警(⛔ 静默 ⇒ 用户以为跑的是副本)", "常驻续命回落到当前这份" in src)) res.append(("🔴 `_pid_is_collabd` 的身份基准也走 `_own_path()`(⛔ 否则真本区常驻被判成「不是我」)", "_self = str(_own_path())" in src)) res.append(("三处共用同一函数(⛔ 不再各写一份 ⇒ 同一事实三套答案)", src.count("_own_path()") >= 4)) return res @case("🔴🔴 一个工作区只能有一个常驻:**启动前必判 + 身份核验**(pid 会复用)") def t_supervise_singleton_identity(): """🔴🔴 2026-10-05 用户点破逐字: > 「**一个工作区只能有一个常驻。启动的时候必须先判断有没有**」 **两个方向的病**(同一句判据缺失的两面): ① **漏补**:`supervise_alive()` 原来只看 `心跳里的 pid 活着 ∧ 心跳新鲜`, **从不验证那个 pid 到底是不是 collabd** —— 而 **Windows pid 会复用** ⇒ 心跳里的号被别的进程占着 ⇒ 判"活着"、而真常驻早死了 ⇒ **永远没人补**(静默瘫)。 ② **双起**:心跳没写进去的那几秒,多个 `--ensure` 并发**各自读到"无心跳"** ⇒ 各起一条 ⇒ **两条常驻**(双写 `collabd-state.json` = 脑裂)。 ✅ 修法: · `_pid_is_collabd(pid, argv0)` —— 按 `QueryFullProcessImageNameW` 取真实 exe + `argv0` 对照(心跳里**本来就写了 `argv0`**,可此前**只写不读**,白存); · `--supervise` 加**互斥窗口**:原子抢位 `SUP_PID`(`os.replace`)⇒ sleep 1.5 s ⇒ 回验让位。 🔴 判据必须**变异有效**:⛔ 只断言"有这个函数"是恒真的 ⇒ 测不出"判据又被拆掉"。 """ m = imp() src = (HERE / "collabd.py").read_text(encoding="utf-8") res = [] res.append(("有身份核验 `_pid_is_collabd()`(⛔ 不再只看号还活着)", "def _pid_is_collabd(" in src)) res.append(("`supervise_alive()` 真的**调**它(⛔ 有函数不调=没接线)", "if not _pid_is_collabd(" in src)) res.append(("核验取的是**真实 exe 路径**(`QueryFullProcessImageNameW`,⛔ 不靠进程名猜)", "QueryFullProcessImageNameW" in src)) res.append(("核验**只认 pythonw**(常驻一律 pythonw 起,⛔ python 不算)", '"pythonw" not in exe' in src)) res.append(("有**互斥窗口**(原子抢位 ⇒ 回验 ⇒ 让位)⛔ 防两条同时启动", "互斥窗口回验" in src)) res.append(("抢位用 `os.replace` **原子替换**(⛔ 不用文件锁 —— 本机实测会卡死)", "os.replace(str(_tmp_pid), str(SUP_PID))" in src)) # ── 行为级:拿**本进程**当样本(它显然是活着的 python,但**不是 pythonw 常驻**) # ⇒ `_pid_is_collabd(自己的 pid)` 必须判 **False**(这是"身份核验真的在工作"的铁证; # 若实现退化成"只看 pid 活不活"就会恒判 True ⇒ 本条必红)。 try: _me_alive = m._pid_alive(os.getpid()) _me_collabd = m._pid_is_collabd(os.getpid(), "") res.append(("行为级:本进程 pid 活着(前提成立)", bool(_me_alive) is True)) res.append(("🔴 行为级:**本进程活着但判不是 collabd**(当前用 python 跑,⛔ 不是 pythonw)" " ⇒ 证明核验**真在看身份**,⛔ 不是恒 True", _me_collabd is False)) except Exception as _e: res.append(("行为级核验跑不起来:%r" % (_e,), False)) res.append(("行为级:pid 空/非法 ⇒ 一律 False(⛔ 不保守判在导致漏起)", m._pid_is_collabd(0, "") is False and m._pid_is_collabd("x", "") is False)) return res @case("🔴🔴 换目标(`declare --switch-goal`)⇒ `lifecycle` **自动复位「进行中」**(⛔ 不靠人手补)") def t_declare_resets_lifecycle(): """🔴🔴 2026-10-05(用户定案逐字): > 「一个工作区 同时只能执行一个目标,如果要切换目标,需要用户确认, > **然后切换和关注检查切换后的目标**」 🔴 **改前事实**:`declare` **从不碰 `lifecycle`** —— 只在末尾打一句 「别忘了同步 `lifecycle`(⛔ `declare` 不碰它)」**让人手补**。 🔴 **为什么必须自动做**:旧目标做完 ⇒ `lifecycle` 停在「已完成」⇒ 换到新目标后 **机制照旧不拉检查会话**(`collabd.py` 的判据:`life != 进行中` 时只在 "队列还有非 done 件"才拉)⇒ **新目标没人检查** = 与用户"切换后要关注检查"**方向相反**。 ⚠️ 且靠人手补 = 又一条"要记得"的待办(同族:用户「最好是记录会话,这样就不用每次 会话都去做声明了」—— **凡"要人记"的都会忘**)。 ⚠️ **不是破坏性动作**:旧 `goal.json` **整份归档**(同一步做)⇒ 旧值可回查; 且只在**已确认换目标**时做,⛔ 纯细化(只改措辞/补 why/kpi)不触发。 🔴 **判据必须行为级**(⛔ 不许只查源码里有没有那行): 真造一份 `goal.json`(`lifecycle=已完成`)⇒ 真跑 `goalctl.py declare --switch-goal --yes` ⇒ 读回落盘的 `lifecycle` 必须是「进行中」;**对照组**:不带 `--switch-goal` 的 纯细化(标题不变)⇒ `lifecycle` **必须原样不动**(⛔ 证明复位绑的是"换目标"这个事件, 不是"跑了一次 declare")。 ⚠️ 隔离一律用**临时 `COLLABD_CONFIG`**(同 `t_acc_selfref_excluded`: 只设 env 不给 config 会回落读到真工作区,曾覆盖真 `goal.json`)。 """ import json as _j import tempfile as _tf import subprocess as _sb import pathlib as _pl res = [] gctl = HERE / "goalctl.py" # 源码级:确认 `_is_switch` 分支里真写了 lifecycle(⛔ 只作辅助,主判在行为级) src = gctl.read_text(encoding="utf-8") res.append(("`declare` 的换目标分支里**真写** `new[\"lifecycle\"]`(⛔ 不再只打提示)", 'new["lifecycle"] = "进行中"' in src)) # 🔴🔴 2026-10-05 **误红修正**:第一版直接查 `"别忘了同步" not in src` ⇒ 误红。 # 真因:`goalctl.py` 里那句字面**仍合法保留在注释**中 —— 它是「**改前事实**」的 # 取证记录(写明"改前 `declare` 从不碰 lifecycle"),是有价值的考古证据, # ⛔ 不该为了过一条断言而删掉。 # ✅ 正解=**剔掉注释行再查**:只判「有没有这句提示出现在**可执行语句**里」。 _code_lines = [ln for ln in src.splitlines() if not ln.lstrip().startswith("#")] _code = "\n".join(_code_lines) res.append(("⛔ **可执行语句**里已无那句「别忘了同步 `lifecycle`」旧提示(注释里的改前取证不算)", "别忘了同步" not in _code)) res.append(("⛔ 也确认旧的「让人手补」`print` 真被换成自动复位提示", "自动复位为「进行中」" in src or "自动复位" in _code)) # 🔴🔴 2026-10-05 **护栏自身要被判**(⛔ 别让护栏变成死代码): # 本用例出过两次"隔离失效 ⇒ 静默写真工作区"的事故 ⇒ 夹具必须**自查落点**。 # ⚠️ 注意查的是**本文件**(`selftest.py`),⛔ 不是 `goalctl.py` —— 第一版查错文件 ⇒ 误红。 _self_src = Path(__file__).read_text(encoding="utf-8") res.append(("🔴 夹具**自查子进程自报落点**(越界即记红,⛔ 不许静默写到别处)", "夹具越界" in _self_src and "落点 `" in _self_src)) res.append(("🔴 夹具用对变量:`DSH_COLLAB_WS`(`_resolve_ws()` 的第一顺位)", '"DSH_COLLAB_WS": tmp' in _self_src)) def _run(tmp, args, goal): # 🔴🔴 2026-10-05 **隔离夹具的真修(本用例的第 2 个坑)**: # `goalctl.py` 的 `INBOX` 是**硬编码** `WS/"tmp"/"supervise-inbox"` # —— ⛔ **不读配置里的 `inbox` 字段**(实测:`{"inbox":"inbox"}` 被无视)。 # 第一版夹具把 `goal.json` 写在 `/inbox/` ⇒ 脚本读到「(无)」, # 写又落到 `/tmp/supervise-inbox/` ⇒ **读回当然还是旧值**(看着像"没生效")。 # ✅ 正解=**落点跟着脚本走**:夹具与脚本用**同一个** `WS/tmp/supervise-inbox`。 cfg = _pl.Path(tmp) / "collabd.config.json" cfg.write_text(_j.dumps({"workspace": tmp.replace("\\", "/"), "inbox": "inbox", "taskgraph": "inbox/taskgraph.json", "live": "inbox/realtime.md"}, ensure_ascii=False), encoding="utf-8") inbox = _pl.Path(tmp) / "tmp" / "supervise-inbox" inbox.mkdir(parents=True, exist_ok=True) (inbox / "goal.json").write_text(_j.dumps(goal, ensure_ascii=False), encoding="utf-8") # 🔴🔴 2026-10-05 **隔离夹具的真修**(同一课**第四遍**): # `goalctl.py:_resolve_ws()` 的候选顺序是 # `DSH_COLLAB_WS` → `DSH_WS_ROOT` → cwd(含 `.workbuddy/collab/`) # —— **⛔ 完全不看 `COLLABD_CONFIG`,⛔ 也不认 `COLLABD_WORKSPACE`**! # 第一版夹具只设了 `COLLABD_CONFIG` + `COLLABD_WORKSPACE` ⇒ `_resolve_ws()` 两个都不认, # cwd 又是临时目录(没有 `.workbuddy/collab/`)⇒ **回落到技能包上级目录**, # 而若那里恰好叠了真工作区 ⇒ **静默写进真 `goal.json`**(上一轮就是这么把真 title 改掉的)。 # ✅ 正解=**设 `DSH_COLLAB_WS`**(第一顺位,显式压倒一切)。 env = {**os.environ, "COLLABD_CONFIG": str(cfg).replace("\\", "/"), "DSH_COLLAB_WS": tmp.replace("\\", "/")} r = _sb.run([sys.executable, str(gctl)] + args, env=env, cwd=tmp, capture_output=True, timeout=90) # 🔴🔴🔴 2026-10-05 **兜底断言(防再犯)**:子进程**自报**的落点必须在本 tmp 内。 # 为什么要"自报"而不是"我猜":`goalctl.py` 的 `INBOX` 是**硬编码**的,夹具 # 一旦把目录猜错(本用例真犯过两次:`COLLABD_CONFIG` 不认、`inbox` 字段不读), # 就会**静默写到别处** —— 甚至写到真工作区(上一轮真出过事故)。 # ⇒ 从子进程 stdout 里抠出它自己打印的落点,**核对前缀**;不在 tmp 内 ⇒ 直接记红。 _so = (r.stdout or b"").decode("utf-8", "replace") _claimed = "" for _ln in _so.splitlines(): if "落点 `" in _ln and "`" in _ln: _claimed = _ln.split("落点 `", 1)[1].split("`", 1)[0] _normp = lambda s: str(s or "").replace("\\", "/").strip().lower().rstrip("/") _claimed_ok = (not _claimed) or _normp(_claimed).startswith(_normp(tmp)) try: back = _j.loads((inbox / "goal.json").read_text(encoding="utf-8")) except Exception as e: back = {"__err": repr(e)} if not _claimed_ok: back = {"__err": "🔴 夹具越界:子进程自报落点 %r 不在 tmp %r 内 ⇒ 已污染别处!" % (_claimed, tmp)} return (r.returncode, _so, (r.stderr or b"").decode("utf-8", "replace"), back) base = {"title": "旧目标甲", "short": "甲", "lifecycle": "已完成", "lifecycle_at": "2026-10-01T00:00:00", "acceptance_state": {"a1": "过|x"}} # 🔴🔴 2026-10-05 **防再犯硬护栏**(上一轮真出过事故:夹具隔离失效 ⇒ 静默改真 `goal.json`): # 跑任何写动作**前后**都量一遍**真工作区 `goal.json` 的 md5**,必须逐字不动。 # ⛔ 这不是"重复劳动" —— 它把「隔离失效」从**静默事故**变成**当场报红**。 _real_goal = Path(TEST_WS).parent / "supervise-inbox" / "goal.json" _digest = lambda p: (hashlib.sha1(p.read_bytes()).hexdigest() if p.is_file() else "(无)") _real_before = _digest(_real_goal) # ① 换目标(带 --switch-goal)⇒ 必须复位 try: d1 = _tf.mkdtemp(prefix="gl1-") rc1, o1, e1, b1 = _run(d1, ["declare", "--title", "新目标乙", "--switch-goal", "--yes"], dict(base)) res.append(("① 换目标:rc=0(实测 rc=%s;err=%s)" % (rc1, (e1 or "")[-120:]), rc1 == 0)) res.append(("① 换目标 ⇒ 标题真换成「新目标乙」(实测 %r)" % b1.get("title"), b1.get("title") == "新目标乙")) res.append(("🔴 换目标 ⇒ `lifecycle` 复位成「进行中」(实测 %r,改前会是『已完成』)" % b1.get("lifecycle"), b1.get("lifecycle") == "进行中")) res.append(("① 复位留痕(`lifecycle_by` 写明自动复位,实测 %r)" % str(b1.get("lifecycle_by"))[:40], "自动复位" in str(b1.get("lifecycle_by") or ""))) res.append(("① 旧值存进 `lifecycle_why` 可回查(实测 %r)" % str(b1.get("lifecycle_why"))[:50], "已完成" in str(b1.get("lifecycle_why") or ""))) res.append(("⛔ `acceptance_state` **不许被顺手清掉**(那是上一条目标的判据=证据)", bool(b1.get("acceptance_state")))) except Exception as e: res.append(("① 换目标用例跑不起来:%r" % (e,), False)) # ② 对照:**纯细化**(标题不变,只补 why)⇒ lifecycle 必须原样不动 try: d2 = _tf.mkdtemp(prefix="gl2-") g2 = dict(base); g2["title"] = "同名目标" rc2, o2, e2, b2 = _run(d2, ["declare", "--title", "同名目标", "--why", "补充说明", "--yes"], g2) res.append(("② 对照·纯细化:rc=0(实测 rc=%s)" % rc2, rc2 == 0)) res.append(("🔴 对照·纯细化 ⇒ `lifecycle` **原样不动**(实测 %r;⛔ 证复位绑的是『换目标』不是『跑过 declare』)" % b2.get("lifecycle"), b2.get("lifecycle") == "已完成")) except Exception as e: res.append(("② 对照用例跑不起来:%r" % (e,), False)) # 🔴🔴 硬护栏收口:真工作区 `goal.json` 必须**逐字未动** _real_after = _digest(_real_goal) res.append(("🔴🔴 护栏:**真工作区 `goal.json` 逐字未动**(%s ⇒ %s)" % (_real_before[:12], _real_after[:12]), _real_before == _real_after)) return res def main() -> int: only = "" if "-k" in sys.argv: only = sys.argv[sys.argv.index("-k") + 1] _prepare() _snap_prod() cases = [v for v in globals().values() if callable(v) and hasattr(v, "_case")] npass = nfail = nrep = 0 print("=" * 66) print("协作机制 · 回归自测(每改一次必跑) 测试工作区:%s" % TEST_WS) print("协作机制 · 回归自测(每改一次必跑) 测试工作区:%s" % TEST_WS) print("=" * 66) for fn in cases: if only and only not in fn._case: continue try: checks = fn() except Exception as exc: checks = [("用例抛异常:%r" % exc, False)] bad = [c for c, ok in checks if not ok] _ro = getattr(fn, "_report_only", False) if bad: nfail += 1 print(" ✗ %s" % fn._case) for c, ok in checks: print(" %s %s" % ("✓" if ok else "✗", c)) if _ro: nrep += 1 nfail -= 1 # 🔴 报告型不计入失败(见 `report()` 的说明) print(" ⓘ 报告型 ⇒ ⛔ 不计入 --verify 成败(存量问题,⛔ 与安装无关)") else: npass += 1 print(" ✓ %s (%d 项)" % (fn._case, len(checks))) print("-" * 66) print(" 合计:PASS %d / FAIL %d%s" % (npass, nfail, ("(另有 %d 条报告型,⛔ 不计入)" % nrep) if nrep else "")) print("=" * 66) return 0 if nfail == 0 else 1 @case("⛔ 派活触发句:说了就必须把执行会话拉起来(收工前闸)") def t_dispatch_gate(): """🔴 2026-10-07 立。 用户令逐字:「**重点问题是 会话接收到 调用执行会话完成目标 没有去调用对应技能去执行**」。 实测(会话 `b232218f` · vibe-product):用户 01:32:00 / 01:33:46 两次说触发句, 直到 01:36:27 才建出排期 —— 中间那一轮 AI 只在回复里解释口径,**没拉起执行会话**。 守三样(都确定性,⛔ 不依赖现网存量): ① 派活族触发句 ⇒ `skill-load-guard.py` 留待办(含**带空格**的写法 —— 实测用户就是这么写的); ② 非派活发言 ⇒ ⛔ 不留(⛔ 别把裸词「执行会话」当派活,那会误报); ③ 收工闸:同会话 + 超 90 s 宽限 + 该区此后**没有**新建排期 ⇒ 拦(`continue:false`)。 """ import json as _j import subprocess as _sp import tempfile as _tf import time as _t gate = HERE / "hooks" / "stop-dialog-guard.py" mark = HERE / "hooks" / "skill-load-guard.py" sid = "selftest-gate-%d" % os.getpid() tmp = _tf.mkdtemp(prefix="dsh-gate-") mk = os.path.join(tmp, ".workbuddy", ".dispatch-pending") os.makedirs(os.path.dirname(mk), exist_ok=True) def _feed(script, payload): return _sp.run([sys.executable, str(script)], input=_j.dumps(payload).encode("utf-8"), stdout=_sp.PIPE, stderr=_sp.PIPE, timeout=60 ).stdout.decode("utf-8", "replace") def _mark(ts): with open(mk, "w", encoding="utf-8", newline="\n") as f: f.write("%s\t%.3f\t使用执行会话\n" % (sid, ts)) r = [] for tag, prompt in (("直连写法", "调用执行会话完成目标:按 1-2-3 步骤做"), ("带空格写法", "应该使用 执行会话的技能去完成工作")): if os.path.exists(mk): os.remove(mk) _feed(mark, {"prompt": prompt, "session_id": sid, "cwd": tmp}) r.append(("派活句留待办(%s)" % tag, os.path.isfile(mk))) if os.path.exists(mk): os.remove(mk) _feed(mark, {"prompt": "重点是技能规则和机制也要和这几个文档匹配", "session_id": sid, "cwd": tmp}) r.append(("非派活句⛔ 不留待办", not os.path.isfile(mk))) pl = {"session_id": sid, "cwd": tmp, "stop_hook_active": False, "transcript_path": os.path.join(tmp, "x.jsonl")} _mark(_t.time() - 200) r.append(("超 90 s 宽限 + 该区未建排期 ⇒ 拦", '"continue": false' in _feed(gate, pl))) _mark(_t.time()) r.append(("宽限内(刚说完)⇒ 放行", '"continue": false' not in _feed(gate, pl))) return r @case("⛔ 机制自建的检查排期必须挂进工作区(少 workspace_scope ⇒ 会话落到未分组)") def t_check_schedule_scope(): """🔴 2026-10-07 立。 用户报障逐字:「**vibe-product 的检查会话没有创建到工作区下,跑到未分组的会话中去了**」。 实测根因:`collabd.py::create_check_schedule()` 的 INSERT **少了 `workspace_scope` 列** ⇒ 落库 NULL ⇒ 触发出来的会话被宿主当游乐场建(`is_playground=1` / `source_mode='craft'` / `use_sandbox_cli=0`)⇒ 不进工作区分组。 对照读数(同一晚两条排期):会话侧 `automation_update` 建的是 `'workspace'` ✓, 机制自建的是 `None` ❌;近 3 天 59 条排期里**只有机制那条**是 None。 """ src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace") i = src.find("INSERT INTO automations") seg = src[i:i + 1000] if i > 0 else "" return [ ("找得到建排期的那处 INSERT", i > 0), ("列清单里有 `workspace_scope`(⛔ 少了它,会话就落到未分组)", "workspace_scope" in seg), ("且写死 `workspace`(⛔ 不是 NULL、也不是别的档)", ('"workspace"' in seg) or ("'workspace'" in seg)), ] @case("⛔ 检查程序静默阈值:常量、闸门、试算、文案必须同源(⛔ 别只改一处)") def t_check_idle_min(): """🔴 2026-10-07 立(用户拍板:检查程序等待时间 **20 → 10 分钟**)。 这个数**散落在四处**,改一处漏三处就会"判据说 A、写给检查会话的文案说 B": ① 常量 `CHECK_IDLE_MIN`;② 闸门 `if idle_min < CHECK_IDLE_MIN`; ③ `--dry-run` 试算(⛔ 不同源 ⇒ 试算说"不会建"、实际却建了);④ `CHECK_PROMPT_GOAL` 的文案。 ⚠️ 本条**不锁具体数值**(换个值不该报红)—— 只锁"四处同源"。 """ import re as _re src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace") m = _re.search(r"^CHECK_IDLE_MIN = (\d+)", src, _re.M) v = int(m.group(1)) if m else -1 # ⚠️ 锚**定义处**(`= (`),⛔ 不能用 `find("CHECK_PROMPT_GOAL")` —— # 它先命中本常量上面那条注释 ⇒ 3000 字窗口够不到真正的文案 ⇒ **假红**(第一版就这么栽的)。 i = src.find("CHECK_PROMPT_GOAL = (") prompt = src[i:i + 3000] if i > 0 else "" return [ ("常量 `CHECK_IDLE_MIN` 存在且是整数(现为 %d 分钟)" % v, v > 0), ("闸门读常量、⛔ 不写死数字", "idle_min < CHECK_IDLE_MIN" in src), ("`--dry-run` 试算读同一个常量(⛔ 不同源 ⇒ 试算与实际打架)", "idle >= CHECK_IDLE_MIN" in src), ("写给检查会话的文案与常量同值(⛔ 别只改判据、文案还写旧值)", ("≥%d 分钟" % v) in prompt), ] @case("⛔ 加载闸门:派活族不受冷却 + 冷却跳过要留痕(⛔ 别让抑制态隐身)") def t_skillguard_cooldown(): """🔴 2026-10-07 立(用户问「有什么影响 该如何处理」时落的)。 实测(会话 `b232218f`):01:32:00 命中后,**01:33:46 那条最明确的触发句** (「每次我说使用执行会话完成目标…」)距上次仅 **105 s < COOLDOWN(300)** ⇒ 被吞掉, 且旧实现**只在注入成功后才写日志** ⇒ 连一行痕迹都没有(排查时只能靠状态文件时间戳反推)。 两处处置:① 派活族显式触发句**不受冷却**;② 被冷却跳过**写 `COOL_SKIP`**。 ⚠️ 本条是**源码级**判据(跑一次真钩子会动到真工作区的冷却状态,⛔ 不适合放进回归套)。 """ src = (HERE / "hooks" / "skill-load-guard.py").read_text(encoding="utf-8", errors="replace") return [ ("有共用的 `_is_dispatch()`(⛔ 别在两处各写一遍判定)", "def _is_dispatch(" in src), ("冷却旁路在:`_cooling(...) and not (_is_dispatch(...) or _is_load(...))`", "_cooling(workdir, sid) and not (_is_dispatch(prompt) or _is_load(prompt))" in src), ("被跳过写 `COOL_SKIP`(⛔ 否则「没命中」与「被吞」分不出来)", "COOL_SKIP" in src), ] @case("⛔ 点名了机制却没加载:注入之外还要有「收工前自检」抓手") def t_load_gate(): """🔴 2026-10-07 立(用户令逐字:「**问题是 会话没有加载决策方法,之前的优化改动无效**」)。 实测(`content_marketing_agent` 的闸门日志):`20:09:45 HIT … hits=决策方法` ⇒ **注入确实发出**, 可会话**仍未去加载** ⇒ 根因=注入只是"劝",**没有任何东西核对 `Skill` 到底调没调**。 两处处置:① 点名族命中即写 `/.workbuddy/.load-pending`; ② `stop-dialog-guard.py` 收工前核「转录尾部里有没有**真的** Skill 加载过本技能」。 ⚠️ 源码级判据(跑真钩子会动到真工作区的状态,⛔ 不适合进回归套)。 """ a = (HERE / "hooks" / "skill-load-guard.py").read_text(encoding="utf-8", errors="replace") b = (HERE / "hooks" / "stop-dialog-guard.py").read_text(encoding="utf-8", errors="replace") return [ ("点名族词表从 `TRIGGERS` **减**出来(⛔ 不另抄一份:`_LOAD_WORDS = tuple(...)`)", "_LOAD_WORDS = tuple(w for w in TRIGGERS" in a), ("命中即写待办 `LOAD_PENDING`(⛔ 写在冷却之前)", "LOAD_PENDING" in a and "_mark_load(" in a), ("收工闸在:`_load_gate(payload)` 已接进 main,且在自作用域判定之前", "def _load_gate(" in b and "if _load_gate(payload):" in b), ("判据认**转义引号**那种编码(⛔ 普通引号匹配 = 恒假阴性)", '\\\\"skill\\\\":\\s*\\\\"session-mechanism' in b), ("只读转录**尾部**(⛔ 不整读,有 200 MB 级转录)", "LOAD_TAIL_BYTES" in b), ("读不到转录 ⇒ 判「不确定」放行(fail-open)", "got is None" in b), ] @case("⛔ 检查任务要「确认并写回验收」,⛔ 不是只读一遍就标完成") def t_checker_confirms_acceptance(): """🔴 2026-10-07 立(用户令逐字:「**不是拦截 是检查任务要确认这种情况**」)。 实测病例(`content_marketing_agent`):权威文档写「5 条全过」,而 `goal.json.acceptance_state` 仍是「全未过」⇒ 检查会话按文档判「做完了」并标了「已完成」⇒ 看板显示自相矛盾。 根因=检查会话**有判验收的职责、却没有写回验收的动作**(="有读者、没写手")。 ⚠️ 用户明确否掉「加闸拦截」这条路 ⇒ 判据必须落在**检查会话的职责与动作**上。 """ src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace") i = src.find("CHECK_PROMPT_GOAL = (") p = src[i:i + 6000] if i > 0 else "" return [ ("检查任务 prompt 要求**先确认并写回验收**、再改状态", "确认并写回验收" in p), ("给了写回手段(`goalctl.py declare --kpi`),⛔ 不是只叫它「去同步一下」", "declare --kpi" in p), ("写明 `--kpi` **按分号切**(该分隔符本项目栽过反)", "按分号切" in p), ("要求写完**回读确认**(⛔ 不许「以为写成功了」)", "必须回读一次" in p), ("写明**规范取值**、⛔ 禁 `未过`/`pass` 这类域外值(机器分不出「还没判」与「判了没过」)", "不在取值域里" in p), ("确认确实没过 ⇒ 继续派活、⛔ 不许标已完成", "不许标已完成" in p), ] @case("⛔ 说人话:既要「被看到」(每轮注入)也要有英文硬核版在包内") def t_voice_core(): """🔴 2026-10-07 立(用户令逐字:「重点就是做到说人话就行了」)。 病根(§26):语气那一档原先**只有指针、没有注入** ⇒ 实际没被读到 ⇒ 用户报障「content_marketing_agent 的会话生成的文档并没有说人话,还是我手动要求的」。 处置:① 照排版那条**每轮注入**的现成路子给语气也做一块; ② 按用户令把英文硬核版(55 模式/5 语气档/打分)整包纳入本包。 """ h = (HERE / "hooks" / "reply-style-guard.py").read_text(encoding="utf-8", errors="replace") d = (HERE.parent / "references" / "作业规矩" / "04-去AI味与说话方式.md").read_text(encoding="utf-8", errors="replace") en = HERE.parent / "references" / "humanizer-en" / "SKILL.md" return [ ("文件里有 `VOICE-CORE` 标记对(注入块取得到)", d.count("") == 1), ("注入钩子读该标记并接进 `_core`(⛔ 不是只写进文档)", "VOICE_BEGIN" in h and "def _voice_core(" in h), ("接入方式是「原地重定义」:`_core` 包住原实现(⛔ 不改 `main()`)", "_core_reply = _core" in h and "c, p = _core_reply(workdir)" in h), ("英文硬核版已整包纳进包内(`humanizer-en/SKILL.md` 在位)", en.is_file()), ] @case("🔴 孤儿锁:授权检查会话自解除 —— 但**只**放行「持有者已退出」这一档") def t_orphan_lock(): """🔴 2026-10-09 立(用户令逐字:「如果 工作区 项目机制 对应的会话异常 导致未解锁的情况下, 可以授权 检查会话自解除孤儿锁」)。 病根:`contentm_agent` 一把孤儿锁(持有者会话已被 terminated)把目标第 4 步挡住, 而 R9「AI 一律不得删锁、不得接管」只有行为约定、**没有任何执行面** ⇒ 检查会话连开 11 棒 (约 5 小时)每棒都撞同一面墙。⇒ 授权必须落成**一条带判据的命令**,⛔ 不靠 AI 记得去取证。 ⚠️ 本用例**非有变异对照不可**:只验「terminated 能解除」的话,一个「见锁就删」的野实现 也照样全绿 —— 那正是 R9 要防的事。⇒ 必须同时验「`working` 的锁一把都不许动」。 """ import importlib.util import shutil as _sh import sqlite3 as _sq import time as _t spec = importlib.util.spec_from_file_location("orphan_lock", HERE / "lock" / "orphan-lock.py") m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) now_ms = int(_t.time() * 1000) old_ms = now_ms - 40 * 60 * 1000 # 40 分钟前 unit = [ ("持有者 `working` ⇒ ⛔ 不动(R9 原样)", m._judge("working", now_ms, 30.0)[0] is False), ("持有者 `terminated` ⇒ 可解除", m._judge("terminated", 0, 30.0)[0] is True), ("持有者 `error` ⇒ 可解除", m._judge("error", 0, 30.0)[0] is True), ("持有者 `archived` ⇒ 可解除", m._judge("archived", 0, 30.0)[0] is True), ("`completed` 但刚收工(0 分钟)⇒ ⛔ 不动(别掐掉随时会续聊的会话)", m._judge("completed", now_ms, 30.0)[0] is False), ("`completed` 且已静默 40 分钟 ⇒ 可解除", m._judge("completed", old_ms, 30.0)[0] is True), ("`completed` 但读不到活动时刻 ⇒ ⛔ 不动(fail-closed)", m._judge("completed", None, 30.0)[0] is False), ("查不到该会话(status 空)⇒ ⛔ 不动", m._judge("", None, 30.0)[0] is False), ("状态不在已知档 ⇒ ⛔ 不动(fail-closed)", m._judge("某个没见过的档", None, 30.0)[0] is False), ] # ── 端到端:真造锁目录 + 真造宿主库,跑真脚本 ──────────────────────── T = TEST_WS / "orphan" _sh.rmtree(T, ignore_errors=True) T.mkdir(parents=True, exist_ok=True) db = T / "host.db" con = _sq.connect(str(db)) con.execute("create table sessions (id text, status text, last_activity_at integer)") con.execute("insert into sessions values (?,?,?)", ("deadbeef-0000-0000-0000-000000000001", "terminated", now_ms)) con.execute("insert into sessions values (?,?,?)", ("alivebee-0000-0000-0000-000000000002", "working", now_ms)) con.commit() con.close() def _mk(d, owner, sid): p = T / d p.mkdir(parents=True, exist_ok=True) (p / "OWNER").write_text("%s\n开始:10-09 00:48\n会话:%s\nME:%s\n" % (owner, sid, owner), encoding="utf-8") return p _mk("lock-dead", "执行会话-甲", "deadbeef-0000-0000-0000-000000000001") _mk("lock-alive", "执行会话-乙", "alivebee-0000-0000-0000-000000000002") _mk("lock-nosid", "执行会话-丙", "未取到") # 旧锁没记 sid def _run(hostdb): env = dict(os.environ) env["DSH_HOST_DB"] = str(hostdb) env["COLLABD_NO_ENSURE"] = "1" p = subprocess.run([PY, "-u", str(HERE / "lock" / "orphan-lock.py"), "--locks-root", str(T)], cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env, timeout=60, errors="replace") return p.returncode, p.stdout or "" rc, out = _run(db) e2e = [ ("持有者 `terminated` 的锁**真被解除**", not (T / "lock-dead").exists()), ("持有者 `working` 的锁**原封不动**(R9,本用例的变异对照)", (T / "lock-alive").is_dir()), ("`OWNER` 里没记 sid 的锁**原封不动**(fail-closed)", (T / "lock-nosid").is_dir()), ("有锁未动 ⇒ 退出码 1(⛔ 不是「我全清了」的 0)", rc == 1), ("输出里写明了授权来源(可追溯)", "2026-10-09" in out), ] # 反向对照:宿主库读不到 ⇒ **一把都不许动** _sh.rmtree(T, ignore_errors=True) T.mkdir(parents=True, exist_ok=True) _mk("lock-dead", "执行会话-甲", "deadbeef-0000-0000-0000-000000000001") rc2, out2 = _run(T / "不存在.db") neg = [ ("宿主库读不到 ⇒ 一把锁都不动(fail-closed)", (T / "lock-dead").is_dir()), ("宿主库读不到 ⇒ 退出码 1", rc2 == 1), ] _sh.rmtree(T, ignore_errors=True) return unit + e2e + neg @case("🔴🔴 遇阻碍 ⇒ 置「阻碍」⇒ 机制暂停(⛔ 但「已完成」仍要建 —— 变异对照)") def t_blocked_goal_pauses_check(): """🔴 2026-10-09 立(用户令逐字:「**其他类型 阻碍 遇到阻碍修改目标状态 暂停机制 让用户决策**」)。 **病根**(`contentm_agent` 实测坐实):口径「遇阻碍 ⇒ 改目标状态 ⇒ 暂停机制」**只在注释里**, 代码里没接:① 闸③「`sessions-ended` 队列必须非空」对一条 `blocked` **恒真**; ② 闸① 2026-10-04 收窄时把 `sessions-ended` **整条腿**摘了出去 ⇒ 就算置了「阻碍」也照样建; ③ 结果检查 prompt 又明写「你没有改目标状态的出口」⇒ 看得见的没权、有权的看不见。 ⇒ 三条腿互相锁死,只剩**每 30 分钟空转一棒**(实测连开 11 棒、约 5 小时、零派活)。 ⚠️ **变异对照非有不可**:只验「阻碍 ⇒ 不建」的话,把闸① **整道**搬回来(=2026-10-04 修过的 病:目标被标「已完成」而队列里还有活 ⇒ 活永远没人接)也照样全绿。⇒ 必须同时验「已完成 ⇒ 仍建」。 """ import inspect as _ins m = imp() made = [] def _fake_create(name, prompt, delay_s=90, cwd=""): made.append(name) return {"ok": True, "id": "selftest", "fire_at": "00:00"} # 只桩掉"外部世界",闸本身的判据一律走真代码 m.create_check_schedule = _fake_create m._all_sessions_idle = lambda: True m.queue_pending = lambda: 1 # 队列非空 ⇒ 满足闸③(`sessions-ended` 的条件) m.ws_pending_schedules = lambda exclude=None: [] m.active_schedule_exists = lambda n: False m._check_stamp = lambda: {} m._check_stamp_save = lambda d: None m.log = lambda *a, **k: None out = [] for life, want in ((m.GOAL_LIFE_BLOCK, False), (m.GOAL_LIFE_RUN, True), (m.GOAL_LIFE_DONE, True), (m.GOAL_LIFE_WAIT, True)): made.clear() m.goal_life = (lambda L: (lambda: L))(life) try: r = m.maybe_spawn_check_agent("sessions-ended") except Exception as e: out.append(("目标=%s:闸不炸(实得 %s)" % (life, e.__class__.__name__), False)) continue built = (r is not None) or bool(made) note = "建" if built else "不建" if life == m.GOAL_LIFE_BLOCK: out.append(("目标=阻碍 ⇒ **停建**(机制暂停、等用户决策;实得:%s)" % note, not built)) elif life == m.GOAL_LIFE_DONE: out.append(("目标=已完成 ⇒ **仍要建**(⛔ 别把 2026-10-04 的病搬回来;实得:%s)" % note, built)) else: out.append(("目标=%s ⇒ 照常建(实得:%s)" % (life, note), built)) # 结构断言:闸① 必须**在闸②(会话空闲)之前**读到 `goal_life()`(⛔ 顺序颠倒=白拦) src = _ins.getsource(m.maybe_spawn_check_agent) p1 = src.find("goal_life()") p2 = src.find("_all_sessions_idle()") out.append(("闸① 排在闸② 之前(先判目标状态,再判会话是否结束)", p1 >= 0 and p2 >= 0 and p1 < p2)) out.append(("闸① 保留 `queue-empty` 那档原样(⛔ 没把 2026-10-04 的收窄改坏)", 'reason == "queue-empty"' in src and "GOAL_LIFE_RUN" in src)) return out if __name__ == "__main__": sys.exit(main())