現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就 中止的技能,在紀錄裡長得一模一樣。 start 由技能用量 hook 順手發,不必改技能文件。end 只能由技能自己在收尾 步驟寫——hook 接在技能工具呼叫上,而實際工作發生在之後的模型輪次,它在 原理上看不到成敗。有 start 沒有配對的 end,就是那一輪中止了。 status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜 跳過,回報失敗一律不改變技能自己的結論。
15 KiB
name, description
| name | description |
|---|---|
| pkg-update | Update every external package of a project to its latest stable version using list-packages.sh and latest-version.sh, then run build and tests. Hold suspects at their old version and retry once, and revert only on a real install, build or test failure. Supports nodejs, python, and dotnet projects. Use for dependency refresh; not for adding or removing packages. |
pkg-update — batch-update packages
Exit-code routes
Every tool exit code below has exactly one route. Three routes exist:
- skip — record the reason, keep going.
- stop — report and end the run. Never enter step 4, so nothing is reverted and the working tree is left exactly as it stands.
- revert — go to step 4, which reverts the working tree.
What a stop leaves behind. Step 1 writes nothing, so a stop there leaves every file untouched. From step 2 on, a stop can land after apply-version.sh has already rewritten one or more version source files, and the stop route still reverts nothing: a broken toolchain is not a broken package, and step 1 already proved the tree was clean, so those rewrites are recoverable by hand. Every stop from step 2 on therefore reports three things — the failing tool with its exit code, every package already applied, and every version source file already rewritten. Report "no file was changed" only for a stop that happened before step 2 started.
Only a real install, build or test failure takes the revert route. A missing input, a bad argument, an unknown ecosystem or a missing command is a broken call or a broken toolchain, so it takes the stop route.
One number carries one category across all six tools, and the table below names the object that category points at for each tool. Exit 6 is always a missing project directory and always stops the run — never fold it into the exit 3 skip, or a bad path hides as "nothing to do".
| Tool | 0 | 1 (bad argument count) | 2 (unknown input) | 3 (no source file) | 4 | 5 | 6 (project dir not found) | other |
|---|---|---|---|---|---|---|---|---|
git-guard.sh |
continue | stop (also revert without --confirm-destructive) |
stop (unknown subcommand) | — | stop (git missing) | stop (not a git work tree, tree not clean, or still dirty after a revert) | stop | stop |
list-packages.sh |
continue | stop | — | — | stop (python3 missing) | — | stop | stop |
latest-version.sh |
continue | stop | stop (unknown ecosystem) | — | skip (not in registry) | stop (curl or python3 missing) | — (takes no project dir) | stop |
apply-version.sh |
continue | stop | stop (unknown ecosystem) | skip | skip (package absent from the file) | stop (python3 missing) | stop | stop |
install-deps.sh |
continue | stop | stop (unknown ecosystem) | skip | stop (npm, pip or dotnet missing) | — | stop | revert |
build-test.sh |
continue | stop | stop (unknown ecosystem) | — (uses 5 instead) | stop (npm, python3, pytest or dotnet missing) | ask the user | stop | revert |
Exit 4 and exit 5 are the two codes whose meaning depends on the tool, so read them off this table rather than from memory.
build-test.sh --detect returns the same codes with the same routes. It writes nothing, so every stop route it takes leaves the working tree untouched.
Steps
-
Preflight: gate on a clean tree, then take inventory and prove a build and test command exists. Nothing in this step writes a file. Run the three substeps in this order — each one only earns its cost once the one before it passed:
-
The gate. Run
tools/git-guard.sh check {project dir}first, before any other tool. It proves the directory is a git work tree and thatgit status --porcelainprints zero bytes, untracked??lines included. Step 4 runsgit clean -fdthrough the same script, which deletes untracked files with no reflog and no way back, so a clean tree is the precondition for the whole run. A non-zero exit ends the run here: report the guard's stderr verbatim, including every??line, and run no further tool. Order the gate first on purpose — a dirty tree then costs one guard call instead of a full package scan that gets thrown away. -
Inventory. The gate exited 0 → run
tools/list-packages.sh {project dir}for every external package (ecosystem, name, current version). Route the exit code per the table above. Zero rows → stop and report the project directory you scanned plus the supported ecosystems (nodejs / python / dotnet). Keep every row's current version: step 3 pins packages back to it. -
Build and test commands. For every distinct ecosystem the inventory listed, run
tools/build-test.sh --detect {ecosystem} {project dir}. This mode only detects, so the working tree stays untouched here too.- 0 → the commands are inferable. Record them against this ecosystem.
- 5 → no build or test command could be inferred. Ask the user for the build command and the test command per the
jsc-ask:askrules, and record both against this ecosystem. A user who cannot name them takes the stop route right here, where no file has been touched and there is nothing to revert. - 1, 2, 4 or 6 → stop route. Report the code together with the bad argument, unknown ecosystem, missing command or missing directory.
Asking this question here is the whole point: found out in step 3 instead, it wastes every rewrite and forces a revert.
-
Step 1 is done only when all three hold:
git-guard.sh checkexited 0, the inventory holds at least one row with an ecosystem, a name and a current version, and every ecosystem in that inventory carries either a detected command set or a user-supplied build command plus test command, written down. Report all three results, then enter step 2.
-
-
Update ecosystem by ecosystem. This step MUST run as a sub agent — one sub agent per ecosystem, and the ecosystems run in parallel, because no ecosystem touches another's version source files:
- Query first, write second. Run
tools/latest-version.sh {ecosystem} {name}for every package of the ecosystem as one concurrent batch; the queries are independent registry reads. Then runtools/apply-version.sh {ecosystem} {project dir} {name} {version}one package at a time, sequentially: several packages of one ecosystem share one version source file (package.json / requirements.txt / pyproject.toml / *.csproj), and concurrent rewrites of one file lose edits. This substep is done when every package of the ecosystem carries either an applied version or a skip reason from substep 2. - Route every exit code per the table above. The skip route covers
latest-version.shexit 4 (package not found in the registry),apply-version.shexit 3 (no version source file) andapply-version.shexit 4 (package absent from the file): record the package and the reason, move on to the next package, and never abort the whole ecosystem. The stop route covers exit 1, exit 2,latest-version.shorapply-version.shexit 5 (a required command is missing) andapply-version.shexit 6 (project directory not found): report the code, list every package already applied and every file already rewritten, and end the run without entering step 4. This substep is done when every exit code seen has taken exactly one route. - Run
tools/install-deps.sh {ecosystem} {project dir}once, after every package of that ecosystem is applied. Exit 0 means the install finished. Exit 3 means this ecosystem has no dependency source file, which is the same documented skip as substep 2: record the ecosystem and the reason, and install nothing. Exit 1, exit 2, exit 4 and exit 6 take the stop route — exit 6 is a missing project directory, so report it instead of skipping it. Only another non-zero code is a real install failure: hand that ecosystem to step 3's hold-and-retry route at substep 3.2. This substep is done when the exit code is recorded together with the route it took. - The sub agent returns one row per package from substep 1: name, old version, new version — or
skippedplus the reason. It also returns theinstall-deps.shexit code and its route. The ecosystem is done only when every package appears in exactly one row. - Comments this step writes. Coding around an incompatibility needs a comment in the version source file or in the code, and so does every hold written in step 3. Write why the workaround or the hold exists. The one this skill trips over most sits on the allow list, not the ban list: a third-party package's issue link belongs in the comment — it is what states the cause and the condition for removing the workaround, as in
// works around github.com/foo/bar/issues/88; drop this hold once that ships. The ban list itself has one home,jsc-review/references/comment-scope.md, andjsc-hooks/hooks/comment-scope.shenforces it in code, so do not re-audit it line by line here. Know when that enforcement actually fires: only claude scans per file at write time (PostToolUse). codex scans when a turn ends, kiro only when the next prompt is submitted, copilot and antigravity only through a session-level wrapper — so on those four thecomment-scope.sh sweepthatjsc-git:commitruns in step 5 is the only pass that lands before the comment reaches a commit. Fix any warning that does arrive on the spot. This substep is done when every comment this step wrote names the reason for its workaround or hold.
- Query first, write second. Run
-
Build and test, and on a real failure hold the culprits and retry once. Holding is the only route that leaves a package on an older version, and it runs at most one extra build and test round per failing ecosystem:
- Run the build and test for every ecosystem, using what step 1 recorded:
tools/build-test.sh {ecosystem} {project dir}where substep 1.3 detected the commands, or the user's build command followed by the user's test command where substep 1.3 collected them. Route on the exit code:- 0 → this ecosystem passed.
- 1, 2, 4 or 6 → stop route. Report the code and the missing argument, ecosystem, command or directory, and enter no further step. Exit 4 means the toolchain is broken, not that the packages are broken, so reverting would destroy files for nothing.
- 5 → substep 1.3 already proved a command existed, so this code means the project changed underneath the run. Stop route: report the contradiction against what substep 1.3 recorded, and do not ask the same question twice.
- any other code → a real build or test failure. Go to substep 2. An ecosystem handed over by substep 2.3 for a real
install-deps.shfailure enters at substep 2 as well.
- Name the suspects: every package this run updated whose name appears in the install, build or test failure output. No named suspect → go straight to step 4.
- Run
tools/apply-version.sh {ecosystem} {project dir} {name} {old version}for each suspect, sequentially, with the old version substep 1.2 recorded. Route per the table, with one change:apply-version.shexit 3 and exit 4 mean the hold cannot be written at all, so that ecosystem goes to step 4 instead of skipping the package. - Write the comment for every hold per substep 2.5 — the reason for the hold and, where a third-party issue drives it, that issue's link.
- Run
tools/install-deps.sh {ecosystem} {project dir}once, then this ecosystem's build and test command once, routing both per substep 2.3 and substep 3.1. Retry exactly once: exit 0 → this ecosystem passed with those packages held, and step 5 reports them as held rather than updated. Any non-zero on the retry → step 4.
Step 3 is done only when every ecosystem landed in exactly one of three end states: exit 0 on the first run; exit 0 on the single retry, with every suspect of that ecosystem carrying a written hold plus a comment naming its reason; or entered step 4.
- Run the build and test for every ecosystem, using what step 1 recorded:
-
A real install, build or test failure reached this step → revert. Run
tools/git-guard.sh revert {project dir} --confirm-destructive. The script re-proves the target is a git work tree before it runs anything destructive, restores every tracked file withgit checkout -- ., removes this run's untracked files withgit clean -fd, and verifiesgit status --porcelainprints nothing. Gitignored paths survive-fdon purpose:node_modules,__pycache__,bin/andobj/stay behind, so restoring install output is out of scope for this skill. Route the exit code per the table above. Step 4 is done only when the script exited 0; report the failing packages with an error summary after that. -
Success → report the update list (package, old version, new version), every package held at its old version by step 3 with the reason, and every skip with its reason, then hand off to
jsc-git:commit. Step 5 is done when the hand-off is made and every package from step 1 appears exactly once, as an update, a hold or a skip. -
Record how the run ended. This is the closing step and it runs on all three endings — the success of step 5, the revert of step 4, and every stop route. Run
jsc-hooks/tools/report-status.sh skill-end jsc-pkg:pkg-update {status} {exit} "{detail}"; the leadingjsc-hooks/is the whole point — every other script this skill runs is its owntools/{name}.sh, and this one lives in a sibling plugin, so a baretools/path would resolve to a file that is not there. What records a skill's start cannot see how it ended, so an ending that is never written reads afterwards as a run that was abandoned mid-way, and a reverted run looks the same as a clean one.{status}is one of five.ok: every package landed on its new version and build and test passed, with no hold and no skip.degraded: the run finished and part of it did not land — a package held at its old version by step 3 while build and test passed on the retry, or a package skipped for a missing registry entry or a missing version source file. That isdegradedand neverfailed, because step 5 was reached and the hand-off tojsc-git:commitwas made.failed: a real install, build or test failure that step 3's single retry did not clear, so step 4 reverted the working tree.blocked:git-guard.sh checkrefused the run because the tree was not clean or was not a git work tree, so nothing was ever written.aborted: the run stopped before it could do its work on a precondition that does not hold — the inventory listed zero packages, or the user could not name a build and test command at substep 1.3.- Take
{exit}from the tool that decided the ending — thebuild-test.sh,install-deps.shorgit-guard.shcode that routed the run — and otherwise use 0 forokand 1 for every other status.{detail}is optional, one line, at most 200 characters: the counts of updated, held and skipped packages, or the failing tool with its code. Never put a build log or a package list of unbounded length there; the full lists belong in the report of step 5. - Reporting never changes the run. A machine without
report-status.shskips this step in silence and keeps its step 5 result exactly as it stands; the script swallows its own write failures and always exits 0, so nothing here is worth routing on. Step 6 is done when the call was made, or the script was absent and the step was skipped without a word.