Files
cli/skills/doctor/SKILL.md
T
jiantw83 d5bc40be13 feat(wiki): 體檢目錄頁改條列式版面,鍵改用體檢頁頁名
What:`templates/check-contents.md` 從 markdown 表格改成一台執行環境一個 H2 區塊,H2 標題就是那一台的體檢頁頁名 `CHECK_{HASH}`,原本的七個欄位改成標題底下一層 `- {欄位名}:{值}` 的條列,頁上不留任何表格。`skills/doctor/SKILL.md` 與 `skills/setup/SKILL.md` 對目錄頁的呼叫從 `wiki-contents.sh upsert CHECK 4 "{HASH}"` 改成 `upsert CHECK 1 "CHECK_{HASH}"`,`references/behaviors.md` 的關鍵步驟、完成條件與可驗證跡象,以及 `README.md` 的兩段技能敘述都跟著對齊。

Why:一列七格的表格,欄位一多就要橫向捲動,讀的人得先數欄位再對照表頭才知道哪一格是什麼;一筆一個 H2 區塊、每一條自己帶欄位名,掃過去就讀得懂。版面換成區塊之後鍵也得跟著換:表格時代的鍵是第 4 欄的裸 `HASH`,區塊沒有欄位可指,唯一能當鍵的是 H2 標題。key-col 與 key 沿用舊值會讓腳本比對不中,同一台機器每體檢一次就在頁尾多附一個區塊,舊區塊從此再也更新不到,而頁面看起來完全正常,錯得無聲無息。頁名只由 `{短主機名}/{登入帳號}` 決定,`GITEA_HOST` 換掉、`JSC_WIKI_REPO_CHECK` 搬到別的存取庫、Gitea 對頁名的編碼有差都動不到它,拿它當鍵比拿任何含網址的值都穩。

How:範本頁首的寫入語意從「一列」改寫成「一個區塊」,並補上四個參數的說明。第三個參數 `<key>` 收 H2 標題文字,也就是 `CHECK_{HASH}`;第四個參數收的是區塊檔而不是列檔,內容為 `## CHECK_{HASH}` 那一行、一個空行,再照範本的欄位順序每欄一條條列,`HASH` 那一欄照樣要寫,標題是鍵不代表欄位可以省,否則下一個讀的人讀不到。第二個參數 `1` 定位成 `<key-col>`,只在頁面還留著舊表格、需要自動轉檔時才用得到,指舊表格裡持有 `[CHECK_{HASH}](網址)` 的第 1 欄,轉檔時取那一格的文字當 H2 標題,頁面已經是條列格式就忽略它。「體檢頁」那一條維持 `[{文字}]({絕對網址})` 的人用連結寫法,H2 標題本身不放連結也不放網址。兩支技能的結束碼分流一併校正:`wiki-contents.sh` 的結束碼 1 從「寫入失敗」改寫成「組不出頁面內容或寫入失敗」,頁上找不到本機那一個區塊不算這一碼,腳本會改成附加。實際的轉檔與 upsert 邏輯都在 `jsc-gitea/tools/wiki-contents.sh`,這個存取庫只改敘述與範本。

Who:屬於「wiki 目錄頁改條列式呈現」這個需求落在 jsc-cli 的部分,也就是 `CHECK_CONTENTS` 這一型目錄頁的去表格化。內容頁維持圖表優先,不在這一輪範圍。
2026-09-02 17:25:37 +08:00

18 KiB
Raw Blame History

name, description
name description
doctor 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 {short hostname}/{user}; the page keeps only the latest run, while its CHECK_CONTENTS block is upserted through wiki-contents.sh into the contents repo. 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.

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. 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.

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.

Done when every installed domain has a status literal, or the check is reported as unverifiable with its reason.

1.2 Hook wiring

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 carries one of those verdicts and its missing items are listed.

1.3 Settings, global and project in one scan

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.

Done when the summary line is read, the rows are split into the two scopes, and every missing and invalid row is named.

1.4 Orphan variables

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.

They are a maintenance note for the skill set, not a fault on this machine, so they never enter the 待修項目 table.

Done when the orphan list is returned or the check is reported as skipped with its reason.

2. Report the five blocks, then build the 待修項目 table

2.1 The five blocks

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.

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

This run writes two pages, and they live in two different wiki repos. Resolve each repo on its own and never reuse one for the other.

The HASH — take it from the machine, do not compose it by hand. jsc-assist's MONITOR_{HASH} claims the same hash source and takes it in code, so the two pages only ever line up when this skill takes it the same way:

host=$(hostname 2>/dev/null || uname -n 2>/dev/null || printf 'unknown'); host=${host%%.*}
user=${USER:-$(id -un 2>/dev/null || printf 'unknown')}

${host%%.*} is the point of the snippet: hostname prints the FQDN on some machines and the short name on others, so a hand-written value gives one machine two pages that never merge again. Pass "{host}/{user}" to gitea.sh hash-id and use exactly what it prints. It is 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.

CHECK_{HASH} — content page. Repo: jsc-gitea/tools/gitea.sh wiki-repo CHECK. Write it through jsc-gitea:wiki and overwrite the whole page, because this page type keeps only the latest run of this one machine. Whole-page overwrite is correct here and forbidden on the page below.

CHECK_CONTENTS — contents page, in the contents repo. Repo: gitea.sh wiki-repo CONTENTS. Every contents page lives there now; it never falls back to JSC_WIKI_REPO_CHECK. It is an H1, a > preamble and one H2 block per machine — no markdown table anywhere on it. Do not hand-edit it — write the block with

jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 "CHECK_{HASH}" {block file} templates/check-contents.md

The block file holds the whole H2 block: the ## CHECK_{HASH} line, a blank line, then one bullet per field in the order templates/check-contents.md lists them, written as - {欄位名}:{值} with a full-width colon. Every field of the template gets a bullet, HASH included — the heading is the key, and a field only in the heading is a field the next reader cannot read.

The script reads the page back, replaces this machine's block or appends it, then writes the whole page. That keeps the rule in one place: one block per run, never a whole-page overwrite, never another machine's block — those blocks are other people's records, and this run read them from nowhere else.

The key is the H2 heading, CHECK_{HASH}. It is the name of the content page this block points at: the literal CHECK_ plus exactly what hash-id printed — 40 uppercase hex characters, not shortened, not otherwise prefixed, not wrapped in a link, no date appended. The script compares the whole heading text after trimming, so the key and that heading must match character for character.

The page name is the key because it is the one value that does not move. It is decided by {host}/{user} alone, so it survives a changed GITEA_HOST, a JSC_WIKI_REPO_CHECK moved to another repo, and a different Gitea encoding of the page name — all of which change the URL. Key on anything holding a URL and the comparison never matches, so every run appends a second block for the same machine — silently, because the page still looks right.

1 is <key-col>, and it only matters while a page is still the old markdown table: it is the 1-based index of the column that held the identity, the [CHECK_{HASH}]({URL}) cell in column 1, whose text becomes the H2 heading when the script converts that table to blocks. On a page already in block form the script ignores it.

The 體檢頁 bullet stays the human-facing link and is never the key; the heading itself carries no link and no URL. Write the bullet as [CHECK_{HASH}]({absolute URL}) — text plus link, the one link form this skill set uses. The URL comes from gitea.sh wiki-url {CHECK repo} CHECK_{HASH} and is never composed by hand. Fetch it after CHECK_{HASH} is written: wiki-url exits 4 on a page that does not exist yet.

A same-wiki link form resolves only inside its own wiki, and the two pages are no longer in the same one. It fails without an error, reading on screen as plain text or a dead link, so nobody finds it and nobody fixes it.

Verify every link before writing it. Collect every URL heading into CHECK_{HASH} or into the CHECK_CONTENTS block, then hand the whole list to jsc-gitea/tools/link-check.sh. It prints {OK|DEAD|SKIP}<TAB>{URL}<TAB>{reason} per line. Only exit 0 may be written.

Exit Meaning Action
0 Every link answers Write the page
1 At least one link is unreachable Write nothing, on either page. Report the DEAD lines to the caller
2 Usage error: no URL was given Report it as a defect in this skill and pass the URLs
3 The list holds a Gitea URL but GITEA_HOST is unset Skip both writes and put GITEA_HOST at the top of 待修項目. Never write without verifying
7 Gitea authentication failed Stop and report the key problem. This is not a dead link

Exit 7 stays apart from exit 1 on purpose: with a dead key, Gitea's answer for a private repo looks the same as "page absent". Merge the two and one expired key marks every live page dead, and the blocks pointing at them get rewritten or dropped.

The script asks the API and never reads a web status code. A private repo's web URL answers 404 to a request with no credentials, so status codes turn good links into dead ones.

Exit Meaning Action
0 updated or added Report which one it printed, with the repo and page it named
1 The page content could not be built, or the write failed Nothing landed. Report it with the stderr, and keep 2.1's tables on screen. A page with no block to replace is not this case: the script appends instead
2 Usage error Report it as a defect in this skill. Do not retry with guessed arguments. A templates/check-contents.md that is not on disk also lands here — then name the path the script looked for, confirm the plugin install is complete, and rerun
3 No contents repo configured Skip this write and put JSC_WIKI_REPO_CONTENTS at the top of 待修項目
4 Page absent and no template given Unreachable the way this skill calls the script — the command above always passes templates/check-contents.md. A template that is not on disk comes back as exit 2, not 4. So treat a 4 as a malformed call: report it as a defect in this skill, name the command that produced it, and do not retry with guessed arguments
7 Key invalid or no permission Nothing was read and nothing written. Name the exit code and create nothing
8 Any other API failure Same as 7: the old blocks are unknown, so name the exit code and create nothing

Exits 7 and 8 never mean the page is missing. Writing a fresh template over a directory whose blocks were never read wipes every other machine's block, with no merge and no backup behind it — which is exactly why the script creates a page only when its own read reported that page absent, and why it owns that branch instead of this prose.

wiki-repo exiting 3 means that page type has no wiki repo configured: JSC_WIKI_REPO_CHECK for the content page, JSC_WIKI_REPO_CONTENTS for the contents page. Print the tables, skip that one write, and put the unset variable at the top of 待修項目 — it is itself a finding, so a failed write never fails the health check. Any other non-zero exit from wiki-repo, wiki-url, hash-id or the wiki write is reported the same way: tables on screen, write skipped, exit code named.

3.2 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.

Done when every link written into either page passed link-check.sh first — or the DEAD list is on screen and that write was skipped — each of the two pages is reported with its URL, or its skipped write is reported together with its reason, and the four counts are stated with the recommendation given or explicitly withheld.

3.3 Record how the run ended

This is the last thing this skill does, and it runs on every path out of the skill. Call

jsc-hooks/tools/report-status.sh skill-end jsc-cli:doctor {status} {exit code} [detail]

{exit code} is the exit code of whatever decided the outcome, and 0 when nothing failed. {detail} is one short line, no more than 200 characters: the four counts fit there, the five blocks do not. If the script is not on this machine, skip this step in silence and finish the run as it stood — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports.

This one call writes, and it is the only write this skill makes. It records what the run found; it changes no setting, no wiring and no version, so the read-only contract of the opening paragraph still holds.

status When this skill uses it
ok All four checks reached a conclusion, the five blocks and the 待修項目 table are on screen, and both pages were written
degraded The checkup ran but part of it has no conclusion, and this is the common outcome for a read-only skill that cannot reach a source. Any check reported as 無法驗證 lands here — version-guard.sh report exiting non-zero, scan-config.sh exiting 3 on a missing spec table, wire-cli.sh status returning an unexpected code, a Gitea-dependent row coming back skipped in offline mode — and so does a wiki-repo exit 3 that skipped a page write, which this skill treats as a finding rather than a fault
failed Reading the machine worked, then recording it broke on an error: link-check.sh, gitea.sh or wiki-contents.sh returned 7 on an invalid key, or 8 on any other API failure. Both are errors, never an absent page, and neither leaves a usable record
aborted The user stopped the run before the record was written, for example by declining to supply GITEA_HOST and asking to end the checkup there

blocked has no place in this skill. Nothing gates a read-only checkup: a machine with no CLI installed, no plugin registry and no wiki repo still produces four findings, and reporting that as blocked would hide a run that did its whole job.

Done when exactly one skill-end line was recorded for this run, or the script was absent and the run finished without it.