From b45a98af870a0f7c737c887e866cb4516dcdb60a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 18:18:10 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(hooks-install):=20=E5=AE=89=E8=A3=9D?= =?UTF-8?q?=E6=94=B9=E7=82=BA=E5=85=88=E6=B8=85=E7=A9=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?=20hook=20=E5=86=8D=E6=8E=A5=E7=B7=9A=E4=B8=A6=E9=A9=97?= =?UTF-8?q?=E5=9F=B7=E8=A1=8C=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/hooks-install/SKILL.md | 23 +- tools/scan-hook-errors.sh | 190 ++++++++++++++ tools/wire-cli.sh | 457 +++++++++++++++++++++++++++++++++- 3 files changed, 652 insertions(+), 18 deletions(-) create mode 100755 tools/scan-hook-errors.sh diff --git a/skills/hooks-install/SKILL.md b/skills/hooks-install/SKILL.md index 0d80b40..390188e 100644 --- a/skills/hooks-install/SKILL.md +++ b/skills/hooks-install/SKILL.md @@ -1,27 +1,38 @@ --- name: hooks-install -description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SDLC model gate, plugin version guard) into every installed AI CLI. Detect CLIs via jsc-cli detect-clis.sh, then apply native hook config, the jsc-wrap.sh launcher, or an instruction-file fallback per CLI. Use after installing or updating the jsc plugin set; not for writing new hooks. +description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SDLC model gate, plugin version guard) into every installed AI CLI, purging all pre-existing hooks first — third-party ones included, backed up before removal. Drive it per CLI through tools/wire-cli.sh purge, tools/wire-cli.sh, tools/wire-cli.sh smoke and tools/scan-hook-errors.sh. Hand any hook error, wiring or runtime, to jsc-hooks:repair, which must finish with a PR against develop; aborting the rest of the install to start that repair is allowed. Use after installing or updating the jsc plugin set; not for writing new hooks. --- # hooks-install — wire jsc hooks into every installed CLI -Goal: make the five hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`, `version-guard.sh`) effective in every CLI. +Goal: make the five hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`, `version-guard.sh`) effective in every CLI, with nothing else wired alongside them. + +Install on a clean slate. Every CLI is purged of all hooks first, third-party ones included, so a later failure has exactly one owner. `tools/wire-cli.sh purge` backs up every file it touches before it removes anything, so the removal stays reversible. + Only claude has both PreToolUse and UserPromptSubmit, so only claude reports `wired`. On codex, copilot, antigravity and kiro the version guard cannot be wired at all and the SDLC gate degrades to the skill-step check, so all four report `degraded` — report that gap as the script words it instead of implying every CLI is covered. The lock file still works on those four because the SDLC skills call `sdlc-gate.sh lock {stage}` directly — that call is where the capability-tag comparison happens, so the gate keeps its force even where the prompt hook cannot be wired. The gate needs `$JSC_HOME/model-tags.tsv`; when it is missing, report that `jsc-cli:models` (or `jsc-cli/tools/model-tags.sh sync`) must run once, because `sdlc-gate.sh lock` refuses to lock without it. -When `tools/wire-cli.sh` reports `failed`, hand the failure to `jsc-hooks:repair` right away. Do not leave a failed wiring as a dead end. + +Treat any hook error as repair work, whether it appeared while wiring or while running. Stopping the remaining installs to start that repair is the right call; leaving a broken hook wired is not. + The detailed flow **MUST run as a sub agent**; the main agent only reports the summary. ## Steps 1. Run `jsc-cli/tools/detect-clis.sh`. Done when you hold the list of installed CLIs; when the list is empty, report that and stop. -2. For each installed CLI, run `tools/wire-cli.sh {cli}`. The script owns both the wiring and its verification: it writes the config, alias or hook file inside a `` (or `# jsc-hooks`) marker block, re-reads every file it wrote, and confirms the block is present and correctly placed before it prints a success status. Trust its first line, `status=wired|degraded|skipped|failed reason=...`. Exit 2 means a bad CLI name, not a wiring outcome — fix the name and rerun. Done when every installed CLI has printed exactly one `status=` line and none exited 2. -3. For each CLI whose status is `failed`, record it: run `tools/report-error.sh --hook wire-cli.sh --exit 4 --summary "{the reason field}" --cli {cli}` and feed the script's `[jsc]` output in on stdin. Then hand the failure to `jsc-hooks:repair`, which must run as a sub agent and must finish by opening a PR against `develop`. Done when each `failed` CLI has either an `ERROR_{HASH}` page name on stdout, or an empty exit 0 meaning `JSC_WIKI_REPO_ERROR` and `JSC_WIKI_REPO` are both unset — in that second case carry the reason into step 4 instead. Skip this step when no CLI reported `failed`. -4. Report the exact `status=` line `tools/wire-cli.sh` printed for each CLI, plus the `ERROR_{HASH}` page and repair PR URL for any `failed` one. Done when every detected CLI has exactly one reported status: wired, degraded, skipped or failed, each with its reason, and every failed wiring has a repair PR against `develop`. +2. For each installed CLI, run `tools/wire-cli.sh purge {cli}`. The script backs up every file it touches, removes all hooks, re-reads each file to confirm the removal, and restores the backup by itself when a check fails. Done when every CLI has printed exactly one `status=purged|skipped|failed reason=...` line and you have noted the backup directory path from its `[jsc]` output. +3. For each installed CLI, run `tools/wire-cli.sh {cli}`. The script owns both the wiring and its verification: it writes the config, alias or hook file inside a `` (or `# jsc-hooks`) marker block, re-reads every file it wrote, and confirms the block is present and correctly placed before it prints a success status. Trust its first line, `status=wired|degraded|skipped|failed reason=...`. Exit 2 means a bad CLI name, not a wiring outcome — fix the name and rerun. Done when every installed CLI has printed exactly one `status=` line and none exited 2. +4. For each installed CLI, run `tools/wire-cli.sh smoke {cli}`. This runs all five hooks once each and catches what the wiring check cannot see: a hook that is wired correctly and still fails when it executes. Done when every CLI has printed one `status=ok|failed reason=...` line plus one result line per hook. +5. For each installed CLI, run `tools/scan-hook-errors.sh --cli {cli}`. Only claude keeps hook results in its native records and can answer `clean` or `errors`; codex, copilot, antigravity and kiro answer `unavailable`, and their runtime evidence comes from step 4 alone. Done when every CLI has printed one `status=clean|errors|unavailable reason=...` line and the four `unavailable` CLIs are reported as exactly that, not as clean. +6. For each error — `purge` failed, wiring failed, smoke failed, or a scanned error with `jsc=true` — run `tools/report-error.sh --hook {script name} --exit {code} --summary "{reason}" --cli {cli}` with the script's `[jsc]` output on stdin, then hand the failure to `jsc-hooks:repair`, which **MUST run as a sub agent** and must finish by opening a PR against `develop`. Aborting the remaining installs here is allowed as long as the repair starts. A scanned error with `jsc=false` belongs to a third-party hook: report it and leave it alone. Done when each error has either an `ERROR_{HASH}` page name on stdout, or an empty exit 0 meaning `JSC_WIKI_REPO_ERROR` and `JSC_WIKI_REPO` are both unset — in that second case carry the reason into step 7 instead. Skip this step when every CLI passed all four checks. +7. Report four results per CLI — purge, wiring, smoke, scan — each with the reason its script printed, plus any `ERROR_{HASH}` page name and repair PR URL. Done when every detected CLI has exactly one status per check and every repair has a PR against `develop`. ## Notes - Every hook script accepts both stdin JSON and environment variables (`JSC_CLI`, `JSC_SESSION_ID`, `JSC_SKILL`, `JSC_TOOL_NAME`, `JSC_MODEL`); `jsc-wrap.sh` sets the first two itself. - `session-timer.sh` takes `start` (keep an existing start time), `restart` (always overwrite it, for a CLI with no session id — kiro), `mark` and `report`. `wire-cli.sh` picks the right one per CLI; do not hand-edit the generated hook files. +- `purge` reaches the user-level config only. Hooks that another plugin ships in its own `hooks.json` stay active, and uninstalling that plugin is the only way to clear them — say so when reporting, and treat their errors as third-party. +- Backups land in `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`, one directory per purge run, under the original file names. Hand that path to the user whenever a purge removed something. +- `smoke` treats `sdlc-gate.sh check` exit 2 as healthy: that exit is the stage lock blocking a turn on purpose, not a runtime error. - `tools/report-error.sh` is operator- or skill-invoked only. Never wire it to fire from a failing hook: hooks stay silent and exit 0, and a failing hook that reports itself can loop. - Data lands in `$JSC_HOME` (default `~/.jsc`), consumed by `jsc-log:worklog` and `jsc-log:stats`. diff --git a/tools/scan-hook-errors.sh b/tools/scan-hook-errors.sh new file mode 100755 index 0000000..98884d0 --- /dev/null +++ b/tools/scan-hook-errors.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env sh +# scan-hook-errors.sh — 掃 CLI 的原生紀錄,找出 hook 的「執行期」錯誤:接線寫對了、 +# hook 也真的被觸發了,但跑起來出錯(缺執行檔、路徑錯、權限不足)。 +# 用法: scan-hook-errors.sh --cli {claude|codex|copilot|antigravity|kiro} +# +# 覆蓋範圍要據實回報,不得暗示每個 CLI 都掃得到: +# claude 有 hook 結果紀錄,掃 ~/.claude/projects/**/*.jsonl 兩種紀錄—— +# `"type":"hook_non_blocking_error"` 的 attachment +# (hookName、hookEvent、exitCode、stderr、command、timestamp) +# 與非空的 `"hookErrors":[...]` +# codex、copilot、antigravity、kiro 原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與 +# stderr,一律回報 unavailable,改用 wire-cli.sh smoke {cli} +# 主動跑一輪驗執行期 +# +# 去重: 以 $JSC_HOME/errors/scan-state/ 記住每個日誌檔已掃描的位元組數(同 tools/scan-logs.sh), +# 重掃只讀新增段落。兩種紀錄各自成筆,不互相配對:同一次失敗在不同 transcript 條目 +# 裡各留一筆,靠位置猜配對只會把真錯誤併掉。 +# +# 產出: 每筆錯誤附加一行 JSON 到 $JSC_HOME/errors/hooks.jsonl(格式比照 hooks/skill-usage.sh): +# {ts,cli,hook,event,exit,detail,jsc} +# jsc 欄位:command 或 stderr 命中 ste100-guard.sh、session-timer.sh、skill-usage.sh、 +# sdlc-gate.sh、version-guard.sh 任一支就是 true,否則 false。分得出來才用得上—— +# 非 jsc 的 hook 錯誤不是 jsc 該修的,hooks-install 只回報、不轉 jsc-hooks:repair。 +# +# 輸出: 第一行 `status={clean|errors|unavailable} reason=...`(可供程式判讀), +# errors 時其後每筆一行人類可讀的繁中摘要(hook 名、退出碼、是否屬 jsc)。 +# 結束碼: 0=clean 或 unavailable、1=errors、2=用法錯誤 +set -u +HERE=$(cd "$(dirname "$0")" && pwd) +. "$HERE/../hooks/lib.sh" + +cli="" +while [ $# -gt 0 ]; do + case "$1" in + --cli) cli="${2:-}"; shift 2 ;; + *) shift ;; + esac +done +case "$cli" in + claude|codex|copilot|antigravity|kiro) ;; + *) + echo "用法:scan-hook-errors.sh --cli {claude|codex|copilot|antigravity|kiro}" >&2 + exit 2 ;; +esac + +ERRDIR="$JSC_HOME/errors" +STATE="$ERRDIR/scan-state" +OUT="$ERRDIR/hooks.jsonl" +mkdir -p "$STATE" 2>/dev/null || true + +case "$cli" in + codex|copilot|antigravity|kiro) + printf 'status=unavailable reason=%s\n' "$cli 沒有 hook 結果紀錄,執行期錯誤掃不到" + echo "[jsc] $cli:原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與 stderr。" + echo "[jsc] $cli:改跑 tools/wire-cli.sh smoke $cli,主動執行五支 hook 驗執行期。" + exit 0 ;; +esac + +# 印出日誌檔自上次掃描後的新增內容,並更新位移(位移即去重機制) +new_content() { # $1=file + key=$(printf '%s' "$1" | cksum | tr ' \t' '--') + off_f="$STATE/$cli-$key.offset" + off=$(cat "$off_f" 2>/dev/null || echo 0) + size=$(wc -c < "$1" 2>/dev/null || echo 0) + [ "$size" -gt "$off" ] 2>/dev/null || return 0 + tail -c +"$((off + 1))" "$1" 2>/dev/null + echo "$size" > "$off_f" +} + +# 從新增內容萃取錯誤,每筆一行 TSV:hook、event、exit、jsc、ts、detail。 +# 欄位用手寫掃描取,不靠正規式一次抓完:stderr 裡有轉義引號,正規式會抓過頭。 +extract_errors() { + awk ' + # 取 JSON 字串或純量欄位;字串保留原本的轉義序列,寫回 jsonl 時才不必重新轉義。 + function jstr(s, name, p, i, c, out) { + p = index(s, "\"" name "\":") + if (p == 0) return "" + i = p + length(name) + 3 + while (substr(s, i, 1) == " ") i++ + if (substr(s, i, 1) != "\"") { + out = "" + while (i <= length(s) && substr(s, i, 1) !~ /[,}\]]/) { out = out substr(s, i, 1); i++ } + return out + } + i++ + out = "" + while (i <= length(s)) { + c = substr(s, i, 1) + if (c == "\\") { out = out substr(s, i, 2); i += 2; continue } + if (c == "\"") break + out = out c; i++ + } + return out + } + function is_jsc(t) { + if (index(t, "ste100-guard.sh") || index(t, "session-timer.sh") \ + || index(t, "skill-usage.sh") || index(t, "sdlc-gate.sh") \ + || index(t, "version-guard.sh")) return "true" + return "false" + } + # 摘要收斂成單行短字串:TSV 欄位不能有 tab,jsonl 欄位不能有裸換行; + # 截斷可能切到半個轉義序列,尾端的反斜線要清掉才是合法 JSON 字串。 + function clean(t, x) { + x = t + gsub(/\t/, " ", x) + gsub(/\\n/, " ", x) + x = substr(x, 1, 300) + sub(/\\+$/, "", x) + if (x == "") x = "(無錯誤輸出)" + return x + } + # hookErrors 的內容是 JSON 陣列原文,元素外面那對引號是結構、不是文字。 + # 直接寫進 jsonl 會多出一對裸引號把字串切斷,所以先拆成純文字,多筆用分號串起來。 + function unarray(a) { + gsub(/","/, "; ", a) + sub(/^"/, "", a) + sub(/"$/, "", a) + return a + } + function emit(hook, ev, ec, own, ts, detail) { + if (hook == "") hook = "unknown" + if (ev == "") ev = "-" + if (ec == "") ec = "-" + printf "%s\t%s\t%s\t%s\t%s\t%s\n", hook, ev, ec, own, ts, clean(detail) + } + /"type":"hook_non_blocking_error"/ { + err = jstr($0, "stderr"); cmd = jstr($0, "command") + detail = (err != "" ? err : cmd) + emit(jstr($0, "hookName"), jstr($0, "hookEvent"), jstr($0, "exitCode"), \ + is_jsc(cmd " " err), jstr($0, "timestamp"), detail) + next + } + # 非空的 hookErrors:只有訊息陣列,沒有 hook 名與退出碼,欄位據實留空。 + /"hookErrors":\[[^]]/ { + p = index($0, "\"hookErrors\":[") + rest = substr($0, p + length("\"hookErrors\":[")) + q = index(rest, "]") + arr = (q > 1 ? substr(rest, 1, q - 1) : rest) + emit("", "", "", is_jsc(arr), jstr($0, "timestamp"), unarray(arr)) + } + ' +} + +d="$HOME/.claude/projects" +if [ ! -d "$d" ]; then + printf 'status=clean reason=%s\n' "找不到 $d,沒有 claude 紀錄可掃" + echo "[jsc] claude:這台機器沒有 transcript 目錄,掃不到紀錄跟沒有錯誤是兩件事,請改跑 tools/wire-cli.sh smoke claude。" + exit 0 +fi + +recs=$(mktemp) || { printf 'status=clean reason=%s\n' "無法建立暫存檔"; exit 0; } +files=$(mktemp) || { rm -f "$recs"; printf 'status=clean reason=%s\n' "無法建立暫存檔"; exit 0; } +trap 'rm -f "$recs" "$files"' EXIT +find "$d" -type f -name '*.jsonl' 2>/dev/null | sort > "$files" +# 迴圈從檔案讀,不放在管線右邊:管線會開子 shell,計數與旗標傳不回本 shell。 +while IFS= read -r f; do + [ -n "$f" ] || continue + new_content "$f" | extract_errors >> "$recs" +done < "$files" + +total=0; jsc_n=0 +human=$(mktemp) || { printf 'status=clean reason=%s\n' "無法建立暫存檔"; exit 0; } +TAB=$(printf '\t') +while IFS="$TAB" read -r hook ev ec isjsc ts detail; do + [ -n "$hook" ] || continue + [ -n "$ts" ] || ts=$(now_iso) + total=$((total + 1)) + printf '{"ts":"%s","cli":"%s","hook":"%s","event":"%s","exit":"%s","detail":"%s","jsc":%s}\n' \ + "$ts" "$cli" "$hook" "$ev" "$ec" "$detail" "$isjsc" >> "$OUT" + if [ "$isjsc" = true ]; then + jsc_n=$((jsc_n + 1)); own="屬 jsc" + else + own="非 jsc 的第三方 hook" + fi + printf '[jsc] %s(%s 事件)exit %s,%s:%s\n' "$hook" "$ev" "$ec" "$own" "$detail" >> "$human" +done < "$recs" + +if [ "$total" -eq 0 ]; then + rm -f "$human" + printf 'status=clean reason=%s\n' "claude 紀錄裡沒有新的 hook 執行期錯誤" + exit 0 +fi + +printf 'status=errors reason=%s\n' "掃到 $total 筆 hook 執行期錯誤,其中 $jsc_n 筆屬 jsc" +cat "$human" +rm -f "$human" +echo "[jsc] 屬 jsc 的錯誤請先以 tools/report-error.sh 回報,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" +echo "[jsc] 非 jsc 的第三方 hook 錯誤只回報,不由 jsc 修正。" +echo "[jsc] 全部紀錄已附加到 $OUT。" +exit 1 diff --git a/tools/wire-cli.sh b/tools/wire-cli.sh index 764db10..2785397 100755 --- a/tools/wire-cli.sh +++ b/tools/wire-cli.sh @@ -1,7 +1,18 @@ #!/usr/bin/env sh -# wire-cli.sh — 把 jsc 五支 hook 接線到單一 CLI(供 hooks-install 技能呼叫)。 -# 用法: wire-cli.sh {claude|codex|copilot|antigravity|kiro} -# 行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加): +# wire-cli.sh — 單一 CLI 的 hook 生命週期:先清、再接、再冒煙(供 hooks-install 技能呼叫)。 +# 用法: +# wire-cli.sh {claude|codex|copilot|antigravity|kiro} 接線 +# wire-cli.sh purge {claude|codex|copilot|antigravity|kiro} 備份後移除該 CLI 的所有 hook +# wire-cli.sh smoke {claude|codex|copilot|antigravity|kiro} 跑一輪五支 hook,驗執行期 +# +# purge 移除的是「所有 hook」,含非 jsc 的第三方項目。安裝一律先 purge 再接線:混著別人的 +# hook 接線,出錯時分不清是誰的 hook 壞掉,也修不了。移除前每個要動的檔案先原樣複製到 +# $JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/,備份失敗就不移除。 +# +# smoke 在接線之後跑,補上接線驗證看不到的那一半:接線只證明設定寫對位置,證不了 hook +# 跑起來不出錯(缺 node、路徑錯、權限不足都只在真的執行時才現形)。 +# +# 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加): # claude — 什麼都不用寫,hooks.json 已自動接線五支 hook # codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時); # 在 config.toml 設 notify(每輪補 session-timer.sh start 再 mark,JSC_CLI=codex); @@ -24,11 +35,16 @@ # 正確位置(codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對 # 且 on、run 在最上層)。腳本說寫好了卻寫錯位置,是最難查的失敗,所以驗證放在腳本裡。 # -# 輸出: 第一行固定為 `status={wired|degraded|skipped|failed} reason=...`(可供程式判讀), -# 其後為人類可讀的繁中說明。 -# 結束碼: 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查) +# 輸出: 第一行固定為 `status=... reason=...`(可供程式判讀),其後為人類可讀的繁中說明。 +# 結束碼(接線): 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查) # 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過) # 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報) +# 結束碼(purge): 0=purged 2=用法錯誤 3=skipped 4=failed +# 結束碼(smoke): 0=ok 2=用法錯誤 4=failed +# +# JSC_CLAUDE_SETTINGS_DIR 可覆寫 claude 使用者層設定檔目錄(預設 ~/.claude)。 +# 有這個逃生門才測得動 purge 的 JSON 刪鍵:預設路徑是使用者自己的設定檔,拿真檔案試刪 +# 等於拿使用者的環境當測試場。指向一份複製品就能完整跑過 purge claude 而不動到本人設定。 set -u HERE=$(cd "$(dirname "$0")" && pwd) ROOT=$(cd "$HERE/.." && pwd) @@ -36,12 +52,21 @@ HOOKS="$ROOT/hooks" # cli_bin(CLI 代號 → 實際執行檔)的唯一來源在 lib.sh,包裝啟動器也用同一份 . "$HOOKS/lib.sh" +usage() { + echo "用法:wire-cli.sh [purge|smoke] {claude|codex|copilot|antigravity|kiro}" >&2 + exit 2 +} + +# 第一個參數是子命令時走新流程,否則沿用原本的「wire-cli.sh {cli}」接線。 +action=wire +case "${1:-}" in + purge|smoke) action="$1"; shift ;; +esac + cli="${1:-}" case "$cli" in claude|codex|copilot|antigravity|kiro) ;; - *) - echo "用法:wire-cli.sh {claude|codex|copilot|antigravity|kiro}" >&2 - exit 2 ;; + *) usage ;; esac # STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出 @@ -111,10 +136,12 @@ toml_root_key() { ' "$1" 2>/dev/null } -# 驗證:JSON 括號成對,且某個鍵出現在最上層($1=檔案 $2=鍵名)。 +# 驗證:JSON 括號成對,且某個鍵出現在最上層($1=檔案 $2=鍵名 $3=要求)。 # 解析失敗(括號不成對、字串沒收尾)也回傳非 0,所以這支同時當語法檢查用。 +# $3=key(預設)要求該鍵存在;$3=pairs 只檢查語法,不管鍵在不在——purge 之後要驗的是 +# 「鍵不見了而且檔案還是合法 JSON」,這兩件事得分開問,不然刪壞檔也會被當成刪成功。 json_top_key() { - awk -v k="$2" ' + awk -v k="$2" -v want="${3:-key}" ' { s = s $0 "\n" } END { n = length(s); depth = 0; i = 1; found = 0; bad = 0 @@ -138,10 +165,23 @@ json_top_key() { else if (c == "}" || c == "]") { depth--; if (depth < 0) { bad = 1; break } } i++ } - exit((found && !bad && depth == 0) ? 0 : 1) + if (bad || depth != 0) exit(1) + exit((want == "pairs" || found) ? 0 : 1) }' "$1" 2>/dev/null } +# 驗證:JSON 語法成對(括號收齊、字串收尾)。鍵不管。 +json_pairs_ok() { json_top_key "$1" __no_such_key__ pairs; } + +# 找出已存在的 shell rc 檔(purge 用)。rc_files 找不到會建立 ~/.bashrc,移除流程不建檔: +# 為了清 hook 而生出一個新檔案,是把環境弄得更亂,不是更乾淨。 +rc_files_existing() { + for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do + [ -f "$f" ] && printf '%s\n' "$f" + done + return 0 +} + # 找出可寫入別名的 shell rc 檔;都不存在就以 ~/.bashrc 為預設(自動建立)。 # 印出找到或建立的 rc 檔路徑,一行一個。 rc_files() { @@ -168,6 +208,205 @@ write_alias_rc() { [ "$_ok" = 1 ] } +# --- 移除(purge)用的函式 --- + +# 以標記整段移除(冪等)。與 replace_block 對稱:同一組標記,一支寫入、一支移除。 +# 標記不存在就當成已移除、回傳成功——purge 重跑不該因為「上次已經清掉了」而失敗。 +# $1=檔案 $2=開頭標記行 $3=結尾標記行 +remove_block() { + file="$1"; bopen="$2"; bshut="$3" + [ -f "$file" ] || return 0 + grep -qF "$bopen" "$file" 2>/dev/null || return 0 + awk -v bopen="$bopen" -v bshut="$bshut" ' + $0==bopen { skip=1; next } + $0==bshut { skip=0; next } + skip { next } + { print } + ' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } + mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } +} + +# 移除 rc 檔裡所有 `# jsc-hooks*` 標記段落,不管後面接哪個 CLI 名。 +# 舊版接線可能留下已改名的段落,逐一指名會漏掉,所以用前綴一次掃乾淨。 +# $1=檔案 +remove_rc_blocks() { + file="$1" + [ -f "$file" ] || return 0 + grep -q '^# jsc-hooks' "$file" 2>/dev/null || return 0 + awk ' + /^# jsc-hooks/ { skip=1; next } + /^# \/jsc-hooks/ { skip=0; next } + skip { next } + { print } + ' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } + mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } +} + +# 移除 TOML 的根層鍵(第一個表頭之前的那個鍵),含非 jsc 設的值。 +# 值可能是多行陣列或多行行內表,所以要追括號深度,收齊才停;只刪一行會留下孤兒括號。 +# $1=檔案 $2=鍵名 +remove_toml_root_key() { + file="$1"; key="$2" + [ -f "$file" ] || return 0 + awk -v k="$key" ' + BEGIN { intable=0; drop=0; depth=0 } + /^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { intable=1 } + { + if (drop) { + depth += gsub(/[[{]/, "&") - gsub(/[]}]/, "&") + if (depth <= 0) drop=0 + next + } + if (!intable && $0 ~ "^[ \t]*" k "[ \t]*=") { + depth = gsub(/[[{]/, "&") - gsub(/[]}]/, "&") + if (depth > 0) drop=1 + next + } + print + } + ' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } + mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } +} + +# 刪掉 JSON 最上層的 hooks 鍵(含後面多餘的逗號),逐字元追蹤引號與括號深度。 +# jq 在目標機器上不保證存在,所以要有這條純 awk 的路;只用 sed 刪不了嵌套的 {...}。 +# 追蹤引號是必要的:字串裡的 { 與 } 不算深度,漏算就會把整段設定切壞。 +# $1=檔案,結果印到標準輸出;解析不出來(括號不成對、字串沒收尾)就 exit 1,不輸出半份檔案。 +awk_del_hooks() { + awk ' + function skip_string(s, i, n, c) { + i++ + while (i <= n) { + c = substr(s, i, 1) + if (c == "\\") { i += 2; continue } + if (c == "\"") return i + 1 + i++ + } + return 0 + } + function skip_value(s, i, n, c, d) { + while (i <= n && substr(s, i, 1) ~ /[ \t\r\n]/) i++ + c = substr(s, i, 1) + if (c == "\"") return skip_string(s, i, n) + if (c == "{" || c == "[") { + d = 0 + while (i <= n) { + c = substr(s, i, 1) + if (c == "\"") { i = skip_string(s, i, n); if (i == 0) return 0; continue } + if (c == "{" || c == "[") { d++; i++; continue } + if (c == "}" || c == "]") { d--; i++; if (d == 0) return i; continue } + i++ + } + return 0 + } + while (i <= n && substr(s, i, 1) !~ /[,}\]\t\r\n ]/) i++ + return i + } + { s = s $0 "\n" } + END { + n = length(s); i = 1; depth = 0; out = "" + while (i <= n) { + c = substr(s, i, 1) + if (c == "\"") { + start = i; buf = ""; j = i + 1 + while (j <= n) { + c = substr(s, j, 1) + if (c == "\\") { j += 2; continue } + if (c == "\"") break + buf = buf c; j++ + } + if (j > n) exit 1 + i = j + 1 + j = i + while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++ + if (depth == 1 && buf == "hooks" && substr(s, j, 1) == ":") { + i = skip_value(s, j + 1, n) + if (i == 0) exit 1 + j = i + while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++ + if (substr(s, j, 1) == ",") { + # 後面還有成員:連逗號一起吃掉,並把 hooks 那行留下的縮排收乾淨 + i = j + 1 + sub(/[ \t]+$/, "", out) + while (i <= n && substr(s, i, 1) ~ /[ \t\r]/) i++ + if (substr(s, i, 1) == "\n") i++ + } else { + # hooks 是最後一個成員:改刪前一個逗號,不刪會留下「, }」這種壞掉的 JSON + sub(/,[ \t\r\n]*$/, "", out) + } + continue + } + out = out substr(s, start, i - start) + continue + } + if (c == "{" || c == "[") depth++ + else if (c == "}" || c == "]") { depth--; if (depth < 0) exit 1 } + out = out c; i++ + } + if (depth != 0) exit 1 + printf "%s", out + }' "$1" +} + +# 刪掉設定檔最上層的 hooks 鍵:有 jq 就用 jq,沒有就走 awk_del_hooks。$1=檔案 +json_del_hooks() { + _f="$1" + [ -f "$_f" ] || return 0 + if command -v jq >/dev/null 2>&1; then + jq 'del(.hooks)' "$_f" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } + else + awk_del_hooks "$_f" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } + fi + [ -s "$_f.jsc-tmp" ] || { rm -f "$_f.jsc-tmp"; return 1; } + mv "$_f.jsc-tmp" "$_f" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } +} + +# --- 備份:先備份才准移除 --- + +BACKUP_DIR="" +BACKUP_STAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_LIST="" # 每行「{備份檔}{原檔}」,還原時反向複製回去 + +# 備份目錄延後建立:沒有檔案要動時不留空目錄。 +# 只設全域變數、不印路徑:呼叫端若寫成 $(backup_dir) 就變成子 shell,設好的 BACKUP_DIR +# 與 BACKUP_LIST 傳不回本 shell,接著的備份與還原全部失準。 +ensure_backup_dir() { + [ -z "$BACKUP_DIR" ] || return 0 + _d="$JSC_HOME/backup/hooks/$cli/$BACKUP_STAMP" + mkdir -p "$_d" 2>/dev/null || return 1 + BACKUP_LIST=$(mktemp) || return 1 + BACKUP_DIR="$_d" + return 0 +} + +# 原樣複製一份到備份目錄,保留原檔名;同名就加 -1、-2 後綴(不同目錄可能有同名檔)。 +# 複製失敗回傳 1,呼叫端必須就此停手:沒有備份就移除,等於把使用者的設定弄不見。 +backup_file() { # $1=檔案 + _src="$1" + [ -f "$_src" ] || return 0 + ensure_backup_dir || return 1 + _base=$(basename "$_src") + _dst="$BACKUP_DIR/$_base"; _n=0 + while [ -e "$_dst" ]; do + _n=$((_n + 1)); _dst="$BACKUP_DIR/$_base-$_n" + done + cp "$_src" "$_dst" 2>/dev/null || return 1 + [ -f "$_dst" ] || return 1 + printf '%s\t%s\n' "$_dst" "$_src" >> "$BACKUP_LIST" || return 1 + return 0 +} + +# 還原這次所有備份(驗證沒過時用)。已刪除的檔案會被複製回來。 +restore_backups() { + [ -n "$BACKUP_LIST" ] && [ -f "$BACKUP_LIST" ] || return 0 + while IFS="$(printf '\t')" read -r _b _o; do + [ -n "$_b" ] && [ -n "$_o" ] || continue + mkdir -p "$(dirname "$_o")" 2>/dev/null || true + cp "$_b" "$_o" 2>/dev/null || true + done < "$BACKUP_LIST" + return 0 +} + skip() { # $1=reason printf 'status=skipped reason=%s\n' "$1" echo "[jsc] 略過:$1" @@ -181,6 +420,200 @@ fail() { # $1=reason exit 4 } +# purge 專用的失敗出口:先把備份還原回去,再回報。移除做一半的環境比沒動過更難修。 +pfail() { # $1=reason + restore_backups + printf 'status=failed reason=%s\n' "$1" + echo "[jsc] 移除沒完成,已從備份還原:$1" >&2 + [ -n "$BACKUP_DIR" ] && echo "[jsc] 備份目錄:$BACKUP_DIR" >&2 + echo "[jsc] 請先以 tools/report-error.sh 回報這次失敗,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2 + exit 4 +} + +purged() { # $1=reason + printf 'status=purged reason=%s\n' "$1" + if [ -n "$BACKUP_DIR" ]; then + echo "[jsc] $cli:已移除全部 hook,移除前的原檔備份在 $BACKUP_DIR。" + else + echo "[jsc] $cli:沒有找到任何 hook 設定,已是乾淨狀態,未建立備份目錄。" + fi +} + +if [ "$action" = purge ]; then + case "$cli" in + claude) + bin=$(cli_bin claude) + command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 claude 執行檔" + # claude 的 hook 全部宣告在使用者層設定檔的 hooks 鍵裡,清掉那個鍵就等於清掉所有 hook。 + cdir="${JSC_CLAUDE_SETTINGS_DIR:-$HOME/.claude}" + done_files="" + for f in "$cdir/settings.json" "$cdir/settings.local.json"; do + [ -f "$f" ] || continue + # 先問語法:讀不懂的設定檔不能刪鍵,也不能當成「沒有 hooks 鍵」帶過—— + # 那會回報 purged 卻留著整套 hook,比直接說失敗更難查。 + json_pairs_ok "$f" || pfail "$f 不是成對的 JSON,讀不懂就不動它,請先修好這個檔案" + json_top_key "$f" hooks || continue + backup_file "$f" || pfail "無法備份 $f,沒有備份就不移除" + json_del_hooks "$f" || pfail "無法從 $f 刪除 hooks 鍵" + json_pairs_ok "$f" || pfail "$f 刪除 hooks 鍵後 JSON 括號不成對" + ! json_top_key "$f" hooks || pfail "$f 刪除後最上層仍有 hooks 鍵" + done_files="$done_files $f" + done + if [ -n "$done_files" ]; then + purged "已從 claude 使用者層設定檔刪除 hooks 鍵,含非 jsc 的第三方項目" + echo "[jsc] claude:已處理的設定檔:$done_files" + else + purged "claude 使用者層設定檔沒有 hooks 鍵,沒有 hook 要移除" + fi + echo "[jsc] claude:其他 plugin 自帶的 hooks.json 不在使用者設定檔裡,purge 動不到;要靠移除該 plugin 才能清掉。" + echo "[jsc] claude:jsc 自己的 hooks/hooks.json 同樣隨 plugin 提供,移除 jsc-hooks plugin 才會消失。" + exit 0 ;; + + codex) + bin=$(cli_bin codex) + command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 codex 執行檔" + CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" + config="$CODEX_HOME/config.toml" + agents="$CODEX_HOME/AGENTS.md" + if [ -f "$config" ]; then + if has_block "$config" "# jsc-hooks" || toml_root_key "$config" notify; then + backup_file "$config" || pfail "無法備份 $config,沒有備份就不移除" + remove_block "$config" "# jsc-hooks" "# /jsc-hooks" \ + || pfail "無法從 $config 移除 jsc-hooks 標記段落" + remove_toml_root_key "$config" notify || pfail "無法從 $config 移除根層 notify" + has_block "$config" "# jsc-hooks" && pfail "$config 移除後仍讀得到 jsc-hooks 標記段落" + toml_root_key "$config" notify && pfail "$config 移除後仍有根層 notify" + fi + fi + # rc 檔清所有 `# jsc-hooks*` 段落:別名段落沒有分 CLI 的必要,一次清乾淨最可靠。 + rclist=$(mktemp) || pfail "無法建立暫存檔" + rc_files_existing > "$rclist" + while IFS= read -r rc; do + [ -n "$rc" ] || continue + grep -q '^# jsc-hooks' "$rc" 2>/dev/null || continue + backup_file "$rc" || pfail "無法備份 $rc,沒有備份就不移除" + remove_rc_blocks "$rc" || pfail "無法從 $rc 移除 jsc-hooks 標記段落" + grep -q '^# jsc-hooks' "$rc" 2>/dev/null && pfail "$rc 移除後仍有 jsc-hooks 標記段落" + done < "$rclist" + rm -f "$rclist" + if [ -f "$agents" ] && has_block "$agents" ""; then + backup_file "$agents" || pfail "無法備份 $agents,沒有備份就不移除" + remove_block "$agents" "" "" \ + || pfail "無法從 $agents 移除 jsc-hooks 標記段落" + has_block "$agents" "" && pfail "$agents 移除後仍讀得到 jsc-hooks 標記段落" + fi + purged "已移除 config.toml 的標記段落與根層 notify、rc 檔的 jsc-hooks 段落、AGENTS.md 的規則段落" + echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才真的失效。" + exit 0 ;; + + copilot) + bin=$(cli_bin copilot) + command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 copilot 執行檔" + instr="${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" + rclist=$(mktemp) || pfail "無法建立暫存檔" + rc_files_existing > "$rclist" + while IFS= read -r rc; do + [ -n "$rc" ] || continue + has_block "$rc" "# jsc-hooks:copilot" || continue + backup_file "$rc" || pfail "無法備份 $rc,沒有備份就不移除" + remove_block "$rc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot" \ + || pfail "無法從 $rc 移除 jsc-hooks:copilot 段落" + has_block "$rc" "# jsc-hooks:copilot" && pfail "$rc 移除後仍有 jsc-hooks:copilot 段落" + done < "$rclist" + rm -f "$rclist" + if [ -f "$instr" ] && has_block "$instr" ""; then + backup_file "$instr" || pfail "無法備份 $instr,沒有備份就不移除" + remove_block "$instr" "" "" \ + || pfail "無法從 $instr 移除 jsc-hooks 標記段落" + has_block "$instr" "" && pfail "$instr 移除後仍讀得到 jsc-hooks 標記段落" + fi + purged "已移除 rc 檔的 jsc-hooks:copilot 段落與指引檔的規則段落" + echo "[jsc] copilot:別名要開新的 shell 或重新 source rc 檔才真的失效。" + exit 0 ;; + + antigravity) + bin=$(cli_bin antigravity) + command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 antigravity(agy)執行檔" + rules="${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" + rclist=$(mktemp) || pfail "無法建立暫存檔" + rc_files_existing > "$rclist" + while IFS= read -r rc; do + [ -n "$rc" ] || continue + has_block "$rc" "# jsc-hooks:antigravity" || continue + backup_file "$rc" || pfail "無法備份 $rc,沒有備份就不移除" + remove_block "$rc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity" \ + || pfail "無法從 $rc 移除 jsc-hooks:antigravity 段落" + has_block "$rc" "# jsc-hooks:antigravity" && pfail "$rc 移除後仍有 jsc-hooks:antigravity 段落" + done < "$rclist" + rm -f "$rclist" + if [ -f "$rules" ] && has_block "$rules" ""; then + backup_file "$rules" || pfail "無法備份 $rules,沒有備份就不移除" + remove_block "$rules" "" "" \ + || pfail "無法從 $rules 移除 jsc-hooks 標記段落" + has_block "$rules" "" && pfail "$rules 移除後仍讀得到 jsc-hooks 標記段落" + fi + purged "已移除 rc 檔的 jsc-hooks:antigravity 段落與全域規則檔的規則段落" + echo "[jsc] antigravity:別名要開新的 shell 或重新 source rc 檔才真的失效。" + exit 0 ;; + + kiro) + command -v "$(cli_bin kiro)" >/dev/null 2>&1 || skip "未偵測到 kiro-cli 執行檔" + hookdir="./.kiro/hooks" + if [ -d "$hookdir" ]; then + for f in "$hookdir"/*; do + [ -f "$f" ] || continue + backup_file "$f" || pfail "無法備份 $f,沒有備份就不移除" + rm -f "$f" 2>/dev/null || pfail "無法刪除 $f" + done + left=$(find "$hookdir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ') + [ "$left" = 0 ] || pfail "$hookdir 底下還有 $left 個 hook 檔沒刪掉" + fi + purged "已刪除工作區 .kiro/hooks/ 底下所有 hook 檔,含非 jsc 的第三方項目" + echo "[jsc] kiro:hook 檔綁在工作區,這次只清得到目前目錄的 ./.kiro/hooks/,其他工作區要各自跑一次。" + exit 0 ;; + esac +fi + +if [ "$action" = smoke ]; then + smoke_out=$(mktemp) || { printf 'status=failed reason=%s\n' "無法建立暫存檔"; exit 4; } + smoke_fails=0 + + # 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略) + # $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。 + smoke_one() { + _h="$1"; _s="${2:-}" + _out=$(printf '{}' | JSC_CLI="$cli" sh "$HOOKS/$_h" $_s 2>&1); _rc=$? + if [ "$_rc" -eq 0 ]; then + printf '[jsc] %s%s:exit 0,正常。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out" + elif [ "$_h" = sdlc-gate.sh ] && [ "$_s" = check ] && [ "$_rc" -eq 2 ]; then + # 唯一放行的非零退出:sdlc-gate.sh check 的 exit 2 是刻意設計的階段鎖阻擋 + # (見 hooks/lib.sh 開頭)——鎖存在且模型不符時就該擋下該輪提示。那是 hook 正常 + # 工作,不是執行期錯誤;把它算成錯誤會讓每個正在上鎖的工作階段都誤報一次失敗。 + printf '[jsc] %s check:exit 2,SDLC 階段鎖擋下該輪提示,屬設計行為,不算錯誤。\n' "$_h" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] %s%s:exit %s,執行期出錯:%s\n' "$_h" "${_s:+ $_s}" "$_rc" \ + "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out" + fi + } + + smoke_one session-timer.sh mark + smoke_one sdlc-gate.sh check + smoke_one version-guard.sh + smoke_one skill-usage.sh + smoke_one ste100-guard.sh + + if [ "$smoke_fails" -eq 0 ]; then + printf 'status=ok reason=%s\n' "五支 hook 都跑得完,沒有執行期錯誤" + cat "$smoke_out"; rm -f "$smoke_out"; exit 0 + fi + printf 'status=failed reason=%s\n' "$smoke_fails 支 hook 有執行期錯誤" + cat "$smoke_out" + rm -f "$smoke_out" + echo "[jsc] 請先以 tools/report-error.sh 回報,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2 + exit 4 +fi + case "$cli" in claude) bin=$(cli_bin claude) -- 2.53.0 From 6ecb7d03871c958eea1ad1fdae70f69bbbb00917 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 18:18:10 +0800 Subject: [PATCH 2/6] =?UTF-8?q?docs(hooks):=20=E5=90=8C=E6=AD=A5=20purge?= =?UTF-8?q?=E3=80=81smoke=20=E8=88=87=E9=8C=AF=E8=AA=A4=E6=8E=83=E6=8F=8F?= =?UTF-8?q?=E7=9A=84=E5=B7=A5=E5=85=B7=E8=88=87=E6=8A=80=E8=83=BD=E8=AA=AA?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0bb18bd..3fe0c78 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-in | `tools/jsc-wrap.sh` | 沒有完整 hook 系統的 CLI 的包裝啟動器:匯出 `JSC_CLI`、`JSC_SESSION_ID`,前後接 `session-timer.sh`,結束時自動跑 `scan-logs.sh` 回填。`JSC_CLI` 存 CLI 代號,實際執行的是對應的執行檔(antigravity 是 agy、kiro 是 kiro-cli) | | `tools/scan-logs.sh` | 離線回填:解析 copilot、antigravity、codex 的原生日誌,把技能用量與階段界線補進 `$JSC_HOME`,重掃不重複 | | `tools/report-error.sh` | 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 `ERROR_{HASH}`,並在 `ERROR_CONTENTS` 附上一列索引。wiki 位置由 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR` 解析,解析不出來就安靜降級。由操作者手動執行,或由 `hooks-install` 在 `wire-cli.sh` 回報 `status=failed` 時執行;**不接在失敗的 hook 上自動觸發**(hook 一律安靜 exit 0,自我回報會疊出迴圈) | -| `tools/wire-cli.sh` | 單一 CLI 的接線流程:`{cli}` 對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 ``(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝。寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、kiro 的 JSON 必須成對且 `on`、`run` 在最上層);以 `status=wired\|degraded\|skipped\|failed` 回報結果 | +| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共三個用法。`{cli}` 是接線:對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 ``(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝;寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、kiro 的 JSON 必須成對且 `on`、`run` 在最上層),以 `status=wired\|degraded\|skipped\|failed` 回報。`purge {cli}` 是移除:把該 CLI 的**所有** hook 清掉,含非 jsc 的第三方項目,動到的檔案先原樣備份到 `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`,備份失敗就不移除,移除後重讀驗證,驗不過自動還原備份,以 `status=purged\|skipped\|failed` 回報。`smoke {cli}` 是執行期冒煙測試:五支 hook 各跑一次,非零退出即為錯誤(唯一例外是 `sdlc-gate.sh check` 的 exit 2,那是階段鎖的設計行為),以 `status=ok\|failed` 回報 | +| `tools/scan-hook-errors.sh` | 掃 CLI 原生紀錄找 hook 的執行期錯誤(接線寫對、跑起來出錯)。只有 claude 有 hook 結果紀錄,掃 `~/.claude/projects/**/*.jsonl` 的 `hook_non_blocking_error` 與非空 `hookErrors`;codex、copilot、antigravity、kiro 沒有等價紀錄,一律回報 `unavailable` 並指向 `wire-cli.sh smoke {cli}`。每筆錯誤附加一行 JSON 到 `$JSC_HOME/errors/hooks.jsonl`,`jsc` 欄位標明是不是 jsc 自己的 hook(第三方 hook 的錯誤只回報,不由 jsc 修正);去重與 `scan-logs.sh` 同法,重掃只讀新增段落,以 `status=clean\|errors\|unavailable` 回報 | ## 失敗回報範本 @@ -66,7 +67,7 @@ Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-in ### `hooks-install` -把五支 hook 接線到所有已安裝的 CLI:偵測 CLI 後,逐一呼叫 `tools/wire-cli.sh {cli}` 完成接線(claude 由 `hooks.json` 自動接線,無需寫入)。codex、copilot、antigravity 由該腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍重寫到各自的規則檔(以 `` 標記整段取代,等同先移除再重裝,不重複追加)。codex、copilot、antigravity、kiro 的 SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 SDLC 技能直接呼叫 `sdlc-gate.sh lock` 寫入;這四個 CLI 沒有 pre-tool hook,版本前置檢查接不上,腳本會在 `reason` 裡講明,只有 claude 回報 `wired`。腳本第一行以 `status=wired|degraded|skipped|failed reason=...` 回報結果;`failed` 先寫 `ERROR_{HASH}`,再手動或自動交給 `repair` 技能接手。 +把五支 hook 接線到所有已安裝的 CLI,每個 CLI 走四道關卡:先 `tools/wire-cli.sh purge {cli}` 備份後移除所有 hook(含非 jsc 的第三方項目,乾淨起跑才分得清後續失敗是誰的),再 `tools/wire-cli.sh {cli}` 接線(claude 由 `hooks.json` 自動接線,無需寫入),接著 `tools/wire-cli.sh smoke {cli}` 驗執行期,最後 `tools/scan-hook-errors.sh --cli {cli}` 掃原生紀錄。codex、copilot、antigravity 由接線腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍重寫到各自的規則檔(以 `` 標記整段取代,等同先移除再重裝,不重複追加)。codex、copilot、antigravity、kiro 的 SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 SDLC 技能直接呼叫 `sdlc-gate.sh lock` 寫入;這四個 CLI 沒有 pre-tool hook,版本前置檢查接不上,腳本會在 `reason` 裡講明,只有 claude 回報 `wired`,也只有 claude 掃得到執行期錯誤紀錄。任一關卡出錯(purge、接線、冒煙失敗,或掃到 `jsc=true` 的執行期錯誤)就先寫 `ERROR_{HASH}`,再交給 `repair` 技能接手並以 `develop` PR 收尾;此時允許中止剩下的安裝,但修正一定要開始。掃到 `jsc=false` 的第三方 hook 錯誤只回報,不轉修正。 ### `repair` @@ -81,6 +82,7 @@ Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-in | `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` | | `JSC_WIKI_REPO_ERROR` | `ERROR_CONTENTS`、`ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` | | `JSC_WIKI_REPO` | 未逐類設定時的共用 wiki `{owner}/{repo}` | `tools/report-error.sh` 安靜降級,不寫 wiki | +| `JSC_CLAUDE_SETTINGS_DIR` | `tools/wire-cli.sh purge claude` 要清 `hooks` 鍵的設定檔目錄。指向一份複製品就能完整測過刪鍵邏輯,不必拿使用者本人的設定檔當測試場 | 預設 `~/.claude` | | `JSC_VERSION_GUARD` | 設 `off` 完全略過版本前置檢查(離線工作用) | 啟用檢查 | | `JSC_VERSION_TTL` | 遠端版本查詢的快取秒數 | 預設 600 | | `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` / `JSC_TOOL_NAME` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供,代替 stdin JSON 的 `session_id`、`skill`、`tool_name`(`version-guard.sh` 也收沒有前綴的 `SKILL`、`TOOL_NAME`) | 安靜降級 | -- 2.53.0 From e62511b57f73aae6773f441d1bd387c2299fc234 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 18:18:10 +0800 Subject: [PATCH 3/6] =?UTF-8?q?chore(hooks):=20=E4=B8=89=E4=BB=BD=20manife?= =?UTF-8?q?st=20=E5=90=8C=E6=AD=A5=E5=8D=87=E7=89=88=E5=88=B0=200.1.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 19ede4e..e6c2da0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.1.5", + "version": "0.1.6", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index a50558b..44f381a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.1.5", + "version": "0.1.6", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index dfc00f5..ba07c9f 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.1.5", + "version": "0.1.6", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills/" } -- 2.53.0 From b9ad32193059372798c04c81460870cd442604e6 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 18:59:29 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(sdlc-gate):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=85=20PR=20=E9=96=98=E9=96=80=EF=BC=8C?= =?UTF-8?q?=E6=9C=AA=E7=B5=90=E6=B8=85=E5=B0=B1=E6=93=8B=E4=B8=8B=E5=88=A5?= =?UTF-8?q?=E7=9A=84=E9=9A=8E=E6=AE=B5=E6=8A=80=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/hooks.json | 8 ++++ hooks/sdlc-gate.sh | 115 +++++++++++++++++++++++++++++++++++++++++++-- tools/wire-cli.sh | 10 +++- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index a3899d7..ec57558 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -20,6 +20,10 @@ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" check" + }, + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" wp-check prompt" } ] } @@ -51,6 +55,10 @@ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/version-guard.sh\"" + }, + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" wp-check skill" } ] } diff --git a/hooks/sdlc-gate.sh b/hooks/sdlc-gate.sh index 1cd9d52..fe4202b 100755 --- a/hooks/sdlc-gate.sh +++ b/hooks/sdlc-gate.sh @@ -17,11 +17,26 @@ # sdlc-gate.sh check hook 模式(UserPromptSubmit):模型不符即擋下該輪提示。 # sdlc-gate.sh report 印出 {sid} {stage} {必要標籤} {上鎖時的模型};無鎖不印。 # -# exit code 例外:其他 jsc hook 一律 exit 0 不中斷宿主 CLI;本檔 check 是刻意的例外—— -# 鎖存在且模型不符時 exit 2 擋下該輪提示。只用提示注入的話模型可以無視,閘門形同虛設。 +# sdlc-gate.sh wp-lock {owner}/{repo} {index} 記下一筆未結清的工作包 PR。 +# exit 0 = 已記下;exit 2 = 用法錯誤或寫不進狀態檔(沒記下等於沒鎖)。 +# sdlc-gate.sh wp-unlock {owner}/{repo} 結清後移除狀態檔;檔案不存在也算成功。 +# exit 0 = 已結清;exit 2 = 用法錯誤。 +# sdlc-gate.sh wp-report 印出 {owner}/{repo} {index} {上鎖時間};沒有未結清就不印,exit 0。 +# sdlc-gate.sh wp-check prompt hook 模式(UserPromptSubmit):注入提醒,一律 exit 0。 +# sdlc-gate.sh wp-check skill hook 模式(PreToolUse,matcher Skill):命中別的階段技能時 +# exit 2 擋下該次呼叫;其餘 exit 0。 +# +# exit code 例外:其他 jsc hook 一律 exit 0 不中斷宿主 CLI;本檔 check 與 wp-check skill 是 +# 刻意的例外——鎖存在且不合規時 exit 2 擋下。只用提示注入的話模型可以無視,閘門形同虛設。 # 無鎖、或資料不足無法判定時,仍照舊 exit 0 安靜降級。 HERE=$(dirname "$0"); . "$HERE/lib.sh" -read_stdin +# 只有需要 stdin JSON 的子命令才讀它:模型判定要 transcript_path,session 判定要 session_id。 +# wp-lock、wp-unlock、wp-report 兩者都不需要,而 read_stdin 在標準輸入是管線又沒人關閉時會 +# 一直等——工具腳本(jsc-sdlc 的 wp-gate.sh)轉呼叫這三個子命令時就這樣整支卡死。 +case "${1:-}" in + wp-lock|wp-unlock|wp-report) STDIN_JSON="" ;; + *) read_stdin ;; +esac sid=$(session_id) state="$JSC_HOME/sessions/$sid.stage" TAGS_TSV="$JSC_HOME/model-tags.tsv" @@ -92,6 +107,44 @@ current_model() { printf '%s' "$m" } +# --- 工作包 PR 閘門(狀態檔:$JSC_HOME/wp/{owner}-{repo}.pr) --- +# +# 刻意不綁 session:PR 沒合併就是沒合併,換一個工作階段照樣要擋。綁 session 等於給閘門 +# 留一道「開新對話就自動繞過」的門,規則就不再是強制的。 +# +# 一律只讀檔案,絕不打網路:hook 要快、也要能離線用。PR 的真實合併狀態由 +# jsc-sdlc/tools/wp-gate.sh 去查並負責結清,本檔只反映已記錄的未結清狀態。 + +WP_DIR="$JSC_HOME/wp" + +wp_state_file() { # $1={owner}/{repo} + printf '%s/%s.pr' "$WP_DIR" "$(printf '%s' "$1" | tr '/' '-')" +} + +# 未結清清單,每行「{owner}/{repo} {index} {上鎖時間}」;沒有就不輸出。 +wp_pending() { + [ -d "$WP_DIR" ] || return 0 + for _f in "$WP_DIR"/*.pr; do + [ -f "$_f" ] || continue + _line=$(sed -n '1p' "$_f" 2>/dev/null | tr '\t' ' ') + [ -n "$_line" ] && printf '%s\n' "$_line" + done +} + +# 未結清清單濃縮成一句可讀的「{repo} 第 {index} 號」,多筆用頓號串起。 +wp_brief() { # 標準輸入 = wp_pending 的輸出 + awk '{ out = (out == "" ? $1 " 第 " $2 " 號" : out "、" $1 " 第 " $2 " 號") } END { print out }' +} + +# 存取庫參數格式檢查;不合格就回 1,由呼叫端印訊息後 exit 2。 +wp_valid_repo() { # $1=參數 + case "${1:-}" in + */*/*|/*|*/) return 1 ;; + */*) return 0 ;; + *) return 1 ;; + esac +} + case "${1:-check}" in lock) stage="${2:-}" @@ -169,5 +222,61 @@ case "${1:-check}" in [ -n "$line" ] && echo "$sid $line" fi exit 0 ;; + + wp-lock) + repo="${2:-}"; idx="${3:-}" + wp_valid_repo "$repo" || { + echo "[jsc][工作包閘門][ERR]:存取庫須為 {owner}/{repo} 格式,收到「${repo:-空值}」。" >&2; exit 2; } + case "$idx" in + ''|*[!0-9]*) + echo "[jsc][工作包閘門][ERR]:PR 編號須為數字,收到「${idx:-空值}」。" >&2; exit 2 ;; + esac + mkdir -p "$WP_DIR" 2>/dev/null || true + wpf=$(wp_state_file "$repo") + printf '%s\t%s\t%s\n' "$repo" "$idx" "$(now_iso)" > "$wpf" 2>/dev/null || { + echo "[jsc][工作包閘門][ERR]:寫不進狀態檔 $wpf,工作包鎖未生效。" >&2; exit 2; } + echo "[jsc][工作包閘門][OK]:已記下 $repo 第 $idx 號 PR 未結清,結清前不得開新工作包。" + exit 0 ;; + + wp-unlock) + repo="${2:-}" + wp_valid_repo "$repo" || { + echo "[jsc][工作包閘門][ERR]:存取庫須為 {owner}/{repo} 格式,收到「${repo:-空值}」。" >&2; exit 2; } + # 冪等:狀態檔不存在也算成功。結清流程可能被重跑,第二次失敗只會讓呼叫端誤判。 + rm -f "$(wp_state_file "$repo")" 2>/dev/null || true + exit 0 ;; + + wp-report) + wp_pending + exit 0 ;; + + wp-check) + [ "${JSC_WP_GATE:-}" = "off" ] && exit 0 + pending=$(wp_pending) + [ -n "$pending" ] || exit 0 + brief=$(printf '%s\n' "$pending" | wp_brief) + case "${2:-prompt}" in + prompt) + # 只注入提醒,一律 exit 0。擋提示會連「去把那支 PR 修好」的對話都送不出去, + # 把使用者鎖在門外,連逃生門都下不了指令。 + echo "[jsc] ${brief} PR 尚未合併,禁止開新工作包;請先把該 PR 結清(合併或關閉)再繼續。" + exit 0 ;; + skill) + # 技能名取法比照 version-guard.sh:環境變數優先,非 Claude CLI 只餵得到環境變數。 + skill="${JSC_SKILL:-${SKILL:-$(json_str skill)}}" + # 取不到技能名就安靜降級放行,不能拿沒有的資料當擋人的理由。 + [ -n "$skill" ] || exit 0 + # 帶前綴(jsc-sdlc:plan)與裸名(plan)都要認。 + sname=${skill##*:} + case "$sname" in + plan|analyze|maintain) + echo "[jsc][工作包閘門][ERR]:${brief} PR 尚未合併,禁止開新工作包,「${sname}」不得進行。請先把該 PR 結清(合併或關閉),或執行 jsc-hooks/hooks/sdlc-gate.sh wp-unlock {owner}/{repo} 解除;確定要整體放行請設 JSC_WP_GATE=off。本次技能呼叫已擋下。" >&2 + exit 2 ;; + esac + # implement 與其餘技能一律放行:結清 PR 正是 implement 步驟 4 要做的事, + # 擋掉 implement 就沒有任何路徑能解除這道鎖,等於把流程鎖死。 + exit 0 ;; + esac + exit 0 ;; esac exit 0 diff --git a/tools/wire-cli.sh b/tools/wire-cli.sh index 2785397..b3c4db8 100755 --- a/tools/wire-cli.sh +++ b/tools/wire-cli.sh @@ -582,7 +582,9 @@ if [ "$action" = smoke ]; then # $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。 smoke_one() { _h="$1"; _s="${2:-}" - _out=$(printf '{}' | JSC_CLI="$cli" sh "$HOOKS/$_h" $_s 2>&1); _rc=$? + # 技能名一律清空:冒煙要驗的是「沒有技能情境時腳本跑得完」。留著繼承來的 JSC_SKILL, + # sdlc-gate.sh wp-check skill 會拿它當真實呼叫判定,有未結清 PR 時就誤報成執行期錯誤。 + _out=$(printf '{}' | JSC_CLI="$cli" JSC_SKILL="" SKILL="" sh "$HOOKS/$_h" $_s 2>&1); _rc=$? if [ "$_rc" -eq 0 ]; then printf '[jsc] %s%s:exit 0,正常。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out" elif [ "$_h" = sdlc-gate.sh ] && [ "$_s" = check ] && [ "$_rc" -eq 2 ]; then @@ -602,9 +604,13 @@ if [ "$action" = smoke ]; then smoke_one version-guard.sh smoke_one skill-usage.sh smoke_one ste100-guard.sh + # sdlc-gate.sh 有兩個 hook 模式,接在不同事件上,兩個都要驗:wp-check prompt 一律 exit 0, + # wp-check skill 在取不到技能名時放行(上面已清空技能名),所以兩者都不需要白名單例外。 + smoke_one sdlc-gate.sh "wp-check prompt" + smoke_one sdlc-gate.sh "wp-check skill" if [ "$smoke_fails" -eq 0 ]; then - printf 'status=ok reason=%s\n' "五支 hook 都跑得完,沒有執行期錯誤" + printf 'status=ok reason=%s\n' "五支 hook 的每個接線模式都跑得完,沒有執行期錯誤" cat "$smoke_out"; rm -f "$smoke_out"; exit 0 fi printf 'status=failed reason=%s\n' "$smoke_fails 支 hook 有執行期錯誤" -- 2.53.0 From 26a5a4690d57906772edd8edaba1fc071297939e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 18:59:29 +0800 Subject: [PATCH 5/6] =?UTF-8?q?docs(hooks):=20=E8=A3=9C=E4=B8=8A=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=85=20PR=20=E9=96=98=E9=96=80=E7=9A=84=20hook=20?= =?UTF-8?q?=E8=AA=AA=E6=98=8E=E8=88=87=E9=80=83=E7=94=9F=E9=96=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3fe0c78..2051741 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間 | | `hooks/version-guard.sh` | PreToolUse(Skill) | 技能使用前的版本前置檢查:本機**實際載入**版本落後遠端發佈版本就以 exit 2 擋下該次呼叫並提示更新指令(更新指令依當前 CLI 給)。只擋落後這一種情況:超前放行(開發技能組時本機本來就會超前),讀不到本機版本、推導不出站台、查不到遠端版本也一律放行。逃生門 `JSC_VERSION_GUARD=off`。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-cli:models`、`jsc-meta:*` | | `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 | -| `hooks/sdlc-gate.sh` | UserPromptSubmit | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從 transcript 讀出實際模型 id 比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`unlock` 為逃生門 | +| `hooks/sdlc-gate.sh` | UserPromptSubmit、PreToolUse(Skill) | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從 transcript 讀出實際模型 id 比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`unlock` 為逃生門。另含工作包 PR 閘門:`wp-lock {owner}/{repo} {index}` 記下一筆未結清的工作包 PR、`wp-unlock {owner}/{repo}` 結清(檔案不存在也算成功)、`wp-report` 印出所有未結清、`wp-check {prompt|skill}` 為 hook 模式。狀態檔在 `$JSC_HOME/wp/{owner}-{repo}.pr`,**刻意不綁 session**——PR 沒合併時換一個工作階段照樣要擋。`wp-check prompt` 只注入提醒、絕不擋提示(擋了連「去修那支 PR」的對話都送不出去);`wp-check skill` 在有未結清 PR 時以 exit 2 擋下 `plan`、`analyze`、`maintain`,但一律放行 `implement`(結清 PR 正是 implement 的步驟,擋它會鎖死流程)。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 | Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。 @@ -85,6 +85,7 @@ Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-in | `JSC_CLAUDE_SETTINGS_DIR` | `tools/wire-cli.sh purge claude` 要清 `hooks` 鍵的設定檔目錄。指向一份複製品就能完整測過刪鍵邏輯,不必拿使用者本人的設定檔當測試場 | 預設 `~/.claude` | | `JSC_VERSION_GUARD` | 設 `off` 完全略過版本前置檢查(離線工作用) | 啟用檢查 | | `JSC_VERSION_TTL` | 遠端版本查詢的快取秒數 | 預設 600 | +| `JSC_WP_GATE` | 設 `off` 完全略過工作包 PR 閘門(`wp-check` 一律放行) | 啟用閘門 | | `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` / `JSC_TOOL_NAME` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供,代替 stdin JSON 的 `session_id`、`skill`、`tool_name`(`version-guard.sh` 也收沒有前綴的 `SKILL`、`TOOL_NAME`) | 安靜降級 | | `JSC_MODEL` | 非 Claude CLI 的目前模型,供 `sdlc-gate.sh` 比對;優先序在 transcript 實際值與 stdin `model` 之後 | 改讀 `~/.claude/settings.json`,再不行就安靜降級 | -- 2.53.0 From 65d3c13d225a60ae9d402823f1641711bf9fbff6 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 18:59:29 +0800 Subject: [PATCH 6/6] =?UTF-8?q?chore(hooks):=20=E4=B8=89=E4=BB=BD=20manife?= =?UTF-8?q?st=20=E5=90=8C=E6=AD=A5=E5=8D=87=E7=89=88=E5=88=B0=200.1.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e6c2da0..5ccf44c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.1.6", + "version": "0.1.7", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 44f381a..9389335 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.1.6", + "version": "0.1.7", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index ba07c9f..79ba94f 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.1.6", + "version": "0.1.7", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills/" } -- 2.53.0