Files
workbuddy_skills/session-mechanism/scripts/lock/preflight-lock.sh
T
admin 9b839e7a02 session-mechanism:决策判据补每轮通路 + preflight-lock 补回落修复
用户问:「为什么决策方法 每次都要说一遍才会加载,是否需要把它纳入 hook」

一、根因(实测,⛔ 不是猜)
`decision-rules-hook.py` 早就存在、也早就接线(SessionStart,install.py 声明表第 11 条),
但它**只挂 SessionStart** ⇒ 会话开头注入一次,而**长会话被压缩后那段会一起丢掉**。
本机实测:一个由压缩接续而来的会话,上下文里**没有**那段注入。
用户的会话基本都很长 ⇒ 每次都丢 ⇒ 外在就是「不点名就不加载」,
而 `skill-load-guard.py` 只在**点名时**才提醒 ⇒ 形成「必须说一遍」的循环。

二、修法(零新增进程)
`decision-rules-hook.py` 增加 `UserPromptSubmit` 分支,注入极短版 `RULES_BRIEF`(四行);
由 `prompt-guards.py`(UserPromptSubmit 的多守卫合并入口)把本文件作为**第 5 个 guard**
合进同一进程调用 —— 实测 `decision=0.002 s`,⛔ 不新增 Python 冷启动
(该入口存在的理由正是"别再往 UserPromptSubmit 上串新进程")。
全量仍在 SessionStart 那份;每轮只补"最常被违反的四条"。

三、preflight-lock.sh 补回落修复(22799fc 漏了它)
`DOCS_ROOT` 的回落原与 `handoff-guard.sh` 同款(`$_SM_HERE/..` = 回落进技能包 `scripts/`)
⇒ 现改为「取不到就如实报错 + 留痕 + 退出码 2」。
⚠️ `handoff-guard.sh` / `lock-guard-hook.py` / `collabd.py` / `install.py` 四处已由 22799fc 落地。

验收:
· 语法:两个 .py 走 ast.parse、两个 .sh 走 bash -n(PortableGit 1.3.0)—— 全过
· 实跑 `prompt-guards.py`(UserPromptSubmit 载荷):注入 2387 字符,含「决策判据 / 技术实现自己定 /
  只提报用户三类 / 回复排版闸门」四项;各 guard 耗时 0.002–0.013 s
· 实跑 `handoff-guard.sh --status` ⇒ `root=D:/github/dsh_shenxian/dsh-server-docs`(正确落文档库,⛔ 不是技能包)
· 实跑 `collabd.py --domain-status` ⇒ 正常列出在册域锁(读侧已能读到锁)
· 全量自测 PASS 110 / FAIL 0
2026-10-15 22:44:59 +08:00

170 lines
9.3 KiB
Bash
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 bash
# preflight-lock.sh — 【开工门禁】「先判定可锁定范围,确认锁定后再处理」
#
# 用户口径(2026-09-22):
# 「会话执行任务前先判断,当前任务涉及范围是否都可锁定,确认锁定后开始处理」
#
# 与 handoff-guard.sh 的关系:**门禁(本脚本,先判定)→ 抢锁(handoff-guard.sh,后动手)**。
# ⛔ 本脚本**不建任何锁**,只做判定与提示;它**不替换** handoff-guard.sh。
#
# 在 CODEBUDDY.md §6(并发纪律)里是**开工第 0 步**:判定全部可锁 ⇒ 才去 --claim-exec。
#
# 用法:
# bash preflight-lock.sh "<会话名>" <目标文件1> [目标文件2 ...]
#
# 退出码:0 = 全部可锁且已锁(可以开工)|1 = 存在不可锁项(停手)|2 = 用法错误
#
set -uo pipefail
ME="${1:-}"; shift || true
if [ -z "$ME" ] || [ $# -eq 0 ]; then
echo "用法:bash preflight-lock.sh \"<会话名>\" <目标文件...>" >&2
exit 2
fi
# [session-mechanism] roots.env 外置(由 install.py 生成 ⇒ `<包>/env/roots.env`;兼容包根旧位置)
_SM_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_SM_PKG="$(cd "$_SM_HERE/../.." && pwd)"
if [ -f "$_SM_PKG/env/roots.env" ]; then . "$_SM_PKG/env/roots.env"
elif [ -f "$_SM_PKG/roots.env" ]; then . "$_SM_PKG/roots.env"; fi
# 🔴 2026-10-15 修(第 11 条禁令 + D 盘实测事故):原写
# `DOCS_ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}"` —— 回落值是
# `$_SM_HERE/..` = **本技能包自己的 `scripts/`** ⇒ 锁根落进技能包。
# ✅ 现行:取不到 ⇒ **如实报错 + 留痕 + 退出码 2**,⛔ 不回落到包内、⛔ 不猜路径。
if [ -z "${DSH_DOCS_ROOT:-}" ]; then
_SM_ERR="${_SM_PKG}/logs/preflight-lock.err.log"
mkdir -p "$(dirname "$_SM_ERR")" 2>/dev/null || true
printf '%s ⛔ 取不到 DSH_DOCS_ROOT(本包 env/roots.env 缺该键)⇒ 退出码 2\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" >> "$_SM_ERR" 2>/dev/null || true
printf '⛔ 取不到文档库根(DSH_DOCS_ROOT)—— 锁根无法定位 ⇒ 停手(退出码 2)。\n' >&2
printf ' ✅ 修法:把本工作区根写进 <技能包>/env/roots.env 的 DSH_DOCS_ROOT,或跑 install.py --apply。\n' >&2
exit 2
fi
DOCS_ROOT="$DSH_DOCS_ROOT"
CODE_ROOT="${DSH_CODE_REPO:-${DOCS_ROOT%/*}}" # ⛔ 不留盘符:代码库=文档库的上一级(纯 bash 展开,⛔ 不调 dirname)
WS_ROOT="${DSH_WS_ROOT:-$(pwd)}" # ⛔ 不留盘符:默认=当前工作目录(本文件内未被使用)
LOCKROOT="$DOCS_ROOT/05-交接单/.locks"
# ── 机制层(⛔ 不受 architecture.md 的 src/** 分层管辖,但**全平台共用**)────────
# 判据:这些文件任一改动都会改变**所有会话**的行为 ⇒ 与层④同性质(全局影响)。
# 2026-09-22 实测踩到:首版门禁把本任务自己的 4 个目标文件全判进【D】未归类 ⇒ 误报。
declare -a MECHANISM_RE=(
'^07-scripts/handoff-guard\.sh$' # 全局锁本体
'^07-scripts/preflight-lock\.sh$' # 本门禁自身(同属"全平台共用",2026-10-01 补齐——
# ⛔ 漏登时它会把自己判成【D】未归类 ⇒ 任何人改本文件都开不了工)
'^07-scripts/lock-guard-hook\.py$' # 无锁拒写钩子
'^07-scripts/op-lock\.sh$' # 服务器侧操作锁
'^07-scripts/(skill-load-guard|stop-dialog-guard|session-log-guard|bash-output-guard)\.py$'
# ↑ 本行=**全部宿主钩子脚本**。🔴 **新增钩子必须加进本行**,否则本门禁把它判成
# 【D】未归类 ⇒ **直接拒开工**(2026-10-01 实测:`session-log-guard.py` 与
# **既有**的 `bash-output-guard.py` 都踩过;后者自注册起一直缺登)。
# 同步面还有**两处**:宿主 settings.json hooks 段 + 库内 CODEBUDDY.md §A 那行清单。
'^07-scripts/docs-sync-check\.sh$' # 推送前对账(所有会话共用)
'^07-scripts/check-layering\.mjs$' # 分层 ratchet
'^07-scripts/layering-baseline\.json$'
'^state\.py$' # 所有会话的开机第 0 步
'^CODEBUDDY\.md$' # 每会话自动加载的规则
'\.workbuddy/skills/' # 🔴 **技能包全体**(2026-10-01 补登)。判据:任何技能都被
# **任意会话**加载 ⇒ 改它=改全平台行为,与上面几条同性质。
# ⛔ 漏登时会发生什么(本轮实测):技能包**不在**
# `ai1net-dsh-server/`/`dsh-server-docs/` 等工作区根下 ⇒
# 下面的"剥根前缀"全剥不掉 ⇒ 落【D】未归类 ⇒ 门禁报
# 「先定域(R2)」—— **提示词本身是错的**(技能包根本不可域锁,
# 正解是"独占")。⇒ 换个会话改本技能包,一律开不了工。
)
# ── 不可锁资源清单(命中 ⇒ 必须走"秒级独占"或串行排队)──────────────
# 判据来源:architecture.md R1–R4 层号 + docs/tsconfig.json outDir
declare -a NONLOCKABLE_RE=(
'^src/(config|crypto|isolation|index)\.ts$' # 层④ 零业务依赖 · 全层依赖
'^src/db/schema\.ts$' # 迁移号 = 全局单调序列
'^src/web/server\.ts$' # 唯一挂载注册点
'^package\.json$' # 依赖账本单一
'^07-scripts/layering-baseline\.json$' # 分层 ratchet 单一
)
declare -a SHARED_RE=(
'^lib/' # 构建产物(outDir=lib)· 全局独占
'^node_modules/'
)
warn_shared=(); need_skeleton=(); ok_files=(); unknown=(); mechanism=()
for f in "$@"; do
rel="${f//\\//}"
# 剥掉已知根前缀(顺序要紧:先长后短,否则残留中间段 ⇒ 正则永不匹配)
rel="${rel#*ai1net-dsh-server/}"
rel="${rel#*aliyun-dsh-server/}"
rel="${rel#*dsh-server-docs/}"
rel="${rel#*dsh_shenxian/}"
rel="${rel#./}"
matched=0
for re in "${MECHANISM_RE[@]}"; do
if printf '%s' "$rel" | grep -qE "$re"; then mechanism+=("$rel"); matched=1; break; fi
done
[ "$matched" = 1 ] && continue
for re in "${NONLOCKABLE_RE[@]}"; do
if printf '%s' "$rel" | grep -qE "$re"; then need_skeleton+=("$rel"); matched=1; break; fi
done
[ "$matched" = 1 ] && continue
for re in "${SHARED_RE[@]}"; do
if printf '%s' "$rel" | grep -qE "$re"; then warn_shared+=("$rel"); matched=1; break; fi
done
[ "$matched" = 1 ] && continue
if printf '%s' "$rel" | grep -qE '^(src|poc|test|web|docs|scripts|交接单|skills)/'; then
ok_files+=("$rel")
else
unknown+=("$rel")
fi
done
echo "══════════ 开工门禁 · 可锁定范围判定 ══════════"
echo "会话:$ME"
echo
echo "【A】可独立锁定(域锁,全程持有):${#ok_files[@]} 个"
for f in "${ok_files[@]:0:8}"; do echo " ✓ $f"; done
[ ${#ok_files[@]} -gt 8 ] && echo " …还有 $(( ${#ok_files[@]} - 8 )) 个"
echo
echo "【B】不可全程锁定(层④/迁移号/唯一注册点)→ 改走**秒级独占**:${#need_skeleton[@]} 个"
for f in "${need_skeleton[@]}"; do echo " ⚠ $f"; done
[ ${#need_skeleton[@]} -eq 0 ] && echo " (无)"
echo
echo "【E】机制层(⛔ 不受 src 分层管辖,但**全平台共用** ⇒ 与层④同性质):${#mechanism[@]} 个"
for f in "${mechanism[@]}"; do echo " ◆ $f"; done
[ ${#mechanism[@]} -eq 0 ] && echo " (无)"
[ ${#mechanism[@]} -gt 0 ] && echo " ⚠ 改这些 = 改所有会话的行为 ⇒ 必须: ①独占②兼容旧行为③改后跑自证"
echo
echo "【C】共享资源(构建产物 / 依赖树)→ 纳入发布锁:${#warn_shared[@]} 个"
for f in "${warn_shared[@]:0:4}"; do echo " ⚠ $f"; done
[ ${#warn_shared[@]} -eq 0 ] && echo " (无)"
if [ ${#unknown[@]} -gt 0 ]; then
echo
echo "【D】未归类路径(⛔ 必须先想清"属哪个域",R2):${#unknown[@]} 个"
for f in "${unknown[@]:0:6}"; do echo " ? $f"; done
fi
echo
echo "════════════════════════════════════════════"
rc=0
[ ${#unknown[@]} -gt 0 ] && rc=1
[ ${#mechanism[@]} -gt 0 ] && rc=1
if [ "$rc" = 1 ]; then
echo "结论:**暂不可并行开工** ——"
[ ${#unknown[@]} -gt 0 ] && echo " · 【D】${#unknown[@]} 个未归类路径:先定域(R2)"
if [ ${#mechanism[@]} -gt 0 ]; then
echo " · 【E】${#mechanism[@]} 个机制层文件:**全平台共用** ⇒ ⛔ 不得与其他会话并行"
echo " ⇒ 仅当**确认无其他会话在跑**时,才允许独占开工;否则停手等窗口"
fi
exit 1
fi
echo "结论:**可以锁定** ——"
echo " · ${#ok_files[@]} 个走域锁|${#need_skeleton[@]} 个走秒级独占|${#warn_shared[@]} 个并发布锁"
echo " · 下一步:bash handoff-guard.sh --claim-exec \"$ME\" --domains <由 A 组反推的域>"
echo " ⛔ 本脚本是**提示性门禁**:hook 只拦 Write/Edit,Bash 写入不受限(有意安全阀)"
exit 0