decision-rules-hook: 注入补「两个停止点」+「技术方案排序轴」

起因(2026-10-06 用户问):能自决的问题为什么还要问。
复盘发现注入的判据里**缺动作**:
- 缺「十步」的两个停止点 → ① 判类型(方案请求 vs 直接执行)② 判方向(扩大/收窄/中性);
- 缺「自主裁决顺序」的排序轴 → 更小改动 → 更少新增形态 → **不扩大可见面**。
⇒ 前者导致「会扩大可见面」时不停手;后者导致排完仍把已定项混进待拍板清单(=变相征询)。
本次只补这两条动作骨架(1459 → 1906 字符),⛔ 不塞十步全文(避免稀释)。
This commit is contained in:
admin committed 2026-10-06 22:39:51 +08:00
1 parent 64dd82073b
commit 433d43af54
8 files changed
+195 -273

No files matched your search

+34 -240
View File
@@ -21,7 +21,6 @@
· 排期名必须是两级前缀 `[主]-<类别>-<具体>`(⛔ 光杆名 ⇒ 角色判不出,看板认不出)
"""
import argparse
import io
import json
import os
import subprocess
@@ -33,154 +32,10 @@ SK = Path(__file__).resolve().parent
DEFAULT_TITLE = "检查会话协作是否运行正常 + 协作机制问题排查与修复"
def _register_keeper_task(ws: Path, python_exe: Path) -> str:
"""🔴🔴 给本工作区**登记一条计划任务**(="各工作区都能自己常驻"的落点)。
## 为什么必须有这一步(2026-10-04 用户原话)
> 「我需要的是**各工作区会话 完成目标时 自己能建立 常驻程序**」
病根链(逐层实测):
① `ensure_supervise()` 能起常驻,但起法(`DETACHED_PROCESS`)**不脱离会话作业对象**
⇒ 会话一收工就被回收(vibe 会话查出,我已复验);
② 唯一合格载体=**计划任务**(父链根在 `svchost -s Schedule`,与会话树无关);
③ 而 `init_workspace.py` 原来**只铺脚本、⛔ 不登记任务**(当时口径:"
开机自启不是需求")⇒ 于是"只有某个区能常驻"。
## ⚖️ 边界(⛔ 不许越)
· **本函数是"登记"的唯一实现** —— ⛔ 别在别处再写一份(那是漂移的源头);
· ⛔ **只登记本区**(任务名带区名,各区互不干扰);
· ⛔ **不代用户开目标** —— 没目标就没有常驻,那是正常态;
· ✅ **幂等**:重复跑=覆盖同一条任务,⛔ 不产生第二条。
## 返回
人类可读的一行结论(调用方直接打印)。⛔ 不抛异常(登记失败要能继续跑完别的步骤)。
"""
_task = "collabd-keepalive-%s" % (ws.name or "ws")
# 🔴🔴 2026-10-06 收口(P0-74):动作=**`pythonw.exe` + `supervise-launch.py`**。
# 旧形态是 `powershell.exe -File start-supervise.ps1` —— PowerShell 是**控制台程序**
# ⇒ 每次触发分配 `conhost.exe` ⇒ **闪一下黑窗**(用户 2026-10-05:「又弹了窗口」)。
# ⚠️ 旧注释的理由「keeper 里要 set 那几个 env ⇒ 直接跑解释器没有设环境变量这一步」
# **已被 `supervise-launch.py` 推翻** —— 那个启动器存在的**全部目的**就是在进程内设 env。
# ⛔ **登记动作仍走 PowerShell**(那是在**登记**,不是在**当看守**,两者别混)。
_pyw = Path(python_exe).with_name("pythonw.exe")
if not _pyw.is_file(): # ⛔ 没同版本 pythonw ⇒ 找全局那份
_cands = sorted(Path(python_exe).parent.parent.parent.glob("*/pythonw.exe"))
_pyw = _cands[-1] if _cands else Path(python_exe)
_launcher = ws / ".workbuddy" / "collab" / "supervise-launch.py"
# 🔴 `-WorkingDirectory` **必须是「工作区根」**(⛔ 不是脚本目录):
# `collabd.load_cfg()` 按 `<cwd>/.workbuddy/collab/collabd.config.json` 找配置;
# cwd 设成脚本目录 ⇒ 它去找 `<ws>/.workbuddy/collab/.workbuddy/collab/…` ⇒ **找不到**
# ⇒ `CFG_MISSING` ⇒ `--supervise` 拒跑(实测 rc=2;任务侧 `LastTaskResult=1`)。
_wd = str(ws).replace("/", "\\")
_ps = (
"$ErrorActionPreference='Stop';"
"$n='%s';"
# 🔴 幂等:先注销同名(`-Confirm:$false` 免交互;宿主可能 -NonInteractive)
"if(Get-ScheduledTask -TaskName $n -ErrorAction SilentlyContinue){"
"Unregister-ScheduledTask -TaskName $n -Confirm:$false};"
# 🔴 动作=`pythonw.exe` + 启动器(GUI 子系统 ⇒ 零 conhost ⇒ **不闪窗**)
"$a=New-ScheduledTaskAction -Execute '%s' -Argument '\"%s\"' -WorkingDirectory '%s';"
# 🔴 `RepetitionInterval` 5 分钟(与 `collabctl.py` 的既定口径一致)
"$t=New-ScheduledTaskTrigger -Once -At (Get-Date) "
"-RepetitionInterval (New-TimeSpan -Minutes 5);"
# 🔴 四个设置缺一不可(逐条理由见 references/supervise-persistence.md §二②):
# ExecutionTimeLimit=0 ⇒ 默认 72h 会把常驻杀掉;
# ⚠️ ⛔ 不许设 2 分钟(那会被调度器到点掐死 ⇒ 每 2 分钟重建一次 = 抖动)
# MultipleInstances=IgnoreNew ⇒ 防双写台账
# -Hidden ⇒ 计划任务库里不显眼
"$s=New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries "
"-DontStopIfGoingOnBatteries -StartWhenAvailable "
"-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -Hidden;"
"$pr=New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest;"
"Register-ScheduledTask -TaskName $n -Action $a -Trigger $t -Settings $s -Principal $pr -Force|Out-Null;"
# 🔴 立刻起一次(⛔ 别等下次触发 —— 否则"配好了"这个结论当场不可验)
"Start-ScheduledTask -TaskName $n;"
"'OK ' + $n"
) % (_task, str(_pyw), str(_launcher), _wd)
try:
_r = subprocess.run(["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-Command", _ps],
capture_output=True, timeout=180,
# 🔴 `CREATE_NO_WINDOW`(⛔ 缺了每次登记都闪一下)
creationflags=0x08000000)
_o = _r.stdout.decode("utf-8", "replace").strip()
_e = _r.stderr.decode("utf-8", "replace").strip()
if "OK " in _o:
return ("✅ 已登记计划任务 `%s`(pythonw + supervise-launch.py · 每 5 分钟 · "
"ExecutionTimeLimit=0 · IgnoreNew)并已启动" % _task)
# ⛔ 失败要**报原文**,⛔ 不许编原因
_msg = (_e or _o or "无回显").splitlines()
return ("⛔ 登记失败:%s\n ⚠️ 若提示权限/策略 ⇒ 走 PowerShell 工具手动建"
"(`Register-ScheduledTask`;⛔ 别用 `schtasks.exe` —— 本机黑名单拦它)"
% (_msg[0][:160] if _msg else "无"))
except Exception as _ex:
return "⛔ 登记失败(异常):%r" % (_ex,)
def _keeper_task_state(ws: Path) -> str:
"""读本区计划任务状态(⛔ 只读;用来做"到底建成了没"的行为级验收)。"""
_task = "collabd-keepalive-%s" % (ws.name or "ws")
_ps = ("$n='%s';"
"$t=Get-ScheduledTask -TaskName $n -ErrorAction SilentlyContinue;"
"if(-not $t){'MISSING'}else{"
"$i=Get-ScheduledTaskInfo -TaskName $n;"
"'State=' + $t.State + ' Last=' + $i.LastTaskResult}"
) % _task
try:
_r = subprocess.run(["powershell.exe", "-NoProfile", "-Command", _ps],
capture_output=True, timeout=60)
return _r.stdout.decode("utf-8", "replace").strip() or "(无回显)"
except Exception as _ex:
return "(读不到:%r)" % (_ex,)
def _ps_check(ps1: Path) -> str:
"""🔴🔴 验收一份 `.ps1` **确实是合法 PowerShell**(返回''=没问题,否则返回提示串)。
病根(2026-10-04 事故,P0-56):模板首行被误打成「三连双引号 + #」开头,
生成物被 PowerShell 判 9 处语法错、**一秒内秒退** ⇒ 常驻零续命。
⚠️ 而当时自检只查 ①UTF-8 BOM ②模板变量是否填完 —— **两个表层特征全绿** ⇒ 静默通过。
⇒ 判据必须**行为级**:真调 PowerShell 自己的解析器(`Parser::ParseFile`),
⛔ 不查关键字、⛔ 不用正则猜。拿不到解析器时**返回"没验"**(⛔ 不当通过)。
"""
_exe = None
for _c in ("powershell.exe", "pwsh.exe"):
try:
subprocess.run([_c, "-NoProfile", "-Command", "exit 0"],
capture_output=True, timeout=20)
_exe = _c
break
except Exception:
continue
if not _exe:
return " ⚠️ 没法验语法(本机没有 PowerShell)"
_cmd = (
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
"$e=$null;$t=$null;"
"[System.Management.Automation.Language.Parser]::ParseFile('%s',[ref]$t,[ref]$e)|Out-Null;"
"if($e -and $e.Count -gt 0){'ERR '+$e.Count+' @L'+$e[0].Extent.StartLineNumber+': '+$e[0].Message}"
"else{'OK'}"
) % str(ps1).replace("'", "''")
try:
_r = subprocess.run([_exe, "-NoProfile", "-Command", _cmd],
capture_output=True, timeout=60)
_o = _r.stdout.decode("utf-8", "replace").strip()
if _o.startswith("OK"):
return ""
if _o.startswith("ERR"):
# ⚠️ PowerShell 的报错文案**含换行与中文** ⇒ 只取**第一行**、截断到 60 字,
# ⛔ 否则整个自检输出被它冲垮(实测回显里混着源码行)。
_msg = _o[4:].splitlines()[0].strip() if _o[4:].strip() else ""
if len(_msg) > 60:
_msg = _msg[:60] + "…"
return " ⛔ **语法不过**(%s)⇒ 计划任务会秒退 ⇒ 常驻没人续命" % _msg
return " ⚠️ 语法没验成(回显异常:%s)" % ((_o.splitlines() or [""])[0][:60] or "(空)")
except Exception as _e:
return " ⚠️ 语法没验成(%r)" % (_e,)
def main() -> int:
@@ -191,11 +46,9 @@ def main() -> int:
ap.add_argument("--topics", default="会话协作自检,机制排查与修复")
ap.add_argument("--port", type=int, default=0)
ap.add_argument("--python", default=sys.executable)
ap.add_argument("--no-task", action="store_true",
help="只铺 start-supervise.ps1,⛔ 不登记计划任务(要手动登记时用)")
# 🔴 2026-10-04 口径订正:**默认不铺载体**(常态用不着;见 SKILL.md R 红线口径段)
ap.add_argument("--with-keeper", action="store_true",
help="铺 start-supervise.ps1(目标做完还要让程序继续跑时才用)")
# ⛔ 2026-10-06 删掉 `--no-task` / `--with-keeper` 两个开关:
# 它们的存在前提是"本脚本会铺 ps1 并登记计划任务",而那条形态**已整套废弃**(P0-74)。
# ⇒ 常驻的开/关/自愈统一走 `collabctl.py on|off|ensure`,⛔ 本脚本不再碰任务。
a = ap.parse_args()
ws = Path(a.workspace).resolve()
@@ -454,73 +307,27 @@ def main() -> int:
print(" 要并看本区 ⇒ 把 `%s` 加进**主工作区**配置的 `peer_workspaces`(只读),重启主看板。" % ws_s)
if _fallback:
print(" ⚠️ 上面是**技能目录那份**(副本还没分发)⇒ 先跑 deploy_code.py --ws %s" % ws_s)
# ══ ⑥ 🔴🔴 **铺常驻载体**(2026-10-04:用户「任何工作区都要能自己配好」)
# 病根(实测):全机 262 个计划任务里**只有 `collabd-supervise-ws3` 一个**是本机制的
# ⇒ 5 个部署区**只有测试3 有载体** ⇒ 其余区的常驻**没人续命**(实测停摆 15~20 小时)。
# 根因再深一层:那份 `start-supervise.ps1` 里**工作区路径硬编码了 11 处**
# ⇒ 别的区**没法直接用**,于是就都没建。
# ✅ 做法=**模板 + 按区填变量**:模板在 `assets/start-supervise.ps1.tpl`。
_tpl = SK.parent / "assets" / "start-supervise.ps1.tpl" # ⛔ `SK` 指向 **scripts/**,包根要上一层
_keeper = ws / ".workbuddy" / "collab" / "start-supervise.ps1"
# ══ ⑥ 🔴🔴 **常驻载体**(2026-10-04:用户「任何工作区都要能自己配好」)
# 🔴🔴 2026-10-06 收敛(依据:pitfalls §P0-74 的定案三条 + SKILL.md「两条起法必须收敛」):
# ⛔ **不再铺 `start-supervise.ps1`**、⛔ **不再做 ps1 语法校验**、
# ⛔ **不再依赖 `assets/start-supervise.ps1.tpl`**。
# 旧形态=铺 ps1 + 任务动作 `powershell.exe -File start-supervise.ps1`
# ⇒ PowerShell 是**控制台程序** ⇒ 每次触发分配 `conhost.exe` ⇒ **闪黑窗**
# (用户 2026-10-05「又弹了窗口」;实测抓到 `powershell.exe` + `conhost.exe` 这对进程)。
# 新形态=**计划任务 → `pythonw.exe` → `supervise-launch.py`**(GUI 子系统 ⇒ 零控制台)。
# ⚠️ env(`CODEBUDDY_CONFIG_DIR` / `COLLABD_CONFIG`)由**启动器在进程内设** ——
# 这正是 `supervise-launch.py` 存在的全部理由(旧注释"直接跑解释器没法设 env"已被它推翻)。
# ⛔ **建任务这件事只许有一处实现** = `collabctl.py`(常驻唯一管理入口,受 `supervise.switch`
# 总电闸管辖)。本脚本**不建任务** —— 自己建就绕过了开关,是"第二条起法"。
_cdir = ws / ".workbuddy" / "collab"
_logs = _cdir / "logs"
_tmp = ws / "tmp"
for _d in (_logs, _tmp):
_d.mkdir(parents=True, exist_ok=True)
# 🔴🔴 2026-10-04 **口径订正**(用户原话:「从来没说过什么开机自启,
# 只有调用技能完成目标时启动 后台任务和检查程序」):
# **常态不需要载体脚本,也不需要计划任务。**
# ⛔ 本步改成**默认不铺**,只有显式 `--with-keeper` 才铺
# (⛔ 原来无条件铺 = 把"我以为你要长期在线"当需求做掉了,
# 还在输出里说「还差一步:登记计划任务」⇒ 凭空造出一个欠项)。
if not a.with_keeper:
print("⑥ 常驻载体:**跳过**(⛔ 默认不铺)")
print(" 📌 常态=调用技能完成目标时起后台任务 + 检查程序,⛔ 用不着载体。")
print(" 📌 若确实要「目标做完程序还继续跑」⇒ 加 `--with-keeper` 重跑本脚本,")
print(" 它会铺 `start-supervise.ps1` 并**打印**计划任务参数(⛔ 仍不代建)。")
elif _tpl.is_file():
# pythonw 必须**同版本**(⛔ 用 sys.executable 推不出同目录 pythonw)
_pyw_exe = Path(a.python)
_pyw = _pyw_exe.with_name("pythonw.exe")
if not _pyw.exists(): # ⛔ 没同版本 pythonw ⇒ 找全局那份
_cands = sorted(_pyw_exe.parent.parent.parent.glob("*/pythonw.exe"))
_pyw = _cands[-1] if _cands else _pyw_exe
_ws_native = str(ws) # ps1 里要**原生反斜杠**路径
_vals = {
"__WS__": _ws_native,
"__CFGDIR__": os.environ.get("CODEBUDDY_CONFIG_DIR") or str(Path.home() / ".workbuddy"),
"__COLLABDCFG__": str(cfgp),
"__PYW__": str(_pyw),
"__SCRIPT__": str(_own if _own.is_file() else (SK / "collabd.py")),
"__HBPATH__": str(_logs / "supervise-heartbeat.json"),
"__LOG__": str(_tmp / "keeper.log"),
"__STAMP__": str(_tmp / "keeper-last-restart.txt"),
"__STOPFLAG__": str(ws / "tmp" / "supervise-inbox" / "guard.stop"),
"__OUTLOG__": str(_logs / "supervise.out.log"),
"__ERRLOG__": str(_logs / "supervise.err.log"),
}
_t = io.open(_tpl, encoding="utf-8").read()
for _k, _v in _vals.items():
_t = _t.replace(_k, _v)
# 🔴 必须 **UTF-8 带 BOM**(⛔ 不带 ⇒ PowerShell 5.1 按 GBK 解码 ⇒ CJK 路径被毁
# ⇒ 任务一秒内 Result=1 退出 —— 这是实测踩过的)
io.open(_keeper, "w", encoding="utf-8-sig", newline="\r\n").write(_t)
print("⑥ 常驻载体已铺:%s" % _keeper.relative_to(ws))
# 🔴🔴 2026-10-04 用户口径:「我需要的是**各工作区会话 完成目标时 自己能建立 常驻程序**」
# ⇒ **登记这一步由本脚本自己做掉**(⛔ 不再只是打印参数让人去手工建 ——
# 实测结果就是"谁也没建",全机只剩 1 条指向已删区的任务)。
# ⚠️ 幂等:重复跑=覆盖同一条任务名(`_register_keeper_task` 内先 Unregister)。
# ⚠️ 只登记**本区**;⛔ 不动别的区、⛔ 不动用户的其它任务。
if a.no_task:
print(" ⏭ `--no-task` ⇒ **跳过登记计划任务**(只铺了脚本)")
else:
print(" " + _register_keeper_task(ws, _keeper))
print(" 🔎 复核:%s" % _keeper_task_state(ws))
_launcher = _cdir / "supervise-launch.py"
if _launcher.is_file():
print("⑥ 常驻载体:✅ 启动器在位(`%s`)" % _launcher.relative_to(ws))
else:
print("⑥ ⚠️ 找不到载体模板 `assets/start-supervise.ps1.tpl` ⇒ **未铺**"
"(⚠️ 只有传了 `--with-keeper` 才需要它)")
print(" 🔴 **登记范围**:⛔ 本脚本只登记**本工作区**那一条任务(任务名带区名)——")
print(" ⛔ 不碰别的区、⛔ 不动机器上其它任务;重复跑=覆盖同一条(幂等)。")
print("⑥ 常驻载体:⛔ 缺启动器 `supervise-launch.py`")
print(" 📌 先跑 `deploy_code.py --ws %s` 分发副本。" % ws_s)
print(" 📌 常驻的开/关/自愈**一律走唯一入口**:`collabctl.py on|off|ensure`")
print(" (⛔ 本脚本不建计划任务 —— 那会绕过 `supervise.switch` 总电闸,是「第二条起法」)")
# ⑤ 自检收口放在⑥ 之后(⛔ 顺序:先铺载体再体检,否则体检会漏掉"载体有没有")
print()
@@ -544,32 +351,19 @@ def main() -> int:
_o2 = _r2.stdout.decode("utf-8", "replace")
_ws_ok = ("COLLABD_CONFIG" in _o2) or (ws_s in _o2)
print(" `collabd --where` 认这个工作区 = %s(rc=%s)" % ("✅" if _ws_ok else "⛔", _r2.returncode))
# 🔴🔴 2026-10-04 补:**载体**(实测全机只有 1 个区有 ⇒ 其余区常驻没人续命)
_kb = ws / ".workbuddy" / "collab" / "start-supervise.ps1"
if _kb.is_file():
_raw = _kb.read_bytes()
_bom = _raw[:3] == b"\xef\xbb\xbf"
_no_var = "__" in _raw.decode("utf-8-sig", "ignore")
# 🔴🔴 2026-10-04(P0-56):**必须验「这是不是合法 PowerShell」**
# 病根:原自检只查 ①BOM ②模板变量填完没 —— 两个都是**表层特征**。
# 实测事故:模板首行被我误打成「三连双引号 + #」⇒ 生成的 `.ps1`
# 被 PowerShell 判 **9 处语法错**、一秒内秒退 ⇒ 常驻 0 续命。
# ⚠️ 而 BOM 在、变量填完了、文件也在 ⇒ **原自检全绿** ⇒ 静默通过。
# ⇒ 判据必须**行为级**:真拿 PowerShell 的解析器解析一遍,⛔ 不查关键字。
_syn = _ps_check(_kb)
print(" 载体脚本 = %s%s%s%s"
% ("✅ 在位", "" if _bom else "(⛔ **缺 UTF-8 BOM** ⇒ PowerShell 5.1 会按 GBK 解码"
"⇒ CJK 路径被毁 ⇒ 任务秒退)",
" ⛔ 模板变量没填完" if _no_var else "",
_syn))
# 🔴🔴 2026-10-06 收敛(P0-74):载体从 `start-supervise.ps1` 换成 **`supervise-launch.py`**。
# ⇒ 自检一并改:**不再验 ps1、不再验 UTF-8 BOM、不再验模板变量填完没** ——
# 那三个判据的**被测对象已经不存在了**(留着就是过期的探照灯)。
# 判据换成**行为级可核的一条**:启动器在位。
# ⚠️ 任务本身**不在这里验** —— 建任务归 `collabctl.py`(唯一实现),
# 本脚本自己建/自己验就绕过了 `supervise.switch` 总电闸。
_lc = ws / ".workbuddy" / "collab" / "supervise-launch.py"
if _lc.is_file():
print(" 常驻启动器 = ✅ 在位(`supervise-launch.py`)")
else:
# 🔴 2026-10-04 口径订正:⛔ **不许**把「没有载体」说成缺陷。
# 常态=调用技能完成目标时起后台任务 + 检查程序,⛔ 用不着载体;
# 「目标做完程序还继续跑」是**可选需求** ⇒ 没铺只是「没选它」,⛔ 不是「坏了」。
# ⛔ 旧文案「常驻跨会话必被带走」是**编造的后果**(实测常驻是孤儿进程,
# 独立存活 24.7 h)⇒ 本轮订正。
print(" 载体脚本 = ⛔ 没铺(📌 **这不是问题**:常态用不着它;"
"确实要「目标做完程序还继续跑」⇒ 加 `--with-keeper` 重跑)")
print(" 常驻启动器 = ⛔ **缺**(先跑 `deploy_code.py --ws %s` 分发副本)" % ws_s)
print(" 📌 常驻开/关/自愈走唯一入口:`collabctl.py on|off|ensure`"
"(本区任务名 `collabd-keepalive-%s`)" % (ws.name or "ws"))
if _dp.get("deployed") and _ws_ok and not (_dp.get("missing") or []) :
print(" ✅ **本工作区可以直接用**(配置/目标/任务图齐了,程序也认)")
else: