diff --git a/dsh-server-docs/scripts/stop-dialog-guard.py b/dsh-server-docs/scripts/stop-dialog-guard.py index 987a1fa..9cb5a75 100644 --- a/dsh-server-docs/scripts/stop-dialog-guard.py +++ b/dsh-server-docs/scripts/stop-dialog-guard.py @@ -339,6 +339,48 @@ def guard_health(root): '(规则该不该收窄是人的决定)。' % (len(rows), n, why)) +def path_health(root): + """**自动发现并上报**:hooks 指向的脚本、文档库位置,是否还在。 + + 来历(2026-09-15 实测事故):文档库被整目录搬到 `_中间产物_待清理/` ⇒ 宿主 hooks 仍指向旧位置 ⇒ + **锁闸门静默失效**;当时唯一线索是"`lock-hook.log` 今天 0 条 PreToolUse",而**没人会主动去数**。 + ⇒ 让每轮都跑的脚本顺带体检,机制失效能在**下一次用户发言时**自己浮出来。 + + 判据(任一命中即报):① **从 `settings.json` 现读** hooks 的 command,抽出其中的 `.py` 逐个 `os.path.exists` + (配置里是权威指向 ⇒ 能发现"指向了不存在的文件");② 文档库在两处候选位置**都不存在**。 + 取不到配置 ⇒ 跳过该项(**fail-open**,绝不因体检本身误报)。 + """ + bad = [] + cfg = os.path.join(os.environ.get('WORKBUDDY_CONFIG_DIR') + or os.path.join(os.path.expanduser('~'), '.workbuddy'), 'settings.json') + seen = 0 + try: + d = json.load(io.open(cfg, encoding='utf-8')) + for ev, arr in (d.get('hooks') or {}).items(): + for blk in (arr or []): + for h in ((blk or {}).get('hooks') or []): + c = h.get('command') or '' + if c: + seen += 1 + for m in re.finditer(r'([A-Za-z]:[\\/][^"\']*?\.py)', c): + p = m.group(1) + if not os.path.exists(p): + bad.append('hooks「%s」指向的 `%s` 不存在' % (ev, os.path.basename(p))) + except Exception: + return '' # 读不到配置 ⇒ fail-open,绝不因体检本身误报 + if seen == 0: + bad.append('hooks 配置里**没有任何 command 条目** ⇒ 钩子可能被清空 / 被整段覆盖') + if not bad: + return '' + uniq = [] + for x in bad: + if x not in uniq: + uniq.append(x) + return ('🚨【路径自检】%s ⇒ **机制可能已静默失效**。处置:**立刻上报用户**,并核对 `settings.json` 的 ' + 'hooks 路径与文档库当前位置(2026-09-15 同类事故:文档库被整目录搬走,锁闸门失效一整天无人察觉)。' + % ';'.join(uniq[:3])) + + def mode_of(root): try: m = io.open(os.path.join(root or '.', '.workbuddy', 'stop-guard-mode'), encoding='utf-8').read() @@ -364,12 +406,13 @@ def user_prompt_mode(payload): toks, ncalls, prev = session_budget(tp) note = budget_note(toks, ncalls, prev) gh = guard_health(root0 or '.') - log(root0 or '.', 'invoked(user-prompt)|mode=%s|上轮收尾=征询句:%s|上下文=%s tok(+%s)|工具=%s 次|预算告警=%s|门禁自检=%s|%s' - % (mode, hit, toks, (toks - prev) if (toks and prev) else '-', ncalls, bool(note), bool(gh), + ph = path_health(root0 or '.') + log(root0 or '.', 'invoked(user-prompt)|mode=%s|上轮收尾=征询句:%s|上下文=%s tok(+%s)|工具=%s 次|预算告警=%s|门禁自检=%s|路径自检=%s|%s' + % (mode, hit, toks, (toks - prev) if (toks and prev) else '-', ncalls, bool(note), bool(gh), bool(ph), (tail.replace('\n', ' ')[:60] if tail else '(取不到上一轮文本)'))) - if mode == 'inject' and (hit or note or gh): + if mode == 'inject' and (hit or note or gh or ph): ctx = '' - for seg in (CONTEXT if hit else '', note, gh): + for seg in (CONTEXT if hit else '', note, gh, ph): if seg: ctx += ('\n\n' + seg) if ctx else seg _emit({'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit',