Files
workbuddy_skills/session-mechanism/scripts/init_workspace.py
T
admin 19101acd65 init: workbuddy_skills 重建,仅收录 session-mechanism
- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件)
- 附 .gitignore(产物 + 本机凭据)
- 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库)
- 本提交为孤儿提交(父提交为空),历史自此重新开始
2026-10-05 14:13:24 +08:00

493 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
"""🔴 **新建协作工作区**(技能内脚本,用户 2026-10-03 要求"每次使用任务会话机制都可以有对应的目标文件夹")。
用法:
python init_workspace.py <工作区绝对路径> [--title 目标标题] [--short 目标简称]
[--port 单例端口] [--no-main-session]
做四件事(⛔ 顺序固定,缺一件机制就跑不起来):
① 建工作区骨架(`.workbuddy/collab/`、`tmp/supervise-inbox/`、`交付物/`)
② 写**部署配置** `collabd.config.json`(`COLLABD_CONFIG` 指向它 ⇒ `collabd.py` 才认这个工作区)
③ 写**目标** `goal.json`(title/short/topics/lifecycle=进行中/taskgraph)
③b 写**最小任务图**(⛔ 漏了它 ⇒ 常驻每轮报「读任务图失败」,且目标判据缺一条腿)
④ 跑 `collabd.py --ensure-goal-dir` ⇒ **建目标文件夹 + 落《目标执行状态.md》骨架**(幂等)
⑤ 建一条**主会话排期**(`automation_update` 由调用方做 ⇒ 本脚本只**打印**它该用的参数,
⛔ 不自己写宿主库 —— 创建新会话**只能走自动化入口**,这是宿主规矩)
🔴 关键约束(都是踩过的):
· `workspace` 必须**正斜杠**(⛔ 反斜杠会让 `cwds` 裂组)
· `singleton_port` **每个工作区一个**(⛔ 复用 ⇒ 常驻单例互抢、静默只有一个活着)
· `board_ext` 指 `.RETIRED-*` ⇒ 新工作区**不带**使用方扩展(技能侧零项目串)
· 排期名必须是两级前缀 `[主]-<类别>-<具体>`(⛔ 光杆名 ⇒ 角色判不出,看板认不出)
"""
import argparse
import io
import json
import os
import subprocess
import sys
from pathlib import Path
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
SK = Path(__file__).resolve().parent
DEFAULT_TITLE = "检查会话协作是否运行正常 + 协作机制问题排查与修复"
def _register_keeper_task(ws: Path, keeper: Path) -> str:
"""🔴🔴 给本工作区**登记一条计划任务**(="各工作区都能自己常驻"的落点)。
## 为什么必须有这一步(2026-10-04 用户原话)
> 「我需要的是**各工作区会话 完成目标时 自己能建立 常驻程序**」
病根链(逐层实测):
① `ensure_supervise()` 能起常驻,但起法(`DETACHED_PROCESS`)**不脱离会话作业对象**
⇒ 会话一收工就被回收(vibe 会话查出,我已复验);
② 唯一合格载体=**计划任务**(父链根在 `svchost -s Schedule`,与会话树无关);
③ 而 `init_workspace.py` 原来**只铺脚本、⛔ 不登记任务**(当时口径:"
开机自启不是需求")⇒ 于是"只有某个区能常驻"。
## ⚖️ 边界(⛔ 不许越)
· **本函数是"登记"的唯一实现** —— ⛔ 别在别处再写一份(那是漂移的源头);
· ⛔ **只登记本区**(任务名带区名,各区互不干扰);
· ⛔ **不代用户开目标** —— 没目标就没有常驻,那是正常态;
· ✅ **幂等**:重复跑=覆盖同一条任务,⛔ 不产生第二条。
## 返回
人类可读的一行结论(调用方直接打印)。⛔ 不抛异常(登记失败要能继续跑完别的步骤)。
"""
_task = "collabd-supervise-%s" % (ws.name or "ws")
_ps = (
"$ErrorActionPreference='Stop';"
"$n='%s';"
# 🔴 幂等:先注销同名(`-Confirm:$false` 免交互;宿主可能 -NonInteractive)
"if(Get-ScheduledTask -TaskName $n -ErrorAction SilentlyContinue){"
"Unregister-ScheduledTask -TaskName $n -Confirm:$false};"
# 🔴 动作=**powershell.exe -File <本区 keeper>**(⛔ 不是 pythonw):
# keeper 里要 set 那几个 env(CODEBUDDY_CONFIG_DIR / COLLABD_CONFIG)⇒
# 直接跑解释器没有"设环境变量"这一步。
"$a=New-ScheduledTaskAction -Execute 'powershell.exe' "
"-Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"%s\"' "
"-WorkingDirectory '%s';"
# 🔴 `AtLogOn` = 开机/登录即起(这一步让"跨会话"成立:⛔ 不依赖任何 WorkBuddy 会话)
"$t=New-ScheduledTaskTrigger -AtLogOn;"
# 🔴 四个设置缺一不可(少一个就前功尽弃,逐条理由见 references/supervise-persistence.md §二②):
# ExecutionTimeLimit=0 ⇒ 默认 72h 会把常驻杀掉
# MultipleInstances=IgnoreNew ⇒ 防双写台账
# RestartCount/Interval ⇒ 脚本非零退出时重拉(⚠️ 真正的看护在 keeper 的 while 里)
"$s=New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries "
"-DontStopIfGoingOnBatteries -StartWhenAvailable "
"-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew "
"-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1);"
"Register-ScheduledTask -TaskName $n -Action $a -Trigger $t -Settings $s -Force|Out-Null;"
# 🔴 立刻起一次(⛔ 别等下次登录 —— 否则"配好了"这个结论当场不可验)
"Start-ScheduledTask -TaskName $n;"
"'OK ' + $n"
) % (_task, str(keeper), str(ws))
try:
_r = subprocess.run(["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-Command", _ps],
capture_output=True, timeout=180)
_o = _r.stdout.decode("utf-8", "replace").strip()
_e = _r.stderr.decode("utf-8", "replace").strip()
if "OK " in _o:
return "✅ 已登记计划任务 `%s`(AtLogOn · ExecutionTimeLimit=0 · IgnoreNew · Restart×999)并已启动" % _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-supervise-%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:
ap = argparse.ArgumentParser()
ap.add_argument("workspace")
ap.add_argument("--title", default=DEFAULT_TITLE)
ap.add_argument("--short", default="")
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(目标做完还要让程序继续跑时才用)")
a = ap.parse_args()
ws = Path(a.workspace).resolve()
ws_s = str(ws).replace("\\", "/") # 🔴 正斜杠(红线)
short = a.short or ws.name
topics = [t.strip() for t in a.topics.split(",") if t.strip()]
port = a.port or (20000 + (abs(hash(ws_s)) % 5000)) # ⛔ 每区唯一
# 🔴🔴 端口**跨进程稳定**(2026-10-04 修):原来用 `hash(ws_s)`,而
# **Python 3.11+ 字符串 hash 每进程随机化**(PYTHONHASHSEED)⇒
# **同一工作区每跑一次就换一个端口** ⇒ 旧守卫/防火墙/别的配置里的端口全部对不上。
# ⇒ 改成 `zlib.crc32`(确定性、与版本无关);已有配置里写了端口就沿用它。
import zlib
port = port or (20000 + (zlib.crc32(ws_s.encode("utf-8")) % 5000))
print("=== 新建协作工作区:%s ===" % ws_s)
# ① 骨架
for d in (".workbuddy/collab", "tmp/supervise-inbox", "交付物", ".workbuddy/collab/logs"):
(ws / d).mkdir(parents=True, exist_ok=True)
print("① 骨架已建(.workbuddy/collab / tmp/supervise-inbox / 交付物 / logs)")
cfgp = ws / ".workbuddy/collab/collabd.config.json"
gp = ws / "tmp/supervise-inbox/goal.json"
# ══════════════════════════════════════════════════════════════════
# 🔴🔴 2026-10-04 改为**幂等自配置**(用户:「我要求在任何工作区使用都能
# **自己检查环境配置好环境**」)。原来本脚本**无条件重写** `collabd.config.json`
# 与 `goal.json` ⇒ 对**已部署且有真实内容**的工作区是**破坏性**的
# (实测 `vibe-product` 的 `targets` 里登记着 5 个真实目标 ⇒ 会被冲掉)。
# ⇒ 现在:**先读现状 → 只补缺的 → 已有的一律不动**。
_old_cfg = {}
if cfgp.exists():
try:
_old_cfg = json.loads(cfgp.read_text(encoding="utf-8"))
except Exception:
_old_cfg = {}
if _old_cfg.get("singleton_port"):
port = int(_old_cfg["singleton_port"]) # 沿用既有端口(⛔ 换了会让别的引用失效)
_old_goal = {}
if gp.exists():
try:
_old_goal = json.loads(gp.read_text(encoding="utf-8"))
except Exception:
_old_goal = {}
_reuse = bool(_old_cfg) or bool(_old_goal)
if _reuse:
print(" ♻️ **检测到既有部署**(配置 %s / 目标 %s)⇒ **只补缺,不覆盖已有**"
% ("有" if _old_cfg else "⛔无", "有" if _old_goal else "⛔无"))
# ② 部署配置(**补缺式**:以旧配置为底,只加现行键;⛔ 不动用户改过的值)
cfg = {
"_说明": "本文件=**本工作区**的协作机制部署配置(⛔ 不属于技能)。由 `init_workspace.py` 生成;"
"`COLLABD_CONFIG` 必须指向它,否则 `collabd.py` 不认这个工作区。",
"workspace": ws_s,
"inbox": "tmp/supervise-inbox",
"live": "交付物/本机协作-实时状态.md",
"taskgraph": "交付物/任务图-会话协作自检.json",
"targets": {},
"singleton_port": port,
"interval": 10, "idle_min": 12, "stuck_min": 30, "vacuum_min": 5, "health_every": 300,
"supervise_interval": 10,
"wake_enable": False, # 🔴 唤醒/投递整套已退役
"board_ext": ".workbuddy/collab/board_ext.RETIRED.py", # ⛔ 不带使用方扩展
"log": ".workbuddy/collab/logs/_collabd.log",
}
if _old_cfg:
_merged = dict(cfg)
_merged.update(_old_cfg) # ⛔ 旧的**优先**(含 targets / 自定义路径)
cfg = _merged
_added = [k for k in cfg if k not in _old_cfg]
cfg["workspace"] = ws_s # 这两项必须以本次为准(工作区可能搬过家)
cfgp.write_text(json.dumps(cfg, ensure_ascii=False, indent=1), encoding="utf-8", newline="")
print("② 部署配置:%s%s(端口 %d%s)"
% (cfgp.relative_to(ws), (" 补了 %s" % _added) if _added else " 已最新",
port, " 沿用既有" if _old_cfg.get("singleton_port") else ""))
# ③ 目标(**补缺式**:已有 goal.json ⛔ 一个字都不改)
goal = {
"title": a.title, "short": short, "topics": topics,
"lifecycle": "进行中",
"lifecycle_at": "", "lifecycle_by": "", "lifecycle_why": "init_workspace.py 新建",
"taskgraph": "交付物/任务图-会话协作自检.json",
"taskgraph_set_at": "", "taskgraph_set_by": "",
"declared_at": "", "declared_by": "",
"acceptance_state": {},
"_唯一来源": "本文件 title/short/topics 是真源;`execution_doc` 指向目标文件夹里的《目标执行状态.md》。",
}
if _old_goal:
print("③ 目标文件:**已存在,⛔ 不覆盖**(title=%r)" % str(_old_goal.get("title"))[:40])
else:
gp.write_text(json.dumps(goal, ensure_ascii=False, indent=1), encoding="utf-8", newline="")
print("③ 目标已写:title=%r short=%r lifecycle=进行中" % (a.title, short))
# ③b 任务图(🔴 2026-10-03 14:35x 补):配置里 `taskgraph` 指向它,
# 但初始化时**没建这个文件** ⇒ 常驻每 10 秒报「goal_state 读任务图失败」
# ⇒ 更要命的是**判据第二路(任务图 nodes)永远取不到** ⇒ 目标状态判据缺一条腿。
# ⇒ 写一份最小任务图:节点全 `pending`,由任务会话逐个转 `done`。
# 🔴 2026-10-04 **同样改成补缺式**(⛔ 别覆盖既有任务图 —— 那是任务会话逐个转 done 的**进度真源**,
# 冲掉 = 全部工作量白费;实测 `vibe-product` 的 `taskgraph` 指向 `交付物/任务图.json`,
# 与本脚本的默认路径**不同名** ⇒ 更不能碰)。
tg_rel = (_old_cfg.get("taskgraph") or "交付物/任务图-会话协作自检.json")
tgp = ws / tg_rel
if tgp.exists():
try:
_n = len((json.loads(tgp.read_text(encoding="utf-8")) or {}).get("nodes") or [])
except Exception:
_n = "?"
print("③b 任务图:**已存在,⛔ 不覆盖**(%s,%s 个节点)" % (tg_rel, _n))
else:
# 🔴🔴 2026-10-04 修:旧配置**可能没有 `taskgraph` 键**(实测 `vibe-product` 就没有)
# ⇒ 落回默认名"任务图-会话协作自检.json",而用户真实的图叫**别的名字**(如 `任务图.json`)
# ⇒ **新建了一份空图,真实的图被"忽略"** ⇒ 判据第二路(任务图 nodes)仍取不到,
# 而用户以为"初始化过了"。⇒ 先在 `交付物/` 里找一张**看起来是任务图的 json** 再决定。
_cand = []
try:
for f in sorted((ws / "交付物").glob("*.json")):
try:
j = json.loads(f.read_text(encoding="utf-8"))
except Exception:
continue
if isinstance(j, dict) and isinstance(j.get("nodes"), list):
_cand.append(f)
except Exception:
pass
if _cand:
# ⚠️ 有多张 ⇒ **不猜**,只报告并**不新建**(⛔ 猜错等于改用户的目标进度)
if len(_cand) == 1:
print("③b 任务图:配置里没登记 `taskgraph`,但找到既有任务图 **%s** ⇒ ⛔ 不新建、不改它"
% _cand[0].name)
tg_rel = None # 🔴🔴 关键:**置 None 才不会落到下面的写入分支**
else:
print("③b ⚠️ 配置里没登记 `taskgraph`,且 `交付物/` 里有 **%d 张**像任务图的 json(%s)"
" ⇒ ⛔ 不猜、**不新建**。请在配置里登记 `taskgraph` 指明哪一张。"
% (len(_cand), "、".join(f.name for f in _cand[:3])))
tg_rel = None
if tg_rel:
tg = {"title": a.title, "short": short, "topics": topics,
"nodes": [{"id": "N1", "title": "核对目标并完成首轮协作", "status": "pending",
"line": topics[0] if topics else "", "note": "init_workspace.py 生成的最小任务图"}]}
tgp.parent.mkdir(parents=True, exist_ok=True)
tgp.write_text(json.dumps(tg, ensure_ascii=False, indent=1), encoding="utf-8", newline="")
print("③b 任务图已写:%s(1 个节点,pending)" % tg_rel)
# ④ 目标文件夹(走产品入口,⛔ 不自己 mkdir 文档)
# 🔴🔴 2026-10-03 **改用「本工作区的副本」,⛔ 不再用技能目录那份**:
# 定案口径=**各工作区的程序各自独立**(技能目录=源,`.workbuddy/collab/`=各区副本,
# `deploy_code.py` 分发)⇒ 提示用户去跑技能目录那份,等于**又拉起第二个常驻**,
# 它会写**技能目录的 tmp/**(⛔ 那不是任何工作区的 inbox)⇒ 状态写到别处,
# 看板读不到、故障排查也被带偏(本轮 `SESSION_END`-style 误判的同族)。
# ⚠️ 本副本若还不存在(首次初始化、还没分发)⇒ 回落用技能目录那份,
# 但**打印时必须说清"这次是回落"**(⛔ 静默回落=用户以为跑的是副本)。
_own = ws / ".workbuddy" / "collab" / "collabd.py"
_own_ba = ws / ".workbuddy" / "collab" / "board.py"
_fallback = not _own.is_file()
_cd = SK / "collabd.py" if _fallback else _own
if _fallback:
print(" ⚠️ 本工作区还没有 collabd.py 副本 ⇒ 本次**回落用技能目录那份**;"
"请随后跑 deploy_code.py 分发,否则各区程序不独立。")
env = dict(os.environ)
env["COLLABD_CONFIG"] = str(cfgp).replace("\\", "/")
env["COLLABD_NO_ENSURE"] = "1"
r = subprocess.run([a.python, str(_cd), "--ensure-goal-dir"],
env=env, cwd=str(ws), capture_output=True, timeout=120)
out = r.stdout.decode("utf-8", "replace").strip()
print("④ --ensure-goal-dir rc=%s" % r.returncode)
for ln in out.splitlines():
print(" " + ln)
gd = [ln for ln in out.splitlines() if ln.startswith("目标目录:")]
gdir = gd[0].split(":", 1)[1].strip() if gd else ""
# ⑤ 主会话排期参数(⛔ 本脚本不建 —— 创建新会话只能走自动化入口)
print()
print("⑤ 主会话排期(请由调用方用 automation_update 建):")
print(" name = [主]-%s-主会话" % short)
print(" cwds = %s" % ws_s)
print(" 提示:主会话标题必须是**两级前缀**(`[主]-<类别>-<具体>`),⛔ 光杆名看板认不出。")
print()
print("✅ 完成。后续协作(⛔ **都跑本工作区的副本**,别跑技能目录那份 —— "
"各区程序各自独立,跑了目录那份会写错地方):")
print(" export COLLABD_CONFIG=%s" % str(cfgp).replace("\\", "/"))
_cd_show = _cd
_bd_show = (SK / "board.py") if not (ws / ".workbuddy" / "collab" / "board.py").is_file() else _own_ba
print(" python %s --supervise" % _cd_show)
print(" python %s --serve 8789 --takeover" % _bd_show)
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"
_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))
else:
print("⑥ ⚠️ 找不到载体模板 `assets/start-supervise.ps1.tpl` ⇒ **未铺**"
"(⚠️ 只有传了 `--with-keeper` 才需要它)")
print(" 🔴 **登记范围**:⛔ 本脚本只登记**本工作区**那一条任务(任务名带区名)——")
print(" ⛔ 不碰别的区、⛔ 不动机器上其它任务;重复跑=覆盖同一条(幂等)。")
# ⑤ 自检收口放在⑥ 之后(⛔ 顺序:先铺载体再体检,否则体检会漏掉"载体有没有")
print()
print("⑤ 自检(问 `_env.env_check`,⛔ 不自己另算一套):")
try:
import importlib.util as _iu
_sp = _iu.spec_from_file_location("_env_chk", SK / "hooks" / "_env.py")
_e = _iu.module_from_spec(_sp)
_sp.loader.exec_module(_e)
_rep = _e.env_check(ws=ws_s, scope="workspace")
_dp = _rep.get("deploy") or {}
print(" 技能根 = %s" % _rep.get("skills_root"))
print(" 已部署 = %s" % _dp.get("deployed"))
for _m in (_dp.get("missing") or []):
print(" ⛔ %s" % _m)
for _m in (_dp.get("stale") or []):
print(" ⚠️ %s" % _m)
# 🔴 真的按程序判一次「能不能认这个工作区」——⛔ 不用"文件在不在"当通过
_r2 = subprocess.run([a.python, str(_cd), "--where"], env=env, cwd=str(ws),
capture_output=True, timeout=120)
_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))
else:
# 🔴 2026-10-04 口径订正:⛔ **不许**把「没有载体」说成缺陷。
# 常态=调用技能完成目标时起后台任务 + 检查程序,⛔ 用不着载体;
# 「目标做完程序还继续跑」是**可选需求** ⇒ 没铺只是「没选它」,⛔ 不是「坏了」。
# ⛔ 旧文案「常驻跨会话必被带走」是**编造的后果**(实测常驻是孤儿进程,
# 独立存活 24.7 h)⇒ 本轮订正。
print(" 载体脚本 = ⛔ 没铺(📌 **这不是问题**:常态用不着它;"
"确实要「目标做完程序还继续跑」⇒ 加 `--with-keeper` 重跑)")
if _dp.get("deployed") and _ws_ok and not (_dp.get("missing") or []) :
print(" ✅ **本工作区可以直接用**(配置/目标/任务图齐了,程序也认)")
else:
print(" ⛔ **还没到位**,按上面 ⛔/⚠️ 逐条补完再跑一次本脚本(幂等,可反复跑)")
except Exception as _ex:
print(" ⚠️ 自检跑不了:%r(⛔ 不当通过)" % (_ex,))
return 0
if __name__ == "__main__":
sys.exit(main())