2026-09-24 07:51:03 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""DSH 状态快照 —— 让 AI 用「1 次调用」代替「十几轮探索」。
|
|
|
|
|
|
|
|
|
|
|
|
用法:
|
|
|
|
|
|
python state.py # 本地快照(快,无网络)
|
|
|
|
|
|
python state.py --online # 追加:与 Gitea 远端比对基线(需 SSH)
|
|
|
|
|
|
|
|
|
|
|
|
设计原则(2026-09-16 立):
|
2026-10-10 23:13:22 +08:00
|
|
|
|
① 只输出「能决定下一步」的内容。**大输出本身就是成本。**
|
|
|
|
|
|
② 只读宿主库 / 远端,**不写它们**。执行它不需要抢锁。
|
|
|
|
|
|
⚠️ **唯一例外**(2026-10-02 用户加):**会话规则机制体检**会**写一份自己的产物**
|
|
|
|
|
|
`<WS>/.workbuddy/collab/session-rules.json`(=用户要求的那枚「状态标记」)——
|
|
|
|
|
|
它只落在**本工作区自己的协作件目录**里,原子替换、不碰宿主库,故仍不改变本项免锁的性质。
|
2026-09-24 07:51:03 +08:00
|
|
|
|
③ 每条状态带「判据」——让读者不必再自己取证。
|
|
|
|
|
|
④ 默认不联网、不 SSH —— 那些是慢操作,要就显式加 --online。
|
|
|
|
|
|
"""
|
2026-10-10 23:13:22 +08:00
|
|
|
|
import io, json, os, re, subprocess, sys, datetime
|
2026-09-24 07:51:03 +08:00
|
|
|
|
|
|
|
|
|
|
REPO = r"D:/github/dsh_shenxian"
|
|
|
|
|
|
DOCS = os.path.join(REPO, "dsh-server-docs")
|
2026-10-10 23:13:22 +08:00
|
|
|
|
WS = r"E:/ProgramData/AIProject/ai1net-dsh-server"
|
2026-09-24 07:51:03 +08:00
|
|
|
|
# 🔑 2026-09-21 加 `--ws <path>`:让**其他工作区**用同一份脚本取**自己那条线**的状态。
|
|
|
|
|
|
# 用法 = python "<本脚本绝对路径>" --ws "<自己的工作区>"(本工作区无脚本时不必复制第二份)。
|
|
|
|
|
|
# ⛔ 跨工作区调用**不改变 cwds** —— 接续任务一律建在**自己的工作区**(归属三律,见技能 §0.6)。
|
|
|
|
|
|
_WS_CUSTOM = False
|
|
|
|
|
|
for _i, _a in enumerate(sys.argv):
|
|
|
|
|
|
if _a == "--ws" and _i + 1 < len(sys.argv):
|
|
|
|
|
|
WS = sys.argv[_i + 1].replace("\\", "/").rstrip("/")
|
|
|
|
|
|
_WS_CUSTOM = True
|
|
|
|
|
|
LOGDIR = os.path.join(WS, ".workbuddy", "memory")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _norm(p):
|
|
|
|
|
|
return os.path.normcase(os.path.normpath(p.replace("\\", "/")))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _owner_ws(path):
|
|
|
|
|
|
"""读入口文件头部的**工作区归属声明**(2026-09-21 加,治「入口落错家」)。
|
|
|
|
|
|
|
|
|
|
|
|
规范写法(放在文件前 1.2 KB 内):`> 🔴 **工作区**:<该线自己的工作区绝对路径>`
|
|
|
|
|
|
返回 "" = 未声明 ⇒ 按**本工作区**处理(兼容旧文件,不报错)。
|
|
|
|
|
|
⛔ 别的工作区复制来的副本会被识别成「外来线」并排除出 §2 / 口令 ——
|
|
|
|
|
|
否则本工作区会把**别线**当成自己的线,新会话被引到别人的待办上(2026-09-21 实测)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
head = io.open(path, encoding="utf-8", errors="replace", newline="").read()[:1200]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
m = re.search(r"(?:^|[\s>|*])工作区\**\s*[::]\s*"
|
|
|
|
|
|
r"([A-Za-z]:[\\/][^\s|,,()()【】\[\]`]*)", head)
|
|
|
|
|
|
return m.group(1).rstrip("/\\") if m else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _list_entries():
|
|
|
|
|
|
"""全部「接续入口_*.md」,按 mtime 降序 —— ⛔ 不写死文件名(2026-09-16 晚加)。
|
|
|
|
|
|
|
|
|
|
|
|
多线并行时**只取最新一份**会让新会话走错线(这是他线的待办),
|
|
|
|
|
|
所以这里**列全**,由新会话按自己那份接续包 / 自动化 prompt 里的线名认领。
|
|
|
|
|
|
⚠️ 2026-09-21 加归属校验:声明的工作区 ≠ 本工作区 ⇒ 标 `alien`(外来副本,应迁回+删)。
|
|
|
|
|
|
返回 4 元组 `(path, mtime, owner_ws, alien)`。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
c = [os.path.join(WS, f) for f in os.listdir(WS)
|
|
|
|
|
|
if f.startswith("接续入口_") and f.endswith(".md")]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
c = []
|
|
|
|
|
|
c.sort(key=os.path.getmtime, reverse=True)
|
|
|
|
|
|
out = []
|
|
|
|
|
|
for p in c:
|
|
|
|
|
|
ow = _owner_ws(p)
|
|
|
|
|
|
out.append((p,
|
|
|
|
|
|
datetime.datetime.fromtimestamp(os.path.getmtime(p)).strftime("%m-%d %H:%M"),
|
|
|
|
|
|
ow, bool(ow) and _norm(ow) != _norm(WS)))
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ENTRIES = _list_entries()
|
|
|
|
|
|
LOCAL = [e for e in ENTRIES if not e[3]] # 属于本工作区的线(§2 展开 / 口令只用这些)
|
|
|
|
|
|
ALIEN = [e for e in ENTRIES if e[3]] # 别的工作区的入口副本(应迁移 + 删除)
|
|
|
|
|
|
ENTRY = ((LOCAL or ENTRIES)[0][0] if ENTRIES
|
|
|
|
|
|
else os.path.join(WS, "接续入口_覆盖网络线_20260916.md"))
|
|
|
|
|
|
LOCK = os.path.join(DOCS, "交接单", ".exec-lock")
|
|
|
|
|
|
ONLINE = "--online" in sys.argv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sh(cmd, cwd=None, timeout=25):
|
|
|
|
|
|
try:
|
|
|
|
|
|
p = subprocess.run(["bash", "-lc", cmd], cwd=cwd, capture_output=True,
|
|
|
|
|
|
timeout=timeout)
|
|
|
|
|
|
return (p.stdout or b"").decode("utf-8", "replace").strip()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return "!%s" % type(e).__name__
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def read(p):
|
|
|
|
|
|
try:
|
|
|
|
|
|
return io.open(p, encoding="utf-8", errors="replace", newline="").read()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def line(k, v):
|
|
|
|
|
|
print(" %-9s %s" % (k, v))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print("═══ DSH 状态快照 · %s ═══" % datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# ── 1. 执行锁(域锁 + 旧全局锁)────────────────────────────────
|
|
|
|
|
|
# ⚠️ `.exec-lock` 是**目录**(`handoff-guard.sh --claim-exec` 用 `mkdir` 原子创建,OWNER 写在里面),
|
|
|
|
|
|
# **不是文件**。旧版按文件读 ⇒ 永远读空 ⇒ **恒报「空闲」** —— 也就是每个新会话读到的**第一个
|
|
|
|
|
|
# 信号就是错的**(2026-09-16 实测:本会话正持锁时 state.py 仍报空闲)。两种形态都兼容:
|
|
|
|
|
|
# 目录读 `OWNER`,历史遗留的普通文件照读。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 2026-09-22 加:**域锁**(`交接单/.locks/*`)—— 不重叠的域可并行 ⇒ 开机第 0 步必须能看见
|
|
|
|
|
|
# 「谁占了哪些域」,否则新会话会误判"不能开工"而错失并行机会。
|
|
|
|
|
|
LOCKSROOT = os.path.join(DOCS, "交接单", ".locks")
|
|
|
|
|
|
SID = os.environ.get("CODEBUDDY_SESSION_ID", "")
|
|
|
|
|
|
s = ""
|
|
|
|
|
|
if os.path.isdir(LOCK):
|
|
|
|
|
|
s = read(os.path.join(LOCK, "OWNER")).strip()
|
|
|
|
|
|
elif os.path.exists(LOCK):
|
|
|
|
|
|
s = read(LOCK).strip()
|
|
|
|
|
|
|
|
|
|
|
|
dom_locks = []
|
|
|
|
|
|
if os.path.isdir(LOCKSROOT):
|
|
|
|
|
|
for _n in sorted(os.listdir(LOCKSROOT)):
|
|
|
|
|
|
_d = os.path.join(LOCKSROOT, _n)
|
|
|
|
|
|
if not os.path.isdir(_d):
|
|
|
|
|
|
continue
|
|
|
|
|
|
_own = read(os.path.join(_d, "OWNER")).strip()
|
|
|
|
|
|
_who = (_own.splitlines() or [""])[0]
|
|
|
|
|
|
_doms = " ".join(read(os.path.join(_d, "DOMAINS")).split())
|
|
|
|
|
|
if _n == ".publish":
|
|
|
|
|
|
dom_locks.append(("发布锁", _who, "commit/push/scp 期间不可并行"))
|
|
|
|
|
|
elif _n == ".migrations":
|
|
|
|
|
|
for _m in sorted(os.listdir(_d)) if os.path.isdir(_d) else []:
|
|
|
|
|
|
_md = os.path.join(_d, _m)
|
|
|
|
|
|
if os.path.isdir(_md):
|
|
|
|
|
|
dom_locks.append(("骨架", read(os.path.join(_md, "OWNER")).splitlines()[0] if read(os.path.join(_md, "OWNER")).strip() else "?", _m))
|
|
|
|
|
|
elif _n.startswith("."):
|
|
|
|
|
|
continue
|
|
|
|
|
|
else:
|
|
|
|
|
|
_mine = bool(SID) and SID in _own
|
|
|
|
|
|
dom_locks.append(("域锁" + ("(我)" if _mine else ""), _who, _doms))
|
|
|
|
|
|
|
|
|
|
|
|
if not s and not dom_locks:
|
|
|
|
|
|
line("[锁]", "空闲 ✅ —— 「空闲」≠「可以开工」:先 --claim-exec 抢锁")
|
|
|
|
|
|
elif dom_locks and not s:
|
|
|
|
|
|
# 纯域锁形态(目标形态):并行的关键信号 —— 不重叠即可开工
|
|
|
|
|
|
line("[锁]", "🟢 域锁形态(%d 把)—— **不重叠的域可以并行**" % len(dom_locks))
|
|
|
|
|
|
for _k, _who, _x in dom_locks[:5]:
|
|
|
|
|
|
line("", " · [%s] %s | %s" % (_k, _who, _x[:70]))
|
|
|
|
|
|
line("", "⇒ 你要的域若与上面不重叠 ⇒ 可直接抢:--claim-exec \"<会话名>\" --domains <域...>")
|
|
|
|
|
|
line("", "⇒ 重叠 ⇒ 停手或改做不重叠的域(⛔ 不得删锁,R9)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
ls = [x.strip() for x in s.splitlines() if x.strip()]
|
|
|
|
|
|
line("[锁]", "🔴 旧全局锁被占 —— %s" % (" / ".join(ls[:3]) if ls else s))
|
|
|
|
|
|
line("", "⇒ 该会话未升级到域锁 ⇒ 全局独占;等它 --release-exec(⛔ 不得删锁,R9)")
|
|
|
|
|
|
for _k, _who, _x in dom_locks[:4]:
|
|
|
|
|
|
line("", " · [%s] %s | %s" % (_k, _who, _x[:70]))
|
|
|
|
|
|
|
|
|
|
|
|
# ── 2. git 基线 ────────────────────────────────────────────────
|
|
|
|
|
|
head = sh("git rev-parse --short HEAD", REPO)
|
|
|
|
|
|
br = sh("git rev-parse --abbrev-ref HEAD", REPO)
|
|
|
|
|
|
porcelain = sh("git status --porcelain", REPO)
|
|
|
|
|
|
nmod = len([l for l in porcelain.splitlines() if l.strip()])
|
|
|
|
|
|
line("[git]", "分支 %s · HEAD %s" % (br or "?", head or "?"))
|
|
|
|
|
|
line("", "工作区 %s" % ("干净 ✅" if nmod == 0 else "🟡 %d 处改动(未 commit)" % nmod))
|
|
|
|
|
|
if _WS_CUSTOM:
|
|
|
|
|
|
line("", "⚠️ 本次 `--ws` 指向别的工作区 ⇒ [git] 段是平台代码仓 `%s` 的状态,与该工作区无关" % REPO)
|
|
|
|
|
|
for l in porcelain.splitlines()[:8]:
|
|
|
|
|
|
print(" %s" % l[:96])
|
|
|
|
|
|
if nmod > 8:
|
|
|
|
|
|
print(" … 余 %d 处" % (nmod - 8))
|
|
|
|
|
|
|
|
|
|
|
|
# ── 3. 远端 / 镜像(--online)──────────────────────────────────
|
|
|
|
|
|
if ONLINE:
|
|
|
|
|
|
rem = sh('GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=12 " '
|
|
|
|
|
|
'git ls-remote origin refs/heads/master', REPO, 40)
|
|
|
|
|
|
m = re.search(r"([0-9a-f]{7,40})\s+refs/heads/master", rem)
|
|
|
|
|
|
if m:
|
|
|
|
|
|
rsha = m.group(1)[:7]
|
|
|
|
|
|
anc = sh("git merge-base --is-ancestor %s HEAD && echo YES || echo NO" % rsha, REPO)
|
|
|
|
|
|
line("[远端]", "master=%s %s" % (rsha, "✅ 已同步" if rsha == head
|
|
|
|
|
|
else ("可快进" if anc == "YES" else "🔴 已分叉")))
|
|
|
|
|
|
else:
|
|
|
|
|
|
line("[远端]", "⚠️ ls-remote 取不到(本机 refs/remotes 写不进是已知的)")
|
|
|
|
|
|
|
|
|
|
|
|
# ── 4. 接续入口 §2 待办(⚠️ 多线并行:只走你自己那条)──────────
|
|
|
|
|
|
if len(LOCAL) > 1:
|
|
|
|
|
|
line("[入口]", "⚠️ 检测到 **%d 条工作线** —— ⛔ 只走**你自己那条**"
|
|
|
|
|
|
"(以你手上那份接续包 / 自动化 prompt 里的线名为准),别串线" % len(LOCAL))
|
|
|
|
|
|
for p0, mt0, ow0, al0 in ENTRIES:
|
|
|
|
|
|
line("[入口]", "%s(mtime %s)%s" % (
|
|
|
|
|
|
os.path.basename(p0), mt0,
|
|
|
|
|
|
" 🔴 **外来线**(属于 %s ⇒ 迁回该工作区并删本副本)" % ow0 if al0
|
|
|
|
|
|
else (" ← 最新" if p0 == ENTRY else "")))
|
|
|
|
|
|
if ALIEN:
|
|
|
|
|
|
line("", "🔴 上面 %d 条是**别的工作区的入口副本**(归属三律:入口只允许一份、在本工作区)"
|
|
|
|
|
|
"⇒ 内容迁回 %s,删掉这里的副本" % (len(ALIEN), ALIEN[0][2]))
|
|
|
|
|
|
t = read(ENTRY)
|
|
|
|
|
|
if t:
|
|
|
|
|
|
print(" ——下面只展开最新的那条(%s)§2——" % os.path.basename(ENTRY))
|
2026-10-10 23:13:22 +08:00
|
|
|
|
# ⚠️ 2026-09-25 修:原正则只认 `## 2.`,而本工作区的入口一律写 `## §2 xxx`
|
|
|
|
|
|
# ⇒ 一直落到下面的「§2 未解析出条目」(实测:4 条入口**全部**解析为空 ——
|
|
|
|
|
|
# `[入口] §2` 段是每棒开机第一个信号,它空着 = 下一棒拿不到动作)。
|
|
|
|
|
|
# 现兼容三种写法:`## 2.` / `## §2 ` / `## 2、`;⚠️ 用 lookahead 卡住分隔符,
|
|
|
|
|
|
# ⛔ 不能让 `## 2026-…` 这类以 2 开头的标题被误当成 §2。
|
|
|
|
|
|
sec = re.search(r"##\s*(?:§)?\s*2(?=[.\s、]|$)[^\n]*\n(.*?)(?=\n##\s|\Z)", t, re.S)
|
2026-09-24 07:51:03 +08:00
|
|
|
|
body = sec.group(1) if sec else ""
|
|
|
|
|
|
items = re.findall(r"^\s*(?:\d+[.)]|[-*])\s+(.{6,120})", body, re.M)
|
|
|
|
|
|
for i, it in enumerate(items[:6], 1):
|
|
|
|
|
|
print(" %d) %s" % (i, it.replace("**", "").strip()[:104]))
|
|
|
|
|
|
if not items:
|
|
|
|
|
|
line("", "(§2 未解析出条目 —— 请直接读该文件)")
|
|
|
|
|
|
|
|
|
|
|
|
# ── 5. 上次收口点 ─────────────────────────────────────────────
|
|
|
|
|
|
today = os.path.join(LOGDIR, datetime.date.today().isoformat() + ".md")
|
|
|
|
|
|
lg = read(today)
|
|
|
|
|
|
if lg:
|
|
|
|
|
|
# 优先找「接续点 / 收口」章节,其次取最后一个章节
|
|
|
|
|
|
best = None
|
|
|
|
|
|
for m in re.finditer(r"^#{2,3}\s*(.+)$", lg, re.M):
|
|
|
|
|
|
if ("接续" in m.group(1)) or ("收口" in m.group(1)):
|
|
|
|
|
|
best = m
|
|
|
|
|
|
if best is None:
|
|
|
|
|
|
hs = list(re.finditer(r"^#{2,3}\s*(.+)$", lg, re.M))
|
|
|
|
|
|
best = hs[-1] if hs else None
|
|
|
|
|
|
start = best.start() if best else 0
|
|
|
|
|
|
title = best.group(1).strip() if best else "(无章节)"
|
|
|
|
|
|
nxt = re.search(r"^#{2,3}\s", lg[start + 4:], re.M)
|
|
|
|
|
|
sec = lg[start:start + 4 + nxt.start()] if nxt else lg[start:]
|
|
|
|
|
|
line("[收口]", "%s |今日日志 %d 字符" % (title[:64], len(lg)))
|
|
|
|
|
|
# ⚠️ 日志是**多会话共用**的:多线并行时,最后写的那个不一定是「你那条线」的
|
|
|
|
|
|
_ti = [m.group(1).strip() for m in re.finditer(r"^#{2,3}\s*(.+)$", lg, re.M)
|
|
|
|
|
|
if ("接续" in m.group(1)) or ("收口" in m.group(1))]
|
|
|
|
|
|
if len(_ti) > 1:
|
|
|
|
|
|
print(" ⚠️ 今日日志里有 %d 个接续点/收口章节(多线共用同一份日志)"
|
|
|
|
|
|
"⇒ 只认**你那条线**的那一个:" % len(_ti))
|
|
|
|
|
|
for _x in _ti[-3:]:
|
|
|
|
|
|
print(" · %s" % _x[:96])
|
|
|
|
|
|
print(" ⚠️ 下面是**日志原文摘录**,可能含已被后续推翻的结论"
|
|
|
|
|
|
"(实测教训:曾摘出「定时自动化仍在烧钱」「rawUsage 为空」两条已勘误说法)")
|
|
|
|
|
|
print(" ⇒ 当前事实以 `.workbuddy/memory/MEMORY.md` 状态层为准;该做什么看上面 [入口] 段。")
|
|
|
|
|
|
keys = [l.strip() for l in sec.split("\n")
|
|
|
|
|
|
if re.match(r"^\s*[-*]\s*\*\*", l) or re.match(r"^\s*\d+[.)]\s*\*\*", l)]
|
|
|
|
|
|
for b in (keys[:5] or [l.strip() for l in sec.split("\n") if l.strip().startswith("-")][:3]):
|
|
|
|
|
|
print(" %s" % b.replace("**", "").strip()[:104])
|
|
|
|
|
|
if best and best.group(1) != title:
|
|
|
|
|
|
pass
|
|
|
|
|
|
else:
|
|
|
|
|
|
line("[收口]", "⚠️ 今日日志不存在:%s" % today)
|
|
|
|
|
|
|
2026-10-10 23:13:22 +08:00
|
|
|
|
# ── 5.5 钩子闸门自检(2026-09-30 加)────────────────────────────
|
|
|
|
|
|
# 为什么必须有:`settings.json.bak-decision-20260927-233116`(mtime 09-26 21:17)里 **hooks 段是空的**
|
|
|
|
|
|
# ⇒ 那次改动把 `stop-dialog-guard` 的 UserPromptSubmit 钩子(=「上下文到量 ⇒ 强制收口:先落盘、
|
|
|
|
|
|
# 出接续包、登记接续棒」的**唯一触发源**)连同其它闸门**整段清掉**;09-27~09-30 只补回了
|
|
|
|
|
|
# decision_bridge / wb-result-hook ⇒ 该机制**静默失效 4 天**(guard 日志最后一条 09-26 19:19),
|
|
|
|
|
|
# 直到 09-30 才被人发现。⇒ 判据不能靠"记得去数":开工第 0 步(本文件)就把它数出来。
|
|
|
|
|
|
# ⚠️ 读配置必须认 `CODEBUDDY_CONFIG_DIR`(实测本机 = `E:\ProgramData\.workbuddy`);guard 自带的
|
|
|
|
|
|
# `path_health()` 读的是 `WORKBUDDY_CONFIG_DIR`/`~/.workbuddy` ⇒ 本机**永远读空 ⇒ 恒静默**。
|
|
|
|
|
|
GATES = [
|
|
|
|
|
|
("UserPromptSubmit", "stop-dialog-guard.py", "水位/收口触发源(接续机制的起点)"),
|
|
|
|
|
|
("UserPromptSubmit", "skill-load-guard.py", "用户点名方法 ⇒ 强制加载技能"),
|
|
|
|
|
|
("PreToolUse", "lock-guard-hook.py", "无锁不许改文档库 / 代码库"),
|
|
|
|
|
|
("PreToolUse", "bash-output-guard.py", "拦下会灌爆上下文的读命令"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _tail_time(p):
|
|
|
|
|
|
"""只读文件尾 8 KB 取最后一行前 16 字符(⛔ 不整读 —— lock-hook.log 已 3.6 MB)。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with io.open(p, "rb") as f:
|
|
|
|
|
|
f.seek(0, 2)
|
|
|
|
|
|
n = min(8192, f.tell())
|
|
|
|
|
|
f.seek(-n, 2)
|
|
|
|
|
|
rows = [x for x in f.read().decode("utf-8", "replace").split("\n") if x.strip()]
|
|
|
|
|
|
return (rows[-1] or "")[:16] if rows else "空"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return "缺"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_WSROOT = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
GATE_LOGS = [("收口", os.path.join(_WSROOT, ".workbuddy", "stop-dialog-guard.log")),
|
|
|
|
|
|
("技能", os.path.join(_WSROOT, ".workbuddy", "skill-load-guard.log")),
|
|
|
|
|
|
("限流", os.path.join(_WSROOT, ".workbuddy", "bash-guard.log")),
|
|
|
|
|
|
("锁", os.path.join(os.path.dirname(DOCS), ".workbuddy", "lock-hook.log"))]
|
|
|
|
|
|
_CFG = os.path.join(os.environ.get("CODEBUDDY_CONFIG_DIR")
|
|
|
|
|
|
or os.path.join(os.path.expanduser("~"), ".workbuddy"), "settings.json")
|
|
|
|
|
|
try:
|
|
|
|
|
|
_HK = (json.load(io.open(_CFG, encoding="utf-8")) or {}).get("hooks") or {}
|
|
|
|
|
|
_cmds = [(ev, str(h.get("command") or ""))
|
|
|
|
|
|
for ev, arr in _HK.items() for blk in (arr or [])
|
|
|
|
|
|
for h in ((blk or {}).get("hooks") or [])]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
_HK, _cmds = None, []
|
|
|
|
|
|
if _HK is None:
|
|
|
|
|
|
line("[闸门]", "⚠️ 读不到 hooks 配置:%s" % _CFG)
|
|
|
|
|
|
else:
|
|
|
|
|
|
_miss = [g for g in GATES if not any(g[0] == ev and g[1] in c for ev, c in _cmds)]
|
|
|
|
|
|
if _miss:
|
|
|
|
|
|
line("[闸门]", "🔴 关键闸门**不在册** ⇒ 该机制现在不会运行:")
|
|
|
|
|
|
for _ev, _f, _why in _miss:
|
|
|
|
|
|
print(" · %s(应挂 %s)—— %s" % (_f, _ev, _why))
|
|
|
|
|
|
else:
|
|
|
|
|
|
_cnt = {}
|
|
|
|
|
|
for _ev, _c in _cmds:
|
|
|
|
|
|
_cnt[_ev] = _cnt.get(_ev, 0) + 1
|
|
|
|
|
|
line("[闸门]", "✅ 关键闸门在册 | %s"
|
|
|
|
|
|
% " ".join("%s×%d" % (k, v) for k, v in sorted(_cnt.items())))
|
|
|
|
|
|
_bad = []
|
|
|
|
|
|
for _ev, _c in _cmds:
|
|
|
|
|
|
_m = re.search(r'([A-Za-z]:[\\/][^"\']*?\.py)', _c)
|
|
|
|
|
|
if _m and not os.path.exists(_m.group(1)):
|
|
|
|
|
|
_bad.append("%s(%s)" % (os.path.basename(_m.group(1)), _ev))
|
|
|
|
|
|
if _bad:
|
|
|
|
|
|
print(" ⚠️ 路径自检:%s 不存在 ⇒ 该钩子**静默失效**"
|
|
|
|
|
|
% "、".join(sorted(set(_bad))[:4]))
|
|
|
|
|
|
# 每条闸门日志的「最后一行时间」= 它最后一次真被调用的时刻(陈旧 ⇒ 该闸门已掉线)
|
|
|
|
|
|
print(" · 各闸门日志最后一行:%s"
|
|
|
|
|
|
% " ".join("%s=%s" % (t, _tail_time(p)) for t, p in GATE_LOGS))
|
|
|
|
|
|
|
|
|
|
|
|
# ── 5b. 会话规则机制体检(🔴 用户 2026-10-02 口径:开会话后**第一件事**)────────
|
|
|
|
|
|
# 用户原话逐字(**两句,第二句是纠正**):
|
|
|
|
|
|
# ① 「这个会话和协作会话的技能包 运行的第一件事 ,就应该是检查清楚
|
|
|
|
|
|
# 所有会话规划是否配置完整且生效,然后标记一个状态」
|
|
|
|
|
|
# ② 「就应该是检查清楚 **所有会话规则机制** 是否配置完整且生效,**不是规划 是 规则**」
|
|
|
|
|
|
# ⇒ 对象=**规则机制**(钩子 / 闸门 / 技能指针 / 常驻 / 编排…),⛔ 不是"排期规划"。
|
|
|
|
|
|
# 首版按①的字面写成「会话规划体检」、只查排期那一面 ⇒ 当天实测出来的三类失效
|
|
|
|
|
|
# (钩子指向已退役技能名 / 快照写进幽灵目录 / 每轮注入的记忆指针悬空)**一条都查不到**
|
|
|
|
|
|
# ⇒ 按②重做,⚠️ 旧脚本已退役到 `归档/技能包-旧件-20261002/`。
|
|
|
|
|
|
# 为什么放在这里:本文件是**所有会话的开工第 0 步**(见文末口令),把它挂在这儿
|
|
|
|
|
|
# 才能保证"第一件事"真的发生在第一步,而不是靠人记得。
|
|
|
|
|
|
# ⚠️ 逻辑**在技能包里**(skill 就是 skill)—— 这里只做**调用 + 摘要**;
|
|
|
|
|
|
# ⛔ 找不到技能包时**显式告警**,⛔ 绝不静默跳过(静默跳过=本项目反复踩的那个坑)。
|
|
|
|
|
|
print()
|
|
|
|
|
|
def _skills_root():
|
|
|
|
|
|
for c in (os.environ.get("DSH_SKILLS_ROOT"),
|
|
|
|
|
|
os.path.join(os.environ.get("CODEBUDDY_CONFIG_DIR") or "", "skills"),
|
|
|
|
|
|
os.path.join(os.path.expanduser("~"), ".workbuddy", "skills")):
|
|
|
|
|
|
if c and os.path.isdir(c):
|
|
|
|
|
|
return c
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_sk = _skills_root()
|
|
|
|
|
|
_spc = os.path.join(_sk, "session-mechanism", "scripts", "session-rules-check.py") if _sk else ""
|
|
|
|
|
|
if not (_spc and os.path.isfile(_spc)):
|
|
|
|
|
|
line("[会话规则]", "⚠️ 体检脚本不在 ⇒ 本项**未跑**(⛔ 不是「没问题」):"
|
|
|
|
|
|
"skills/session-mechanism/scripts/session-rules-check.py")
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
_pr = subprocess.run([sys.executable, _spc, "--ws", WS],
|
|
|
|
|
|
capture_output=True, timeout=40)
|
|
|
|
|
|
_txt = (_pr.stdout or b"").decode("utf-8", "replace").strip()
|
|
|
|
|
|
for _l in _txt.split("\n"):
|
|
|
|
|
|
print(" " + _l)
|
|
|
|
|
|
if not _txt:
|
|
|
|
|
|
_err = (_pr.stderr or b"").decode("utf-8", "replace").strip()
|
|
|
|
|
|
line("[会话规则]", "⚠️ 体检无输出(rc=%d)%s"
|
|
|
|
|
|
% (_pr.returncode, (":" + _err[:100]) if _err else ""))
|
|
|
|
|
|
except Exception as _e:
|
|
|
|
|
|
line("[会话规则]", "⚠️ 体检自身异常(⛔ 不影响开工):%s" % str(_e)[:120])
|
|
|
|
|
|
|
|
|
|
|
|
# ── 5c. 🔴🔴 钩子环境体检(2026-10-02 用户定案· 技能必须先检查环境配置)────────
|
|
|
|
|
|
# 为什么单列一项(真实代价:另一工作区为「钩子静默零输出」绕了两轮):
|
|
|
|
|
|
# 2026-10-02 实测 —— `reply-style-guard.py` 被注册成文档库里的旧副本,
|
|
|
|
|
|
# 它沿 `__file__` 上溯定位技能库失败 ⇒ 落进 `os.path.expanduser('~/.workbuddy/skills')`
|
|
|
|
|
|
# 而 **Windows 上 `~` 不是真配置目录** ⇒ 静默零输出,日志只有一行 `core=0 字符`
|
|
|
|
|
|
# ⇒ **「机制坏了」与「没配规则」表现完全一样**(原钩子设计判据就是「读不到就静默」)。
|
|
|
|
|
|
# 本项判**两样**:① 技能库根能否定位(⛔ 已删 `~` 回落)② 已注册钩子**逐条目标是否存在**。
|
|
|
|
|
|
# ⛔ 本项**只读**,⛔ 不写环境标记(标记由 `hooks/_env.py --stamp` 显式写)。
|
|
|
|
|
|
_envpy = os.path.join(_sk, "session-mechanism", "scripts", "hooks", "_env.py") if _sk else ""
|
|
|
|
|
|
if not (_envpy and os.path.isfile(_envpy)):
|
|
|
|
|
|
line("[钩子环境]", "⚠️ 自检模块不在 ⇒ 本项**未跑**(⛔ 不是「没问题」):"
|
|
|
|
|
|
"skills/session-mechanism/scripts/hooks/_env.py")
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
_er = subprocess.run([sys.executable, _envpy, "--ws", WS],
|
|
|
|
|
|
capture_output=True, timeout=30)
|
|
|
|
|
|
_et = (_er.stdout or b"").decode("utf-8", "replace").strip()
|
|
|
|
|
|
for _l in _et.split("\n"):
|
|
|
|
|
|
print(" " + _l)
|
|
|
|
|
|
if _er.returncode != 0:
|
|
|
|
|
|
_es = (_er.stderr or b"").decode("utf-8", "replace").strip()
|
|
|
|
|
|
line("[钩子环境]", "❌ 环境**未配好** ⇒ 钩子会静默失效(⛔ 先修再开工,"
|
|
|
|
|
|
"否则每轮规则注入都是空的)%s" % ((":" + _es[:140]) if _es else ""))
|
|
|
|
|
|
except Exception as _e2:
|
|
|
|
|
|
line("[钩子环境]", "⚠️ 体检自身异常(⛔ 不影响开工):%s" % str(_e2)[:120])
|
|
|
|
|
|
# 环境标记(用户 2026-10-02 要求:「在对应文件夹创建状态变量和时间作为标记」):
|
|
|
|
|
|
# ⛔ 标记缺失/过期**只提示不阻塞**(它是"上次何时体检过"的记录,不是闸门)。
|
|
|
|
|
|
# 🔴🔴 2026-10-02 用户拍板 **A 案:装全局,一处配好所有工作区共用** ⇒ 读**全局**那份
|
|
|
|
|
|
# (`<配置目录>/env-stamp.json`),⛔ 不再只读工作区那份(那是旧口径的残留)。
|
|
|
|
|
|
_st = os.path.join(os.environ.get("CODEBUDDY_CONFIG_DIR") or r"E:\ProgramData\.workbuddy",
|
|
|
|
|
|
"env-stamp.json")
|
|
|
|
|
|
_st_old = os.path.join(WS, ".workbuddy", "env-stamp.json")
|
|
|
|
|
|
if os.path.isfile(_st):
|
|
|
|
|
|
try:
|
|
|
|
|
|
_sj = json.loads(open(_st, encoding="utf-8").read())
|
|
|
|
|
|
print(" · 环境标记(全局口径):%s | scope=%s | ok=%s"
|
|
|
|
|
|
% (_sj.get("checked_at", "?"), _sj.get("scope", "?"), _sj.get("ok", "?")))
|
|
|
|
|
|
if os.path.isfile(_st_old):
|
|
|
|
|
|
print(" ⚠️ 本工作区还留着一份旧 `scope=workspace` 标记 ⇒ **读数不认它**"
|
|
|
|
|
|
"(现行口径=全局,用户 2026-10-02 拍板)。⛔ 不影响生效,可删:%s" % _st_old)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
print(" · 全局环境标记存在但**读不出**(⛔ 损坏)⇒ 建议重跑 `_env.py --stamp`")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(" · 全局环境标记:**无** ⇒ 首次使用请跑 "
|
|
|
|
|
|
"`python \"%s\" --ws \"%s\" --stamp --scope global`" % (_envpy, WS))
|
|
|
|
|
|
|
2026-09-24 07:51:03 +08:00
|
|
|
|
# ── 6. 下一步 ─────────────────────────────────────────────────
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(" ⇒ 下一步:按**你那条线**(见下面「口令」)的 [入口] §2 第 1 条开工;"
|
|
|
|
|
|
"⛔ 不要再 Glob/Grep 全库探索(省积分第一纪律)")
|
|
|
|
|
|
print(" ⇒ 口令(复制给新会话):跑 `state.py`,按 **%s** 那段 §2 第 1 条开工"
|
|
|
|
|
|
% os.path.basename(ENTRY).replace("接续入口_", "").replace(".md", "")
|
|
|
|
|
|
.rsplit("_", 1)[0])
|
2026-10-10 23:13:22 +08:00
|
|
|
|
print(" ⇒ 动手前:bash \"%s/07-scripts/handoff-guard.sh\" --claim-exec \"<你的会话名>\"" % DOCS)
|