feat(skills): 新增技能盤點頁與共用部署驗證流程,並把技能驗證移到新行程

技能盤點以前只回到對話裡,換一台機器就得重跑才知道裝了什麼。
現在新增技能盤點這個 wiki 頁類型,雜湊取「主機、工具名稱、登入帳號」三段。
每支 CLI 各有自己的 plugin 集合,也各有自己的 hook 接線,那是互相獨立的事實。
少了工具名稱那一段,同一台機器上五支 CLI 會算出同一個雜湊,五份盤點互相覆蓋,
讀的人還看不出被蓋掉。技能盤點新增寫入這兩頁的步驟,整步規定必須開 sub agent。
兩份樣板刻意分開:內容頁每次盤點覆寫整頁,目錄頁只更新自己那一列,
兩者的寫入語意剛好相反,合成一份遲早有人把別台機器的紀錄刪掉。

四支異動技能原本在部署完的同一個工作階段,就叫用剛做好的技能。
部署收尾自己立起重啟閘門,那支技能必被擋下,驗證做不完。
解法不是把它加進豁免清單。豁免擋得住閘門,擋不住「行程還載著舊版」這件事,
硬過關驗到的是舊版行為,等於假通過。所以把判路線、部署、驗證、失敗分流
抽成一份共用說明,驗證一律另開 CLI 行程執行,四支技能只留一行指標指過去。

新增腳本檢查工具,一次做完語法、執行權限與結束碼宣告三項檢查,
只被 source 的函式庫豁免後兩項,而且逐支記在錯誤輸出,不靜默略過。
新增部署路線判定工具,判定改動有沒有進存取庫的預設分支,
取代四支技能各抄一段、各自漂移的散文;判不出來就回報停下,不自己挑路線走。

同時把四支技能裡的中文段落抽到共用說明、指標改回英文,
修正六處相對路徑,把技能盤點的模糊描述改成查得出來的條件,
並讓 manifest 同步的每一個呼叫端逐碼分流。

七支技能改為併行執行:例行稽核從九步併成七步,技能盤點併成六步。
技能盤點不再重跑盤點腳本內部已經跑過的三支腳本,
而那三支原本兼作獨立交叉檢查,拿掉就少一層保護,
所以把少掉的是什麼、風險由誰擋住,明白寫進 Notes,不當作沒發生。
This commit is contained in:
2026-08-31 11:11:12 +08:00
parent 7dc5c32de3
commit 5aa4a3da57
12 changed files with 571 additions and 128 deletions
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env sh
# lint-scripts.sh — 檢查一個 domain 存取庫的 shell 腳本:語法、可執行、結束碼有沒有記載。
#
# 用法: lint-scripts.sh <domain-path>
#
# 檢查三項(掃 {domain-path}/tools 與 {domain-path}/hooks 底下的 *.sh):
# 1. 語法 — sh -n 通過。
# 2. 可執行 — 檔案有執行權限。技能直接呼叫的腳本沒有 +x,會在部署後才炸。
# 3. 結束碼 — 檔頭前 60 行要有結束碼宣告列(「結束碼:」或「exit code」)。
# 沒有宣告的腳本,呼叫端沒辦法逐碼分流,只能猜;猜錯就把失敗當成功。
#
# 為什麼合成一支: skill-check 原本在 SKILL.md 裡寫 find ... -exec sh -n,另外兩項靠散文
# 要求人工比對。三項的輸入輸出都固定,散文版每個 domain 各做一次,還會跟腳本實況漂移。
#
# 例外: 只被 source 的共用函式庫(判準見 is_library)第 2、3 項都不查——它不是可執行入口,
# 沒有 +x 的必要,也沒有結束碼語意可宣告。第 1 項語法檢查照查,函式庫一樣會被 sh -n 讀。
# 豁免不是靜默的: 每豁免一支就在 stderr 記一行,摘要也帶函式庫支數,才看得出誰被跳過。
#
# 輸出: 一行一個不合格項目,格式 {檔案}:{檢查項}:{說明}(stdout);
# 函式庫豁免通知與統計摘要走 stderr。
# 結束碼: 0=掃到腳本且三項全過
# 1=有不合格項目(清單在 stdout)
# 2=用法錯誤(本腳本只吃一個參數)
# 3=domain 路徑不存在,或 tools/ 與 hooks/ 都沒有 *.sh——**什麼都沒掃**,
# 不等於通過。缺這兩個目錄本身不是失敗,呼叫端照實記「無腳本可掃」即可。
set -u
usage() {
echo 'usage: lint-scripts.sh <domain-path>' >&2
exit 2
}
[ "$#" -eq 1 ] || usage
DOMAIN=${1%/}
[ -n "$DOMAIN" ] || usage
[ -d "$DOMAIN" ] || { echo "找不到 domain 路徑:$DOMAIN" >&2; exit 3; }
SCAN=''
for d in "$DOMAIN/tools" "$DOMAIN/hooks"; do
[ -d "$d" ] && SCAN="$SCAN $d"
done
[ -n "$SCAN" ] || { echo "沒有 tools/ 也沒有 hooks/,無腳本可掃:$DOMAIN" >&2; exit 3; }
TMP=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; }
trap 'rm -f "$TMP"' EXIT
# shellcheck disable=SC2086
find $SCAN -type f -name '*.sh' 2>/dev/null | sort > "$TMP"
[ -s "$TMP" ] || { echo "tools/ 與 hooks/ 底下沒有 *.sh,無腳本可掃:$DOMAIN" >&2; exit 3; }
has_exit_stmt() { # $1=檔案;去掉整行註解後還找得到 exit 敘述就回 0
grep -v '^[[:space:]]*#' "$1" 2>/dev/null \
| grep -qE '(^|[;&|(){}[:space:]])exit([[:space:]]|$)'
}
is_library() { # $1=檔案;只被 source 的共用函式庫,可執行與結束碼兩項都豁免
# 判準兩條,缺一不可:
# 1. 自我宣告是函式庫——檔名 lib.sh,或檔頭前 30 行寫了「以 source 載入」。
# 2. 程式碼裡一個 exit 敘述都沒有。沒有 exit 就沒有結束碼語意,也就沒有東西可宣告;
# 這一條同時是「不是可執行入口」的證據,兩項豁免共用同一個事實。
# 為什麼不能只看第 1 條: 那是字樣比對,會命中「提到這件事」的腳本。本腳本自己的
# 例外段就寫了「以 source 載入」,plugins-root.sh 的用法段也寫了,但兩支都是有
# 結束碼語意的可執行入口。只拿第 1 條去豁免結束碼,等於把工具自己漏掉。
case "${1##*/}" in
lib.sh) ;;
*) head -30 "$1" 2>/dev/null | grep -q '以 source 載入' || return 1 ;;
esac
! has_exit_stmt "$1"
}
hit=0
total=0
libs=0
while IFS= read -r f; do
[ -n "$f" ] || continue
total=$((total + 1))
err=$(sh -n "$f" 2>&1) || {
printf '%s:語法:%s\n' "$f" "$(printf '%s' "$err" | tr '\n' ' ')"
hit=1
}
if is_library "$f"; then
libs=$((libs + 1))
printf '%s:函式庫:判定為只被 source 的共用函式庫(無 exit 敘述),略過「可執行」與「結束碼」\n' "$f" >&2
continue
fi
if [ ! -x "$f" ]; then
printf '%s:可執行:缺執行權限,請 chmod +x\n' "$f"
hit=1
fi
if ! head -60 "$f" 2>/dev/null | grep -qE '結束碼|exit code'; then
printf '%s:結束碼:檔頭沒有結束碼宣告列,呼叫端無法逐碼分流\n' "$f"
hit=1
fi
done < "$TMP"
if [ "$hit" -eq 0 ]; then
echo "腳本檢查通過:$total 支(語法、可執行、結束碼宣告),其中 $libs 支判定為函式庫" >&2
else
echo "腳本檢查有不合格項目:共掃 $total 支(其中 $libs 支判定為函式庫),清單見 stdout" >&2
fi
exit $hit