Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d105f4c1da |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-hooks",
|
||||
"version": "0.4.2",
|
||||
"version": "0.3.8",
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"hooks": "./hooks/codex-hooks.json",
|
||||
"name": "jsc-hooks",
|
||||
"version": "0.4.2",
|
||||
"version": "0.3.8",
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||
"skills": "./skills",
|
||||
"jsc": {
|
||||
|
||||
@@ -33,7 +33,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
||||
| `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 |
|
||||
| `hooks/comment-scope.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 程式碼註解不得夾帶文件相關資訊與審查流程痕跡,共三種模式。`prompt`:在每次提示注入規則摘要(禁止項與白名單各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。掃描時機每個 CLI 不同——claude 逐檔即時(PostToolUse)、codex 每輪結束(`notify`)、kiro 每輪提示送出時(`userPromptSubmit`,掃的是上一輪寫的檔)、copilot 與 antigravity 只有工作階段結束時由 `tools/jsc-wrap.sh` 收尾掃一次。兩種掃描模式都只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入,檔案已經寫好了)。markdown、純文字、資料檔與二進位檔一律跳過。只實作可用樣式判定的項目,專案代號、客戶名稱這類判不出來的交給 `/jsc-review:code-review`。規則正文的唯一來源在 `jsc-review` 的 `references/comment-scope.md`,本存取庫不留副本。逃生門 `JSC_COMMENT_SCOPE=off` |
|
||||
| `hooks/lang-guard.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 所有非程式碼輸出一律繁體中文、UTF-8、無亂碼、無簡體字,共三種模式。`prompt`:在每次提示注入規則摘要(適用範圍與自我檢查各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。接線位置與掃描時機跟 `comment-scope.sh` 完全一樣,見下面那張表。偵測三項:簡體字(字表在 `hooks/simplified.txt`,讀不到就安靜跳過這一項)、亂碼(U+FFFD 替代字元與雙重編碼殘骸)、非 UTF-8 編碼(用 `iconv` 判定,沒有 `iconv` 就跳過)。三項都掃整個檔案、不只掃註解行,`.md` 與純文字檔照掃——那些正是「非程式碼輸出」的主場,這兩點跟 `comment-scope.sh` 刻意不同。掃描深度仍只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入)。二進位檔(只認 NUL 位元組)與 `*.lock`、`*.min.js`、`*.map` 這類產生檔跳過;`hooks/simplified.txt`、`hooks/ste100-guard.sh`、`hooks/lang-guard.sh` 也跳過,那三份檔案裡的簡體字與亂碼樣本是被討論的對象,不是被使用。規則正文的唯一來源在 `jsc-meta` 的 `references/ste100.md`。逃生門 `JSC_LANG_GUARD=off` |
|
||||
| `hooks/sdlc-gate.sh` | UserPromptSubmit、PreToolUse(Skill) | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從可驗證來源讀出模型 id,比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖。**來源依 CLI 分流**,一支 CLI 只讀自己的紀錄:claude 讀 transcript 與 hook stdin JSON,codex 讀 hook stdin JSON 與自己的 session 記錄,copilot、antigravity、kiro 本機沒有可讀的模型紀錄,判不出 CLI 時不採用任何自動來源;所有 CLI 最後都接受 `JSC_MODEL` 人工覆寫,且回報會標明人工覆寫。政策是 **fail-closed**:不知道能力就擋下,三種情形一律擋——判不出 CLI、判不出模型、模型不在能力標籤表上;每一則擋下的訊息都會印出兩條逃生門(設 `JSC_MODEL`,或執行 `sdlc-gate.sh unlock {狀態檔}`)。`check` 在上述任一情形以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`report` 印出階段、必要標籤、模型 id、模型來源與判定結果;`unlock` 為逃生門,不帶參數清這個工作階段的新舊兩份,帶參數只清指定的那一支。階段鎖狀態檔是 `$JSC_HOME/sessions/{CLI 代號}-{sid}.stage`,舊路徑 `$JSC_HOME/sessions/{sid}.stage` 仍讀得到。另含工作包 PR 閘門:`wp-lock {owner}/{repo} {index} [{工作包代號}]` 記下一筆未結清的工作包 PR、`wp-unlock {owner}/{repo} {index}` 結清那一筆(檔案不存在也算成功)、`wp-claim {owner}/{repo} {工作包代號} [{PR 編號}] [{分析頁頁名}]` 記下這個存取庫目前領取哪一包、`wp-unclaim {owner}/{repo}` 交回、`wp-report` 印出所有未結清、`wp-check {prompt|skill}` 為 hook 模式。狀態檔一個工作包一支,在 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr`,**刻意不綁 session**——PR 沒合併時換一個工作階段照樣要擋;一個工作包一支鎖檔是為了讓好幾個互不相依的工作包能同時記在案,不會互相覆蓋掉對方的鎖。`wp-check prompt` 只注入提醒、絕不擋提示(擋了連「去修那支 PR」的對話都送不出去);`wp-check skill` 在有未結清 PR 時以 exit 2 擋下 `analyze` 與 `maintain`,但一律放行 `implement`(結清 PR 正是 implement 的步驟,擋它會鎖死流程),也放行 `plan`,只注入提醒(plan 是純邏輯階段、不碰程式碼,而這道閘門只知道「有 PR 未合併」、判不出跟新計畫有沒有關聯;放棄的是在製品上限,`analyze` 與 `maintain` 兩道仍在,上限晚一個階段才生效)——這一層是整個存取庫共用的粗粒度提醒,「某個候選工作包能不能挑」的細粒度判斷在 `jsc-sdlc/tools/wp-gate.sh check-deps`,不是這裡。另外會比對歸屬:未結清的 PR 不屬於目前領取的工作包時,`prompt` 多注入一行「那幾支交給領取它的工作階段」,`skill` 在擋下 `analyze`、`maintain` 時一併點名,`plan` 與 `implement` 仍放行但收到同一則提醒。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 |
|
||||
| `hooks/sdlc-gate.sh` | UserPromptSubmit、PreToolUse(Skill) | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從可驗證來源讀出模型 id,比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;來源優先序為 transcript、hook stdin JSON、Codex 本機 session 記錄,最後才接受 `JSC_MODEL` 人工覆寫,且回報會標明人工覆寫;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`report` 印出階段、必要標籤、模型 id、模型來源與判定結果;`unlock` 為逃生門。另含工作包 PR 閘門:`wp-lock {owner}/{repo} {index} [{工作包代號}]` 記下一筆未結清的工作包 PR、`wp-unlock {owner}/{repo} {index}` 結清那一筆(檔案不存在也算成功)、`wp-claim {owner}/{repo} {工作包代號} [{PR 編號}] [{分析頁頁名}]` 記下這個存取庫目前領取哪一包、`wp-unclaim {owner}/{repo}` 交回、`wp-report` 印出所有未結清、`wp-check {prompt|skill}` 為 hook 模式。狀態檔一個工作包一支,在 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr`,**刻意不綁 session**——PR 沒合併時換一個工作階段照樣要擋;一個工作包一支鎖檔是為了讓好幾個互不相依的工作包能同時記在案,不會互相覆蓋掉對方的鎖。`wp-check prompt` 只注入提醒、絕不擋提示(擋了連「去修那支 PR」的對話都送不出去);`wp-check skill` 在有未結清 PR 時以 exit 2 擋下 `analyze` 與 `maintain`,但一律放行 `implement`(結清 PR 正是 implement 的步驟,擋它會鎖死流程),也放行 `plan`,只注入提醒(plan 是純邏輯階段、不碰程式碼,而這道閘門只知道「有 PR 未合併」、判不出跟新計畫有沒有關聯;放棄的是在製品上限,`analyze` 與 `maintain` 兩道仍在,上限晚一個階段才生效)——這一層是整個存取庫共用的粗粒度提醒,「某個候選工作包能不能挑」的細粒度判斷在 `jsc-sdlc/tools/wp-gate.sh check-deps`,不是這裡。另外會比對歸屬:未結清的 PR 不屬於目前領取的工作包時,`prompt` 多注入一行「那幾支交給領取它的工作階段」,`skill` 在擋下 `analyze`、`maintain` 時一併點名,`plan` 與 `implement` 仍放行但收到同一則提醒。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 |
|
||||
| `hooks/write-guard.sh` | PreToolUse(Write、Edit、MultiEdit)、PreToolUse(Bash) | 寫入與提交閘門,共三種擋人模式,目前只接在 claude 上;codex、copilot、antigravity 三支已經有可用的 pre-tool hook(見上面「各 CLI 的 pre-tool 接線位置」),只是這三種模式還沒接過去,kiro 則是本來就擋不下來。另有一個不接 hook 的 `release` 解除模式。`stage`:`sdlc-gate.sh` 的階段鎖鎖在 `plan` 或 `analyze` 時,以 exit 2 擋下 `Write`、`Edit`、`MultiEdit`——那兩個階段的產出是計畫頁與分析頁,不是檔案。階段鎖狀態檔沿用 `sdlc-gate.sh` 那一份,這裡只讀不寫。`review`:目前技能是 `jsc-review:code-review` 或 `jsc-review:api-doc` 時擋下寫入,那兩支只回報發現、不改程式碼。技能名先讀環境變數,取不到才讀 `skill-usage.sh` 記下的那一份;沒有「技能結束」事件可讀,所以紀錄超過 `JSC_WRITE_GUARD_TTL` 秒就當那支技能早已跑完。`jsc-review:comment-cleanup` **刻意不擋**:它本來就要改檔,只是限定僅註解行,而精確判定要解析工具參數裡整份新內容再逐語言判斷哪幾行是註解,判錯會擋掉合法的清理,代價比漏擋大,所以那條界線留給技能內文與後續審查。`commit`:擋下「同一道指令把全部變更一次加進索引再提交」,也擋下含簡體字、亂碼或非 UTF-8 編碼的提交訊息(判定整段轉呼叫 `lang-guard.sh`,字表仍是 `hooks/simplified.txt`,這裡不留第二份樣式)。跨兩次工具呼叫的 `git add -A` 不擋:那要記跨呼叫狀態,而被擋下的人沒有辦法讓那個狀態自己消失,閘門會把解除自己的路徑一起鎖掉。`release`:刪掉 `review` 模式認人用的那份紀錄,一律 exit 0,由 `jsc-review:code-review` 與 `jsc-review:api-doc` 在收尾時各呼叫一次。有這個模式是因為那份紀錄記的是「最近一次載入的技能」不是「還在跑的技能」——稽核收尾後呼叫端本來就要動手改,那時紀錄仍寫著稽核技能,TTL 內每一次寫入都被擋,解除路徑只剩逃生門或空等;閘門不得把解除自己的路徑一起鎖掉。逃生門 `JSC_WRITE_GUARD=off`(`release` 不受它影響,清紀錄擋不到任何人) |
|
||||
|
||||
Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。寫進使用者設定的長期命令一律指向 `$JSC_HOME/current/jsc-hooks`,不指向帶版號的 plugin 快取目錄,也不指向開發存取庫。
|
||||
@@ -157,8 +157,7 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
||||
| `$JSC_HOME/errors/scan-state/{CLI 代號}-*.offset` | 有 | 正確 | 原生日誌位置與格式依 CLI 不同,掃描位移不能共用。 |
|
||||
| `$JSC_HOME/usage/scan-state/{CLI 代號}-*.offset` | 有 | 正確 | 離線回填逐 CLI 掃不同日誌,位移檔以 CLI 前綴隔離。 |
|
||||
| `$JSC_HOME/usage/skills.jsonl`、`$JSC_HOME/usage/chains.jsonl` | 每筆有 `cli` 欄位 | 正確 | 統計要能跨 CLI 彙整,也要能用欄位篩選。 |
|
||||
| `$JSC_HOME/sessions/{CLI 代號}-{sid}.stage` | 有 | 正確 | session id 判不出時會退回 `default`,不分 CLI 就會共用同一支 `default.stage`,一支上的階段鎖會擋到另一支——那不只擋提示,`write-guard.sh` 的 `stage` 模式還會連寫檔一起擋。用檔名前綴不用子目錄,是因為外部工具以單層的 `sessions/*.stage` 盤點階段鎖。舊路徑 `$JSC_HOME/sessions/{sid}.stage` 只作往後相容的讀取,`unlock` 會一併清掉。 |
|
||||
| `$JSC_HOME/sessions/` 的其餘檔案(`.start`、`.end`、`.lastskill`) | 以 session id 分 | 正確 | 工作階段 id 由 CLI 或包裝器提供,實質上分離;同 id 才代表同一工作階段。 |
|
||||
| `$JSC_HOME/sessions/` | 以 session id 分 | 正確 | 工作階段 id 由 CLI 或包裝器提供,實質上分離;同 id 才代表同一工作階段。 |
|
||||
| `$JSC_HOME/restart-required.d/{CLI 代號}` | 有 | 正確 | 重啟只清當前 CLI,那一支沒有重啟就不能被另一支解除。 |
|
||||
| `$JSC_HOME/wp/` | 無 | 正確 | 工作包 PR 狀態屬於存取庫與 PR,不屬於 CLI;換 CLI 也要看到同一支未結清 PR。 |
|
||||
| `$JSC_HOME/model-tags.tsv`、`$JSC_HOME/models.conf`、`$JSC_HOME/html-styles.conf` | 無 | 正確 | 這些是全機共用設定,不是檢查紀錄。 |
|
||||
@@ -169,9 +168,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` 回填,再依序跑一次 `comment-scope.sh sweep` 與 `lang-guard.sh sweep` 掃整個 git 工作區的註解範圍與繁中編碼(copilot 與 antigravity 沒有任何逐輪事件,整個工作階段只有這裡掃得到)。兩次收尾掃描一律不影響結束碼:包裝器原樣回傳 CLI 自己的結束碼,`sweep` 命中只把警告印到 stderr。`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-get` 回 4(頁面真的不存在)才用範本建新頁,回 7(金鑰失效)或 8(其他 API 失敗)代表舊內容未知,放棄目錄頁寫入並以 exit 4 回報,免得拿範本蓋掉所有既有列。目錄頁那一列指向異常頁,連結一律寫成 `[{文字}]({連結})`,網址取 `jsc-gitea` 的 `gitea.sh wiki-url` 印出的那一個,不自己組路徑。寫進那一格之前,先把那個網址交給 `jsc-gitea` 的 `tools/link-check.sh` 驗一次,結束碼 0 才寫連結;`link-check.sh` 的路徑由已經解出來的 `gitea.sh` 推得,兩支同一個 tools 目錄。驗不過(含找不到 `link-check.sh`、`GITEA_HOST` 未設定回 3、金鑰失效回 7)就只在那一格留純文字頁名,那一列照寫、異常頁照寫、結束碼照舊,原因走 stderr——回報失敗不該再變成一次失敗。網址在異常頁寫成功之後才取:頁名的 hash 帶時間戳,每次回報都是全新的頁,寫進去之前查一定是 404,先查就只拿得到空字串。取不到網址時只印頁名,原因走 stderr,結束碼照舊回 0。wiki 位置分兩次解析:異常頁走 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR`,目錄頁走 `gitea.sh wiki-repo CONTENTS`,兩者是兩個不同的存取庫。異常頁的存取庫解不出來就整支安靜降級;只有目錄頁的存取庫解不出來,就只寫異常頁、跳過目錄頁更新,仍回 exit 0。由操作者手動執行,或由 `hooks-install` 在 `wire-cli.sh` 回報 `status=failed` 時執行;**不接在失敗的 hook 上自動觸發**(hook 一律安靜 exit 0,自我回報會疊出迴圈) |
|
||||
| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共四個用法。`{cli}` 是接線:先建立或更新 `$JSC_HOME/current/jsc-hooks` 指向目前這版 plugin,接著把對應的設定編輯、包裝別名安裝、hook 檔建立成穩定路徑,皆以 `<!-- jsc-hooks -->`(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝;寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、`.codex-plugin/plugin.json` 的 matcher 必須是 `Bash`、copilot 必須是小寫 `skill` 且沒有第二種大小寫的事件名、antigravity 的 matcher 必須帶錨點 `^view_file$` 且有 `PreInvocation`、kiro 的 agent JSON 必須成對且 `hooks`、`resources`、`tools` 在最上層並含兩層 `skill://` glob),也會確認寫入路徑能解到既有腳本。matcher 本身要單獨驗:鍵在、matcher 卻錯的形態最難查,回報會說接好了,實際一次都不會被叫用。檔案系統不能建立 symlink 時,會明確回報並退回目前根目錄,不會靜默寫出壞路徑。`status=wired\|degraded\|skipped\|failed` 回報接線結果。`purge {cli}` 是移除:把該 CLI 的**所有** hook 清掉,含非 jsc 的第三方項目,動到的檔案先原樣備份到 `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`,備份失敗就不移除;移除標記段落時會先去掉標記行前後空白,所以縮排或尾端補空白的 jsc 區塊一樣會移除;移除後重讀驗證,驗不過自動還原備份,以 `status=purged\|skipped\|failed` 回報。`smoke {cli}` 是執行期冒煙測試:九支 hook 的每個接線模式各跑一次,非零退出即為錯誤,另外把五支 CLI 的真實負載各餵進 `skill-name.sh` 一次驗技能名解析、四種阻擋形態各驗一次 `deny.sh`,再把那些負載直接餵進 `restart-gate.sh` 驗「解析→判定→輸出形態」整條串得起來(含 fail-open、豁免放行與 kiro 的注入路徑)——前兩組分開看都會顯示正常,中間接不上照樣是全程放行,那正是先前三支 CLI 失效的樣子;另外用一份暫時的 `$JSC_HOME` 狀態檔把模型來源與階段鎖、工作包歸屬、部署後重啟閘門與寫入提交閘門的每條判定路徑各跑一次並比對結束碼(模型來源的每個案例各自指定 CLI 代號,不跟著這一輪接線的 CLI 走——偵測鏈已依 CLI 分流;「不知道能力就擋下」的三種情形連訊息裡的逃生門一起驗,只比結束碼的話訊息漏掉逃生門也是綠燈),再用一份暫時的 `HOME`(假的 `installed_plugins.json` 與各 plugin 的 manifest)把 `version-guard.sh` 相依版本檢查的每條路徑跑一次——相依落後的擋人與訊息內容、相等與超前的放行、豁免技能在相依落後時照樣放行、四種 fail-open、逃生門,另加一條回歸:多行縮排的 manifest,`jsc.requires` 的最後一個鍵也要解得到。驗的是判定結果本身,不只是腳本跑得完(例外有四個:`sdlc-gate.sh check` 的 exit 2 是階段鎖的設計行為,`comment-scope.sh`、`lang-guard.sh` 掃描模式與 `write-guard.sh` 三種模式的 exit 2 是命中違規的設計行為——`sweep` 在髒工作區本來就會回 2,`write-guard.sh` 在機器剛好鎖在 `plan` 階段時也會回 2,都不算 hook 壞掉),以 `status=ok\|failed` 回報。**結果行數由腳本自己數、自己斷言**:`status=` 之後緊接一行 `lines<TAB>{數量}`,那是其後 `[jsc]` 結果行的實際條數,與腳本內逐類宣告的預期條數比對,不符就回非零。判定路徑增減時只改腳本裡的預期值,散文一律引用這一行,不另外抄一份數字。`status {cli}` 是唯讀盤點:只讀設定檔判斷段落與 matcher 對不對,不寫檔也不執行 hook,claude、codex、copilot、antigravity 回 `wired`,kiro 回 `degraded` 並在 `reason` 講明那是 CLI 限制;每個接線點印一行 `item<TAB>{項目}<TAB>{路徑}<TAB>{present\|missing\|unverified}`,也會把帶版號快取路徑、開發存取庫路徑與不存在的腳本列為缺項。狀態有三格不是兩格:`unverified` 是「這一項驗不了」,只有 `missing` 才算缺項——`kiro-cli agent validate` 在沒登入時印的是環境問題,不是這個檔案的問題,報 `present` 會讓沒驗到的東西看起來像通過,報 `missing` 會把沒登入算成接線缺漏;`status claude` 讀 Claude Code 實際載入的 `installed_plugins.json`,不再檢查目前腳本旁邊那份 `hooks.json`。體檢類技能(`/jsc-cli:doctor`)只能用這個子命令,另外三個都會動到環境;那道限制另有程式層把關,`JSC_READONLY=1` 之下只准 `status` 與 `smoke`,`purge` 與接線一律以 exit 6 拒絕並回報 `status=readonly`,環境不會被動到 |
|
||||
| `tools/report-status.sh` | 技能與 hook 的執行狀態事件流,寫進 `$JSC_HOME/usage/events.jsonl`,一次一行。`skill-start`、`skill-end`、`hook-end` 三個記錄子命令;`drain` 印出上次排空之後的新事件(位移存在 `usage/scan-state/events.offset`,檔案比位移小就當作輪替過、從頭讀,不比對 inode——五支 CLI 與容器裡的行程看到的 inode 不保證一致);`rotate` 超過 5 MiB 就改名成 `.1` 並把位移歸零,只留一份舊的。`status` 是 `ok`、`blocked`、`failed`、`degraded`、`aborted` 五選一。**三個記錄子命令一律回 0,寫檔失敗也是 0**:回報機制自己壞掉,不可以讓被回報的東西跟著壞——hook 的結束碼是閘門的判準,被記錄動到就等於閘門行為被記錄改寫。參數檢查是例外,那是呼叫端的程式錯誤,寫進去只會汙染事件流,所以以 2 擋在記錄之前。本檔不讀 stdin:技能由 Bash 呼叫它,stdin 可能是還沒關閉的管線,讀下去會卡住宿主,所有資訊一律走參數。輪替不放在每次寫入,那等於每次提示多一次系統呼叫;改由巡檢排空之後呼叫。為什麼不直接寫 wiki:hook 每次提示都跑,網路寫入會拖垮宿主 CLI,而且失敗的 hook 自我回報會疊出迴圈,`report-error.sh` 因此刻意不接在失敗的 hook 上,這裡沿用同一條線 |
|
||||
| `tools/report-error.sh` | 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 `ERROR_{HASH}`,並在 `ERROR_CONTENTS` 附上一列索引。目錄頁一律先讀回舊頁再附加新列、整頁寫回,不整頁覆蓋:只有 `wiki-get` 回 4(頁面真的不存在)才用範本建新頁,回 7(金鑰失效)或 8(其他 API 失敗)代表舊內容未知,放棄目錄頁寫入並以 exit 4 回報,免得拿範本蓋掉所有既有列。目錄頁指向異常頁的連結用 `gitea.sh wiki-url` 的絕對網址,跨存取庫的 `[[頁名]]` 解不開。網址在異常頁寫成功之後才取:頁名的 hash 帶時間戳,每次回報都是全新的頁,寫進去之前查一定是 404,先查就只拿得到空字串。取不到網址時只印頁名,原因走 stderr,結束碼照舊回 0。wiki 位置分兩次解析:異常頁走 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR`,目錄頁走 `gitea.sh wiki-repo CONTENTS`,兩者是兩個不同的存取庫。異常頁的存取庫解不出來就整支安靜降級;只有目錄頁的存取庫解不出來,就只寫異常頁、跳過目錄頁更新,仍回 exit 0。由操作者手動執行,或由 `hooks-install` 在 `wire-cli.sh` 回報 `status=failed` 時執行;**不接在失敗的 hook 上自動觸發**(hook 一律安靜 exit 0,自我回報會疊出迴圈) |
|
||||
| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共四個用法。`{cli}` 是接線:先建立或更新 `$JSC_HOME/current/jsc-hooks` 指向目前這版 plugin,接著把對應的設定編輯、包裝別名安裝、hook 檔建立成穩定路徑,皆以 `<!-- jsc-hooks -->`(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝;寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、`.codex-plugin/plugin.json` 的 matcher 必須是 `Bash`、copilot 必須是小寫 `skill` 且沒有第二種大小寫的事件名、antigravity 的 matcher 必須帶錨點 `^view_file$` 且有 `PreInvocation`、kiro 的 agent JSON 必須成對且 `hooks`、`resources`、`tools` 在最上層並含兩層 `skill://` glob),也會確認寫入路徑能解到既有腳本。matcher 本身要單獨驗:鍵在、matcher 卻錯的形態最難查,回報會說接好了,實際一次都不會被叫用。檔案系統不能建立 symlink 時,會明確回報並退回目前根目錄,不會靜默寫出壞路徑。`status=wired\|degraded\|skipped\|failed` 回報接線結果。`purge {cli}` 是移除:把該 CLI 的**所有** hook 清掉,含非 jsc 的第三方項目,動到的檔案先原樣備份到 `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`,備份失敗就不移除;移除標記段落時會先去掉標記行前後空白,所以縮排或尾端補空白的 jsc 區塊一樣會移除;移除後重讀驗證,驗不過自動還原備份,以 `status=purged\|skipped\|failed` 回報。`smoke {cli}` 是執行期冒煙測試:九支 hook 的每個接線模式各跑一次,非零退出即為錯誤,另外把五支 CLI 的真實負載各餵進 `skill-name.sh` 一次驗技能名解析、四種阻擋形態各驗一次 `deny.sh`,再把那些負載直接餵進 `restart-gate.sh` 驗「解析→判定→輸出形態」整條串得起來(含 fail-open、豁免放行與 kiro 的注入路徑)——前兩組分開看都會顯示正常,中間接不上照樣是全程放行,那正是先前三支 CLI 失效的樣子;另外用一份暫時的 `$JSC_HOME` 狀態檔把工作包歸屬、部署後重啟閘門與寫入提交閘門的每條判定路徑各跑一次並比對結束碼,再用一份暫時的 `HOME`(假的 `installed_plugins.json` 與各 plugin 的 manifest)把 `version-guard.sh` 相依版本檢查的每條路徑跑一次——相依落後的擋人與訊息內容、相等與超前的放行、豁免技能在相依落後時照樣放行、四種 fail-open、逃生門,另加一條回歸:多行縮排的 manifest,`jsc.requires` 的最後一個鍵也要解得到。驗的是判定結果本身,不只是腳本跑得完(例外有四個:`sdlc-gate.sh check` 的 exit 2 是階段鎖的設計行為,`comment-scope.sh`、`lang-guard.sh` 掃描模式與 `write-guard.sh` 三種模式的 exit 2 是命中違規的設計行為——`sweep` 在髒工作區本來就會回 2,`write-guard.sh` 在機器剛好鎖在 `plan` 階段時也會回 2,都不算 hook 壞掉),以 `status=ok\|failed` 回報。**結果行數由腳本自己數、自己斷言**:`status=` 之後緊接一行 `lines<TAB>{數量}`,那是其後 `[jsc]` 結果行的實際條數,與腳本內逐類宣告的預期條數比對,不符就回非零。判定路徑增減時只改腳本裡的預期值,散文一律引用這一行,不另外抄一份數字。`status {cli}` 是唯讀盤點:只讀設定檔判斷段落與 matcher 對不對,不寫檔也不執行 hook,claude、codex、copilot、antigravity 回 `wired`,kiro 回 `degraded` 並在 `reason` 講明那是 CLI 限制;每個接線點印一行 `item<TAB>{項目}<TAB>{路徑}<TAB>{present\|missing\|unverified}`,也會把帶版號快取路徑、開發存取庫路徑與不存在的腳本列為缺項。狀態有三格不是兩格:`unverified` 是「這一項驗不了」,只有 `missing` 才算缺項——`kiro-cli agent validate` 在沒登入時印的是環境問題,不是這個檔案的問題,報 `present` 會讓沒驗到的東西看起來像通過,報 `missing` 會把沒登入算成接線缺漏;`status claude` 讀 Claude Code 實際載入的 `installed_plugins.json`,不再檢查目前腳本旁邊那份 `hooks.json`。體檢類技能(`/jsc-cli:doctor`)只能用這個子命令,另外三個都會動到環境;那道限制另有程式層把關,`JSC_READONLY=1` 之下只准 `status` 與 `smoke`,`purge` 與接線一律以 exit 6 拒絕並回報 `status=readonly`,環境不會被動到 |
|
||||
| `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` 回報 |
|
||||
|
||||
## 失敗回報範本
|
||||
@@ -182,7 +180,7 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
||||
| 範本 | 用途 |
|
||||
| --- | --- |
|
||||
| `templates/error-page.md` | 單筆 hook 異常頁 `ERROR_{HASH}`,記錄當次失敗的觸發條件、錯誤摘要與處理結果。 |
|
||||
| `templates/error-contents.md` | 異常目錄 `ERROR_CONTENTS`,彙整所有異常頁,方便先看最新問題再往下追。落在目錄專用存取庫,一律 upsert 附加,連結一律寫成 `[{文字}]({連結})` 且先過 `link-check.sh` 驗過才寫。 |
|
||||
| `templates/error-contents.md` | 異常目錄 `ERROR_CONTENTS`,彙整所有異常頁,方便先看最新問題再往下追。落在目錄專用存取庫,一律 upsert 附加,連結用絕對網址。 |
|
||||
|
||||
## Skills 目錄
|
||||
|
||||
@@ -228,7 +226,7 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
||||
| `JSC_CHANGED_FILE` | 非 Claude CLI 要掃描的檔案路徑,代替 stdin JSON 的 `file_path`,供 `comment-scope.sh` 與 `lang-guard.sh` 使用 | 安靜降級,不掃描 |
|
||||
| `JSC_TOOL_COMMAND` | 非 Claude CLI 要判定的 Bash 指令字串,代替 stdin JSON 的 `command`,供 `write-guard.sh commit` 使用 | 安靜降級,不判定 |
|
||||
| `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` / `JSC_TOOL_NAME` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供,代替 stdin JSON 的 `session_id`、技能名與 `tool_name`(`hooks/skill-name.sh` 也收沒有前綴的 `SKILL`,而且環境變數蓋過負載解析;`write-guard.sh` 也收 `TOOL_NAME`)。`version-guard.sh` 與 `restart-gate.sh` 已經不篩工具名——五支 CLI 的工具名各不相同(`Skill`、`Bash`、`skill`、`view_file`),拿 Claude 那一個當通用條件會把另外四支整批擋在判定之外 | 安靜降級:`JSC_CLI` 取不到就當查不到 CLI,技能名取不到就由負載解析,兩邊都空就放行 |
|
||||
| `JSC_MODEL` | `sdlc-gate.sh` 在當前 CLI 自己的模型來源都失敗時的人工覆寫模型 id,五支 CLI 都適用;回報會標明 `人工覆寫:JSC_MODEL` | 找不到可驗證模型來源時 `lock` 拒絕、`check` 擋下該輪提示,並列出已檢查來源與兩條逃生門 |
|
||||
| `JSC_MODEL` | `sdlc-gate.sh` 找不到 transcript、hook stdin JSON 與 Codex 本機 session 記錄時的人工覆寫模型 id;回報會標明 `人工覆寫:JSC_MODEL` | 找不到可驗證模型來源時拒絕 `lock`,並列出已檢查來源與修復建議 |
|
||||
|
||||
## 相關 domain
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ if [ "${JSC_ASSISTANT_GATE:-}" = "off" ]; then
|
||||
fi
|
||||
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
hook_trace "assistant-gate ${1:-}"
|
||||
|
||||
read_stdin
|
||||
|
||||
@@ -201,8 +200,5 @@ esac
|
||||
printf '%s\n' "$second"
|
||||
printf '仍可使用:/jsc-assist:*、/jsc-hooks:repair、/jsc-hooks:hooks-install、/jsc-cli:doctor、/jsc-cli:setup、/jsc-cli:deploy、/jsc-cli:models、/jsc-gitea:wiki、/jsc-ask:ask、/jsc-git:commit、/jsc-git:pr(啟動助理與修環境這兩條路徑要永遠走得通,包括它們轉呼叫的下一層)\n'
|
||||
printf '確定要略過閘門:JSC_ASSISTANT_GATE=off\n'
|
||||
# 這條路徑是「已經擋下」,但輸出形態依 CLI 而定:antigravity 走 stdout 的 deny JSON、
|
||||
# kiro 只印警告,兩者的結束碼都是 0。不覆寫狀態的話,事件流會把擋下記成放行。
|
||||
JSC_EVENT_STATUS=blocked
|
||||
} | sh "$HERE/deny.sh" "$(cli_name)"
|
||||
exit $?
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
set -u
|
||||
|
||||
. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/lib.sh" 2>/dev/null || true
|
||||
command -v hook_trace >/dev/null 2>&1 && hook_trace "comment-scope ${1:-}"
|
||||
|
||||
[ "${JSC_COMMENT_SCOPE:-on}" = "off" ] && exit 0
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
# 2。這一點的後果與 restart-gate.sh 不同:那支接在 PreToolUse 上,回 2 等於無聲擋下每一次
|
||||
# 技能呼叫;這支沒接任何 hook,回 2 只會讓呼叫端收到「心跳判不出來」,擋不到任何人。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
hook_trace "heartbeat ${1:-}"
|
||||
|
||||
# 這支永遠不讀標準輸入,但 session_id() 會去看 STDIN_JSON。先設成空字串,讓它直接走環境
|
||||
# 變數那條路,不會因為變數沒定義而拿到不確定的值。
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
set -u
|
||||
|
||||
. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/lib.sh" 2>/dev/null || true
|
||||
command -v hook_trace >/dev/null 2>&1 && hook_trace "lang-guard ${1:-}"
|
||||
|
||||
[ "${JSC_LANG_GUARD:-on}" = "off" ] && exit 0
|
||||
|
||||
|
||||
+25
-173
@@ -2,9 +2,8 @@
|
||||
# lib.sh — jsc hooks 共用函式。所有 hook 腳本 source 此檔。
|
||||
# 輸入相容:Claude 式 stdin JSON、或環境變數(codex/copilot/antigravity/kiro 接線時設定)。
|
||||
# 缺資料時安靜降級,hook 預設 exit 0,不可中斷宿主 CLI。
|
||||
# 唯一例外:sdlc-gate.sh check 在「SDLC 階段鎖存在,而且不知道目前模型的能力」時會 exit 2
|
||||
# 擋下該輪提示。不知道能力有三種:判不出是哪一支 CLI、判不出模型、模型不在能力標籤表上。
|
||||
# 這是刻意的 fail-closed——放行等於閘門不存在。沒有階段鎖時仍照舊 exit 0。
|
||||
# 唯一例外:sdlc-gate.sh check 在「SDLC 階段鎖存在且模型不符」時會 exit 2 擋下該輪提示;
|
||||
# 其餘情況(無鎖、資料不足無法判定)仍照舊 exit 0。
|
||||
#
|
||||
# 結束碼:不適用。本檔是被 source 的共用函式庫,不是可執行入口,內部一次 exit 都沒有。
|
||||
# 載入成功回 0(最後一行是函式定義);拿 `sh lib.sh` 直接跑也只是定義完函式回 0,不做事。
|
||||
@@ -32,58 +31,12 @@ json_str() { # $1=欄位名
|
||||
}
|
||||
|
||||
# 目前 session id:stdin JSON > 環境變數 > 固定值
|
||||
#
|
||||
# 環境變數這一段的順序:
|
||||
# JSC_SESSION_ID jsc 自己的指定值(tools/jsc-wrap.sh 會設),人工指定優先於偵測。
|
||||
# CLAUDE_CODE_SESSION_ID claude 實際匯出的名字,實測確認過。
|
||||
# CLAUDE_SESSION_ID 只留作往後相容,排在實測名之後。這個名字在目前的 claude 上並不
|
||||
# 存在,兩個都設到的話,該信的是實測看得到的那一個;只設到這一個
|
||||
# 的版本仍然退得下來,所以留著不會有損失。
|
||||
# 為什麼這一條要修:退路鏈原本只找 CLAUDE_SESSION_ID,那個名字取不到值,於是沒有 stdin 的
|
||||
# 執行路徑(技能以 Bash 呼叫 sdlc-gate.sh lock)一律退回 default,而 hook 有 stdin、拿得到
|
||||
# 真正的 id。同一個工作階段的兩條路徑因此算出兩支不同的狀態檔,階段鎖上了也永遠對不上。
|
||||
# 這裡只補退路鏈的變數名,不動取值順序以外的行為:其他 CLI 的變數名沒有實測過,猜一個填進來
|
||||
# 只會多一個錯誤來源。
|
||||
session_id() {
|
||||
sid=$(json_str session_id)
|
||||
[ -n "$sid" ] || sid="${JSC_SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-${CLAUDE_SESSION_ID:-default}}}"
|
||||
[ -n "$sid" ] || sid="${JSC_SESSION_ID:-${CLAUDE_SESSION_ID:-default}}"
|
||||
printf '%s' "$sid"
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔名要用的 CLI 代號。這個值直接拼進檔名,所以不像代號的字元一律換掉;
|
||||
# 帶斜線或空白的值會把檔案寫到別的地方去,理由與 restart-gate.sh 的做法相同。
|
||||
stage_state_cli() {
|
||||
cli_name | sed 's/[^A-Za-z0-9._-]/_/g'
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔的舊路徑:$JSC_HOME/sessions/{sid}.stage。只作往後相容的讀取來源,
|
||||
# 以及 unlock 的清除對象,新的寫入一律不走這裡。
|
||||
stage_state_legacy_file() {
|
||||
printf '%s/sessions/%s.stage' "$JSC_HOME" "$(session_id)"
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔(寫入用):$JSC_HOME/sessions/{cli}-{sid}.stage。
|
||||
# 為什麼要帶 CLI 代號:session_id() 判不出工作階段時會退回 default,各 CLI 於是共用同一支
|
||||
# default.stage,一支上的階段鎖就會擋到另一支。那不只擋提示——write-guard.sh 的 stage 模式
|
||||
# 在 plan 或 analyze 持鎖時連 Write、Edit、MultiEdit 一起擋掉。
|
||||
# 為什麼用檔名前綴而不是子目錄:外部工具以單層的 sessions/*.stage 盤點階段鎖,改成子目錄
|
||||
# 會讓每一支鎖從那些盤點裡整批消失;前綴照樣列得到,只是多一段代號。
|
||||
stage_state_file() {
|
||||
printf '%s/sessions/%s-%s.stage' "$JSC_HOME" "$(stage_state_cli)" "$(session_id)"
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔(讀取用):新路徑優先,沒有才回舊路徑。舊檔記的是實際工作,只讀不搬也不刪。
|
||||
# 兩邊都沒有時回新路徑,讓呼叫端一律用「檔案在不在」判斷有沒有鎖。
|
||||
stage_state_read_file() {
|
||||
_sf=$(stage_state_file)
|
||||
if [ -f "$_sf" ]; then
|
||||
printf '%s' "$_sf"
|
||||
else
|
||||
_so=$(stage_state_legacy_file)
|
||||
if [ -f "$_so" ]; then printf '%s' "$_so"; else printf '%s' "$_sf"; fi
|
||||
fi
|
||||
}
|
||||
|
||||
MODEL_SOURCE_CHECKS=""
|
||||
|
||||
model_checked() {
|
||||
@@ -122,17 +75,9 @@ codex_session_file() { # $1=CODEX_HOME $2=session id
|
||||
find "$ch/sessions" -type f -name "*$sid*.jsonl" 2>/dev/null | sort | tail -n 1
|
||||
return 0
|
||||
fi
|
||||
# 全樹最新的一支。原本寫成 `xargs ls -t | head -n 1`,那是錯的:xargs 會依參數長度分批,
|
||||
# ls -t 只在自己那一批裡排序,取到的是「第一批裡最新的」而不是全域最新。改成讓 find 一併
|
||||
# 印出修改時間,所有檔案在同一輪比較,批次邊界就影響不到結果。
|
||||
newest=$(find "$ch/sessions" -type f -name '*.jsonl' -printf '%T@\t%p\n' 2>/dev/null \
|
||||
| sort -rn | head -n 1 | cut -f2-)
|
||||
# find 沒有 -printf(非 GNU)時退回路徑排序。codex 的 session 依「年/月/日」分目錄,
|
||||
# 檔名又以 ISO 時間開頭,字典序等同時間序,仍然是全域比較,不會被分批切斷。
|
||||
[ -n "$newest" ] || newest=$(find "$ch/sessions" -type f -name '*.jsonl' 2>/dev/null \
|
||||
| sort | tail -n 1)
|
||||
[ -n "$newest" ] || return 1
|
||||
printf '%s\n' "$newest"
|
||||
files=$(find "$ch/sessions" -type f -name '*.jsonl' 2>/dev/null)
|
||||
[ -n "$files" ] || return 1
|
||||
printf '%s\n' "$files" | xargs ls -t 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
codex_model() {
|
||||
@@ -153,57 +98,28 @@ codex_model() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# 目前模型的判定。輸出單行「{模型 id}<TAB>{來源}<TAB>{已檢查來源}」,判不出時前兩欄留空。
|
||||
#
|
||||
# 偵測鏈依 CLI 分流:一支 CLI 只讀自己的紀錄。兩個理由。
|
||||
# 一是正確性:跨過去讀別支的紀錄,拿到的是別支的模型,用它判定這一支等於沒有判準。
|
||||
# 二是速度:別支的 session 目錄可能有上千個檔案,每一輪 hook 都掃一次會把宿主 CLI 拖住。
|
||||
# 判不出是哪一支 CLI 時不猜任何來源——不知道是誰,就不知道該讀誰的紀錄。
|
||||
# JSC_MODEL 這個人工覆寫對所有 CLI 都保留,而且一律排在最後:可驗證的紀錄優先於人工宣告。
|
||||
current_model_report() {
|
||||
MODEL_SOURCE_CHECKS=""
|
||||
_mcli=$(cli_name)
|
||||
model_checked "JSC_CLI:$_mcli"
|
||||
|
||||
case "$_mcli" in
|
||||
claude)
|
||||
# transcript 是 claude 自己寫下的執行紀錄,也是唯一逐輪更新的來源。
|
||||
tp=$(json_str transcript_path)
|
||||
[ -n "$tp" ] || tp="${JSC_TRANSCRIPT_PATH:-}"
|
||||
model_checked "transcript_path:${tp:-未提供}"
|
||||
if [ -n "$tp" ] && [ -f "$tp" ]; then
|
||||
m=$(transcript_model)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "transcript:$tp" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
tp=$(json_str transcript_path)
|
||||
[ -n "$tp" ] || tp="${JSC_TRANSCRIPT_PATH:-}"
|
||||
model_checked "transcript_path:${tp:-未提供}"
|
||||
if [ -n "$tp" ] && [ -f "$tp" ]; then
|
||||
m=$(transcript_model)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "transcript:$tp" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
|
||||
model_checked "hook-stdin:model/model_id/model_slug/modelName/current_model/currentModel"
|
||||
m=$(json_model_value "$STDIN_JSON")
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "hook-stdin" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
;;
|
||||
codex)
|
||||
# hook 負載是這一輪由 codex 自己餵進來的,不是別支 CLI 的紀錄,所以照收;
|
||||
# 而且它反映當下這一輪,比落在檔案裡的紀錄新,排在 session 記錄前面。
|
||||
model_checked "hook-stdin:model/model_id/model_slug/modelName/current_model/currentModel"
|
||||
m=$(json_model_value "$STDIN_JSON")
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "hook-stdin" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
model_checked "hook-stdin:model/model_id/model_slug/modelName/current_model/currentModel"
|
||||
m=$(json_model_value "$STDIN_JSON")
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "hook-stdin" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
|
||||
model_checked "codex:${CODEX_HOME:-$HOME/.codex}/sessions、history.jsonl、session_index.jsonl"
|
||||
cm=$(codex_model 2>/dev/null || true)
|
||||
if [ -n "$cm" ]; then
|
||||
m=$(printf '%s' "$cm" | cut -f1)
|
||||
src=$(printf '%s' "$cm" | cut -f2)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "$src" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
;;
|
||||
copilot|antigravity|kiro)
|
||||
# 這三支由 tools/jsc-wrap.sh 包起來跑,只餵得到環境變數:沒有 transcript、沒有 hook
|
||||
# 負載,本機也沒有可讀的模型紀錄。這裡如實記成「沒有來源」,不去翻別支 CLI 的檔案。
|
||||
model_checked "$_mcli:本機沒有可讀的模型紀錄,只認 JSC_MODEL"
|
||||
;;
|
||||
*)
|
||||
model_checked "未知 CLI:判不出是哪一支,不採用任何自動來源"
|
||||
;;
|
||||
esac
|
||||
model_checked "codex:${CODEX_HOME:-$HOME/.codex}、~/.codex/sessions、history.jsonl、session_index.jsonl"
|
||||
cm=$(codex_model 2>/dev/null || true)
|
||||
if [ -n "$cm" ]; then
|
||||
m=$(printf '%s' "$cm" | cut -f1)
|
||||
src=$(printf '%s' "$cm" | cut -f2)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "$src" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
|
||||
model_checked "JSC_MODEL"
|
||||
if [ -n "${JSC_MODEL:-}" ]; then
|
||||
@@ -214,18 +130,10 @@ current_model_report() {
|
||||
printf '\t\t%s\n' "$MODEL_SOURCE_CHECKS"
|
||||
}
|
||||
|
||||
# 目前 CLI 名稱:環境變數 > 依環境特徵判斷 > unknown
|
||||
#
|
||||
# claude 的判準有三個,任一個成立就算。理由與 session_id() 那條退路鏈同源:CLAUDE_PLUGIN_ROOT
|
||||
# 只有 plugin 接線的 hook 執行環境才有,技能以 Bash 工具呼叫腳本時並不存在,於是同一個工作
|
||||
# 階段的兩條路徑一條認得出 claude、一條回 unknown,狀態檔名跟著分岔。CLAUDECODE 與
|
||||
# CLAUDE_CODE_SESSION_ID 兩者在工具呼叫的環境裡都看得到(實測確認),補上去兩條路徑才算得出
|
||||
# 同一個代號。只補 claude 這一支:其他 CLI 的環境特徵沒有實測過,猜一個填進來只會多一個
|
||||
# 錯誤來源,那幾支本來就由接線設定明確帶 JSC_CLI。
|
||||
# 目前 CLI 名稱:環境變數 > 依 stdin 特徵猜測 > unknown
|
||||
cli_name() {
|
||||
if [ -n "${JSC_CLI:-}" ]; then printf '%s' "$JSC_CLI"
|
||||
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || [ -n "${CLAUDECODE:-}" ] \
|
||||
|| [ -n "${CLAUDE_CODE_SESSION_ID:-}" ]; then printf 'claude'
|
||||
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then printf 'claude'
|
||||
else printf 'unknown'; fi
|
||||
}
|
||||
|
||||
@@ -263,59 +171,3 @@ cli_bin() { # $1=CLI 代號
|
||||
|
||||
now_epoch() { date +%s; }
|
||||
now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
|
||||
# --- 執行狀態事件流 ---
|
||||
#
|
||||
# 每支 hook 與每支技能的執行結果都寫進 $JSC_HOME/usage/events.jsonl,助理巡檢時排空。
|
||||
# 為什麼不直接寫 wiki:hook 每次提示都跑,網路寫入會拖垮宿主 CLI;而且失敗的 hook
|
||||
# 自我回報會疊出迴圈,report-error.sh 因此刻意不接在失敗的 hook 上,這裡沿用同一條線。
|
||||
#
|
||||
# 兩條硬規則,違反哪一條這套機制都會反過來害到被它記錄的東西:
|
||||
# 一、一行一次 printf,且長度壓在 4096 位元組內。五支 CLI 併發時,單次 O_APPEND
|
||||
# 寫入才不會互相插隊;拆成多次 printf 就會交錯成無法解析的行。detail 因此要截斷。
|
||||
# 二、寫入失敗一律吞掉,不得改變呼叫端的結束碼。回報機制自己壞掉,不可以讓被回報的
|
||||
# 東西跟著壞——hook 的結束碼是閘門的判準,被記錄動到就等於閘門行為被記錄改寫。
|
||||
|
||||
# JSON 字串值跳脫:只處理反斜線、雙引號與會拆行的字元。這三類不處理就會寫出解析不了的行。
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | tr -d '\n\r\t'
|
||||
}
|
||||
|
||||
# emit_event <kind> <name> <phase> <status> <exit> [ms] [detail]
|
||||
emit_event() {
|
||||
_ek="$1"; _en="$2"; _ep="$3"; _es="$4"; _ex="$5"; _em="${6:-}"; _ed="${7:-}"
|
||||
# detail 截到 200 字元:長內容是硬規則一的主要威脅,來源不可信就先砍再寫。
|
||||
[ -n "$_ed" ] && _ed=$(printf '%s' "$_ed" | cut -c1-200)
|
||||
_ems=""
|
||||
[ -n "$_em" ] && _ems=$(printf ',"ms":%s' "$_em")
|
||||
_eds=""
|
||||
[ -n "$_ed" ] && _eds=$(printf ',"detail":"%s"' "$(json_escape "$_ed")")
|
||||
printf '{"ts":"%s","cli":"%s","session":"%s","kind":"%s","name":"%s","phase":"%s","status":"%s","exit":%s%s%s}\n' \
|
||||
"$(now_iso)" "$(cli_name)" "$(session_id)" "$_ek" "$(json_escape "$_en")" \
|
||||
"$_ep" "$_es" "$_ex" "$_ems" "$_eds" \
|
||||
>> "$JSC_HOME/usage/events.jsonl" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# 結束碼推 status。各 hook 的 2 一律是「擋下」的設計行為,不是壞掉。
|
||||
hook_status_of() {
|
||||
case "$1" in
|
||||
0) printf ok ;;
|
||||
2) printf blocked ;;
|
||||
*) printf failed ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# hook_trace <名稱> — 裝一個 EXIT trap,腳本不論從哪一個 exit 離開都記一筆。
|
||||
#
|
||||
# 為什麼用 trap 而不是逐點改:這幾支 hook 的 exit 點很多,sdlc-gate.sh 一支就有五十幾個。
|
||||
# 逐點換成「記錄再離開」要改動每一條判定路徑,而那些路徑正是閘門的判準;為了加一行紀錄
|
||||
# 去動閘門,風險遠大於收益。trap 只加一行,且涵蓋每一條離開路徑,含 set -e 的中途失敗。
|
||||
#
|
||||
# 狀態預設由結束碼推。推不出來的由 hook 自己在離開前設 JSC_EVENT_STATUS 覆寫——
|
||||
# version-guard.sh 就有這種情形:antigravity 走 stdout 的 deny JSON、kiro 只印警告,
|
||||
# 兩者擋下時結束碼都是 0,單看結束碼會把「已經擋下」記成「放行」。
|
||||
hook_trace() {
|
||||
JSC_EVENT_NAME="$1"
|
||||
JSC_EVENT_STATUS=""
|
||||
trap '_rc=$?; emit_event hook "$JSC_EVENT_NAME" end "${JSC_EVENT_STATUS:-$(hook_status_of "$_rc")}" "$_rc"' EXIT
|
||||
}
|
||||
|
||||
@@ -120,7 +120,6 @@
|
||||
#
|
||||
# 逃生門:JSC_RESTART_GATE=off 完全略過這道閘門。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
hook_trace "restart-gate ${1:-}"
|
||||
|
||||
STATE_DIR="$JSC_HOME/restart-required.d"
|
||||
# 舊格式的單一狀態檔。只為過渡而讀,可移除的時機見檔頭「舊檔相容」。
|
||||
@@ -250,8 +249,5 @@ info=""
|
||||
printf '重新啟動:結束 %s 再重新開啟一次,狀態檔 %s 會在新工作階段開始時自動清除。\n' \
|
||||
"$bin" "$state"
|
||||
printf '仍可使用:/jsc-cli:deploy、/jsc-hooks:hooks-install、/jsc-hooks:repair、/jsc-gitea:wiki、/jsc-log:worklog、/jsc-log:learn、/jsc-meta:*、/jsc-ask:ask、/jsc-git:pr、/jsc-git:commit(部署後的異動報告與工作日誌要寫得完,hook 壞掉也要修得回來) | 確定要略過閘門:JSC_RESTART_GATE=off\n'
|
||||
# 這條路徑是「已經擋下」,但輸出形態依 CLI 而定:antigravity 走 stdout 的 deny JSON、
|
||||
# kiro 只印警告,兩者的結束碼都是 0。不覆寫狀態的話,事件流會把擋下記成放行。
|
||||
JSC_EVENT_STATUS=blocked
|
||||
} | sh "$HERE/deny.sh" "$(cli_name)"
|
||||
exit $?
|
||||
|
||||
+20
-99
@@ -3,37 +3,18 @@
|
||||
#
|
||||
# 判準是階段的「能力標籤」,不是模型名稱:
|
||||
# - 標籤資料讀 $JSC_HOME/model-tags.tsv,由 jsc-cli/tools/model-tags.sh sync 產生。
|
||||
# - 目前模型 id 取自可驗證紀錄,而且只讀屬於當前 CLI 的那一份(判定鏈見 lib.sh 的
|
||||
# current_model_report)。claude 讀 transcript 與 hook stdin,codex 讀 hook stdin 與
|
||||
# 自己的 session 記錄,其餘 CLI 本機沒有可讀的紀錄。只有這些來源都失敗時,才接受
|
||||
# JSC_MODEL 這個人工覆寫。
|
||||
# - 目前模型 id 取自可驗證紀錄。優先讀 transcript,其次讀 hook stdin JSON 與 Codex 本機
|
||||
# session 記錄。只有這些來源都失敗時,才接受 JSC_MODEL 這個人工覆寫。
|
||||
# - 不採用對話內容裡模型自稱的 id。自我回報無法驗證,等同沒有閘門。
|
||||
# - 鎖存的是「階段的必要標籤」,不是「上鎖那一刻的模型」。鎖當時的模型只留作記錄,
|
||||
# 否則用不合格的模型起跑就會把自己鎖成合格,閘門永遠通過。
|
||||
#
|
||||
# 政策是 fail-closed:不知道能力就擋下,知道才比對能力標籤。不知道能力有三種,一律擋下——
|
||||
# 判不出是哪一支 CLI、判不出目前模型、模型判得出來但不在能力標籤表上。放行任何一種,用不
|
||||
# 合格的模型跑階段就查不出來,閘門形同虛設。代價是被擋住的人可能不知道怎麼脫困,所以每一則
|
||||
# 擋下的訊息都要帶逃生門:設 JSC_MODEL 人工指明模型,或執行本檔的 unlock 解除該階段的鎖。
|
||||
#
|
||||
# 狀態檔:$JSC_HOME/sessions/{cli}-{sid}.stage,單行
|
||||
# 「{stage}<TAB>{必要標籤}<TAB>{上鎖時的模型}<TAB>{模型來源}」。
|
||||
# 檔名帶 CLI 代號的理由見 lib.sh 的 stage_state_file:session id 判不出時會退回 default,
|
||||
# 不分 CLI 就會共用同一支 default.stage,一支上的鎖擋到另一支。舊路徑
|
||||
# $JSC_HOME/sessions/{sid}.stage 仍讀得到(往後相容),unlock 也會一併清掉,
|
||||
# 但新的寫入一律走新路徑。
|
||||
# 狀態檔:$JSC_HOME/sessions/{sid}.stage,單行「{stage}<TAB>{必要標籤}<TAB>{上鎖時的模型}<TAB>{模型來源}」。
|
||||
#
|
||||
# 用法:
|
||||
# sdlc-gate.sh lock {stage} 階段閘門:比對實際模型與該階段必要標籤,通過才上鎖。
|
||||
# exit 0 = 通過並已上鎖;exit 1 = 未通過,呼叫端必須停止流程。
|
||||
# sdlc-gate.sh unlock [{狀態檔}]
|
||||
# 移除狀態檔(被擋住又確定要放行時的逃生門)。不帶參數時清掉這個
|
||||
# 工作階段的新舊兩份;帶參數時清掉指定的那一份。
|
||||
# 為什麼要收參數:hook 拿得到 stdin 的 session id 與 JSC_CLI,
|
||||
# 手動執行拿不到,兩邊算出來的檔名可能不同,逃生門就按不到真正
|
||||
# 擋人的那一支。所以擋人訊息會把實際讀到的路徑一起印出來,
|
||||
# 照抄就解得開。參數只收 sessions 目錄底下的 .stage 檔,
|
||||
# 避免這個逃生門變成任意刪檔的工具。
|
||||
# sdlc-gate.sh unlock 移除狀態檔(被擋住又確定要放行時的逃生門)。
|
||||
# sdlc-gate.sh check hook 模式(UserPromptSubmit):模型不符即擋下該輪提示。
|
||||
# sdlc-gate.sh report 印出 {sid} {stage} {必要標籤} {模型 id} {模型來源} {判定};無鎖不印。
|
||||
#
|
||||
@@ -58,10 +39,9 @@
|
||||
# 結束碼(一個子命令一列):
|
||||
# lock 0=通過並已上鎖 1=未通過,呼叫端必須停止流程(階段名不合法、讀不到該
|
||||
# 階段的必要標籤、判定不出目前模型、模型不在標籤表上、缺標籤、寫不進狀態檔)
|
||||
# unlock 0=永遠成功,狀態檔不存在也算;不帶參數時新舊兩種路徑都會清掉
|
||||
# 2=參數不是 sessions 目錄底下的 .stage 檔
|
||||
# check 0=放行 2=擋下該輪提示。安靜放行只剩三種:沒有階段鎖、舊格式狀態檔讀不出
|
||||
# 必要標籤、必要標籤是 any。判定不出目前模型改為擋下,見上面的 fail-closed
|
||||
# unlock 0=永遠成功,狀態檔不存在也算
|
||||
# check 0=放行 2=擋下該輪提示。安靜放行:沒有階段鎖、舊格式狀態檔讀不出必要標籤、
|
||||
# 必要標籤是 any、判定不出目前模型(只提醒不擋)
|
||||
# report 0=永遠成功,無鎖就不印
|
||||
# wp-lock 0=已記下 2=存取庫不是 {owner}/{repo}、PR 編號不是數字、寫不進狀態檔
|
||||
# wp-unlock 0=已結清,狀態檔不存在也算 2=存取庫或 PR 編號格式錯誤
|
||||
@@ -123,7 +103,6 @@
|
||||
# 刻意的例外——鎖存在且不合規時 exit 2 擋下。只用提示注入的話模型可以無視,閘門形同虛設。
|
||||
# 無鎖、或資料不足無法判定時,仍照舊 exit 0 安靜降級。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
hook_trace "sdlc-gate ${1:-}"
|
||||
# 只有需要 stdin JSON 的子命令才讀它:模型判定要 transcript_path,session 判定要 session_id。
|
||||
# wp-lock、wp-unlock、wp-claim、wp-unclaim、wp-report 兩者都不需要,而 read_stdin 在標準輸入
|
||||
# 是管線又沒人關閉時會一直等——工具腳本(jsc-sdlc 的 wp-gate.sh)轉呼叫這些子命令時就這樣整支
|
||||
@@ -133,9 +112,7 @@ case "${1:-}" in
|
||||
*) read_stdin ;;
|
||||
esac
|
||||
sid=$(session_id)
|
||||
# 讀寫兩條路徑分開:寫入一律走帶 CLI 代號的新路徑,讀取則允許退回舊路徑(往後相容)。
|
||||
state=$(stage_state_read_file)
|
||||
state_new=$(stage_state_file)
|
||||
state="$JSC_HOME/sessions/$sid.stage"
|
||||
TAGS_TSV="$JSC_HOME/model-tags.tsv"
|
||||
STAGES="plan analyze implement maintain"
|
||||
|
||||
@@ -205,33 +182,8 @@ model_verdict() { # $1=stage $2=req $3=model
|
||||
[ -z "$miss" ] && printf 'pass' || printf 'fail:%s' "$miss"
|
||||
}
|
||||
|
||||
# 逃生門說明。被擋住的人多半不熟這套東西,訊息要讓他照著做就脫困,所以兩條路都寫成
|
||||
# 可以照抄的指令,而且把「可以填什麼」一併列出來。
|
||||
gate_escape_hint() { # $1=必要標籤 $2=擋人的那一支狀態檔(可省略)
|
||||
_ok=$(eligible_models "$1")
|
||||
echo "逃生門二選一:"
|
||||
echo " 1. 人工指明模型:設環境變數 JSC_MODEL={模型 id} 之後重來一次。標籤表上合格的模型 id:${_ok:-(表上沒有合格模型,請先補表)}。"
|
||||
# 路徑照抄進指令裡:手動執行時算出來的檔名跟 hook 算出來的可能不同,不點名就解不到那一支。
|
||||
if [ -n "${2:-}" ]; then
|
||||
echo " 2. 解除這個階段的鎖:執行 sh $0 unlock $2。解除之後這道閘門就不再擋。"
|
||||
else
|
||||
echo " 2. 解除這個階段的鎖:執行 sh $0 unlock。解除之後這道閘門就不再擋。"
|
||||
fi
|
||||
}
|
||||
|
||||
# 判不出 CLI 時追加一句。這是「不知道能力」的第一種,成因與修法都跟另外兩種不同:
|
||||
# 不知道是哪一支 CLI,就不知道該讀誰的模型紀錄,接線設定要把代號帶上。
|
||||
gate_cli_hint() {
|
||||
[ "$(cli_name)" = unknown ] || return 0
|
||||
echo "另外:目前判不出是哪一支 CLI(環境變數 JSC_CLI 沒有設),所以連該讀哪一份模型紀錄都不知道。接線設定應以 JSC_CLI={CLI 代號} 帶上。"
|
||||
}
|
||||
|
||||
model_fail_message() { # $1=stage $2=checked $3=必要標籤
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:判定不出目前實際使用的模型,無法驗證是否符合階段「$1」(需要 ${3:-未知})。已檢查來源:${2:-無}。本次不進行任何工作。"
|
||||
gate_cli_hint
|
||||
gate_escape_hint "${3:-any}"
|
||||
} >&2
|
||||
model_fail_message() { # $1=stage $2=checked
|
||||
echo "[jsc][SDLC 閘門][ERR]:判定不出目前實際使用的模型,無法驗證是否符合階段「$1」。已檢查來源:${2:-無}。請確認 hook stdin 帶有 transcript_path 或 model 欄位、Codex session 記錄可讀,或明確設定 JSC_MODEL 作為人工覆寫後重跑。本次不進行任何工作。" >&2
|
||||
}
|
||||
|
||||
# --- 工作包 PR 閘門(狀態檔:$JSC_HOME/wp/{owner}-{repo}-{index}.pr,一個工作包一支) ---
|
||||
@@ -339,17 +291,14 @@ case "${1:-check}" in
|
||||
src=$(printf '%s' "$info" | cut -f2)
|
||||
checked=$(printf '%s' "$info" | cut -f3-)
|
||||
if [ -z "$cur" ]; then
|
||||
model_fail_message "$stage" "$checked" "$req"
|
||||
model_fail_message "$stage" "$checked"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$req" != "any" ]; then
|
||||
have=$(model_tags "$cur")
|
||||
if [ -z "$have" ]; then
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:模型「$cur」(來源 ${src:-未知})不在能力標籤表上,無法判定是否夠格跑階段「$stage」(需要 $req)。請把該模型補進 jsc-cli/references/model-tags.md 後執行 model-tags.sh sync,本次不進行任何工作。"
|
||||
gate_escape_hint "$req"
|
||||
} >&2
|
||||
echo "[jsc][SDLC 閘門][ERR]:模型「$cur」不在能力標籤表上,無法判定是否夠格跑階段「$stage」(需要 $req)。請把該模型補進 jsc-cli/references/model-tags.md 後執行 model-tags.sh sync,本次不進行任何工作。" >&2
|
||||
exit 1
|
||||
fi
|
||||
miss=$(missing_tags "$req" "$have")
|
||||
@@ -360,29 +309,13 @@ case "${1:-check}" in
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\t%s\t%s\t%s\n' "$stage" "$req" "$cur" "$src" > "$state_new" 2>/dev/null || {
|
||||
echo "[jsc][SDLC 閘門][ERR]:寫不進狀態檔 $state_new,階段鎖未生效。" >&2; exit 1; }
|
||||
printf '%s\t%s\t%s\t%s\n' "$stage" "$req" "$cur" "$src" > "$state" 2>/dev/null || {
|
||||
echo "[jsc][SDLC 閘門][ERR]:寫不進狀態檔 $state,階段鎖未生效。" >&2; exit 1; }
|
||||
echo "[jsc][SDLC 閘門][OK]:階段「$stage」通過(需要 $req,目前模型 $cur,來源 $src),已上鎖。"
|
||||
exit 0 ;;
|
||||
|
||||
unlock)
|
||||
target="${2:-}"
|
||||
if [ -n "$target" ]; then
|
||||
# 只認「上一層目錄叫 sessions、副檔名是 .stage」的路徑:逃生門不該順便變成任意刪檔的
|
||||
# 工具。判準刻意不寫成「必須在這支行程算出來的 $JSC_HOME 底下」——擋人的是 hook,
|
||||
# 解鎖的是人在殼層手動執行,兩邊的 JSC_HOME 未必相同,綁上去就會把照抄訊息的人擋掉,
|
||||
# 那正是這個逃生門要避免的事。
|
||||
case "$target" in
|
||||
*/sessions/*.stage) ;;
|
||||
*)
|
||||
echo "[jsc][SDLC 閘門][ERR]:unlock 的參數只收 sessions 目錄底下的 .stage 檔,收到「$target」。不帶參數則清掉這個工作階段自己那一份。" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
rm -f "$target" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
# 新舊兩種路徑都要清。只清新的話,舊格式的鎖永遠解不開,被它擋住的人就沒有逃生門。
|
||||
rm -f "$state_new" "$(stage_state_legacy_file)" 2>/dev/null || true
|
||||
rm -f "$state" 2>/dev/null || true
|
||||
exit 0 ;;
|
||||
|
||||
check)
|
||||
@@ -397,24 +330,15 @@ case "${1:-check}" in
|
||||
cur=$(printf '%s' "$info" | cut -f1)
|
||||
src=$(printf '%s' "$info" | cut -f2)
|
||||
checked=$(printf '%s' "$info" | cut -f3-)
|
||||
# 判定不出模型就擋下。這是刻意改成 fail-closed:放行等於「換一支讀不到紀錄的 CLI
|
||||
# 就能繞過閘門」,閘門形同虛設。原本只提醒不擋,怕的是把人鎖在送不出提示的狀態——
|
||||
# 那個風險是真的,所以訊息一定要把兩條逃生門寫清楚,照著做就能脫困。
|
||||
# 判定不出模型時只提醒,不擋——否則使用者會被鎖在無法送出提示的狀態。
|
||||
if [ -z "$cur" ]; then
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:SDLC 階段「${stage}」需要能力標籤「${req}」,但判定不出目前用的是哪個模型,也就無法確認夠不夠格。已檢查來源:${checked:-無}。本輪提示已擋下。"
|
||||
gate_cli_hint
|
||||
gate_escape_hint "$req" "$state"
|
||||
} >&2
|
||||
exit 2
|
||||
echo "[jsc] SDLC 階段「${stage}」需要標籤「${req}」,但判定不出目前模型。已檢查來源:${checked:-無}。請確認 transcript、Codex session 記錄,或設定 JSC_MODEL 人工覆寫。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
have=$(model_tags "$cur")
|
||||
if [ -z "$have" ]; then
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:模型「${cur}」(來源 ${src:-未知})不在能力標籤表上,無法確認是否夠格跑階段「${stage}」(需要 ${req})。請補進 jsc-cli/references/model-tags.md 並執行 model-tags.sh sync。本輪提示已擋下。"
|
||||
gate_escape_hint "$req" "$state"
|
||||
} >&2
|
||||
echo "[jsc][SDLC 閘門][ERR]:模型「${cur}」(來源 ${src:-未知})不在能力標籤表上,無法確認是否夠格跑階段「${stage}」(需要 ${req})。請補進 jsc-cli/references/model-tags.md 並執行 model-tags.sh sync;確定要放行請執行 jsc-hooks/hooks/sdlc-gate.sh unlock。本輪提示已擋下。" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -422,10 +346,7 @@ case "${1:-check}" in
|
||||
[ -z "$miss" ] && exit 0
|
||||
|
||||
ok=$(eligible_models "$req")
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:SDLC 階段「${stage}」需要標籤「${req}」,目前模型「${cur}」(來源 ${src:-未知})缺少「${miss}」。請切換到下列任一模型後重送:${ok:-(表上無合格模型,請補表)}。本輪提示已擋下。"
|
||||
echo "要結束本階段的鎖請執行:sh $0 unlock $state"
|
||||
} >&2
|
||||
echo "[jsc][SDLC 閘門][ERR]:SDLC 階段「${stage}」需要標籤「${req}」,目前模型「${cur}」(來源 ${src:-未知})缺少「${miss}」。請切換到下列任一模型後重送:${ok:-(表上無合格模型,請補表)}。要結束本階段的鎖請執行 jsc-hooks/hooks/sdlc-gate.sh unlock。本輪提示已擋下。" >&2
|
||||
exit 2 ;;
|
||||
|
||||
report)
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
# 清除的範圍是「跑到這一支腳本的那個 CLI 自己那一份狀態檔」,由 restart-gate.sh clear 認定,
|
||||
# 這裡不必也不能過問:這個工作階段開始的只有一支 CLI,別支沒重啟,閘門要留著。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
hook_trace "session-timer ${1:-}"
|
||||
read_stdin
|
||||
sid=$(session_id)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
# 唯一的非零來源同 session-timer.sh:本檔以 `. "$HERE/lib.sh"` 載入,沒有接 `|| true`,
|
||||
# lib.sh 讀不到時 sh 會就地結束並回 2。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
hook_trace "skill-usage ${1:-}"
|
||||
read_stdin
|
||||
skill="${JSC_SKILL:-$(json_str skill)}"
|
||||
[ -n "$skill" ] || exit 0
|
||||
@@ -26,10 +25,4 @@ if [ -n "$last" ]; then
|
||||
"$ts" "$cli" "$sid" "$last" "$skill" >> "$JSC_HOME/usage/chains.jsonl"
|
||||
fi
|
||||
printf '%s' "$skill" > "$last_f"
|
||||
|
||||
# 技能的 start 事件在這裡發,不必改任何 SKILL.md:這支接在技能指示載入之後,那一刻
|
||||
# 就是「技能開始跑」。end 只能由技能自己在收尾步驟寫——本 hook 觸發時,技能的實際工作
|
||||
# 還在後面的模型輪次,看不到成敗。有 start 沒有配對的 end,就是那一輪中止了。
|
||||
emit_event skill "$skill" start ok 0
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
# 再呼叫一次 report,這裡刻意不混印,免得 cut 取值被表格內容打亂。
|
||||
# recommend 只讀不擋,永遠 exit 0:判定結果只看那一行的第二欄。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
hook_trace "version-guard ${1:-}"
|
||||
|
||||
REG="$HOME/.claude/plugins/installed_plugins.json"
|
||||
MK="$HOME/.claude/plugins/known_marketplaces.json"
|
||||
@@ -355,9 +354,6 @@ deny() { # $1=訊息
|
||||
{ printf '[jsc][版本檢查][ERR]:%s\n' "$1"
|
||||
printf '更新指令:%s\n' "$(update_cmd "$domain")"
|
||||
printf '更新整組:/jsc-cli:deploy | 確定要略過檢查:JSC_VERSION_GUARD=off\n'
|
||||
# 這條路徑是「已經擋下」,但輸出形態依 CLI 而定:antigravity 走 stdout 的 deny JSON、
|
||||
# kiro 只印警告,兩者的結束碼都是 0。不覆寫狀態的話,事件流會把擋下記成放行。
|
||||
JSC_EVENT_STATUS=blocked
|
||||
} | sh "$HERE/deny.sh" "$(cli_name)"
|
||||
exit $?
|
||||
}
|
||||
|
||||
+5
-10
@@ -24,12 +24,10 @@
|
||||
#
|
||||
# --- stage 模式 ---
|
||||
#
|
||||
# 階段鎖狀態檔沿用 sdlc-gate.sh 那一份($JSC_HOME/sessions/{cli}-{sid}.stage,單行
|
||||
# 階段鎖狀態檔沿用 sdlc-gate.sh 那一份($JSC_HOME/sessions/{sid}.stage,單行
|
||||
# 「{階段}<TAB>{必要標籤}<TAB>{上鎖時的模型}」),這裡只讀不寫:判準與格式留在 sdlc-gate.sh,
|
||||
# 兩邊各存一份就會漂移。路徑一律取自 lib.sh 的 stage_state_read_file,連「新路徑優先、舊路徑
|
||||
# 往後相容」這條規則也不在這裡抄第二份。plan 與 analyze 是純邏輯階段,產出是 wiki 頁不是
|
||||
# 程式碼,所以那兩個階段鎖著時 Write、Edit、MultiEdit 一律擋下;implement 與 maintain
|
||||
# 本來就要寫檔,放行。
|
||||
# 兩邊各存一份就會漂移。plan 與 analyze 是純邏輯階段,產出是 wiki 頁不是程式碼,所以那兩個
|
||||
# 階段鎖著時 Write、Edit、MultiEdit 一律擋下;implement 與 maintain 本來就要寫檔,放行。
|
||||
#
|
||||
# --- review 模式 ---
|
||||
#
|
||||
@@ -75,7 +73,6 @@ HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
# 的 exit 2——而 PreToolUse 的 exit 2 正是「擋下」,等於每一次寫檔與提交都被無聲擋死。
|
||||
[ -r "$HERE/lib.sh" ] || exit 0
|
||||
. "$HERE/lib.sh"
|
||||
hook_trace "write-guard ${1:-}"
|
||||
# lib.sh 沒載到時這個變數就沒人設,下面兩個模式都要用它組狀態檔路徑,補一份同樣的預設值。
|
||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||
|
||||
@@ -127,18 +124,16 @@ write_tool_or_exit() {
|
||||
# ── stage:plan 與 analyze 階段鎖存在時擋下寫檔
|
||||
if [ "$mode" = stage ]; then
|
||||
write_tool_or_exit
|
||||
state=$(stage_state_read_file)
|
||||
state="$JSC_HOME/sessions/$(session_id).stage"
|
||||
[ -f "$state" ] && [ -r "$state" ] || exit 0
|
||||
stage=$(cut -f1 "$state" 2>/dev/null | head -n1)
|
||||
case "$stage" in
|
||||
plan|analyze) ;;
|
||||
*) exit 0 ;; # implement 與 maintain 本來就要寫檔;讀不出階段也放行
|
||||
esac
|
||||
# 解除指令帶上實際讀到的狀態檔路徑:擋人的是 hook,解鎖的是人在殼層手動執行,兩邊算出來
|
||||
# 的檔名未必相同,不點名就會解到別的地方去。
|
||||
deny "目前鎖在 SDLC「$stage」階段,這個階段只產出計畫或分析頁,不寫檔案。本次寫入已擋下。" \
|
||||
"改法:把結論寫進該階段的 wiki 頁;真的要動程式碼請先進入 implement 階段。
|
||||
解除階段鎖:sh $HERE/sdlc-gate.sh unlock $state"
|
||||
解除階段鎖:jsc-hooks/hooks/sdlc-gate.sh unlock"
|
||||
fi
|
||||
|
||||
# ── review:稽核類技能執行中擋下寫檔
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-hooks",
|
||||
"version": "0.4.2",
|
||||
"version": "0.3.8",
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||
"skills": "./skills/",
|
||||
"jsc": {
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 裝好或更新完 jsc 技能組之後,要把九支 hook 接線到每一支已安裝的 CLI 時用;`jsc-cli:deploy` 收尾會把偵測到的 CLI 清單交給它。不用於撰寫新的 hook,也不用於單獨修一支壞掉的 hook,那是 `jsc-hooks:repair` 的事 |
|
||||
| 關鍵步驟 | 取得 CLI 清單(呼叫端交來的優先,沒有才自己跑 `detect-clis.sh`)、第一支 CLI 單獨跑完整條管線(它負責更新共用的 `$JSC_HOME/current/jsc-hooks` 連結)、其餘 CLI 一支一個 sub agent 並行、每支 CLI 依序走 purge、接線、status、smoke、scan 五道關卡、讀每道關卡自己印的第一行判定、任一關卡出錯就寫 `ERROR_{HASH}` 並轉給 `jsc-hooks:repair`(異常頁與索引目錄頁分屬兩個存取庫,各自解析;只解不出目錄頁的存取庫時異常頁照寫、索引跳過,回報要講明那一頁沒被索引)、目錄頁那一列指向異常頁的連結一律寫成 `[{文字}]({連結})`,網址取 `gitea.sh wiki-url`,寫進去之前先過 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫連結、驗不過那一格只留純文字頁名而那一列與異常頁照寫(`report-error.sh` 內部做完,結束碼不變)、逐 CLI 回報五道關卡的結果、最後由主代理呼叫一次 `tools/report-status.sh skill-end jsc-hooks:hooks-install {status} {結束碼} {detail}` 記下整輪怎麼結束。腳本在同一個存取庫,用 `tools/` 相對路徑;這一筆只由主代理寫一次,寫在並行的各 CLI sub agent 裡會變成五筆互相矛盾的結局。腳本不在就安靜跳過,回報失敗不得變成接線失敗 |
|
||||
| 外部呼叫 | `tools/wire-cli.sh purge`、`tools/wire-cli.sh {cli}`、`tools/wire-cli.sh status`、`tools/wire-cli.sh smoke`、`tools/scan-hook-errors.sh`、`tools/report-error.sh`、`jsc-cli/tools/detect-clis.sh`、`jsc-hooks:repair` 技能、`jsc-gitea:wiki`(寫 `ERROR_{HASH}` 時經 `report-error.sh`)、`jsc-gitea/tools/gitea.sh wiki-url` 與 `jsc-gitea/tools/link-check.sh`(同樣經 `report-error.sh`,取目錄頁那一列的網址並驗它連得到);接線腳本內部另呼叫 `hooks/skill-name.sh` 與 `hooks/deny.sh` 做冒煙斷言 |
|
||||
| 完成條件 | 每一支偵測到的 CLI 都有五道關卡各一行判定,沒有任何一道回結束碼 2,smoke 的 `lines` 條數與它自己的斷言相符,claude、codex、copilot、antigravity 回 `wired` 而 kiro 回 `degraded`(CLI 擋不下技能叫用),四支非 claude 的執行期錯誤掃描一律據實回 `unavailable`,各 CLI 的形狀與觸發驗證等級分開寫進回報(codex、antigravity、kiro 形狀實證,copilot 形狀未證;kiro 觸發部分實證,其餘未驗證),每一筆錯誤都帶一個 `ERROR_{HASH}` 結果與一條對 `develop` 的修正 PR 連結,而且目錄頁那一列的連結驗不過時,回報要講明那一列只有純文字頁名、沒有連結。以上收完之後還要走完最後一步:呼叫 `report-status.sh skill-end`,狀態五選一——每一支偵測到的 CLI 五道關卡全過而且一律回 `wired` 是 `ok`,實務上等於那台機器沒有 kiro;跑完整輪但有一部分沒到 `wired` 是 `degraded`,涵蓋 kiro 只能降級接線,以及某支 CLI 接線失敗、已經轉交 `jsc-hooks:repair` 而且拿到一條對 `develop` 的 PR,還有被跳過的 CLI;關卡失敗而沒人接手是 `failed`,也就是修正技能起不來或回不出 PR,壞掉的 hook 還掛在那裡;`JSC_READONLY=1` 讓 `wire-cli.sh` 回 exit 6、一支 CLI 都沒動是 `blocked`;偵測不到任何 CLI 而主動停止是 `aborted`。腳本不在磁碟上就跳過,這一步照樣算走完 |
|
||||
| 可驗證跡象 | 各 CLI 的設定檔多出 jsc 段落:codex 的 `config.toml` 標記段落、`hooks/codex-hooks.json`(從 `hooks/hooks.json` 推導,matcher `Skill` 換成 `Bash`)與 `.codex-plugin/plugin.json` 指過去的 `hooks` 路徑字串、copilot 的 `~/.copilot/settings.json` 頂層 `hooks` 鍵(matcher `skill`,合併不覆寫,`enabledPlugins` 與第三方條目原樣保留)與 `$COPILOT_HOME` 底下的指引檔、antigravity 的 `~/.gemini/config/hooks.json` 的 `jsc` 段落(`PreToolUse` 為 Grouped、matcher `^view_file$`,`PreInvocation` 維持 Flat)、kiro 的 `~/.kiro/agents/jsc.json`(`hooks` 為 `agentSpawn`、`userPromptSubmit`、`stop` 三個合法事件加 `timeout_ms`、兩層 `skill://` glob 的 `resources`、明列的 `tools`,並通過 `kiro-cli agent validate`)與 `~/.kiro/settings/cli.json` 的 `chat.defaultAgent=jsc`;四支非 claude 的接線命令都以 `JSC_CLI={代號}` 前綴自帶 CLI 代號,缺了它兩道閘門解不出技能名、一律安靜放行,所以 `status` 把它列成單獨一項;另有 `$JSC_HOME/current/jsc-hooks` 符號連結建立或更新、`$JSC_HOME/backup/hooks/{cli}/{時間戳}/` 留下 purge 前的備份、出錯時 wiki 多一頁 `ERROR_{HASH}`(落在 `JSC_WIKI_REPO_ERROR` 解出的存取庫)並在索引目錄頁補一列(落在 `JSC_WIKI_REPO_CONTENTS` 解出的另一個存取庫,那一列的第 2 格寫成 `[{頁名}]({絕對網址})`,網址取自 `gitea.sh wiki-url` 且已經過 `link-check.sh` 驗到結束碼 0;驗不過那一格只有純文字頁名,`report-error.sh` 在 stderr 留一行 `[jsc]` 講明是哪一種原因)、修正路徑留下一條對 `develop` 的 PR 。接線完成後 `$JSC_HOME/usage/events.jsonl` 會逐行長出 `{kind:hook}` 事件,每支 hook 每次執行一筆,欄位含 `status` 與實際結束碼;跑過技能之後另有 `{kind:skill,phase:start}`。這支技能自己收尾時,同一個 `$JSC_HOME/usage/events.jsonl` 尾端會多一筆 `{kind:skill,phase:end}`,`name` 是 `jsc-hooks:hooks-install`,整輪只有一筆,`status` 與那次結局相符,`exit` 是決定結局的那道關卡的結束碼;`report-status.sh` 不在那台機器上就沒有這一筆,接線結果一字不變。事件寫不進去不影響任何 hook 的結束碼,也不影響本技能的結局 |
|
||||
| 關鍵步驟 | 取得 CLI 清單(呼叫端交來的優先,沒有才自己跑 `detect-clis.sh`)、第一支 CLI 單獨跑完整條管線(它負責更新共用的 `$JSC_HOME/current/jsc-hooks` 連結)、其餘 CLI 一支一個 sub agent 並行、每支 CLI 依序走 purge、接線、status、smoke、scan 五道關卡、讀每道關卡自己印的第一行判定、任一關卡出錯就寫 `ERROR_{HASH}` 並轉給 `jsc-hooks:repair`(異常頁與索引目錄頁分屬兩個存取庫,各自解析;只解不出目錄頁的存取庫時異常頁照寫、索引跳過,回報要講明那一頁沒被索引)、逐 CLI 回報五道關卡的結果 |
|
||||
| 外部呼叫 | `tools/wire-cli.sh purge`、`tools/wire-cli.sh {cli}`、`tools/wire-cli.sh status`、`tools/wire-cli.sh smoke`、`tools/scan-hook-errors.sh`、`tools/report-error.sh`、`jsc-cli/tools/detect-clis.sh`、`jsc-hooks:repair` 技能、`jsc-gitea:wiki`(寫 `ERROR_{HASH}` 時經 `report-error.sh`);接線腳本內部另呼叫 `hooks/skill-name.sh` 與 `hooks/deny.sh` 做冒煙斷言 |
|
||||
| 完成條件 | 每一支偵測到的 CLI 都有五道關卡各一行判定,沒有任何一道回結束碼 2,smoke 的 `lines` 條數與它自己的斷言相符,claude、codex、copilot、antigravity 回 `wired` 而 kiro 回 `degraded`(CLI 擋不下技能叫用),四支非 claude 的執行期錯誤掃描一律據實回 `unavailable`,各 CLI 的形狀與觸發驗證等級分開寫進回報(codex、antigravity、kiro 形狀實證,copilot 形狀未證;kiro 觸發部分實證,其餘未驗證),而且每一筆錯誤都帶一個 `ERROR_{HASH}` 結果與一條對 `develop` 的修正 PR 連結 |
|
||||
| 可驗證跡象 | 各 CLI 的設定檔多出 jsc 段落:codex 的 `config.toml` 標記段落、`hooks/codex-hooks.json`(從 `hooks/hooks.json` 推導,matcher `Skill` 換成 `Bash`)與 `.codex-plugin/plugin.json` 指過去的 `hooks` 路徑字串、copilot 的 `~/.copilot/settings.json` 頂層 `hooks` 鍵(matcher `skill`,合併不覆寫,`enabledPlugins` 與第三方條目原樣保留)與 `$COPILOT_HOME` 底下的指引檔、antigravity 的 `~/.gemini/config/hooks.json` 的 `jsc` 段落(`PreToolUse` 為 Grouped、matcher `^view_file$`,`PreInvocation` 維持 Flat)、kiro 的 `~/.kiro/agents/jsc.json`(`hooks` 為 `agentSpawn`、`userPromptSubmit`、`stop` 三個合法事件加 `timeout_ms`、兩層 `skill://` glob 的 `resources`、明列的 `tools`,並通過 `kiro-cli agent validate`)與 `~/.kiro/settings/cli.json` 的 `chat.defaultAgent=jsc`;四支非 claude 的接線命令都以 `JSC_CLI={代號}` 前綴自帶 CLI 代號,缺了它兩道閘門解不出技能名、一律安靜放行,所以 `status` 把它列成單獨一項;另有 `$JSC_HOME/current/jsc-hooks` 符號連結建立或更新、`$JSC_HOME/backup/hooks/{cli}/{時間戳}/` 留下 purge 前的備份、出錯時 wiki 多一頁 `ERROR_{HASH}`(落在 `JSC_WIKI_REPO_ERROR` 解出的存取庫)並在索引目錄頁補一列(落在 `JSC_WIKI_REPO_CONTENTS` 解出的另一個存取庫,那一列指向異常頁的連結是絕對網址)、修正路徑留下一條對 `develop` 的 PR |
|
||||
|
||||
## repair
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | `hooks-install` 或 `report-error.sh` 回報某一支 hook 失敗時用,或是重新接線之後那支 hook 還是一直失敗時用。不用於例行接線,也不用於與 hook 無關的修改 |
|
||||
| 關鍵步驟 | 從 `ERROR_{HASH}` 讀失敗情境(沒有頁就讀失敗的 `status=` 那一行,讀不到就停下來問)、跑 `detect-clis.sh`、每一支偵測到的 CLI 各開一個唯讀 sub agent 診斷並交回根因、要改的檔案與驗證指令、挑最小的修正改進 hooks 存取庫(技能名解析改 `hooks/skill-name.sh`、阻擋形態改 `hooks/deny.sh`,兩支是唯一真實來源,不在閘門裡各補一份)、跑 `wire-cli.sh smoke {cli}` 驗到 exit 0、跑 `sync-skill-manifest.sh .` 同步版本、以 `jsc-git:pr` 對 `develop` 開 PR、收尾呼叫 `tools/report-status.sh skill-end jsc-hooks:repair {status} {結束碼} {detail}` 記下這次修正怎麼結束(腳本在同一個存取庫,用 `tools/` 相對路徑,比照 `tools/wire-cli.sh`;檔案不在就安靜跳過,回報失敗不得變成修正失敗) |
|
||||
| 關鍵步驟 | 從 `ERROR_{HASH}` 讀失敗情境(沒有頁就讀失敗的 `status=` 那一行,讀不到就停下來問)、跑 `detect-clis.sh`、每一支偵測到的 CLI 各開一個唯讀 sub agent 診斷並交回根因、要改的檔案與驗證指令、挑最小的修正改進 hooks 存取庫(技能名解析改 `hooks/skill-name.sh`、阻擋形態改 `hooks/deny.sh`,兩支是唯一真實來源,不在閘門裡各補一份)、跑 `wire-cli.sh smoke {cli}` 驗到 exit 0、跑 `sync-skill-manifest.sh .` 同步版本、以 `jsc-git:pr` 對 `develop` 開 PR |
|
||||
| 外部呼叫 | `jsc-gitea:wiki`、`jsc-cli/tools/detect-clis.sh`、`tools/wire-cli.sh smoke`、`jsc-meta/tools/sync-skill-manifest.sh`、`jsc-git:pr`;診斷階段另以 sub agent 叫用各支已安裝的 AI CLI |
|
||||
| 完成條件 | 修正已經落在磁碟上、`wire-cli.sh smoke` 對受影響的 CLI 回 exit 0、`sync-skill-manifest.sh` 回 exit 0 而且三份 manifest 版本一致,最後拿到一條對 `develop` 的 PR 連結;開不出 PR 時要講明修正已套用但尚未合併、帶上分支名與失敗原因。每一條路線都要走完最後一步:呼叫 `report-status.sh skill-end`,狀態五選一——修正落地、smoke 回 exit 0、三份 manifest 版本一致而且拿到 PR 連結是 `ok`;smoke 過了但東西沒送出去是 `degraded`,也就是開不出 PR 只剩分支,或 manifest 沒對齊;修不好是 `failed`,也就是診斷繞回去以後 smoke 還是回 exit 4,或同步版本踩到環境錯誤,壞掉的接線還是壞的;沒有可修的項目是 `aborted`,也就是讀不到任何失敗情境。本技能豁免版本閘門與部署後重啟閘門,沒有別的閘門擋得住它,所以不會用 `blocked`。腳本不在磁碟上就跳過,這一步照樣算走完 |
|
||||
| 可驗證跡象 | hooks 存取庫多一個修正提交與一條推上去的分支、`develop` 上多一條 PR、三份 manifest 與 README 技能清單版本一致、`wire-cli.sh smoke` 由失敗轉為 exit 0;不論走哪一條路線,`$JSC_HOME/usage/events.jsonl` 尾端都會多一筆 `{kind:skill,phase:end}` 事件,`name` 是 `jsc-hooks:repair`,`status` 與那次結局相符,`exit` 是決定結局的那支工具的結束碼;`report-status.sh` 不在那台機器上就沒有這一筆,修正結果一字不變 |
|
||||
| 完成條件 | 修正已經落在磁碟上、`wire-cli.sh smoke` 對受影響的 CLI 回 exit 0、`sync-skill-manifest.sh` 回 exit 0 而且三份 manifest 版本一致,最後拿到一條對 `develop` 的 PR 連結;開不出 PR 時要講明修正已套用但尚未合併、帶上分支名與失敗原因 |
|
||||
| 可驗證跡象 | hooks 存取庫多一個修正提交與一條推上去的分支、`develop` 上多一條 PR、三份 manifest 與 README 技能清單版本一致、`wire-cli.sh smoke` 由失敗轉為 exit 0 |
|
||||
|
||||
@@ -75,12 +75,8 @@ The detailed flow **MUST run as a sub agent**; the main agent only reports the s
|
||||
5. `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 the smoke stage alone. Exit 0 covers both `clean` and `unavailable`, exit 1 is `errors` and every entry with `jsc=true` goes to step 3, exit 2 is a bad CLI name.
|
||||
|
||||
Done when every detected CLI has exactly one verdict line per stage, no stage exited 2, the smoke stage's `lines` count matches its own assertion, the four non-claude CLIs are reported as `unavailable` rather than clean on the scan stage, and antigravity and kiro carry the note that their hook firing is unverified.
|
||||
3. For each error — a failed purge, a failed wiring, an `unwired` status, a failed smoke, 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. The error page and the error directory page live in two different wiki repos, resolved separately: the page through `wiki-repo ERROR`, the directory through `wiki-repo CONTENTS`. Exit 0 with an `ERROR_{HASH}` page name and URL on stdout means the page was written; the same exit 0 with a `[jsc]` line on stderr still means the page landed, and that line says what is missing — the directory repo would not resolve, so nothing indexes the page; the page URL could not be read back, so the page name comes out on its own; or the URL failed the reachability check, so the directory row carries the page name as plain text with no link — carry that note into step 4. Every link on that row is written as `[{text}]({url})` with the URL from `gitea.sh wiki-url`, and the script checks it with `jsc-gitea/tools/link-check.sh` before writing: exit 0 writes the link, anything else keeps the row and drops the link, and none of it changes the exit code — this is the failure-reporting path, so a failed report must never become a second failure. Exit 0 with no output at all means the run ended on one of the quiet-degradation reasons listed in the script's own header — no `gitea.sh` on the path, the error page's wiki repo unresolved, the hash not computed, or a temp file not created — so no page was written at all and that reason goes into step 4 instead; exit 2 means the call itself was malformed — `--hook` or `--summary` is missing — so fix the arguments and rerun the same call; exit 4 means the wiki record did not land, so report the failure text and still start the repair — a page that could not be written is no reason to leave a broken hook wired. Exit 4 covers two cases, and the report has to say which: a failed write, or the script refusing to write the error directory page because it could not read the old one back. That directory is appended to, never overwritten: every row on it is somebody else's error report, so the script reads the page, adds this run's row, and writes the whole page. Only a genuine 404 (`wiki-get` exit 4) means the page is not there yet and lets it build one from the template. An invalid key (exit 7) or any other API failure (exit 8) leaves the old rows unknown, so it skips the directory write and names the code instead — writing a fresh template over a directory it never read would erase every earlier report, with no merge and no backup behind it. A scanned error with `jsc=false` belongs to a third-party hook: report it and leave it alone. Skip this step when every CLI passed all five stages. Done when every error carries one `ERROR_{HASH}` result — a page name with its URL, a page name plus the reason the URL is missing, or the recorded reason no page was written — and one repair PR URL against `develop`.
|
||||
3. For each error — a failed purge, a failed wiring, an `unwired` status, a failed smoke, 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. The error page and the error directory page live in two different wiki repos, resolved separately: the page through `wiki-repo ERROR`, the directory through `wiki-repo CONTENTS`. Exit 0 with an `ERROR_{HASH}` page name and URL on stdout means the page was written; the same exit 0 with a `[jsc]` line on stderr still means the page landed, and that line says what is missing — either the directory repo would not resolve, so nothing indexes the page, or the page URL could not be read back, so the page name comes out on its own — carry that note into step 4. Exit 0 with no output at all means the run ended on one of the quiet-degradation reasons listed in the script's own header — no `gitea.sh` on the path, the error page's wiki repo unresolved, the hash not computed, or a temp file not created — so no page was written at all and that reason goes into step 4 instead; exit 2 means the call itself was malformed — `--hook` or `--summary` is missing — so fix the arguments and rerun the same call; exit 4 means the wiki record did not land, so report the failure text and still start the repair — a page that could not be written is no reason to leave a broken hook wired. Exit 4 covers two cases, and the report has to say which: a failed write, or the script refusing to write the error directory page because it could not read the old one back. That directory is appended to, never overwritten: every row on it is somebody else's error report, so the script reads the page, adds this run's row, and writes the whole page. Only a genuine 404 (`wiki-get` exit 4) means the page is not there yet and lets it build one from the template. An invalid key (exit 7) or any other API failure (exit 8) leaves the old rows unknown, so it skips the directory write and names the code instead — writing a fresh template over a directory it never read would erase every earlier report, with no merge and no backup behind it. A scanned error with `jsc=false` belongs to a third-party hook: report it and leave it alone. Skip this step when every CLI passed all five stages. Done when every error carries one `ERROR_{HASH}` result — a page name with its URL, a page name plus the reason the URL is missing, or the recorded reason no page was written — and one repair PR URL against `develop`.
|
||||
4. Report five results per CLI — purge, wiring, status, smoke, scan — each with the reason its script printed, plus the smoke `lines` count, any `ERROR_{HASH}` page name and every repair PR URL. Done when every detected CLI appears with one verdict per stage and every repair has a PR against `develop`.
|
||||
5. Record how the whole install ended. Run `tools/report-status.sh skill-end jsc-hooks:hooks-install {status} {exit} "{detail}"` — the script is in this same repo, so it takes the plain `tools/` path that every other stage above uses. **The main agent makes this one call, after every per-CLI report is in.** The per-CLI pipelines run as parallel sub agents and one skill run is one event, so a call inside those sub agents would write one line per CLI and turn the install's outcome into five contradictory ones. The gate that records a skill's start fires when the skill is loaded and can never see how it ended; without this line a finished install and an install abandoned halfway look identical afterwards, which is the whole reason the closing step exists.
|
||||
- `{status}` is one of five. `ok`: every detected CLI passed all five stages and every one of them reported `wired` — in practice that means kiro was not on the machine. `degraded`: the pipeline ran to the end and part of it did not reach `wired`. That covers kiro, which is `degraded` by design because the CLI cannot block a skill call, and it covers a CLI whose stage failed and was handed to `jsc-hooks:repair` with a PR against `develop` — the failure has an owner and a fix in flight, so the install is incomplete, not broken. A `skipped` CLI belongs here too. `failed`: a stage failed and the failure was left with nobody holding it — `jsc-hooks:repair` could not be started, or it came back with no PR — so a broken hook stays wired and nothing is going to fix it. `blocked`: `wire-cli.sh` refused with exit 6 under `JSC_READONLY=1`, so no CLI was purged or wired at all. `aborted`: step 1 detected no CLI, so there was nothing to wire and the run stopped on a precondition rather than on an error.
|
||||
- Take `{exit}` from the stage that decided the ending — the `wire-cli.sh`, `smoke` or `scan-hook-errors.sh` code — and otherwise use 0 for `ok` and 1 for every other status. `{detail}` is optional, one line, at most 200 characters: the CLI count per verdict, or the CLI and stage that failed. Never fold the five per-CLI reports into it; those go to the user in step 4.
|
||||
- Reporting never changes the install. `report-status.sh` swallows its own write failures and always exits 0, and an absent file is skipped in silence — the same rule the nine hooks follow, and for the same reason: a reporter that can fail the thing it reports on is worse than no reporter. Done when the one call was made, or the script was absent and this step was skipped without a word.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -16,7 +16,3 @@ This skill is exempt from the version guard and the post-deploy restart gate, be
|
||||
3. Pick the smallest repair that makes the wiring pass, then apply it in the `hooks` repo. When the fix touches wiring behaviour, update `hooks/tools/wire-cli.sh`, `hooks/skills/hooks-install/SKILL.md` and `hooks/README.md` in the same change. Run `tools/wire-cli.sh smoke {cli}` for the affected CLI: exit 0 means the repair holds, exit 4 means it does not — go back to step 2 with the new output, exit 2 means a bad CLI name, so fix the name and rerun. Done when the fix is on disk and smoke exits 0.
|
||||
4. Run `jsc-meta/tools/sync-skill-manifest.sh .` from the repo root. Exit 0 means the README skill list and all three manifests carry the same new version. Exit 1 means a missing path, a missing `JSC-SKILLS` marker or an unreadable manifest — fix the named file and rerun. Exit 2 means a usage error, so pass exactly one path. Any other exit code is an environment fault, never a successful sync: stop and report it. Done when the script exits 0 and the three manifests show the same version.
|
||||
5. Commit, push and open a PR with `jsc-git:pr` against `develop`. When `jsc-git:pr` returns no PR URL, report the repair as applied but unmerged, name the branch that holds it, and hand back the failure reason — never claim a PR exists. Done when a PR URL comes back, or the branch name and the failure reason are both reported.
|
||||
6. Record how the repair ended. Run `tools/report-status.sh skill-end jsc-hooks:repair {status} {exit} "{detail}"` — the script is in this same repo, so it takes the plain `tools/` path, like `tools/wire-cli.sh` in step 3. This step runs on every route out of steps 1 to 5, the ones that stopped early included. What records a skill's start fires when the skill is loaded and cannot see how it ended, so a repair that never writes this line is indistinguishable afterwards from one that was abandoned with a hook still broken.
|
||||
- `{status}` is one of five. `ok`: the fix is on disk, `wire-cli.sh smoke` exits 0 for every affected CLI, `sync-skill-manifest.sh` exits 0 with all three manifests on the same version, and a PR URL against `develop` came back. `degraded`: the repair holds — smoke exits 0 — but the change did not get all the way out, because `jsc-git:pr` returned no PR URL and the fix is sitting on a branch, or `sync-skill-manifest.sh` left the manifests unaligned. `failed`: the hook could not be repaired. Smoke still exits 4 after the diagnosis loop of step 2 came back around, or step 4 hit an environment fault, so the broken wiring is still broken. `aborted`: there was nothing to repair — step 1 found no failure context, no `ERROR_{HASH}` page and no failed `status=` line, so the run stopped on a precondition rather than on a fault. `blocked` does not arise: this skill is exempt from the version guard and the post-deploy restart gate, which are the only two gates that could hold it, and that exemption exists precisely because it is the way back from a broken hook.
|
||||
- Take `{exit}` from whatever decided the ending — the `wire-cli.sh smoke` or `sync-skill-manifest.sh` code — and otherwise use 0 for `ok` and 1 for every other status. `{detail}` is optional, one line, at most 200 characters: the hook and CLI that were repaired, or the branch that holds an unmerged fix. Diagnosis output and diffs never go on this line.
|
||||
- Reporting never changes the repair. `report-status.sh` swallows its own write failures and always exits 0, and an absent file is skipped in silence, so a missing reporter can never be the reason a repaired hook reads as unrepaired. Done when the call was made, or the script was absent and this step was skipped without a word.
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
>
|
||||
> 寫入語意:一列代表一次 hook 異常回報。寫入前先讀回整頁,同一筆異常已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。一律 upsert 附加,禁止整頁覆蓋,也不得改動別人的列。
|
||||
>
|
||||
> 連結寫法:一律寫成 `[{文字}]({絕對網址})`,網址取 `jsc-gitea/tools/gitea.sh wiki-url` 印出的那一個,不自己組路徑。wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常文字或死連結。
|
||||
>
|
||||
> 寫入前驗證:這一列要放進去的連結,先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才把連結寫進那一格。驗不過就只留純文字頁名,那一列照寫,異常紀錄不因為一條連結整份丟掉。驗證走 API,不看網頁狀態碼——私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把還在的頁判成死連結。結束碼 7 是金鑰失效,不算死連結,也不改寫任何既有列。
|
||||
> 連結寫法:指向異常頁的連結一律用 `gitea.sh wiki-url` 產出的絕對網址。跨存取庫的 `[[頁名]]` 解不開,只會留下死連結。
|
||||
|
||||
## 異常清單
|
||||
|
||||
| 時間 | 頁名 | 存取庫名稱 | 觸發 hook | 退出碼 | 摘要 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| {yyyy-MM-dd HH:mm:ss} | [{頁名}]({wiki-url 印出的絕對網址}) | {owner}/{repo} | {hook_name} | {exit_code} | {error_summary} |
|
||||
| {yyyy-MM-dd HH:mm:ss} | [{error title}]({error url}) | {owner}/{repo} | {hook_name} | {exit_code} | {error_summary} |
|
||||
|
||||
+11
-41
@@ -24,13 +24,6 @@
|
||||
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
|
||||
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
||||
#
|
||||
# 連結:
|
||||
# 目錄頁那一列指向異常頁,一律寫成 [{文字}]({連結}),網址取 jsc-gitea 的
|
||||
# gitea.sh wiki-url,不自己組路徑。
|
||||
# 寫入前先把那個網址交給 jsc-gitea 的 link-check.sh,結束碼 0 才把連結寫進那一列。
|
||||
# 驗不過就只留純文字頁名:那一列照寫、異常頁照寫、結束碼照舊。這一段一律不改結束碼,
|
||||
# 本腳本是失敗回報路徑,回報失敗不該再變成一次失敗。
|
||||
#
|
||||
# 結束碼: 0=已寫入異常頁並印出頁名(取得網址就一併印出),或以上列四種安靜降級原因之一
|
||||
# 結束、沒有寫出任何頁也沒有任何輸出——回報失敗不該再變成一次失敗
|
||||
# 2=用法錯誤(缺 --hook 或 --summary)
|
||||
@@ -146,9 +139,8 @@ if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# 目錄頁那一列指向異常頁,連結一律寫成 [{文字}]({連結}),網址取 gitea.sh wiki-url。
|
||||
# wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常
|
||||
# 文字或死連結,巡不到也修不了。
|
||||
# 目錄頁與異常頁分屬兩個存取庫,wiki 的 [[頁名]] 只在同一個存取庫內解得開,跨庫一律解成
|
||||
# 死連結。所以目錄頁指向異常頁的連結一律用絕對網址。
|
||||
# 網址取不到不算失敗:異常頁已經寫成功了,只是這一列少一條連結。這裡把原因記下來走 stderr,
|
||||
# 結束碼照舊——安靜降級仍是 exit 0,回報失敗不該再變成一次失敗。
|
||||
url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null)
|
||||
@@ -169,39 +161,17 @@ emit() { # 異常頁已經寫成功,頁名一定要印;網址取不到就只
|
||||
if [ -n "$url" ]; then printf '%s %s\n' "$page" "$url"; else printf '%s\n' "$page"; fi
|
||||
}
|
||||
|
||||
# 連結先驗證連得到,才寫進目錄頁那一列。沒驗過的連結寫進去,異常頁一樣會在目錄頁長出
|
||||
# 死連結,而目錄頁是別人查問題的入口。驗不過就只留純文字頁名,那一列照寫。
|
||||
link_cell="$page"
|
||||
if [ -n "$url" ]; then
|
||||
# link-check.sh 與 gitea.sh 同一個 tools 目錄,路徑直接由已經解出來的那一支推得,
|
||||
# 不另寫一套搜尋,兩邊才不會一支解到開發版面、一支解到安裝版面。
|
||||
lcs="$(dirname "$gsh")/link-check.sh"
|
||||
if [ ! -f "$lcs" ]; then
|
||||
echo "[jsc] 找不到 $lcs,這一列的連結沒驗過,只留頁名;$page 已建立。" >&2
|
||||
else
|
||||
sh "$lcs" "$url" >/dev/null 2>&1
|
||||
lc_code=$?
|
||||
case "$lc_code" in
|
||||
0) link_cell=$(printf '[%s](%s)' "$page" "$url") ;;
|
||||
# 7 是金鑰失效,不是死連結。金鑰過期時私有存取庫的回應與「頁不存在」分不出來,
|
||||
# 把它當成死連結就會連還在的頁一起判死。
|
||||
7) echo "[jsc] 連結驗證遇上金鑰失效(link-check.sh 回 7),不判成死連結,這一列只留頁名;$page 已建立。" >&2 ;;
|
||||
3) echo "[jsc] GITEA_HOST 未設定(link-check.sh 回 3),連結沒驗過,這一列只留頁名;$page 已建立。" >&2 ;;
|
||||
*) echo "[jsc] 連結驗不過(link-check.sh 結束碼 $lc_code),這一列只留頁名;$page 已建立。" >&2 ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
row=$(printf '| %s | %s | %s | %s | %s | %s |' \
|
||||
"$ts" "$link_cell" "$repo" "$hook" "$code" "$summary")
|
||||
row=$(printf '| %s | [%s](%s) | %s | %s | %s | %s |' \
|
||||
"$ts" "$hook 異常 $ts" "$url" "$repo" "$hook" "$code" "$summary")
|
||||
|
||||
build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫
|
||||
# 範本的示範列整列換成本次這一列,不逐格填。連結那一格已經驗過也組好了,拆成頁名與
|
||||
# 網址兩個佔位再填,會在驗不過的時候留下一個空網址的死連結。
|
||||
ROW="$row" awk '
|
||||
index($0, "| {yyyy-MM-dd HH:mm:ss} |") == 1 { print ENVIRON["ROW"]; next }
|
||||
{ print }
|
||||
' "$ROOT/templates/error-contents.md" > "$tmp_list"
|
||||
fill '{yyyy-MM-dd HH:mm:ss}' "$ts" < "$ROOT/templates/error-contents.md" \
|
||||
| fill '{error url}' "$url" \
|
||||
| fill '{error title}' "$hook 異常 $ts" \
|
||||
| fill '{owner}/{repo}' "$repo" \
|
||||
| fill '{hook_name}' "$hook" \
|
||||
| fill '{exit_code}' "$code" \
|
||||
| fill '{error_summary}' "$summary" > "$tmp_list"
|
||||
}
|
||||
|
||||
if [ -z "$crepo" ]; then
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# report-status.sh — 技能與 hook 的執行狀態事件流。
|
||||
#
|
||||
# 為什麼要有這支:現行 usage/skills.jsonl 只記「被叫用」,欄位是 {ts,cli,session,skill},
|
||||
# 沒有成敗、沒有結束碼。跑完整輪的技能與開場就中止的技能,在紀錄裡長得一模一樣。
|
||||
# hook 成功時更是完全不留紀錄,只有錯誤路徑會寫 wiki,而那條路徑刻意不自動觸發。
|
||||
#
|
||||
# 為什麼不直接寫 wiki:hook 每次提示都跑,網路寫入會拖垮宿主 CLI;失敗的 hook 自我回報
|
||||
# 還會疊出迴圈。所以一律先寫本機事件流,助理巡檢時排空、彙整、寫 MONITOR 頁。
|
||||
#
|
||||
# 用法:
|
||||
# report-status.sh skill-start <名稱>
|
||||
# report-status.sh skill-end <名稱> <status> [結束碼] [detail]
|
||||
# report-status.sh hook-end <名稱> <status> <結束碼> [detail]
|
||||
# report-status.sh drain # 印出上次排空之後的新事件
|
||||
# report-status.sh rotate # 超過上限就輪替,只留一份舊的
|
||||
#
|
||||
# <名稱>: 技能寫 {domain}:{skill},hook 寫 {腳本檔名} 加子命令,例如 sdlc-gate check。
|
||||
# <status>: ok、blocked、failed、degraded、aborted 五選一。
|
||||
# ok 完成條件全部達成
|
||||
# blocked 被閘門或前置條件擋下,沒有做事
|
||||
# failed 做到一半失敗
|
||||
# degraded 做完了但有部分沒達成
|
||||
# aborted 使用者中止,或前提不成立而主動停止
|
||||
#
|
||||
# 規則:
|
||||
# - 三個記錄子命令一律回 0,寫檔失敗也是 0。回報機制自己壞掉,不可以讓被回報的東西
|
||||
# 跟著壞——hook 的結束碼是閘門的判準,被記錄動到就等於閘門行為被記錄改寫。
|
||||
# 參數檢查是例外:那是呼叫端的程式錯誤,寫進去只會汙染事件流,所以先擋下來。
|
||||
# - 本檔不讀 stdin。技能由 Bash 呼叫它,stdin 可能是還沒關閉的管線,讀下去會卡住宿主。
|
||||
# 所有資訊一律走參數。
|
||||
# - 輪替不放在每次寫入。每次提示都寫事件,順手 stat 一次檔案就是每次提示多一次系統呼叫;
|
||||
# 改由巡檢排空之後呼叫 rotate,成本落在本來就週期性執行的地方。
|
||||
#
|
||||
# 結束碼: 0=成功(記錄子命令一律 0) 2=用法錯誤 3=drain 沒有新事件
|
||||
set -eu
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
# lib.sh 在同一個存取庫,不是跨 plugin 依賴。hook 必須自足,事件寫入的函式因此留在 lib.sh,
|
||||
# 這支只是把它包成命令列介面給技能用。
|
||||
STDIN_JSON=""
|
||||
. "$HERE/../hooks/lib.sh"
|
||||
|
||||
EVENTS="$JSC_HOME/usage/events.jsonl"
|
||||
OFFSET="$JSC_HOME/usage/scan-state/events.offset"
|
||||
MAX_BYTES=5242880
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
用法:
|
||||
report-status.sh skill-start <名稱>
|
||||
report-status.sh skill-end <名稱> <status> [結束碼] [detail]
|
||||
report-status.sh hook-end <名稱> <status> <結束碼> [detail]
|
||||
report-status.sh drain
|
||||
report-status.sh rotate
|
||||
|
||||
status: ok、blocked、failed、degraded、aborted
|
||||
結束碼: 0=成功 2=用法錯誤 3=drain 沒有新事件
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
valid_status() {
|
||||
case "$1" in
|
||||
ok|blocked|failed|degraded|aborted) ;;
|
||||
*) echo "[jsc][狀態回報][ERR]:status 須為 ok、blocked、failed、degraded、aborted 五選一,收到「$1」。" >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
valid_exit() {
|
||||
case "$1" in
|
||||
''|*[!0-9]*) echo "[jsc][狀態回報][ERR]:結束碼須為非負整數,收到「$1」。" >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd="${1:-}"; [ -n "$cmd" ] || usage
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
skill-start)
|
||||
name="${1:-}"; [ -n "$name" ] || usage
|
||||
# start 沒有成敗可言,狀態欄固定 ok、結束碼固定 0。判讀靠的是「有沒有配對的 end」:
|
||||
# 有 start 沒 end 就是中止,那正是現行紀錄分不出來的那一種。
|
||||
emit_event skill "$name" start ok 0
|
||||
;;
|
||||
skill-end)
|
||||
name="${1:-}"; status="${2:-}"
|
||||
[ -n "$name" ] && [ -n "$status" ] || usage
|
||||
valid_status "$status"
|
||||
code="${3:-0}"; valid_exit "$code"
|
||||
emit_event skill "$name" end "$status" "$code" "" "${4:-}"
|
||||
;;
|
||||
hook-end)
|
||||
name="${1:-}"; status="${2:-}"; code="${3:-}"
|
||||
[ -n "$name" ] && [ -n "$status" ] && [ -n "$code" ] || usage
|
||||
valid_status "$status"; valid_exit "$code"
|
||||
emit_event hook "$name" end "$status" "$code" "" "${4:-}"
|
||||
;;
|
||||
drain)
|
||||
[ -f "$EVENTS" ] || exit 3
|
||||
size=$(wc -c < "$EVENTS" 2>/dev/null || echo 0)
|
||||
old=0
|
||||
[ -f "$OFFSET" ] && old=$(cat "$OFFSET" 2>/dev/null || echo 0)
|
||||
case "$old" in ''|*[!0-9]*) old=0 ;; esac
|
||||
# 檔案比已存位移還小就是輪替過,從頭讀。不比對 inode:五支 CLI 與容器裡的行程
|
||||
# 看到的 inode 不保證一致,用大小判斷才在每個環境都成立。
|
||||
[ "$size" -lt "$old" ] && old=0
|
||||
[ "$size" -eq "$old" ] && exit 3
|
||||
mkdir -p "$(dirname "$OFFSET")" 2>/dev/null || true
|
||||
# tail -c +N 從第 N 個位元組起(1 起算),所以位移要加一。
|
||||
# 不用 dd bs=1 skip=:那是一個位元組一次系統呼叫,位移到了幾 MB 就是幾百萬次,
|
||||
# 每輪巡檢都排空一次的話會慢到不能用。
|
||||
tail -c "+$((old + 1))" "$EVENTS" 2>/dev/null || true
|
||||
printf '%s' "$size" > "$OFFSET" 2>/dev/null || true
|
||||
;;
|
||||
rotate)
|
||||
[ -f "$EVENTS" ] || exit 0
|
||||
size=$(wc -c < "$EVENTS" 2>/dev/null || echo 0)
|
||||
if [ "$size" -gt "$MAX_BYTES" ]; then
|
||||
mv "$EVENTS" "$EVENTS.1" 2>/dev/null || true
|
||||
: > "$EVENTS" 2>/dev/null || true
|
||||
# 位移歸零:新檔從頭算起,不歸零的話下一次 drain 會跳過開頭那一段。
|
||||
printf '0' > "$OFFSET" 2>/dev/null || true
|
||||
printf '已輪替:%s -> %s.1(原大小 %s 位元組)\n' "$EVENTS" "$EVENTS" "$size"
|
||||
fi
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
+30
-151
@@ -1354,8 +1354,7 @@ if [ "$action" = smoke ]; then
|
||||
# 那一類會讓總數永遠對不上,斷言也就形同虛設。
|
||||
# hook 模式 九支 hook 的每個接線模式各一條。sdlc-gate.sh、comment-scope.sh、
|
||||
# lang-guard.sh 與 write-guard.sh 各有多個模式,所以比 hook 支數多
|
||||
# 模型與階段鎖 sdlc-gate.sh 取模型代號的四條來源判定路徑,加上「不知道能力就擋下」的
|
||||
# 三種情形、check 的 fail-closed,以及階段鎖狀態檔的 CLI 區隔與舊格式相容
|
||||
# 模型來源 sdlc-gate.sh lock 取模型代號的四條來源判定路徑
|
||||
# 工作包 sdlc-gate.sh wp-check skill 的歸屬判定路徑
|
||||
# 重啟閘門 restart-gate.sh 的判定、清除路徑與狀態檔範圍檢查
|
||||
# 寫入閘門 write-guard.sh 三種擋人模式與 release 解除模式的判定路徑
|
||||
@@ -1365,7 +1364,7 @@ if [ "$action" = smoke ]; then
|
||||
# 跨 CLI 貫通 restart-gate.sh 吃五支 CLI 的真實負載,驗判定與輸出形態串得起來
|
||||
# 接線形狀 每支 CLI 要寫出去的內容真的產出來一次,驗結構本身(正反案例各一組)
|
||||
SMOKE_EXPECT_HOOK=17
|
||||
SMOKE_EXPECT_MODEL=12
|
||||
SMOKE_EXPECT_MODEL=4
|
||||
SMOKE_EXPECT_WP=6
|
||||
SMOKE_EXPECT_RS=16
|
||||
SMOKE_EXPECT_WG=21
|
||||
@@ -1425,11 +1424,8 @@ if [ "$action" = smoke ]; then
|
||||
smoke_one sdlc-gate.sh "wp-check prompt"
|
||||
smoke_one sdlc-gate.sh "wp-check skill"
|
||||
|
||||
# 第四個參數是這個案例要用的 CLI 代號,不跟著這一輪接線的 $cli 走:模型偵測鏈已經依 CLI
|
||||
# 分流,transcript 只有 claude 讀得到、codex session 記錄只有 codex 讀得到。拿 $cli 去跑
|
||||
# 每一個案例,換一支 CLI 冒煙就會有一半的案例本來就不該命中,測的東西跟著變。
|
||||
smoke_model_case() { # $1=情境 $2=來源模式 $3=預期來源片段 $4=案例用的 CLI 代號
|
||||
_name="$1"; _mode="$2"; _want="$3"; _mcli="$4"
|
||||
smoke_model_case() { # $1=情境 $2=來源模式 $3=預期來源片段
|
||||
_name="$1"; _mode="$2"; _want="$3"
|
||||
# 先計數再開工:底下建不出暫存目錄那條路徑也會印一條結果行,計數放在後面就會漏掉它。
|
||||
smoke_n_model=$((smoke_n_model + 1))
|
||||
_home=$(mktemp -d 2>/dev/null) || {
|
||||
@@ -1450,11 +1446,11 @@ if [ "$action" = smoke ]; then
|
||||
override) _json='{"session_id":"smoke-model-override"}'; _env="JSC_MODEL=gpt-5.x" ;;
|
||||
esac
|
||||
if [ "$_mode" = override ]; then
|
||||
_out=$(printf '%s' "$_json" | JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$_mcli" JSC_MODEL="gpt-5.x" sh "$HOOKS/sdlc-gate.sh" lock plan 2>&1); _rc=$?
|
||||
_out=$(printf '%s' "$_json" | JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$cli" JSC_MODEL="gpt-5.x" sh "$HOOKS/sdlc-gate.sh" lock plan 2>&1); _rc=$?
|
||||
else
|
||||
_out=$(printf '%s' "$_json" | JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$_mcli" sh "$HOOKS/sdlc-gate.sh" lock plan 2>&1); _rc=$?
|
||||
_out=$(printf '%s' "$_json" | JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$cli" sh "$HOOKS/sdlc-gate.sh" lock plan 2>&1); _rc=$?
|
||||
fi
|
||||
_rep=$(JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$_mcli" \
|
||||
_rep=$(JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$cli" \
|
||||
sh "$HOOKS/sdlc-gate.sh" report </dev/null 2>/dev/null)
|
||||
if [ "$_rc" -eq 0 ] && printf '%s\n%s' "$_out" "$_rep" | grep -qF "$_want"; then
|
||||
printf '[jsc] sdlc-gate.sh model(%s):exit 0,來源含 %s,與預期相同。\n' "$_name" "$_want" >> "$smoke_out"
|
||||
@@ -1465,117 +1461,10 @@ if [ "$action" = smoke ]; then
|
||||
fi
|
||||
rm -rf "$_home"
|
||||
}
|
||||
smoke_model_case "transcript 實查" transcript "transcript:" claude
|
||||
smoke_model_case "Codex session 記錄" codex "codex-session:" codex
|
||||
smoke_model_case "stdin model 欄位" stdin "hook-stdin" claude
|
||||
# 人工覆寫對每一支 CLI 都保留,所以這一條照這一輪接線的 CLI 跑。
|
||||
smoke_model_case "JSC_MODEL 人工覆寫" override "人工覆寫:JSC_MODEL" "$cli"
|
||||
|
||||
# 「不知道能力就擋下」的三種情形。判定結果與訊息一起驗:只比結束碼的話,訊息漏掉逃生門
|
||||
# 一樣是綠燈,而被擋住的人就真的出不來了——那才是這道 fail-closed 真正的風險。
|
||||
smoke_model_deny_case() { # $1=情境 $2=案例用的 CLI 代號 $3=JSC_MODEL 值 $4=預期訊息片段
|
||||
smoke_n_model=$((smoke_n_model + 1))
|
||||
_home=$(mktemp -d 2>/dev/null) || {
|
||||
smoke_fails=$((smoke_fails + 1))
|
||||
printf '[jsc] sdlc-gate.sh model(%s):建不出暫存目錄,擋人判定沒驗到。\n' "$1" >> "$smoke_out"
|
||||
return
|
||||
}
|
||||
mkdir -p "$_home/sessions" "$_home/codex/sessions/2026/08/28" 2>/dev/null
|
||||
printf 'stage\tplan\treasoning-max\nmodel\tgpt-5.x\treasoning-max,reasoning-high,coding\n' > "$_home/model-tags.tsv"
|
||||
# codex 的 session 記錄照樣擺著:claude 情境要驗的正是「明明有這一份,也不去讀」。
|
||||
printf '{"type":"turn_context","payload":{"model":"gpt-5.x"}}\n' \
|
||||
> "$_home/codex/sessions/2026/08/28/rollout-2026-08-28T00-00-00-smoke-deny.jsonl"
|
||||
# claude 的三個環境特徵一併清空:JSC_CLI 是空字串時 cli_name() 會退去看它們,只要有一個
|
||||
# 留著繼承來的值,「判不出 CLI」那一條就會被測成 claude。三個要一起清,漏一個等於沒清。
|
||||
_out=$(printf '{"session_id":"smoke-model-deny"}' \
|
||||
| env JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$2" \
|
||||
CLAUDE_PLUGIN_ROOT="" CLAUDECODE="" CLAUDE_CODE_SESSION_ID="" \
|
||||
JSC_MODEL="$3" sh "$HOOKS/sdlc-gate.sh" lock plan 2>&1); _rc=$?
|
||||
if [ "$_rc" -eq 1 ] && printf '%s' "$_out" | grep -qF "$4" \
|
||||
&& printf '%s' "$_out" | grep -qF 'JSC_MODEL={模型 id}'; then
|
||||
printf '[jsc] sdlc-gate.sh model(%s):exit 1,擋下且訊息帶得出逃生門,與預期相同。\n' "$1" >> "$smoke_out"
|
||||
else
|
||||
smoke_fails=$((smoke_fails + 1))
|
||||
printf '[jsc] sdlc-gate.sh model(%s):exit %s,預期 exit 1 且訊息含「%s」與逃生門,能力閘門壞了:%s\n' \
|
||||
"$1" "$_rc" "$4" "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
|
||||
fi
|
||||
rm -rf "$_home"
|
||||
}
|
||||
smoke_model_deny_case "claude 情境不去讀 codex 紀錄" claude "" "判定不出目前實際使用的模型"
|
||||
smoke_model_deny_case "判不出是哪一支 CLI" "" "" "判不出是哪一支 CLI"
|
||||
smoke_model_deny_case "模型不在能力標籤表上" claude "mystery-model-not-listed" "不在能力標籤表上"
|
||||
|
||||
# check 的 fail-closed:階段鎖在、判不出模型,要 exit 2 擋下該輪提示並帶出逃生門。
|
||||
# 這一條原本是「只提醒不擋」,改成擋下之後逃生門就是唯一的出路,所以訊息一起驗。
|
||||
smoke_n_model=$((smoke_n_model + 1))
|
||||
if ck_home=$(mktemp -d 2>/dev/null) && mkdir -p "$ck_home/sessions" 2>/dev/null; then
|
||||
printf 'stage\tplan\treasoning-max\nmodel\tgpt-5.x\treasoning-max,coding\n' > "$ck_home/model-tags.tsv"
|
||||
printf 'plan\treasoning-max\tgpt-5.x\tsmoke\n' > "$ck_home/sessions/claude-smoke-check.stage"
|
||||
ck_out=$(printf '{"session_id":"smoke-check"}' \
|
||||
| env JSC_HOME="$ck_home" JSC_CLI=claude CLAUDE_PLUGIN_ROOT="" JSC_MODEL="" \
|
||||
JSC_SESSION_ID=smoke-check sh "$HOOKS/sdlc-gate.sh" check 2>&1); ck_rc=$?
|
||||
if [ "$ck_rc" -eq 2 ] && printf '%s' "$ck_out" | grep -qF 'JSC_MODEL={模型 id}' \
|
||||
&& printf '%s' "$ck_out" | grep -qF 'unlock'; then
|
||||
printf '[jsc] sdlc-gate.sh check(判不出模型時擋下):exit 2,兩條逃生門都印出來,與預期相同。\n' >> "$smoke_out"
|
||||
else
|
||||
smoke_fails=$((smoke_fails + 1))
|
||||
printf '[jsc] sdlc-gate.sh check(判不出模型時擋下):exit %s,預期 exit 2 且訊息含 JSC_MODEL 與 unlock 兩條逃生門,輸出:%s\n' \
|
||||
"$ck_rc" "$(printf '%s' "$ck_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
|
||||
fi
|
||||
rm -rf "$ck_home"
|
||||
else
|
||||
smoke_fails=$((smoke_fails + 1))
|
||||
printf '[jsc] sdlc-gate.sh check(判不出模型時擋下):建不出暫存目錄,沒驗到。\n' >> "$smoke_out"
|
||||
fi
|
||||
|
||||
# 階段鎖狀態檔的 CLI 區隔:兩支 CLI 各上一次鎖,兩份狀態檔要分得開,解鎖也只解自己那一份。
|
||||
# session id 判不出時會退回 default,不分 CLI 就會共用同一支檔案,一支上的鎖擋到另一支。
|
||||
smoke_st_case() { # $1=情境 $2=條件成立就通過(已求值的結果字串 ok 或其他)
|
||||
smoke_n_model=$((smoke_n_model + 1))
|
||||
if [ "$2" = ok ]; then
|
||||
printf '[jsc] sdlc-gate.sh 階段鎖(%s):與預期相同。\n' "$1" >> "$smoke_out"
|
||||
else
|
||||
smoke_fails=$((smoke_fails + 1))
|
||||
printf '[jsc] sdlc-gate.sh 階段鎖(%s):不如預期,狀態檔的 CLI 區隔壞了。\n' "$1" >> "$smoke_out"
|
||||
fi
|
||||
}
|
||||
if st_home=$(mktemp -d 2>/dev/null) && mkdir -p "$st_home/sessions" 2>/dev/null; then
|
||||
printf 'stage\tplan\treasoning-max\nstage\timplement\tcoding\nmodel\tgpt-5.x\treasoning-max,coding\n' \
|
||||
> "$st_home/model-tags.tsv"
|
||||
# 兩支各上一次鎖,而且刻意上不同的階段:檔名分開之外,內容也要各記各的。
|
||||
# 不寫成迴圈是為了不動到腳本的位置參數——這一段跑在函式外,set -- 會蓋掉整支腳本的 $@。
|
||||
env JSC_HOME="$st_home" JSC_CLI=claude CLAUDE_PLUGIN_ROOT="" JSC_MODEL=gpt-5.x \
|
||||
JSC_SESSION_ID=smoke-stage sh "$HOOKS/sdlc-gate.sh" lock plan </dev/null >/dev/null 2>&1
|
||||
env JSC_HOME="$st_home" JSC_CLI=codex CLAUDE_PLUGIN_ROOT="" JSC_MODEL=gpt-5.x \
|
||||
JSC_SESSION_ID=smoke-stage sh "$HOOKS/sdlc-gate.sh" lock implement </dev/null >/dev/null 2>&1
|
||||
st_a="$st_home/sessions/claude-smoke-stage.stage"
|
||||
st_b="$st_home/sessions/codex-smoke-stage.stage"
|
||||
st_r=bad; { [ -f "$st_a" ] && [ -f "$st_b" ]; } && st_r=ok
|
||||
smoke_st_case "兩支 CLI 各寫出自己那一份" "$st_r"
|
||||
st_r=bad
|
||||
[ "$(cut -f1 "$st_a" 2>/dev/null)" = plan ] \
|
||||
&& [ "$(cut -f1 "$st_b" 2>/dev/null)" = implement ] && st_r=ok
|
||||
smoke_st_case "兩份互不覆蓋,各自記自己的階段" "$st_r"
|
||||
env JSC_HOME="$st_home" JSC_CLI=claude CLAUDE_PLUGIN_ROOT="" \
|
||||
JSC_SESSION_ID=smoke-stage sh "$HOOKS/sdlc-gate.sh" unlock </dev/null >/dev/null 2>&1
|
||||
st_r=bad; { [ ! -f "$st_a" ] && [ -f "$st_b" ]; } && st_r=ok
|
||||
smoke_st_case "解鎖只解自己那一份" "$st_r"
|
||||
# 舊格式 sessions/{sid}.stage 的往後相容:新路徑沒有時要讀得到它,unlock 也要清得掉。
|
||||
rm -f "$st_b"
|
||||
printf 'plan\treasoning-max\tgpt-5.x\tsmoke\n' > "$st_home/sessions/smoke-legacy.stage"
|
||||
st_out=$(env JSC_HOME="$st_home" JSC_CLI=claude CLAUDE_PLUGIN_ROOT="" JSC_MODEL=gpt-5.x \
|
||||
JSC_SESSION_ID=smoke-legacy sh "$HOOKS/sdlc-gate.sh" report </dev/null 2>/dev/null)
|
||||
st_r=bad; printf '%s' "$st_out" | grep -qF 'smoke-legacy plan reasoning-max' && st_r=ok
|
||||
smoke_st_case "舊格式狀態檔仍讀得到" "$st_r"
|
||||
rm -rf "$st_home"
|
||||
else
|
||||
smoke_fails=$((smoke_fails + 4))
|
||||
printf '[jsc] sdlc-gate.sh 階段鎖:建不出暫存目錄,狀態檔的 CLI 區隔沒驗到。\n' >> "$smoke_out"
|
||||
printf '[jsc] sdlc-gate.sh 階段鎖:同上,兩份互不覆蓋沒驗到。\n' >> "$smoke_out"
|
||||
printf '[jsc] sdlc-gate.sh 階段鎖:同上,解鎖範圍沒驗到。\n' >> "$smoke_out"
|
||||
printf '[jsc] sdlc-gate.sh 階段鎖:同上,舊格式相容沒驗到。\n' >> "$smoke_out"
|
||||
smoke_n_model=$((smoke_n_model + 4))
|
||||
fi
|
||||
smoke_model_case "transcript 實查" transcript "transcript:"
|
||||
smoke_model_case "Codex session 記錄" codex "codex-session:"
|
||||
smoke_model_case "stdin model 欄位" stdin "hook-stdin"
|
||||
smoke_model_case "JSC_MODEL 人工覆寫" override "人工覆寫:JSC_MODEL"
|
||||
|
||||
# 工作包歸屬判定(wp-check 的第二段):上面兩支清空技能名,只走得到「沒有未結清 PR」與
|
||||
# 「取不到技能名」兩條捷徑,歸屬比對整段都沒跑到。這裡自備一份暫時的 $JSC_HOME 狀態檔,
|
||||
@@ -1761,8 +1650,6 @@ if [ "$action" = smoke ]; then
|
||||
wg_state="$wg_home/sessions/smoke-write.stage"
|
||||
wg_last="$wg_home/sessions/smoke-write.lastskill"
|
||||
# stage:階段鎖狀態檔的格式沿用 sdlc-gate.sh 那一份,這裡照樣寫三欄。
|
||||
# 檔名刻意用舊格式的 sessions/{sid}.stage,順帶把 write-guard 這一側的往後相容也蓋到:
|
||||
# 新路徑不存在時要退回舊路徑讀得出來,否則既有的鎖會在換路徑那一刻整批失效。
|
||||
smoke_wg_case "沒有階段鎖" stage 0
|
||||
printf 'plan\treasoning-max\tsmoke\n' > "$wg_state"
|
||||
smoke_wg_case "階段鎖 plan" stage 2
|
||||
@@ -2174,7 +2061,7 @@ if [ "$action" = smoke ]; then
|
||||
[ "$smoke_n_hook" = "$SMOKE_EXPECT_HOOK" ] \
|
||||
|| smoke_mismatch="${smoke_mismatch}hook 模式 $smoke_n_hook 條(預期 $SMOKE_EXPECT_HOOK);"
|
||||
[ "$smoke_n_model" = "$SMOKE_EXPECT_MODEL" ] \
|
||||
|| smoke_mismatch="${smoke_mismatch}模型與階段鎖 $smoke_n_model 條(預期 $SMOKE_EXPECT_MODEL);"
|
||||
|| smoke_mismatch="${smoke_mismatch}模型來源 $smoke_n_model 條(預期 $SMOKE_EXPECT_MODEL);"
|
||||
[ "$smoke_n_wp" = "$SMOKE_EXPECT_WP" ] \
|
||||
|| smoke_mismatch="${smoke_mismatch}工作包 $smoke_n_wp 條(預期 $SMOKE_EXPECT_WP);"
|
||||
[ "$smoke_n_rs" = "$SMOKE_EXPECT_RS" ] \
|
||||
@@ -2194,7 +2081,7 @@ if [ "$action" = smoke ]; then
|
||||
[ "$smoke_lines" = "$smoke_total" ] \
|
||||
|| smoke_mismatch="${smoke_mismatch}結果行數 $smoke_lines 行(預期 $smoke_total);"
|
||||
|
||||
smoke_breakdown="hook 模式 $SMOKE_EXPECT_HOOK 條、模型與階段鎖 $SMOKE_EXPECT_MODEL 條、工作包 $SMOKE_EXPECT_WP 條、重啟閘門 $SMOKE_EXPECT_RS 條、寫入閘門 $SMOKE_EXPECT_WG 條、相依版本 $SMOKE_EXPECT_VG 條、技能名解析 $SMOKE_EXPECT_SN 條、阻擋形態 $SMOKE_EXPECT_DN 條、跨 CLI 貫通 $SMOKE_EXPECT_CX 條、接線形狀 $SMOKE_EXPECT_SH 條"
|
||||
smoke_breakdown="hook 模式 $SMOKE_EXPECT_HOOK 條、模型來源 $SMOKE_EXPECT_MODEL 條、工作包 $SMOKE_EXPECT_WP 條、重啟閘門 $SMOKE_EXPECT_RS 條、寫入閘門 $SMOKE_EXPECT_WG 條、相依版本 $SMOKE_EXPECT_VG 條、技能名解析 $SMOKE_EXPECT_SN 條、阻擋形態 $SMOKE_EXPECT_DN 條、跨 CLI 貫通 $SMOKE_EXPECT_CX 條、接線形狀 $SMOKE_EXPECT_SH 條"
|
||||
if [ -n "$smoke_mismatch" ]; then
|
||||
printf 'status=failed reason=%s\n' "冒煙結果行數與預期不符:${smoke_mismatch}判定路徑有增減時要一併改腳本裡的預期條數"
|
||||
printf 'lines\t%s\n' "$smoke_lines"
|
||||
@@ -2205,7 +2092,7 @@ if [ "$action" = smoke ]; then
|
||||
fi
|
||||
|
||||
if [ "$smoke_fails" -eq 0 ]; then
|
||||
printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,模型與階段鎖、工作包歸屬、部署後重啟閘門、寫入提交閘門、相依版本檢查、五支 CLI 的技能名解析、四種阻擋形態、跨 CLI 貫通與各 CLI 的接線形狀的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤"
|
||||
printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,模型來源、工作包歸屬、部署後重啟閘門、寫入提交閘門、相依版本檢查、五支 CLI 的技能名解析、四種阻擋形態、跨 CLI 貫通與各 CLI 的接線形狀的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤"
|
||||
printf 'lines\t%s\n' "$smoke_lines"
|
||||
cat "$smoke_out"; rm -f "$smoke_out"; exit 0
|
||||
fi
|
||||
@@ -2302,30 +2189,22 @@ if [ "$action" = status ]; then
|
||||
if [ -n "$claude_root" ] && [ -f "$claude_hooks" ]; then st_item hooks.json "$claude_hooks" present
|
||||
else st_item hooks.json "${claude_hooks:-$HOME/.claude/plugins/installed_plugins.json}" missing; fi
|
||||
# 九支 hook 全靠這一個檔宣告,只看檔案在不在會漏掉「檔在、某支沒接進去」。
|
||||
# 每一支都列一項,一支都不省。reason 那行講的是「全部九支」,列舉卻只挑幾支的話,
|
||||
# 拿這份輸出驗收接線的人會把沒列到的當成沒接——模型能力鎖就是這樣被誤判成沒接線的。
|
||||
for _h in comment-scope lang-guard restart-gate skill-usage version-guard; do
|
||||
if [ -f "$claude_hooks" ] && grep -qF "$_h.sh" "$claude_hooks" 2>/dev/null
|
||||
then st_item "$_h" "$claude_hooks" present
|
||||
else st_item "$_h" "$claude_hooks" missing; fi
|
||||
done
|
||||
# 一支腳本接在多個接線點時,每個點各自是一件事,只驗腳本名會漏掉少接的那一個。
|
||||
# 三支這種腳本的接線點對照(左邊是項目名,右邊是 hooks.json 裡的子命令):
|
||||
# write-guard stage、review、commit
|
||||
# sdlc-gate check(階段模型鎖)、wp-check prompt、wp-check skill(工作包歸屬)
|
||||
# session-timer start、mark
|
||||
for _p in "write-guard-stage write-guard.sh\\\" stage" \
|
||||
"write-guard-review write-guard.sh\\\" review" \
|
||||
"write-guard-commit write-guard.sh\\\" commit" \
|
||||
"sdlc-gate-check sdlc-gate.sh\\\" check" \
|
||||
"sdlc-gate-wp-prompt sdlc-gate.sh\\\" wp-check prompt" \
|
||||
"sdlc-gate-wp-skill sdlc-gate.sh\\\" wp-check skill" \
|
||||
"session-timer-start session-timer.sh\\\" start" \
|
||||
"session-timer-mark session-timer.sh\\\" mark"; do
|
||||
_name=${_p%% *}; _pat=${_p#* }
|
||||
if [ -f "$claude_hooks" ] && grep -qF "$_pat" "$claude_hooks" 2>/dev/null
|
||||
then st_item "$_name" "$claude_hooks" present
|
||||
else st_item "$_name" "$claude_hooks" missing; fi
|
||||
# 後來才加進來的 comment-scope.sh、lang-guard.sh、restart-gate.sh 與 write-guard.sh
|
||||
# 是最可能漏的四支,所以各列一項。
|
||||
if [ -f "$claude_hooks" ] && grep -qF 'comment-scope.sh' "$claude_hooks" 2>/dev/null
|
||||
then st_item comment-scope "$claude_hooks" present
|
||||
else st_item comment-scope "$claude_hooks" missing; fi
|
||||
if [ -f "$claude_hooks" ] && grep -qF 'lang-guard.sh' "$claude_hooks" 2>/dev/null
|
||||
then st_item lang-guard "$claude_hooks" present
|
||||
else st_item lang-guard "$claude_hooks" missing; fi
|
||||
if [ -f "$claude_hooks" ] && grep -qF 'restart-gate.sh' "$claude_hooks" 2>/dev/null
|
||||
then st_item restart-gate "$claude_hooks" present
|
||||
else st_item restart-gate "$claude_hooks" missing; fi
|
||||
# write-guard.sh 接在兩個 matcher 上,三種模式各自是一件事,只驗腳本名會漏掉少接的那一個
|
||||
for _m in stage review commit; do
|
||||
if [ -f "$claude_hooks" ] && grep -qF "write-guard.sh\\\" $_m" "$claude_hooks" 2>/dev/null
|
||||
then st_item "write-guard-$_m" "$claude_hooks" present
|
||||
else st_item "write-guard-$_m" "$claude_hooks" missing; fi
|
||||
done ;;
|
||||
|
||||
codex)
|
||||
|
||||
Reference in New Issue
Block a user