Files
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

421 lines
24 KiB
Python
Raw Permalink 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
# -*- coding: utf-8 -*-
"""DSH 状态快照 —— 让 AI 用「1 次调用」代替「十几轮探索」。
用法:
python state.py # 本地快照(快,无网络)
python state.py --online # 追加:与 Gitea 远端比对基线(需 SSH)
设计原则(2026-09-16 立):
① 只输出「能决定下一步」的内容。**大输出本身就是成本。**
② 只读宿主库 / 远端,**不写它们**。执行它不需要抢锁。
⚠️ **唯一例外**(2026-10-02 用户加):**会话规则机制体检**会**写一份自己的产物**
`<WS>/.workbuddy/collab/session-rules.json`(=用户要求的那枚「状态标记」)——
它只落在**本工作区自己的协作件目录**里,原子替换、不碰宿主库,故仍不改变本项免锁的性质。
③ 每条状态带「判据」——让读者不必再自己取证。
④ 默认不联网、不 SSH —— 那些是慢操作,要就显式加 --online。
"""
import io, json, os, re, subprocess, sys, datetime
REPO = r"D:/github/dsh_shenxian"
DOCS = os.path.join(REPO, "dsh-server-docs")
WS = r"E:/ProgramData/AIProject/ai1net-dsh-server"
# 🔑 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-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)
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)
# ── 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))
# ── 6. 下一步 ─────────────────────────────────────────────────
print()
print(" ⇒ 下一步:按**你那条线**(见下面「口令」)的 [入口] §2 第 1 条开工;"
"⛔ 不要再 Glob/Grep 全库探索(省积分第一纪律)")
print(" ⇒ 口令(复制给新会话):跑 `state.py`,按 **%s** 那段 §2 第 1 条开工"
% os.path.basename(ENTRY).replace("接续入口_", "").replace(".md", "")
.rsplit("_", 1)[0])
print(" ⇒ 动手前:bash \"%s/07-scripts/handoff-guard.sh\" --claim-exec \"<你的会话名>\"" % DOCS)