Files
meta/tools/check-behaviors.sh
jiantw83 b87dbb12cd feat(狀態回報): 收尾寫一筆 skill-end 事件
現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就
中止的技能,在紀錄裡長得一模一樣。

start 由技能用量 hook 順手發,不必改技能文件。end 只能由技能自己在收尾
步驟寫——hook 接在技能工具呼叫上,而實際工作發生在之後的模型輪次,它在
原理上看不到成敗。有 start 沒有配對的 end,就是那一輪中止了。

status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜
跳過,回報失敗一律不改變技能自己的結論。
2026-09-02 16:01:16 +08:00

189 lines
8.5 KiB
Bash
Executable File
Raw Permalink 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 sh
# check-behaviors.sh — 檢查一個 domain 的技能行為清單 references/behaviors.md 有沒有跟 skills/ 對齊。
#
# 用法: check-behaviors.sh <domain-path>
#
# 檢查七項(格式合約見 jsc-meta references/guidelines.md 的「技能行為清單」一節):
# 1. 標題 — 第一行是「# jsc-{domain} 技能行為清單」,檔案不得有 UTF-8 BOM。
# 2. 節對技能 — 每支 skills/*/SKILL.md 一個「## {技能名}」節,名稱與目錄名逐字相同,不多不少。
# 3. 節順序 — 節的排列照技能目錄名的字典序(LC_ALL=C)。
# 4. 表格 — 每節恰好一張表,表頭是「| 項目 | 內容 |」。
# 5. 五個欄位 — 依序為 觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,不多不少。
# 6. 內容 — 每一列的「內容」欄不得空白。
# 7. 收尾事件 — 「可驗證跡象」那一列要寫到 skill-end 或 events.jsonl。
#
# 為什麼要這支: 技能改了行為、清單沒跟著改,兩邊就漂移。漂移靠眼睛比對,10 個 domain 每次稽核
# 都要重做一遍,還會漏。這七項的輸入輸出固定,交給程式判定才穩。
#
# 為什麼加第 7 項: 技能的成敗只有技能自己寫得出來。hook 接在技能工具呼叫之後就觸發,那一刻
# 實際工作還在後面的模型輪次,看不到結果;有 start 沒有配對的 end 就是中止。收尾少寫這一筆,
# 那支技能每一次都會被算成中止,而且不會有任何錯誤訊息。跡象欄是清單裡唯一寫得下「跑完在
# 環境裡留下什麼」的地方,所以判準放在這一列,而不是另開一項只檢查文字。
# 本腳本是所有 domain 共用的稽核入口,這一項加進來之後,還沒補收尾事件的 domain 會開始回 1。
# 那是預期的結果,不是誤報:要修的是那些 domain 的技能與清單,不是把斷言放寬。
#
# 輸出: 一行一個不合格項目,格式 {behaviors.md 路徑}:{技能名或 -}:{說明}(stderr);
# 通過時在 stderr 印一行摘要。stdout 不印東西。
# 結束碼: 0=行為清單與 skills/ 相符,五個欄位齊全、內容欄非空,且每節都寫了收尾事件
# 1=不符:缺節、多節、順序不對、表格不對、缺欄位、欄位空白,或跡象欄沒寫收尾事件,
# 清單在 stderr
# 2=用法錯誤(本腳本只吃一個參數)
# 3=找不到 {domain-path}/references/behaviors.md,或找不到 {domain-path}/skills/,
# 或 skills/ 底下一支 SKILL.md 都沒有——**什麼都沒查**,不等於通過
set -u
usage() {
echo 'usage: check-behaviors.sh <domain-path>' >&2
exit 2
}
[ "$#" -eq 1 ] || usage
DOMAIN=${1%/}
[ -n "$DOMAIN" ] || usage
SKILLS="$DOMAIN/skills"
DOC="$DOMAIN/references/behaviors.md"
[ -d "$DOMAIN" ] || { echo "找不到 domain 路徑:$DOMAIN" >&2; exit 3; }
[ -d "$SKILLS" ] || { echo "找不到 skills/:$SKILLS" >&2; exit 3; }
[ -f "$DOC" ] || { echo "找不到行為清單:$DOC" >&2; exit 3; }
TMPD=$(mktemp -d) || { echo "無法建立暫存目錄" >&2; exit 3; }
trap 'rm -rf "$TMPD"' EXIT
TAB=$(printf '\t')
BOM=$(printf '\357\273\277')
FIELDS='觸發時機 關鍵步驟 外部呼叫 完成條件 可驗證跡象'
fail=0
report() { # $1=技能名或 -,$2=說明
printf '%s:%s:%s\n' "$DOC" "$1" "$2" >&2
fail=1
}
# 技能清單: skills/ 底下帶 SKILL.md 的目錄名,字典序。
for d in "$SKILLS"/*/; do
[ -f "${d}SKILL.md" ] || continue
n=${d%/}
echo "${n##*/}"
done | LC_ALL=C sort > "$TMPD/skills.txt"
[ -s "$TMPD/skills.txt" ] || { echo "skills/ 底下沒有任何 SKILL.md:$SKILLS" >&2; exit 3; }
# 解析 behaviors.md,攤平成三種記錄:
# SEC<TAB>{節名} 一個「## 」標題
# HDR<TAB>{節名} 一列表頭「| 項目 | 內容 |」
# ROW<TAB>{節名}<TAB>{項目}<TAB>{內容} 一列資料(分隔列不算)
awk '
function trim(s) { gsub(/^[ \t]+/, "", s); gsub(/[ \t]+$/, "", s); return s }
BEGIN { FS = "|"; sec = "-" }
{ sub(/\r$/, "") }
/^## / {
sec = trim(substr($0, 4))
printf "SEC\t%s\n", sec
next
}
/^[ \t]*\|/ {
item = trim($2)
body = ""
for (i = 3; i < NF; i++) body = (body == "" ? $i : body "|" $i)
body = trim(body)
if (item ~ /^[-: ]+$/ && body ~ /^[-: ]*$/) next
if (item == "項目" && body == "內容") { printf "HDR\t%s\n", sec; next }
printf "ROW\t%s\t%s\t%s\n", sec, item, body
}
' "$DOC" > "$TMPD/parsed.txt"
# --- 1. 標題 ---
first=$(head -1 "$DOC" | tr -d '\r')
case $first in
"$BOM"*) report - '檔頭帶 UTF-8 BOM,請改存無 BOM'; first=${first#"$BOM"} ;;
esac
# domain 名以 plugin.json 的 name 為準,checkout 目錄名只是退路。
# 目錄名是誰 clone 誰決定的,同一個 repo 換一台機器就可能叫別的名字,
# 拿它當唯一來源會在別人的工作區誤報一次「標題不符」。
dom=$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$DOMAIN/plugin.json" 2>/dev/null | head -n1)
if [ -z "$dom" ]; then
dom=$(cd "$DOMAIN" 2>/dev/null && pwd) || dom=$DOMAIN
dom=${dom##*/}
fi
dom=${dom#jsc-}
want_title="# jsc-$dom 技能行為清單"
[ "$first" = "$want_title" ] || report - "第一行要是「$want_title」,實際是「$first」"
# --- 2. 節對技能 ---
awk -F"$TAB" '$1 == "SEC" { print $2 }' "$TMPD/parsed.txt" > "$TMPD/sections.txt"
LC_ALL=C sort "$TMPD/sections.txt" > "$TMPD/sections-sorted.txt"
LC_ALL=C uniq -d "$TMPD/sections-sorted.txt" | while IFS= read -r s; do
[ -n "$s" ] && printf '%s:%s:%s\n' "$DOC" "$s" '同一支技能出現多個節,只留一節' >&2
done
if [ -n "$(LC_ALL=C uniq -d "$TMPD/sections-sorted.txt")" ]; then fail=1; fi
LC_ALL=C uniq "$TMPD/sections-sorted.txt" > "$TMPD/sections-uniq.txt"
LC_ALL=C comm -23 "$TMPD/skills.txt" "$TMPD/sections-uniq.txt" > "$TMPD/missing.txt"
LC_ALL=C comm -13 "$TMPD/skills.txt" "$TMPD/sections-uniq.txt" > "$TMPD/extra.txt"
while IFS= read -r s; do
[ -n "$s" ] || continue
report "$s" "skills/$s/SKILL.md 存在,行為清單缺這一節,請補「## $s」"
done < "$TMPD/missing.txt"
while IFS= read -r s; do
[ -n "$s" ] || continue
report "$s" "行為清單多這一節,skills/ 底下沒有這支技能,請移除或改名"
done < "$TMPD/extra.txt"
# --- 3. 節順序 ---
if ! cmp -s "$TMPD/sections.txt" "$TMPD/sections-sorted.txt"; then
report - '節的排列不是技能目錄名的字典序,請重排'
fi
# --- 4~6. 逐節查表格與五個欄位 ---
while IFS= read -r name; do
[ -n "$name" ] || continue
grep -q "^$name\$" "$TMPD/missing.txt" && continue
hdr=$(awk -F"$TAB" -v s="$name" '$1 == "HDR" && $2 == s' "$TMPD/parsed.txt" | wc -l)
hdr=$((hdr + 0))
if [ "$hdr" -eq 0 ]; then
report "$name" '這一節沒有表頭「| 項目 | 內容 |」,五個欄位無從判讀'
continue
fi
[ "$hdr" -eq 1 ] || report "$name" "這一節有 $hdr 張表,合約規定恰好一張"
awk -F"$TAB" -v s="$name" '$1 == "ROW" && $2 == s { print $3 "\t" $4 }' \
"$TMPD/parsed.txt" > "$TMPD/rows.txt"
rows=$(wc -l < "$TMPD/rows.txt")
rows=$((rows + 0))
[ "$rows" -eq 5 ] || report "$name" "表格有 $rows 列,合約規定 5 列:$FIELDS"
i=0
while IFS="$TAB" read -r item body; do
i=$((i + 1))
[ "$i" -le 5 ] || { report "$name" "第 $i 列「$item」是多的,合約只收 5 列"; continue; }
want=$(echo "$FIELDS" | cut -d' ' -f"$i")
[ "$item" = "$want" ] || report "$name" "第 $i 列的項目要是「$want」,實際是「$item」"
[ -n "$body" ] || report "$name" "「$item」的內容欄空白,請補實際行為"
# 第 7 項只在跡象欄成立。認項目名不認列號: 列號錯位時上面那一行已經報過,
# 這裡再報一次只是同一個缺陷印兩遍,反而蓋掉真正沒寫收尾事件的那幾節。
if [ "$item" = '可驗證跡象' ]; then
case $body in
*skill-end*|*events.jsonl*) ;;
*) report "$name" '「可驗證跡象」沒寫到收尾的 skill-end 事件,請補上這一筆(見準則「執行狀態回報」)' ;;
esac
fi
done < "$TMPD/rows.txt"
done < "$TMPD/skills.txt"
# --- 節裡以外的表格 ---
if awk -F"$TAB" '$2 == "-" { found = 1 } END { exit found ? 0 : 1 }' "$TMPD/parsed.txt"; then
report - '有表格落在任何「## 」節之外,請搬進所屬技能的節裡'
fi
if [ "$fail" -eq 0 ]; then
echo "行為清單檢查通過:$DOC 對上 $(wc -l < "$TMPD/skills.txt" | tr -d ' ') 支技能,五個欄位齊全,跡象欄都寫了收尾事件" >&2
else
echo "行為清單檢查不符:$DOC 與 $SKILLS 對不起來,逐項見上方" >&2
fi
exit $fail