異常回報依 wiki 讀取的結束碼分流,只有頁面確定不存在才套範本建新頁。 版本閘門與重啟閘門的豁免清單各補上 hook 修復技能。 階段閘門把計畫階段移出擋人名單,改成只注入提醒。 錯誤掃描的自家 hook 判定補齊九支腳本,並加一條路徑判定。 修復技能與接線技能的內文改成真的走得到的路徑與真的存在的關卡數。 原本 wiki 讀取失敗會一路落到套範本那一步,金鑰失效或 API 出狀況時, 就拿一份空白範本蓋掉整份異常目錄,而寫入不做合併也不留備份,蓋掉就救不回來。 兩道閘門把唯一的 hook 修復路徑一起擋住,hook 一壞就沒有任何方法修回來, 閘門等於鎖掉解除自己的路徑。工作包閘門擋下計畫階段是誤擋: 計畫是純邏輯階段、不碰程式碼,而閘門只知道有 PR 未合併,判不出跟新計畫有沒有關聯。 自家 hook 判定只認得早期那五支,後來加的四支出錯會被當成第三方的,只回報不修正。 修復技能裡三個指向流程的路徑指到不存在的位置,照著走一定撲空; 接線流程寫四道關卡,實際上有五道,兩段中文說明也混在英文內文裡。 異常目錄改成先讀回舊頁、把新列附在文末、再整頁寫回;讀不回來就放棄寫目錄頁並回報, 寧可少一列索引,也不覆蓋別人的紀錄。兩份豁免清單各補一項,理由逐項寫在腳本檔頭。 計畫階段改印提醒後放行,放棄的在製品上限與代價一併寫在檔頭。 自家 hook 判定逐支列出腳本名,再加一條安裝路徑判定,日後新增 hook 忘了補清單也還認得出來。 三個路徑改指到擁有它的技能組,關卡數改成五道並逐關寫明結束碼,兩段中文說明改回英文。 版本閘門在同一次改動另補唯讀的建議子命令,把版本比對表收斂成一行結論, 部署技能不必自己再解一次那張表。
18 KiB
name, description
| name | description |
|---|---|
| hooks-install | 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
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.
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, 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:
| CLI | Scanning moment | Wired through |
|---|---|---|
| claude | Per file, the instant it is written | PostToolUse |
| codex | End of every turn, over the whole git worktree | notify in config.toml |
| kiro | On every prompt submit, over the whole git worktree — it sees what the previous turn wrote | userPromptSubmit in .kiro/hooks/jsc-hooks.json |
| copilot, antigravity | Once, when the session ends | tools/jsc-wrap.sh teardown |
The table above holds for both scanners. The sweep mode reads git diff HEAD, so its coverage matches what claude sees; only the feedback delay differs. Outside a git worktree sweep exits 0 in silence and nothing is scanned at all — say so when the user works outside git. Both prompt rule reminders still go into every rule file alongside the STE100 block, because a warning that arrives a turn late is worth less than not writing the offending text in the first place.
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.
Treat any hook error as repair work, whether it appeared while wiring or while running. Stopping the remaining installs to start that repair is the right call; leaving a broken hook wired is not.
The detailed flow MUST run as a sub agent; the main agent only reports the summary.
Steps
-
Take the CLI list from the caller when it hands one over —
jsc-cli:deploypasses the list it already detected, and probing the same five executables a second time buys nothing. Runjsc-cli/tools/detect-clis.shyourself 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 onename<TAB>path<TAB>versionline 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. -
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-hookslink 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.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 ispurged, exit 3 isskipped(that CLI's executable is not on this machine, so skip its remaining stages too), exit 4 isfailedand goes to step 3. Exit 2 is a bad CLI name, not a purge outcome — fix the name and rerun the stage.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 iswired, exit 1 isdegradedand is the expected result on the four non-claude CLIs, exit 3 isskipped, exit 4 isfailedand goes to step 3. Exit 2 is a bad CLI name — fix the name and rerun.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 iswired, exit 1 isdegraded, exit 3 isskipped, exit 5 isunwired, 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 installedjsc-hooksmanifest in the Codex plugin cache and reports a staleUserPromptSubmitcommand there, the one that expands${CLAUDE_PLUGIN_ROOT}into/hooks/...; carry that item into the report.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 aslines<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 isok, exit 4 isfailed— either a hook errored or the line count did not match, and both go to step 3. Exit 2 is a bad CLI name.tools/scan-hook-errors.sh --cli {cli}— only claude keeps hook results in its native records and can answercleanorerrors; codex, copilot, antigravity and kiro answerunavailable, and their runtime evidence comes from the smoke stage alone. Exit 0 covers bothcleanandunavailable, exit 1 iserrorsand every entry withjsc=truegoes 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
linescount matches its own assertion, and the four non-claude CLIs are reported asunavailablerather than clean. -
For each error — a failed purge, a failed wiring, an
unwiredstatus, a failed smoke, or a scanned error withjsc=true— runtools/report-error.sh --hook {script name} --exit {code} --summary "{reason}" --cli {cli}with the script's[jsc]output on stdin, then hand the failure tojsc-hooks:repair, which MUST run as a sub agent and must finish by opening a PR againstdevelop. Aborting the remaining installs here is allowed as long as the repair starts. Exit 0 with anERROR_{HASH}page name and URL on stdout means the page was written; exit 0 with empty output meansJSC_WIKI_REPO_ERRORandJSC_WIKI_REPOare both unset, so carry that reason into step 4 instead; exit 2 means the call itself was malformed —--hookor--summaryis 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-getexit 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 withjsc=falsebelongs 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 oneERROR_{HASH}result — a page name and URL, or the recorded reason no page was written — and one repair PR URL againstdevelop. -
Report five results per CLI — purge, wiring, status, smoke, scan — each with the reason its script printed, plus the smoke
linescount, anyERROR_{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 againstdevelop.
Notes
- 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.shsets the first two itself. session-timer.shtakesstart(keep an existing start time),restart(always overwrite it, for a CLI with no session id — kiro),markandreport.wire-cli.shpicks the right one per CLI; do not hand-edit the generated hook files.startandrestartalso 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 setsJSC_RESTART_GATE=off.restart-gate.shblocks 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:deploywrites the current CLI's file throughrestart-gate.sh require {install|update} [{domain}...]at the end of an install or update;restart-gate.sh reportprints one line per file, so it is visible which CLIs still owe a restart. A leftover old-format single file at$JSC_HOME/restart-requiredblocks every CLI and is deleted on the nextclear— transitional only, andhooks/restart-gate.shrecords 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.showns that list with a reason per entry, andjsc-meta/references/guidelines.md「部署後重啟閘門」carries the same list. Escape hatch:JSC_RESTART_GATE=off.write-guard.shtakes 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.stagereads the stage lock thatsdlc-gate.shalready owns and blocksWrite,EditandMultiEditwhileplanoranalyzeholds it, because those two stages produce wiki pages rather than files.reviewreads the current skill — the environment variable first, then the recordskill-usage.shkeeps — and blocks writes whilejsc-review:code-revieworjsc-review:api-docruns, since both only report findings. It deliberately does not blockjsc-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.commitis wired onBashand 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 callinglang-guard.shrather than keeping a second word list. Agit add -Asplit 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.releasedeletes that recorded skill and always exits 0;jsc-review:code-reviewandjsc-review:api-doccall 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 withoutreleaseevery 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, whichreleaseignores because clearing a record blocks nobody, plusJSC_WRITE_GUARD_TTLfor how long a recorded skill counts as still running.purgereaches the user-level config only. Hooks that another plugin ships in its ownhooks.jsonstay 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. status claudereads Claude Code'sinstalled_plugins.jsonand checks theinstallPaththat the CLI actually loads. It must not check only thehooks.jsonnext to thewire-cli.shthat happens to be running, because a development checkout can otherwise hide a broken installed plugin.- Never set
JSC_READONLY=1for this skill.wire-cli.shrefusespurgeand wiring with exit 6 under that variable, which is exactly what a health check wants and exactly what an install must not have. smoketreatssdlc-gate.sh checkexit 2 as healthy: that exit is the stage lock blocking a turn on purpose, not a runtime error.comment-scope.sh,lang-guard.shandwrite-guard.shexit 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;sweepdepends 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, andwrite-guard.shanswers 2 whenever the machine happens to hold aplanstage lock or a recent audit skill. None of these is a broken hook.comment-scope.shtakes three modes:prompt(inject the rule summary at UserPromptSubmit), no argument at all (scan the file just written at PostToolUse, readingfile_pathfrom stdin JSON orJSC_CHANGED_FILE), andsweep [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 withJSC_COMMENT_SCOPE=off. The rule text itself lives in one place only,jsc-review'sreferences/comment-scope.md; never restate the list anywhere in this repo.lang-guard.shtakes the same three modes ascomment-scope.shand 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.mdand plain-text files, because those are exactly the non-code output the rule targets. It flags three things — simplified characters (word list inhooks/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 byiconv; noiconvskips 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 withJSC_LANG_GUARD=off. The rule text lives only injsc-meta'sreferences/ste100.md.jsc-wrap.shruns both sweeps after the CLI exits and always returns the CLI's own exit code. Asweephit warns on stderr and changes nothing else — never let a language or comment warning turn a successful CLI run into a failed one.tools/report-error.shis 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 byjsc-log:worklogandjsc-log:stats.