Merge pull request 'chore(release): 放行技能組稽核修正到預設分支' (#14) from develop into master

Reviewed-on: #14
Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #14.
This commit is contained in:
2026-08-31 03:54:46 +00:00
7 changed files with 199 additions and 51 deletions
+3 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "jsc-pkg", "name": "jsc-pkg",
"version": "0.0.7", "version": "0.0.8",
"description": "套件批次更新(nodejs/python/dotnet),失敗還原", "description": "套件批次更新(nodejs/python/dotnet),先把嫌疑套件釘回舊版重試一次,真失敗才還原",
"skills": "./skills", "skills": "./skills",
"author": { "author": {
"name": "JSC" "name": "JSC"
@@ -18,6 +18,7 @@
"requires": { "requires": {
"jsc-ask": ">=0.0.6", "jsc-ask": ">=0.0.6",
"jsc-git": ">=0.0.9", "jsc-git": ">=0.0.9",
"jsc-hooks": ">=0.2.8",
"jsc-review": ">=0.0.8" "jsc-review": ">=0.0.8"
} }
} }
+3 -2
View File
@@ -1,12 +1,13 @@
{ {
"name": "jsc-pkg", "name": "jsc-pkg",
"version": "0.0.7", "version": "0.0.8",
"description": "套件批次更新(nodejs/python/dotnet),失敗還原", "description": "套件批次更新(nodejs/python/dotnet),先把嫌疑套件釘回舊版重試一次,真失敗才還原",
"skills": "./skills", "skills": "./skills",
"jsc": { "jsc": {
"requires": { "requires": {
"jsc-ask": ">=0.0.6", "jsc-ask": ">=0.0.6",
"jsc-git": ">=0.0.9", "jsc-git": ">=0.0.9",
"jsc-hooks": ">=0.2.8",
"jsc-review": ">=0.0.8" "jsc-review": ">=0.0.8"
} }
} }
+16 -9
View File
@@ -1,6 +1,6 @@
# jsc-pkg — 套件批次更新 # jsc-pkg — 套件批次更新
jsc 技能組的 pkg domain:把專案所有外部套件更新到最新穩定版本,完成後執行建置與測試,失敗則還原變更。支援 nodejs / python / dotnet。 jsc 技能組的 pkg domain:把專案所有外部套件更新到最新穩定版本,完成後執行建置與測試。過不了先把嫌疑套件釘回舊版重試一次,真的裝不起來或建置測試仍過不了才還原變更。支援 nodejs / python / dotnet。
## 安裝、更新、移除 ## 安裝、更新、移除
@@ -20,28 +20,35 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
## 工具 ## 工具
五支工具都靠 python3 解析或改寫檔案。少了 python3,每一支都回報「需要的指令不存在」,不會裝成查無套件。 `list-packages.sh`、`latest-version.sh`、`apply-version.sh`、`build-test.sh` 靠 python3 解析或改寫檔案。少了 python3,這幾支都回報「需要的指令不存在」,不會裝成查無套件。`install-deps.sh` 不碰 python3,它把安裝交給各生態系自己的安裝器;`git-guard.sh` 只用 git。每支工具實際需要哪些指令,看下表最右欄。
| 工具 | 用途 | 需要的指令 | | 工具 | 用途 | 需要的指令 |
| --- | --- | --- | | --- | --- | --- |
| `tools/git-guard.sh` | `git-guard.sh check <project dir>` 確認是 git 工作樹且工作區乾淨;`git-guard.sh revert <project dir> --confirm-destructive` 還原工作區 | git |
| `tools/list-packages.sh` | `list-packages.sh [專案目錄]` 偵測專案類型並列出所有外部套件與目前版本(TSV:ecosystem / name / current) | python3 | | `tools/list-packages.sh` | `list-packages.sh [專案目錄]` 偵測專案類型並列出所有外部套件與目前版本(TSV:ecosystem / name / current) | python3 |
| `tools/latest-version.sh` | `latest-version.sh <ecosystem> <name>` 查最新穩定版(npm / pypi / nuget) | curl、python3 | | `tools/latest-version.sh` | `latest-version.sh <ecosystem> <name>` 查最新穩定版(npm / pypi / nuget) | curl、python3 |
| `tools/apply-version.sh` | `apply-version.sh <ecosystem> <project dir> <name> <version>` 改寫版本來源檔案,把套件釘選到指定版本 | python3 | | `tools/apply-version.sh` | `apply-version.sh <ecosystem> <project dir> <name> <version>` 改寫版本來源檔案,把套件釘選到指定版本 | python3 |
| `tools/install-deps.sh` | `install-deps.sh <ecosystem> <project dir>` 重新解析並安裝相依套件(npm install、pip install、dotnet restore) | npm、pip、dotnet | | `tools/install-deps.sh` | `install-deps.sh <ecosystem> <project dir>` 重新解析並安裝相依套件(npm install、pip install、dotnet restore) | npm、pip、dotnet |
| `tools/build-test.sh` | `build-test.sh <ecosystem> <project dir>` 偵測並執行建置與測試 | npm、python3、pytest、dotnet | | `tools/build-test.sh` | `build-test.sh [--detect] <ecosystem> <project dir>` 偵測並執行建置與測試;`--detect` 只偵測不執行,用來在動任何檔案之前先問出推不出指令的情況 | npm、python3、pytest、dotnet |
### 還原的破壞性前提
`git-guard.sh revert` 會執行 `git clean -fd`,未追蹤檔案刪掉沒有 reflog 可救。所以前提判定寫在腳本裡,不寫在技能內文:目錄存在、git 指令存在、確實是 git 工作樹(不是裸存取庫)、呼叫端明確傳入 `--confirm-destructive`、目標不是檔案系統根目錄,五條全過才會跑第一個破壞性指令。任何一條不過就回 1、4、5 或 6,工作區一個位元組都不動。
`check` 的乾淨判定看整個存取庫,`revert` 的還原只作用在專案目錄以下:判定從嚴,動手從窄。
### 結束碼總表 ### 結束碼總表
一個數字只有一個意思,五支工具共用。新增工具請沿用這張表,不要自己編號。 一個數字一個類別,六支工具共用;同一個類別在各工具指的對象可以不同,差別寫在該列裡。新增工具請沿用這張表,不要自己編號。
| 碼 | 意思 | 呼叫方的動作 | | 碼 | 意思 | 呼叫方的動作 |
| --- | --- | --- | | --- | --- | --- |
| 0 | 成功 | 繼續 | | 0 | 成功 | 繼續 |
| 1 | 參數個數不對 | 停手 | | 1 | 參數個數不對 | 停手 |
| 2 | ecosystem 不認識 | 停手 | | 2 | 不認識的輸入:多數工具是 ecosystem 不認識,`git-guard.sh` 是子指令不認識 | 停手 |
| 3 | 該 ecosystem 沒有來源檔案 | 跳過這個 ecosystem 或套件 | | 3 | 該 ecosystem 沒有來源檔案 | 跳過這個 ecosystem 或套件 |
| 4 | 依工具而定:`list-packages.sh`、`build-test.sh`、`install-deps.sh` 是指令不存在(停手);`latest-version.sh`、`apply-version.sh` 是查不到該套件(跳過) | 見左欄 | | 4 | 依工具而定:`list-packages.sh`、`build-test.sh`、`install-deps.sh` 是指令不存在(停手);`latest-version.sh`、`apply-version.sh` 是查不到該套件(跳過) | 見左欄 |
| 5 | 依工具而定:`latest-version.sh`、`apply-version.sh` 是指令不存在(停手);`build-test.sh` 是推不出建置或測試指令(改問使用者) | 見左欄 | | 5 | 依工具而定:`latest-version.sh`、`apply-version.sh` 是指令不存在(停手);`build-test.sh` 是推不出建置或測試指令(改問使用者);`git-guard.sh` 是前提不成立,指不是 git 工作樹、工作區不乾淨、還原後仍不乾淨(停手) | 見左欄 |
| 6 | 找不到專案目錄 | 停手 | | 6 | 找不到專案目錄 | 停手 |
| 其他 | 底層指令的結束碼 | 只有 `install-deps.sh`、`build-test.sh` 會走還原 | | 其他 | 底層指令的結束碼 | 只有 `install-deps.sh`、`build-test.sh` 會走還原 |
@@ -49,7 +56,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
4 與 5 在各工具間意思不同,是為了保住既有的跳過路由;新增工具請優先用 4 表示指令不存在。 4 與 5 在各工具間意思不同,是為了保住既有的跳過路由;新增工具請優先用 4 表示指令不存在。
回 1、回 2、回 6 與回「指令不存在」時,技能只回報並停手,不還原工作樹——工具鏈壞了不是套件壞了,還原只會白白刪掉檔案。 回 1、回 2、回 6 與回「指令不存在」時,技能只回報並停手,不還原工作樹——工具鏈壞了不是套件壞了,還原只會白白刪掉檔案。停手不等於沒動過檔案:更新步驟跑到一半才停手時,已改寫的版本來源檔案留在原地,技能要一併報出改了哪些檔案與哪些套件。
## Skills 目錄 ## Skills 目錄
@@ -59,9 +66,9 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
### `pkg-update` ### `pkg-update`
套件批次更新:列套件 → 查最新穩定版 → 逐 ecosystem 以 sub agent 更新 → 建置與測試(指令不明時決策樹詢問)→ 失敗還原全部變更。 套件批次更新:先驗 git 工作區乾淨當硬閘門,過了才列套件並問出建置與測試指令 → 各 ecosystem 以 sub agent 並行更新 → 建置與測試,失敗先把兇手釘回舊版重試一次 → 仍失敗才還原全部變更。
更新途中要在版本來源檔案或程式碼留註解時,只寫鎖版本或繞道的原因,不寫內部議題編號、工作包編號、人名與 @ 提及、產生來源署名;第三方套件的 issue 連結可以寫,用來說明繞道成因與解除條件。完整清單與白名單見 [`jsc-review`](https://gitea.jsc.idv.tw/plugins/review) 的 `references/comment-scope.md`。 更新途中要在版本來源檔案或程式碼留註解時,只寫繞道或釘回舊版的原因;第三方套件的 issue 連結可以寫,用來說明成因與解除條件。禁止清單的正本只有一份,在 [`jsc-review`](https://gitea.jsc.idv.tw/plugins/review) 的 `references/comment-scope.md`,由 `jsc-hooks` 的 `hooks/comment-scope.sh` 在程式層強制。
<!-- JSC-SKILLS:END --> <!-- JSC-SKILLS:END -->
+3 -2
View File
@@ -1,12 +1,13 @@
{ {
"name": "jsc-pkg", "name": "jsc-pkg",
"version": "0.0.7", "version": "0.0.8",
"description": "套件批次更新(nodejs/python/dotnet),失敗還原", "description": "套件批次更新(nodejs/python/dotnet),先把嫌疑套件釘回舊版重試一次,真失敗才還原",
"skills": "./skills/", "skills": "./skills/",
"jsc": { "jsc": {
"requires": { "requires": {
"jsc-ask": ">=0.0.6", "jsc-ask": ">=0.0.6",
"jsc-git": ">=0.0.9", "jsc-git": ">=0.0.9",
"jsc-hooks": ">=0.2.8",
"jsc-review": ">=0.0.8" "jsc-review": ">=0.0.8"
} }
} }
+43 -31
View File
@@ -1,6 +1,6 @@
--- ---
name: pkg-update name: pkg-update
description: 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. Revert all changes on failure. Supports nodejs, python, and dotnet projects. Use for dependency refresh; not for adding or removing packages. description: 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 # pkg-update — batch-update packages
@@ -10,45 +10,57 @@ description: Update every external package of a project to its latest stable ver
Every tool exit code below has exactly one route. Three routes exist: Every tool exit code below has exactly one route. Three routes exist:
- **skip** — record the reason, keep going. - **skip** — record the reason, keep going.
- **stop** — report and end the run. Change no file, and never enter step 5. - **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 5, which reverts the working tree. - **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. 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 meaning across all five tools. 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". 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 ecosystem) | 3 (no source file) | 4 | 5 | 6 (project dir not found) | other | | 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 | | `list-packages.sh` | continue | stop | — | — | stop (python3 missing) | — | stop | stop |
| `latest-version.sh` | continue | stop | stop | — | skip (not in registry) | stop (curl or python3 missing) | — (takes no project dir) | 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 | skip | skip (package absent from the file) | stop (python3 missing) | stop | 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 | skip | stop (npm, pip or dotnet missing) | — | stop | **revert** | | `install-deps.sh` | continue | stop | stop (unknown ecosystem) | skip | stop (npm, pip or dotnet missing) | — | stop | **revert** |
| `build-test.sh` | continue | stop | stop | — (uses 5 instead) | stop (npm, python3, pytest or dotnet missing) | ask the user | 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. 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 ## Steps
1. Run `tools/list-packages.sh {project dir}` to get every external package (ecosystem, name, current version). Route the exit code per the table above. Step 1 is done only when the tool exited 0 and at least one row came back, with an ecosystem, a name and a current version in every row. Zero rows → stop and report the project directory you scanned plus the supported ecosystems (nodejs / python / dotnet), and run no further step. 1. **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:
2. Prove the project directory is a git repository with a clean tree. Run this before any step changes a file: 1. **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 that `git status --porcelain` prints zero bytes, untracked `??` lines included. Step 4 runs `git clean -fd` through 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.
1. Run `git -C {project dir} rev-parse --git-dir`. Exit 0 is required to continue, because step 5's revert is impossible outside a git repository. Any other exit code → stop and report the directory, and run no further step. 2. **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.
2. Run `git -C {project dir} status --porcelain`. Continuing requires that it prints **nothing at all**. Any output stops the run and reports that output, **including `??` untracked lines**: step 5 runs `git clean -fd`, which deletes untracked files with no reflog and no way back. 3. **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.
3. Step 2 is done only when `rev-parse --git-dir` exited 0 and `status --porcelain` printed zero bytes. Report both results, then enter step 3. - 0 → the commands are inferable. Record them against this ecosystem.
3. Update ecosystem by ecosystem. This step **MUST run as a sub agent** (one sub agent per 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:ask` rules, 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. For every package, run `tools/latest-version.sh {ecosystem} {name}` for the latest stable version, then `tools/apply-version.sh {ecosystem} {project dir} {name} {version}` to rewrite the version source file (package.json / requirements.txt / pyproject.toml / *.csproj). This substep is done when every package of the ecosystem carries either an applied version or a skip reason from substep 2. - 1, 2, 4 or 6 → stop route. Report the code together with the bad argument, unknown ecosystem, missing command or missing directory.
2. Route every exit code per the table above. The skip route covers `latest-version.sh` exit 4 (package not found in the registry), `apply-version.sh` exit 3 (no version source file) and `apply-version.sh` exit 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.sh` or `apply-version.sh` exit 5 (a required command is missing) and `apply-version.sh` exit 6 (project directory not found): report the code and end the run without entering step 5. This substep is done when every exit code seen has taken exactly one route.
3. 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 and goes to step 5. This substep is done when the exit code is recorded together with the route it took.
4. The sub agent returns one row per package from substep 1: name, old version, new version — or `skipped` plus the reason. It also returns the `install-deps.sh` exit code and its route. The ecosystem is done only when every package appears in exactly one row.
5. **Comments this step writes.** Holding a package at an older version, or coding around an incompatibility, sometimes needs a comment in the version source file or in the code. Write **why** the pin or the workaround exists, never a tracking number. The one this skill trips over most: **a third-party package's issue link is on the allow list** — it is what states the cause of the workaround and the condition for removing it, as in `// works around github.com/foo/bar/issues/88; drop this pin once that ships`. Out of a comment: internal issue ids, work package ids, personal names, `@` mentions and generated-by credits. Full list and allow list: `jsc-review/references/comment-scope.md`. `jsc-hooks/hooks/comment-scope.sh` compares every write against that list right after the file is written and prints a warning on a hit; fix the comment on the spot, then continue this step. Completion condition: every comment line this step added names a reason, carries no internal issue id, work package id, personal name, `@` mention or generated-by credit, and every `comment-scope.sh` warning this step received has been fixed and re-checked with no warning left.
4. Run `tools/build-test.sh {ecosystem} {project dir}` for every ecosystem and 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 → no build or test command could be inferred. Ask the user for the build command and the test command per the `jsc-ask:ask` rules, run the answers in that order, and take their exit codes as this ecosystem's result: all 0 → passed; any non-zero → step 5.
- any other code → a real build or test failure. Go to step 5.
Step 4 is done only when every ecosystem reached exit 0, from the tool or from the user's own commands. Asking this question here is the whole point: found out in step 3 instead, it wastes every rewrite and forces a revert.
5. A real install, build or test failure reached this step → revert: 4. Step 1 is done only when all three hold: `git-guard.sh check` exited 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.
1. Run `git -C {project dir} checkout -- .` to restore every tracked file to HEAD. 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:
2. Run `git -C {project dir} clean -fd` to remove the untracked files and directories this run created, such as a new lock file. Gitignored paths survive `-fd` on purpose: `node_modules`, `__pycache__`, `bin/` and `obj/` stay behind, so restoring install output is out of scope for this skill. Do not reach for `-fdx` — it deletes far more than this run created. 1. 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 run `tools/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.
3. The revert is done only when `git -C {project dir} status --porcelain` prints nothing. Report the failing packages with an error summary after that check passes. 2. Route every exit code per the table above. The skip route covers `latest-version.sh` exit 4 (package not found in the registry), `apply-version.sh` exit 3 (no version source file) and `apply-version.sh` exit 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.sh` or `apply-version.sh` exit 5 (a required command is missing) and `apply-version.sh` exit 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.
6. Success → report the update list (package, old version, new version), plus every skip and its reason, and hand off to `jsc-git:commit`. Step 6 is done when the hand-off is made and every package from step 1 appears either as an update or as a skip. 3. 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.
4. The sub agent returns one row per package from substep 1: name, old version, new version — or `skipped` plus the reason. It also returns the `install-deps.sh` exit code and its route. The ecosystem is done only when every package appears in exactly one row.
5. **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`, and `jsc-hooks/hooks/comment-scope.sh` enforces 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 the `comment-scope.sh sweep` that `jsc-git:commit` runs 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.
3. **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:
1. 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.sh` failure enters at substep 2 as well.
2. 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.
3. 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.sh` exit 3 and exit 4 mean the hold cannot be written at all, so that ecosystem goes to step 4 instead of skipping the package.
4. 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.
5. 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.
4. 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 with `git checkout -- .`, removes this run's untracked files with `git clean -fd`, and verifies `git status --porcelain` prints nothing. Gitignored paths survive `-fd` on purpose: `node_modules`, `__pycache__`, `bin/` and `obj/` 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.
5. 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.
+34 -5
View File
@@ -1,25 +1,37 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# build-test.sh — 偵測並執行專案的建置與測試,先建置再測試。 # build-test.sh — 偵測並執行專案的建置與測試,先建置再測試。
# 用法:build-test.sh <ecosystem> <project-dir> # 用法:
# build-test.sh <ecosystem> <project-dir> 偵測後實際執行
# build-test.sh --detect <ecosystem> <project-dir> 只偵測,唯讀,不執行任何建置或測試
# ecosystem:nodejs | python | dotnet # ecosystem:nodejs | python | dotnet
# 偵測方式: # 偵測方式:
# nodejs → package.json 的 scripts.build 與 scripts.test(npm init 產生的佔位 test 視為沒有) # nodejs → package.json 的 scripts.build 與 scripts.test(npm init 產生的佔位 test 視為沒有)
# python → pytest 設定(pytest.ini、setup.cfg、tox.ini、pyproject.toml)或 tests 目錄與 test_*.py # python → pytest 設定(pytest.ini、setup.cfg、tox.ini、pyproject.toml)或 tests 目錄與 test_*.py
# dotnet → *.sln 或 *.csproj,建置與測試都用 dotnet # dotnet → *.sln 或 *.csproj,建置與測試都用 dotnet
# 輸出:底層指令的原始輸出。 # 為什麼要有 --detect:推不出指令(5)本來要等所有版本都改寫完才發現,使用者又答不出指令時,
# 前面全數作廢還要走還原。--detect 讓呼叫方在動任何檔案之前先問出結果,工作區完全沒被動過。
# --detect 只做偵測與前置檢查,一個檔案都不寫。
# 輸出:底層指令的原始輸出;--detect 模式輸出偵測到的指令名稱,一行一個(nodejs 是 build、test,
# python 是 pytest,dotnet 是 dotnet build、dotnet test)。
# 結束碼(慣例見 README「工具」章的結束碼總表): # 結束碼(慣例見 README「工具」章的結束碼總表):
# 0 建置與測試都通過 # 0 建置與測試都通過;--detect 模式是推得出指令
# 1 參數個數不對 # 1 參數個數不對
# 2 ecosystem 不認識 # 2 ecosystem 不認識
# 4 需要的指令不存在(npm、python3、pytest、dotnet) # 4 需要的指令不存在(npm、python3、pytest、dotnet)
# 5 推不出任何建置或測試指令——呼叫方要改問使用者 # 5 推不出任何建置或測試指令——呼叫方要改問使用者
# 6 找不到專案目錄——呼叫方停手 # 6 找不到專案目錄——呼叫方停手
# 其他 建置或測試失敗,帶回失敗指令的結束碼 # 其他 建置或測試失敗,帶回失敗指令的結束碼;--detect 模式不會回這一類
# 本工具不用 3:3 保留給「該 ecosystem 沒有來源檔案」,這支改用 5 表達推不出指令。 # 本工具不用 3:3 保留給「該 ecosystem 沒有來源檔案」,這支改用 5 表達推不出指令。
set -u set -u
DETECT=0
if [ "${1:-}" = "--detect" ]; then
DETECT=1
shift
fi
if [ "$#" -ne 2 ]; then if [ "$#" -ne 2 ]; then
echo "用法:build-test.sh <ecosystem> <project-dir>" >&2 echo "用法:build-test.sh [--detect] <ecosystem> <project-dir>" >&2
exit 1 exit 1
fi fi
@@ -49,6 +61,10 @@ EOF
) )
[ -n "$scripts" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } [ -n "$scripts" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; }
command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; } command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; }
if [ "$DETECT" -eq 1 ]; then
printf '%s\n' "$scripts"
exit 0
fi
for s in $scripts; do for s in $scripts; do
( cd "$DIR" && npm run --if-present "$s" ) || exit $? ( cd "$DIR" && npm run --if-present "$s" ) || exit $?
done done
@@ -67,9 +83,17 @@ EOF
fi fi
[ "$has_tests" -eq 1 ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } [ "$has_tests" -eq 1 ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; }
if command -v pytest >/dev/null 2>&1; then if command -v pytest >/dev/null 2>&1; then
if [ "$DETECT" -eq 1 ]; then
echo "pytest"
exit 0
fi
( cd "$DIR" && pytest ) ( cd "$DIR" && pytest )
exit $? exit $?
elif command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then elif command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then
if [ "$DETECT" -eq 1 ]; then
echo "python3 -m pytest"
exit 0
fi
( cd "$DIR" && python3 -m pytest ) ( cd "$DIR" && python3 -m pytest )
exit $? exit $?
else else
@@ -82,6 +106,11 @@ EOF
projs=$(find "$DIR" -maxdepth 3 \( -name '*.sln' -o -name '*.csproj' \) 2>/dev/null) projs=$(find "$DIR" -maxdepth 3 \( -name '*.sln' -o -name '*.csproj' \) 2>/dev/null)
[ -n "$projs" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } [ -n "$projs" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; }
command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; } command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; }
if [ "$DETECT" -eq 1 ]; then
echo "dotnet build"
echo "dotnet test"
exit 0
fi
( cd "$DIR" && dotnet build ) || exit $? ( cd "$DIR" && dotnet build ) || exit $?
( cd "$DIR" && dotnet test ) || exit $? ( cd "$DIR" && dotnet test ) || exit $?
exit 0 exit 0
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env sh
# git-guard.sh — 還原路線的前提把關與還原本身,兩個子指令一支腳本。
# 用法:
# git-guard.sh check <project-dir>
# 確認目標是 git 工作樹,而且工作區乾淨。任何一步改檔案之前先跑這支。
# git-guard.sh revert <project-dir> --confirm-destructive
# 還原工作區:git checkout -- . 之後 git clean -fd,最後複驗乾淨。
#
# 為什麼還原要多一個 --confirm-destructive:
# revert 會跑 git clean -fd,未追蹤檔案刪掉就沒有 reflog 可救。把「確實是 git 工作樹」與
# 「呼叫端真的要還原」兩個前提寫進腳本,誤觸就退回 exit 1 或 exit 5,工作區一個位元組都不動。
# 前提逐條擋在動作之前,順序是:目錄存在 → git 指令存在 → 確實是 git 工作樹(不是裸存取庫)
# → 旗標給了 → 目標不是檔案系統根目錄。五條全過才會執行第一個破壞性指令。
#
# 範圍:check 的 status --porcelain 是整個存取庫(比較嚴,髒在別的目錄也擋得住);
# revert 的 checkout 與 clean 只作用在 <project-dir> 以下(比較窄,不會波及上層目錄)。
# clean 用 -fd 不用 -fdx:被 gitignore 的 node_modules、__pycache__、bin、obj 要留著。
#
# 輸出:
# 成功 → stdout 最後一行是 `check ok <project-dir>` 或 `revert ok <project-dir>`;
# revert 會先帶出 git clean 自己的 Removing 清單,那是刪掉哪些檔案的紀錄,要留給呼叫方回報。
# 前提不成立 → stderr 第一行是原因(not a git repository、working tree not clean、
# still dirty after revert),工作區不乾淨時後面接 git status --porcelain 的原始輸出。
#
# 結束碼(慣例見 README「工具」章的結束碼總表):
# 0 check:是 git 工作樹而且乾淨/revert:還原完成而且乾淨
# 1 參數個數不對,或 revert 少了 --confirm-destructive——本工具不動任何檔案
# 2 子指令不認識(只收 check、revert)
# 4 需要的指令不存在(git)
# 5 前提不成立。check 是「不是 git 工作樹」或「工作區不乾淨」;revert 是「不是 git 工作樹」
# 或「還原後仍不乾淨」。呼叫方停手,stderr 第一行講明是哪一種
# 6 找不到專案目錄——呼叫方停手
# 其他 底層 git 指令的結束碼
set -u
usage() {
echo "用法:git-guard.sh check <project-dir>" >&2
echo " git-guard.sh revert <project-dir> --confirm-destructive" >&2
}
[ "$#" -ge 1 ] || { usage; exit 1; }
cmd="$1"
case "$cmd" in
check) [ "$#" -eq 2 ] || { usage; exit 1; } ;;
revert) [ "$#" -eq 3 ] || { usage; exit 1; } ;;
*) echo "unknown subcommand: $cmd" >&2; usage; exit 2 ;;
esac
DIR="$2"
[ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; }
command -v git >/dev/null 2>&1 || { echo "git not found" >&2; exit 4; }
# 確實是 git 工作樹才有還原路線。裸存取庫沒有工作樹,checkout 與 clean 都無從跑起。
inside=$(git -C "$DIR" rev-parse --is-inside-work-tree 2>/dev/null || true)
[ "$inside" = "true" ] || { echo "not a git repository: $DIR" >&2; exit 5; }
if [ "$cmd" = "check" ]; then
dirty=$(git -C "$DIR" status --porcelain 2>/dev/null)
rc=$?
[ "$rc" -eq 0 ] || { echo "git status failed: $DIR" >&2; exit "$rc"; }
if [ -n "$dirty" ]; then
# 未追蹤的 ?? 行也算髒:還原會跑 git clean -fd,那些檔案刪掉沒有還原路徑。
echo "working tree not clean: $DIR" >&2
printf '%s\n' "$dirty" >&2
exit 5
fi
echo "check ok $DIR"
exit 0
fi
# 以下只有 revert 走得到。破壞性指令的最後兩道前提。
[ "$3" = "--confirm-destructive" ] || {
echo "revert 會執行 git clean -fd,必須明確傳入 --confirm-destructive" >&2
usage
exit 1
}
# 目標是檔案系統根目錄時直接拒絕:路徑組錯的時候,這是唯一擋得住的地方。
abs=$(CDPATH= cd -- "$DIR" 2>/dev/null && pwd) || { echo "project dir not found: $DIR" >&2; exit 6; }
[ "$abs" != "/" ] || { echo "refusing to revert the filesystem root" >&2; exit 5; }
git -C "$DIR" checkout -- . || exit $?
git -C "$DIR" clean -fd || exit $?
left=$(git -C "$DIR" status --porcelain 2>/dev/null)
rc=$?
[ "$rc" -eq 0 ] || { echo "git status failed: $DIR" >&2; exit "$rc"; }
if [ -n "$left" ]; then
echo "still dirty after revert: $DIR" >&2
printf '%s\n' "$left" >&2
exit 5
fi
echo "revert ok $DIR"
exit 0