session-mechanism: 修复钩子静默失效 + 3 处判据缺陷;禁「变相征询」

1) stop-dialog-guard: session_budget() 早退路径返回 2 值、末尾返回 3 值,调用方按 3 值解包
   ⇒ transcript > 64 MiB 时每轮 ValueError。因 fail-open(异常仍 exit 0),
   宿主零报错、install.py --verify 只判 rc=0 ⇒ 假绿;实测 86 条 EXCEPTION,
   死掉的是整条(水位/收口、接续机制起点、预算告警、门禁自检、路径自检)。

2) session-rules-check 三处判据:
   · hook_reg  按旧文件名找 ⇒ 合并成 prompt-guards.py 后每轮假红 ⇒ 改为一组可接受名
   · snap_sync 拿 mtime 当内容判据 ⇒ 连续 4 天假红 ⇒ 改为复用抽取器本体比对内容
               (变异对照:截断快照能报 fail,非恒绿)
   · mem_ptr   只查全局技能根 ⇒ 工作区自带技能被判悬空 ⇒ 改查「全局 ∪ 工作区」

3) pitfalls 新增 P0-95(改判据必须重跑变异对照;fail-open + 只看 rc=0 = 假绿温床)

4) 回复排版核心块新增「变相征询同样禁止」(先只报不动/等你发话/我倾向X你看呢
   这类不带选项的待定清单,一律按待拍板项写:问题+说明+各候选优缺点+倾向)
This commit is contained in:
admin committed 2026-10-06 22:27:03 +08:00
1 parent 19101acd65
commit 64dd82073b
21 files changed
+8444 -4523

No files matched your search

+672 -123
View File
@@ -44,6 +44,7 @@ except Exception:
pass
import hashlib
import json
import os
import re
@@ -79,6 +80,66 @@ def _mech_on() -> bool:
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)
@@ -359,6 +420,169 @@ def t_goal_pending():
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` 里**只有 `_说明`/`_更新`
@@ -1055,18 +1279,80 @@ def t_lock_held():
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/...` ⇒ 换掉配置根即可**完全离线**复现本坑。
用 `truncate` 建**稀疏文件**:秒级拿到 10 MiB 的**体积读数**(`getsize`),⛔ 不真写 10 MiB。
`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():
with open(root / (_sid + ".log"), "wb") as f:
_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"
@@ -2546,16 +2832,20 @@ def t_board_multi_goal():
_in.mkdir(parents=True, exist_ok=True)
# ⚠️ **三个目标**,⛔ 不是一个 —— 单块时任何排序键都「升序」,变异根本抓不到
#(实测栽过:删掉 `blocks.sort(...)` 判据照样绿)。
# 🔴🔴 2026-10-06:**`declared_at` 必须"乱序"给**(t2 最老、t0 居中、t3 最新),
# 而且**文件出现次序 = 声明时间倒序** ⇒ 只要排序失效,输出顺序**立刻不一样**。
# ⛔ 若三个时间给成同序/全相同 ⇒ 排序与否输出无差异 ⇒ **判据恒绿**(同族已栽多次)。
(_in / "goal.json").write_text(_j8.dumps(
{"id": "t0", "short": "甲", "title": "甲目标", "lifecycle": "进行中",
"run": "active", "acceptance_state": {"A1": "过|x"}}, ensure_ascii=False),
"run": "active", "declared_at": "2026-10-02T10:00",
"acceptance_state": {"A1": "过|x"}}, ensure_ascii=False),
encoding="utf-8")
_gd8 = _in / "goals"
_gd8.mkdir(parents=True, exist_ok=True)
for _i in (2, 3):
for _i, _at in ((2, "2026-10-01T09:00"), (3, "2026-10-03T11:00")):
(_gd8 / ("%d.json" % _i)).write_text(_j8.dumps(
{"id": "t%d" % _i, "short": "乙%d" % _i, "title": "乙目标%d" % _i,
"lifecycle": "进行中", "run": "active",
"lifecycle": "进行中", "run": "active", "declared_at": _at,
"acceptance_state": {"A1": "过|x"}}, ensure_ascii=False),
encoding="utf-8")
# 🔴🔴 **env 必须「白名单重建」,⛔ 不能 `dict(os.environ, ...)`**:
@@ -2576,34 +2866,63 @@ def t_board_multi_goal():
_bj = _in / "board.json"
_d8 = _j8.loads(_bj.read_text(encoding="utf-8")) if _bj.exists() else {}
_gs8 = _d8.get("goals") or []
_hots = [g.get("tab_hot") for g in _gs8]
_R.append(("后端给了 `tab_hot` 排序键(实得:%s)" % (_hots[:1] or "无"),
bool(_hots) and all(h is not None for h in _hots)))
_R.append(("后端 `tab_hot` **升序**(最新活动在前):%s" % _hots,
_hots == sorted(_hots)))
# 🔴 这条要能抓「无活动当0」那个变异,靠**多个目标**才看得出排序差异:
# 变异前 = [1e9](单块,升序也成立);变异后 = [0] ⇒ 抓不到。
# ⇒ 这里**直接钉住数值**:没活动的必须是垫底大数,⛔ 不是 0。
_R.append(("没活动的目标 `tab_hot`=**垫底大数**(⛔ 当 0 就会排最前):%s" % _hots,
bool(_hots) and all(h == 1e9 for h in _hots)))
# 🔴🔴 2026-10-06 用户定案:「**tab 的排列顺序应该按创建时间顺序**」(推翻 10-04 的活跃度排序)。
# ⇒ 判据整体改判 **`tab_created`(=`declared_at`)升序**。
# ⚠️ **必须钉住"实际次序"**,⛔ 不能只判"升序":夹具里三块时间**全不同** ⇒
# 升序成立**等价于**真的按时间排;但若哪天夹具退化成同值,升序就恒真 ⇒
# ⇒ 所以**额外钉一条"首个是最老的"**(`2026-10-01`)当冗余保险。
_crts = [g.get("tab_created") for g in _gs8]
_R.append(("后端给了 `tab_created` 排序键(实得:%s)" % (_crts[:3] or "无"),
bool(_gs8) and all(c is not None for c in _crts)))
_R.append(("后端 `tab_created` **升序**(最早创建的在最前):%s" % _crts,
_crts == sorted(_crts)))
# 🔴 抓「拿活跃度当创建时间」/「键取错字段」:夹具里 **t2 最老**(10-01)
# ⇒ 排第一的必须是它;⛔ 若还是甲(goal.json,文件序第一)= 没排序。
# ⚠️ `short` 在块的 **`goal` 子字典**里(⛔ 不在块顶层 —— 我第一版就取错层,
# 判据恒红;实测栽过)。
_R.append(("首个 tab = **最早创建的目标**(夹具里是最老的 `乙2`):%s"
% [str((g.get("goal") or {}).get("short") or "") for g in _gs8][:1],
bool(_gs8)
and str((_gs8[0].get("goal") or {}).get("short") or "") == "乙2"))
# 🔴 抓「缺 `declared_at` 的块被当成 0/空串 ⇒ 排到最前」:
# ⚠️ **必须真造一块没有 `declared_at` 的**(t4,且它的标题明显"最老",人眼会想放前面)
# ⇒ 它必须**垫底**(`tab_created == "~"` 且 `tab_i` 最大),⛔ 不许因为读不到就抢到第一。
# ⛔ 不造这一块 ⇒ 本判据**恒真**(列表里没有 `~`)= 假绿(同族已栽多次)。
_gd8.joinpath("4.json").write_text(_j8.dumps(
{"id": "t4", "short": "丙4", "title": "丙目标4", "lifecycle": "进行中",
"run": "active", "acceptance_state": {"A1": "过|x"}}, ensure_ascii=False),
encoding="utf-8")
_sp8.run([_s8.executable, str(HERE / "board.py"), "--out", str(_in / "board.json")],
capture_output=True, text=True, timeout=180, env=_env8,
encoding="utf-8", errors="replace")
_d9 = _j8.loads((_in / "board.json").read_text(encoding="utf-8")) \
if (_in / "board.json").exists() else {}
_gs9 = _d9.get("goals") or []
_c9 = [g.get("tab_created") for g in _gs9]
_i9 = [g.get("tab_i") for g in _gs9]
_n9 = len(_gs9)
_R.append(("缺 `declared_at` 的块 **垫底**(⛔ 不许读不到就排最前):%s" % _c9,
_n9 == 4 and _c9[-1] == "~" and _c9[:3] == sorted(_c9[:3])
and _i9 == [0, 1, 2, 3]))
_R.append(("垫底块的 `tab_i` = **最后一位**(⛔ 不是靠 '~' 撞巧):%s" % _i9,
_n9 == 4 and _i9[-1] == 3))
_R.append(("后端给了 `tab_more`(前端⛔ 不自己重算 `gs.length-6`):%s"
% [g.get("tab_more") for g in _gs8],
bool(_gs8) and all(g.get("tab_more") == max(0, len(_gs8) - 6)
for g in _gs8)))
# 🔴 抓「排序后没重编序号」:多块时 `tab_i` 必须恰好是 0..n-1
# (⛔ 排序前的旧序号会留着 ⇒ 前端 `data-gi` 与位置对不上 ⇒ 点A切到B)。
# 🔴 抓「干脆不排序」:三块 `tab_hot` 全相同(都没活动)⇒ 排序与否**输出无差异**
# ⇒ 上面那条「升序」永远成立、永远绿(实测栽过:删掉 `blocks.sort(...)` 判据照绿)。
# ⇒ 只能**钉住代码里存在排序语句**(⛔ 这是源码级判据的正当用途:验"语句在不在")。
# 🔴 抓「干脆不排序」:夹具里文件出现次序**本就是时间倒序**(t0 居中、t2 最老在后加)
# ⇒ 排序与否**输出有差异** ⇒ 上面那条"升序"能抓到;此处再钉一条源码级保险
# (⛔ 这是源码级判据的正当用途:验"语句在不在")。
_bsrc = (HERE / "board.py").read_text(encoding="utf-8")
_R.append(("`blocks.sort(...)` 真在(⛔ 键全相同时删掉它输出无差异、判据会假绿)",
"blocks.sort(key=lambda b: (b.get(\"tab_hot\"" in _bsrc))
# 🔴 抓「取 max 变取 min」:⛔ **行为上抓不到** —— 夹具里几块 `tab_hot` 全相同
# (都没活动)⇒ `min` 与 `max` 结果**一样** ⇒ 判据照样绿(实测栽过)。
# ⇒ 只能钉住**聚合函数名**(验"语句在不在、用的哪个函数"这种纯形式问题,源码级是正当的)。
_R.append(("排序键取的是 **min**(最新活动在前;⛔ 取 max = 越旧越靠前):%s"
% ("min(_ages)" in _bsrc),
"min(_ages)" in _bsrc and "max(_ages)" not in _bsrc))
_R.append(("`blocks.sort(...)` 真在且按 `tab_created`:%s"
% ("tab_created" in _bsrc and "blocks.sort(" in _bsrc),
'blocks.sort(key=lambda b: (b.get("tab_created"' in _bsrc))
# 🔴 抓「又改回活跃度排序」(口径回退):⛔ 源码里不许再出现 `tab_hot` 当排序键。
_R.append(("排序键**不再是** `tab_hot`(⛔ 防口径回退):%s"
% ("blocks.sort(key=lambda b: (b.get(\"tab_hot\"" not in _bsrc),
"blocks.sort(key=lambda b: (b.get(\"tab_hot\"" not in _bsrc))
_tis = [g.get("tab_i") for g in _gs8]
_R.append(("多目标时 `tab_i` **重编为 0..n-1**(⛔ 不重编 = 前端点A切到B):%s" % _tis,
len(_gs8) > 1 and _tis == list(range(len(_gs8)))))
@@ -3740,12 +4059,44 @@ def t_execution_doc():
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),
("⚠️ 生产 `goal.json` 已登记 `execution_doc`(%s)" % (prod_g or "未登记"), bool(prod_g)),
# ⚠️ 原第 5 条「生产 `goal.json` 已登记 `execution_doc`」已**并入上面第 4 条**
# (那条现在既判"登记了"又判"登记对");⛔ 别两条都留 ⇒ 同一个事实两处判,
# 迟早一处改了另一处忘了(同族 P0:同一规则两份实现)。
("目标检查 prompt 把它列为**判断依据**", "目标执行状态文档" in p_goal),
("⛔ 明确禁止再去工作区翻文件(用户报障:到处找⇒被判重复执行)",
"别再去工作区里翻文件" in p_goal),
@@ -3914,15 +4265,32 @@ def t_goal_dir_mechanism():
# ="字面反斜杠+s",⛔ 匹配不到空格 ⇒ **误报**,而产品代码其实完全正确:
# 实测 `目标-s-t-c245d9` 无非法字符、无空格 ⇒ 教训=**判据自身的转义错误会伪装成产品缺陷**,
# 改完要回头确认"被测对象到底对不对",⛔ 别直接改产品去迎合判据。
# ⚠️ 判据拆成**两条**:非法字符 与 空格 分开(踩过:合成一条时 `\\s` 写在 raw 串里
# ="字面反斜杠+s",⛔ 匹配不到空格 ⇒ **误报**,而产品代码其实完全正确:
# 实测 `目标-s-t-c245d9` 无非法字符、无空格 ⇒ 教训=**判据自身的转义错误会伪装成产品缺陷**,
# 改完要回头确认"被测对象到底对不对",⛔ 别直接改产品去迎合判据。
("目录名已清洗非法字符(实测 %r)" % clean,
not _re.search(r"[\\/:*?\"<>|]", clean)),
# 🔴🔴 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)),
@@ -4208,19 +4576,45 @@ def t_tab_peer_workspace():
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:
_n = len((json.loads(_pcfg.read_text(encoding="utf-8")) or {}).get(
"peer_workspaces") or [])
# ⚠️ 两侧粒度不同,⛔ 不能直接比字符串:
# · `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 = [g.get("peer") for g in gp if g.get("peer")]
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")]
prod_ok = (_n >= 1 and len(pp) == _n and len(ap) == 1)
prod_txt = "生产配置登记 %d 个 peer ⇒ %d 格(peer=%s,active %d)" % (
_n, len(gp), pp, len(ap))
# 出格数 ≤ 登记数(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 [
@@ -4357,14 +4751,13 @@ def t_view_takes_all_scoped_fields():
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_hot`=该块最近会话活动距今分钟数、`tab_i`=排序后重编的 tab 序号
# —— 二者由 `board.py` 在 `blocks.sort(key=…)` 里**当场用掉**(board.py L1883-1887),
# 前端 `board.html` 对它们的**消费点命中数 = 0**(`tab_i` grep 命中 0;`tab_hot` 仅出现在
# 一句解释「排序由后端做」的注释里)⇒ 它们**不是视图字段**,
# `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_hot", "tab_i"}
_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)]
@@ -4686,6 +5079,24 @@ def t_peer_supervise_sweep():
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` 与末尾
@@ -4710,8 +5121,16 @@ def t_init_points_to_own_copy():
"_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 and "_bd_show" in code),
"_cd_show" in code),
("末尾提示**不起本区看板**、要并看走 `peer_workspaces`(2026-10-03 定案:看板只保留一份)",
_tail_has_board_note(code)),
]
return res
@@ -5132,6 +5551,14 @@ def t_pkg_hygiene():
# ⛔ 排除三个"提到备份名是**机制本身/说明书**"的文件
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")
@@ -5329,6 +5756,13 @@ def t_artifacts_land_in_goal_dir():
% (_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)
@@ -5337,15 +5771,17 @@ def t_artifacts_land_in_goal_dir():
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 都落在「目标-*/交付物」内(⛔ 代码/规则/归档除外);"
res.append(("产物型 md 都落在「执行会话/目标-*/ 或 目标-*/ 或 交付物/」内(⛔ 代码/规则/归档除外);"
"已在目标文件夹或交付物=%d,散在别处=%d %s"
% (inside, len(stray), ("→ " + str(stray[:5])) if stray else ""),
not stray))
@@ -6152,56 +6588,43 @@ def t_mirror_keep_by_name():
]
@case("🔴 各工作区自我建立常驻:**副本里也得找得到载体模板**(⛔ 写死包根=每区必红)")
def t_keeper_tpl_found_in_copy_layout():
"""🔴🔴 2026-10-04 实测踩到的 bug(**用户诉求「各工作区自己能建立常驻」的拦路石**)。
@case("🔴 常驻载体形态:**旧看守已彻底清除 + 不许回潮**(2026-10-05 收敛)")
def t_keeper_legacy_ps1_form_gone():
"""🔴🔴 2026-10-05 **形态收敛的守卫**(取代此前 `t_keeper_tpl_found_in_copy_layout`)。
**病根**:`_escalate_to_keeper()` 找模板写死
`Path(__file__).resolve().parent.parent / "assets" / ...`
· 技能目录里对(`<pkg>/scripts/collabd.py` ⇒ `.parent.parent` = `<pkg>`);
· **工作区副本里错**(`.workbuddy/collab/collabd.py` ⇒ `.parent.parent` =
`<WS>/.workbuddy` ⇒ 去找 `<WS>/.workbuddy/assets/`,**不存在**)
⇒ 每个工作区的自我供给**必然返回「⛔ 缺模板」**,
而模板其实好好躺在 `<WS>/.workbuddy/skills/session-mechanism/assets/`。
**为什么换掉旧的**:旧用例判「副本形态下找不找得到 `assets/start-supervise.ps1.tpl`」,
而该模板与「旧 看守」这套形态**已整套废弃** ——
任务动作从「壳脚本 -File」改成 **`pythonw.exe` + `supervise-launch.py`**
(GUI 子系统 ⇒ 零 conhost ⇒ **不闪黑窗**,用户 2026-10-05 报「又弹了窗口」的直接修复)。
⇒ 旧用例连**被测对象都不存在了** ⇒ 恒红、且无法修复 ⇒ **必须删,换成本条**。
判据**必须变异有效**(旧写法必须红、新写法必须绿):
造一个「副本形态」的目录(`<cfg>/collab/collabd.py` +
`<cfg>/skills/session-mechanism/assets/*.tpl`),
分别用**旧算法**与**新函数**去找 ⇒ 断言「旧红新绿」。
⛔ 只断言"函数存在"是**恒真**的 ⇒ 那种判据测不出东西。
🔴 本条的职责是**守形态**(防回潮),五条一起判:
① 代码里**不许再出现**读那个 `.tpl` 的路径拼接;
② 任务动作**必须是** `pythonw.exe` + 启动器;
③ ⛔ 动作里不许再把壳脚本当看守(⚠️ 登记任务本身仍走 PowerShell,
那是在**登记**、不是在**当看守**,两者别混);
④ `CREATE_NO_WINDOW` 在位;
⑤ `_find_keeper_tpl` 死代码已清。
"""
import importlib.util
import tempfile as _tf
res = []
# ① 源码级:模板查找已收进 `_find_keeper_tpl`(⛔ 不再散在 `_escalate_to_keeper` 里写死)
src = (HERE / "collabd.py").read_text(encoding="utf-8")
res.append(("有 `_find_keeper_tpl()` 这个唯一查找实现", "def _find_keeper_tpl(" in src))
res.append(("`_escalate_to_keeper` 改调它(⛔ 不再自己拼路径)",
"_tpl = _find_keeper_tpl()" in src))
res.append(("⚠️ 旧写法(写死 `parent.parent/assets`)**只在候选里**、⛔ 不再是唯一路径",
'Path(__file__).resolve().parent.parent / "assets"' in src
and "_cands" in src))
# ② 行为级+变异对照:造副本形态,旧算法必红、新函数必绿
_root = Path(_tf.mkdtemp(prefix="tplchk_"))
try:
_cfg = _root / ".workbuddy"
(_cfg / "collab").mkdir(parents=True)
(_cfg / "skills" / "session-mechanism" / "assets").mkdir(parents=True)
_tpl = _cfg / "skills" / "session-mechanism" / "assets" / "start-supervise.ps1.tpl"
_tpl.write_text("$script = \"__SCRIPT__\"\n", encoding="utf-8")
_fake = _cfg / "collab" / "collabd.py"
_fake.write_text("# fake copy\n", encoding="utf-8")
# 变异体=旧算法(写死包根推)
_old = Path(str(_fake)).resolve().parent.parent / "assets" / "start-supervise.ps1.tpl"
# 新算法=真副本形态优先找 `<WS>/.workbuddy/skills/.../assets/`
_new = _cfg / "skills" / "session-mechanism" / "assets" / "start-supervise.ps1.tpl"
res.append(("🔴 **变异对照**:旧算法(写死包根)在副本形态下**找不到**(⛔ 恒绿判据测不出东西)",
not _old.is_file()))
res.append(("✅ 新算法(副本形态)**找得到**模板", _new.is_file()))
finally:
shutil.rmtree(str(_root), ignore_errors=True)
# 🔴🔴 2026-10-05 **必须剔注释 _和 docstring_ 再判**(同一课第 N 遍,这次真栽了):
# 考古记录**不止在 `#` 注释里** —— 还大量写在 **docstring**(`"""…"""`)里。
# 第一版只剔 `#` 行 ⇒ 744 行那句(在 `_escalate_to_keeper` 的 docstring 里)
# **照样命中** ⇒ 误红。⚠️ 「剔注释」≠「剔 docstring」,两者是**两件事**。
# ✅ 正解=用 `tokenize` 剔掉 COMMENT + STRING(最稳,不怕多行字符串/嵌套引号)。
_code = _strip_comments_and_docs(src)
res = []
res.append(("⛔ 已无「读 `start-supervise.ps1.tpl`」的路径拼接(形态废弃 ⇒ 不该有人再找它)",
"start-supervise.ps1.tpl" not in _code))
res.append(("🔴 任务动作=`pythonw.exe` + 启动器 `supervise-launch.py`",
"New-ScheduledTaskAction" in _code and "supervise-launch.py" in _code
and "with_name" in _code and "pythonw.exe" in src))
res.append(("🔴 动作里**不再有** `-File start-supervise.ps1`(⛔ 壳脚本当看守=闪黑窗)",
"-File start-supervise.ps1" not in _code))
res.append(("🔴 `CREATE_NO_WINDOW`(0x08000000)在位(⛔ 缺了每次登记都闪一下)",
"0x08000000" in _code))
res.append(("`_find_keeper_tpl` 死代码已清(⛔ 留着会再招一批过期判据)",
"def _find_keeper_tpl(" not in _code))
return res
@@ -6492,36 +6915,13 @@ def t_main_registered_on_goal_create():
return res
@case("🔴🔴 keeper 生成 ps1 时 `--script` 必须填**本区发布物**(⛔ 不许照着当前这份抄)")
def t_keeper_script_is_published_copy():
"""🔴🔴 2026-10-05 实测事故(**自我污染**,两区常驻一起被打歪)。
**现象**:`ai1net-dsh-server` 的 `.workbuddy/collab/start-supervise.ps1` 被重铺成
`$script = "…\\.workbuddy\\skills\\session-mechanism\\scripts\\collabd.py"`
⇒ keeper 拉起的全是**技能目录那份**,本区副本成**孤儿**(P0-57 判据②必不过)。
**真因**:`_escalate_to_keeper()` 里 `_own = Path(__file__).resolve()` ——
它填的是**"当前正在跑的那一份"**;而当前跑的那份**本身就可能是错的**
(技能目录那份)⇒ 重铺时把**正确的本区路径覆盖成技能目录**
⇒ **自我强化**:错一次就再也回不来。
✅ 正解=**发布物优先**:本区 `.workbuddy/collab/collabd.py` 在位 ⇒ `__SCRIPT__` 一律填它。
🔴 判据必须**变异有效**:撤掉"发布物优先"这层 ⇒ 本条必红。
"""
src = (HERE / "collabd.py").read_text(encoding="utf-8")
res = []
# 🔴 2026-10-05 二次收敛:判据抽进 `_own_path()` 唯一实现
# (原先三处各写一遍"以谁为准"= 同一事实三套答案,正是本 bug 的温床)。
res.append(("有唯一基准函数 `_own_path()`(⛔ 不再各处各写一份判定)",
"def _own_path() -> Path:" in src))
res.append(("🔴 `_own_path()` 里「发布物在位 ⇒ 用它」的分支",
'_pub = _cdir / "collabd.py"' in src or 'pub = Path(WS) / ".workbuddy" / "collab" / "collabd.py"' in src))
res.append(("🔴 `__SCRIPT__` 走 `_own_path()`(⛔ 不再直接填 `Path(__file__).resolve()`)",
"_own = _own_path()" in src))
res.append(("⚠️ 回落时**必须打日志告警**(⛔ 静默回落=用户以为跑的是副本)",
"keeper 的 --script 回落到当前这份" in src))
res.append(("替换表仍用 `_own` 填 `__SCRIPT__`(⛔ 没被改成别处算出的值)",
'"__SCRIPT__": str(_own)' in src))
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("🔴🔴 常驻**自我繁殖**必须断:续命只许起**本区发布物**(用户「越创建越多」的真因)")
@@ -6610,6 +7010,155 @@ def t_supervise_singleton_identity():
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` 写在 `<tmp>/inbox/` ⇒ 脚本读到「(无)」,
# 写又落到 `<tmp>/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: