hooks 0.1.7 發佈:安裝先全清、出錯轉修正、工作包 PR 閘門 #19
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/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/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/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 -->`(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝。寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、kiro 的 JSON 必須成對且 `on`、`run` 在最上層);以 `status=wired\|degraded\|skipped\|failed` 回報結果 |
|
| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共三個用法。`{cli}` 是接線:對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 `<!-- jsc-hooks -->`(或 `# 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`
|
### `hooks-install`
|
||||||
|
|
||||||
把五支 hook 接線到所有已安裝的 CLI:偵測 CLI 後,逐一呼叫 `tools/wire-cli.sh {cli}` 完成接線(claude 由 `hooks.json` 自動接線,無需寫入)。codex、copilot、antigravity 由該腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍重寫到各自的規則檔(以 `<!-- jsc-hooks -->` 標記整段取代,等同先移除再重裝,不重複追加)。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`),語言規則仍重寫到各自的規則檔(以 `<!-- jsc-hooks -->` 標記整段取代,等同先移除再重裝,不重複追加)。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`
|
### `repair`
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-in
|
|||||||
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
|
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
|
||||||
| `JSC_WIKI_REPO_ERROR` | `ERROR_CONTENTS`、`ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
|
| `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_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_GUARD` | 設 `off` 完全略過版本前置檢查(離線工作用) | 啟用檢查 |
|
||||||
| `JSC_VERSION_TTL` | 遠端版本查詢的快取秒數 | 預設 600 |
|
| `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`) | 安靜降級 |
|
| `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`) | 安靜降級 |
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
|
||||||
"skills": "./skills/"
|
"skills": "./skills/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,38 @@
|
|||||||
---
|
---
|
||||||
name: hooks-install
|
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
|
# 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.
|
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 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.
|
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.
|
The detailed flow **MUST run as a sub agent**; the main agent only reports the summary.
|
||||||
|
|
||||||
## Steps
|
## 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.
|
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 `<!-- jsc-hooks -->` (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.
|
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 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`.
|
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 `<!-- jsc-hooks -->` (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. 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`.
|
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
|
## 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.
|
- 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.
|
- `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.
|
- `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`.
|
- Data lands in `$JSC_HOME` (default `~/.jsc`), consumed by `jsc-log:worklog` and `jsc-log:stats`.
|
||||||
|
|||||||
Executable
+190
@@ -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
|
||||||
+445
-12
@@ -1,7 +1,18 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# wire-cli.sh — 把 jsc 五支 hook 接線到單一 CLI(供 hooks-install 技能呼叫)。
|
# wire-cli.sh — 單一 CLI 的 hook 生命週期:先清、再接、再冒煙(供 hooks-install 技能呼叫)。
|
||||||
# 用法: wire-cli.sh {claude|codex|copilot|antigravity|kiro}
|
# 用法:
|
||||||
# 行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加):
|
# 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
|
# claude — 什麼都不用寫,hooks.json 已自動接線五支 hook
|
||||||
# codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時);
|
# codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時);
|
||||||
# 在 config.toml 設 notify(每輪補 session-timer.sh start 再 mark,JSC_CLI=codex);
|
# 在 config.toml 設 notify(每輪補 session-timer.sh start 再 mark,JSC_CLI=codex);
|
||||||
@@ -24,11 +35,16 @@
|
|||||||
# 正確位置(codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對
|
# 正確位置(codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對
|
||||||
# 且 on、run 在最上層)。腳本說寫好了卻寫錯位置,是最難查的失敗,所以驗證放在腳本裡。
|
# 且 on、run 在最上層)。腳本說寫好了卻寫錯位置,是最難查的失敗,所以驗證放在腳本裡。
|
||||||
#
|
#
|
||||||
# 輸出: 第一行固定為 `status={wired|degraded|skipped|failed} reason=...`(可供程式判讀),
|
# 輸出: 第一行固定為 `status=... reason=...`(可供程式判讀),其後為人類可讀的繁中說明。
|
||||||
# 其後為人類可讀的繁中說明。
|
# 結束碼(接線): 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查)
|
||||||
# 結束碼: 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查)
|
|
||||||
# 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過)
|
# 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過)
|
||||||
# 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報)
|
# 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
|
set -u
|
||||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
ROOT=$(cd "$HERE/.." && pwd)
|
ROOT=$(cd "$HERE/.." && pwd)
|
||||||
@@ -36,12 +52,21 @@ HOOKS="$ROOT/hooks"
|
|||||||
# cli_bin(CLI 代號 → 實際執行檔)的唯一來源在 lib.sh,包裝啟動器也用同一份
|
# cli_bin(CLI 代號 → 實際執行檔)的唯一來源在 lib.sh,包裝啟動器也用同一份
|
||||||
. "$HOOKS/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:-}"
|
cli="${1:-}"
|
||||||
case "$cli" in
|
case "$cli" in
|
||||||
claude|codex|copilot|antigravity|kiro) ;;
|
claude|codex|copilot|antigravity|kiro) ;;
|
||||||
*)
|
*) usage ;;
|
||||||
echo "用法:wire-cli.sh {claude|codex|copilot|antigravity|kiro}" >&2
|
|
||||||
exit 2 ;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出
|
# STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出
|
||||||
@@ -111,10 +136,12 @@ toml_root_key() {
|
|||||||
' "$1" 2>/dev/null
|
' "$1" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# 驗證:JSON 括號成對,且某個鍵出現在最上層($1=檔案 $2=鍵名)。
|
# 驗證:JSON 括號成對,且某個鍵出現在最上層($1=檔案 $2=鍵名 $3=要求)。
|
||||||
# 解析失敗(括號不成對、字串沒收尾)也回傳非 0,所以這支同時當語法檢查用。
|
# 解析失敗(括號不成對、字串沒收尾)也回傳非 0,所以這支同時當語法檢查用。
|
||||||
|
# $3=key(預設)要求該鍵存在;$3=pairs 只檢查語法,不管鍵在不在——purge 之後要驗的是
|
||||||
|
# 「鍵不見了而且檔案還是合法 JSON」,這兩件事得分開問,不然刪壞檔也會被當成刪成功。
|
||||||
json_top_key() {
|
json_top_key() {
|
||||||
awk -v k="$2" '
|
awk -v k="$2" -v want="${3:-key}" '
|
||||||
{ s = s $0 "\n" }
|
{ s = s $0 "\n" }
|
||||||
END {
|
END {
|
||||||
n = length(s); depth = 0; i = 1; found = 0; bad = 0
|
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 } }
|
else if (c == "}" || c == "]") { depth--; if (depth < 0) { bad = 1; break } }
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
exit((found && !bad && depth == 0) ? 0 : 1)
|
if (bad || depth != 0) exit(1)
|
||||||
|
exit((want == "pairs" || found) ? 0 : 1)
|
||||||
}' "$1" 2>/dev/null
|
}' "$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 為預設(自動建立)。
|
# 找出可寫入別名的 shell rc 檔;都不存在就以 ~/.bashrc 為預設(自動建立)。
|
||||||
# 印出找到或建立的 rc 檔路徑,一行一個。
|
# 印出找到或建立的 rc 檔路徑,一行一個。
|
||||||
rc_files() {
|
rc_files() {
|
||||||
@@ -168,6 +208,205 @@ write_alias_rc() {
|
|||||||
[ "$_ok" = 1 ]
|
[ "$_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="" # 每行「{備份檔}<TAB>{原檔}」,還原時反向複製回去
|
||||||
|
|
||||||
|
# 備份目錄延後建立:沒有檔案要動時不留空目錄。
|
||||||
|
# 只設全域變數、不印路徑:呼叫端若寫成 $(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
|
skip() { # $1=reason
|
||||||
printf 'status=skipped reason=%s\n' "$1"
|
printf 'status=skipped reason=%s\n' "$1"
|
||||||
echo "[jsc] 略過:$1"
|
echo "[jsc] 略過:$1"
|
||||||
@@ -181,6 +420,200 @@ fail() { # $1=reason
|
|||||||
exit 4
|
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" "<!-- jsc-hooks -->"; then
|
||||||
|
backup_file "$agents" || pfail "無法備份 $agents,沒有備份就不移除"
|
||||||
|
remove_block "$agents" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" \
|
||||||
|
|| pfail "無法從 $agents 移除 jsc-hooks 標記段落"
|
||||||
|
has_block "$agents" "<!-- jsc-hooks -->" && 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" "<!-- jsc-hooks -->"; then
|
||||||
|
backup_file "$instr" || pfail "無法備份 $instr,沒有備份就不移除"
|
||||||
|
remove_block "$instr" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" \
|
||||||
|
|| pfail "無法從 $instr 移除 jsc-hooks 標記段落"
|
||||||
|
has_block "$instr" "<!-- jsc-hooks -->" && 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" "<!-- jsc-hooks -->"; then
|
||||||
|
backup_file "$rules" || pfail "無法備份 $rules,沒有備份就不移除"
|
||||||
|
remove_block "$rules" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" \
|
||||||
|
|| pfail "無法從 $rules 移除 jsc-hooks 標記段落"
|
||||||
|
has_block "$rules" "<!-- jsc-hooks -->" && 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
|
case "$cli" in
|
||||||
claude)
|
claude)
|
||||||
bin=$(cli_bin claude)
|
bin=$(cli_bin claude)
|
||||||
|
|||||||
Reference in New Issue
Block a user