init: workbuddy_skills 重建,仅收录 session-mechanism
- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件) - 附 .gitignore(产物 + 本机凭据) - 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库) - 本提交为孤儿提交(父提交为空),历史自此重新开始
This commit is contained in:
commit
19101acd65
58 files changed
+36134
No files matched your search
@@ -0,0 +1,356 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""**环境定位与环境自检**(2026-10-02 · 用户定案「技能使用时必须先检查环境配置」)。
|
||||
|
||||
一句话:把「技能库根 / 配置目录」从**各脚本各自推导**改成**一处判据+一处体检**。
|
||||
|
||||
## 为什么要有这个模块(2026-10-02 实测事故)
|
||||
|
||||
`reply-style-guard.py` 曾被注册成文档库 `07-scripts/reply-style-guard.py`(D 盘旧副本)。
|
||||
它沿 `__file__` 上溯 4 级找 `skills/` ⇒ 从 D 盘够不到 ⇒ 落进最后一行
|
||||
`os.path.expanduser('~/.workbuddy/skills')`。
|
||||
⚠️🔴 **Windows 上 `~` 不等于真配置目录**(本机真值在 E 盘 `CODEBUDDY_CONFIG_DIR`)⇒ 那一行是**错的**。
|
||||
|
||||
后果比报错更坏:该钩子的设计判据是「哪一级都读不到就**静默零输出**、不影响任何人」
|
||||
⇒ **「机制坏了」和「没配规则」表现完全一样**,日志里只有一行 `core=0 字符`。
|
||||
(真实代价:另一个工作区为这件事绕了两轮,在「规则文件在不在」上打转。)
|
||||
|
||||
## 三条硬规矩(本模块的判据)
|
||||
|
||||
1. 🔴 **顺序固定**:`DSH_SKILLS_ROOT` env → `CODEBUDDY_CONFIG_DIR` env(+ `roots.env` 兜底)
|
||||
→ 沿 `__file__` 上溯找含 `agent-operating-rules` 的那一层 → ⛔ **不再回落到 `~`**。
|
||||
2. 🔴 **找不到就报错,不静默**:写 **stderr** + 返回 `None`;调用方据此**非零退出**。
|
||||
⛔ 绝不「静默零输出」—— 那正是把两类事故压成一个表象的元凶。
|
||||
3. 🔴 **体检与环境标记分离**:`env_check()` 只**读**(体检);`write_env_stamp()` 才**写**标记。
|
||||
⇒ 体检永远不该有副作用(否则「查一下」就把标记刷新了,标记就永不报警)。
|
||||
|
||||
## 标记文件(用户 2026-10-02 要求:「在对应文件夹创建状态变量和时间作为标记」)
|
||||
|
||||
落在**配置作用域对应的文件夹**里,文件名 `env-stamp.json`:
|
||||
|
||||
- 全局作用域 ⇒ `<配置目录>/env-stamp.json`(本机 `E:/ProgramData/.workbuddy/env-stamp.json`)
|
||||
- 工作区作用域 ⇒ `<工作区>/.workbuddy/env-stamp.json`
|
||||
|
||||
字段:`scope`(global/workspace)|`checked_at`(ISO 到秒)|`ok`(bool)|
|
||||
`config_dir` / `skills_root`(**判据读数**,便于事后核)|`hooks`(逐条注册的存在性)|
|
||||
`fixed`(本轮修过的问题清单)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_STAMP_NAME = "env-stamp.json"
|
||||
_ROOTS_ENV = "roots.env" # 技能根的权威落点(install.py 生成)
|
||||
# 🔴🔴 2026-10-04 单包自包含改造:识别特征改成**多个候选**,⛔ 不再只认
|
||||
# `agent-operating-rules`(那是**另一个**技能 ⇒ 只装本包时会把真技能库判成"不存在")。
|
||||
# 🔴 旧写法的病:`skills_root()` / `env_check()` 三处都拿`(root)/_SNIFF).is_dir()` 当
|
||||
# "技能库在不在"的判据 ⇒ **单包环境下永远判否**(实测:隔离库只搬本包时全部降级)。
|
||||
# ✅ 正解:任一候选目录存在即认;⛔ 全都没有才算定位失败(硬规矩②「找不到就报错不静默」不变)。
|
||||
# 判据数组**第一个元素 = 本包自己** ⇒ 只拷本包也能定位。
|
||||
_SNIFFS = ("session-mechanism", "agent-operating-rules")
|
||||
|
||||
|
||||
def _has_skill(root) -> bool:
|
||||
"""技能库根的判据 =⛔ 至少存在一个识别特征目录(⛔ 只认目录真存在,不猜)。"""
|
||||
if not root:
|
||||
return False
|
||||
try:
|
||||
return any((Path(_norm(root)) / s).is_dir() for s in _SNIFFS)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ⛔ 保留旧名给外部引用(⛔ 外部脚本若 import 它做文案,仍能拿到第一个候选)
|
||||
_SNIFF = _SNIFFS[0]
|
||||
|
||||
|
||||
def _norm(p) -> str:
|
||||
s = str(p or "").strip()
|
||||
return os.path.normpath(s).replace("\\", "/") if s else ""
|
||||
|
||||
|
||||
def _load_roots_env() -> dict:
|
||||
"""读技能包里的 `roots.env`(⛔ 各脚本的 `_sm_load_roots()` 注入 env 前的原始来源)。"""
|
||||
out = {}
|
||||
d = Path(__file__).resolve().parent
|
||||
for _ in range(5):
|
||||
f = d / _ROOTS_ENV
|
||||
if f.is_file():
|
||||
try:
|
||||
for ln in f.read_text(encoding="utf-8").splitlines():
|
||||
ln = ln.strip()
|
||||
if ln and not ln.startswith("#") and "=" in ln:
|
||||
k, v = ln.split("=", 1)
|
||||
out[k.strip()] = v.strip()
|
||||
return out
|
||||
except Exception:
|
||||
return {}
|
||||
n = d.parent
|
||||
if n == d:
|
||||
break
|
||||
d = n
|
||||
return out
|
||||
|
||||
|
||||
def _norm_ws(p) -> str:
|
||||
"""工作区路径**归一化**(判归属专用)。
|
||||
|
||||
🔴 为什么不能直接比字符串:`sessions.cwd` / `automations.cwds` 里
|
||||
**同一区出现过反斜杠与正斜杠两种写法**(`E:\\ProgramData\\…` vs `E:/ProgramData/…`),
|
||||
末尾斜杠有时在有时不在 ⇒ **逐字比会把同一个区判成两个** ⇒ 归属误判、裂组。
|
||||
⇒ 统一:转正斜杠、去尾斜杠、转小写。
|
||||
"""
|
||||
s = str(p or "").strip().replace("\\", "/")
|
||||
while s.endswith("/") and len(s) > 3:
|
||||
s = s[:-1]
|
||||
return s.lower()
|
||||
|
||||
|
||||
def config_dir() -> str:
|
||||
"""**真配置目录** —— 顺序见硬规矩①的前三档。"""
|
||||
v = os.environ.get("CODEBUDDY_CONFIG_DIR")
|
||||
if v:
|
||||
return _norm(v)
|
||||
v = _load_roots_env().get("CODEBUDDY_CONFIG_DIR")
|
||||
if v:
|
||||
return _norm(v)
|
||||
#⛔ 最后一档仍不给 `~`:Windows 上它通常不是真配置目录(实测会指到 C:\Users\...)
|
||||
return _norm(Path.home() / ".workbuddy")
|
||||
|
||||
|
||||
def skills_root() -> str:
|
||||
"""**技能库根** —— 顺序见硬规矩①。⛔ 不再回落到 `~`(硬规矩①末档已删)。
|
||||
|
||||
🔴 2026-10-02 补:**每一档都要验"真的存在"**,⛔ 不信任 env 值本身。
|
||||
实测踩过:`DSH_SKILLS_ROOT` 指向一个不存在的目录时,旧写法直接返回它
|
||||
⇒ 下游 `os.path.isfile(核心块)` 失败 ⇒ **静默零输出**(同款静默失效)。
|
||||
⇒ env 给错 ⇒ 继续往下找;全都找不到 ⇒ 返回 `""`(=报错,⛔ 不猜)。
|
||||
"""
|
||||
v = os.environ.get("DSH_SKILLS_ROOT")
|
||||
if v and _has_skill(v):
|
||||
return _norm(v)
|
||||
d = Path(__file__).resolve().parent
|
||||
for _ in range(6): # 沿 __file__ 上溯(换机可用,⛔ 不写死盘符)
|
||||
cand = d.parent
|
||||
if _has_skill(cand):
|
||||
return _norm(cand)
|
||||
if d.parent == d:
|
||||
break
|
||||
d = d.parent
|
||||
r = _load_roots_env().get("CODEBUDDY_CONFIG_DIR")
|
||||
if r:
|
||||
cand = Path(_norm(r)) / "skills"
|
||||
if _has_skill(cand):
|
||||
return _norm(cand)
|
||||
return "" # 🔴 空串=找不到(⛔ 不是"猜一个")
|
||||
|
||||
|
||||
def _stamp_path(ws: str = "", scope: str = "global") -> str:
|
||||
if scope == "workspace" and ws:
|
||||
return _norm(Path(ws) / ".workbuddy" / _STAMP_NAME)
|
||||
return _norm(Path(config_dir()) / _STAMP_NAME)
|
||||
|
||||
|
||||
def registered_hooks() -> list:
|
||||
"""读全局 `settings.json` 里已注册的钩子 ⇒ `[(事件, 命令, timeout, 脚本路径)]`。"""
|
||||
sp = Path(config_dir()) / "settings.json"
|
||||
out = []
|
||||
try:
|
||||
d = json.loads(sp.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return out
|
||||
for ev, groups in (d.get("hooks") or {}).items():
|
||||
for g in (groups or []):
|
||||
for e in (g or {}).get("hooks", []):
|
||||
c = (e or {}).get("command", "") or ""
|
||||
# 取最后一个 .py 片段(命令形如`<python>"<脚本>" <参数>`)
|
||||
parts = [p for p in c.split('"') if p.strip().endswith(".py")]
|
||||
out.append((ev, c, (e or {}).get("timeout"),
|
||||
_norm(parts[-1]) if parts else ""))
|
||||
return out
|
||||
|
||||
|
||||
def env_check(ws: str = "", scope: str = "global") -> dict:
|
||||
"""🔍 **只读体检**:返回逐项读数,⛔ **绝不写标记**(硬规矩③)。
|
||||
|
||||
🔴🔴 2026-10-04 补上**工作区部署**那一半(用户:「使用技能时就要检查环境、做相关配置,
|
||||
⚠️ **说了无数遍但没实现**」)。**为什么必须补**:
|
||||
原来的 `env_check` 只查两件事 —— 技能根能不能定位、钩子有没有注册
|
||||
⇒ **「技能被复制进某个工作区、但那个工作区根本没部署」这类状态它永远查不出来**
|
||||
⇒ 没人提醒、没人负责 ⇒ 机制在该工作区**跑不起来却看不出为什么**。
|
||||
🔴 实测坐实:`vibe-product` 复制了完整副本(47 个文件),但**无 `goal.json`、
|
||||
无工作区级 `collabd.py`、配置是旧格式** ⇒ 旧版 `env_check` 读数全绿。
|
||||
"""
|
||||
rep = {"scope": scope, "checked_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"config_dir": config_dir(), "skills_root": skills_root(), "bad": [],
|
||||
"warn": []}
|
||||
if not rep["skills_root"]:
|
||||
rep["bad"].append("skills_root 定位失败(⛔ 已不回落~)")
|
||||
elif not _has_skill(rep["skills_root"]):
|
||||
rep["bad"].append("skills_root 存在但缺识别特征目录 %s:%s"
|
||||
% ("/".join(_SNIFFS), rep["skills_root"]))
|
||||
hooks = []
|
||||
for ev, cmd, t, path in registered_hooks():
|
||||
ok = bool(path) and os.path.isfile(path)
|
||||
row = {"event": ev, "timeout": t, "path": path, "exists": ok}
|
||||
hooks.append(row)
|
||||
if not ok:
|
||||
rep["bad"].append("钩子目标不存在:%s %s" % (ev, path or cmd[:60]))
|
||||
rep["hooks"] = hooks
|
||||
# ── 🔴🔴 补:**工作区部署**体检(原来完全没有这一段)──────────────────
|
||||
# ⚠️ `ws` 传空就**回落成技能根的父目录**(⛔ 不猜:副本场景下技能根在
|
||||
# `<工作区>/.workbuddy/skills/<包>/`,往上一级才是工作区)。
|
||||
_ws = _norm(ws or "")
|
||||
if not _ws and rep["skills_root"]:
|
||||
_ws = _norm(str(Path(rep["skills_root"]).parent.parent))
|
||||
dep = {"workspace": _ws, "deployed": None, "missing": [], "stale": []}
|
||||
if not _ws:
|
||||
dep["missing"].append("**工作区根定位不到**(⛔ 传 `ws=` 或让技能根可定位)")
|
||||
else:
|
||||
w = Path(_ws)
|
||||
cfg_p = w / ".workbuddy" / "collab" / "collabd.config.json"
|
||||
goal_p = w / "tmp" / "supervise-inbox" / "goal.json"
|
||||
# ① 部署配置:⛔ 不存在 = 没部署(`collabd.py` 不认这个工作区)
|
||||
dep["config"] = {"path": _norm(str(cfg_p)), "exists": cfg_p.exists()}
|
||||
if not cfg_p.exists():
|
||||
dep["missing"].append("**部署配置不存在**(`collabd.py` 不认这个工作区)"
|
||||
" ⇒ 跑 `init_workspace.py <工作区>`")
|
||||
# ② 目标文件:⛔ 没有它,机制没有目标可推
|
||||
dep["goal"] = {"path": _norm(str(goal_p)), "exists": goal_p.exists()}
|
||||
if not goal_p.exists():
|
||||
dep["missing"].append("**目标文件不存在**(机制没有目标可推)"
|
||||
" ⇒ 建 `tmp/supervise-inbox/goal.json`")
|
||||
# ③ inbox 骨架
|
||||
if not (w / "tmp" / "supervise-inbox").is_dir():
|
||||
dep["missing"].append("**inbox 骨架缺失**(`tmp/supervise-inbox/`)")
|
||||
# ④ 配置**格式是否过期**(有配置却缺现行键 = 旧格式 = 行为不可预期)
|
||||
if cfg_p.exists():
|
||||
try:
|
||||
_c = json.loads(cfg_p.read_text(encoding="utf-8"))
|
||||
need = ("log", "supervise_interval", "wake_enable")
|
||||
gone = [k for k in need if k not in _c]
|
||||
if gone:
|
||||
dep["stale"].append("配置缺现行键 %s ⇒ **旧格式**(升级技能后不会自动补)"
|
||||
% "/".join(gone))
|
||||
except Exception as e:
|
||||
dep["missing"].append("部署配置**读不出/解析失败**:%r" % (e,))
|
||||
dep["deployed"] = not dep["missing"]
|
||||
rep["deploy"] = dep
|
||||
# ⚠️ 归类:**没部署进 `warn` 而不塞 `bad`** ——
|
||||
# 「这个工作区还没启用机制」是**合法状态**(技能可只作资产存放),
|
||||
# ⛔ 不该让只装了技能副本的工作区一律体检红。查 `deploy.deployed` 即可。
|
||||
rep["warn"].extend(dep["missing"] + dep["stale"])
|
||||
|
||||
# 🔴🔴🔴 ⑤ **工作区归属核对**(S 红线 · 2026-10-04 用户当场纠正后补)
|
||||
# 病根:用户在本工作区下达「监控 vibe-product」的目標,我把任务会话建到了
|
||||
# `vibe-product` ⇒ 那条会话在本工作区看板/台账里**直接不可见**,
|
||||
# 而本工作区恰恰是问题最集中、最需要它的地方。⛔ 没有任何机制提醒过。
|
||||
# ⇒ 这里做成**体检项**:技能一被用起来就核对一次归属,⛔ 不靠人记住。
|
||||
att = {"this_ws": _ws, "declared": [], "mismatch": [], "peer_missing": []}
|
||||
try:
|
||||
_db = Path(config_dir()) / "workbuddy.db"
|
||||
if _db.exists():
|
||||
import sqlite3
|
||||
_con = sqlite3.connect("file:%s?mode=ro" % _db.as_posix(), uri=True, timeout=8)
|
||||
try:
|
||||
_con.execute("PRAGMA busy_timeout=8000")
|
||||
# 读法:⛔ **不按 `sessions.cwd` 分组**(后台会话可带独立 cwd),
|
||||
# 而按 `automations.cwds` 的字面正斜杠形态 —— 那才是「这条线归谁」。
|
||||
_rows = _con.execute(
|
||||
"SELECT name, cwds, status FROM automations").fetchall()
|
||||
finally:
|
||||
_con.close()
|
||||
_this = _norm_ws(_ws)
|
||||
for _nm, _cw, _stt in _rows:
|
||||
try:
|
||||
_c = json.loads(_cw or "[]")
|
||||
except Exception:
|
||||
_c = []
|
||||
for _one in _c:
|
||||
att["declared"].append({"name": _nm, "cwd": _one,
|
||||
"status": _stt, "is_self": _norm_ws(_one) == _this})
|
||||
# 🔴 判「有没有本工作区自己的线」:⛔ **只数有名字的**(按库实测:
|
||||
# 机制里存在多条**无 `name` 的排期** ⇒ 拿来凑数会恒绿)
|
||||
att["self_lines"] = [d for d in att["declared"] if d["is_self"]]
|
||||
else:
|
||||
att["peer_missing"].append("宿主库 `%s` 读不到(⛔ 归属无法核对)" % _db)
|
||||
except Exception as e:
|
||||
att["peer_missing"].append("归属核对失败:%r" % (e,))
|
||||
if not att.get("self_lines") and not att["peer_missing"]:
|
||||
att["peer_missing"].append(
|
||||
"**本工作区名下没有任何协作线**(`automations.cwds` 里找不到它)"
|
||||
" ⇒ 目标/会话会落到别的工作区去,本区看板与台账看不到")
|
||||
rep["attribution"] = att
|
||||
rep["warn"].extend(att["peer_missing"])
|
||||
|
||||
rep["ok"] = not rep["bad"]
|
||||
return rep
|
||||
|
||||
|
||||
|
||||
def write_env_stamp(ws: str = "", scope: str = "global", fixed=None) -> str:
|
||||
"""✍️ **写环境标记**(含状态变量+时间)。`scope` 决定落在哪个文件夹。"""
|
||||
rep = env_check(ws, scope)
|
||||
rep["fixed"] = list(fixed or [])
|
||||
p = Path(_stamp_path(ws, scope))
|
||||
try:
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(rep, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8", newline="\n")
|
||||
except Exception as e:
|
||||
sys.stderr.write("env-stamp 写失败:%r\n" % (e,))
|
||||
return ""
|
||||
return _norm(p)
|
||||
|
||||
|
||||
def read_env_stamp(ws: str = "", scope: str = "global") -> dict:
|
||||
try:
|
||||
return json.loads(Path(_stamp_path(ws, scope)).read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def require_env(ws: str = "", what: str = "") -> str:
|
||||
"""🔴 **调用方统一入口**:定位不到就**报错并返回空串**(硬规矩②)。
|
||||
|
||||
⛔ 绝不在这里 `sys.exit` —— 钩子进程退出码由调用方决定(各钩子预算不同)。
|
||||
"""
|
||||
root = skills_root()
|
||||
if root:
|
||||
return root
|
||||
sys.stderr.write(
|
||||
"[env] 技能库根定位失败%s:DSH_SKILLS_ROOT / CODEBUDDY_CONFIG_DIR 均未给,"
|
||||
"沿 __file__ 上溯也没找到含 %s 的目录。\n"
|
||||
" ⛔ 已⛔静默跳过(不再回落 ~,因Windows 上 ~ 不是真配置目录)\n"
|
||||
" ✅ 修法:设环境变量 DSH_SKILLS_ROOT,或跑 "
|
||||
"`python scripts/hooks/_env.py --stamp` 重建标记并体检\n"
|
||||
% (("(%s)" % what) if what else "", "/".join(_SNIFFS)))
|
||||
return ""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
_ap = argparse.ArgumentParser(description="环境自检/环境标记")
|
||||
_ap.add_argument("--ws", default="", help="工作区绝对路径")
|
||||
_ap.add_argument("--scope", default="global", choices=["global", "workspace"])
|
||||
_ap.add_argument("--stamp", action="store_true", help="写环境标记")
|
||||
_ap.add_argument("--fixed", default="", help="--stamp 时记进 fixed 的说明")
|
||||
_a = _ap.parse_args()
|
||||
if _a.stamp:
|
||||
_p = write_env_stamp(_a.ws, _a.scope,
|
||||
[_a.fixed] if _a.fixed else None)
|
||||
print("已写标记:%s" % (_p or "⛔ 写失败"))
|
||||
raise SystemExit(0 if _p else 1)
|
||||
_r = env_check(_a.ws, _a.scope)
|
||||
print("配置目录 = %s" % _r["config_dir"])
|
||||
print("技能库根 = %s" % (_r["skills_root"] or "⛔ 定位失败"))
|
||||
print("已注册钩子 = %d 条|不存在的 = %d"
|
||||
% (len(_r["hooks"]), sum(1 for h in _r["hooks"] if not h["exists"])))
|
||||
for _b in _r["bad"]:
|
||||
print(" ⛔ %s" % _b)
|
||||
print("结论 = %s" % ("✅ 通过" if _r["ok"] else "❌ 不通过"))
|
||||
raise SystemExit(0 if _r["ok"] else 1)
|
||||
Reference in new issue
Block a user