Files
hooks/hooks/comment-scope.sh
T
jiantw83 a3ef205489 feat(狀態回報): 技能與 hook 的執行結果寫進本機事件流
現行紀錄只記「被叫用」,欄位是 ts、cli、session、skill,沒有成敗也沒有
結束碼。跑完整輪的技能與開場就中止的技能,在紀錄裡長得一模一樣。hook
成功時更是完全不留紀錄,只有錯誤路徑會寫 wiki,而那條路徑刻意不自動觸發。

事件流走本機檔案,不直接寫 wiki。hook 每次提示都跑,網路寫入會拖垮宿主
CLI;失敗的 hook 自我回報還會疊出迴圈,既有的錯誤回報因此不接在失敗的
hook 上,這裡沿用同一條線。助理巡檢時排空、彙整、寫頁。

hook 端用 EXIT trap 接,一支只加一行。這幾支的 exit 點很多,階段閘門一支
就有五十幾個;逐點改要動到每一條判定路徑,而那些路徑正是閘門的判準,為了
加一行紀錄去動閘門,風險遠大於收益。trap 涵蓋每一條離開路徑,含中途失敗。

狀態預設由結束碼推,推不出來的由 hook 自己覆寫。相依版本檢查與兩道閘門有
這種情形:antigravity 走 deny JSON、kiro 只印警告,兩者擋下時結束碼都是 0,
單看結束碼會把擋下記成放行。

技能的 start 由既有的技能用量 hook 順手發,不必改任何技能文件。end 只能由
技能自己在收尾步驟寫——hook 觸發時技能的實際工作還在後面的模型輪次,看不到
成敗。有 start 沒有配對的 end,就是那一輪中止了。
2026-09-02 15:40:17 +08:00

161 lines
9.6 KiB
Bash
Executable File
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 sh
# comment-scope.sh — 程式碼註解不得夾帶文件相關資訊與暫時性流程資訊(hook > prompt 的強制層)。
# 規則正文的唯一來源:jsc-review 的 references/comment-scope.md。本腳本只實作可用樣式判定的項目;
# 專案代號、客戶名稱這類無法用樣式判定的,交給 jsc-review:code-review 第 2 組人工審查。
#
# 用法:
# comment-scope.sh prompt 注入規則摘要(UserPromptSubmit 或規則檔取文字用)
# comment-scope.sh 掃描剛寫入的單一檔案(PostToolUse)
# comment-scope.sh sweep [dir] 掃描整個工作區這次改過的所有檔案(沒有 post-tool hook 的 CLI 用)
#
# 為什麼要有 sweep:只有 claude 接得到 PostToolUse,逐檔精準掃得到。codex 只有每輪結束的
# notify、kiro 只有 userPromptSubmit、copilot 與 antigravity 只有包裝別名,這四個都拿不到
# 「剛剛寫了哪個檔」,只能改成掃整個工作區的 git diff。時機晚一點,涵蓋範圍一樣。
#
# 輸入相容:
# Claude: PostToolUse 的 stdin JSON,取 tool_input.file_path。
# 其他 CLI: 環境變數 JSC_CHANGED_FILE。
# 兩者都取不到就安靜降級(exit 0)。
#
# 掃描範圍:檔案在 git 工作區內就只掃 `git diff HEAD` 的新增行,不翻舊帳;
# 不在 git 內或檔案尚未追蹤才整檔掃描。sweep 一律只看 git diff。
#
# 結束碼:0=沒命中或資料不足;2=命中,訊息走 stderr 交回模型自行修正(不擋寫入,檔案已經寫好了)。
# 逃生門:JSC_COMMENT_SCOPE=off。
set -u
. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/lib.sh" 2>/dev/null || true
command -v hook_trace >/dev/null 2>&1 && hook_trace "comment-scope ${1:-}"
[ "${JSC_COMMENT_SCOPE:-on}" = "off" ] && exit 0
if [ "${1:-}" = "prompt" ]; then
echo "[jsc] 程式碼註解只寫「為什麼這樣寫」,不寫「這件事記在哪份文件」。禁止寫入:議題與 PR 編號、變更單編號、wiki 頁編號與網址、工作包編號、TDD 待辦編號、使用者故事與驗收條件與測試案例編號、規格章節與稽核項編號、commit hash 與分支名、版本號與 Sprint 與里程碑、人名與認領者與 @ 提及、工時估算、專案代號與客戶名稱、產生來源署名、外部文件連結、審查流程痕跡。"
echo "[jsc] 註解可以寫:日期與時間戳、需求變更歷程、RFC 與 ISO 標準編號、CVE 編號、第三方套件 issue 連結、授權標頭與 SPDX 標記、@deprecated 與 @since 等語言原生標記。命中禁止項就把編號指向的內容搬進註解,再刪掉編號。規則正文見 jsc-review 的 references/comment-scope.md。"
exit 0
fi
hit() { # $1=樣式 $2=說明;命中就把說明與最多三行證據印到 stdout
m=$(printf '%s\n' "$cleaned" | grep -nE "$1" | head -n 3)
[ -n "$m" ] || return 0
printf ' %s\n' "$2"
printf '%s\n' "$m" | sed 's/^/ /'
}
scan_file() { # $1=檔案路徑;命中就把報告印到 stdout 並回傳 1,沒命中回傳 0
f=$1
[ -f "$f" ] || return 0
# 非程式碼檔不受本規則限制:markdown、純文字、資料檔沒有「程式碼註解」。
case "$f" in
*.md|*.markdown|*.txt|*.rst|*.json|*.csv|*.tsv|*.svg|*.lock|*.log|*COMMIT_EDITMSG) return 0 ;;
esac
# 二進位檔跳過。只認 NUL 位元組——拿「非可列印字元」當判準會把所有含中文的檔案誤判成二進位。
raw=$(head -c 1024 "$f" 2>/dev/null | wc -c)
txt=$(head -c 1024 "$f" 2>/dev/null | LC_ALL=C tr -d '\000' | wc -c)
[ "$raw" = "$txt" ] || return 0
d=$(dirname -- "$f")
if git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1 &&
git -C "$d" ls-files --error-unmatch -- "$f" >/dev/null 2>&1; then
lines=$(git -C "$d" diff HEAD -- "$f" 2>/dev/null | sed -n 's/^+[^+]/&/p' | cut -c2-)
[ -n "$lines" ] || return 0
else
lines=$(cat "$f" 2>/dev/null)
fi
# 只留註解行:行首註解符號,或行中出現 // 與 # 的行尾註解。
comments=$(printf '%s\n' "$lines" | grep -E '^[[:space:]]*(//|#|--|\*|/\*|<!--|;|%)|[[:space:]](//|#)[[:space:]]' || true)
[ -n "$comments" ] || return 0
# 白名單先剪掉,再比對禁止樣式。剪掉而不是整行放行——同一行可能一半合規、一半違規。
cleaned=$(printf '%s\n' "$comments" | sed -E \
-e 's#SPDX-License-Identifier:[^[:space:]]*##g' \
-e 's#CVE-[0-9]{4}-[0-9]+##g' \
-e 's#(RFC|ISO|IEEE|ANSI|ECMA|UTF|SHA|MD|AES|RSA|HMAC|PBKDF|TLS|SSL|HTTP|BIG|EUC|JIS|GB|RS|IPV|X)-?[0-9]+(-[0-9]+)?##g' \
-e 's#@(deprecated|since|param|returns?|throws|type|typedef|example|see|link|inheritdoc|override|nullable|internal)##g' \
-e 's#https?://(github|gitlab|bitbucket)\.com/[^[:space:]]*##g' \
-e 's#https?://[^[:space:]]*[{<][^[:space:]]*##g' \
-e 's#[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2}##g')
out=$(
hit '(^|[^[:alnum:]_/])#[0-9]+' '議題編號(#123)'
hit '(^|[^[:alnum:]_])![0-9]+' 'PR、MR 編號(!45)'
hit '[A-Z]{2,6}-[0-9]{1,6}' '工作包、故事、驗收、測試案例、變更單、議題編號(前綴加流水號)'
# 頁名樣式:十五種頁型,尾段是 CONTENTS(目錄頁)、40 碼大寫十六進位(內容頁),
# 或尚未遷移的舊頁編號。舊頁那兩條要留著,不然舊頁編號會漏偵測。
# 舊頁為什麼有 H 開頭這一條:舊的短碼演算法只要首碼落在 0-9ABC 就改寫成 H 加原前 7 碼,
# 十六個十六進位首碼有十三個會命中,所以既有舊頁名大多是 H 開頭,只收 [0-9A-F]{8} 會漏掉。
# 這條式子在別處另有兩份各自獨立的定義,稽核時才比對一致。刻意不共用函式:hook 要能
# 自足執行,執行期相依別的 plugin 路徑,那條路徑一缺,整支 hook 就掃不動了。
hit '(QUESTION|PLAN|ANALYZE|DELIVER|MAINTAIN|REPO|LOG|LEARN|ERROR|CHECK|REPORT|SKILLSET|TOOLING|MONITOR|CONTENTS)_(CONTENTS|[0-9A-F]{8}|H[0-9A-F]{7}|[0-9A-F]{40})' 'jsc wiki 頁面編號'
hit '(todo|TODO|待辦)[[:space:]]*#?[0-9]+' 'TDD 待辦編號'
hit '([Ss]print|里程碑|[Mm]ilestone)[[:space:]]*[0-9]+' 'Sprint、里程碑編號'
hit '(^|[^[:alnum:].])v[0-9]+\.[0-9]+|版本[[:space:]]*v?[0-9]+\.[0-9]+' '版本號'
hit '(commit|提交|hash|SHA)[[:space:]:]*[0-9a-f]{7,40}' 'commit hash'
hit '(branch|分支)[[:space:]:]*[a-z]+/[a-z0-9-]+' '分支名稱'
hit '(^|[[:space:]])@[a-zA-Z][a-zA-Z0-9_.-]{2,}' '人名、認領者、@ 提及(含 @author)'
hit 'https?://[^[:space:]]*(wiki|confluence|atlassian|notion\.so|docs\.google|sharepoint)' 'wiki 或外部文件連結'
hit 'https?://[^[:space:]]*/(issues|pulls)/[0-9]+' '內部議題、PR 連結'
hit '([Gg]enerated (with|by)|Co-Authored-By|本檔(案)?由|AI (產生|生成|撰寫))' '產生來源署名'
hit '預估[[:space:]]*[0-9]+[[:space:]]*(小時|分鐘|人日|人天|天)|[0-9]+[[:space:]]*(人日|人天|工時)' '工時估算'
hit '(規格書|需求書|準則|規範|清單|guidelines)[^。]{0,8}第[[:space:]]*[0-9]+|(規格書|需求書|SRS)[^。]{0,6}[0-9]+(\.[0-9]+)+' '規格文件章節、稽核檢查項編號'
hit '([Cc]ode[[:space:]]+[Rr]eview|[Rr]eview[[:space:]]*(round|finding|findings|feedback|fix|status)|審查(流程|輪次|狀態))' '審查流程字樣'
hit '((審查|[Rr]eview|留言|修正)[^。]{0,12}(第[[:space:]]*[0-9一二三四五六七八九十]+[[:space:]]*輪|追加|後續)|第[[:space:]]*[0-9一二三四五六七八九十]+[[:space:]]*輪[^。]{0,12}(審查|[Rr]eview|留言|修正|追加|後續|檢查)|[Rr]ound[[:space:]]*#?[0-9]+)' '審查輪次描述'
hit '([Ff]inding|問題|缺陷)[[:space:]]*#?[0-9]+' '問題、發現、缺陷編號'
hit '([Hh]ermes|H[0-9]{4}|[Cc]ode[ -]?[Rr]eview[[:space:]]*[Bb]ot)' '審查者代稱或工具名'
hit '(真缺陷|BLOCKING|已解決|未解決)' '審查狀態標籤'
)
[ -n "$out" ] || return 0
printf '%s\n' "$f"
printf '%s\n' "$out"
return 1
}
advice() {
printf ' 修法:把編號指向的內容搬進註解,然後刪掉編號。搬不動就代表那件事不該用註解表達。\n'
printf ' 規則正文與白名單見 jsc-review 的 references/comment-scope.md。誤判時用 JSC_COMMENT_SCOPE=off 關閉。\n'
}
if [ "${1:-}" = "sweep" ]; then
target=${2:-.}
[ -d "$target" ] || exit 0
root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null) || exit 0
[ -n "$root" ] || exit 0
changed=$(git -C "$root" diff --name-only HEAD 2>/dev/null)
[ -n "$changed" ] || exit 0
# 報告累積在暫存檔:迴圈跑在管線的子行程裡,變數帶不回來。
tmp=${TMPDIR:-/tmp}/jsc-comment-scope.$$
: > "$tmp" 2>/dev/null || exit 0
printf '%s\n' "$changed" | while IFS= read -r rel; do
[ -n "$rel" ] || continue
scan_file "$root/$rel" >> "$tmp" 2>/dev/null
done
if [ -s "$tmp" ]; then
{
printf '[jsc] 工作區有註解夾帶文件相關資訊或審查流程痕跡,請就地修正:\n'
sed 's/^/ /' "$tmp"
advice
} >&2
rm -f "$tmp"
exit 2
fi
rm -f "$tmp"
exit 0
fi
read_stdin 2>/dev/null || STDIN_JSON=""
file=$(json_str file_path 2>/dev/null || true)
[ -n "$file" ] || file="${JSC_CHANGED_FILE:-}"
[ -n "$file" ] || exit 0
report=$(scan_file "$file") && exit 0
{
printf '[jsc] 程式碼註解夾帶了文件相關資訊或審查流程痕跡,請就地修正:\n'
printf '%s\n' "$report"
advice
} >&2
exit 2