chore(session-mechanism): 交接单目录改名(去 05- 前缀)+图模式触发词改为「图完成目标」

This commit is contained in:
admin committed 2026-10-20 15:49:38 +08:00
1 parent ce05207aa2
commit 5c862e4005
15 files changed
+62 -62

No files matched your search

+4 -4
View File
@@ -3941,7 +3941,7 @@ def active_schedule_exists(name: str) -> bool:
# handoff-guard.sh : dsh-server-docs ai1net-dsh-server aliyun-dsh-server src poc web test
# docs scripts skills 交接单
# lock-guard-hook.py: dsh-server-docs ai1net-dsh-server aliyun-dsh-server src poc web test
# docs 07-scripts 08-skills 05-交接单
# docs 07-scripts 08-skills 交接单
# 差异:`scripts`/`skills` vs `07-scripts`/`08-skills`。⇒ 命中「第一个锚点段」时,
# **工作区下的路径两边结果一致**(都先命中工作区名)⇒ 本节的实际判据不受影响;
# ⚠️ 但**代码仓根目录下的路径**(无工作区名那段)两边会算出不同键 ⇒ 已存在假绿风险。
@@ -3951,7 +3951,7 @@ def active_schedule_exists(name: str) -> bool:
_DS_ANCHORS = ("dsh-server-docs", "ai1net-dsh-server", "aliyun-dsh-server",
"src", "poc", "web", "test", "docs", "scripts", "skills", "交接单")
# ⚠️ 另一份(`lock-guard-hook.py:83 _DOMAIN_SEGS`)—— 🔴 2026-10-03 10:0x **已对齐**:
# 原值是 `07-scripts/08-skills/05-交接单`(带数字前缀的**旧仓目录名**)⇒ 与本表不同
# 原值是 `07-scripts/08-skills/交接单`(带数字前缀的**旧仓目录名**)⇒ 与本表不同
# ⇒ 那个文件里的 `domain_key()` 会跟抢锁侧算出**不同域键** ⇒ 域锁静默失效(假绿)。
# ✅ 已改成 `scripts/skills/交接单`,与 `_DS_ANCHORS` 及 shell 侧**三处逐字一致**。
# ⛔ 本变量**不参与任何计算**,只保留作"另一侧读到什么"的对照;自检会逐字比三处。
@@ -4019,7 +4019,7 @@ def ds_active_domain_locks() -> dict:
"""
out = {}
_me = str(os.environ.get("DSH_SESSION_NAME") or "").strip()
# 🔴🔴 锁根=文档库的 `05-交接单/.locks`。**键名必须与写侧逐字同源:`DSH_DOCS_ROOT`**
# 🔴🔴 锁根=文档库的 `交接单/.locks`。**键名必须与写侧逐字同源:`DSH_DOCS_ROOT`**
# (写侧六处:`lock/handoff-guard.sh:46`/`lock/preflight-lock.sh:30`/`lock/handoff-status.py:40`/
# `lock/orphan-lock.py:203`/`hooks/lock-guard-hook.py:74`/`session-rules-check.py:107`)
# 2026-10-15 修:原读 `DSH_LOCKROOT`/`DSH_DOCS_REPO` —— 这两个键**全包只此一处**读,
@@ -4030,7 +4030,7 @@ def ds_active_domain_locks() -> dict:
_cands.append(os.environ["DSH_LOCKROOT"])
_dr = (os.environ.get("DSH_DOCS_ROOT") or "").strip() # ← 与写侧同源
if _dr:
_cands.append(os.path.join(_dr, "05-交接单", ".locks"))
_cands.append(os.path.join(_dr, "交接单", ".locks"))
root = next((c for c in _cands if os.path.isdir(c)), "")
if not root:
# ⛔ 不再回落到"猜的盘符"(第 11 条禁令):如实报错 + 留痕。
@@ -6,7 +6,7 @@ lock-guard-hook.py — 「无锁不许改库」的强制钩子(WorkBuddy PreTo
────────────
2026-09-12 实证:`handoff-guard.sh` 输出「无全局锁」被会话读成「环境干净,可以开工」
(正确读法是「你快去抢锁」),结果**两个会话同时改了本库**。措辞已在 guard 与
`05-交接单/README.md` 里补正,但**约定拦不住不看的人** —— 本脚本是强制层。
`交接单/README.md` 里补正,但**约定拦不住不看的人** —— 本脚本是强制层。
作用域(**刻意收窄:对其他项目零影响**)
────────────
@@ -17,7 +17,7 @@ lock-guard-hook.py — 「无锁不许改库」的强制钩子(WorkBuddy PreTo
判据
────
`<文档库根>/05-交接单/.exec-lock` 存在 = 有人持锁 → 放行(归属由台账 OWNER 承担);
`<文档库根>/交接单/.exec-lock` 存在 = 有人持锁 → 放行(归属由台账 OWNER 承担);
不存在 = **deny**,并把可直接粘贴的抢锁命令回给 Agent。
为什么**不拦 Bash**
@@ -91,9 +91,9 @@ if not DOCS_ROOT:
sys.exit(0)
# ⛔ 不留盘符:代码库=文档库的上一级(`<repo>/dsh-server-docs` 的约定)
CODE_REPO = os.environ.get("DSH_CODE_REPO") or (os.path.dirname(DOCS_ROOT) if DOCS_ROOT else "")
LOCK_DIR = os.path.join(DOCS_ROOT, "05-交接单", ".exec-lock")
LOCK_DIR = os.path.join(DOCS_ROOT, "交接单", ".exec-lock")
OWNER_FILE = os.path.join(LOCK_DIR, "OWNER")
LOCKS_ROOT = os.path.join(DOCS_ROOT, "05-交接单", ".locks")
LOCKS_ROOT = os.path.join(DOCS_ROOT, "交接单", ".locks")
GUARD_CMD = 'bash 07-scripts/handoff-guard.sh --claim-exec "<你的会话名>"'
GUARD_DOMAIN_CMD = 'bash 07-scripts/handoff-guard.sh --claim-exec "<你的会话名>" --domains <域...>'
@@ -101,7 +101,7 @@ GUARD_DOMAIN_CMD = 'bash 07-scripts/handoff-guard.sh --claim-exec "<你的会话
# ⚠️ 顺序有意义:**先长后短**,与 handoff-guard.sh 的 _ANCHOR_SEGS 逐字一致。
# 两侧只要顺序或集合不同 ⇒ 同一文件算出不同域键 ⇒ 域锁静默失效(假绿)。
# 🔴🔴 2026-10-03 10:0x **修一处真缺陷(不是文档漂移)**:本行原为
# `… "docs", "07-scripts", "08-skills", "05-交接单"`,
# `… "docs", "07-scripts", "08-skills", "交接单"`,
# 而真源 `handoff-guard.sh:66 _ANCHOR_SEGS` 是 `… "docs", "scripts", "skills", "交接单"`。
# ⚠️ 后果不是"看起来不一致":`domain_key()` **拿本表参与实际计算** ⇒ 同一个文件
# 在 hook 侧与抢锁侧会算出**不同域键** ⇒ 两边各抢各的 ⇒ **域锁等于没有(假绿)**。
@@ -339,7 +339,7 @@ def main() -> None:
who = lock_owner()
msg = f"🔐 全局执行锁【已被占用】:{who} —— 但**域锁可绕开它**:不重叠的域仍能并行。"
hook_log("SessionStart", f"旧全局锁被占 owner={who} source={src}")
elif os.path.isdir(DOCKS := os.path.join(DOCS_ROOT, "05-交接单", ".locks")) and os.listdir(DOCKS):
elif os.path.isdir(DOCKS := os.path.join(DOCS_ROOT, "交接单", ".locks")) and os.listdir(DOCKS):
msg = "🔐 已有域锁在跑 —— 开工前先抢你要的域(不重叠即可并行):" + GUARD_DOMAIN_CMD
hook_log("SessionStart", f"有域锁无全局锁 source={src}")
elif os.path.isdir(DOCS_ROOT):
@@ -160,11 +160,11 @@ TRIGGERS = (
# 🔴 2026-10-19 用户令(逐字「把skill 改成 完成目标也能触发」):光写「项目机制完成目标」不够 ——
# 用户口语里会把机制名字省掉,只说「完成目标」;词表里必须有**裸「完成目标」**这一条,
# 否则那句同样是派活授权,却一条都命不中。
# 🔴 2026-10-19 用户令(逐字「完成复杂目标 走graph , 完成目标 走 loop」):
# **两条口令分工** —— `完成复杂目标` ⇒ 图模式 graph;`完成目标` ⇒ 现有 loop 机制。
# ⚠️ 两句不是包含关系("完成复杂目标"里没有连续的"完成目标"子串),所以两词都要进表,
# 🔴 2026-10-19 用户令(逐字「图完成目标 走graph , 完成目标 走 loop」):
# **两条口令分工** —— `图完成目标` ⇒ 图模式 graph;`完成目标` ⇒ 现有 loop 机制。
# ⚠️ 两句不是包含关系("图完成目标"里没有连续的"完成目标"子串),所以两词都要进表,
# 分流按"先看复杂"判,⛔ 不许靠子串顺序碰运气。
'完成复杂目标', '复杂目标',
'图完成目标', '复杂目标',
'完成目标',
'使用任务会话', '使用执行会话', '使用协作会话', '任务会话完成', '执行会话完成',
'继续完成目标', '继续目标', '继续完成之前', '继续之前的',
@@ -188,7 +188,7 @@ _LOAD_RULES = ('按规则来', '按规则做', '按作业规则', '遵守规则'
# 命中"使用任务会话完成目标"的人,要的是**会话机制本体**(怎么建排期/怎么派活/
# 自决策白名单在 `references/02-功能优先协作协议.md`),⛔ 不是决策方法论。
_LOAD_SESSION = ('使用项目机制', '项目机制完成', '项目机制',
'完成复杂目标', '复杂目标', '完成目标',
'图完成目标', '复杂目标', '完成目标',
'使用任务会话', '使用执行会话', '使用协作会话', '任务会话完成', '执行会话完成',
'继续完成目标', '继续目标', '继续完成之前', '继续之前的',
'会话技能', '会话机制', '创建任务会话', '建任务会话',
@@ -413,18 +413,18 @@ def main():
hit_rules = [w for w in hits if w in _LOAD_RULES]
if hit_session:
head = '检测到用户本轮**点名了会话机制 / 派活**(命中:%s)。' % '、'.join(hit_session)
# 🔴 2026-10-19 用户令(逐字「完成复杂目标 走graph , 完成目标 走 loop」):
# 两条口令分流。**先判"复杂"**("完成复杂目标"里没有连续的"完成目标"子串,但仍按先后判更稳)。
# 🔴 2026-10-19 用户令(逐字「图完成目标 走graph , 完成目标 走 loop」):
# 两条口令分流。**先判"复杂"**("图完成目标"里没有连续的"完成目标"子串,但仍按先后判更稳)。
mode = ''
if '复杂目标' in prompt:
mode = ('🔴 **本轮走 graph(图模式)**:用户说「完成复杂目标」⇒ 用本包 `graph/graphctl.py`:\n'
if '图完成目标' in prompt or '复杂目标' in prompt:
mode = ('🔴 **本轮走 graph(图模式)**:用户说「图完成目标」⇒ 用本包 `graph/graphctl.py`:\n'
' `init` 建图 → `check` 校验 → `tick`(算完就开)→ 执行方 `report` 回报 → `progress` 看进度;\n'
' 看板 `http://127.0.0.1:20100/` 的「图模式」块会实时显示这张图。\n'
' ⛔ 不走 loop 排期、⛔ 不写 `goal.json`/`tasks.json`;图只负责编排,节点上的活仍由会话去干。\n\n')
elif '完成目标' in prompt:
mode = ('🔴 **本轮走 loop(现有项目机制)**:用户说「完成目标」⇒ 按目标 + 台账 + 队列那套推进'
'(`goalctl`/`collabd`/检查会话)。\n'
' ⛔ 不擅自改走 graph —— 要走图,用户会说「完成复杂目标」。\n\n')
' ⛔ 不擅自改走 graph —— 要走图,用户会说「图完成目标」。\n\n')
body = mode + (
'⛔ **不要凭记忆代替、也不要以"我已经知道这套机制"为由跳过**:\n'
' **先调用 Skill 工具加载 `session-mechanism`**(会话机制 + 多会话执行总入口 · 含**首屏第 0 步加载门槛**'
+17 -17
View File
@@ -5,8 +5,8 @@
# 约定全部依赖“人记得做”。本脚本把判据变成**一条命令 + 退出码**,不靠记忆。
#
# 关键设计:**mtime 只作提示、不作判定**(它分不清“谁改的”)。真正的判定来自四处:
# ① 全局执行锁(05-交接单/.exec-lock)—— **一粗**:同一时刻只允许一个任务会话动「文档/代码/服务器」
# ① 单级占用锁(05-交接单/.doing-<单号>)—— **一细**:这个单归谁做(供台账 / 接管使用)
# ① 全局执行锁(交接单/.exec-lock)—— **一粗**:同一时刻只允许一个任务会话动「文档/代码/服务器」
# ① 单级占用锁(交接单/.doing-<单号>)—— **一细**:这个单归谁做(供台账 / 接管使用)
# ② 越界改动(不在我声明清单里的未提交文件)—— 推送前检查,防“顺手重放别人的半成品”
# ④ 双端一致性(docs-sync-check.sh)—— 推送前检查,防“幽灵文件”(推回了别人已移走的文件)
#
@@ -24,7 +24,7 @@
# # ⛔ 不带名字 ⇒ 拒绝释放(R9,2026-09-25 修)
# # 域锁侧:--claim-exec <会话> --domains <域…> / 同姿势 --release-exec <会话>
# bash 07-scripts/handoff-guard.sh # 看全局状态(信息模式)
# ME="exec-session-B" MINE="05-交接单/T03-*.md" bash 07-scripts/handoff-guard.sh T03 # 开工检查(严格)
# ME="exec-session-B" MINE="交接单/T03-*.md" bash 07-scripts/handoff-guard.sh T03 # 开工检查(严格)
# ME="exec-session-B" MINE="..." PUSH=1 bash 07-scripts/handoff-guard.sh T03 # 推送前检查
#
# 环境变量:ME(我是谁 —— 用来判断锁是不是自己的;不设则一律按“别人的锁”处理)
@@ -46,7 +46,7 @@ fi
# 🔴 2026-10-15 修(第 11 条禁令 + D 盘实测事故):
# 原写 `ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}"` —— 回落值是
# `$_SM_HERE/..` = **本技能包自己的 `scripts/`** ⇒ 锁根落进技能包
# (实测已在 `<技能包>/scripts/05-交接单/.locks` 长出一把锁,本该落文档库)。
# (实测已在 `<技能包>/scripts/交接单/.locks` 长出一把锁,本该落文档库)。
# ✅ 现行:取不到 ⇒ **如实报错 + 留痕 + 退出码 2**,⛔ 不回落到包内、⛔ 不猜路径。
if [ -z "${DSH_DOCS_ROOT:-}" ]; then
_SM_ERR="${_SM_PKG}/logs/handoff-guard.err.log"
@@ -59,9 +59,9 @@ if [ -z "${DSH_DOCS_ROOT:-}" ]; then
fi
ROOT="$DSH_DOCS_ROOT"
cd "$ROOT" || exit 2
LOCKDIR="$ROOT/05-交接单/.doing-"
LOCKEXEC="$ROOT/05-交接单/.exec-lock"
MELOCK="$ROOT/05-交接单/.me-lock"
LOCKDIR="$ROOT/交接单/.doing-"
LOCKEXEC="$ROOT/交接单/.exec-lock"
MELOCK="$ROOT/交接单/.me-lock"
ME="${ME:-}"
# ══════════════════════════════════════════════════════════════════════════
@@ -77,7 +77,7 @@ ME="${ME:-}"
#
# 兼容:`.exec-lock` 分支**原样保留** ⇒ 未升级的会话照旧可用(hook 是启动时快照)。
# ══════════════════════════════════════════════════════════════════════════
LOCKSROOT="$ROOT/05-交接单/.locks"
LOCKSROOT="$ROOT/交接单/.locks"
GATELOCK="$LOCKSROOT/.gate" # 秒级临界区:算交集时必须串行,否则两个会话可能同时判定"无冲突"
# 域字符串规范化 ⇒ 域键。**必须与 lock-guard-hook.py 的 domain_key() 同规则**,
@@ -418,15 +418,15 @@ if [ "${1:-}" = "--claim" ]; then
[ -n "$T" ] || { echo "用法:--claim <单号> [占用者]" >&2; exit 2; }
if mkdir "$LOCKDIR$T" 2>/dev/null; then
printf '%s\n' "$OWNER" > "$LOCKDIR$T/OWNER"
echo "✓ 已占位:05-交接单/.doing-$T($OWNER)—— 完工请 --release $T"
echo "✓ 已占位:交接单/.doing-$T($OWNER)—— 完工请 --release $T"
exit 0
fi
echo "✗ 占位失败:05-交接单/.doing-$T 已存在(占用者 $(cat "$LOCKDIR$T/OWNER" 2>/dev/null || echo '?')) → 停手" >&2
echo "✗ 占位失败:交接单/.doing-$T 已存在(占用者 $(cat "$LOCKDIR$T/OWNER" 2>/dev/null || echo '?')) → 停手" >&2
exit 1
fi
if [ "${1:-}" = "--release" ]; then
T="${2:-}"; [ -n "$T" ] || { echo "用法:--release <单号>" >&2; exit 2; }
rm -rf "$LOCKDIR$T" && echo "✓ 已释放:05-交接单/.doing-$T" || { echo "✗ 释放失败" >&2; exit 1; }
rm -rf "$LOCKDIR$T" && echo "✓ 已释放:交接单/.doing-$T" || { echo "✗ 释放失败" >&2; exit 1; }
exit 0
fi
@@ -451,7 +451,7 @@ in_mine() { [ -z "$MINE" ] && return 1; for p in $MINE; do case "$1" in $p) retu
# ── ① 占用锁(硬判定)────────────────────────────────────
echo
echo "【1】占用锁(05-交接单/.doing-*)"
echo "【1】占用锁(交接单/.doing-*)"
shopt -s nullglob
LOCKS=("$LOCKDIR"*)
if [ ${#LOCKS[@]} -eq 0 ]; then
@@ -475,9 +475,9 @@ fi
if [ ${#LOCKS[@]} -gt 0 ]; then
for L in "${LOCKS[@]}"; do
n="$(basename "$L")"; T="${n#.doing-}"
row="$(grep -m1 "^| \`$T-" 05-交接单/README.md 2>/dev/null || true)"
row="$(grep -m1 "^| \`$T-" 交接单/README.md 2>/dev/null || true)"
if [ -z "$row" ]; then
echo " ⚠ $T 有占用锁,但 \`05-交接单/README.md §一\` 里**没有这一行** → 补上(新增单)"
echo " ⚠ $T 有占用锁,但 \`交接单/README.md §一\` 里**没有这一行** → 补上(新增单)"
VERDICT=1; HARD+=("①b $T 有锁但台账缺行")
elif printf '%s' "$row" | grep -qE "执行中|已完成|已归档"; then
echo " · $T 台账已标「执行中/已完成」✓"
@@ -491,7 +491,7 @@ fi
# ── ①c 全局执行锁(2026-09-12 新增:同一时刻只允许一个任务会话)────
echo
echo "【1c】全局执行锁(05-交接单/.exec-lock)"
echo "【1c】全局执行锁(交接单/.exec-lock)"
if [ ! -d "$LOCKEXEC" ]; then
echo " ✓ 无全局锁 —— ⚠️ 这**不是「可以开工」,是「你快去抢」**:"
echo " 开工前必须先:bash 07-scripts/handoff-guard.sh --claim-exec \"<你的会话名>\""
@@ -544,7 +544,7 @@ fi
# ── ② 越界改动(推送前硬判定)────────────────────────────
echo
echo "【2】未提交改动(含未跟踪)"
CHANGED="$(git -c core.quotepath=false status --short 2>/dev/null | awk '{print $NF}' | grep -vE '^05-交接单/\.(doing-|exec-lock)' || true)"
CHANGED="$(git -c core.quotepath=false status --short 2>/dev/null | awk '{print $NF}' | grep -vE '^交接单/\.(doing-|exec-lock)' || true)"
OUTSIDE=(); INSIDE=()
if [ -z "$CHANGED" ]; then
echo " ✓ 工作区干净"
@@ -570,7 +570,7 @@ echo
echo "【3】近 $WINDOW 分钟被改动的文件(**仅提示**:mtime 分不清谁改的)"
HOT="$(find . -type f -mmin "-$WINDOW" \
-not -path './.git/*' -not -path '*/node_modules/*' -not -name '*.bak*' \
-not -path './05-交接单/.doing-*' -not -path './05-交接单/.exec-lock*' 2>/dev/null | sed 's|^\./||' | LC_ALL=C sort)"
-not -path './交接单/.doing-*' -not -path './交接单/.exec-lock*' 2>/dev/null | sed 's|^\./||' | LC_ALL=C sort)"
if [ -z "$HOT" ]; then
echo " ✓ 无(这块是「冷」的)"
else
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""handoff-status.py — 一条命令看清 05-交接单 里哪些单子【待执行】/【已完成】/【无状态】。
"""handoff-status.py — 一条命令看清 交接单 里哪些单子【待执行】/【已完成】/【无状态】。
为什么需要它:`05-交接单/交接单-已完成/` 那层目录摊平后,**"目录位置"不再表达状态**。
为什么需要它:`交接单/交接单-已完成/` 那层目录摊平后,**"目录位置"不再表达状态**。
状态改由**机读字段**承载(与布局解耦,摊平/搬家都不失效):
① 单子头部 `- 状态:…` 行(模板已有)→ ② 回退取 `05-交接单/README.md` 台账表的「状态」列
① 单子头部 `- 状态:…` 行(模板已有)→ ② 回退取 `交接单/README.md` 台账表的「状态」列
用法:python 07-scripts/handoff-status.py
"""
@@ -38,7 +38,7 @@ import os
import re
ROOT = os.environ.get("DSH_DOCS_ROOT") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
D = os.path.join(ROOT, "05-交接单")
D = os.path.join(ROOT, "交接单")
README = os.path.join(D, "README.md")
HEAD_STATUS = re.compile(r"^[\s>*\-|#]*\*{0,2}状态\*{0,2}\s*[::]\s*(.+?)\s*$", re.M)
+1 -1
View File
@@ -7,7 +7,7 @@
# 文档冲突靠 git status / mtime 能看出来,**服务器态变更看不出来**
# (systemctl 不会告诉你 10 分钟前谁重启过)→ 只能靠显式锁。
#
# 与文档库锁的关系:`05-交接单/.doing-<单>` + `.exec-lock` 管「文档与单子」;本锁管「生产态」。
# 与文档库锁的关系:`交接单/.doing-<单>` + `.exec-lock` 管「文档与单子」;本锁管「生产态」。
# 顺序:**先抢全局执行锁 → 再占本锁**。
#
# 用法:
@@ -205,7 +205,7 @@ def main() -> int:
root = Path(a.locks_root)
exec_lock = root / ".exec-lock"
elif docs and os.path.isdir(docs):
base = Path(docs) / "05-交接单"
base = Path(docs) / "交接单"
root = base / ".locks"
exec_lock = base / ".exec-lock"
else:
@@ -43,7 +43,7 @@ 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"
LOCKROOT="$DOCS_ROOT/交接单/.locks"
# ── 机制层(⛔ 不受 architecture.md 的 src/** 分层管辖,但**全平台共用**)────────
# 判据:这些文件任一改动都会改变**所有会话**的行为 ⇒ 与层④同性质(全局影响)。
@@ -537,19 +537,19 @@ def check_lockroot_in_pkg():
为什么是红线:`lock/handoff-guard.sh` 与 `lock/preflight-lock.sh` 的 `DOCS_ROOT`
原先回落到 `$_SM_HERE/..`(=**技能包自己的 `scripts/`**)⇒ 取不到文档库根时,
锁目录直接长在技能包里(另一环境实测已出现 `<技能包>/scripts/05-交接单/.locks/`)。
锁目录直接长在技能包里(另一环境实测已出现 `<技能包>/scripts/交接单/.locks/`)。
后果两条:① 污染技能包(它是只读发布物);② 换工作区后锁跟着错地方 ⇒ **域锁静默失效**。
✅ 现行:两处回落实已改成「取不到就如实报错 + 留痕 + 退出码 2」;
本条是**兜底判据** —— 万一将来又有人改回"包内回落",这里要能报出来。
✅ 变异对照(2026-10-15 实测,⛔ 证明它抓得住,不是恒绿):
1. `mkdir -p <技能包>/scripts/05-交接单`
1. `mkdir -p <技能包>/scripts/交接单`
2. 跑 `python session-rules-check.py --ws <本区> --all` ⇒ 输出
`✗ [A] 锁根落进了技能包 ⇒ … —— <技能包>/scripts/05-交接单`
3. `rmdir <技能包>/scripts/05-交接单` 复原 ⇒ 回到 `✓ [A] 锁根未落进技能包`
`✗ [A] 锁根落进了技能包 ⇒ … —— <技能包>/scripts/交接单`
3. `rmdir <技能包>/scripts/交接单` 复原 ⇒ 回到 `✓ [A] 锁根未落进技能包`
⚠️ 为什么不把它做成 `selftest.py` 的自动化用例:那样必须**真在技能包里造目录**,
用例中途被打断就会**把一个 `05-交接单` 永久留在包里**(不可逆污染)——
用例中途被打断就会**把一个 `交接单` 永久留在包里**(不可逆污染)——
风险大于收益。⇒ 保留为上面这段**可手工复现**的步骤。
"""
roots = []
@@ -565,7 +565,7 @@ def check_lockroot_in_pkg():
roots.append(r)
bad = []
for r in roots:
for rel in (("05-交接单",), ("scripts", "05-交接单")):
for rel in (("交接单",), ("scripts", "交接单")):
if os.path.isdir(os.path.join(r, *rel)):
bad.append("%s/%s" % (r, "/".join(rel)))
if bad: