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/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`) | 安靜降級 | 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/" } 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)