fix(doctor,setup): 兩支技能補上路徑守則,呼叫一律字面絕對路徑
這兩支技能的腳本呼叫原本全是裸相對路徑,整份文件沒有任何路徑守則。相對路徑會對著操作者當下的工作目錄解,而那裡從來不是外掛根目錄,所以每一個呼叫點都是模型就地猜前綴的地方。部署技能原本三處相對路徑被補成帶變數路徑,就是同一個機制。 實際掃到的處數比預估多:體檢 18 處、修復 20 處。多出來的是兩類原本沒想到的——裸檔名的 Gitea 呼叫,以及被當成參數傳的資料檔。後者同樣會解錯地方,而且解錯了不會報錯。 兩支各補路徑守則與前置步驟,解出兩條根目錄:跨外掛走 current 那一層(解到那一層就停,再往下解會落到帶版本號的快取路徑,那種路徑進不了允許清單),技能自己的腳本與範本走 CLI 載入技能時講明的外掛基底目錄。 兩支都不靠 current 底下那條 jsc-cli 連結,但理由各自不同,不是照抄部署那一支的。體檢不能靠,因為它正是機器可疑時才跑的技能——觸發時機本身就是連結可能出錯的那幾個時刻,而連結指著舊版時拿到的是舊的掃描腳本與舊的規格表,掃出來的每一列都像真的發現,不會有任何錯誤訊息。修復更不能靠,因為它寫檔,而且它要修的其中一列正是那個決定連結位置的變數:那種機器上根目錄根本解不出來,而修它要用的腳本掛在外掛基底目錄底下、不依賴那個變數,所以解不出來既不停這一輪也不擋那一項修復。何況拿舊的寫入腳本改設定檔,再用同一份舊腳本重驗,兩邊當然對得上,整輪會報成已修。 失敗分支也與部署不同。部署解不出根目錄就停手;這兩支都不停——體檢把它記成一項發現然後跑完不需要跨外掛的檢查,因為唯讀體檢中途停掉操作者什麼都拿不到;修復把它當成待修的那一項,修好再重解一次。 兩份範本與說明文件一併改。範本是這兩支技能在同一輪一起讀的,而且指示寫入,留著裸路徑等於留一個繞過新規則的入口。範本裡另外補掉兩個路徑洞:指向範本自己的佔位符原本沒有路徑,以及一個連目錄都沒有的裸檔名。 行為契約四列跟著改,並補上可稽核的跡象:回報裡的腳本路徑全是字面絕對路徑,跨外掛的路徑中間是 current 那一層而不是帶版本號的快取路徑。
This commit is contained in:
+52
-18
@@ -7,7 +7,41 @@ description: Health-check the execution environment in one pass and record the r
|
||||
|
||||
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.
|
||||
The read-only contract is enforced in code, not by prose: every `{JSC_ROOT}/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.
|
||||
|
||||
## Path rule — every script call is a literal absolute path
|
||||
|
||||
Write every script call in this skill as a literal absolute path. Never hand the shell a path that still holds a variable or a tilde — `$JSC_HOME/...`, `~/.jsc/...`, or anything like them — and never a bare relative one either. The permission layer matches paths statically: it expands no variable and no tilde, so such a path matches no allow rule and the call falls through to an approval prompt. A bare relative path is the worse form, because it resolves against whatever directory the CLI happens to be in — and this skill deliberately runs from the operator's project directory, which is never a plugin root — so every bare call site is a place where a prefix gets guessed.
|
||||
|
||||
A guessed prefix is the specific danger here, and it is quiet. This skill measures a machine and writes what it found onto a page other people act on, so a call that reaches the wrong copy of a script does not fail — it reports. An old `tools/scan-config.sh` reached through some other version's directory grades this machine against that version's `tools/config-spec.tsv`, and every row it prints looks exactly like a real finding. A checkup that cannot say which script produced its rows is worth less than no checkup, because nobody doubts it.
|
||||
|
||||
Portability is no reason to put the variable back. Step 0 resolves the roots once, at run time, on whatever machine this runs on — that is where portability comes from.
|
||||
|
||||
## Step 0 — resolve the two roots, once
|
||||
|
||||
Before step 1, run this one command:
|
||||
|
||||
`readlink -f "$JSC_HOME/current"`
|
||||
|
||||
It prints one absolute directory: the `current` directory itself, a farm of version-free symbolic links with one entry per plugin. Call it `{JSC_ROOT}` for the rest of this document. **Stop at that directory — never resolve one level further.** Resolving one of those entries lands on the versioned plugin cache (`/root/.claude/plugins/cache/jsc/jsc-hooks/0.4.2`, say), and a versioned path is exactly the kind no allow rule can hold: a rule with `*` where the version segment goes matches nothing, measured. `{JSC_ROOT}` is the version-free root, and staying at it is the whole point. This is the only place a variable may appear; the shell expands it inside the command itself, so no unexpanded path ever reaches the permission layer.
|
||||
|
||||
Confirm the directory exists, in the same approved step: `[ -d "{the path just printed}" ]`. **That is the check the other two wave through.** `JSC_HOME` is an optional variable with a default, so an unset one is an ordinary machine state rather than an exotic one — it has its own row in `{CLI_ROOT}/tools/config-spec.tsv`, and finding it unset is a normal outcome of this very checkup. With it unset the command prints `/current` and exits 0 — non-empty, absolute, and nowhere — and every literal path built from it then names a place that is not there.
|
||||
|
||||
**Unlike `jsc-cli:deploy`, an unresolvable `{JSC_ROOT}` never stops this run.** It is a finding, and findings are what this skill produces: report `JSC_HOME` in the 全域設定 block and at the top of the 待修項目 table. Mark every check that needed a cross-plugin script as 無法驗證 with that as its reason, and let the checks that do not need one finish. A read-only checkup that aborts tells the operator nothing, and the machine most in need of a checkup is the machine whose paths do not resolve.
|
||||
|
||||
Substitute `{JSC_ROOT}` in every cross-plugin call, so what runs is a literal absolute path. `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh` becomes, for example, `/root/.jsc/current/jsc-hooks/hooks/version-guard.sh`. Resolve it once, at the start of the run. Do not re-resolve it per call, and do not add a tool that prints it.
|
||||
|
||||
### The second root — this skill's own `tools/` and `templates/`
|
||||
|
||||
**Do not reach this skill's own files through `{JSC_ROOT}`, even though a `jsc-cli` link is normally sitting there.** `jsc-cli:deploy` refreshes the whole farm at the end of every round, so on a machine that has deployed, that link exists. This skill still may not lean on it, for a reason of its own: **doctor is the skill that gets run when the machine is suspect.** Its own triggers say so — after an install or update, after a skill failed on a settings or wiring problem, before a handover. A deploy that ended `degraded` is one whose `link` lines came back `fail`, or `skip` on a path holding something that is not a symbolic link at all, leaving a plugin's documented path on an old version — and doctor is what runs next. Reached through that link, `tools/scan-config.sh` and its `tools/config-spec.tsv` come from a version this machine is not running, and the findings table then describes a machine that does not exist. Nothing errors: an old scanner runs perfectly well.
|
||||
|
||||
The second root is free of that. `tools/scan-config.sh`, `tools/detect-clis.sh` and `tools/build-todo.sh` reached through it keep working on a machine whose farm is stale, broken, or never built — which is precisely the machine being measured — so `JSC_HOME` itself still gets scanned and still gets reported.
|
||||
|
||||
They sit at `{plugin root}/tools/` and `{plugin root}/templates/`, and the plugin root is the base directory the CLI states when it loads this skill. Take that literal path verbatim, call it `{CLI_ROOT}`, and write every own-plugin path as `{CLI_ROOT}/tools/{script}` or `{CLI_ROOT}/templates/{file}` — `/root/.claude/plugins/cache/jsc/jsc-cli/0.3.3/tools/scan-config.sh`, for example. No command runs for this one, and it is taken once, like `{JSC_ROOT}`.
|
||||
|
||||
That base directory carries a version segment, so no allow rule covers it and each of those calls raises an approval prompt. **That is acceptable in this skill and in no unattended one**: doctor runs with the operator in front of it — the five blocks and the 待修項目 table are printed for a person to read and act on — so there is somebody to approve. Never carry this branch into a skill that runs from a scheduler, and never guess a prefix when the invocation states no base directory: report that this skill's own plugin root is unknown and stop, because a guessed prefix reports some other version's idea of this machine as if it were this machine.
|
||||
|
||||
Done when `{JSC_ROOT}` holds one existing absolute directory or its failure is recorded as a `JSC_HOME` finding, and `{CLI_ROOT}` holds one literal absolute path.
|
||||
|
||||
## 1. Collect, four checks in parallel
|
||||
|
||||
@@ -17,7 +51,7 @@ Done when all four sub agents have returned, and each has either its raw lines o
|
||||
|
||||
### 1.1 Skill versions
|
||||
|
||||
Run `jsc-hooks/hooks/version-guard.sh report`. It prints `{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}` per plugin, then `behind<TAB>{count}`.
|
||||
Run `{JSC_ROOT}/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.
|
||||
|
||||
@@ -27,9 +61,9 @@ Done when every installed domain has a status literal, or the check is reported
|
||||
|
||||
### 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.
|
||||
First run `{CLI_ROOT}/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.
|
||||
Then run `JSC_READONLY=1 {JSC_ROOT}/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 |
|
||||
| --- | --- | --- |
|
||||
@@ -46,7 +80,7 @@ Done when every detected CLI carries one of those verdicts and its missing items
|
||||
|
||||
### 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}`.
|
||||
Run `{CLI_ROOT}/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 |
|
||||
| --- | --- |
|
||||
@@ -63,7 +97,7 @@ Done when the summary line is read, the rows are split into the two scopes, and
|
||||
|
||||
### 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.
|
||||
Run `{CLI_ROOT}/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.
|
||||
|
||||
@@ -73,7 +107,7 @@ Done when the orphan list is returned or the check is reported as skipped with i
|
||||
|
||||
### 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.
|
||||
Report all five blocks per `{CLI_ROOT}/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.
|
||||
|
||||
@@ -83,7 +117,7 @@ When `.env` or `.envrc` exists in that directory, name the spec-table variables
|
||||
|
||||
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 輸出}`
|
||||
`{CLI_ROOT}/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 無.
|
||||
|
||||
@@ -111,15 +145,15 @@ host=$(hostname 2>/dev/null || uname -n 2>/dev/null || printf 'unknown'); 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.
|
||||
`${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 `{JSC_ROOT}/jsc-gitea/tools/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_{HASH}` — content page.** Repo: `{JSC_ROOT}/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
|
||||
**`CHECK_CONTENTS` — contents page, in the contents repo.** Repo: `{JSC_ROOT}/jsc-gitea/tools/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`
|
||||
`{JSC_ROOT}/jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 "CHECK_{HASH}" {block file} {CLI_ROOT}/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 block file holds the whole H2 block: the `## CHECK_{HASH}` line, a blank line, then one bullet per field in the order `{CLI_ROOT}/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.
|
||||
|
||||
@@ -129,11 +163,11 @@ The page name is the key because it is the one value that does not move. It is d
|
||||
|
||||
`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.
|
||||
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 `{JSC_ROOT}/jsc-gitea/tools/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.
|
||||
**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_ROOT}/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 |
|
||||
| --- | --- | --- |
|
||||
@@ -151,9 +185,9 @@ The script asks the API and never reads a web status code. A private repo's web
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
| 2 | Usage error | Report it as a defect in this skill. Do not retry with guessed arguments. A `{CLI_ROOT}/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 |
|
||||
| 4 | Page absent and no template given | Unreachable the way this skill calls the script — the command above always passes `{CLI_ROOT}/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 |
|
||||
|
||||
@@ -171,7 +205,7 @@ Done when every link written into either page passed `link-check.sh` first — o
|
||||
|
||||
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]`
|
||||
`{JSC_ROOT}/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.
|
||||
|
||||
|
||||
+55
-19
@@ -7,9 +7,45 @@ description: Fix what jsc-cli:doctor found, one confirmed item at a time. Read t
|
||||
|
||||
This skill writes. Every write is confirmed first, backed up, and verified afterwards.
|
||||
|
||||
## Path rule — every script call is a literal absolute path
|
||||
|
||||
Write every script call in this skill as a literal absolute path. Never hand the shell a path that still holds a variable or a tilde — `$JSC_HOME/...`, `~/.jsc/...`, or anything like them — and never a bare relative one either. The permission layer matches paths statically: it expands no variable and no tilde, so such a path matches no allow rule and the call falls through to an approval prompt. A bare relative path is the worse form, because it resolves against whatever directory the CLI happens to be in — the operator's project directory, which is never a plugin root — so every bare call site is a place where a prefix gets guessed.
|
||||
|
||||
**This skill writes, and that is what turns a guessed prefix from an annoyance into a wrong machine.** An old `tools/apply-config.sh` reached through some other version's directory rewrites the `# jsc-config` block of every rc file here to that version's idea of the key set, backs the old block up as though that were correct, and then step 4 re-verifies it with `show` from the same wrong copy — which agrees, because it is the same copy. The run reports that item as 已修 and the operator believes it, and nothing in this document catches it. Only the path does.
|
||||
|
||||
Portability is no reason to put the variable back. Step 0 resolves the roots once, at run time, on whatever machine this runs on — that is where portability comes from.
|
||||
|
||||
## Step 0 — resolve the two roots, once
|
||||
|
||||
Before step 1, run this one command:
|
||||
|
||||
`readlink -f "$JSC_HOME/current"`
|
||||
|
||||
It prints one absolute directory: the `current` directory itself, a farm of version-free symbolic links with one entry per plugin. Call it `{JSC_ROOT}` for the rest of this document. **Stop at that directory — never resolve one level further.** Resolving one of those entries lands on the versioned plugin cache (`/root/.claude/plugins/cache/jsc/jsc-gitea/0.4.2`, say), and a versioned path is exactly the kind no allow rule can hold: a rule with `*` where the version segment goes matches nothing, measured. `{JSC_ROOT}` is the version-free root, and staying at it is the whole point. This is the only place a variable may appear; the shell expands it inside the command itself, so no unexpanded path ever reaches the permission layer.
|
||||
|
||||
Confirm the directory exists, in the same approved step: `[ -d "{the path just printed}" ]`. **No skill meets an unset `JSC_HOME` as often as this one.** It is an optional variable with a default, it has an `auto` row in `{CLI_ROOT}/tools/config-spec.tsv`, and writing it is one of the repairs this skill performs — so a machine whose `JSC_HOME` is unset or wrong is the ordinary reason this skill was called. With it unset the command prints `/current` and exits 0: non-empty, absolute, and nowhere. The emptiness check and the exit code both wave that through, and every literal path built from it names a place that is not there.
|
||||
|
||||
**An unresolvable `{JSC_ROOT}` stops neither this run nor the repair.** Everything needed to write `JSC_HOME` — `{CLI_ROOT}/tools/apply-config.sh`, `{CLI_ROOT}/tools/scan-config.sh`, `{CLI_ROOT}/tools/build-todo.sh` — hangs off the second root below, which does not depend on `JSC_HOME` at all. Fix that item first, re-resolve `{JSC_ROOT}` once afterwards, and carry on; whatever is still unreachable — the wiki record of step 5, a delegated repair — is recorded as 未修好 with that as its reason, never guessed at.
|
||||
|
||||
Substitute `{JSC_ROOT}` in every cross-plugin call, so what runs is a literal absolute path. `{JSC_ROOT}/jsc-gitea/tools/gitea.sh` becomes, for example, `/root/.jsc/current/jsc-gitea/tools/gitea.sh`. Resolve it once. Do not re-resolve it per call, and do not add a tool that prints it.
|
||||
|
||||
### The second root — this skill's own `tools/` and `templates/`
|
||||
|
||||
**Do not reach this skill's own files through `{JSC_ROOT}`, even though a `jsc-cli` link is normally sitting there.** `jsc-cli:deploy` refreshes the whole farm at the end of every round, so on a machine that has deployed, that link exists. This skill still may not lean on it, for two reasons of its own.
|
||||
|
||||
The first is the paragraph above: the machine this skill is called to repair is often the machine whose `JSC_HOME` is unset or wrong, and on it the farm cannot be reached while the repair itself must still run. Route the repair tools through the farm and the one fault they exist to fix becomes the fault that stops them.
|
||||
|
||||
The second is what this skill does with a stale script. Step 3 hands every 落後 domain to `jsc-cli:deploy` precisely because the versions on this machine may be behind — and the farm is governed by that same staleness. Reaching `apply-config.sh` through it would repair the machine with the very tools that machine has just been judged to have outgrown, and the result is written into rc files rather than merely printed.
|
||||
|
||||
They sit at `{plugin root}/tools/` and `{plugin root}/templates/`, and the plugin root is the base directory the CLI states when it loads this skill. Take that literal path verbatim, call it `{CLI_ROOT}`, and write every own-plugin path as `{CLI_ROOT}/tools/{script}` or `{CLI_ROOT}/templates/{file}` — `/root/.claude/plugins/cache/jsc/jsc-cli/0.3.3/tools/apply-config.sh`, for example. No command runs for this one, and it is taken once, like `{JSC_ROOT}`.
|
||||
|
||||
That base directory carries a version segment, so no allow rule covers it and each of those calls raises an approval prompt. **That is acceptable in this skill and in no unattended one**: setup runs with the operator in front of it — step 2 puts every item to them one at a time, and nothing is written before they answer — so there is somebody to approve. Never carry this branch into a skill that runs from a scheduler, and never guess a prefix when the invocation states no base directory: report that this skill's own plugin root is unknown and stop **before writing anything**, because a guessed prefix writes this machine with some other version's script.
|
||||
|
||||
Done when `{JSC_ROOT}` holds one existing absolute directory or its failure is recorded as the `JSC_HOME` item, and `{CLI_ROOT}` holds one literal absolute path.
|
||||
|
||||
## 1. Get the work list
|
||||
|
||||
Read the 待修項目 table from wiki `CHECK_{HASH}` — repo from `jsc-gitea/tools/gitea.sh wiki-repo CHECK`, page name from `gitea.sh hash-id "{host}/{user}"`, where `host` is the **short hostname** and `user` the login account, both taken from the machine:
|
||||
Read the 待修項目 table from wiki `CHECK_{HASH}` — repo from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CHECK`, page name from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh hash-id "{host}/{user}"`, where `host` is the **short hostname** and `user` the login account, both taken from the machine:
|
||||
|
||||
```sh
|
||||
host=$(hostname 2>/dev/null || uname -n 2>/dev/null || printf 'unknown'); host=${host%%.*}
|
||||
@@ -22,11 +58,11 @@ No page, or `wiki-repo` exits 3, or any other non-zero exit from `wiki-repo`, `h
|
||||
|
||||
| Checker | Command | Exit branching |
|
||||
| --- | --- | --- |
|
||||
| Settings | `tools/scan-config.sh scan all` | 0 → use the rows; 2 → usage error, report it as a defect in this skill; 3 → spec table missing, name the path and `JSC_CONFIG_SPEC`; other → report settings as 無法驗證 |
|
||||
| Wiring | `tools/detect-clis.sh`, then `JSC_READONLY=1 jsc-hooks/tools/wire-cli.sh status {cli}` per detected CLI — this step only takes stock, and `wire-cli.sh` without a subcommand rewires, so the read-only contract is carried in the environment rather than trusted to a correctly typed subcommand | detect-clis exit 0 with no row → no CLI to wire, say so and skip; detect-clis non-zero → report wiring as 無法驗證 with the exit code. Per CLI: 0 wired and 1 degraded → nothing to fix; 2 → usage error, defect in this skill; 3 → CLI not installed, drop it; 5 → collect its `missing` items; 6 → readonly refused the call, which means the subcommand was mistyped into a writing one — nothing on the machine changed; fix the command and rerun that CLI; other → report that CLI as 無法驗證 |
|
||||
| Versions | `jsc-hooks/hooks/version-guard.sh report` | 0 → use the rows, and treat a report with no `{domain}` row or a `noregistry` line as 無法驗證 — other exits → report versions as 無法驗證 with the exit code |
|
||||
| Settings | `{CLI_ROOT}/tools/scan-config.sh scan all` | 0 → use the rows; 2 → usage error, report it as a defect in this skill; 3 → spec table missing, name the path and `JSC_CONFIG_SPEC`; other → report settings as 無法驗證 |
|
||||
| Wiring | `{CLI_ROOT}/tools/detect-clis.sh`, then `JSC_READONLY=1 {JSC_ROOT}/jsc-hooks/tools/wire-cli.sh status {cli}` per detected CLI — this step only takes stock, and `wire-cli.sh` without a subcommand rewires, so the read-only contract is carried in the environment rather than trusted to a correctly typed subcommand | detect-clis exit 0 with no row → no CLI to wire, say so and skip; detect-clis non-zero → report wiring as 無法驗證 with the exit code. Per CLI: 0 wired and 1 degraded → nothing to fix; 2 → usage error, defect in this skill; 3 → CLI not installed, drop it; 5 → collect its `missing` items; 6 → readonly refused the call, which means the subcommand was mistyped into a writing one — nothing on the machine changed; fix the command and rerun that CLI; other → report that CLI as 無法驗證 |
|
||||
| Versions | `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh report` | 0 → use the rows, and treat a report with no `{domain}` row or a `noregistry` line as 無法驗證 — other exits → report versions as 無法驗證 with the exit code |
|
||||
|
||||
Merge the three with `tools/build-todo.sh --config {設定輸出} --wiring {cli}={接線輸出} --version {版本輸出}` so the ordering rule lives in one place. Exit 0 → the `todo` rows are the work list; exit 2 → usage error, report it as a defect in this skill; exit 3 → name the unreadable input and rerun that one checker; any other exit → stop and report, because a half-merged list would silently drop a whole class of items.
|
||||
Merge the three with `{CLI_ROOT}/tools/build-todo.sh --config {設定輸出} --wiring {cli}={接線輸出} --version {版本輸出}` so the ordering rule lives in one place. Exit 0 → the `todo` rows are the work list; exit 2 → usage error, report it as a defect in this skill; exit 3 → name the unreadable input and rerun that one checker; any other exit → stop and report, because a half-merged list would silently drop a whole class of items.
|
||||
|
||||
Keep the version report from that run. Step 3 hands it to `jsc-cli:deploy` instead of making it query again.
|
||||
|
||||
@@ -50,8 +86,8 @@ Done when every item is either confirmed with a value or recorded as skipped.
|
||||
|
||||
| Route | Action |
|
||||
| --- | --- |
|
||||
| `auto` on a variable | `tools/apply-config.sh set {KEY} {VALUE}` |
|
||||
| `auto` on a directory | `tools/apply-config.sh mkdir {PATH}` |
|
||||
| `auto` on a variable | `{CLI_ROOT}/tools/apply-config.sh set {KEY} {VALUE}` |
|
||||
| `auto` on a directory | `{CLI_ROOT}/tools/apply-config.sh mkdir {PATH}` |
|
||||
| `ask` | same two commands, with the value the user just gave |
|
||||
| `manual` | print the exact steps and the file to edit; the operator does it |
|
||||
| domain 落後 | call `jsc-cli:deploy` with mode `update` **and the version report from step 1**, so it neither re-asks the mode nor re-queries the versions |
|
||||
@@ -81,8 +117,8 @@ Pick the check by what was actually written, because the two kinds of write beco
|
||||
|
||||
| What was written | Re-verify with | Why this check |
|
||||
| --- | --- | --- |
|
||||
| An environment variable in a shell rc file | `tools/apply-config.sh show`, confirming the `KEY<TAB>VALUE` line is in the `# jsc-config` block | The block is a fact that is already true. The variable reaching the environment is not — a rc file does not touch the running shell |
|
||||
| A directory | `tools/scan-config.sh scan {scope}`, confirming the row is no longer `missing` or `invalid` | The directory exists the moment it is created |
|
||||
| An environment variable in a shell rc file | `{CLI_ROOT}/tools/apply-config.sh show`, confirming the `KEY<TAB>VALUE` line is in the `# jsc-config` block | The block is a fact that is already true. The variable reaching the environment is not — a rc file does not touch the running shell |
|
||||
| A directory | `{CLI_ROOT}/tools/scan-config.sh scan {scope}`, confirming the row is no longer `missing` or `invalid` | The directory exists the moment it is created |
|
||||
| Wiring, versions, model tags (delegated) | The owner skill's own returned result | The owner already ran its own verification |
|
||||
|
||||
The same exit branching as step 1 applies to `scan-config.sh` and to `apply-config.sh`.
|
||||
@@ -97,15 +133,15 @@ Done when every applied item has a fresh verdict from the checker its own row na
|
||||
|
||||
The two pages live in **two different wiki repos**. Resolve each one on its own.
|
||||
|
||||
Rewrite `CHECK_{HASH}` through `jsc-gitea:wiki` with the post-fix state, per `templates/check-page.md` — repo from `gitea.sh wiki-repo CHECK`. That page is a **content page** and keeps only the latest run, so this overwrites the pre-fix picture on purpose.
|
||||
Rewrite `CHECK_{HASH}` through `jsc-gitea:wiki` with the post-fix state, per `{CLI_ROOT}/templates/check-page.md` — repo from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CHECK`. That page is a **content page** and keeps only the latest run, so this overwrites the pre-fix picture on purpose.
|
||||
|
||||
`CHECK_CONTENTS` is a **contents page**, it lives in the contents repo (`gitea.sh wiki-repo CONTENTS`, never a fallback to `JSC_WIKI_REPO_CHECK`), and it gets the opposite treatment. It is an H1, a `>` preamble and one H2 block per machine — no markdown table anywhere on it. Write the block with
|
||||
`CHECK_CONTENTS` is a **contents page**, it lives in the contents repo (`{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CONTENTS`, never a fallback to `JSC_WIKI_REPO_CHECK`), and it gets the opposite treatment. It is an H1, a `>` preamble and one H2 block per machine — no markdown table anywhere on it. Write the block with
|
||||
|
||||
`jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 "CHECK_{HASH}" {block file} templates/check-contents.md`
|
||||
`{JSC_ROOT}/jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 "CHECK_{HASH}" {block file} {CLI_ROOT}/templates/check-contents.md`
|
||||
|
||||
which reads the page back and refreshes this machine's block, or appends it when missing. Never overwrite the whole page, and never touch another machine's block.
|
||||
|
||||
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 block file holds the whole H2 block: the `## CHECK_{HASH}` line, a blank line, then one bullet per field in the order `{CLI_ROOT}/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 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 for `{host}/{user}` in step 1 — 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.
|
||||
|
||||
@@ -113,11 +149,11 @@ A key holding a URL would be a moving key: the URL changes with `GITEA_HOST`, wi
|
||||
|
||||
`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}`, fetched after `CHECK_{HASH}` is rewritten, and is never composed by hand.
|
||||
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 `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-url {CHECK repo} CHECK_{HASH}`, fetched after `CHECK_{HASH}` is rewritten, and is never composed by hand.
|
||||
|
||||
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.
|
||||
**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_ROOT}/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 |
|
||||
| --- | --- | --- |
|
||||
@@ -135,15 +171,15 @@ The script asks the API and never reads a web status code. A private repo's web
|
||||
| --- | --- |
|
||||
| 0 | Report the `updated` or `added` result with the repo and page it named |
|
||||
| 1 | The page content could not be built, or the write failed, and nothing landed. Report it with the stderr. 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 |
|
||||
| 2 | Usage error. Report it as a defect in this skill; do not retry with guessed arguments. A `{CLI_ROOT}/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 report `JSC_WIKI_REPO_CONTENTS` as still unfixed |
|
||||
| 4 | Unreachable the way this skill calls the script — the command above always passes `templates/check-contents.md`, and a template that is not on disk comes back as exit 2. 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 |
|
||||
| 4 | Unreachable the way this skill calls the script — the command above always passes `{CLI_ROOT}/templates/check-contents.md`, and a template that is not on disk comes back as exit 2. 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 or written; name the exit code and create nothing |
|
||||
| 8 | Any other API failure. Same as 7 |
|
||||
|
||||
Exits 7 and 8 never mean the page is missing: the whole-page overwrite that is correct for `CHECK_{HASH}` would here destroy every other machine's block, unread and unrecoverable. The script creates a page only when its own read reported that page absent, and it owns that branch.
|
||||
|
||||
`gitea.sh wiki-url` has its own exits, and they are read before the upsert runs. Exit 4 means `CHECK_{HASH}` is not on the wiki yet, so rewrite that page first and fetch the URL again. Any other non-zero exit: name the exit code and stop — never hand-build the URL, because a guessed link goes into the block and points nowhere.
|
||||
`{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-url` has its own exits, and they are read before the upsert runs. Exit 4 means `CHECK_{HASH}` is not on the wiki yet, so rewrite that page first and fetch the URL again. Any other non-zero exit: name the exit code and stop — never hand-build the URL, because a guessed link goes into the block and points nowhere.
|
||||
|
||||
No wiki repo configured, or any non-zero exit from `wiki-repo`, `hash-id`, `wiki-url` or the wiki write → report the tables on screen, say the record was skipped, and name the exit code.
|
||||
|
||||
@@ -155,7 +191,7 @@ Done when every link written into either page passed `link-check.sh` first — o
|
||||
|
||||
This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 included. Call
|
||||
|
||||
`jsc-hooks/tools/report-status.sh skill-end jsc-cli:setup {status} {exit code} [detail]`
|
||||
`{JSC_ROOT}/jsc-hooks/tools/report-status.sh skill-end jsc-cli:setup {status} {exit code} [detail]`
|
||||
|
||||
`{exit code}` is the exit code of whatever decided the outcome — usually the `apply-config.sh` call that ruled the run — and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters: the four counts fit there, the item table does not, and no value the user typed goes in it. **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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user