chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
This commit is contained in:
1 parent
c70d5d860e
commit
5ad755116e
173 files changed
+27632
No files matched your search
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env bash
|
||||
# handoff-guard.sh — 开工 / 推送前的「并行冲突预检」(只读;--claim 除外,它只建一个锁目录)
|
||||
#
|
||||
# 为什么需要它:本库由**多个 AI 会话并行**读写,而「先读后改 / Edit 增量 / 改完 commit」这类
|
||||
# 约定全部依赖“人记得做”。本脚本把判据变成**一条命令 + 退出码**,不靠记忆。
|
||||
#
|
||||
# 关键设计:**mtime 只作提示、不作判定**(它分不清“谁改的”)。真正的判定来自四处:
|
||||
# ① 全局执行锁(交接单/.exec-lock)—— **一粗**:同一时刻只允许一个执行会话动「文档/代码/服务器」
|
||||
# ① 单级占用锁(交接单/.doing-<单号>)—— **一细**:这个单归谁做(供台账 / 接管使用)
|
||||
# ② 越界改动(不在我声明清单里的未提交文件)—— 推送前检查,防“顺手重放别人的半成品”
|
||||
# ④ 双端一致性(docs-sync-check.sh)—— 推送前检查,防“幽灵文件”(推回了别人已移走的文件)
|
||||
#
|
||||
# 两级锁的关系:**先抢全局锁 → 再占单级锁**;释放时**先放单级、再放全局**。
|
||||
# 单级锁允许“两个会话各做一单”(冲突域不重叠时);全局锁则彻底禁止并行执行。
|
||||
# ⇒ 本库现状(共享入口文件多 + 要动服务器)建议**默认只跑一个执行会话**,即始终持全局锁。
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/handoff-guard.sh --claim-exec "exec-session-B" # 【第一步】抢全局执行锁
|
||||
# bash scripts/handoff-guard.sh --claim T03 "exec-session-B" # 【第二步】占单级锁
|
||||
# bash scripts/handoff-guard.sh --release T03 # 完工:先放单级
|
||||
# bash scripts/handoff-guard.sh --release-exec # 再放全局
|
||||
# bash scripts/handoff-guard.sh # 看全局状态(信息模式)
|
||||
# ME="exec-session-B" MINE="交接单/T03-*.md" bash scripts/handoff-guard.sh T03 # 开工检查(严格)
|
||||
# ME="exec-session-B" MINE="..." PUSH=1 bash scripts/handoff-guard.sh T03 # 推送前检查
|
||||
#
|
||||
# 环境变量:ME(我是谁 —— 用来判断锁是不是自己的;不设则一律按“别人的锁”处理)
|
||||
# MINE(我本次要改的文件,空格分隔,支持 * 通配)
|
||||
# PUSH=1 启用推送前硬判定(④ 幽灵文件)
|
||||
# GUARD_WINDOW(分钟,默认 30)|SKIP_SYNC=1 跳过 ④
|
||||
# 退出码:0 = 放行;1 = 命中硬冲突/硬判定;2 = 环境错误
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT" || exit 2
|
||||
LOCKDIR="$ROOT/交接单/.doing-"
|
||||
LOCKEXEC="$ROOT/交接单/.exec-lock"
|
||||
ME="${ME:-}"
|
||||
|
||||
# ── 全局执行锁:占位 / 释放 ──────────────────────────────
|
||||
if [ "${1:-}" = "--claim-exec" ]; then
|
||||
OWNER="${2:-${ME:-$(whoami)}}"
|
||||
if mkdir "$LOCKEXEC" 2>/dev/null; then
|
||||
printf '%s\n开始:%s\n在做:%s\n' "$OWNER" "$(date '+%m-%d %H:%M')" "${3:-(未声明单号)}" > "$LOCKEXEC/OWNER"
|
||||
echo "✓ 已持全局执行锁($OWNER)"
|
||||
echo " ⛔ **锁的生命周期 = 任务的生命周期**(2026-09-14 用户明令):执行完成 → 必须 \`--release-exec\` 才算完成;"
|
||||
echo " 禁止抢锁做一半、不解锁就结束回合/会话(本库无心跳,带锁结束 = 把所有人挡在门外)。中途要停 ⇒ 先释放再停。"
|
||||
exit 0
|
||||
fi
|
||||
echo "✗ 抢锁失败:已有执行会话在跑 ——
|
||||
占用者:$(sed -n '1p' "$LOCKEXEC/OWNER" 2>/dev/null || echo '?') $(sed -n '2p' "$LOCKEXEC/OWNER" 2>/dev/null)
|
||||
$(sed -n '3p' "$LOCKEXEC/OWNER" 2>/dev/null)
|
||||
→ **停手**:等它做完(它会 --release-exec)。⛔ **不得人工删锁、不得接管**(**R9**,用户 2026-09-12 明令)——
|
||||
抢不到锁 = **停手 + 报告用户**;**锁的处置权只属于用户本人**" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "${1:-}" = "--release-exec" ]; then
|
||||
rm -rf "$LOCKEXEC" && echo "✓ 已释放全局执行锁" || { echo "✗ 释放失败" >&2; exit 1; }
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
# ── 占位 / 释放 ─────────────────────────────────────────
|
||||
if [ "${1:-}" = "--claim" ]; then
|
||||
T="${2:-}"; OWNER="${3:-$(whoami)@$(date +%H:%M)}"
|
||||
[ -n "$T" ] || { echo "用法:--claim <单号> [占用者]" >&2; exit 2; }
|
||||
if mkdir "$LOCKDIR$T" 2>/dev/null; then
|
||||
printf '%s\n' "$OWNER" > "$LOCKDIR$T/OWNER"
|
||||
echo "✓ 已占位:交接单/.doing-$T($OWNER)—— 完工请 --release $T"
|
||||
exit 0
|
||||
fi
|
||||
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 "✓ 已释放:交接单/.doing-$T" || { echo "✗ 释放失败" >&2; exit 1; }
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 预检 ────────────────────────────────────────────────
|
||||
MY_TASK="${1:-}"
|
||||
WINDOW="${GUARD_WINDOW:-30}"
|
||||
MINE="${MINE:-}"
|
||||
PUSH="${PUSH:-0}"
|
||||
VERDICT=0
|
||||
HARD=() # 硬失败原因
|
||||
[ -n "$MINE" ] && STRICT=1 || STRICT=0
|
||||
|
||||
echo "=================================================================="
|
||||
echo "并行冲突预检 root=$ROOT"
|
||||
echo "本次任务 = ${MY_TASK:-(未声明)}"
|
||||
echo "我是谁(ME) = ${ME:-(未声明 → 任何锁都按"别人的"处理)}"
|
||||
echo "我声明要改 = ${MINE:-(未声明 → 仅信息模式)}"
|
||||
echo "模式 = $([ "$PUSH" = "1" ] && echo 推送前检查 || echo 开工检查)"
|
||||
echo "=================================================================="
|
||||
|
||||
in_mine() { [ -z "$MINE" ] && return 1; for p in $MINE; do case "$1" in $p) return 0;; esac; done; return 1; }
|
||||
|
||||
# ── ① 占用锁(硬判定)────────────────────────────────────
|
||||
echo
|
||||
echo "【1】占用锁(交接单/.doing-*)"
|
||||
shopt -s nullglob
|
||||
LOCKS=("$LOCKDIR"*)
|
||||
if [ ${#LOCKS[@]} -eq 0 ]; then
|
||||
echo " ✓ 无人占用 —— ⚠ 这不是「可以开工」,是「**你快去抢**」:"
|
||||
echo " 开工前先:ME=\"<你的会话名>\" bash scripts/handoff-guard.sh --claim <单号> \"<你的会话名>\""
|
||||
else
|
||||
for L in "${LOCKS[@]}"; do
|
||||
n="$(basename "$L")"; o="$(cat "$L/OWNER" 2>/dev/null || echo '(未写 OWNER)')"
|
||||
if [ -n "$MY_TASK" ] && [ "$n" = ".doing-$MY_TASK" ]; then
|
||||
echo " · $n ← 你自己占的($o)"
|
||||
else
|
||||
echo " ⚠ $n 被占用:$o → 冲突域重叠就别开工"
|
||||
VERDICT=1; HARD+=("① 别的会话持有占用锁 $n($o)")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── ①b 锁 ↔ 台账一致性(2026-09-12 新增)────────────────
|
||||
# 实证:T03 10:14 就被占了,但台账那行直到 12:59 还写着「待执行」→ 别人看台账会以为**没人做**,
|
||||
# 于是可能重复开工(这不是文件写冲突,而是"状态不可见"造成的重复劳动)。
|
||||
if [ ${#LOCKS[@]} -gt 0 ]; then
|
||||
for L in "${LOCKS[@]}"; do
|
||||
n="$(basename "$L")"; T="${n#.doing-}"
|
||||
row="$(grep -m1 "^| \`$T-" 交接单/README.md 2>/dev/null || true)"
|
||||
if [ -z "$row" ]; then
|
||||
echo " ⚠ $T 有占用锁,但 \`交接单/README.md §一\` 里**没有这一行** → 补上(新增单)"
|
||||
VERDICT=1; HARD+=("①b $T 有锁但台账缺行")
|
||||
elif printf '%s' "$row" | grep -qE "执行中|已完成|已归档"; then
|
||||
echo " · $T 台账已标「执行中/已完成」✓"
|
||||
else
|
||||
echo " ⚠ $T 已被占用($n),但台账那行仍写作「$(printf '%s' "$row" | awk -F'|' '{print $3}' | tr -d ' ')」"
|
||||
echo " → **台账滞后 = 别人可能重复开工**:立刻把该行状态改成「🔄 执行中」"
|
||||
VERDICT=1; HARD+=("①b $T 有锁但台账未标「执行中」(重复开工风险)")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── ①c 全局执行锁(2026-09-12 新增:同一时刻只允许一个执行会话)────
|
||||
echo
|
||||
echo "【1c】全局执行锁(交接单/.exec-lock)"
|
||||
if [ ! -d "$LOCKEXEC" ]; then
|
||||
echo " ✓ 无全局锁 —— ⚠️ 这**不是「可以开工」,是「你快去抢」**:"
|
||||
echo " 开工前必须先:bash scripts/handoff-guard.sh --claim-exec \"<你的会话名>\""
|
||||
echo " (「环境干净」≠「没人动过」;抢锁是原子的,抢到才是你的开工许可)"
|
||||
else
|
||||
O1="$(sed -n '1p' "$LOCKEXEC/OWNER" 2>/dev/null)"
|
||||
O2="$(sed -n '2p' "$LOCKEXEC/OWNER" 2>/dev/null)"
|
||||
O3="$(sed -n '3p' "$LOCKEXEC/OWNER" 2>/dev/null)"
|
||||
if [ -n "$ME" ] && [ "$O1" = "$ME" ]; then
|
||||
echo " · 全局锁是**你自己**持有的($ME)—— 完工记得 --release-exec"
|
||||
else
|
||||
echo " ⚠ 全局锁被占用:$O1 $O2"
|
||||
[ -n "$O3" ] && echo " $O3"
|
||||
echo " → **同一时刻只允许一个执行会话**:要动「文档 / 代码 / 服务器」就先等它释放;"
|
||||
echo " ⛔ **不得人工删锁 / 不得接管**(R9):锁只能由持有者自己 --release-exec ——"
|
||||
echo " 抢不到 = 停手 + 报告用户;读 OWNER 仅用于「用户已点头、且用户自己撤锁之后」的续做"
|
||||
VERDICT=1; HARD+=("①c 别的会话持有全局执行锁($O1)→ 不允许并行执行")
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── ①d 服务器侧操作锁(2026-09-12 新增,T04):平台高危操作的跨会话互斥 ────
|
||||
echo
|
||||
echo "【1d】服务器侧操作锁(/opt/dsh/state/.op-lock)"
|
||||
if [ "${SKIP_OPLOCK:-0}" = "1" ]; then
|
||||
echo " · 已跳过(SKIP_OPLOCK=1)"
|
||||
else
|
||||
OPLOCK_OUT="$(ssh -o BatchMode=yes -o ConnectTimeout=8 "${OP_LOCK_REMOTE:-bt-server}" \
|
||||
"if [ -d /opt/dsh/state/.op-lock ]; then ls -1 /opt/dsh/state/.op-lock 2>/dev/null | grep -v '^README\$' || true; else echo __NOLOCKDIR__; fi" 2>/dev/null)"; OPLOCK_RC=$?
|
||||
if [ "$OPLOCK_RC" -ne 0 ]; then
|
||||
echo " · 服务器不可达(离线)→ **只提示、不失败**;但要动线上时须先恢复可见性再动手"
|
||||
elif [ "$OPLOCK_OUT" = "__NOLOCKDIR__" ]; then
|
||||
echo " ⚠ 锁根目录不存在:/opt/dsh/state/.op-lock(T04 应已建立 → 需复查)"
|
||||
VERDICT=1; HARD+=("①d 服务器侧锁根目录缺失")
|
||||
elif [ -z "$OPLOCK_OUT" ]; then
|
||||
echo " ✓ 无平台操作锁(此刻没有会话在动线上)"
|
||||
else
|
||||
echo " ⚠ 有会话正在动线上:"
|
||||
for L in $OPLOCK_OUT; do
|
||||
echo " 🔴 $L"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=8 "${OP_LOCK_REMOTE:-bt-server}" \
|
||||
"sed 's/^/ /' '/opt/dsh/state/.op-lock/$L/OWNER' 2>/dev/null" 2>/dev/null || true
|
||||
done
|
||||
echo " → 凡「重启 / drain / 改实例 env·quota / 批量铺插件 / 改 nginx·nft·证书」类操作,"
|
||||
echo " 开工前必须先占位(bash scripts/op-lock.sh claim <操作名> \"<影响面>\");"
|
||||
echo " 占位失败 = 有会话在动线上 → **停手**。"
|
||||
VERDICT=1; HARD+=("①d 有会话持有服务器侧操作锁($OPLOCK_OUT)")
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── ② 越界改动(推送前硬判定)────────────────────────────
|
||||
echo
|
||||
echo "【2】未提交改动(含未跟踪)"
|
||||
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 " ✓ 工作区干净"
|
||||
else
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
if in_mine "$f"; then INSIDE+=("$f"); else OUTSIDE+=("$f"); fi
|
||||
done <<< "$CHANGED"
|
||||
echo " · 我声明的:${#INSIDE[@]} 个"
|
||||
if [ ${#OUTSIDE[@]} -eq 0 ]; then
|
||||
echo " ✓ 无越界改动(没有别人的半成品混在里面)"
|
||||
else
|
||||
echo " ⚠ 越界(别人的/我没声明):${#OUTSIDE[@]} 个"
|
||||
for f in "${OUTSIDE[@]:0:6}"; do echo " $f"; done
|
||||
[ ${#OUTSIDE[@]} -gt 6 ] && echo " …还有 $(( ${#OUTSIDE[@]} - 6 )) 个"
|
||||
echo " → **推送时只能 scp 自己声明的文件,切勿 'git add -A'**(本项不构成硬失败;"
|
||||
echo " 真正的推送硬判定在【4】——那里能精确看出「你正要推什么」)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── ③ 热点提示(仅提示,不作判定)────────────────────────
|
||||
echo
|
||||
echo "【3】近 $WINDOW 分钟被改动的文件(**仅提示**:mtime 分不清谁改的)"
|
||||
HOT="$(find . -type f -mmin "-$WINDOW" \
|
||||
-not -path './.git/*' -not -path '*/node_modules/*' -not -name '*.bak*' \
|
||||
-not -path './交接单/.doing-*' -not -path './交接单/.exec-lock*' 2>/dev/null | sed 's|^\./||' | LC_ALL=C sort)"
|
||||
if [ -z "$HOT" ]; then
|
||||
echo " ✓ 无(这块是「冷」的)"
|
||||
else
|
||||
HIT=(); OTHER=""
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
if in_mine "$f"; then HIT+=("$f"); else OTHER="$OTHER$f"$'\n'; fi
|
||||
done <<< "$HOT"
|
||||
if [ ${#HIT[@]} -gt 0 ]; then
|
||||
echo " ⚠ **我的目标文件近期被改动过**(可能是你自己,也可能是别人)→ 改前务必先读最新内容:"
|
||||
for f in "${HIT[@]}"; do echo " $f"; done
|
||||
else
|
||||
echo " · 我的目标文件均未被近期改动"
|
||||
fi
|
||||
echo " · 其它近期被改动的文件:$(printf '%s' "$OTHER" | grep -c . || true) 个(与你无关,仅供感知全库热度)"
|
||||
[ "$WINDOW" -gt 10 ] && echo " 想更锐利:GUARD_WINDOW=10 再跑一次"
|
||||
fi
|
||||
|
||||
# ── ④ 双端一致性(推送前硬判定)──────────────────────────
|
||||
echo
|
||||
echo "【4】双端一致性(docs-sync-check.sh)"
|
||||
if [ "${SKIP_SYNC:-0}" = "1" ]; then
|
||||
echo " (SKIP_SYNC=1,跳过)"
|
||||
elif [ -f scripts/docs-sync-check.sh ]; then
|
||||
# 全量对账耗时(2026-09-12 实测):优化前 **2m15s**(Git Bash 逐文件 spawn md5sum → 曾被当成"挂死"),
|
||||
# 优化后 **≈28s**。这里再加硬超时兜底,避免对账自身卡住时把整个 guard 拖死;急用时 `SKIP_SYNC=1` 跳过。
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
SYNC="$(timeout 300 bash scripts/docs-sync-check.sh 2>/dev/null)"
|
||||
else
|
||||
SYNC="$(bash scripts/docs-sync-check.sh 2>/dev/null)"
|
||||
fi
|
||||
printf '%s\n' "$SYNC" | tail -6
|
||||
# 关键:对账结果里的「仅本地(待推送)」= 一次朴素推送**实际会推上去**的东西。
|
||||
# 其中只要有一个「不在我声明清单里」,就是幽灵文件信号(2026-09-12 事故:把已被对方归档的
|
||||
# T02 又推回服务器)→ 推送前硬失败。
|
||||
ONLYL="$(printf '%s\n' "$SYNC" | sed -n 's/.*仅本地(待推送) //p')"
|
||||
if [ -n "$ONLYL" ]; then
|
||||
GHOST=(); GOOD=0
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
if in_mine "$f"; then GOOD=$((GOOD+1)); else GHOST+=("$f"); fi
|
||||
done <<< "$ONLYL"
|
||||
echo " · 仅本地(= 一次朴素推送会推上去的):$GOOD 个已声明 + ${#GHOST[@]} 个未声明"
|
||||
if [ ${#GHOST[@]} -gt 0 ]; then
|
||||
echo " ⚠ 未声明却「仅本地」——**幽灵文件风险**:"
|
||||
for f in "${GHOST[@]}"; do echo " $f"; done
|
||||
echo " → 它可能是别人**刚归档/移走**的文件(你今天就是这么推回去的)"
|
||||
if [ "$PUSH" = "1" ]; then
|
||||
VERDICT=1; HARD+=("④ 有 ${#GHOST[@]} 个「仅本地」文件不在你的推送清单里 → 停手核实后再推")
|
||||
else
|
||||
echo " (开工阶段仅提示;推送前请带 PUSH=1 复跑)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " (未找到 scripts/docs-sync-check.sh)"
|
||||
fi
|
||||
|
||||
# ── 结论 ────────────────────────────────────────────────
|
||||
echo
|
||||
echo "=================================================================="
|
||||
if [ "$VERDICT" -ne 0 ]; then
|
||||
echo "结论:**不可放行** ——"
|
||||
for r in "${HARD[@]}"; do echo " $r"; done
|
||||
elif [ "$STRICT" -eq 0 ]; then
|
||||
echo "结论:未声明改动清单 → 仅为信息输出,**不构成放行依据**(开工请带 MINE=\"...\")"
|
||||
echo " ⚠️ 且「无锁」不等于「可以开工」—— 开工前必须先 --claim-exec 抢锁(见上方【1c】)"
|
||||
echo " (2026-09-12 实证:把「无锁」读成「可以动手」,导致两个会话同时改库)"
|
||||
else
|
||||
echo "结论:未命中硬冲突 → 可以继续(仍须遵守:Edit 增量 / 改前先读 / 只推自己的文件)"
|
||||
fi
|
||||
exit "$VERDICT"
|
||||
Reference in new issue
Block a user