--- name: setup description: Fix what jsc-cli:doctor found, one confirmed item at a time. Read the 待修項目 table from wiki CHECK_{HASH}, or rebuild it by running the three checkers in parallel and merging them with tools/build-todo.sh. Route each item by its fix column - auto writes it through tools/apply-config.sh, ask collects the value through the jsc-ask decision tree first, manual prints the steps for the operator. Delegate compound repairs to their owners, handing jsc-cli:deploy the mode and the version report it already has. Re-verify every item after writing and rewrite the CHECK page; use when doctor reports something to fix, not for a read-only checkup. --- # setup — guide or apply the fixes doctor found 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_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%%.*} user=${USER:-$(id -un 2>/dev/null || printf 'unknown')} ``` `${host%%.*}` matters: `hostname` prints the FQDN on some machines, and a hash built on the long name reads a page doctor never wrote. This is the same value doctor hashes, so it must be taken the same way. No page, or `wiki-repo` exits 3, or any other non-zero exit from `wiki-repo`, `hash-id` or the wiki read → rebuild the list here. Rebuilding **MUST run as a sub agent**, and its three checkers **start together**: they read different files and share no state, so serialising them only triples the wait. | Checker | Command | Exit branching | | --- | --- | --- | | 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 `{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. State which source the list came from. A stale page and a live scan can disagree, and the operator has to know which one is on screen. Done when every item carries its class, scope, current state and fix route, and the source of the list is named. ## 2. Confirm each item Ask per the `jsc-ask:ask` decision tree, one item at a time, in the table's order. Confirmation stays strictly sequential: each answer can change what the next item should be, and a batch of questions fired at once takes that away from the operator. Every option states its impact scope: which file gets written, which skills start working, what stays broken when skipped. An `ask` item needs its value in the same question — the wiki repo as `{owner}/{repo}`, the Gitea host, the directory path. Never invent one. Skipping is always an option and is recorded as skipped, not as fixed. Done when every item is either confirmed with a value or recorded as skipped. ## 3. Apply | Route | Action | | --- | --- | | `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 | | hook unwired | call `jsc-hooks:hooks-install` | | `$JSC_HOME/model-tags.tsv` missing | call `jsc-cli:models` | `apply-config.sh` exit codes: | Exit | Action | | --- | --- | | 0 | Written. Record the `wrote` or `created` result and the `backup` path | | 2 | Usage error — the subcommand, the key or the value is wrong. Record the item as 未修好 with that reason, and do not retry with a guessed argument | | 4 | Backup or write failed, so nothing was written. Record the item as 未修好 and name the rc file and the stderr, then say the machine is unchanged | | other | Record the item as 未修好 with the exit code and stderr. Never mark it fixed on an unrecognised exit | `apply-config.sh` writes into the `# jsc-config` block of every existing shell rc file, backs each one up to `$JSC_HOME/backup/config/{timestamp}/` before touching it, and rewrites the block whole. It never edits anything outside that block. Report the `backup` path it prints. That path is the whole undo story for this run. Done when every confirmed item has a `wrote`, `created`, delegated or 未修好 result. ## 4. Re-verify Re-verify every applied item. The items are independent, so **run the re-verifications in parallel** — one batch, one wait. Only the confirmation in step 2 has to stay sequential. Pick the check by what was actually written, because the two kinds of write become true at different moments: | What was written | Re-verify with | Why this check | | --- | --- | --- | | An environment variable in a shell rc file | `{CLI_ROOT}/tools/apply-config.sh show`, confirming the `KEYVALUE` 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`. An item that still fails is reported as 未修好 with the reason. Never mark it fixed because the write succeeded: writing the variable and the variable verifying are two different facts. For every environment variable written, print the matching `export KEY=VALUE` line for the current session and tell the operator to open a new shell or `source` the rc file. The environment-level proof is handed to the next `/jsc-cli:doctor` run, which starts in a fresh shell — asserting it here would read the shell that could not have picked the value up yet, and report a false failure every time. Done when every applied item has a fresh verdict from the checker its own row names, and every environment variable carries its `export` line. ## 5. Record 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 `{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 (`{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_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 `{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. A key holding a URL would be a moving key: the URL changes with `GITEA_HOST`, with a move of `JSC_WIKI_REPO_CHECK` to another repo, and with Gitea's encoding of the page name. The page name moves with none of them — `{host}/{user}` alone decides it. Key on the URL and the comparison never matches, so every run appends a second block for the same machine instead of updating it. `1` is ``, 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 `{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_ROOT}/jsc-gitea/tools/link-check.sh`. It prints `{OK|DEAD|SKIP}{URL}{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 and leave both pages as they are | | 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 report `GITEA_HOST` as still unfixed. 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 | Action | | --- | --- | | 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 `{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 `{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. `{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. Then state the counts: fixed, skipped, delegated, and 未修好. Recommend `/jsc-cli:doctor` for a clean re-check when anything was delegated. 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 written or its skip is reported, and the four counts are stated. ## 6. Record how the run ended 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_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. | status | When this skill uses it | | --- | --- | | `ok` | Every item on the list was confirmed and applied, each one re-verified by the checker its own row names, nothing was skipped, and both pages were written | | `blocked` | The environment refused every write, so nothing on the machine changed: `apply-config.sh` returned 4 on each item because the backup or the write failed. A run that could not touch a single rc file did no work, so it is never reported as `failed` half-done | | `failed` | Writes landed but the run broke: an applied item still fails its re-verification in step 4, `apply-config.sh` returned 2 on a malformed call this skill made, or `link-check.sh`, `gitea.sh` or `wiki-contents.sh` returned 7 or 8 and the record could not be rewritten | | `degraded` | The run finished with part of the list untouched. The usual case is the user turning an item down at step 2 — a skip is recorded as skipped and never as fixed — and a delegated repair that its owner skill did not close counts the same way. The machine is better than it was, and the 未修好 and 略過 counts are above zero | | `aborted` | The user stopped the sequential confirmation partway and asked to end the run, so the remaining items were never put to them | Done when exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it.