fix(skillset): 技能組稽核修正與第九支 hook #52
+16
-3
@@ -4,6 +4,17 @@
|
|||||||
# 技能組更新後,正在跑的 CLI 行程載入的還是舊版:SKILL.md、hook 腳本與 tools 都在啟動當下
|
# 技能組更新後,正在跑的 CLI 行程載入的還是舊版:SKILL.md、hook 腳本與 tools 都在啟動當下
|
||||||
# 讀進記憶體。所以部署收尾要求重新啟動,這道閘門負責讓「還沒重啟就繼續用技能」擋在門外。
|
# 讀進記憶體。所以部署收尾要求重新啟動,這道閘門負責讓「還沒重啟就繼續用技能」擋在門外。
|
||||||
#
|
#
|
||||||
|
# 結束碼(hook 模式):0=放行 2=擋下該次技能呼叫,訊息走 stderr。
|
||||||
|
# 安靜放行(exit 0)的情況:逃生門 JSC_RESTART_GATE=off、工具名取得到但不是 Skill、
|
||||||
|
# 取不到技能名、技能名不是 jsc-{domain}:{name}、命中下方豁免清單那 10 支、取不到 CLI 代號、
|
||||||
|
# 當前 CLI 那份狀態檔與舊格式狀態檔都不在。
|
||||||
|
# 只有「當前 CLI 那份狀態檔存在」或「退回讀到的舊格式狀態檔存在」會 exit 2。
|
||||||
|
# 結束碼(require):0=閘門已掛上 2=取不到 CLI 代號或寫不進狀態檔,兩種都等於沒掛上。
|
||||||
|
# 結束碼(clear、report):0=永遠成功。clear 檔案不存在也算成功,report 一份都沒有就不印。
|
||||||
|
# 結束碼(不認得的子命令):0=安靜放行,不中斷宿主 CLI。
|
||||||
|
# 註:本檔以 `. "$HERE/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會就地
|
||||||
|
# 結束並回 2,接在 PreToolUse 上就是無聲擋下每一次技能呼叫,上面那些放行路徑一條都跑不到。
|
||||||
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# restart-gate.sh hook 模式:當前 CLI 那份狀態檔存在就擋下該次技能
|
# restart-gate.sh hook 模式:當前 CLI 那份狀態檔存在就擋下該次技能
|
||||||
# 呼叫(exit 2)。別支 CLI 那幾份不看。
|
# 呼叫(exit 2)。別支 CLI 那幾份不看。
|
||||||
@@ -79,6 +90,8 @@
|
|||||||
# 豁免(這些技能永遠放行,改動前想清楚後果):
|
# 豁免(這些技能永遠放行,改動前想清楚後果):
|
||||||
# jsc-cli:deploy 部署入口本身,也是唯一能把技能組換成新版的路徑,擋了會死鎖
|
# jsc-cli:deploy 部署入口本身,也是唯一能把技能組換成新版的路徑,擋了會死鎖
|
||||||
# jsc-hooks:hooks-install 部署後要重新接線,擋了會讓部署做一半卡住
|
# jsc-hooks:hooks-install 部署後要重新接線,擋了會讓部署做一半卡住
|
||||||
|
# jsc-hooks:repair 接線或執行期出錯時唯一的修復路徑。修 hook 的技能被 hook 擋下,
|
||||||
|
# 就沒有任何方法把 hook 修回來,閘門等於把解除自己的路徑一起鎖掉
|
||||||
# jsc-gitea:wiki 寫技能組異動報告與工作日誌都要它落地,擋了報告寫不完
|
# jsc-gitea:wiki 寫技能組異動報告與工作日誌都要它落地,擋了報告寫不完
|
||||||
# jsc-log:worklog 部署後還要寫得完工作日誌(R1)
|
# jsc-log:worklog 部署後還要寫得完工作日誌(R1)
|
||||||
# jsc-log:learn 同上,教訓也要記得完
|
# jsc-log:learn 同上,教訓也要記得完
|
||||||
@@ -90,7 +103,7 @@
|
|||||||
# 「先重啟」與「先寫完報告」會互相打死,使用者兩件事都做不完。
|
# 「先重啟」與「先寫完報告」會互相打死,使用者兩件事都做不完。
|
||||||
#
|
#
|
||||||
# 清單認的是技能名,不是呼叫鏈:豁免技能轉呼叫的下一層若不在清單上,那一層照樣會被擋。
|
# 清單認的是技能名,不是呼叫鏈:豁免技能轉呼叫的下一層若不在清單上,那一層照樣會被擋。
|
||||||
# 後三支(ask、pr、commit)就是為了這件事補進來的——它們自己不是收尾規則的主體,但前六支
|
# 後三支(ask、pr、commit)就是為了這件事補進來的——它們自己不是收尾規則的主體,但前七支
|
||||||
# 少了它們就走不完:deploy 問不出模式、報告寫完開不了 PR。version-guard.sh 當年把
|
# 少了它們就走不完:deploy 問不出模式、報告寫完開不了 PR。version-guard.sh 當年把
|
||||||
# jsc-ask:ask 與 jsc-gitea:wiki 放進豁免,也是同一個原因。
|
# jsc-ask:ask 與 jsc-gitea:wiki 放進豁免,也是同一個原因。
|
||||||
# 還有巢狀呼叫走不下去時,先重新啟動;真的卡死才下 JSC_RESTART_GATE=off。
|
# 還有巢狀呼叫走不下去時,先重新啟動;真的卡死才下 JSC_RESTART_GATE=off。
|
||||||
@@ -187,7 +200,7 @@ esac
|
|||||||
|
|
||||||
# 豁免清單(理由見檔頭)
|
# 豁免清單(理由見檔頭)
|
||||||
case "$skill" in
|
case "$skill" in
|
||||||
jsc-cli:deploy|jsc-hooks:hooks-install|jsc-gitea:wiki|jsc-log:worklog|jsc-log:learn|jsc-meta:*|jsc-ask:ask|jsc-git:pr|jsc-git:commit)
|
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)
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -227,5 +240,5 @@ printf '[jsc][重啟閘門][ERR]:技能組已更新%s,%s 還在跑舊版,
|
|||||||
"${info:+($info)}" "$bin" >&2
|
"${info:+($info)}" "$bin" >&2
|
||||||
printf '重新啟動:結束 %s 再重新開啟一次,狀態檔 %s 會在新工作階段開始時自動清除。\n' \
|
printf '重新啟動:結束 %s 再重新開啟一次,狀態檔 %s 會在新工作階段開始時自動清除。\n' \
|
||||||
"$bin" "$state" >&2
|
"$bin" "$state" >&2
|
||||||
printf '仍可使用:/jsc-cli:deploy、/jsc-hooks:hooks-install、/jsc-gitea:wiki、/jsc-log:worklog、/jsc-log:learn、/jsc-meta:*、/jsc-ask:ask、/jsc-git:pr、/jsc-git:commit(部署後的異動報告與工作日誌要寫得完) | 確定要略過閘門:JSC_RESTART_GATE=off\n' >&2
|
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' >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|||||||
+41
-4
@@ -32,8 +32,30 @@
|
|||||||
# sdlc-gate.sh wp-report 印出 {owner}/{repo} {index} {上鎖時間} {工作包代號},每個未結清
|
# sdlc-gate.sh wp-report 印出 {owner}/{repo} {index} {上鎖時間} {工作包代號},每個未結清
|
||||||
# 工作包各一行;沒有未結清就不印,exit 0。查無歸屬時第四欄留白。
|
# 工作包各一行;沒有未結清就不印,exit 0。查無歸屬時第四欄留白。
|
||||||
# sdlc-gate.sh wp-check prompt hook 模式(UserPromptSubmit):注入提醒,一律 exit 0。
|
# sdlc-gate.sh wp-check prompt hook 模式(UserPromptSubmit):注入提醒,一律 exit 0。
|
||||||
# sdlc-gate.sh wp-check skill hook 模式(PreToolUse,matcher Skill):命中別的階段技能時
|
# sdlc-gate.sh wp-check skill hook 模式(PreToolUse,matcher Skill):命中 analyze 或 maintain
|
||||||
# exit 2 擋下該次呼叫;其餘 exit 0。
|
# 時 exit 2 擋下該次呼叫;plan 與 implement 只注入提醒後 exit 0,
|
||||||
|
# 其餘技能一律 exit 0。
|
||||||
|
#
|
||||||
|
# 結束碼(一個子命令一列):
|
||||||
|
# lock 0=通過並已上鎖 1=未通過,呼叫端必須停止流程(階段名不合法、讀不到該
|
||||||
|
# 階段的必要標籤、判定不出目前模型、模型不在標籤表上、缺標籤、寫不進狀態檔)
|
||||||
|
# unlock 0=永遠成功,狀態檔不存在也算
|
||||||
|
# check 0=放行 2=擋下該輪提示。安靜放行:沒有階段鎖、舊格式狀態檔讀不出必要標籤、
|
||||||
|
# 必要標籤是 any、判定不出目前模型(只提醒不擋)
|
||||||
|
# report 0=永遠成功,無鎖就不印
|
||||||
|
# wp-lock 0=已記下 2=存取庫不是 {owner}/{repo}、PR 編號不是數字、寫不進狀態檔
|
||||||
|
# wp-unlock 0=已結清,狀態檔不存在也算 2=存取庫或 PR 編號格式錯誤
|
||||||
|
# wp-claim 0=已記下 2=存取庫格式錯誤、工作包代號不帶數字、寫不進領取檔
|
||||||
|
# wp-unclaim 0=已交回,領取檔不存在也算 2=存取庫格式錯誤
|
||||||
|
# wp-report 0=永遠成功,沒有未結清就不印
|
||||||
|
# wp-check prompt 0=永遠放行,只注入提醒
|
||||||
|
# wp-check skill 0=放行 2=擋下該次技能呼叫。安靜放行:逃生門 JSC_WP_GATE=off、沒有未結清
|
||||||
|
# 的工作包、取不到技能名、技能是 plan 或 implement、其餘不在名單上的技能。
|
||||||
|
# 只有 analyze 與 maintain 會 exit 2
|
||||||
|
# 不認得的子命令 0=安靜放行
|
||||||
|
# 註:本檔以 `. "$HERE/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會就地結束
|
||||||
|
# 並回 2;check 接在 UserPromptSubmit、wp-check skill 接在 PreToolUse,那一下都是無聲擋人,
|
||||||
|
# 上面那些放行路徑一條都跑不到。
|
||||||
#
|
#
|
||||||
# 鎖檔一個工作包一支($JSC_HOME/wp/{owner}-{repo}-{index}.pr),不是整個存取庫共用一支:
|
# 鎖檔一個工作包一支($JSC_HOME/wp/{owner}-{repo}-{index}.pr),不是整個存取庫共用一支:
|
||||||
# SDLC 實作可能同時有好幾個互不相依的工作包平行進行,各自開各自的 PR。整庫共用一支鎖檔
|
# SDLC 實作可能同時有好幾個互不相依的工作包平行進行,各自開各自的 PR。整庫共用一支鎖檔
|
||||||
@@ -42,6 +64,15 @@
|
|||||||
# 跟「implement 挑下一個工作包能不能挑到某一包」是兩件事——後者的判斷依據是該包在分析頁
|
# 跟「implement 挑下一個工作包能不能挑到某一包」是兩件事——後者的判斷依據是該包在分析頁
|
||||||
# WBS 表的相依欄,走 jsc-sdlc/tools/wp-gate.sh check-deps,不靠這支鎖檔。
|
# WBS 表的相依欄,走 jsc-sdlc/tools/wp-gate.sh check-deps,不靠這支鎖檔。
|
||||||
#
|
#
|
||||||
|
# --- plan 已從擋人名單移出,被放棄的保護寫在這裡 ---
|
||||||
|
#
|
||||||
|
# plan 原本跟 analyze、maintain 一起被擋。現在改成只提醒、照樣放行,放棄的是「在製品上限」:
|
||||||
|
# 手上的工作包還沒結清就不准開新計畫。放棄的理由有兩個。一是 plan 是純邏輯階段,產出是計畫頁,
|
||||||
|
# 不碰程式碼,開一份新計畫不會動到那支未結清的 PR。二是這道閘門手上只有「這個存取庫有 PR
|
||||||
|
# 未合併」這一個事實,判不出新計畫跟那支 PR 有沒有關聯,擋下去多半是誤擋。
|
||||||
|
# 代價要據實看待:沒有東西再擋住計畫越積越多,計畫的產出速度可以快過實作。
|
||||||
|
# analyze 與 maintain 兩道仍在,上限只是晚一個階段才生效。
|
||||||
|
#
|
||||||
# --- 狀態檔格式(與 jsc-sdlc/tools/wp-gate.sh 對齊,兩邊都靠這段註解對格式) ---
|
# --- 狀態檔格式(與 jsc-sdlc/tools/wp-gate.sh 對齊,兩邊都靠這段註解對格式) ---
|
||||||
#
|
#
|
||||||
# 兩種檔案都放在 $JSC_HOME/wp/ 底下,都是純文字 key=value,一行一欄位,順序不拘,
|
# 兩種檔案都放在 $JSC_HOME/wp/ 底下,都是純文字 key=value,一行一欄位,順序不拘,
|
||||||
@@ -428,11 +459,17 @@ WP_PENDING_EOF
|
|||||||
# 帶前綴(jsc-sdlc:plan)與裸名(plan)都要認。
|
# 帶前綴(jsc-sdlc:plan)與裸名(plan)都要認。
|
||||||
sname=${skill##*:}
|
sname=${skill##*:}
|
||||||
case "$sname" in
|
case "$sname" in
|
||||||
plan|analyze|maintain)
|
analyze|maintain)
|
||||||
echo "[jsc][工作包閘門][ERR]:${brief} PR 尚未合併,禁止在此存取庫執行「${sname}」。${foreign:+其中 ${foreign}還不屬於領取中的工作包,更不該由這裡處理。}請先把該 PR 結清(合併或關閉),或執行 jsc-hooks/hooks/sdlc-gate.sh wp-unlock {owner}/{repo} {index} 解除;確定要整體放行請設 JSC_WP_GATE=off。本次技能呼叫已擋下。" >&2
|
echo "[jsc][工作包閘門][ERR]:${brief} PR 尚未合併,禁止在此存取庫執行「${sname}」。${foreign:+其中 ${foreign}還不屬於領取中的工作包,更不該由這裡處理。}請先把該 PR 結清(合併或關閉),或執行 jsc-hooks/hooks/sdlc-gate.sh wp-unlock {owner}/{repo} {index} 解除;確定要整體放行請設 JSC_WP_GATE=off。本次技能呼叫已擋下。" >&2
|
||||||
exit 2 ;;
|
exit 2 ;;
|
||||||
|
plan)
|
||||||
|
# plan 只提醒不擋,理由見檔頭「plan 已從擋人名單移出」。放棄的是在製品上限,
|
||||||
|
# 換來的是不再誤擋跟那支 PR 無關的新計畫;analyze 與 maintain 兩道仍在。
|
||||||
|
echo "[jsc][工作包閘門]:${brief} PR 尚未合併。plan 是純邏輯階段、不碰程式碼,這裡只提醒不擋,但新計畫排進實作前請先把它結清。"
|
||||||
|
[ -n "$foreign" ] && echo "[jsc][工作包閘門]:${foreign}不屬於這個存取庫領取中的工作包。那幾支交給領取它的工作階段結清:這裡不要改它的程式碼,也不要回它的留言。"
|
||||||
|
exit 0 ;;
|
||||||
implement)
|
implement)
|
||||||
# implement 一律放行,連別包的 PR 未結清也放行:結清 PR 正是 implement 步驟 4 要做
|
# implement 一律放行,連別包的 PR 未結清也放行:結清 PR 正是 implement 步驟 2 要做
|
||||||
# 的事,擋掉就沒有任何路徑能解除這道鎖。領取檔不綁工作階段,擋下去連領取那一包的
|
# 的事,擋掉就沒有任何路徑能解除這道鎖。領取檔不綁工作階段,擋下去連領取那一包的
|
||||||
# 工作階段都會被自己的舊紀錄擋住,等於把流程鎖死。所以這裡只注入歸屬提醒。
|
# 工作階段都會被自己的舊紀錄擋住,等於把流程鎖死。所以這裡只注入歸屬提醒。
|
||||||
[ -n "$foreign" ] && echo "[jsc][工作包閘門]:${foreign}不屬於這個存取庫領取中的工作包。這裡只結清自己領取那一包的 PR${mine:+(${mine})};別包的 PR 不要改、留言也不要回,交給領取它的工作階段。"
|
[ -n "$foreign" ] && echo "[jsc][工作包閘門]:${foreign}不屬於這個存取庫領取中的工作包。這裡只結清自己領取那一包的 PR${mine:+(${mine})};別包的 PR 不要改、留言也不要回,交給領取它的工作階段。"
|
||||||
|
|||||||
+62
-4
@@ -3,6 +3,17 @@
|
|||||||
#
|
#
|
||||||
# 本機版本落後遠端發佈版本時擋下該次技能呼叫,並提示更新指令。
|
# 本機版本落後遠端發佈版本時擋下該次技能呼叫,並提示更新指令。
|
||||||
#
|
#
|
||||||
|
# 結束碼(hook 模式):0=放行 2=擋下該次技能呼叫,訊息走 stderr。
|
||||||
|
# 安靜放行(exit 0)的情況要記清楚,這道閘門絕大多數時候走的是這幾條:逃生門
|
||||||
|
# JSC_VERSION_GUARD=off、工具名取得到但不是 Skill、取不到技能名、技能名不是
|
||||||
|
# jsc-{domain}:{name}、拆不出 domain、命中下方豁免清單那 7 支、讀不到本機實際載入版本、
|
||||||
|
# 推導不出遠端站台、查不到遠端版本、本機版本等於或超前遠端。
|
||||||
|
# 只有「本機落後遠端」這一條會 exit 2。
|
||||||
|
# 結束碼(report、recommend):0=永遠成功,只讀不擋。結論看 stdout,不看結束碼。
|
||||||
|
# 註:本檔以 `. "$HERE/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會就地
|
||||||
|
# 結束並回 2,接在 PreToolUse 上就是無聲擋下每一次技能呼叫,上面那些放行路徑一條都跑不到
|
||||||
|
# (write-guard.sh 踩過這個坑)。部署時要確認 hooks/lib.sh 跟這支腳本一起裝上。
|
||||||
|
#
|
||||||
# 輸入:stdin JSON(Claude 格式)或環境變數,兩者都收。
|
# 輸入:stdin JSON(Claude 格式)或環境變數,兩者都收。
|
||||||
# 工具名 JSC_TOOL_NAME、TOOL_NAME、stdin 的 tool_name
|
# 工具名 JSC_TOOL_NAME、TOOL_NAME、stdin 的 tool_name
|
||||||
# 技能名 JSC_SKILL、SKILL、stdin 的 skill
|
# 技能名 JSC_SKILL、SKILL、stdin 的 skill
|
||||||
@@ -30,6 +41,8 @@
|
|||||||
# deploy 卡在問不出模式那一步,跟直接擋 deploy 是同一種死鎖
|
# deploy 卡在問不出模式那一步,跟直接擋 deploy 是同一種死鎖
|
||||||
# jsc-gitea:wiki jsc-ask:ask 問完一定寫回 wiki 才算完成,理由同上一條,
|
# jsc-gitea:wiki jsc-ask:ask 問完一定寫回 wiki 才算完成,理由同上一條,
|
||||||
# 擋在這一步一樣是 deploy 做不完
|
# 擋在這一步一樣是 deploy 做不完
|
||||||
|
# jsc-hooks:repair hook 壞掉時唯一的修復路徑。修 hook 的技能被 hook 擋下,
|
||||||
|
# 就沒有任何方法把 hook 修回來,跟直接擋 deploy 是同一種死鎖
|
||||||
#
|
#
|
||||||
# 逃生門:JSC_VERSION_GUARD=off 完全略過檢查(離線工作時用)。
|
# 逃生門:JSC_VERSION_GUARD=off 完全略過檢查(離線工作時用)。
|
||||||
#
|
#
|
||||||
@@ -37,13 +50,26 @@
|
|||||||
# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。hook 與 report 在同一支 CLI 內共用。
|
# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。hook 與 report 在同一支 CLI 內共用。
|
||||||
# 舊路徑 $JSC_HOME/version-cache/{domain} 會在第一次讀取時複製到當前 CLI 的新路徑。
|
# 舊路徑 $JSC_HOME/version-cache/{domain} 會在第一次讀取時複製到當前 CLI 的新路徑。
|
||||||
#
|
#
|
||||||
# 另有一個非 hook 的子指令:
|
# 另有兩個非 hook 的子指令:
|
||||||
# version-guard.sh report 把每個已安裝 jsc-* plugin 的版本比對印成 TSV,每行
|
# version-guard.sh report 把每個已安裝 jsc-* plugin 的版本比對印成 TSV,每行
|
||||||
# 「{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}」,
|
# 「{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}」,
|
||||||
# 最後一行「behind<TAB>{落後個數}」。供 jsc-cli:deploy 判斷要不要
|
# 最後一行「behind<TAB>{落後個數}」。供 jsc-cli:deploy 判斷要不要
|
||||||
# 把「更新」設成推薦選項。report 只讀不擋,永遠 exit 0。
|
# 把「更新」設成推薦選項。report 只讀不擋,永遠 exit 0。
|
||||||
# 本機沒有 Claude 的 plugin 註冊檔時改印「noregistry<TAB>{路徑}」
|
# 本機沒有 Claude 的 plugin 註冊檔時改印「noregistry<TAB>{路徑}」
|
||||||
# 再接 behind 0:那代表這台機器無法做版本檢查,跟「全部最新」是兩件事。
|
# 再接 behind 0:那代表這台機器無法做版本檢查,跟「全部最新」是兩件事。
|
||||||
|
#
|
||||||
|
# version-guard.sh recommend
|
||||||
|
# 把 report 那張表收斂成一個結論,只印一行、只有這一種格式:
|
||||||
|
# recommend<TAB>update|none|unverifiable
|
||||||
|
# 判定規則(呼叫端不必自己再判一次):
|
||||||
|
# update 任何一個 plugin 落後就是 update,一個就夠,不等多數
|
||||||
|
# unverifiable 查不到註冊資訊(noregistry),或一列 domain 都沒有,
|
||||||
|
# 或每一列都是查詢失敗——沒有任何一項查得到的證據
|
||||||
|
# none 至少有一列查得到結果,而且沒有任何一項落後
|
||||||
|
# 查詢失敗的那幾列不計入:查不到不等於最新,也不等於落後,只是沒有證據。
|
||||||
|
# 輸出格式必須穩定,別的 domain 的技能直接讀第二欄;證據表要另外看的話
|
||||||
|
# 再呼叫一次 report,這裡刻意不混印,免得 cut 取值被表格內容打亂。
|
||||||
|
# recommend 只讀不擋,永遠 exit 0:判定結果只看那一行的第二欄。
|
||||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||||
|
|
||||||
REG="$HOME/.claude/plugins/installed_plugins.json"
|
REG="$HOME/.claude/plugins/installed_plugins.json"
|
||||||
@@ -165,7 +191,9 @@ ver_cmp() { # $1=版本 A $2=版本 B
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── report:一次比對所有已安裝的 jsc plugin(非 hook 模式,不讀 stdin)
|
# ── report:一次比對所有已安裝的 jsc plugin(非 hook 模式,不讀 stdin)
|
||||||
if [ "${1:-}" = "report" ]; then
|
# 抽成函式是為了讓 recommend 讀同一份輸出。判定規則只寫在這一支腳本裡,recommend 直接解析
|
||||||
|
# 這裡印出來的表;兩邊各實作一次比對邏輯就會漂移,結論與證據對不起來。
|
||||||
|
do_report() {
|
||||||
# 註冊檔不存在或讀不到就明講。這裡不能只印 behind 0:呼叫端會把它讀成「都是最新」,
|
# 註冊檔不存在或讀不到就明講。這裡不能只印 behind 0:呼叫端會把它讀成「都是最新」,
|
||||||
# 於是把「這台機器無法做版本檢查」誤報成「不用更新」。
|
# 於是把「這台機器無法做版本檢查」誤報成「不用更新」。
|
||||||
# 舊版用 `tr -d '\n' < "$REG" 2>/dev/null`,那個 2>/dev/null 只蓋住 tr 的 stderr,
|
# 舊版用 `tr -d '\n' < "$REG" 2>/dev/null`,那個 2>/dev/null 只蓋住 tr 的 stderr,
|
||||||
@@ -173,7 +201,7 @@ if [ "${1:-}" = "report" ]; then
|
|||||||
if [ ! -f "$REG" ] || [ ! -r "$REG" ]; then
|
if [ ! -f "$REG" ] || [ ! -r "$REG" ]; then
|
||||||
printf 'noregistry\t%s\n' "$REG"
|
printf 'noregistry\t%s\n' "$REG"
|
||||||
printf 'behind\t0\n'
|
printf 'behind\t0\n'
|
||||||
exit 0
|
return 0
|
||||||
fi
|
fi
|
||||||
ho=$(remote_host_owner)
|
ho=$(remote_host_owner)
|
||||||
r_host=$(printf '%s' "$ho" | cut -d' ' -f1)
|
r_host=$(printf '%s' "$ho" | cut -d' ' -f1)
|
||||||
@@ -197,6 +225,36 @@ if [ "${1:-}" = "report" ]; then
|
|||||||
printf '%s\t%s\t%s\t%s\n' "$d" "${lv:-?}" "${rv:-?}" "$st"
|
printf '%s\t%s\t%s\t%s\n' "$d" "${lv:-?}" "${rv:-?}" "$st"
|
||||||
done
|
done
|
||||||
printf 'behind\t%s\n' "$behind"
|
printf 'behind\t%s\n' "$behind"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${1:-}" = "report" ]; then
|
||||||
|
do_report
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── recommend:把 report 那張表收斂成一個結論(非 hook 模式,不讀 stdin)
|
||||||
|
# 規則的唯一來源就是這一段,jsc-cli:deploy 只讀第二欄,不再自己解那張表。
|
||||||
|
if [ "${1:-}" = "recommend" ]; then
|
||||||
|
rep=$(do_report)
|
||||||
|
verdict=none
|
||||||
|
if printf '%s\n' "$rep" | grep -q '^noregistry '; then
|
||||||
|
# 沒有本機註冊檔,一項都比不了。這跟「全部最新」是兩件事,不能推薦 none。
|
||||||
|
verdict=unverifiable
|
||||||
|
else
|
||||||
|
behind_n=$(printf '%s\n' "$rep" | sed -n 's/^behind //p' | head -n1)
|
||||||
|
# 查得到結果的列:狀態欄是落後、最新或超前三種之一。查詢失敗那幾列不算證據。
|
||||||
|
known=$(printf '%s\n' "$rep" | awk -F'\t' '$1 != "behind" && $1 != "noregistry" && ($4 == "落後" || $4 == "最新" || $4 == "超前")' | wc -l | tr -d ' ')
|
||||||
|
if [ -n "$behind_n" ] && [ "$behind_n" -gt 0 ] 2>/dev/null; then
|
||||||
|
verdict=update
|
||||||
|
elif [ "${known:-0}" -gt 0 ] 2>/dev/null; then
|
||||||
|
verdict=none
|
||||||
|
else
|
||||||
|
# 一列 domain 都沒有,或每一列都查詢失敗:兩種都是「沒有任何查得到的證據」。
|
||||||
|
verdict=unverifiable
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf 'recommend\t%s\n' "$verdict"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -225,7 +283,7 @@ domain=${domain%%:*}
|
|||||||
|
|
||||||
# 豁免清單
|
# 豁免清單
|
||||||
case "$skill" in
|
case "$skill" in
|
||||||
jsc-cli:deploy|jsc-hooks:hooks-install|jsc-cli:models|jsc-meta:*|jsc-ask:ask|jsc-gitea:wiki) exit 0 ;;
|
jsc-cli:deploy|jsc-hooks:hooks-install|jsc-hooks:repair|jsc-cli:models|jsc-meta:*|jsc-ask:ask|jsc-gitea:wiki) exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# 更新指令依實際 CLI 給。印別的 CLI 的指令等於沒給指令,使用者照著打只會失敗。
|
# 更新指令依實際 CLI 給。印別的 CLI 的指令等於沒給指令,使用者照著打只會失敗。
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
---
|
---
|
||||||
name: hooks-install
|
name: hooks-install
|
||||||
description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SDLC model gate, plugin version guard, post-deploy restart gate, comment scope scanner, language guard) into every installed AI CLI, purging all pre-existing hooks first — third-party ones included, backed up before removal. Drive it per CLI through tools/wire-cli.sh purge, tools/wire-cli.sh, tools/wire-cli.sh smoke and tools/scan-hook-errors.sh. Hand any hook error, wiring or runtime, to jsc-hooks:repair, which must finish with a PR against develop; aborting the rest of the install to start that repair is allowed. Use after installing or updating the jsc plugin set; not for writing new hooks.
|
description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SDLC model gate, plugin version guard, post-deploy restart gate, comment scope scanner, language guard, write and commit guard) into every installed AI CLI, purging all pre-existing hooks first — third-party ones included, backed up before removal. Drive it per CLI through tools/wire-cli.sh purge, tools/wire-cli.sh, tools/wire-cli.sh status, tools/wire-cli.sh smoke and tools/scan-hook-errors.sh. Hand any hook error, wiring or runtime, to jsc-hooks:repair, which must finish with a PR against develop; aborting the rest of the install to start that repair is allowed. Use after installing or updating the jsc plugin set; not for writing new hooks.
|
||||||
---
|
---
|
||||||
|
|
||||||
# hooks-install — wire jsc hooks into every installed CLI
|
# hooks-install — wire jsc hooks into every installed CLI
|
||||||
|
|
||||||
Goal: make the eight hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`, `version-guard.sh`, `restart-gate.sh`, `comment-scope.sh`, `lang-guard.sh`) effective in every CLI, with nothing else wired alongside them.
|
Goal: make the nine hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`, `version-guard.sh`, `restart-gate.sh`, `comment-scope.sh`, `lang-guard.sh`, `write-guard.sh`) effective in every CLI, with nothing else wired alongside them.
|
||||||
|
|
||||||
Install on a clean slate. Every CLI is purged of all hooks first, third-party ones included, so a later failure has exactly one owner. `tools/wire-cli.sh purge` backs up every file it touches before it removes anything, so the removal stays reversible.
|
Install on a clean slate. Every CLI is purged of all hooks first, third-party ones included, so a later failure has exactly one owner. `tools/wire-cli.sh purge` backs up every file it touches before it removes anything, so the removal stays reversible.
|
||||||
|
|
||||||
The wiring commands stored in user config use `$JSC_HOME/current/jsc-hooks`, not the versioned plugin cache path and not the development checkout. `tools/wire-cli.sh {cli}` creates or refreshes that symlink before it writes `notify`, shell aliases or Kiro hook JSON, then verifies the linked scripts exist. If the filesystem cannot create the symlink, the script must say so and explicitly fall back to the current root; it must never write a silent broken path. 外掛提供的 `hooks/hooks.json` 也必須遵守同一條規則:主機有提供 `${CLAUDE_PLUGIN_ROOT}` 時才使用它;其他 CLI 讀取同一份 manifest 時,必須退回 `$JSC_HOME/current/jsc-hooks`,避免 Claude 專用變數未設定時展開成 `/hooks/...`。
|
The wiring commands stored in user config use `$JSC_HOME/current/jsc-hooks`, not the versioned plugin cache path and not the development checkout. `tools/wire-cli.sh {cli}` creates or refreshes that symlink before it writes `notify`, shell aliases or Kiro hook JSON, then verifies the linked scripts exist. If the filesystem cannot create the symlink, the script must say so and explicitly fall back to the current root; it must never write a silent broken path. The bundled `hooks/hooks.json` follows the same rule: use `${CLAUDE_PLUGIN_ROOT}` only where the host provides it, and fall back to `$JSC_HOME/current/jsc-hooks` for any other CLI reading the same manifest, so an unset Claude-only variable never expands into `/hooks/...`.
|
||||||
|
|
||||||
Only claude has PreToolUse, PostToolUse and UserPromptSubmit, so only claude reports `wired`. On codex, copilot, antigravity and kiro neither the version guard nor the post-deploy restart gate can 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. On those four the restart gate blocks no skill call whatsoever: the state file is still written and still cleared at the next session start, so the restart itself rests on the `jsc-cli:deploy` closing message.
|
Only claude has PreToolUse, PostToolUse and UserPromptSubmit, so only claude reports `wired`. On codex, copilot, antigravity and kiro neither the version guard, the post-deploy restart gate nor any mode of the write and commit guard can 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. On those four the restart gate blocks no skill call whatsoever: the state file is still written and still cleared at the next session start, so the restart itself rests on the `jsc-cli:deploy` closing message.
|
||||||
|
|
||||||
`comment-scope.sh` and `lang-guard.sh` both reach all five, wired at the same set of places, but on a different event and at a different moment each. Report the timing per CLI; never state it as one uniform behaviour:
|
`comment-scope.sh` and `lang-guard.sh` both reach all five, wired at the same set of places, but on a different event and at a different moment each. Report the timing per CLI; never state it as one uniform behaviour:
|
||||||
|
|
||||||
@@ -32,23 +32,29 @@ The detailed flow **MUST run as a sub agent**; the main agent only reports the s
|
|||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Run `jsc-cli/tools/detect-clis.sh`. Done when you hold the list of installed CLIs; when the list is empty, report that and stop.
|
1. Take the CLI list from the caller when it hands one over — `jsc-cli:deploy` passes the list it already detected, and probing the same five executables a second time buys nothing. Run `jsc-cli/tools/detect-clis.sh` yourself only when no list came in; that fallback is what keeps this skill usable when it is called on its own. The script always exits 0 and prints one `name<TAB>path<TAB>version` line per installed CLI. Done when you hold that list and have said which of the two ways produced it; when it is empty, report that no CLI was detected and stop.
|
||||||
2. For each installed CLI, run `tools/wire-cli.sh purge {cli}`. The script backs up every file it touches, removes all hooks, re-reads each file to confirm the removal, and restores the backup by itself when a check fails. Marker matching trims leading and trailing whitespace, so an indented or padded marker block is still removed as the same jsc-owned block. 對 Codex 而言,後續 `status` 也必須回報已安裝的 `jsc-hooks` manifest 是否仍含有舊版 `UserPromptSubmit` command,因為它可能把 `${CLAUDE_PLUGIN_ROOT}` 展開成 `/hooks/...`。Done when every CLI has printed exactly one `status=purged|skipped|failed reason=...` line and you have noted the backup directory path from its `[jsc]` output.
|
2. Run the five-stage pipeline **purge → wire → status → smoke → scan** once per detected CLI. Run the first CLI's pipeline on its own, because `tools/wire-cli.sh {cli}` is what refreshes the shared `$JSC_HOME/current/jsc-hooks` link and two CLIs must not rewrite it at the same time; once that first pipeline has finished, run every remaining CLI's pipeline in parallel, one sub agent per CLI — the five stages of one CLI stay in this order, but different CLIs touch different config files and share nothing else. Every stage prints its verdict on its first line, so read that line and never infer the outcome from the prose below it.
|
||||||
3. For each installed CLI, run `tools/wire-cli.sh {cli}`. The script owns both the wiring and its verification: it refreshes `$JSC_HOME/current/jsc-hooks`, writes the config, alias or hook file inside a `<!-- jsc-hooks -->` (or `# jsc-hooks`) marker block, re-reads every file it wrote, confirms the block is present and correctly placed, and confirms the stored runtime paths resolve to existing scripts 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.
|
1. `tools/wire-cli.sh purge {cli}` — backs up every file it touches, removes all hooks, re-reads each file to confirm the removal, and restores the backup by itself when a check fails. Marker matching trims leading and trailing whitespace, so an indented or padded marker block is still removed as the same jsc-owned block. Exit 0 is `purged`, exit 3 is `skipped` (that CLI's executable is not on this machine, so skip its remaining stages too), exit 4 is `failed` and goes to step 3. Exit 2 is a bad CLI name, not a purge outcome — fix the name and rerun the stage.
|
||||||
4. For each installed CLI, run `tools/wire-cli.sh smoke {cli}`. This runs all eight hooks once each, every wired mode included, plus each decision path of the work-package check and of the restart gate, and catches what the wiring check cannot see: a hook that is wired correctly and still fails when it executes. Done when every CLI has printed one `status=ok|failed reason=...` line plus 31 result lines: 14 hook mode lines, 5 work-package decision lines, and 12 restart-gate decision and cleanup lines. The line count is higher than the hook count because `sdlc-gate.sh`, `comment-scope.sh` and `lang-guard.sh` each have multiple wired modes.
|
2. `tools/wire-cli.sh {cli}` — owns both the wiring and its verification: it refreshes the link, writes the config, alias or hook file inside a `<!-- jsc-hooks -->` (or `# jsc-hooks`) marker block, re-reads every file it wrote, confirms the block is present and correctly placed, and confirms the stored runtime paths resolve to existing scripts before it prints a success status. Exit 0 is `wired`, exit 1 is `degraded` and is the expected result on the four non-claude CLIs, exit 3 is `skipped`, exit 4 is `failed` and goes to step 3. Exit 2 is a bad CLI name — fix the name and rerun.
|
||||||
5. For each installed CLI, run `tools/scan-hook-errors.sh --cli {cli}`. Only claude keeps hook results in its native records and can answer `clean` or `errors`; codex, copilot, antigravity and kiro answer `unavailable`, and their runtime evidence comes from step 4 alone. Done when every CLI has printed one `status=clean|errors|unavailable reason=...` line and the four `unavailable` CLIs are reported as exactly that, not as clean.
|
3. `tools/wire-cli.sh status {cli}` — the read-only inventory of what the previous stage wrote. It writes nothing and runs no hook, so it is safe to run right after wiring. Exit 0 is `wired`, exit 1 is `degraded`, exit 3 is `skipped`, exit 5 is `unwired`, which names every missing item and means the wiring stage has to run again before you continue. Exit 2 is a bad CLI name. For codex this stage is the only one that reads the installed `jsc-hooks` manifest in the Codex plugin cache and reports a stale `UserPromptSubmit` command there, the one that expands `${CLAUDE_PLUGIN_ROOT}` into `/hooks/...`; carry that item into the report.
|
||||||
6. For each error — `purge` failed, wiring failed, smoke failed, or a scanned error with `jsc=true` — run `tools/report-error.sh --hook {script name} --exit {code} --summary "{reason}" --cli {cli}` with the script's `[jsc]` output on stdin, then hand the failure to `jsc-hooks:repair`, which **MUST run as a sub agent** and must finish by opening a PR against `develop`. Aborting the remaining installs here is allowed as long as the repair starts. A scanned error with `jsc=false` belongs to a third-party hook: report it and leave it alone. Done when each error has either an `ERROR_{HASH}` page name on stdout, or an empty exit 0 meaning `JSC_WIKI_REPO_ERROR` and `JSC_WIKI_REPO` are both unset — in that second case carry the reason into step 7 instead. Skip this step when every CLI passed all four checks.
|
4. `tools/wire-cli.sh smoke {cli}` — runs every wired mode of all nine hooks once, plus each decision path of the work-package check, of the restart gate and of the write and commit guard. It catches what the wiring check cannot see: a hook that is wired correctly and still fails when it executes. It prints its own result-line count as `lines<TAB>{count}` and asserts that count against what it expected to run, so read the number from that line and never restate a number of your own. Exit 0 is `ok`, exit 4 is `failed` — either a hook errored or the line count did not match, and both go to step 3. Exit 2 is a bad CLI name.
|
||||||
7. Report four results per CLI — purge, wiring, smoke, scan — each with the reason its script printed, plus any `ERROR_{HASH}` page name and repair PR URL. Done when every detected CLI has exactly one status per check and every repair has a PR against `develop`.
|
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, and the four non-claude CLIs are reported as `unavailable` rather than clean.
|
||||||
|
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. Exit 0 with an `ERROR_{HASH}` page name and URL on stdout means the page was written; exit 0 with empty output means `JSC_WIKI_REPO_ERROR` and `JSC_WIKI_REPO` are both unset, so carry that reason 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 and URL, 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`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Every hook script accepts both stdin JSON and environment variables (`JSC_CLI`, `JSC_SESSION_ID`, `JSC_SKILL`, `JSC_TOOL_NAME`, `JSC_MODEL`); `jsc-wrap.sh` sets the first two itself.
|
- Every hook script accepts both stdin JSON and environment variables (`JSC_CLI`, `JSC_SESSION_ID`, `JSC_SKILL`, `JSC_TOOL_NAME`, `JSC_TOOL_COMMAND`, `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. `start` and `restart` also clear the restart gate whenever they decide this SessionStart is a new session, so the wiring of those two events is what lowers the gate after a restart — a CLI wired without them keeps the gate up until the user sets `JSC_RESTART_GATE=off`.
|
- `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. `start` and `restart` also clear the restart gate whenever they decide this SessionStart is a new session, so the wiring of those two events is what lowers the gate after a restart — a CLI wired without them keeps the gate up until the user sets `JSC_RESTART_GATE=off`.
|
||||||
- `restart-gate.sh` blocks jsc skill calls while `$JSC_HOME/restart-required.d/{cli}` exists — one file per CLI, named after the CLI code — so a freshly deployed skill set is not used by a process still running the old one. Each CLI reads only its own file: another CLI's file never blocks this one, and a restart clears only the file of the CLI that restarted. `jsc-cli:deploy` writes the current CLI's file through `restart-gate.sh require {install|update} [{domain}...]` at the end of an install or update; `restart-gate.sh report` prints one line per file, so it is visible which CLIs still owe a restart. A leftover old-format single file at `$JSC_HOME/restart-required` blocks every CLI and is deleted on the next `clear` — transitional only, and `hooks/restart-gate.sh` records when it can be dropped. Exempt skills stay callable — `jsc-cli:deploy`, `jsc-hooks:hooks-install`, `jsc-gitea:wiki`, `jsc-log:worklog`, `jsc-log:learn`, `jsc-meta:*`, `jsc-ask:ask`, `jsc-git:pr`, `jsc-git:commit` — because the change report and the worklog still have to be finished after a deploy, and the first six reach that finish line only through the last three: the deploy asks for its mode, the report closes with a PR. The gate matches skill names, not call chains, so a nested call to anything off the list is blocked all the same. `hooks/restart-gate.sh` owns the list; guidelines.md「部署後重啟閘門」carries the same nine with a reason per entry. Escape hatch: `JSC_RESTART_GATE=off`.
|
- `restart-gate.sh` blocks jsc skill calls while `$JSC_HOME/restart-required.d/{cli}` exists — one file per CLI, named after the CLI code — so a freshly deployed skill set is not used by a process still running the old one. Each CLI reads only its own file: another CLI's file never blocks this one, and a restart clears only the file of the CLI that restarted. `jsc-cli:deploy` writes the current CLI's file through `restart-gate.sh require {install|update} [{domain}...]` at the end of an install or update; `restart-gate.sh report` prints one line per file, so it is visible which CLIs still owe a restart. A leftover old-format single file at `$JSC_HOME/restart-required` blocks every CLI and is deleted on the next `clear` — transitional only, and `hooks/restart-gate.sh` records when it can be dropped. The gate matches skill names, not call chains, so a nested call to anything off the exemption list is blocked all the same; `hooks/restart-gate.sh` owns that list with a reason per entry, and `jsc-meta/references/guidelines.md`「部署後重啟閘門」carries the same list. Escape hatch: `JSC_RESTART_GATE=off`.
|
||||||
|
- `write-guard.sh` takes three blocking modes, wired on two PreToolUse matchers, so claude is the only CLI where any of it takes effect, plus a fourth mode, `release`, that is wired nowhere and is called by a skill itself. `stage` reads the stage lock that `sdlc-gate.sh` already owns and blocks `Write`, `Edit` and `MultiEdit` while `plan` or `analyze` holds it, because those two stages produce wiki pages rather than files. `review` reads the current skill — the environment variable first, then the record `skill-usage.sh` keeps — and blocks writes while `jsc-review:code-review` or `jsc-review:api-doc` runs, since both only report findings. It deliberately does **not** block `jsc-review:comment-cleanup`: that skill has to write, limited to comment lines, and deciding that limit needs per-language comment parsing of the whole proposed content, which would block legitimate cleanups more often than it caught bad ones — that boundary stays with the skill text and the later review. `commit` is wired on `Bash` and blocks a single command that stages everything and commits in one go, plus any commit message carrying simplified characters or mojibake, which it decides by calling `lang-guard.sh` rather than keeping a second word list. A `git add -A` split across two separate tool calls is not caught, on purpose: catching it needs cross-call state that the blocked operator has no way to clear. `release` deletes that recorded skill and always exits 0; `jsc-review:code-review` and `jsc-review:api-doc` call it once each as they hand their findings back. It exists because the record says which skill was loaded last, not which one is still running: both audit skills end by leaving the fixing to their caller, and without `release` every write that caller makes stays blocked for the whole TTL, with the escape hatch or a wait as the only way out — a gate must never lock away its own release. Escape hatch: `JSC_WRITE_GUARD=off`, which `release` ignores because clearing a record blocks nobody, plus `JSC_WRITE_GUARD_TTL` for how long a recorded skill counts as still running.
|
||||||
- `purge` reaches the user-level config only. Hooks that another plugin ships in its own `hooks.json` stay active, and uninstalling that plugin is the only way to clear them — say so when reporting, and treat their errors as third-party.
|
- `purge` reaches the user-level config only. Hooks that another plugin ships in its own `hooks.json` stay active, and uninstalling that plugin is the only way to clear them — say so when reporting, and treat their errors as third-party.
|
||||||
- Backups land in `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`, one directory per purge run, under the original file names. Hand that path to the user whenever a purge removed something.
|
- Backups land in `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`, one directory per purge run, under the original file names. Hand that path to the user whenever a purge removed something.
|
||||||
- `status claude` reads Claude Code's `installed_plugins.json` and checks the `installPath` that the CLI actually loads. It must not check only the `hooks.json` next to the `wire-cli.sh` that happens to be running, because a development checkout can otherwise hide a broken installed plugin.
|
- `status claude` reads Claude Code's `installed_plugins.json` and checks the `installPath` that the CLI actually loads. It must not check only the `hooks.json` next to the `wire-cli.sh` that happens to be running, because a development checkout can otherwise hide a broken installed plugin.
|
||||||
- `smoke` treats `sdlc-gate.sh check` exit 2 as healthy: that exit is the stage lock blocking a turn on purpose, not a runtime error. `comment-scope.sh` and `lang-guard.sh` exit 2 count as healthy for the same reason — the scan found something and warned about it. Their no-argument mode has no file name during smoke and exits 0 in silence; `sweep` depends on the worktree it runs in, so it answers 2 whenever that worktree happens to carry an offending comment, a simplified character or a mojibake sequence. None of these is a broken hook.
|
- Never set `JSC_READONLY=1` for this skill. `wire-cli.sh` refuses `purge` and wiring with exit 6 under that variable, which is exactly what a health check wants and exactly what an install must not have.
|
||||||
|
- `smoke` treats `sdlc-gate.sh check` exit 2 as healthy: that exit is the stage lock blocking a turn on purpose, not a runtime error. `comment-scope.sh`, `lang-guard.sh` and `write-guard.sh` exit 2 count as healthy for the same reason — the check found something and said so. Their no-argument mode has no file name during smoke and exits 0 in silence; `sweep` depends on the worktree it runs in, so it answers 2 whenever that worktree happens to carry an offending comment, a simplified character or a mojibake sequence, and `write-guard.sh` answers 2 whenever the machine happens to hold a `plan` stage lock or a recent audit skill. None of these is a broken hook.
|
||||||
- `comment-scope.sh` takes three modes: `prompt` (inject the rule summary at UserPromptSubmit), no argument at all (scan the file just written at PostToolUse, reading `file_path` from stdin JSON or `JSC_CHANGED_FILE`), and `sweep [dir]` (scan every file the git worktree changed, for the four CLIs with no post-tool hook). All scanning modes read only the lines a diff added, skip markdown and binary files, and turn off entirely with `JSC_COMMENT_SCOPE=off`. The rule text itself lives in one place only, `jsc-review`'s `references/comment-scope.md`; never restate the list anywhere in this repo.
|
- `comment-scope.sh` takes three modes: `prompt` (inject the rule summary at UserPromptSubmit), no argument at all (scan the file just written at PostToolUse, reading `file_path` from stdin JSON or `JSC_CHANGED_FILE`), and `sweep [dir]` (scan every file the git worktree changed, for the four CLIs with no post-tool hook). All scanning modes read only the lines a diff added, skip markdown and binary files, and turn off entirely with `JSC_COMMENT_SCOPE=off`. The rule text itself lives in one place only, `jsc-review`'s `references/comment-scope.md`; never restate the list anywhere in this repo.
|
||||||
- `lang-guard.sh` takes the same three modes as `comment-scope.sh` and is wired at the same places, but it scans differently on purpose: it reads the whole file rather than comment lines only, and it does scan `.md` and plain-text files, because those are exactly the non-code output the rule targets. It flags three things — simplified characters (word list in `hooks/simplified.txt`, the single source of truth for this repo; a missing list skips that check in silence), mojibake (U+FFFD and double-encoding remnants), and non-UTF-8 encoding (decided by `iconv`; no `iconv` skips that check). It skips binaries, generated files, and the three files whose subject is those very characters (`simplified.txt`, `ste100-guard.sh`, `lang-guard.sh`). Turn it off with `JSC_LANG_GUARD=off`. The rule text lives only in `jsc-meta`'s `references/ste100.md`.
|
- `lang-guard.sh` takes the same three modes as `comment-scope.sh` and is wired at the same places, but it scans differently on purpose: it reads the whole file rather than comment lines only, and it does scan `.md` and plain-text files, because those are exactly the non-code output the rule targets. It flags three things — simplified characters (word list in `hooks/simplified.txt`, the single source of truth for this repo; a missing list skips that check in silence), mojibake (U+FFFD and double-encoding remnants), and non-UTF-8 encoding (decided by `iconv`; no `iconv` skips that check). It skips binaries, generated files, and the three files whose subject is those very characters (`simplified.txt`, `ste100-guard.sh`, `lang-guard.sh`). Turn it off with `JSC_LANG_GUARD=off`. The rule text lives only in `jsc-meta`'s `references/ste100.md`.
|
||||||
- `jsc-wrap.sh` runs both sweeps after the CLI exits and always returns the CLI's own exit code. A `sweep` hit warns on stderr and changes nothing else — never let a language or comment warning turn a successful CLI run into a failed one.
|
- `jsc-wrap.sh` runs both sweeps after the CLI exits and always returns the CLI's own exit code. A `sweep` hit warns on stderr and changes nothing else — never let a language or comment warning turn a successful CLI run into a failed one.
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ description: Repair failed hook wiring by delegating diagnosis to installed AI a
|
|||||||
|
|
||||||
# repair — repair a failed hook
|
# repair — repair a failed hook
|
||||||
|
|
||||||
Single source of guidelines: [`../../references/guidelines.md`](../../references/guidelines.md).
|
Single source of guidelines: `jsc-meta`'s `references/guidelines.md`.
|
||||||
|
|
||||||
|
This skill is exempt from the version guard and the post-deploy restart gate, because it is the only path back from a broken hook. `hooks/version-guard.sh` and `hooks/restart-gate.sh` own those two exemption lists.
|
||||||
|
|
||||||
## Flow
|
## 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.
|
1. Read the failure context from `ERROR_{HASH}` through `jsc-gitea:wiki`, or from the failed `status=` line when no page was written. A wiki read that fails stops the skill: report which page could not be read and ask for the failure output instead of guessing. Done when the failure context names the script, the exit code and the CLI, and the PR base branch is fixed at `develop`.
|
||||||
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.
|
2. Run `jsc-cli/tools/detect-clis.sh`. It always exits 0 and prints one `name<TAB>path<TAB>version` line per installed CLI; empty output means no CLI is installed. Delegate diagnosis to one subagent per detected CLI — each **MUST run as a sub agent**, must receive the failure context, must stay read-only, and must return root cause, the files to change, and the verification command to run. Done when every detected CLI has returned one proposal, or the output was empty and the main agent has recorded that it diagnoses 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.
|
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 `../meta/tools/sync-skill-manifest.sh .`. Completion condition: the README skill list and all three manifests show the same new version.
|
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 develop`. Completion condition: a PR URL comes back and the repair is ready for review.
|
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.
|
||||||
|
|||||||
+33
-6
@@ -17,6 +17,15 @@
|
|||||||
# gitea.sh)時安靜降級:不輸出、exit 0。回報失敗不該再變成一次失敗。
|
# gitea.sh)時安靜降級:不輸出、exit 0。回報失敗不該再變成一次失敗。
|
||||||
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
||||||
#
|
#
|
||||||
|
# 結束碼: 0=已寫入 wiki 並印出「{頁名} {網址}」,或安靜降級(找不到 gitea.sh、解析不出
|
||||||
|
# wiki 存取庫、算不出 HASH、建不出暫存檔)——回報失敗不該再變成一次失敗
|
||||||
|
# 2=用法錯誤(缺 --hook 或 --summary)
|
||||||
|
# 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容
|
||||||
|
# 讀不回來(wiki-get 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;訊息走 stderr。
|
||||||
|
# 讀不回來就不寫,是為了不拿範本蓋掉一份還在的目錄頁
|
||||||
|
# 註: 本檔以 `. "$ROOT/hooks/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會
|
||||||
|
# 就地結束並回 2,跟用法錯誤同碼;分不出是哪一種時,先確認 hooks/lib.sh 在不在。
|
||||||
|
#
|
||||||
# 頁名:
|
# 頁名:
|
||||||
# ERROR_{HASH},HASH 取「{owner}/{repo} {hook} {時間}」的 SHA-1 前 8 碼(共用 hash 規則)。
|
# ERROR_{HASH},HASH 取「{owner}/{repo} {hook} {時間}」的 SHA-1 前 8 碼(共用 hash 規則)。
|
||||||
# 時間放進 hash:同一種失敗再發生時要另開新頁,不覆寫舊紀錄。
|
# 時間放進 hash:同一種失敗再發生時要另開新頁,不覆寫舊紀錄。
|
||||||
@@ -110,11 +119,7 @@ fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \
|
|||||||
row=$(printf '| %s | [[%s|%s]] | %s | %s | %s | %s |' \
|
row=$(printf '| %s | [[%s|%s]] | %s | %s | %s | %s |' \
|
||||||
"$ts" "$hook 異常 $ts" "$page" "$repo" "$hook" "$code" "$summary")
|
"$ts" "$hook 異常 $ts" "$page" "$repo" "$hook" "$code" "$summary")
|
||||||
|
|
||||||
# 目錄頁:已存在就把新列附在文末(最新一筆在最後);不存在就用範本建立
|
build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫
|
||||||
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 '{yyyy-MM-dd HH:mm:ss}' "$ts" < "$ROOT/templates/error-contents.md" \
|
||||||
| fill '{HASH}' "$hash" \
|
| fill '{HASH}' "$hash" \
|
||||||
| fill '{error title}' "$hook 異常 $ts" \
|
| fill '{error title}' "$hook 異常 $ts" \
|
||||||
@@ -122,12 +127,34 @@ else
|
|||||||
| fill '{hook_name}' "$hook" \
|
| fill '{hook_name}' "$hook" \
|
||||||
| fill '{exit_code}' "$code" \
|
| fill '{exit_code}' "$code" \
|
||||||
| fill '{error_summary}' "$summary" > "$tmp_list"
|
| fill '{error_summary}' "$summary" > "$tmp_list"
|
||||||
fi
|
}
|
||||||
|
|
||||||
|
# 異常目錄頁一律附加,不整頁覆蓋。頁上每一列都是別人回報的異常,本腳本沒有從別處讀過
|
||||||
|
# 那些列,所以先把舊頁讀回來、把新列附在文末(最新一筆在最後),再整頁寫回。
|
||||||
|
# 這個語意完全靠「讀得回舊內容」撐著,因此依 wiki-get 的結束碼分流:
|
||||||
|
# 0 → 讀到既有內容,附加新列(讀得到但整頁是空的,沒有既有列會被蓋掉,套範本才安全)
|
||||||
|
# 4 → 頁面真的還不存在,只有這個碼可以用範本建立新頁
|
||||||
|
# 7 → 金鑰失效或權限不足,舊內容未知,放棄目錄頁寫入
|
||||||
|
# 8 → 其他 API 失敗,舊內容一樣未知,處置同 7
|
||||||
|
# 為什麼 7 與 8 不能當成「頁面不存在」:拿範本蓋掉一份讀不回來的目錄頁,等於刪光所有既有
|
||||||
|
# 列,而 wiki-put 不做合併、也不留備份,蓋掉就救不回來。
|
||||||
|
sh "$gsh" wiki-get "$wrepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null
|
||||||
|
get_code=$?
|
||||||
|
contents_skip=''
|
||||||
|
case "$get_code" in
|
||||||
|
0) if [ -s "$tmp_list" ]; then printf '%s\n' "$row" >> "$tmp_list"; else build_contents; fi ;;
|
||||||
|
4) build_contents ;;
|
||||||
|
*) contents_skip=$get_code ;;
|
||||||
|
esac
|
||||||
|
|
||||||
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
|
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
|
||||||
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
|
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
if [ -n "$contents_skip" ]; then
|
||||||
|
echo "[jsc] 讀取 ERROR_CONTENTS 失敗($wrepo,wiki-get 結束碼 $contents_skip),舊內容未知,不寫目錄頁;$page 已建立。" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
if ! sh "$gsh" wiki-put "$wrepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then
|
if ! sh "$gsh" wiki-put "$wrepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then
|
||||||
echo "[jsc] 寫入 ERROR_CONTENTS 失敗($wrepo),$page 已建立。" >&2
|
echo "[jsc] 寫入 ERROR_CONTENTS 失敗($wrepo),$page 已建立。" >&2
|
||||||
exit 4
|
exit 4
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
#
|
#
|
||||||
# 產出: 每筆錯誤附加一行 JSON 到 $JSC_HOME/errors/hooks.jsonl(格式比照 hooks/skill-usage.sh):
|
# 產出: 每筆錯誤附加一行 JSON 到 $JSC_HOME/errors/hooks.jsonl(格式比照 hooks/skill-usage.sh):
|
||||||
# {ts,cli,hook,event,exit,detail,jsc}
|
# {ts,cli,hook,event,exit,detail,jsc}
|
||||||
# jsc 欄位:command 或 stderr 命中 ste100-guard.sh、session-timer.sh、skill-usage.sh、
|
# jsc 欄位:command 或 stderr 命中九支 hook 腳本任一支,或命中 jsc-hooks 路徑,就是 true,
|
||||||
# sdlc-gate.sh、version-guard.sh 任一支就是 true,否則 false。分得出來才用得上——
|
# 否則 false。分得出來才用得上——非 jsc 的 hook 錯誤不是 jsc 該修的,hooks-install 只回報、
|
||||||
# 非 jsc 的 hook 錯誤不是 jsc 該修的,hooks-install 只回報、不轉 jsc-hooks:repair。
|
# 不轉 jsc-hooks:repair。
|
||||||
#
|
#
|
||||||
# 輸出: 第一行 `status={clean|errors|unavailable} reason=...`(可供程式判讀),
|
# 輸出: 第一行 `status={clean|errors|unavailable} reason=...`(可供程式判讀),
|
||||||
# errors 時其後每筆一行人類可讀的繁中摘要(hook 名、退出碼、是否屬 jsc)。
|
# errors 時其後每筆一行人類可讀的繁中摘要(hook 名、退出碼、是否屬 jsc)。
|
||||||
@@ -52,7 +52,7 @@ case "$cli" in
|
|||||||
codex|copilot|antigravity|kiro)
|
codex|copilot|antigravity|kiro)
|
||||||
printf 'status=unavailable reason=%s\n' "$cli 沒有 hook 結果紀錄,執行期錯誤掃不到"
|
printf 'status=unavailable reason=%s\n' "$cli 沒有 hook 結果紀錄,執行期錯誤掃不到"
|
||||||
echo "[jsc] $cli:原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與 stderr。"
|
echo "[jsc] $cli:原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與 stderr。"
|
||||||
echo "[jsc] $cli:改跑 tools/wire-cli.sh smoke $cli,主動執行五支 hook 驗執行期。"
|
echo "[jsc] $cli:改跑 tools/wire-cli.sh smoke $cli,主動執行九支 hook 驗執行期。"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -92,10 +92,16 @@ extract_errors() {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
# 判定這筆錯誤是不是 jsc 自己的 hook。腳本名逐支列,再補一條 jsc-hooks 路徑判定:
|
||||||
|
# 接線寫進設定的命令一律走 $JSC_HOME/current/jsc-hooks,路徑本身就是證據,新增 hook 時
|
||||||
|
# 就算忘了補進下面這張清單也還認得出來。認錯邊的代價不對稱——漏認會把 jsc 的錯誤當成
|
||||||
|
# 第三方的,只回報不修正。
|
||||||
function is_jsc(t) {
|
function is_jsc(t) {
|
||||||
if (index(t, "ste100-guard.sh") || index(t, "session-timer.sh") \
|
if (index(t, "ste100-guard.sh") || index(t, "session-timer.sh") \
|
||||||
|| index(t, "skill-usage.sh") || index(t, "sdlc-gate.sh") \
|
|| index(t, "skill-usage.sh") || index(t, "sdlc-gate.sh") \
|
||||||
|| index(t, "version-guard.sh")) return "true"
|
|| index(t, "version-guard.sh") || index(t, "restart-gate.sh") \
|
||||||
|
|| index(t, "comment-scope.sh") || index(t, "lang-guard.sh") \
|
||||||
|
|| index(t, "write-guard.sh") || index(t, "jsc-hooks")) return "true"
|
||||||
return "false"
|
return "false"
|
||||||
}
|
}
|
||||||
# 摘要收斂成單行短字串:TSV 欄位不能有 tab,jsonl 欄位不能有裸換行;
|
# 摘要收斂成單行短字串:TSV 欄位不能有 tab,jsonl 欄位不能有裸換行;
|
||||||
|
|||||||
Reference in New Issue
Block a user