Files
workbuddy_skills/session-mechanism/scripts/hooks/reply-style-guard.py
T
admin ea4e6d6d58 权威统一:提问/排版格式与自决策判据都归位到会话机制包内
起因(用户):「提问拍板格式不应该在会话机制中吗,怎么跑到别的技能中去了」
病根=**权威声明分裂**:
  · SKILL.md:963 说 03 号是「内联副本」⇒ 隐指权威在 agent-operating-rules;
  · 而 03 号自己头部写着「**这份是权威源**」,且把它说的第②个消费者
    `scripts/apply-reply-rules.py` 写成了**不在本包**的路径。
  ⇒ 同一件事两处说法打架 ⇒ 改的人(我)会把改动做错地方。

处置(统一为「会话机制包内即权威」,与 02/04 两份的既有处置一致):
  ① 注入器归位:新增 `session-mechanism/scripts/apply-reply-rules.py`,默认读**包内** 03 号;
  ② 03 号头部改写:权威=本文件,两个消费者都在本包内;
  ③ `reply-style-guard.py` 第③级兜底从**别的技能**改回包内路径;
  ④ `agent-operating-rules/references/回复排版-核心块.md` 标注「副本·权威在会话机制」;
  ⑤ `dsh-decision/references/00-决策方法论.md` 标注「来源·权威在会话机制」;
  ⑥ SKILL.md:963 措辞由「内联副本」校正为「收进本包并定为权威」。

验收:注入器 `--check` 报权威源=包内 03 号且与 CODEBUDDY.md 一致;
      每轮注入 1183 字符(含「竖排」「变相征询」);
      **无 CODEBUDDY.md 的干净工作区也能取到**(1185 字符)⇒ 自包含成立。
2026-10-06 22:45:43 +08:00

226 lines
11 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 python3
# -*- coding: utf-8 -*-
"""reply-style-guard.py —— 「回复排版与格式」的 UserPromptSubmit **全机**强制注入钩子
为什么需要它
────────────
2026-10-02 用户令(逐字):
· 「所有会话中回复排版和格式要求和规则,也要整合到会话技能中,使用时配置到对应环境文件中」
· 前一轮:「这个规则改为强遵循 固化在必循加入会话的地方」
起因:同一件事 **连点两次**(「我发现你又忘记如何回复执行结果了」「我确认你是又把执行后的
内容回复排版和格式给忘记了」)—— 根因**不是规则丢了**,是**规则只在会话开始时被读一次**:
长会话里会被稀释,而"回复长什么样"这件事**每一轮都要判一次**。
机制对照(同族,已存在)
────────────────────────
· `stop-dialog-guard.py` —— 治「收尾时的征询句」
· `skill-load-guard.py` —— 治「用户点名了方法却没加载技能」
本钩子治第三条面:**「每轮回复的形态」**。三者同挂 `UserPromptSubmit`,互补。
🔴 三条设计判据(本需求的关键,⛔ 都别改)
──────────────────────────────────────────
1. **真相源在技能里**(不在本文件、也不在某个工作区):
`<技能库>/agent-operating-rules/references/回复排版-核心块.md`
⇒ 技能是**跨工作区**的 ⇒ 一次改动,**所有会话**都跟着变。
⛔ **绝不在本文件里写死规则文本** —— 写死就变成第二真相源,两边必然漂。
2. **每轮都注入,⛔ 不设冷却**:这条规矩的价值就在"紧贴用户消息、每轮重述"。
设冷却(如 300s)= 让它按会话衰减回原样,那就白做了。
3. **落环境文件 = 可选项不是必需**:某环境若已用 `apply-reply-rules.py` 把核心块落到自己的
规则文件(`CODEBUDDY.md` / `AGENTS.md`)里,**优先用那份**(可能被本地化过);
没有 ⇒ 回落到技能里那份。两级回退,⛔ 哪一级都读不到就**静默零输出**(不影响任何人)。
安装(`settings.json` 的 `hooks.UserPromptSubmit`,`timeout` 建议 10)
⛔ 绝不要给本族脚本加 `-E`(会屏蔽 PYTHONUTF8 ⇒ cp936 ⇒ 含 `⛔` 的 payload 静默炸)。
急停:env `DSH_REPLY_GUARD_OFF=1` 或 `<工作区>/.workbuddy/reply-guard.disabled`。
退出码:始终 0;决策通过 stdout 的 JSON 表达。
"""
import io
import json
import os
import sys
import time
# 🔴 2026-10-02:环境定位/体检收敛到 `_env`(**同目录优先,其次技能包 scripts/**)——
# ⛔ 必须在 `_norm`/`_skills_root` 定义**之前**导入(它被下面的函数体引用)。
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
import _env
except Exception: # 🔴 找不到就报错(⛔ 不静默,见 _env 硬规矩②)
sys.stderr.write('[env] 无法导入 _env.py(%s)⇒ 技能库定位不可信\n'
% os.path.dirname(os.path.abspath(__file__)))
raise
# 🔴 2026-10-06:第③级兜底原指向**另一个技能**(agent-operating-rules)⇒ 与「本包自包含」矛盾,
# 且权威已定为包内 03 号 ⇒ 这里指回包内;包内路径由第②级处理,本行只是 env 覆盖时的兜底。
CORE_REL = os.path.join('session-mechanism', 'references', '03-回复排版-核心块.md')
BEGIN = '<!-- REPLY-CORE:BEGIN'
END = '<!-- REPLY-CORE:END -->'
MAX_CHARS = 1400
LOG_REL = os.path.join('.workbuddy', 'reply-style-guard.log')
DISABLE_REL = os.path.join('.workbuddy', 'reply-guard.disabled')
def _read_stdin():
try:
raw = sys.stdin.buffer.read()
except Exception:
return {}
if not raw:
return {}
for enc in ('utf-8', 'utf-8-sig', 'gbk'):
try:
return json.loads(raw.decode(enc))
except Exception:
continue
return {}
def _emit(obj):
try:
sys.stdout.buffer.write(json.dumps(obj, ensure_ascii=False).encode('utf-8'))
sys.stdout.buffer.flush()
except Exception:
pass
def _norm(p):
"""MSYS 风格 `/e/foo` → `E:/foo`(Windows 原生 python 不认前者)。"""
s = str(p or '')
if len(s) > 2 and s[0] == '/' and s[2:3] == '/':
return s[1].upper() + ':' + s[2:]
return s
def _skills_root():
"""技能库根 —— 🔴 **2026-10-02 改为共用 `_env`**(原实现是本起事故的根)。
🔴 原实现的病:`沿 __file__ 上溯失败 ⇒ return os.path.expanduser('~/.workbuddy/skills')`
而 **Windows 上 `~` 不是真配置目录**(本机真值在 E 盘 `CODEBUDDY_CONFIG_DIR`)
⇒ 本文件一旦被注册到「够不到 skills/ 的位置」(实测:文档库 D 盘副本),
就定位到C 盘一个不存在的目录 ⇒ **静默零输出**(日志只有 `core=0 字符`)
⇒ **「机制坏了」与「没配规则」表现完全一样**(真实代价:另一工作区为此绕了两轮)。
✅ 新判据见 `_env` 模块三条硬规矩:顺序固定、**找不到就报错不静默**、体检与标记分离。
"""
return _env.skills_root()
def _extract(text):
i = text.find(BEGIN)
if i < 0:
return ''
i = text.find('\n', i)
j = text.find(END, i + 1)
if i < 0 or j < 0:
return ''
body = text[i:j].strip()
if len(body) > MAX_CHARS:
body = body[:MAX_CHARS].rstrip() + '\n…(超长已截断,全文见本包 `references/03-回复排版-核心块.md`)'
return body
def _read(p):
try:
with io.open(p, encoding='utf-8', newline='') as f:
return f.read()
except Exception:
return ''
def _core(workdir):
"""三级回退:① 本环境规则文件里的落地副本 ② **本包内联件** ③ 外部技能里的权威件。
🔴🔴 **2026-10-04 单包自包含改造**(用户定案:复制**一个**技能到别的机器,这些功能都要能用):
原来第② 级直接指向**另一个技能** `agent-operating-rules/references/回复排版-核心块.md`
⇒ 只装本包时它找不到(实测降级成`core=0 字符`,日志里看不出是"机制坏"还是"没配规则")
⇒ 现在**把那份核心块内联进本包** `references/03-回复排版-核心块.md`(**内容守恒,逐字搬**)
⇒ 优先读**包内**;包内没有才回退外部那份(保留与 `agent-operating-rules` 的一致性)。
⚠️ 顺序⛔ 不许换:包内优先 ⇒ 只拷本包也能用;外部优先 ⇒ 又变成"看别人脸色"。
"""
for p in (os.path.join(workdir, 'CODEBUDDY.md'),
os.path.join(workdir, '.codebuddy', 'CODEBUDDY.md'),
os.path.join(workdir, 'AGENTS.md')):
c = _extract(_read(p))
if c:
return c, p
# ② 本包内联件(⛔ 单包自包含的主力路径)
# ⚠️⚠️ 上溯**两级**到包根:`__file__` = `<包>/scripts/hooks/reply-style-guard.py`
# ⇒ `dirname` ①= hooks/,②= scripts/,③= **包根**。
# 🔴 2026-10-04 首次改造时只上溯了两级(落到 `scripts/`)⇒ 找不到本档 ⇒ 静默零输出
# (实测:干净工作区里 hook 输出空 JSON,而日志里看不出是"路径算错")。
# ⇒ 判据:**必须 `isfile()` 验到文件**,⛔ 不许"算出来就信"。
_hooks = os.path.dirname(os.path.abspath(__file__))
_pkg = os.path.dirname(os.path.dirname(_hooks)) # scripts/hooks → scripts → 包根
p = os.path.join(_pkg, 'references', '03-回复排版-核心块.md')
c = _extract(_read(p))
if c:
return c, p
# ③ 外部技能那份(⛔ 最后兜底;也兼容"规则块单独放在技能库里"的旧布局)
p = os.environ.get('DSH_REPLY_CORE') or os.path.join(_skills_root(), CORE_REL)
return _extract(_read(p)), p
def _log(workdir, line):
try:
with io.open(os.path.join(workdir, LOG_REL), 'a', encoding='utf-8', newline='\n') as f:
f.write('%s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), line))
except Exception:
pass
def main():
if os.environ.get('DSH_REPLY_GUARD_OFF') == '1':
return
obj = _read_stdin()
if not obj:
return
workdir = ''
for k in ('cwd', 'project_dir', 'workspace', 'projectDir'):
if isinstance(obj.get(k), str) and obj[k]:
workdir = _norm(obj[k])
break
if not workdir or not os.path.isdir(workdir):
return
if os.path.exists(os.path.join(workdir, DISABLE_REL)):
return
sid = str(obj.get('session_id') or obj.get('sessionId') or 'unknown')
# 🔴 2026-10-02 **先查环境**(用户定案「技能使用时先检查环境配置」)——
# ⛔ 定位失败时**必须报错 + 非零退出**,⛔ 不许再「静默零输出」。
# 为什么:静默兜底把「机制坏了」和「没配规则」压成同一表象,日志只留一行 core=0。
if not _env.skills_root():
msg = ('[env] 技能库根定位失败(sid=%s)⇒ 本钩子无法定位权威规则块。\n'
' ✅ 修法:设 DSH_SKILLS_ROOT,或跑 `python scripts/hooks/_env.py --stamp` 体检。'
% sid[:8])
_log(workdir, 'ENV-FAIL sid=%s|技能库根定位失败' % sid[:8])
try:
sys.stderr.write(msg)
sys.stderr.flush()
except Exception:
pass
raise SystemExit(3) # 🔴 非零 ⇒ 宿主侧可见(⛔ 不再静默)
core, src = _core(workdir)
# ★ 入口即留痕:命中与否都要能回答"它到底有没有被宿主调用"
_log(workdir, 'entry|sid=%s|core=%d 字符|源=%s' % (sid[:8], len(core), src))
if not core:
# 🔴 区分两种「空」:环境坏了(该报)vs 规则块真的没配(正常,静默)
_log(workdir, 'EMPTY sid=%s|技能库根=%s|⚠️环境正常但规则块读不到'
% (sid[:8], _env.skills_root()))
return
ctx = ('【回复排版闸门 · 强遵循】本机已把「回复形态」定为**强遵循**规则(源:%s)\n'
% os.path.basename(src).replace('.md', '') + core +
'\n(以上每条都是硬约束;发出前过一遍。⛔ 表格/长散文/碎标签堆叠三者一律不许出现。)')
_emit({'hookSpecificOutput': {
'hookEventName': 'UserPromptSubmit',
'additionalContext': ctx,
}})
_log(workdir, 'HIT sid=%s|%d 字节' % (sid[:8], len(ctx.encode('utf-8'))))
if __name__ == '__main__':
try:
main()
except Exception:
pass # fail-open:钩子绝不因自己出错而挡人
sys.exit(0)