feat(cli): 待修項目合併收成共用腳本,檢查改為併行

「待修項目」表原本由 doctor 與 setup 各寫一次。同一套合併與排序規則寫在
兩個地方,遲早各自漂移:一邊改了排序,另一邊漏掉一整類項目,而且沒有
任何地方看得出來。現在規則只留一份,兩支技能都呼叫它。輸入與輸出都是
TSV,一項都沒有時照樣印一列,呼叫端永遠有東西可以呈現。

doctor 的四項檢查彼此不共用資料,排成一列跑只是把等待時間乘上四倍,
現在同時啟動。doctor 呼叫接線腳本一律帶唯讀旗標,把「打錯一個子命令就
改到或刪掉檔案」的風險移進程式層,不再只靠指令打對。

deploy 的 CLI 偵測、版本結論與 marketplace 清單同樣互不相干,改成併行
取得。版本結論改讀版本守門腳本的單行結論,不再自己從表格推導。setup 把
已經確認過的模式與版本報告直接交給 deploy,操作者不必再答一次同樣的問題。

deploy、doctor、models 都補上結束碼分流:腳本回什麼碼就走哪條路,不再從
輸出內容猜。體檢目錄頁改成先讀回再更新自己那一列,整頁覆蓋會把別台機器
的紀錄一次抹掉。設定規格表補上技能盤點頁要用的環境變數,盤點頁才有地方
可寫。
This commit is contained in:
2026-08-31 11:09:59 +08:00
parent e8bf4ddaaf
commit e30bbf5780
7 changed files with 332 additions and 55 deletions
+75 -27
View File
@@ -1,71 +1,119 @@
---
name: doctor
description: Health-check the execution environment in one pass and record the result, changing nothing. Four checks - plugin versions from jsc-hooks/hooks/version-guard.sh report, hook wiring from jsc-hooks/tools/wire-cli.sh status, global settings and current-directory settings from tools/scan-config.sh against tools/config-spec.tsv. Report one findings table per check, then write the whole run to wiki CHECK_{HASH} where HASH comes from {hostname}/{user}; the page keeps only the latest run. Use after installing or updating the skill set, when a skill fails on a settings or wiring problem, or before handing a machine over; not for applying fixes, which is jsc-cli:setup.
description: Health-check the execution environment in one pass and record the result, changing nothing. Four checks - plugin versions from jsc-hooks/hooks/version-guard.sh report, hook wiring from jsc-hooks/tools/wire-cli.sh status, settings from tools/scan-config.sh against tools/config-spec.tsv, and orphan variables. Report one findings table per check, build the 待修項目 table with tools/build-todo.sh, then write the whole run to wiki CHECK_{HASH} where HASH comes from {hostname}/{user}; the page keeps only the latest run. Use after installing or updating the skill set, when a skill fails on a settings or wiring problem, or before handing a machine over; not for applying fixes, which is jsc-cli:setup.
---
# doctor — execution environment health check
Read-only. Every command below either reads a file or asks Gitea; none of them writes a setting. That is the contract with `jsc-cli:setup`: doctor states the facts, setup changes things.
Collection (steps 1 to 4) **MUST run as a sub agent** — one sub agent for all four, returning the raw TSV lines. Only the report and the wiki write stay in the main agent.
The read-only contract is enforced in code, not by prose: every `jsc-hooks/tools/wire-cli.sh` call in this skill runs with `JSC_READONLY=1` in its environment. A mistyped subcommand then refuses to write instead of rewiring the machine that was only supposed to be measured.
## 1. Skill versions
## 1. Collect, four checks in parallel
Start all four collectors at once. They share no data, so serialising them only makes the check four times slower. Each one **MUST run as a sub agent**, and each returns its raw output lines unchanged — no summarising inside the sub agent, because step 2.2 parses those lines.
Done when all four sub agents have returned, and each has either its raw lines or an explicit failure reason.
### 1.1 Skill versions
Run `jsc-hooks/hooks/version-guard.sh report`. It prints `{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}` per plugin, then `behind<TAB>{count}`.
A report with no `{domain}` row, or one carrying `noregistry<TAB>{path}`, means this CLI has no local plugin registry. Report it as 無法驗證 — never as 最新. `behind<TAB>0` proves nothing when no domain row precedes it.
Done when every installed domain has a status literal, or the CLI is reported as unverifiable.
`report` only reads, so it exits 0 even when every lookup fails. Any non-zero exit means the script itself could not run: report the whole version check as 無法驗證 together with the exit code, and let the other three checks finish.
## 2. Hook wiring
Done when every installed domain has a status literal, or the check is reported as unverifiable with its reason.
Run `jsc-hooks/tools/wire-cli.sh status {cli}` for every CLI that `tools/detect-clis.sh` found. Use `status` and nothing else: `wire-cli.sh` without a subcommand rewires, `purge` deletes, and `smoke` executes hooks — all three break the read-only contract.
### 1.2 Hook wiring
Exit codes: 0 wired, 1 degraded, 3 skipped (CLI not installed), 5 unwired. Each `item` line names one wiring point and whether it is present.
First run `jsc-cli/tools/detect-clis.sh`. Exit 0 with at least one row → those are the CLIs to check. Exit 0 with no row → report the wiring table as empty and say no AI agent CLI was detected; that is a finding, not a pass. Any non-zero exit → report the wiring check as 無法驗證 with the exit code and stderr.
Then run `JSC_READONLY=1 jsc-hooks/tools/wire-cli.sh status {cli}` for every detected CLI. Use `status` and nothing else: `wire-cli.sh` without a subcommand rewires, `purge` deletes, and `smoke` executes hooks — all three break the read-only contract.
| Exit | Meaning | Action |
| --- | --- | --- |
| 0 | wired | Record as wired |
| 1 | degraded | Record as degraded and quote the `reason` text |
| 2 | usage error | Stop this CLI's wiring check. The subcommand or the CLI name is wrong — report it as a defect in this skill, not as a machine fault |
| 3 | skipped | The CLI is not installed. Drop it from the wiring table |
| 5 | unwired | Record every `item` line whose state is `missing` |
| other | unexpected | Report that CLI's wiring as 無法驗證 with the exit code and stderr. Never read it as wired |
Only claude reaches `wired`. The other four have no pre-tool hook, so `degraded` is their healthy state — report the degradation reason as-is and never present it as a defect to fix.
Done when every detected CLI has a status and its missing items are listed.
Done when every detected CLI carries one of those verdicts and its missing items are listed.
## 3. Global settings
### 1.3 Settings, global and project in one scan
Run `tools/scan-config.sh scan global`. It checks every `scope=global` row of `tools/config-spec.tsv` and prints `item<TAB>scope<TAB>required<TAB>actual<TAB>expect<TAB>fix<TAB>verdict`, closing with `summary<TAB>{missing}<TAB>{invalid}<TAB>{unset}<TAB>{skipped}`.
Run `jsc-cli/tools/scan-config.sh scan all` from the current working directory. One call covers both scopes: the spec table is read once and the `scope` column separates the rows. It prints `{項目}<TAB>{範圍}<TAB>{必要}<TAB>{現況}<TAB>{說明}<TAB>{修法}<TAB>{判定}`, closing with `summary<TAB>{missing}<TAB>{invalid}<TAB>{unset}<TAB>{skipped}`.
| Exit | Action |
| --- | --- |
| 0 | Scan finished. Split the rows by the `範圍` column into a global table and a project table |
| 2 | Usage error. Report it as a defect in this skill and skip the settings check |
| 3 | The spec table is missing. Name the path it looked for and `JSC_CONFIG_SPEC`, then skip the settings check |
| other | Report the settings check as 無法驗證 with the exit code and stderr |
Verdicts: `ok`, `default` (unset, default works), `unset` (optional, feature degrades), `missing` (required, skills break), `invalid` (set but fails verification), `skipped` (offline).
Add `-o` when Gitea is unreachable; the Gitea-dependent rows then come back `skipped`. Report those rows as 未取得結論 and never as passes.
Also run `tools/scan-config.sh orphans` — variables used in the source but absent from the spec table. They are a maintenance note for the skill set, not a fault on this machine.
Done when the summary line is read, the rows are split into the two scopes, and every `missing` and `invalid` row is named.
Done when the summary line is read and every `missing` and `invalid` row is named.
### 1.4 Orphan variables
## 4. Own settings
Run `jsc-cli/tools/scan-config.sh orphans` — variables used in the source but absent from the spec table. Same exit codes as 1.3; exit 3 here also covers a missing plugins root, so name `JSC_PLUGINS_ROOT` in that case.
Run `tools/scan-config.sh scan project` from the current working directory. Same output format, `scope=project` rows only.
They are a maintenance note for the skill set, not a fault on this machine, so they never enter the 待修項目 table.
Say which directory was scanned in the report. A project-scope result is meaningless without it, because the answer changes with every `cd`.
Done when the orphan list is returned or the check is reported as skipped with its reason.
When `.env` or `.envrc` exists, name the spec-table variables it overrides and state the value actually in effect. A global setting silently overridden here is the failure this check exists to catch.
## 2. Report the five blocks, then build the 待修項目 table
Done when the scanned directory is stated and every project row has a verdict.
### 2.1 The five blocks
## 5. Report and record
Report all five blocks per `templates/check-page.md`: 技能版本 from 1.1, Hook 接線 from 1.2, 全域設定 and 自我設定 from 1.3's two scopes, and 未登錄變數 from 1.4. Step 1.4 is the only place the orphan list is collected, so leaving it out here drops it from the run entirely — it never enters the 待修項目 table of 2.2, which is exactly why it needs its own block.
Report all four tables per `templates/check-page.md`. Then build the 待修項目 table from every `missing`, `invalid` and `unwired` item, plus every domain reported 落後. Order them `missing` → `invalid` → `unwired` → `落後`. Nothing wrong → one row reading 無.
The project rows carry the working directory in their heading. A project-scope result is meaningless without it, because the answer changes with every `cd` — so name the directory that step 1.3 scanned, even when the project table is empty.
When `.env` or `.envrc` exists in that directory, name the spec-table variables it overrides and state the value actually in effect. A global setting silently overridden here is the failure this check exists to catch.
### 2.2 The 待修項目 table
Save each collector's raw lines to a file, then run
`jsc-cli/tools/build-todo.sh --config {scan-all 輸出} --wiring {cli}={status 輸出} --version {report 輸出}`
one `--wiring` per detected CLI. The script merges the three sources and orders them `missing` → `invalid` → `unwired` → `落後`. Nothing wrong → it prints one row whose class reads 無.
| Exit | Action |
| --- | --- |
| 0 | Render the `todo` rows as the 待修項目 table and the `summary` line as 3.2's counts |
| 2 | Usage error. Report it as a defect in this skill; fall back to no 待修項目 table and say the merge did not run |
| 3 | An input file was unreadable. Name the file, rerun that one collector, and say so when it still fails |
| other | Report the merge as failed with the exit code, and keep 2.1's tables on screen |
A check that step 1 reported as 無法驗證 contributes no rows. Say that in the report: an unverified check and a clean check look identical in this table, and only the sentence tells them apart.
Done when all five blocks of 2.1 are on screen with the scanned directory stated — 未登錄變數 included, showing either its rows or the reason 1.4 gave for skipping — and 2.2's 待修項目 table is on screen with its rows in that order, or 2.2's merge failure is reported with its reason while 2.1's blocks stay on screen.
## 3. Record, then hand off
### 3.1 Record
Write the page through `jsc-gitea:wiki`:
- Wiki repo: `jsc-gitea/tools/gitea.sh wiki-repo CHECK`.
- Page name: `CHECK_` plus `gitea.sh hash-id "{hostname}/{user}"` — the host and the login account, not `{owner}/{repo}`. Doctor checks a machine, and it has to work in directories that are not repositories at all.
- Overwrite the whole page. This page type keeps only the latest run.
- Update `CHECK_CONTENTS` from `templates/check-contents.md` in the same pass.
- `CHECK_{HASH}` is a **content page**: overwrite the whole page, because this page type keeps only the latest run of this one machine.
- `CHECK_CONTENTS` is a **contents page** and follows the opposite rule: read it back first, then upsert this machine's row from `templates/check-contents.md` in the same pass — add the row if missing, otherwise refresh its 必要項缺漏、設定錯誤、最後體檢 columns. Never overwrite the whole page, and never touch a row belonging to another machine: those rows are other people's records, and this run never read them from anywhere else.
- The `CHECK_CONTENTS` read branches by exit code, and only exit 4 opens the create path. Exit 0 means the page is there, so upsert into what came back. Exit 4 means the page really does not exist yet, so build it from the template. Exit 7 (key invalid or no permission) and exit 8 (any other API failure) both mean the old rows are unknown, never that the page is missing: skip the `CHECK_CONTENTS` write, name the exit code in the report, and create nothing. Writing a fresh template over a directory whose rows were never read wipes every other machine's row, and the write carries no merge and no backup.
`wiki-repo` exiting 3 means no wiki repo is configured for CHECK. Print the tables, skip the wiki write, and put `JSC_WIKI_REPO_CHECK` at the top of 待修項目 — that unset variable is itself a finding, so a failed write never fails the health check.
`wiki-repo` exiting 3 means no wiki repo is configured for CHECK. Print the tables, skip the wiki write, and put `JSC_WIKI_REPO_CHECK` at the top of 待修項目 — that unset variable is itself a finding, so a failed write never fails the health check. Any other non-zero exit from `wiki-repo`, `hash-id` or the wiki write is reported the same way: tables on screen, write skipped, exit code named.
Done when either the wiki page URL is reported, or the skipped write is reported together with the reason.
### 3.2 Hand off
## 6. Hand off
State the four counts from 2.2's `summary` line: required items missing, settings invalid, CLIs unwired, domains behind. Recommend `/jsc-cli:setup` when any of those is above zero. Never fix anything here.
State the counts: required items missing, settings invalid, CLIs unwired, domains behind. Recommend `/jsc-cli:setup` when any of those is above zero. Never fix anything here.
Done when the counts are stated and the recommendation is given or explicitly withheld.
Done when either the wiki page URL is reported or the skipped write is reported together with its reason, **and** the four counts are stated with the recommendation given or explicitly withheld.