起因(2026-10-06 用户问):能自决的问题为什么还要问。
复盘发现注入的判据里**缺动作**:
- 缺「十步」的两个停止点 → ① 判类型(方案请求 vs 直接执行)② 判方向(扩大/收窄/中性);
- 缺「自主裁决顺序」的排序轴 → 更小改动 → 更少新增形态 → **不扩大可见面**。
⇒ 前者导致「会扩大可见面」时不停手;后者导致排完仍把已定项混进待拍板清单(=变相征询)。
本次只补这两条动作骨架(1459 → 1906 字符),⛔ 不塞十步全文(避免稀释)。
205 lines
12 KiB
Python
205 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""decision-rules-hook.py —— `SessionStart` 钩子:**把决策判据常驻注入每一次对话**。
|
||
|
||
## 🔴 为什么要有这个脚本(2026-10-06 用户明令)
|
||
|
||
用户原话:
|
||
|
||
> 「**决策方法 必须想办法 加载到每次对话中**」
|
||
> 「要把这个动作 加到 会话技能配置环境的时候一并处理」
|
||
|
||
**改前的缺口(实测取证)**:
|
||
|
||
· 决策判据(`U1–U28` / `A1–A25`)原本只住在 `skills/dsh-decision/references/` 里;
|
||
· `dsh-decision` 是**独立技能包**,**⛔ 不在** `session-mechanism` 的安装面里;
|
||
· `session-mechanism/install.py` 的声明表**明确写了** `⛔ decision_bridge 不在此表内`
|
||
⇒ **"配置环境"跑一遍,决策判据一个字节都不会进会话**;
|
||
· 结果:判据**写在文件里**,但 **AI 不会在正确的时机取用它**
|
||
(2026-09-16 实测:用户点名「参考决策方法」后,AI 全程 `Skill` 调用 **0 次**)。
|
||
⇒ 一句话:**能力齐了,缺一根接线。** 本脚本就是那根接线。
|
||
|
||
**本次一并补齐的两件事(2026-10-06)**:
|
||
|
||
① **接线**:本脚本 + `install.py` 声明表 + `OWN_BASENAMES` ⇒ 跑一次「配置环境」就带上;
|
||
② **判据实体入包**:本包 `SKILL.md:395` 既定口径是
|
||
「**判据实体就在本档正文里 ⇒ 只复制这一个技能到别的机器,这些功能全部可用,
|
||
⛔ 不依赖任何其他技能**」,但 10-04 那次**只搬了 `01-功能优先协作协议`(问不问)**,
|
||
**决策方法论(U27/A6 那套)从未搬入**(证据:本包正文搜 `U27`/`A6` **零命中**)。
|
||
⇒ 本次逐字搬入 `references/04-决策方法论.md`(+ `dsh-decision-method/` 三份素材库),
|
||
本脚本的指针**指向包内**,⛔ 不再指向 `dsh-decision`。
|
||
|
||
## 为什么挂 `SessionStart` 而不是 `UserPromptSubmit`
|
||
|
||
· `SessionStart` **每开一个会话注入一次** ⇒ 规矩"从会话开头就在",
|
||
且**每轮不重复注入**(不白烧 token);
|
||
· `UserPromptSubmit` 每轮都跑 ⇒ 同一段文字被灌 N 遍,开销大且会稀释注意力。
|
||
· ⚠️ 与既有 `decision_bridge.py`(也挂 SessionStart)**职责不重叠**:
|
||
那个注入的是**《提问规范》骨架**("要问的时候怎么写");
|
||
本脚本注入的是**决策判据**("怎么想、怎么定、什么不许")。
|
||
⇒ 两者**互补**,⛔ 不合并(合并会让任一方改判据时动到另一方)。
|
||
|
||
## 注入什么(用户 2026-10-06 拍板:**判据式精简版**)
|
||
|
||
⛔ 不注入全文(那是几百 KB);✅ 只注入**能被违反、且违反就是事故**的硬判据,
|
||
按"**一句话能判**"重写。完整判据仍以 `dsh-decision/references/` 为正本,本文只放指针。
|
||
|
||
## 三条不可破的性质(同 `decision_bridge.py` 的既定口径)
|
||
|
||
1. **不介入会话** —— 只返回 `additionalContext`;⛔ 不返回 `permissionDecision`、
|
||
⛔ 不改 `updatedInput`、⛔ 不返回 `continue:false`。
|
||
2. **不拖慢会话** —— **纯本地字符串**,不读文件、不调模型、不起进程。
|
||
3. **fail-open** —— 任何异常都返回空(⛔ 不抛)。钩子失败 ⇔ 用户的话被吞,是最坏结果。
|
||
⚠️ 但**留痕**:没留痕时"钩子没被调用"与"调用了但静默失败"无法区分。
|
||
|
||
## ⚠️ 改本文件的头号坑(2026-10-06 实际踩到)
|
||
|
||
`RULES` 是**用半角双引号包起来的中文长串**。正文里的**引号必须用全角 `“` `”`**,
|
||
⛔ **绝不能写半角 `"`** —— 写半角会把字符串**提前闭合**,后面剩余的中文
|
||
就变成了"串外的裸标识符",直接 `SyntaxError`(实测报
|
||
`invalid character '/' (U+FF0F)`),**整个钩子加载失败**。
|
||
⇒ 改完**必须**跑一次:`python -c "import ast;ast.parse(open(p,encoding='utf-8').read())"`。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import io
|
||
import json
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
# ── 留痕(钩子唯一的取证出口)──
|
||
_LOG = Path(__file__).resolve().parent.parent.parent / "logs" / "_decision-rules-hook.log"
|
||
|
||
|
||
def _log(line: str) -> None:
|
||
try:
|
||
_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||
with io.open(_LOG, "a", encoding="utf-8", newline="\n") as f:
|
||
f.write("[%s] %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), line))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
|
||
def _emit(obj: dict) -> None:
|
||
"""🔴 必须走 buffer 显式 UTF-8 —— 正文含中文与 `⛔`,文本模式在 cp936 下会炸。"""
|
||
try:
|
||
sys.stdout.buffer.write(json.dumps(obj, ensure_ascii=False).encode("utf-8"))
|
||
sys.stdout.buffer.flush()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
|
||
def _read_stdin_text() -> str:
|
||
try:
|
||
raw = sys.stdin.buffer.read()
|
||
except Exception: # noqa: BLE001
|
||
return ""
|
||
return raw.decode("utf-8", errors="replace")
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# 注入正文 —— **判据式精简版**
|
||
#
|
||
# 🔴 编写口径(⛔ 改动前先读):
|
||
# · 每条都必须"**能被违反**" —— 那种"本来就会做"的常识不要写(写了只会稀释注意力);
|
||
# · 每条都带**可判的触发条件**("遇到 X 时"),⛔ 不写成抽象口号;
|
||
# · 编号 `U27`/`A6` 是**正本的编号**,保留它 ⇒ 便于回查原文与引用;
|
||
# · 全文以 `dsh-decision/references/` 为正本,本文**只放浓缩版 + 指针**;
|
||
# · 🔴 **正文内引号一律用全角 `“` `”`**(半角会闭合字符串 ⇒ SyntaxError,见文件头)。
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
RULES = (
|
||
"【决策判据 · 常驻(违反即事故;正本 references/04-决策方法论.md)】\n"
|
||
"遇到“要不要做/怎么做/能不能先凑合”这类判断,先过下面这几条:\n"
|
||
"\n"
|
||
"🔴 **目标不打折,路径取最小代价**(U27 + A6)\n"
|
||
" 发现问题默认目标是**解决**。下面三种**都不算解决**,⛔ 不许当成交付:\n"
|
||
" ① **降级目标**(把“要做到 A”悄悄改成“做到 A′ 也行”)\n"
|
||
" ② **延期**(“下次顺手再说 / 等窗口再补”)\n"
|
||
" ③ **静默兜底**(“先这样也能跑”,而风险与触发条件一个字没写)\n"
|
||
" ⚠️ 唯一允许“暂时接受”=**客观不可逾越**,且必须写清三项:\n"
|
||
" 卡在哪(证据)/当前已做到哪一步/什么条件一出现就必须回头解决。\n"
|
||
" ⚠️ A6 的“最小代价”只约束**路径**,⛔ 不许读成“可以降低目标”。\n"
|
||
"\n"
|
||
"🔴 **先取证,再结论**(A1)\n"
|
||
" ⛔ 不许拿文档/记忆/推断当既成事实。层级:`L1 文档说` < `L2 文件在` < `L3 本机实测`\n"
|
||
" < `L4 真机/生产` < `L5 用户原话`;**冲突以高层级为准**,⛔ 不许用低层级否定高层级。\n"
|
||
" ⛔ **不许把“我猜的”写成“事实”** —— 尤其“疑似是我刚才改坏的”:先 diff 备份再说。\n"
|
||
"\n"
|
||
"🔴 **不懂就问,但先自己查到位**(A2/A3 + 功能优先协议)\n"
|
||
" · **技术实现**(框架/库/文件组织/命名/测试/性能/部署/目录结构/错误处理)\n"
|
||
" ⇒ **自己定**,作为陈述句写进回复(“我选了什么,可推翻”),⛔ 不做成选项让用户选。\n"
|
||
" · **只准提报用户三类**:① 功能语义分叉(用户能感知的差别)② 红线门禁\n"
|
||
" ③ 超出决策方法边界。**必须问** ≠ 把技术方案捆进去问。\n"
|
||
" · 提报时把技术话翻成**功能话**(“影响谁、断多久、花多少钱”),\n"
|
||
" ⛔ 不写包名/环境变量/文件路径/代码标识符。一轮只问一个问题。\n"
|
||
"\n"
|
||
"🔴 **两个停止点:没结论就不要往下走**(十步里的第 1、4 步)\n"
|
||
" ① **判类型**:这是「方案请求」还是「任务明确·直接执行」?\n"
|
||
" 方案请求 ⇒ **只出方案**,未经明确授权 ⛔ 不改任何文件。\n"
|
||
" ② **判方向**:这次改动是 **扩大 / 收窄 / 中性**?\n"
|
||
" 🔴 **只要会「扩大可见面」⇒ 立即停手**,并把「要不要扩大」**单独**作为红线问题问,\n"
|
||
" ⛔ **不许**把它和某个技术方案**捆成一条**问(捆包=逼用户在没看到技术选项时先答红线)。\n"
|
||
"\n"
|
||
"🔴 **技术方案怎么排序:自己排,⛔ 不问用户**(`dsh-decision` 默认裁决顺序)\n"
|
||
" 只需比三条轴,**按序**取先满足的那条:\n"
|
||
" ① **更小改动** → ② **更少新增形态** → ③ **不扩大可见面**。\n"
|
||
" 排完后:**已定的写成陈述句**(「我选了什么,可推翻」),\n"
|
||
" **只把真未定的**列进待拍板项 —— ⛔ 已定项混进待拍板清单 = 变相征询。\n"
|
||
"\n"
|
||
"🔴 **删/改/迁移先判代价对称性**(A8/A14/A16/A22)\n"
|
||
" 删除收益 < 潜在破坏 ⇒ **标注废弃保留**,⛔ 不删。改名/迁移先列**伴随物清单**\n"
|
||
" (只改主体必留隐患)。替换/退役:**先补位,再退役**。\n"
|
||
" ⛔ 删任何东西之前先扫引用(“看起来像资料” ≠ “没被引用”)。\n"
|
||
"\n"
|
||
"🔴 **本机改完 ≠ 交付**(A25/A19/A18)\n"
|
||
" 宣布完成前先画出**改动层 → 生效链路**:改了哪个文件、谁读它、什么时候生效。\n"
|
||
" ⛔ **静默失败会伪造结论**(工具静默 + 降级静默,两头都要防)。\n"
|
||
" ⛔ **只看回显不算验收** —— 落到**进程级/文件级证据**(pid / argv0 / 心跳 / 增量字节)。\n"
|
||
"\n"
|
||
"🔴 **不确定就说不确定**(A11/A15/A10)\n"
|
||
" 未验证的能力显式抛 `unsupported`,⛔ 不假装支持;写状态必须带**三态 + 级别**,\n"
|
||
" ⛔ 不用“支持/可用”描述没验过的项;失败面**必须留证据**,⛔ 不吞错误。\n"
|
||
"\n"
|
||
"📂 完整判据(U1–U28 用户决策 · A1–A25 AI 推理 · X1–X13 反例)\n"
|
||
" ⇒ 本技能包内 `references/04-决策方法论.md`(+ `dsh-decision-method/` 三份素材库);\n"
|
||
" 决策与“问不问”的完整裁决 ⇒ 本包 `references/02-功能优先协作协议.md`。\n"
|
||
)
|
||
|
||
|
||
def main() -> int:
|
||
raw = _read_stdin_text()
|
||
try:
|
||
payload = json.loads(raw) if raw.strip() else {}
|
||
except Exception: # noqa: BLE001
|
||
payload = {}
|
||
if not isinstance(payload, dict):
|
||
payload = {}
|
||
|
||
event = str(payload.get("hook_event_name") or payload.get("hookEventName") or "")
|
||
session_id = str(payload.get("session_id") or "")
|
||
|
||
# 🔴 只认 SessionStart。别的档位走到这里 ⇒ 直接合法地什么都不做
|
||
# (本脚本会被挂到哪个档由 `install.py` 的声明表决定;⛔ 不在这里猜)。
|
||
if event != "SessionStart":
|
||
_log("skip event=%s(本钩子只处理 SessionStart)" % (event or "-"))
|
||
return 0
|
||
|
||
_log("inject session_id=%s len=%d" % (session_id or "-", len(RULES)))
|
||
_emit({"hookSpecificOutput": {
|
||
"hookEventName": "SessionStart",
|
||
"additionalContext": RULES,
|
||
}})
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
sys.exit(main())
|
||
except Exception as e: # noqa: BLE001
|
||
# 🔴 fail-open:钩子炸了 ⛔ 不许影响会话(但必须留痕)。
|
||
try:
|
||
_log("FATAL %r" % (e,))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
sys.exit(0)
|