Merge pull request 'develop-to-master' (#17) from develop into master

Reviewed-on: #17
Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #17.
This commit is contained in:
2026-08-25 09:03:57 +00:00
16 changed files with 632 additions and 246 deletions
+3 -3
View File
@@ -43,7 +43,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
},
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
},
{
"name": "jsc-log",
@@ -75,7 +75,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
},
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
},
{
"name": "jsc-sdlc",
@@ -83,7 +83,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
},
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
}
]
}
+3 -3
View File
@@ -43,7 +43,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
},
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
},
{
"name": "jsc-log",
@@ -75,7 +75,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
},
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
},
{
"name": "jsc-sdlc",
@@ -83,7 +83,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
},
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
}
]
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "jsc-hooks",
"version": "0.0.9",
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖",
"version": "0.1.5",
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
"skills": "./skills",
"author": {
"name": "JSC"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "jsc-hooks",
"version": "0.0.9",
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖",
"version": "0.1.5",
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
"skills": "./skills"
}
+1 -1
View File
@@ -1,6 +1,6 @@
# jsc-hooks — 給 AI 助理的指引
本 repo 是 jsc 技能組的 `hooks` domain(跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖),可同時被 Claude Code / Codex / Copilot / Antigravity / Kiro 使用。
本 repo 是 jsc 技能組的 `hooks` domain(跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查),可同時被 Claude Code / Codex / Copilot / Antigravity / Kiro 使用。
## 規則
+21 -9
View File
@@ -23,29 +23,35 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
| 腳本 | 事件 | 作用 |
| --- | --- | --- |
| `hooks/ste100-guard.sh` | UserPromptSubmit | 注入 STE100 繁體中文輸出規則(hook > prompt 強制層) |
| `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖;`report` 子指令供 `jsc-log:worklog` 取花費時間 |
| `hooks/version-guard.sh` | PreToolUse(Skill) | 技能使用前的版本前置檢查:本機**實際載入**版本落後遠端發佈版本就以 exit 2 擋下該次呼叫並提示更新指令。只擋落後(超前放行,開發技能組時本機本來就會超前);遠端查不到一律擋(fail-closed),逃生門 `JSC_VERSION_GUARD=off`。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-cli:models`、`jsc-meta:*` |
| `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間 |
| `hooks/version-guard.sh` | PreToolUse(Skill) | 技能使用前的版本前置檢查:本機**實際載入**版本落後遠端發佈版本就以 exit 2 擋下該次呼叫並提示更新指令(更新指令依當前 CLI 給)。只擋落後這一種情況:超前放行(開發技能組時本機本來就會超前),讀不到本機版本、推導不出站台、查不到遠端版本也一律放行。逃生門 `JSC_VERSION_GUARD=off`。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-cli:models`、`jsc-meta:*` |
| `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 |
| `hooks/sdlc-gate.sh` | UserPromptSubmit | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從 transcript 讀出實際模型 id 比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`unlock` 為逃生門 |
Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。
Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。
> 覆蓋範圍要據實看待:只有 claude 同時有 PreToolUse 與 UserPromptSubmit,五支 hook 全接得上,回報 `wired`。codex、copilot、antigravity、kiro 都沒有 pre-tool hook,接不上 `version-guard.sh` 的版本前置檢查,SDLC 模型鎖也只剩技能步驟檢查,這四個 CLI 一律回報 `degraded`,靠 `/jsc-cli:deploy` 定期更新。codex 另外沒有工作階段開始事件,計時改由 `tools/jsc-wrap.sh` 的 `codex` 別名在啟動當下開始;沒走別名啟動時,時間從第一輪回應算起。
> `version-guard.sh report` 是非 hook 的子指令:印出每個已安裝 jsc plugin 的
> 「{domain} {本機} {遠端} {落後|最新|超前|查詢失敗}」,最後一行 `behind {落後個數}`。
> 本機沒有 Claude 的 plugin 註冊檔時改印 `noregistry {路徑}` 再接 `behind 0`,
> 代表這台機器無法做版本檢查,跟「全部最新」是兩件事。查遠端版本走與 hook 同一份快取
> 與同一個 `JSC_VERSION_TTL`,一次部署不會為每個 domain 各打一輪網路。
> `jsc-cli:deploy` 用它決定要不要把「更新」設成推薦選項。
## 工具
| 腳本 | 用途 |
| --- | --- |
| `tools/jsc-wrap.sh` | 無 hook 系統 CLI 的包裝啟動器:匯出 `JSC_CLI`、`JSC_SESSION_ID`,前後接 `session-timer.sh`,結束時自動跑 `scan-logs.sh` 回填 |
| `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/wire-cli.sh` | 單一 CLI 的接線流程:`{cli}` 對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 `<!-- jsc-hooks -->`(或 `# jsc-hooks`)標記整段取代,重跑不重複;以 `status=wired\|degraded\|skipped` 回報結果 |
| `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` 回報結果 |
## 失敗回報範本
這兩個模板給外層的 hook 失敗回報流程使用,不改動現有 hook 行為。
失敗時若要寫入 wiki,套用這兩個檔案即可。
這兩個模板是失敗回報頁的文案來源,由 `tools/report-error.sh` 填欄位後寫進 wiki。
用法:`tools/report-error.sh --hook {名稱} --exit {碼} --summary {摘要}`,錯誤輸出摘要走標準輸入。
| 範本 | 用途 |
| --- | --- |
@@ -60,7 +66,11 @@ Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技
### `hooks-install`
把四支 hook 接線到所有已安裝的 CLI:偵測 CLI 後,逐一呼叫 `tools/wire-cli.sh {cli}` 完成接線(claude 由 `hooks.json` 自動接線,無需寫入)。copilot、antigravity 由該腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍追加到各自的規則檔(以 `<!-- jsc-hooks -->` 標記整段取代,不重複追加)。codex、kiro 的 SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 SDLC 技能直接呼叫 `sdlc-gate.sh lock` 寫入。腳本以 `status=wired|degraded|skipped` 回報結果,供技能對照 verify 表。
把五支 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` 技能接手。
### `repair`
接手 `hooks-install` 或 `report-error.sh` 留下的失敗:讀 `ERROR_{HASH}` 與相關檔案後,把診斷拆給安裝中的其他 AI agent CLI 當 sub agent,彙整建議修正、實作 hooks repo 的修補、同步 manifest,最後以 `develop` 為基底開 PR。只有在真的沒有可用 CLI 時,才退回主 agent 自己判讀。
<!-- JSC-SKILLS:END -->
@@ -69,9 +79,11 @@ Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技
| 變數 | 用途 | 未設定時 |
| --- | --- | --- |
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
| `JSC_WIKI_REPO_ERROR` | `ERROR_CONTENTS`、`ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO` | 未逐類設定時的共用 wiki `{owner}/{repo}` | `tools/report-error.sh` 安靜降級,不寫 wiki |
| `JSC_VERSION_GUARD` | 設 `off` 完全略過版本前置檢查(離線工作用) | 啟用檢查 |
| `JSC_VERSION_TTL` | 遠端版本查詢的快取秒數 | 預設 600 |
| `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供 | 安靜降級 |
| `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` / `JSC_TOOL_NAME` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供,代替 stdin JSON 的 `session_id`、`skill`、`tool_name`(`version-guard.sh` 也收沒有前綴的 `SKILL`、`TOOL_NAME`) | 安靜降級 |
| `JSC_MODEL` | 非 Claude CLI 的目前模型,供 `sdlc-gate.sh` 比對;優先序在 transcript 實際值與 stdin `model` 之後 | 改讀 `~/.claude/settings.json`,再不行就安靜降級 |
## 相關 domain
+36
View File
@@ -8,6 +8,10 @@
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
mkdir -p "$JSC_HOME/sessions" "$JSC_HOME/usage" 2>/dev/null || true
# 呼叫端腳本所在目錄。source 不會改變 $0,所以這裡取到的是 hooks/ 或 tools/。
JSC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
JSC_SCRIPT_DIR="${JSC_SCRIPT_DIR:-.}"
# 讀完 stdin(可能為空;非阻塞宿主)
read_stdin() {
if [ -t 0 ]; then STDIN_JSON=""; else STDIN_JSON=$(cat 2>/dev/null || true); fi
@@ -47,5 +51,37 @@ cli_name() {
else printf 'unknown'; fi
}
# 找出 jsc-gitea 的 tools/gitea.sh 絕對路徑。所有 gitea 操作一律經由它(技能準則),
# 不可自行拼 API 呼叫:token 取用與 tea 金鑰退回都寫在那支腳本裡。
# 找不到就回傳 1,由呼叫端安靜降級(hook 一律 exit 0,不中斷宿主 CLI)。
jsc_gitea_sh() {
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
fi
_root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}"
# 開發用的並排存取庫版面:{workspace}/hooks 旁邊就是 {workspace}/gitea
for _c in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
done
# 已安裝版面:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版)
_c=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \
"$_root"/../../gitea/*/tools/gitea.sh \
"$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \
| sort | tail -n1)
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
_c=$(command -v gitea.sh 2>/dev/null || true)
[ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; }
return 1
}
# 每個 CLI 代號對應的實際執行檔(antigravity 是 agy、kiro 是 kiro-cli,其餘同名)
cli_bin() { # $1=CLI 代號
case "$1" in
antigravity) printf 'agy' ;;
kiro) printf 'kiro-cli' ;;
*) printf '%s' "$1" ;;
esac
}
now_epoch() { date +%s; }
now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
+10 -4
View File
@@ -35,15 +35,21 @@ stage_tags() { # $1=階段
awk -F'\t' -v s="$1" '$1 == "stage" && $2 == s { print $3; exit }' "$TAGS_TSV"
}
# 某模型的能力標籤。模型鍵與實際 id 雙向包含即視為同一家族
#(例:表列 claude-haiku-4-5 對得上 claude-haiku-4-5-20251001);多筆命中取最長鍵。
# 某模型的能力標籤。查法:先找完全相同的鍵,沒有才退回「表列鍵是實際 id 的前綴」
# 之中最長的一筆(例:表列 claude-haiku-4-5 對得上 claude-haiku-4-5-20251001)。
# 只認前綴這個方向。反向包含(實際 id 是表列鍵的前綴)會讓 gpt-5.x 命中更長的
# gpt-5.x-mini,最長鍵勝出就把 mini 的標籤發給 gpt-5.x,把夠格的模型擋掉。
model_tags() { # $1=模型 id
[ -s "$TAGS_TSV" ] || return 0
awk -F'\t' -v m="$1" '
$1 == "model" && (index(m, $2) > 0 || index($2, m) > 0) {
$1 == "model" && $2 == m { exact = $3 }
$1 == "model" && $2 != m && substr(m, 1, length($2)) == $2 {
if (length($2) > best_len) { best_len = length($2); best = $3 }
}
END { if (best != "") print best }
END {
if (exact != "") print exact
else if (best != "") print best
}
' "$TAGS_TSV"
}
+11 -1
View File
@@ -1,9 +1,16 @@
#!/usr/bin/env sh
# session-timer.sh — 記錄工作階段花費時間(供 jsc-log:worklog 取用)。
# 用法:
# session-timer.sh start # SessionStart:記錄起始時間
# session-timer.sh start # SessionStart:記錄起始時間(已有紀錄就不動)
# session-timer.sh restart # SessionStart:一律覆寫起始時間
# session-timer.sh mark # Stop/SessionEnd:更新最後活動時間
# session-timer.sh report [sid] # 印出 {sid} {seconds};無紀錄印 0
#
# start 與 restart 的差別在「同一個 session id 會不會重複開始」:
# start 給 Claude 這種每個工作階段都有自己 session id 的 CLI。續接同一階段時
# SessionStart 會再觸發一次,覆寫起始時間會讓花費時間歸零。
# restart 給接不到 session id 的 CLI(kiro)。那些 CLI 的紀錄共用 default,
# 不覆寫就會把上一個工作階段的起始時間算進來,花費時間虛胖。
HERE=$(dirname "$0"); . "$HERE/lib.sh"
read_stdin
sid=$(session_id)
@@ -11,6 +18,9 @@ case "${1:-mark}" in
start)
f="$JSC_HOME/sessions/$sid.start"
[ -f "$f" ] || now_epoch > "$f" ;;
restart)
now_epoch > "$JSC_HOME/sessions/$sid.start"
rm -f "$JSC_HOME/sessions/$sid.end" ;;
mark)
now_epoch > "$JSC_HOME/sessions/$sid.end" ;;
report)
Regular → Executable
+180 -158
View File
@@ -3,15 +3,22 @@
#
# 本機版本落後遠端發佈版本時擋下該次技能呼叫,並提示更新指令。
#
# 輸入:stdin JSON(Claude 格式)或環境變數,兩者都收。
# 工具名 JSC_TOOL_NAME、TOOL_NAME、stdin 的 tool_name
# 技能名 JSC_SKILL、SKILL、stdin 的 skill
# 兩邊都拿不到就安靜降級 exit 0。
#
# 判準與取值:
# - 比對對象是「遠端發佈版本」與「本機**實際載入**的版本」。
# 實際載入版本要從 installed_plugins.json 的 installPath 讀該版目錄下的
# plugin.json,不能只看註冊在 installed_plugins.json 的版本欄位——那兩者
# 可能不同,只看註冊值會放過真正被載入的舊版。
# 實際載入版本只認 installed_plugins.json 的 installPath 底下那份 plugin.json,
# 不看註冊在 installed_plugins.json 的版本欄位——那兩者可能不同,註冊值比較新時
# 會放過真正被載入的舊版。讀不到那份檔案就當查不到,安靜放行。
# - 只擋落後。本機版本等於或超前遠端一律放行:開發技能組時本機本來就會
# 超前 master,擋下去會讓維護者自己動不了。
# - 遠端版本查不到(離線、站台維護、repo 改名)一律**擋**(fail-closed),
# 避免「查不到就當作沒事」而讓落後版本靜靜跑下去。逃生門見下。
# 超前預設分支,擋下去會讓維護者自己動不了。
# - **只有「本機落後遠端」會擋**。查不到資料一律放行(exit 0):本機版本、
# Gitea 站台、遠端版本全部來自 Claude 的 plugin 檔案與 Gitea API,沒裝
# Claude 或離線的機器一筆都讀不到。那種情況擋下去,等於在沒有任何版本
# 證據時停掉每一次技能呼叫,護欄變成故障點。
#
# 豁免(這些技能永遠放行):
# jsc-cli:deploy 更新整組技能的入口,擋了就沒有任何方法更新,會死鎖
@@ -22,84 +29,148 @@
# 逃生門:JSC_VERSION_GUARD=off 完全略過檢查(離線工作時用)。
#
# 快取:$JSC_HOME/version-cache/{domain},單行「{版本} {epoch}」,
# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。
# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。hook 與 report 共用同一份。
#
# 另有一個非 hook 的子指令:
# version-guard.sh report 把每個已安裝 jsc-* plugin 的版本比對印成 TSV,每行
# 「{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}」,
# 最後一行「behind<TAB>{落後個數}」。供 jsc-cli:deploy 判斷要不要
# 把「更新」設成推薦選項。report 只讀不擋,永遠 exit 0。
# 本機沒有 Claude 的 plugin 註冊檔時改印「noregistry<TAB>{路徑}」
# 再接 behind 0:那代表這台機器無法做版本檢查,跟「全部最新」是兩件事。
HERE=$(dirname "$0"); . "$HERE/lib.sh"
REG="$HOME/.claude/plugins/installed_plugins.json"
MK="$HOME/.claude/plugins/known_marketplaces.json"
# 從檔案取 JSON 字串欄位。與 lib.sh 的 json_str 同一種 naive 解析,只是來源是檔案:
# 先把換行換成空白、再以逗號斷行,這樣每行最多一個欄位,取值不會被貪婪比對吃掉。
file_json_str() { # $1=檔案 $2=欄位名
[ -f "$1" ] || return 0
tr '\n' ' ' < "$1" | tr ',' '\n' \
| sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -n1
}
# 本機實際載入版本:先取該 plugin 的 installPath,再讀那個目錄下的 plugin.json。
# 只認 installPath 底下那份檔案。註冊在 installed_plugins.json 的 version 欄位不當備援:
# 註冊值可能比實際載入的版本新,拿它來比對會放過真正被載入的舊版,護欄形同虛設。
# 讀不到那份檔案就當「查不到本機載入版本」,由呼叫端安靜放行。
local_version() { # $1=domain
[ -f "$REG" ] && [ -r "$REG" ] || return 0
_seg=$(tr -d '\n' < "$REG" \
| sed -n "s/.*\"jsc-$1@jsc\"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p")
[ -n "$_seg" ] || return 0
_path=$(printf '%s' "$_seg" | tr ',' '\n' \
| sed -n 's/.*"installPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
[ -n "$_path" ] || return 0
file_json_str "$_path/plugin.json" version
}
# 遠端站台與 owner:從已註冊的 jsc marketplace 來源推導,其次 GITEA_HOST。
# 印出「{host} {owner}」;推導不出來時 host 為空字串。
remote_host_owner() {
_url=""
if [ -f "$MK" ]; then
_url=$(tr -d '\n' < "$MK" | sed -n 's/.*"jsc"[[:space:]]*:[[:space:]]*{//p' \
| tr ',' '\n' \
| sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
fi
_h=$(printf '%s' "$_url" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p')
_o=$(printf '%s' "$_url" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p')
if [ -z "$_h" ] || [ -z "$_o" ]; then
_h="${GITEA_HOST:-}"; _o="${JSC_GITEA_OWNER:-plugins}"
fi
printf '%s %s' "$_h" "$_o"
}
# 遠端發佈版本:一律經由 jsc-gitea 的 gitea.sh(技能準則),它會帶 GITEA_TOKEN,
# 並在缺 token 或 401/403 時退回 tea 的登入金鑰,私有存取庫才讀得到。
# 不指定 ref:Gitea 的 raw 端點預設就取該存取庫的預設分支,比在這裡寫死分支名準。
# 查不到就回傳空字串,由呼叫端放行。
remote_version() { # $1=domain $2=host $3=owner
_gsh=$(jsc_gitea_sh) || return 0
_v=""
for _try in 1 2; do # 暫時性網路失敗不該誤判成版本問題,失敗重試一次
_body=$(GITEA_HOST="$2" sh "$_gsh" api GET "/repos/$3/$1/raw/plugin.json" 2>/dev/null) \
&& _v=$(printf '%s' "$_body" | tr '\n' ' ' | tr ',' '\n' \
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
[ -n "$_v" ] && break
sleep 1
done
printf '%s' "$_v"
}
TTL="${JSC_VERSION_TTL:-600}"
cache_dir="$JSC_HOME/version-cache"
# 帶快取的遠端版本查詢。hook 與 report 共用同一份快取與同一個 TTL:
# report 每個 domain 各打一次網路(還帶重試),/jsc-cli:deploy 一跑就是全部 domain,
# 不共用快取等於每次部署都付一輪網路成本。
cached_remote_version() { # $1=domain $2=host $3=owner
_cache="$cache_dir/$1"
_now=$(now_epoch)
if [ -f "$_cache" ]; then
_cv=$(cut -d' ' -f1 "$_cache" 2>/dev/null)
_ca=$(cut -d' ' -f2 "$_cache" 2>/dev/null)
if [ -n "$_cv" ] && [ -n "$_ca" ] && [ $((_now - _ca)) -lt "$TTL" ]; then
printf '%s' "$_cv"; return 0
fi
fi
_rv=$(remote_version "$1" "$2" "$3")
if [ -n "$_rv" ]; then
mkdir -p "$cache_dir" 2>/dev/null || true
printf '%s %s\n' "$_rv" "$_now" > "$_cache" 2>/dev/null || true
fi
printf '%s' "$_rv"
}
# 語意化比較:印出 -1($1 落後)、0(相等)、1($1 超前)
ver_cmp() { # $1=版本 A $2=版本 B
awk -v a="$1" -v b="$2" '
BEGIN {
n = split(a, x, "."); m = split(b, y, ".")
for (i = 1; i <= 3; i++) {
xi = (i <= n ? x[i] + 0 : 0); yi = (i <= m ? y[i] + 0 : 0)
if (xi < yi) { print -1; exit }
if (xi > yi) { print 1; exit }
}
print 0
}'
}
# ── report:一次比對所有已安裝的 jsc plugin(非 hook 模式,不讀 stdin)
if [ "${1:-}" = "report" ]; then
python3 - <<'PY'
import json, os, re, subprocess, sys
home = os.path.expanduser("~")
try:
reg = json.load(open(f"{home}/.claude/plugins/installed_plugins.json")).get("plugins", {})
except Exception:
print("behind\t0"); sys.exit(0)
host = owner = ""
try:
mk = json.load(open(f"{home}/.claude/plugins/known_marketplaces.json"))
m = re.match(r"(https?://[^/]+)/([^/]+)/[^/]+?(?:\.git)?/?$",
(mk.get("jsc", {}).get("source", {}) or {}).get("url", ""))
if m:
host, owner = m.group(1), m.group(2)
except Exception:
pass
if not host:
host = os.environ.get("GITEA_HOST", "")
owner = os.environ.get("JSC_GITEA_OWNER", "plugins")
def ver(v):
parts = ((v or "").split(".") + ["0", "0", "0"])[:3]
return tuple(int(p) if p.isdigit() else 0 for p in parts)
behind = 0
for key in sorted(reg):
m = re.match(r"^jsc-([^@]+)@", key)
if not m:
continue
domain = m.group(1)
# 本機版本一律以 installPath 底下那份 plugin.json 為準(實際載入版本),
# 讀不到才退回註冊欄位。
local = ""
for e in reg[key]:
try:
local = json.load(open(os.path.join(e.get("installPath", ""), "plugin.json"))).get("version", "")
break
except Exception:
local = e.get("version", "")
remote = ""
if host:
url = f"{host}/{owner}/{domain}/raw/branch/master/plugin.json"
for _ in range(2): # 暫時性網路失敗不該誤判成版本問題,失敗重試一次
try:
out = subprocess.run(["curl", "-sS", "--max-time", "10", url],
capture_output=True, text=True, timeout=20).stdout
mm = re.search(r'"version"\s*:\s*"([^"]+)"', out)
if mm:
remote = mm.group(1)
break
except Exception:
pass
if not remote:
state = "查詢失敗"
elif ver(local) < ver(remote):
state = "落後"; behind += 1
elif ver(local) > ver(remote):
state = "超前"
else:
state = "最新"
print(f"{domain}\t{local or '?'}\t{remote or '?'}\t{state}")
print(f"behind\t{behind}")
PY
# 註冊檔不存在或讀不到就明講。這裡不能只印 behind 0:呼叫端會把它讀成「都是最新」,
# 於是把「這台機器無法做版本檢查」誤報成「不用更新」。
# 舊版用 `tr -d '\n' < "$REG" 2>/dev/null`,那個 2>/dev/null 只蓋住 tr 的 stderr,
# 蓋不住 shell 開檔失敗的訊息,所以沒裝 Claude 的機器會先漏一行 cannot open。
if [ ! -f "$REG" ] || [ ! -r "$REG" ]; then
printf 'noregistry\t%s\n' "$REG"
printf 'behind\t0\n'
exit 0
fi
ho=$(remote_host_owner)
r_host=$(printf '%s' "$ho" | cut -d' ' -f1)
r_owner=$(printf '%s' "$ho" | cut -d' ' -f2)
behind=0
domains=$(tr -d '\n' < "$REG" | tr ',' '\n' \
| sed -n 's/.*"jsc-\([a-z0-9][a-z0-9-]*\)@[^"]*"[[:space:]]*:.*/\1/p' | sort -u)
for d in $domains; do
lv=$(local_version "$d")
rv=""
[ -n "$r_host" ] && rv=$(cached_remote_version "$d" "$r_host" "$r_owner")
if [ -z "$rv" ]; then
st="查詢失敗"
else
case "$(ver_cmp "$lv" "$rv")" in
-1) st="落後"; behind=$((behind + 1)) ;;
1) st="超前" ;;
*) st="最新" ;;
esac
fi
printf '%s\t%s\t%s\t%s\n' "$d" "${lv:-?}" "${rv:-?}" "$st"
done
printf 'behind\t%s\n' "$behind"
exit 0
fi
@@ -107,11 +178,13 @@ read_stdin
[ "${JSC_VERSION_GUARD:-}" = "off" ] && exit 0
# 只管 Skill 工具
tool=$(json_str tool_name)
# 輸入相容:stdin JSON(Claude 格式)與環境變數(其他四支 CLI 接線時設定)都要收。
# 只讀 stdin 的話,用環境變數餵資料的 CLI 一律拿到空值,檢查會整支靜靜放行。
# 兩者都缺才是真的沒資料,那時照舊安靜降級 exit 0。
tool="${JSC_TOOL_NAME:-${TOOL_NAME:-$(json_str tool_name)}}"
[ -z "$tool" ] || [ "$tool" = "Skill" ] || exit 0
skill=$(json_str skill)
skill="${JSC_SKILL:-${SKILL:-$(json_str skill)}}"
[ -n "$skill" ] || exit 0
# 只管本技能組(jsc-{domain}:{name})
@@ -129,95 +202,44 @@ case "$skill" in
jsc-cli:deploy|jsc-hooks:hooks-install|jsc-cli:models|jsc-meta:*) exit 0 ;;
esac
TTL="${JSC_VERSION_TTL:-600}"
cache_dir="$JSC_HOME/version-cache"
mkdir -p "$cache_dir" 2>/dev/null || true
# 更新指令依實際 CLI 給。印別的 CLI 的指令等於沒給指令,使用者照著打只會失敗。
update_cmd() { # $1=domain
case "$(cli_name)" in
claude)
printf 'claude plugin marketplace update jsc && claude plugin update jsc-%s@jsc' "$1" ;;
codex)
printf 'codex plugin marketplace upgrade jsc' ;;
copilot)
printf 'copilot plugin marketplace update jsc && copilot plugin update jsc-%s@jsc' "$1" ;;
antigravity)
printf 'git -C ~/plugins/%s pull && agy plugin uninstall jsc-%s && agy plugin install ~/plugins/%s' "$1" "$1" "$1" ;;
kiro)
printf 'kiro-cli plugin marketplace update jsc && kiro-cli plugin update jsc-%s@jsc' "$1" ;;
*)
printf '用你的 CLI 的 plugin 更新指令更新 jsc-%s@jsc' "$1" ;;
esac
}
deny() { # $1=訊息
printf '[jsc][版本檢查][ERR]:%s\n' "$1" >&2
printf '更新指令:claude plugin marketplace update jsc && claude plugin update jsc-%s@jsc\n' "$domain" >&2
printf '更新指令:%s\n' "$(update_cmd "$domain")" >&2
printf '更新整組:/jsc-cli:deploy | 確定要略過檢查:JSC_VERSION_GUARD=off\n' >&2
exit 2
}
# 本機實際載入版本:從 installPath 的 plugin.json 讀,不用註冊欄位
local_ver=$(python3 - "$domain" <<'PY' 2>/dev/null
import json, os, sys
domain = sys.argv[1]
try:
reg = json.load(open(os.path.expanduser("~/.claude/plugins/installed_plugins.json")))
except Exception:
sys.exit(0)
entries = reg.get("plugins", {}).get(f"jsc-{domain}@jsc") or []
for e in entries:
p = os.path.join(e.get("installPath", ""), "plugin.json")
try:
print(json.load(open(p)).get("version", "")); sys.exit(0)
except Exception:
continue
# 讀不到實際檔案時退回註冊版本,並在後面標記為次要來源
if entries and entries[0].get("version"):
print(entries[0]["version"])
PY
)
[ -n "$local_ver" ] || deny "讀不到本機 jsc-$domain 的實際載入版本(installed_plugins.json 或該版目錄的 plugin.json 不可用)"
# 讀不到本機實際載入版本就放行:沒有版本證據時擋下等於停掉每一次技能呼叫
local_ver=$(local_version "$domain")
[ -n "$local_ver" ] || exit 0
# 遠端站台與 owner:從已註冊的 jsc marketplace 來源推導,其次 GITEA_HOST
remote_src=$(python3 - <<'PY' 2>/dev/null
import json, os, re
try:
d = json.load(open(os.path.expanduser("~/.claude/plugins/known_marketplaces.json")))
except Exception:
raise SystemExit
url = (d.get("jsc", {}).get("source", {}) or {}).get("url", "")
m = re.match(r"(https?://[^/]+)/([^/]+)/[^/]+?(?:\.git)?/?$", url)
if m:
print(m.group(1), m.group(2))
PY
)
host=$(printf '%s' "$remote_src" | cut -d' ' -f1)
owner=$(printf '%s' "$remote_src" | cut -d' ' -f2)
if [ -z "$host" ] || [ -z "$owner" ]; then
host="${GITEA_HOST:-}"; owner="${JSC_GITEA_OWNER:-plugins}"
fi
[ -n "$host" ] || deny "推導不出 Gitea 站台(known_marketplaces.json 無 jsc 來源,GITEA_HOST 也未設定)"
ho=$(remote_host_owner)
host=$(printf '%s' "$ho" | cut -d' ' -f1)
owner=$(printf '%s' "$ho" | cut -d' ' -f2)
[ -n "$host" ] || exit 0
# 快取
cache="$cache_dir/$domain"
now=$(now_epoch)
remote_ver=""
if [ -f "$cache" ]; then
c_ver=$(cut -d' ' -f1 "$cache" 2>/dev/null)
c_at=$(cut -d' ' -f2 "$cache" 2>/dev/null)
if [ -n "$c_ver" ] && [ -n "$c_at" ] && [ $((now - c_at)) -lt "$TTL" ]; then
remote_ver="$c_ver"
fi
fi
# 遠端版本(走 hook 與 report 共用的快取與 TTL)
remote_ver=$(cached_remote_version "$domain" "$host" "$owner")
[ -n "$remote_ver" ] || exit 0
if [ -z "$remote_ver" ]; then
url="$host/$owner/$domain/raw/branch/master/plugin.json"
# 暫時性網路失敗不該誤判成版本問題,所以失敗重試一次再放棄
for _try in 1 2; do
body=$(curl -sS --max-time 10 "$url" 2>/dev/null) && \
remote_ver=$(printf '%s' "$body" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
[ -n "$remote_ver" ] && break
sleep 1
done
[ -n "$remote_ver" ] || deny "查不到 jsc-$domain 的遠端發佈版本($url)。無法確認本機是否為最新,依 fail-closed 規則擋下"
printf '%s %s\n' "$remote_ver" "$now" > "$cache" 2>/dev/null || true
fi
# 語意化比較:只擋「本機 < 遠端」
cmp=$(awk -v a="$local_ver" -v b="$remote_ver" '
BEGIN {
n = split(a, x, "."); m = split(b, y, ".")
for (i = 1; i <= 3; i++) {
xi = (i <= n ? x[i] + 0 : 0); yi = (i <= m ? y[i] + 0 : 0)
if (xi < yi) { print -1; exit }
if (xi > yi) { print 1; exit }
}
print 0
}')
[ "$cmp" = "-1" ] || exit 0
# 只擋「本機 < 遠端」這一種情況
[ "$(ver_cmp "$local_ver" "$remote_ver")" = "-1" ] || exit 0
deny "jsc-$domain 本機版本 $local_ver 落後遠端發佈版本 $remote_ver,本次技能呼叫已擋下"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "jsc-hooks",
"version": "0.0.9",
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖",
"version": "0.1.5",
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查",
"skills": "./skills/"
}
+9 -15
View File
@@ -6,28 +6,22 @@ description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SD
# 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.
`version-guard.sh` runs on PreToolUse(Skill) and blocks a skill whose locally loaded plugin version is behind the published one. Where a CLI has no pre-tool hook, that guard cannot be wired — say so in the report rather than implying every CLI is covered.
Claude wiring is automatic via `hooks.json`. On codex and kiro the SDLC gate degrades to the skill-step check only; the lock file still works 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.
Only claude has both PreToolUse and UserPromptSubmit, so only claude reports `wired`. On codex, copilot, antigravity and kiro the version guard cannot be wired at all and the SDLC gate degrades to the skill-step check, so all four report `degraded` — report that gap as the script words it instead of implying every CLI is covered.
The lock file still works on those four because the SDLC skills call `sdlc-gate.sh lock {stage}` directly — that call is where the capability-tag comparison happens, so the gate keeps its force even where the prompt hook cannot be wired.
The gate needs `$JSC_HOME/model-tags.tsv`; when it is missing, report that `jsc-cli:models` (or `jsc-cli/tools/model-tags.sh sync`) must run once, because `sdlc-gate.sh lock` refuses to lock without it.
When `tools/wire-cli.sh` reports `failed`, hand the failure to `jsc-hooks:repair` right away. Do not leave a failed wiring as a dead end.
The detailed flow **MUST run as a sub agent**; the main agent only reports the summary.
## Steps
1. Run `jsc-cli/tools/detect-clis.sh`. Done when you hold the list of installed CLIs; when the list is empty, report that and stop.
2. For each installed CLI, run `tools/wire-cli.sh {cli}`. The script performs the config edit, wrapper alias install, or hook file creation for that CLI, and replaces its `<!-- jsc-hooks -->` (or `# jsc-hooks`) marker block idempotently — reruns never duplicate content. Read its exit code and first output line (`status=wired|degraded|skipped reason=...`), then confirm against the table below:
| CLI | Exit / status | Verify |
| --- | --- | --- |
| claude | `status=wired` (exit 0) — hooks.json auto-wires everything, nothing to write | `claude plugin list` shows `jsc-hooks` and `/hooks` shows the registrations |
| codex | `status=degraded` (exit 1) — notify + AGENTS.md prompt fallback | `~/.codex/config.toml` contains the `notify` entry and `AGENTS.md` contains the block |
| copilot | `status=wired` (exit 0) when the CLI is detected, `status=skipped` (exit 3) otherwise | the alias resolves to `jsc-wrap.sh copilot` and `copilot-instructions.md` contains the block |
| antigravity | `status=wired` (exit 0) when the CLI is detected, `status=skipped` (exit 3) otherwise | the alias resolves to `jsc-wrap.sh antigravity` and the rules file contains the block |
| kiro | `status=degraded` (exit 1) | the hook file exists under `.kiro/hooks/` and names the script |
A row counts as done only when its verify check passes. Exit 2 means bad usage (wrong CLI name), not a wiring outcome.
3. Report the exact `status=` line `tools/wire-cli.sh` printed for each CLI; do not reinterpret or recompute the outcome by hand. Done when every detected CLI has exactly one reported status: wired, degraded, or skipped with a reason.
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.
3. For each CLI whose status is `failed`, record it: run `tools/report-error.sh --hook wire-cli.sh --exit 4 --summary "{the reason field}" --cli {cli}` and feed the script's `[jsc]` output in on stdin. Then hand the failure to `jsc-hooks:repair`, which must run as a sub agent and must finish by opening a PR against `develop`. Done when each `failed` CLI has either an `ERROR_{HASH}` page name on stdout, or an empty exit 0 meaning `JSC_WIKI_REPO_ERROR` and `JSC_WIKI_REPO` are both unset — in that second case carry the reason into step 4 instead. Skip this step when no CLI reported `failed`.
4. Report the exact `status=` line `tools/wire-cli.sh` printed for each CLI, plus the `ERROR_{HASH}` page and repair PR URL for any `failed` one. Done when every detected CLI has exactly one reported status: wired, degraded, skipped or failed, each with its reason, and every failed wiring has a repair PR against `develop`.
## Notes
- The hook scripts accept both stdin JSON and environment variables (`JSC_CLI`, `JSC_SESSION_ID`, `JSC_SKILL`, `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.
- `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`.
+16
View File
@@ -0,0 +1,16 @@
---
name: repair
description: Repair failed hook wiring by delegating diagnosis to installed AI agent CLIs as subagents, patching the hooks repo, syncing manifests, and opening a PR against develop. Use when hooks-install or report-error.sh reports a failed hook, or when a hook keeps failing after rewiring; not for routine wiring or unrelated work.
---
# repair — repair a failed hook
Single source of guidelines: [`../../references/guidelines.md`](../../references/guidelines.md).
## Flow
1. Read the failure context from `ERROR_{HASH}` through `jsc-gitea:wiki` or from the failed `status=` line, then confirm the target repo is `hooks` and the PR base branch is `develop`. Completion condition: the failure context and target branch are explicit.
2. Detect installed AI CLIs with `../cli/tools/detect-clis.sh`, then delegate diagnosis to one subagent per available CLI. Each subagent must receive the failure context and the `/jsc-shared:spec-output` rules, must stay read-only, and must return one structured repair proposal: root cause, changed files, and verification command. Completion condition: every available CLI has one returned proposal, or there are no CLIs and the main agent has noted that it must diagnose alone.
3. Pick the smallest repair that makes the wiring pass, then apply it in the `hooks` repo. If the fix touches wiring behavior, update `hooks/tools/wire-cli.sh`, `hooks/skills/hooks-install/SKILL.md`, and `hooks/README.md` together. Run the relevant verification command before moving on. Completion condition: the fix is on disk and the verification command passes.
4. Run `../meta/tools/sync-skill-manifest.sh .`. Completion condition: the README skill list and all three manifests show the same new version.
5. Commit, push, and open a PR with `jsc-git:pr develop`. Completion condition: a PR URL comes back and the repair is ready for review.
+5 -1
View File
@@ -3,20 +3,24 @@
# 用法: jsc-wrap.sh {cli} [args...]
# 行為: 匯出 JSC_CLI 與 JSC_SESSION_ID → session-timer start → 執行 CLI →
# 結束後 session-timer mark 並以 scan-logs.sh 回填用量,最後回傳 CLI 的結束碼。
# 注意: JSC_CLI 存的是 CLI 代號(antigravity、kiro),實際執行的是 cli_bin 對應的
# 執行檔(agy、kiro-cli)。直接拿代號當指令跑會 127,因為沒有這兩個執行檔。
HERE=$(cd "$(dirname "$0")" && pwd)
HOOKS="$HERE/../hooks"
. "$HOOKS/lib.sh"
cli="${1:-}"
if [ -z "$cli" ]; then
echo "用法:jsc-wrap.sh {cli} [args...]" >&2
exit 2
fi
shift
bin=$(cli_bin "$cli")
JSC_CLI="$cli"
# 未提供 session id 就自動產生({cli}-時間戳-PID),讓計時與用量共用同一個 session
[ -n "${JSC_SESSION_ID:-}" ] || JSC_SESSION_ID="$cli-$(date +%Y%m%d%H%M%S)-$$"
export JSC_CLI JSC_SESSION_ID
sh "$HOOKS/session-timer.sh" start </dev/null
"$cli" "$@"
"$bin" "$@"
rc=$?
# 收尾:補記結束時間,並從原生日誌回填技能用量
sh "$HOOKS/session-timer.sh" mark </dev/null
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env sh
# report-error.sh — 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 ERROR_{HASH},
# 並在 ERROR_CONTENTS 附上一列索引。頁面內容套用 templates/ 的兩份範本,
# 範本是文案的唯一來源,本腳本只填欄位。
#
# 用法:
# report-error.sh --hook {名稱} --exit {碼} --summary {摘要}
# [--repo {owner}/{repo}] [--cli {名稱}] [--session {id}]
# [--source {stdin|env|command}] [--symptom {現象}]
# [--cause {可能原因}] [--action {處理結果}]
# 相關輸出(stdout/stderr 摘要)由標準輸入讀入,可省略。
#
# 輸出:
# 成功印出「{頁名} {網址}」一行。
# wiki 位置解析不出來(JSC_WIKI_REPO_ERROR 與 JSC_WIKI_REPO 都沒設,或找不到
# gitea.sh)時安靜降級:不輸出、exit 0。回報失敗不該再變成一次失敗。
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
#
# 頁名:
# ERROR_{HASH},HASH 取「{owner}/{repo} {hook} {時間}」的 SHA-1 前 8 碼(共用 hash 規則)。
# 時間放進 hash:同一種失敗再發生時要另開新頁,不覆寫舊紀錄。
#
# 誰來呼叫:
# 由操作者手動執行,或由技能步驟執行(`jsc-hooks:hooks-install` 在 wire-cli.sh 回報
# status=failed 時呼叫)。**不接在失敗的 hook 上自動觸發**:hook 一律安靜 exit 0,
# 而且自我回報要走網路寫 wiki,失敗的 hook 再去回報自己會疊出迴圈。
set -u
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/.." && pwd)
. "$ROOT/hooks/lib.sh"
STDIN_JSON="" # 本腳本的標準輸入是錯誤輸出摘要,不是 JSON
hook=""; code=""; summary=""; repo=""; cli=""; session=""
source_kind=""; symptom=""; cause=""; action=""
while [ $# -gt 0 ]; do
case "$1" in
--hook) hook="${2:-}"; shift 2 ;;
--exit) code="${2:-}"; shift 2 ;;
--summary) summary="${2:-}"; shift 2 ;;
--repo) repo="${2:-}"; shift 2 ;;
--cli) cli="${2:-}"; shift 2 ;;
--session) session="${2:-}"; shift 2 ;;
--source) source_kind="${2:-}"; shift 2 ;;
--symptom) symptom="${2:-}"; shift 2 ;;
--cause) cause="${2:-}"; shift 2 ;;
--action) action="${2:-}"; shift 2 ;;
*) shift ;;
esac
done
if [ -z "$hook" ] || [ -z "$summary" ]; then
echo "用法:report-error.sh --hook {名稱} --exit {碼} --summary {摘要} [...]" >&2
exit 2
fi
gsh=$(jsc_gitea_sh) || exit 0
wrepo=$(sh "$gsh" wiki-repo ERROR 2>/dev/null) || exit 0
[ -n "$wrepo" ] || exit 0
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
if [ -z "$repo" ]; then
origin=$(git config --get remote.origin.url 2>/dev/null || true)
repo=$(printf '%s' "$origin" \
| sed -n 's#.*[/:]\([^/]*\)/\([^/]*\)$#\1/\2#p' | sed 's/\.git$//')
fi
[ -n "$repo" ] || repo="-"
[ -n "$cli" ] || cli=$(cli_name)
[ -n "$session" ] || session=$(session_id)
[ -n "$code" ] || code="-"
[ -n "$source_kind" ] || source_kind="command"
[ -n "$symptom" ] || symptom="$summary"
[ -n "$cause" ] || cause="待查"
[ -n "$action" ] || action="待處理"
ts=$(date +'%Y-%m-%d %H:%M:%S')
ticket_ts=$(date +'%Y%m%d_%H%M%S')
if [ -t 0 ]; then detail=""; else detail=$(cat 2>/dev/null | tr '\n' ' ' | cut -c1-500); fi
[ -n "$detail" ] || detail="(無)"
# 摘要與相關輸出都落在 markdown 表格欄位裡,半形 | 會把欄位切斷,改成全形
detail=$(printf '%s' "$detail" | sed 's/|/|/g')
summary=$(printf '%s' "$summary" | sed 's/|/|/g')
hash=$(sh "$gsh" hash-id "$repo $hook $ts" 2>/dev/null) || exit 0
[ -n "$hash" ] || exit 0
page="ERROR_$hash"
# sed 取代值要先轉義:& 與分隔字元 | 會被 sed 當語法,換行會整行斷掉
esc() { printf '%s' "$1" | tr '\n' ' ' | sed 's/[\\&|]/\\&/g'; }
fill() { sed "s|$1|$(esc "$2")|g"; }
tmp_page=$(mktemp) || exit 0
tmp_list=$(mktemp) || { rm -f "$tmp_page"; exit 0; }
trap 'rm -f "$tmp_page" "$tmp_list"' EXIT
fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \
| fill '{yyyy-MM-dd HH:mm:ss}' "$ts" \
| fill '{owner}/{repo}' "$repo" \
| fill '{cli}' "$cli" \
| fill '{session_id}' "$session" \
| fill '{hook_name}' "$hook" \
| fill '{exit_code}' "$code" \
| fill '{error_summary}' "$summary" \
| fill '{現象描述}' "$symptom" \
| fill '{可能原因}' "$cause" \
| fill '{處理方式}' "$action" \
| fill '{stdin / env / command}' "$source_kind" \
| fill '{stdout / stderr 摘要}' "$detail" \
| fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page"
row=$(printf '| %s | [[%s|%s]] | %s | %s | %s | %s |' \
"$ts" "$hook 異常 $ts" "$page" "$repo" "$hook" "$code" "$summary")
# 目錄頁:已存在就把新列附在文末(最新一筆在最後);不存在就用範本建立
if sh "$gsh" wiki-get "$wrepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null \
&& [ -s "$tmp_list" ]; then
printf '%s\n' "$row" >> "$tmp_list"
else
fill '{yyyy-MM-dd HH:mm:ss}' "$ts" < "$ROOT/templates/error-contents.md" \
| fill '{HASH}' "$hash" \
| fill '{error title}' "$hook 異常 $ts" \
| fill '{owner}/{repo}' "$repo" \
| fill '{hook_name}' "$hook" \
| fill '{exit_code}' "$code" \
| fill '{error_summary}' "$summary" > "$tmp_list"
fi
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
exit 4
fi
if ! sh "$gsh" wiki-put "$wrepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then
echo "[jsc] 寫入 ERROR_CONTENTS 失敗($wrepo),$page 已建立。" >&2
exit 4
fi
url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null || true)
printf '%s %s\n' "$page" "$url"
+195 -45
View File
@@ -1,24 +1,40 @@
#!/usr/bin/env sh
# wire-cli.sh — 把 jsc 四支 hook 接線到單一 CLI(供 hooks-install 技能呼叫)。
# wire-cli.sh — 把 jsc 五支 hook 接線到單一 CLI(供 hooks-install 技能呼叫)。
# 用法: wire-cli.sh {claude|codex|copilot|antigravity|kiro}
# 行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加):
# claude — 什麼都不用寫,hooks.json 已自動接線四支 hook
# codex — 在 config.toml 設 notify(呼叫 session-timer.sh mark,JSC_CLI=codex);
# claude — 什麼都不用寫,hooks.json 已自動接線五支 hook
# codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時);
# 在 config.toml 設 notify(每輪補 session-timer.sh start 再 mark,JSC_CLI=codex);
# 在 AGENTS.md 附加 STE100 規則段落(prompt 降級)
# copilot — 在 shell rc 檔加上 copilot 別名,轉呼叫 tools/jsc-wrap.sh copilot;
# 在 copilot-instructions.md 附加 STE100 規則段落
# antigravity — 在 shell rc 檔加上 agy 別名,轉呼叫 tools/jsc-wrap.sh antigravity;
# 在全域規則檔附加 STE100 規則段落
# kiro — 在工作區 .kiro/hooks/ 下建立 jsc-hooks.json(JSC_CLI=kiro)
# kiro — 在工作區 .kiro/hooks/ 下建立 jsc-hooks.json(每輪 mark 加 STE100)
# 與 jsc-hooks-session-start.json(sessionStart 開始計時),皆帶 JSC_CLI=kiro
#
# 輸出: 第一行固定為 `status={wired|degraded|skipped} reason=...`(可供程式判讀),
# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護:
# claude 五支 hook 全接,回報 wired
# codex、copilot、antigravity、kiro 只有別名或規則檔,接不上 PreToolUse 與
# UserPromptSubmit,版本前置檢查與 SDLC 模型鎖
# 都沒接上,一律回報 degraded 並在 reason 講明
# 所有 jsc 標記段落都採整段重寫,重跑等同先移除舊內容再重裝
#
# 寫入後自我驗證,通過才回報成功:每個寫過的檔案重新讀一次,確認標記段落存在且落在
# 正確位置(codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對
# 且 on、run 在最上層)。腳本說寫好了卻寫錯位置,是最難查的失敗,所以驗證放在腳本裡。
#
# 輸出: 第一行固定為 `status={wired|degraded|skipped|failed} reason=...`(可供程式判讀),
# 其後為人類可讀的繁中說明。
# 結束碼: 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查)
# 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過)
# 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報)
set -u
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/.." && pwd)
HOOKS="$ROOT/hooks"
# cli_bin(CLI 代號 → 實際執行檔)的唯一來源在 lib.sh,包裝啟動器也用同一份
. "$HOOKS/lib.sh"
cli="${1:-}"
case "$cli" in
@@ -28,25 +44,20 @@ case "$cli" in
exit 2 ;;
esac
# 每個 CLI 對應的實際執行檔名稱(antigravity 的執行檔是 agy,其餘與 CLI 代號同名)
cli_bin() {
case "$1" in
antigravity) printf 'agy' ;;
kiro) printf 'kiro-cli' ;;
*) printf '%s' "$1" ;;
esac
}
# STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出
ste100_text() { sh "$HOOKS/ste100-guard.sh" 2>/dev/null | sed '/^exit /d'; }
# 以標記整段取代(冪等);標記不存在就在檔尾新增;檔案不存在就建立。
# 適用 markdown 規則檔與 shell rc 檔:這兩種檔案沒有「區段」概念,附在檔尾就對了。
# $1=檔案 $2=開頭標記行 $3=結尾標記行 $4=標記之間要寫入的內容
replace_block() {
file="$1"; bopen="$2"; bshut="$3"; content="$4"
dir=$(dirname "$file")
mkdir -p "$dir" 2>/dev/null || return 1
touch "$file" 2>/dev/null || return 1
# touch 對目錄也會成功,所以要另外確認它真的是一般檔案;不然接著的寫入才失敗,
# 而 shell 開檔失敗的訊息蓋不掉,會漏一行 cannot create 給使用者看。
[ -f "$file" ] || return 1
block=$(printf '%s\n%s\n%s' "$bopen" "$content" "$bshut")
if grep -qF "$bopen" "$file" 2>/dev/null; then
awk -v bopen="$bopen" -v bshut="$bshut" -v block="$block" '
@@ -54,14 +65,85 @@ replace_block() {
$0==bshut { skip=0; next }
skip { next }
{ print }
' "$file" > "$file.jsc-tmp" 2>/dev/null && mv "$file.jsc-tmp" "$file"
' "$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; }
else
printf '\n%s\n' "$block" >> "$file"
# 包一層子 shell 才蓋得住 shell 自己的開檔失敗訊息(>> 失敗時那行不走命令的 stderr)
( printf '\n%s\n' "$block" >> "$file" ) 2>/dev/null || return 1
fi
}
# TOML 版的整段取代:標記段落一律放在第一個表頭(`[table]`、`[[array]]`)之前。
# TOML 的根層鍵只在第一個表頭之前有效,附在檔尾會被歸進最後那張表——檔案照樣解析
# 得過,codex 卻永遠讀不到 notify,hook 靜靜失效。所以位置本身就是正確性的一部分。
# 舊版寫錯位置的段落也會被這支函式移到正確位置(先整段刪除,再插到表頭之前)。
# $1=檔案 $2=開頭標記行 $3=結尾標記行 $4=標記之間要寫入的內容
replace_block_toml() {
file="$1"; bopen="$2"; bshut="$3"; content="$4"
dir=$(dirname "$file")
mkdir -p "$dir" 2>/dev/null || return 1
touch "$file" 2>/dev/null || return 1
# touch 對目錄也會成功,所以要另外確認它真的是一般檔案;不然接著的寫入才失敗,
# 而 shell 開檔失敗的訊息蓋不掉,會漏一行 cannot create 給使用者看。
[ -f "$file" ] || return 1
block=$(printf '%s\n%s\n%s' "$bopen" "$content" "$bshut")
awk -v bopen="$bopen" -v bshut="$bshut" -v block="$block" '
$0==bopen { skip=1; next }
$0==bshut { skip=0; next }
skip { next }
# 表頭樣式:整行只有 [name] 或 [[name]]。多行陣列裡的 [1, 2], 不會命中。
!done && /^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { print block; print ""; done=1 }
{ print }
END { if (!done) print block }
' "$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; }
}
# 驗證:檔案裡有這段標記嗎($1=檔案 $2=開頭標記行)
has_block() { grep -qF "$2" "$1" 2>/dev/null; }
# 驗證:TOML 的某個鍵是不是落在根層(第一個表頭之前)。$1=檔案 $2=鍵名
toml_root_key() {
awk -v k="$2" '
/^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { intable=1; next }
!intable && $0 ~ "^[ \t]*" k "[ \t]*=" { found=1 }
END { exit(found ? 0 : 1) }
' "$1" 2>/dev/null
}
# 驗證:JSON 括號成對,且某個鍵出現在最上層($1=檔案 $2=鍵名)。
# 解析失敗(括號不成對、字串沒收尾)也回傳非 0,所以這支同時當語法檢查用。
json_top_key() {
awk -v k="$2" '
{ s = s $0 "\n" }
END {
n = length(s); depth = 0; i = 1; found = 0; bad = 0
while (i <= n) {
c = substr(s, i, 1)
if (c == "\"") {
buf = ""; i++; closed = 0
while (i <= n) {
c = substr(s, i, 1)
if (c == "\\") { i += 2; continue }
if (c == "\"") { i++; closed = 1; break }
buf = buf c; i++
}
if (!closed) { bad = 1; break }
j = i
while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++
if (substr(s, j, 1) == ":" && depth == 1 && buf == k) found = 1
continue
}
if (c == "{" || c == "[") depth++
else if (c == "}" || c == "]") { depth--; if (depth < 0) { bad = 1; break } }
i++
}
exit((found && !bad && depth == 0) ? 0 : 1)
}' "$1" 2>/dev/null
}
# 找出可寫入別名的 shell rc 檔;都不存在就以 ~/.bashrc 為預設(自動建立)。
# 印出找到/建立的 rc 檔路徑,一行一個。
# 印出找到或建立的 rc 檔路徑,一行一個。
rc_files() {
found=""
for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do
@@ -70,32 +152,74 @@ rc_files() {
[ -n "$found" ] || printf '%s\n' "$HOME/.bashrc"
}
# 把別名寫進每個 rc 檔並逐檔驗證。$1=標記名(不含 # 與 /)$2=別名內容
# 迴圈不可以放在管線右邊:那會變成子 shell,寫入失敗的旗標傳不回來,
# 明明沒寫成功也照樣回報 wired。改成從暫存檔讀,迴圈就留在本 shell。
write_alias_rc() {
_mark="$1"; _line="$2"; _ok=1
_list=$(mktemp) || return 1
rc_files > "$_list" || { rm -f "$_list"; return 1; }
while IFS= read -r rc; do
[ -n "$rc" ] || continue
replace_block "$rc" "# $_mark" "# /$_mark" "$_line" || { _ok=0; continue; }
grep -qF "$_line" "$rc" 2>/dev/null || _ok=0
done < "$_list"
rm -f "$_list"
[ "$_ok" = 1 ]
}
skip() { # $1=reason
printf 'status=skipped reason=%s\n' "$1"
echo "[jsc] 略過:$1"
exit 3
}
fail() { # $1=reason
printf 'status=failed reason=%s\n' "$1"
echo "[jsc] 接線沒生效:$1" >&2
echo "[jsc] 請先以 tools/report-error.sh 回報這次失敗,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2
exit 4
}
case "$cli" in
claude)
bin=$(cli_bin claude)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 claude 執行檔"
[ -f "$HOOKS/hooks.json" ] || fail "找不到 $HOOKS/hooks.json,claude 接不到任何 hook"
printf 'status=wired reason=%s\n' "hooks.json 自動接線"
echo "[jsc] claude:由 hooks/hooks.json 自動接線全部四支 hook,無需寫入設定。"
echo "[jsc] claude:由 hooks/hooks.json 自動接線全部五支 hook,無需寫入設定。"
exit 0 ;;
codex)
command -v "$(cli_bin codex)" >/dev/null 2>&1 || skip "未偵測到 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"
notify_line="notify = [\"env\", \"JSC_CLI=codex\", \"sh\", \"$HOOKS/session-timer.sh\", \"mark\"]"
replace_block "$config" "# jsc-hooks" "# /jsc-hooks" "$notify_line" \
|| skip "無法寫入 $config"
# codex 的 notify 只在每一輪結束時觸發,沒有工作階段開始事件,所以計時分兩段接:
# 1. shell 別名走 jsc-wrap.sh:啟動當下就 session-timer start,並給這次工作階段
# 一個 JSC_SESSION_ID,codex 內觸發的 notify 會沿用同一個 id。
# 2. notify 每輪先補 start 再 mark。start 已有紀錄就不動,所以沒走別名啟動時
# 仍拿得到起始時間(從第一輪算起)。少了這一段,worklog 只會拿到 0 秒。
timer="sh '$HOOKS/session-timer.sh'"
notify_line="notify = [\"env\", \"JSC_CLI=codex\", \"sh\", \"-c\", \"$timer start </dev/null; $timer mark </dev/null\"]"
alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" codex'"
replace_block_toml "$config" "# jsc-hooks" "# /jsc-hooks" "$notify_line" \
|| fail "無法寫入 $config"
has_block "$config" "# jsc-hooks" || fail "$config 寫入後讀不到 jsc-hooks 標記段落"
toml_root_key "$config" notify \
|| fail "$config 的 notify 沒有落在根層(被歸進某張表,codex 讀不到,hook 會靜靜失效)"
write_alias_rc "jsc-hooks:codex" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔"
replace_block "$agents" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" "$(ste100_text)" \
|| skip "無法寫入 $agents"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查"
echo "[jsc] codex:已設定 $config 的 notify 呼叫 session-timer.sh mark。"
|| fail "無法寫入 $agents"
has_block "$agents" "<!-- jsc-hooks -->" || fail "$agents 寫入後讀不到 jsc-hooks 標記段落"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查"
echo "[jsc] codex:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh codex,啟動當下開始計時。"
echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才生效。"
echo "[jsc] codex:已設定 $config 的 notify(根層鍵,已驗證),每輪補 session-timer.sh start 再 mark。"
echo "[jsc] codex:已在 $agents 寫入 STE100 規則段落(prompt 降級)。"
echo "[jsc] codex:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] codex:版本前置檢查接不上(codex 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
exit 1 ;;
copilot)
@@ -103,47 +227,73 @@ case "$cli" in
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 copilot 執行檔"
instr="${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}"
alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" copilot'"
rc_files | while IFS= read -r rc; do
replace_block "$rc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot" "$alias_line"
done
write_alias_rc "jsc-hooks:copilot" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔"
replace_block "$instr" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" "$(ste100_text)" \
|| skip "無法寫入 $instr"
printf 'status=wired reason=%s\n' "wrapper 別名 + 規則檔已接線"
|| fail "無法寫入 $instr"
has_block "$instr" "<!-- jsc-hooks -->" || fail "$instr 寫入後讀不到 jsc-hooks 標記段落"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查"
echo "[jsc] copilot:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh copilot。"
echo "[jsc] copilot:已在 $instr 寫入 STE100 規則段落。"
exit 0 ;;
echo "[jsc] copilot:已在 $instr 寫入 STE100 規則段落(prompt 降級)。"
echo "[jsc] copilot:別名要開新的 shell 或重新 source rc 檔才生效。"
echo "[jsc] copilot:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] copilot:版本前置檢查接不上(copilot 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
exit 1 ;;
antigravity)
bin=$(cli_bin antigravity)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 antigravity(agy)執行檔"
rules="${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}"
alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" antigravity'"
rc_files | while IFS= read -r rc; do
replace_block "$rc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity" "$alias_line"
done
write_alias_rc "jsc-hooks:antigravity" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔"
replace_block "$rules" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" "$(ste100_text)" \
|| skip "無法寫入 $rules"
printf 'status=wired reason=%s\n' "wrapper 別名 + 規則檔已接線"
|| fail "無法寫入 $rules"
has_block "$rules" "<!-- jsc-hooks -->" || fail "$rules 寫入後讀不到 jsc-hooks 標記段落"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查"
echo "[jsc] antigravity:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh antigravity。"
echo "[jsc] antigravity:已在 $rules 寫入 STE100 規則段落。"
exit 0 ;;
echo "[jsc] antigravity:已在 $rules 寫入 STE100 規則段落(prompt 降級)。"
echo "[jsc] antigravity:別名要開新的 shell 或重新 source rc 檔才生效。"
echo "[jsc] antigravity:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] antigravity:版本前置檢查接不上(antigravity 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
exit 1 ;;
kiro)
command -v "$(cli_bin kiro)" >/dev/null 2>&1 || skip "未偵測到 kiro-cli 執行檔"
hookdir="./.kiro/hooks"
hookfile="$hookdir/jsc-hooks.json"
mkdir -p "$hookdir" 2>/dev/null || skip "無法建立 $hookdir"
cat > "$hookfile" 2>/dev/null <<EOF || skip "無法寫入 $hookfile"
startfile="$hookdir/jsc-hooks-session-start.json"
mkdir -p "$hookdir" 2>/dev/null || fail "無法建立 $hookdir"
# 計時要分兩個檔:kiro 的一個 hook 檔只有一組 run,所有事件共用。
# sessionStart 單獨一檔跑 restart,才算得出這一次工作階段的花費時間;
# kiro 給不到 session id,紀錄共用 default,不覆寫起始時間就會把上一階段算進來。
cat > "$startfile" 2>/dev/null <<EOF || fail "無法寫入 $startfile"
{
"name": "jsc-hooks-session-start",
"description": "jsc session timer start (auto-generated by jsc-hooks:hooks-install, do not edit by hand)",
"on": ["sessionStart"],
"env": { "JSC_CLI": "kiro" },
"run": "sh \\"$HOOKS/session-timer.sh\\" restart </dev/null"
}
EOF
cat > "$hookfile" 2>/dev/null <<EOF || fail "無法寫入 $hookfile"
{
"name": "jsc-hooks",
"description": "jsc session timer + STE100 guard bridge (auto-generated by jsc-hooks:hooks-install, do not edit by hand)",
"on": ["sessionStart", "sessionEnd", "userPromptSubmit"],
"on": ["sessionEnd", "userPromptSubmit"],
"env": { "JSC_CLI": "kiro" },
"run": "sh \\"$HOOKS/session-timer.sh\\" mark; sh \\"$HOOKS/ste100-guard.sh\\""
"run": "sh \\"$HOOKS/session-timer.sh\\" mark </dev/null; sh \\"$HOOKS/ste100-guard.sh\\" </dev/null"
}
EOF
printf 'status=degraded reason=%s\n' "SDLC 模型鎖降級為技能步驟檢查"
echo "[jsc] kiro:已建立 $hookfile(JSC_CLI=kiro)。"
for f in "$startfile" "$hookfile"; do
json_top_key "$f" on || fail "$f 不是成對的 JSON,或 on 不在最上層"
json_top_key "$f" run || fail "$f 不是成對的 JSON,或 run 不在最上層"
grep -qF '"JSC_CLI": "kiro"' "$f" 2>/dev/null || fail "$f 缺少 JSC_CLI=kiro"
grep -qF 'session-timer.sh' "$f" 2>/dev/null || fail "$f 的 run 沒有接到 session-timer.sh"
done
grep -qF '"sessionStart"' "$startfile" 2>/dev/null || fail "$startfile 沒有接在 sessionStart"
grep -qF '"userPromptSubmit"' "$hookfile" 2>/dev/null || fail "$hookfile 沒有接在 userPromptSubmit"
printf 'status=degraded reason=%s\n' "SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查"
echo "[jsc] kiro:已建立 $startfile(sessionStart 開始計時)與 $hookfile(JSC_CLI=kiro),兩份都已驗證。"
echo "[jsc] kiro:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] kiro:版本前置檢查接不上(kiro 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
exit 1 ;;
esac